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
51 class InputSequenceAborted < StandardError; end
54 attr_reader :mode, :x, :y, :width, :height, :title
56 bool_accessor :force_to_top
58 def initialize window, mode, width, height, opts={}
63 @title = opts[:title] || ""
64 @force_to_top = opts[:force_to_top] || false
65 @x, @y, @width, @height = 0, 0, width, height
68 def content_height; @height - 1; end
69 def content_width; @width; end
72 return if cols == @width && rows == @height
76 mode.resize rows, cols
89 def mark_dirty; @dirty = true; end
102 ## s nil means a blank line!
103 def write y, x, s, opts={}
104 return if x >= @width || y >= @height
106 @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
109 @w.mvaddstr y, x, s[0 ... maxl]
110 unless s.length >= maxl || opts[:no_fill]
111 @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
119 def draw_status status
120 write @height - 1, 0, status, :color => :status_color
139 attr_reader :focus_buf
141 ## we have to define the key used to continue in-buffer search here, because
142 ## it has special semantics that BufferManager deals with---current searches
143 ## are canceled by any keypress except this one.
144 CONTINUE_IN_BUFFER_SEARCH_KEY = "n"
146 HookManager.register "status-bar-text", <<EOS
147 Sets the status bar. The default status bar contains the mode name, the buffer
148 title, and the mode status. Note that this will be called at least once per
149 keystroke, so excessive computation is discouraged.
152 num_inbox: number of messages in inbox
153 num_inbox_unread: total number of messages marked as unread
154 num_total: total number of messages in the index
155 num_spam: total number of messages marked as spam
156 title: title of the current buffer
157 mode: current mode name (string)
158 status: current mode status (string)
159 Return value: a string to be used as the status bar.
162 HookManager.register "terminal-title-text", <<EOS
163 Sets the title of the current terminal, if applicable. Note that this will be
164 called at least once per keystroke, so excessive computation is discouraged.
166 Variables: the same as status-bar-text hook.
167 Return value: a string to be used as the terminal title.
176 @minibuf_mutex = Mutex.new
179 @shelled = @asking = false
180 @in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
182 self.class.i_am_the_instance self
185 def buffers; @name_map.to_a; end
188 return unless @buffers.member? buf
189 return if buf == @focus_buf
190 @focus_buf.blur if @focus_buf
195 def raise_to_front buf
196 @buffers.delete(buf) or return
197 if @buffers.length > 0 && @buffers.last.force_to_top?
198 @buffers.insert(-2, buf)
202 focus_on @buffers.last
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 raise "status must be supplied if draw_screen is called within a sync" if opts[:sync] == false
263 get_status_and_title @focus_buf # must be called outside of the ncurses lock
266 print "\033]2;#{title}\07" if title && @in_x
268 Ncurses.mutex.lock unless opts[:sync] == false
270 ## disabling this for the time being, to help with debugging
271 ## (currently we only have one buffer visible at a time).
272 ## TODO: reenable this if we allow multiple buffers
273 false && @buffers.inject(@dirty) do |dirty, buf|
274 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
275 #dirty ? buf.draw : buf.redraw
283 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
284 @dirty ? buf.draw(status) : buf.redraw(status)
287 draw_minibuf :sync => false unless opts[:skip_minibuf]
291 Ncurses.refresh if opts[:refresh]
292 Ncurses.mutex.unlock unless opts[:sync] == false
295 ## if the named buffer already exists, pops it to the front without
296 ## calling the block. otherwise, gets the mode from the block and
297 ## creates a new buffer. returns two things: the buffer, and a boolean
298 ## indicating whether it's a new buffer or not.
299 def spawn_unless_exists title, opts={}
301 if @name_map.member? title
302 raise_to_front @name_map[title] unless opts[:hidden]
306 spawn title, mode, opts
309 [@name_map[title], new]
312 def spawn title, mode, opts={}
313 raise ArgumentError, "title must be a string" unless title.is_a? String
316 while @name_map.member? realtitle
317 realtitle = "#{title} <#{num}>"
321 width = opts[:width] || Ncurses.cols
322 height = opts[:height] || Ncurses.rows - 1
324 ## since we are currently only doing multiple full-screen modes,
325 ## use stdscr for each window. once we become more sophisticated,
326 ## we may need to use a new Ncurses::WINDOW
328 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
329 ## (opts[:left] || 0))
331 b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
333 @name_map[realtitle] = b
337 focus_on b unless @focus_buf
344 ## requires the mode to have #done? and #value methods
345 def spawn_modal title, mode, opts={}
346 b = spawn title, mode, opts
350 c = Ncurses.nonblocking_getch
351 next unless c # getch timeout
352 break if c == Ncurses::KEY_CANCEL
355 rescue InputSequenceAborted # do nothing
365 def kill_all_buffers_safely
366 until @buffers.empty?
367 ## inbox mode always claims it's unkillable. we'll ignore it.
368 return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
369 kill_buffer @buffers.last
374 def kill_buffer_safely buf
375 return false unless buf.mode.killable?
381 kill_buffer @buffers.first until @buffers.empty?
385 raise ArgumentError, "buffer not on stack: #{buf}: #{buf.title.inspect}" unless @buffers.member? buf
389 @name_map.delete buf.title
390 @focus_buf = nil if @focus_buf == buf
392 ## TODO: something intelligent here
393 ## for now I will simply prohibit killing the inbox buffer.
395 raise_to_front @buffers.last
399 def ask_with_completions domain, question, completions, default=nil
400 ask domain, question, default do |s|
401 completions.select { |x| x =~ /^#{Regexp::escape s}/i }.map { |x| [x, x] }
405 def ask_many_with_completions domain, question, completions, default=nil
406 ask domain, question, default do |partial|
411 when /^(.*\s+)?(.*?)$/
414 raise "william screwed up completion: #{partial.inspect}"
417 completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
421 def ask_many_emails_with_completions domain, question, completions, default=nil
422 ask domain, question, default do |partial|
423 prefix, target = partial.split_on_commas_with_remainder
424 target ||= prefix.pop || ""
425 prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
426 completions.select { |x| x =~ /^#{Regexp::escape 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 =~ /^#{Regexp::escape 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)
457 elsif answer =~ /(~([^\s\/]*))/
459 name = $2.empty? ? Etc.getlogin : $2
460 dir = Etc.getpwnam(name).dir rescue nil
461 answer.sub(full, dir) if dir
470 ## returns an array of labels
471 def ask_for_labels domain, question, default_labels, forbidden_labels=[]
472 default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
473 default = default_labels.join(" ")
474 default += " " unless default.empty?
476 applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
478 answer = ask_many_with_completions domain, question, applyable_labels, default
482 user_labels = answer.split(/\s+/).map { |l| l.intern }
483 user_labels.each do |l|
484 if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
485 BufferManager.flash "'#{l}' is a reserved label!"
492 def ask_for_contacts domain, question, default_contacts=[]
493 default = default_contacts.map { |s| s.to_s }.join(" ")
494 default += " " unless default.empty?
496 recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
497 contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
499 completions = (recent + contacts).flatten.uniq.sort
500 answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
503 answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
507 ## for simplicitly, we always place the question at the very bottom of the
509 def ask domain, question, default=nil, &block
510 raise "impossible!" if @asking
513 @textfields[domain] ||= TextField.new
514 tf = @textfields[domain]
517 status, title = get_status_and_title @focus_buf
520 tf.activate Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols, question, default, &block
521 @dirty = true # for some reason that blanks the whole fucking screen
522 draw_screen :sync => false, :status => status, :title => title
528 c = Ncurses.nonblocking_getch
529 next unless c # getch timeout
530 break unless tf.handle_input c # process keystroke
532 if tf.new_completions?
533 kill_buffer completion_buf if completion_buf
535 shorts = tf.completions.map { |full, short| short }
536 prefix_len = shorts.shared_prefix.length
538 mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
539 completion_buf = spawn "<completions>", mode, :height => 10
541 draw_screen :skip_minibuf => true
543 elsif tf.roll_completions?
544 completion_buf.mode.roll
545 draw_screen :skip_minibuf => true
549 Ncurses.sync { Ncurses.refresh }
552 kill_buffer completion_buf if completion_buf
558 draw_screen :sync => false, :status => status, :title => title
563 def ask_getch question, accept=nil
564 raise "impossible!" if @asking
567 accept = accept.split(//).map { |x| x[0] } if accept
569 status, title = get_status_and_title @focus_buf
571 draw_screen :sync => false, :status => status, :title => title
572 Ncurses.mvaddstr Ncurses.rows - 1, 0, question
573 Ncurses.move Ncurses.rows - 1, question.length + 1
581 key = Ncurses.nonblocking_getch or next
582 if key == Ncurses::KEY_CANCEL
584 elsif accept.nil? || accept.empty? || accept.member?(key)
593 draw_screen :sync => false, :status => status, :title => title
599 ## returns true (y), false (n), or nil (ctrl-g / cancel)
600 def ask_yes_or_no question
601 case(r = ask_getch question, "ynYN")
611 ## turns an input keystroke into an action symbol. returns the action
612 ## if found, nil if not found, and throws InputSequenceAborted if
613 ## the user aborted a multi-key sequence. (Because each of those cases
614 ## should be handled differently.)
616 ## this is in BufferManager because multi-key sequences require prompting.
617 def resolve_input_with_keymap c, keymap
618 action, text = keymap.action_for c
619 while action.is_a? Keymap # multi-key commands, prompt
620 key = BufferManager.ask_getch text
621 unless key # user canceled, abort
623 raise InputSequenceAborted
625 action, text = action.action_for(key) if action.has_key?(key)
631 @minibuf_mutex.synchronize do
634 @minibuf_stack.compact.size, 1].max
638 def draw_minibuf opts={}
640 @minibuf_mutex.synchronize do
641 m = @minibuf_stack.compact
642 m << @flash if @flash
643 m << "" if m.empty? unless @asking # to clear it
646 Ncurses.mutex.lock unless opts[:sync] == false
647 Ncurses.attrset Colormap.color_for(:none)
648 adj = @asking ? 2 : 1
649 m.each_with_index do |s, i|
650 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
652 Ncurses.refresh if opts[:refresh]
653 Ncurses.mutex.unlock unless opts[:sync] == false
659 @minibuf_mutex.synchronize do
661 id ||= @minibuf_stack.length
662 @minibuf_stack[id] = s
666 draw_screen :refresh => true
668 draw_minibuf :refresh => true
681 def erase_flash; @flash = nil; end
685 draw_screen :refresh => true
688 ## a little tricky because we can't just delete_at id because ids
689 ## are relative (they're positions into the array).
691 @minibuf_mutex.synchronize do
692 @minibuf_stack[id] = nil
693 if id == @minibuf_stack.length - 1
695 break if @minibuf_stack[i]
696 @minibuf_stack.delete_at i
701 draw_screen :refresh => true
704 def shell_out command
716 def default_status_bar buf
717 " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
720 def default_terminal_title buf
721 "Sup #{Redwood::VERSION} :: #{buf.title}"
724 def get_status_and_title buf
726 :num_inbox => lambda { Index.num_results_for :label => :inbox },
727 :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
728 :num_total => lambda { Index.size },
729 :num_spam => lambda { Index.num_results_for :label => :spam },
731 :mode => buf.mode.name,
732 :status => buf.mode.status
735 statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
736 term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
738 [statusbar_text, term_title_text]
744 while(u = Etc.getpwent)