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
267 ## http://rtfm.etla.org/xterm/ctlseq.html (see Operating System Controls)
268 print "\033]2;#{title}\07" # window
269 print "\033]0;#{title}\07" # icon title
272 Ncurses.mutex.lock unless opts[:sync] == false
274 ## disabling this for the time being, to help with debugging
275 ## (currently we only have one buffer visible at a time).
276 ## TODO: reenable this if we allow multiple buffers
277 false && @buffers.inject(@dirty) do |dirty, buf|
278 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
279 #dirty ? buf.draw : buf.redraw
287 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
288 @dirty ? buf.draw(status) : buf.redraw(status)
291 draw_minibuf :sync => false unless opts[:skip_minibuf]
295 Ncurses.refresh if opts[:refresh]
296 Ncurses.mutex.unlock unless opts[:sync] == false
299 ## if the named buffer already exists, pops it to the front without
300 ## calling the block. otherwise, gets the mode from the block and
301 ## creates a new buffer. returns two things: the buffer, and a boolean
302 ## indicating whether it's a new buffer or not.
303 def spawn_unless_exists title, opts={}
305 if @name_map.member? title
306 raise_to_front @name_map[title] unless opts[:hidden]
310 spawn title, mode, opts
313 [@name_map[title], new]
316 def spawn title, mode, opts={}
317 raise ArgumentError, "title must be a string" unless title.is_a? String
320 while @name_map.member? realtitle
321 realtitle = "#{title} <#{num}>"
325 width = opts[:width] || Ncurses.cols
326 height = opts[:height] || Ncurses.rows - 1
328 ## since we are currently only doing multiple full-screen modes,
329 ## use stdscr for each window. once we become more sophisticated,
330 ## we may need to use a new Ncurses::WINDOW
332 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
333 ## (opts[:left] || 0))
335 b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
337 @name_map[realtitle] = b
341 focus_on b unless @focus_buf
348 ## requires the mode to have #done? and #value methods
349 def spawn_modal title, mode, opts={}
350 b = spawn title, mode, opts
354 c = Ncurses.nonblocking_getch
355 next unless c # getch timeout
356 break if c == Ncurses::KEY_CANCEL
359 rescue InputSequenceAborted # do nothing
369 def kill_all_buffers_safely
370 until @buffers.empty?
371 ## inbox mode always claims it's unkillable. we'll ignore it.
372 return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
373 kill_buffer @buffers.last
378 def kill_buffer_safely buf
379 return false unless buf.mode.killable?
385 kill_buffer @buffers.first until @buffers.empty?
389 raise ArgumentError, "buffer not on stack: #{buf}: #{buf.title.inspect}" unless @buffers.member? buf
393 @name_map.delete buf.title
394 @focus_buf = nil if @focus_buf == buf
396 ## TODO: something intelligent here
397 ## for now I will simply prohibit killing the inbox buffer.
399 raise_to_front @buffers.last
403 def ask_with_completions domain, question, completions, default=nil
404 ask domain, question, default do |s|
405 completions.select { |x| x =~ /^#{Regexp::escape s}/i }.map { |x| [x, x] }
409 def ask_many_with_completions domain, question, completions, default=nil
410 ask domain, question, default do |partial|
415 when /^(.*\s+)?(.*?)$/
418 raise "william screwed up completion: #{partial.inspect}"
421 completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
425 def ask_many_emails_with_completions domain, question, completions, default=nil
426 ask domain, question, default do |partial|
427 prefix, target = partial.split_on_commas_with_remainder
428 target ||= prefix.pop || ""
429 prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
430 completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
434 def ask_for_filename domain, question, default=nil
435 answer = ask domain, question, default do |s|
436 if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
438 name = $2.empty? ? Etc.getlogin : $2
439 dir = Etc.getpwnam(name).dir rescue nil
441 [[s.sub(full, dir), "~#{name}"]]
443 users.select { |u| u =~ /^#{Regexp::escape name}/ }.map do |u|
444 [s.sub("~#{name}", "~#{u}"), "~#{u}"]
447 else # regular filename completion
448 Dir["#{s}*"].sort.map do |fn|
449 suffix = File.directory?(fn) ? "/" : ""
450 [fn + suffix, File.basename(fn) + suffix]
458 spawn_modal "file browser", FileBrowserMode.new
459 elsif File.directory?(answer)
460 spawn_modal "file browser", FileBrowserMode.new(answer)
462 File.expand_path answer
469 ## returns an array of labels
470 def ask_for_labels domain, question, default_labels, forbidden_labels=[]
471 default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
472 default = default_labels.join(" ")
473 default += " " unless default.empty?
475 applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
477 answer = ask_many_with_completions domain, question, applyable_labels, default
481 user_labels = answer.split(/\s+/).map { |l| l.intern }
482 user_labels.each do |l|
483 if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
484 BufferManager.flash "'#{l}' is a reserved label!"
491 def ask_for_contacts domain, question, default_contacts=[]
492 default = default_contacts.map { |s| s.to_s }.join(" ")
493 default += " " unless default.empty?
495 recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
496 contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
498 completions = (recent + contacts).flatten.uniq.sort
499 answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
502 answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
506 ## for simplicitly, we always place the question at the very bottom of the
508 def ask domain, question, default=nil, &block
509 raise "impossible!" if @asking
512 @textfields[domain] ||= TextField.new
513 tf = @textfields[domain]
516 status, title = get_status_and_title @focus_buf
519 tf.activate Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols, question, default, &block
520 @dirty = true # for some reason that blanks the whole fucking screen
521 draw_screen :sync => false, :status => status, :title => title
527 c = Ncurses.nonblocking_getch
528 next unless c # getch timeout
529 break unless tf.handle_input c # process keystroke
531 if tf.new_completions?
532 kill_buffer completion_buf if completion_buf
534 shorts = tf.completions.map { |full, short| short }
535 prefix_len = shorts.shared_prefix.length
537 mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
538 completion_buf = spawn "<completions>", mode, :height => 10
540 draw_screen :skip_minibuf => true
542 elsif tf.roll_completions?
543 completion_buf.mode.roll
544 draw_screen :skip_minibuf => true
548 Ncurses.sync { Ncurses.refresh }
551 kill_buffer completion_buf if completion_buf
557 draw_screen :sync => false, :status => status, :title => title
562 def ask_getch question, accept=nil
563 raise "impossible!" if @asking
565 accept = accept.split(//).map { |x| x[0] } if accept
567 status, title = get_status_and_title @focus_buf
569 draw_screen :sync => false, :status => status, :title => title
570 Ncurses.mvaddstr Ncurses.rows - 1, 0, question
571 Ncurses.move Ncurses.rows - 1, question.length + 1
580 key = Ncurses.nonblocking_getch or next
581 if key == Ncurses::KEY_CANCEL
583 elsif accept.nil? || accept.empty? || accept.member?(key)
592 draw_screen :sync => false, :status => status, :title => title
598 ## returns true (y), false (n), or nil (ctrl-g / cancel)
599 def ask_yes_or_no question
600 case(r = ask_getch question, "ynYN")
610 ## turns an input keystroke into an action symbol. returns the action
611 ## if found, nil if not found, and throws InputSequenceAborted if
612 ## the user aborted a multi-key sequence. (Because each of those cases
613 ## should be handled differently.)
615 ## this is in BufferManager because multi-key sequences require prompting.
616 def resolve_input_with_keymap c, keymap
617 action, text = keymap.action_for c
618 while action.is_a? Keymap # multi-key commands, prompt
619 key = BufferManager.ask_getch text
620 unless key # user canceled, abort
622 raise InputSequenceAborted
624 action, text = action.action_for(key) if action.has_key?(key)
630 @minibuf_mutex.synchronize do
633 @minibuf_stack.compact.size, 1].max
637 def draw_minibuf opts={}
639 @minibuf_mutex.synchronize do
640 m = @minibuf_stack.compact
641 m << @flash if @flash
642 m << "" if m.empty? unless @asking # to clear it
645 Ncurses.mutex.lock unless opts[:sync] == false
646 Ncurses.attrset Colormap.color_for(:none)
647 adj = @asking ? 2 : 1
648 m.each_with_index do |s, i|
649 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
651 Ncurses.refresh if opts[:refresh]
652 Ncurses.mutex.unlock unless opts[:sync] == false
658 @minibuf_mutex.synchronize do
660 id ||= @minibuf_stack.length
661 @minibuf_stack[id] = s
665 draw_screen :refresh => true
667 draw_minibuf :refresh => true
680 def erase_flash; @flash = nil; end
684 draw_screen :refresh => true
687 ## a little tricky because we can't just delete_at id because ids
688 ## are relative (they're positions into the array).
690 @minibuf_mutex.synchronize do
691 @minibuf_stack[id] = nil
692 if id == @minibuf_stack.length - 1
694 break if @minibuf_stack[i]
695 @minibuf_stack.delete_at i
700 draw_screen :refresh => true
703 def shell_out command
715 def default_status_bar buf
716 " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
719 def default_terminal_title buf
720 "Sup #{Redwood::VERSION} :: #{buf.title}"
723 def get_status_and_title buf
725 :num_inbox => lambda { Index.num_results_for :label => :inbox },
726 :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
727 :num_total => lambda { Index.size },
728 :num_spam => lambda { Index.num_results_for :label => :spam },
730 :mode => buf.mode.name,
731 :status => buf.mode.status
734 statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
735 term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
737 [statusbar_text, term_title_text]
743 while(u = Etc.getpwent)