9 stdscr.getmaxyx lame, lamer
15 stdscr.getmaxyx lame, lamer
21 stdscr.getyx lame, lamer
25 def mutex; @mutex ||= Mutex.new; end
26 def sync &b; mutex.synchronize(&b); end
28 ## magically, this stuff seems to work now. i could swear it didn't
31 if IO.select([$stdin], nil, nil, 1)
38 module_function :rows, :cols, :curx, :nonblocking_getch, :mutex, :sync
40 remove_const :KEY_ENTER
41 remove_const :KEY_CANCEL
44 KEY_CANCEL = 7 # ctrl-g
52 attr_reader :mode, :x, :y, :width, :height, :title
54 bool_accessor :force_to_top
56 def initialize window, mode, width, height, opts={}
61 @title = opts[:title] || ""
62 @force_to_top = opts[:force_to_top] || false
63 @x, @y, @width, @height = 0, 0, width, height
66 def content_height; @height - 1; end
67 def content_width; @width; end
70 return if cols == @width && rows == @height
74 mode.resize rows, cols
87 def mark_dirty; @dirty = true; end
100 ## s nil means a blank line!
101 def write y, x, s, opts={}
102 return if x >= @width || y >= @height
104 @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
107 @w.mvaddstr y, x, s[0 ... maxl]
108 unless s.length >= maxl || opts[:no_fill]
109 @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
117 def draw_status status
118 write @height - 1, 0, status, :color => :status_color
137 attr_reader :focus_buf
139 ## we have to define the key used to continue in-buffer search here, because
140 ## it has special semantics that BufferManager deals with---current searches
141 ## are canceled by any keypress except this one.
142 CONTINUE_IN_BUFFER_SEARCH_KEY = "n"
144 HookManager.register "status-bar-text", <<EOS
145 Sets the status bar. The default status bar contains the mode name, the buffer
146 title, and the mode status. Note that this will be called at least once per
147 keystroke, so excessive computation is discouraged.
150 num_inbox: number of messages in inbox
151 num_inbox_unread: total number of messages marked as unread
152 num_total: total number of messages in the index
153 num_spam: total number of messages marked as spam
154 title: title of the current buffer
155 mode: current mode name (string)
156 status: current mode status (string)
157 Return value: a string to be used as the status bar.
160 HookManager.register "terminal-title-text", <<EOS
161 Sets the title of the current terminal, if applicable. Note that this will be
162 called at least once per keystroke, so excessive computation is discouraged.
164 Variables: the same as status-bar-text hook.
165 Return value: a string to be used as the terminal title.
174 @minibuf_mutex = Mutex.new
177 @shelled = @asking = false
179 self.class.i_am_the_instance self
182 def buffers; @name_map.to_a; end
185 return unless @buffers.member? buf
187 return if buf == @focus_buf
188 @focus_buf.blur if @focus_buf
193 def raise_to_front buf
194 return unless @buffers.member? buf
197 if @buffers.length > 0 && @buffers.last.force_to_top?
198 @buffers.insert(-2, buf)
206 ## we reset force_to_top when rolling buffers. this is so that the
207 ## human can actually still move buffers around, while still
208 ## programmatically being able to pop stuff up in the middle of
209 ## drawing a window without worrying about covering it up.
211 ## if we ever start calling roll_buffers programmatically, we will
212 ## have to change this. but it's not clear that we will ever actually
215 @buffers.last.force_to_top = false
216 raise_to_front @buffers.first
219 def roll_buffers_backwards
220 return unless @buffers.length > 1
221 @buffers.last.force_to_top = false
222 raise_to_front @buffers[@buffers.length - 2]
227 if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY[0]
228 @focus_buf.mode.cancel_search!
229 @focus_buf.mark_dirty
231 @focus_buf.mode.handle_input c
235 def exists? n; @name_map.member? n; end
236 def [] n; @name_map[n]; end
238 raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
239 raise ArgumentError, "title must be a string" unless n.is_a? String
243 def completely_redraw_screen
246 status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
251 draw_screen :sync => false, :status => status, :title => title
255 def draw_screen opts={}
259 if opts.member? :status
260 [opts[:status], opts[:title]]
262 get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
265 print "\033]2;#{title}\07" if title
267 Ncurses.mutex.lock unless opts[:sync] == false
269 ## disabling this for the time being, to help with debugging
270 ## (currently we only have one buffer visible at a time).
271 ## TODO: reenable this if we allow multiple buffers
272 false && @buffers.inject(@dirty) do |dirty, buf|
273 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
274 #dirty ? buf.draw : buf.redraw
282 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
283 @dirty ? buf.draw(status) : buf.redraw(status)
286 draw_minibuf :sync => false unless opts[:skip_minibuf]
290 Ncurses.refresh if opts[:refresh]
291 Ncurses.mutex.unlock unless opts[:sync] == false
294 ## if the named buffer already exists, pops it to the front without
295 ## calling the block. otherwise, gets the mode from the block and
296 ## creates a new buffer. returns two things: the buffer, and a boolean
297 ## indicating whether it's a new buffer or not.
298 def spawn_unless_exists title, opts={}
300 if @name_map.member? title
301 raise_to_front @name_map[title] unless opts[:hidden]
305 spawn title, mode, opts
308 [@name_map[title], new]
311 def spawn title, mode, opts={}
312 raise ArgumentError, "title must be a string" unless title.is_a? String
315 while @name_map.member? realtitle
316 realtitle = "#{title} <#{num}>"
320 width = opts[:width] || Ncurses.cols
321 height = opts[:height] || Ncurses.rows - 1
323 ## since we are currently only doing multiple full-screen modes,
324 ## use stdscr for each window. once we become more sophisticated,
325 ## we may need to use a new Ncurses::WINDOW
327 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
328 ## (opts[:left] || 0))
330 b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
332 @name_map[realtitle] = b
336 focus_on b unless @focus_buf
343 ## requires the mode to have #done? and #value methods
344 def spawn_modal title, mode, opts={}
345 b = spawn title, mode, opts
349 c = Ncurses.nonblocking_getch
350 next unless c # getch timeout
351 break if c == Ncurses::KEY_CANCEL
361 def kill_all_buffers_safely
362 until @buffers.empty?
363 ## inbox mode always claims it's unkillable. we'll ignore it.
364 return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
365 kill_buffer @buffers.last
370 def kill_buffer_safely buf
371 return false unless buf.mode.killable?
377 kill_buffer @buffers.first until @buffers.empty?
381 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
385 @name_map.delete buf.title
386 @focus_buf = nil if @focus_buf == buf
388 ## TODO: something intelligent here
389 ## for now I will simply prohibit killing the inbox buffer.
397 def ask_with_completions domain, question, completions, default=nil
398 ask domain, question, default do |s|
399 completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] }
403 def ask_many_with_completions domain, question, completions, default=nil
404 ask domain, question, default do |partial|
409 when /^(.*\s+)?(.*?)$/
412 raise "william screwed up completion: #{partial.inspect}"
415 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
419 def ask_many_emails_with_completions domain, question, completions, default=nil
420 ask domain, question, default do |partial|
421 prefix, target = partial.split_on_commas_with_remainder
422 Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}"
423 target ||= prefix.pop || ""
424 prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
425 Redwood::log "after: prefix #{prefix.inspect}, target #{target.inspect}"
426 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
430 def ask_for_filename domain, question, default=nil
431 answer = ask domain, question, default do |s|
432 if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
434 name = $2.empty? ? Etc.getlogin : $2
435 dir = Etc.getpwnam(name).dir rescue nil
437 [[s.sub(full, dir), "~#{name}"]]
439 users.select { |u| u =~ /^#{name}/ }.map do |u|
440 [s.sub("~#{name}", "~#{u}"), "~#{u}"]
443 else # regular filename completion
444 Dir["#{s}*"].sort.map do |fn|
445 suffix = File.directory?(fn) ? "/" : ""
446 [fn + suffix, File.basename(fn) + suffix]
454 spawn_modal "file browser", FileBrowserMode.new
455 elsif File.directory?(answer)
456 spawn_modal "file browser", FileBrowserMode.new(answer)
465 ## returns an array of labels
466 def ask_for_labels domain, question, default_labels, forbidden_labels=[]
467 default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
468 default = default_labels.join(" ")
469 default += " " unless default.empty?
471 applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
473 answer = ask_many_with_completions domain, question, applyable_labels, default
477 user_labels = answer.split(/\s+/).map { |l| l.intern }
478 user_labels.each do |l|
479 if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
480 BufferManager.flash "'#{l}' is a reserved label!"
487 def ask_for_contacts domain, question, default_contacts=[]
488 default = default_contacts.map { |s| s.to_s }.join(" ")
489 default += " " unless default.empty?
491 recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
492 contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
494 completions = (recent + contacts).flatten.uniq.sort
495 answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
498 answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
503 def ask domain, question, default=nil, &block
504 raise "impossible!" if @asking
507 @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
508 tf = @textfields[domain]
511 ## this goddamn ncurses form shit is a fucking 1970's nightmare.
512 ## jesus christ. the exact sequence of ncurses events that needs
513 ## to happen in order to display a form and have the entire screen
514 ## not disappear and have the cursor in the right place is TOO
515 ## FUCKING COMPLICATED.
517 tf.activate question, default, &block
519 draw_screen :skip_minibuf => true, :sync => false
525 c = Ncurses.nonblocking_getch
526 next unless c # getch timeout
527 break unless tf.handle_input c # process keystroke
529 if tf.new_completions?
530 kill_buffer completion_buf if completion_buf
532 shorts = tf.completions.map { |full, short| short }
533 prefix_len = shorts.shared_prefix.length
535 mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
536 completion_buf = spawn "<completions>", mode, :height => 10
540 elsif tf.roll_completions?
541 completion_buf.mode.roll
546 Ncurses.sync { Ncurses.refresh }
549 Ncurses.sync { tf.deactivate }
550 kill_buffer completion_buf if completion_buf
557 ## some pretty lame code in here!
558 def ask_getch question, accept=nil
559 accept = accept.split(//).map { |x| x[0] } if accept
564 Ncurses.move Ncurses.rows - 1, question.length + 1
572 key = Ncurses.nonblocking_getch or next
573 if key == Ncurses::KEY_CANCEL
575 elsif (accept && accept.member?(key)) || !accept
586 draw_screen :sync => false
593 ## returns true (y), false (n), or nil (ctrl-g / cancel)
594 def ask_yes_or_no question
595 case(r = ask_getch question, "ynYN")
606 @minibuf_mutex.synchronize do
609 @minibuf_stack.compact.size, 1].max
613 def draw_minibuf opts={}
615 @minibuf_mutex.synchronize do
616 m = @minibuf_stack.compact
617 m << @flash if @flash
621 Ncurses.mutex.lock unless opts[:sync] == false
622 Ncurses.attrset Colormap.color_for(:none)
623 adj = @asking ? 2 : 1
624 m.each_with_index do |s, i|
625 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
627 Ncurses.refresh if opts[:refresh]
628 Ncurses.mutex.unlock unless opts[:sync] == false
634 @minibuf_mutex.synchronize do
636 id ||= @minibuf_stack.length
637 @minibuf_stack[id] = s
641 draw_screen :refresh => true
643 draw_minibuf :refresh => true
656 def erase_flash; @flash = nil; end
660 draw_screen :refresh => true
663 ## a little tricky because we can't just delete_at id because ids
664 ## are relative (they're positions into the array).
666 @minibuf_mutex.synchronize do
667 @minibuf_stack[id] = nil
668 if id == @minibuf_stack.length - 1
670 break if @minibuf_stack[i]
671 @minibuf_stack.delete_at i
676 draw_screen :refresh => true
679 def shell_out command
691 def default_status_bar buf
692 " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
695 def default_terminal_title buf
696 "Sup #{Redwood::VERSION} :: #{buf.title}"
699 def get_status_and_title buf
701 :num_inbox => lambda { Index.num_results_for :label => :inbox },
702 :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
703 :num_total => lambda { Index.size },
704 :num_spam => lambda { Index.num_results_for :label => :spam },
706 :mode => buf.mode.name,
707 :status => buf.mode.status
710 statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
711 term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
713 [statusbar_text, term_title_text]
719 while(u = Etc.getpwent)