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
181 self.class.i_am_the_instance self
184 def buffers; @name_map.to_a; end
187 return unless @buffers.member? buf
188 return if buf == @focus_buf
189 @focus_buf.blur if @focus_buf
194 def raise_to_front buf
195 @buffers.delete(buf) or return
196 if @buffers.length > 0 && @buffers.last.force_to_top?
197 @buffers.insert(-2, buf)
201 focus_on @buffers.last
205 ## we reset force_to_top when rolling buffers. this is so that the
206 ## human can actually still move buffers around, while still
207 ## programmatically being able to pop stuff up in the middle of
208 ## drawing a window without worrying about covering it up.
210 ## if we ever start calling roll_buffers programmatically, we will
211 ## have to change this. but it's not clear that we will ever actually
214 @buffers.last.force_to_top = false
215 raise_to_front @buffers.first
218 def roll_buffers_backwards
219 return unless @buffers.length > 1
220 @buffers.last.force_to_top = false
221 raise_to_front @buffers[@buffers.length - 2]
226 if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY[0]
227 @focus_buf.mode.cancel_search!
228 @focus_buf.mark_dirty
230 @focus_buf.mode.handle_input c
234 def exists? n; @name_map.member? n; end
235 def [] n; @name_map[n]; end
237 raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
238 raise ArgumentError, "title must be a string" unless n.is_a? String
242 def completely_redraw_screen
245 status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
250 draw_screen :sync => false, :status => status, :title => title
254 def draw_screen opts={}
258 if opts.member? :status
259 [opts[:status], opts[:title]]
261 get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
264 print "\033]2;#{title}\07" if title
266 Ncurses.mutex.lock unless opts[:sync] == false
268 ## disabling this for the time being, to help with debugging
269 ## (currently we only have one buffer visible at a time).
270 ## TODO: reenable this if we allow multiple buffers
271 false && @buffers.inject(@dirty) do |dirty, buf|
272 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
273 #dirty ? buf.draw : buf.redraw
281 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
282 @dirty ? buf.draw(status) : buf.redraw(status)
285 draw_minibuf :sync => false unless opts[:skip_minibuf]
289 Ncurses.refresh if opts[:refresh]
290 Ncurses.mutex.unlock unless opts[:sync] == false
293 ## if the named buffer already exists, pops it to the front without
294 ## calling the block. otherwise, gets the mode from the block and
295 ## creates a new buffer. returns two things: the buffer, and a boolean
296 ## indicating whether it's a new buffer or not.
297 def spawn_unless_exists title, opts={}
299 if @name_map.member? title
300 raise_to_front @name_map[title] unless opts[:hidden]
304 spawn title, mode, opts
307 [@name_map[title], new]
310 def spawn title, mode, opts={}
311 raise ArgumentError, "title must be a string" unless title.is_a? String
314 while @name_map.member? realtitle
315 realtitle = "#{title} <#{num}>"
319 width = opts[:width] || Ncurses.cols
320 height = opts[:height] || Ncurses.rows - 1
322 ## since we are currently only doing multiple full-screen modes,
323 ## use stdscr for each window. once we become more sophisticated,
324 ## we may need to use a new Ncurses::WINDOW
326 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
327 ## (opts[:left] || 0))
329 b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
331 @name_map[realtitle] = b
335 focus_on b unless @focus_buf
342 ## requires the mode to have #done? and #value methods
343 def spawn_modal title, mode, opts={}
344 b = spawn title, mode, opts
348 c = Ncurses.nonblocking_getch
349 next unless c # getch timeout
350 break if c == Ncurses::KEY_CANCEL
353 rescue InputSequenceAborted # do nothing
363 def kill_all_buffers_safely
364 until @buffers.empty?
365 ## inbox mode always claims it's unkillable. we'll ignore it.
366 return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
367 kill_buffer @buffers.last
372 def kill_buffer_safely buf
373 return false unless buf.mode.killable?
379 kill_buffer @buffers.first until @buffers.empty?
383 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
387 @name_map.delete buf.title
388 @focus_buf = nil if @focus_buf == buf
390 ## TODO: something intelligent here
391 ## for now I will simply prohibit killing the inbox buffer.
393 raise_to_front @buffers.last
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) }
502 def ask domain, question, default=nil, &block
503 raise "impossible!" if @asking
506 @textfields[domain] ||= TextField.new
507 tf = @textfields[domain]
510 ## this goddamn ncurses form shit is a fucking 1970's nightmare.
511 ## jesus christ. the exact sequence of ncurses events that needs
512 ## to happen in order to display a form and have the entire screen
513 ## not disappear and have the cursor in the right place can only
514 ## be determined by hours of trial and error and is TOO FUCKING
517 tf.activate Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols, 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
538 draw_screen :skip_minibuf => true
540 elsif tf.roll_completions?
541 completion_buf.mode.roll
542 draw_screen :skip_minibuf => true
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")
605 ## turns an input keystroke into an action symbol. returns the action
606 ## if found, nil if not found, and throws InputSequenceAborted if
607 ## the user aborted a multi-key sequence. (Because each of those cases
608 ## should be handled differently.)
610 ## this is in BufferManager because multi-key sequences require prompting.
611 def resolve_input_with_keymap c, keymap
612 action, text = keymap.action_for c
613 while action.is_a? Keymap # multi-key commands, prompt
614 key = BufferManager.ask_getch text
615 unless key # user canceled, abort
617 raise InputSequenceAborted
619 action, text = action.action_for(key) if action.has_key?(key)
625 @minibuf_mutex.synchronize do
628 @minibuf_stack.compact.size, 1].max
632 def draw_minibuf opts={}
634 @minibuf_mutex.synchronize do
635 m = @minibuf_stack.compact
636 m << @flash if @flash
640 Ncurses.mutex.lock unless opts[:sync] == false
641 Ncurses.attrset Colormap.color_for(:none)
642 adj = @asking ? 2 : 1
643 m.each_with_index do |s, i|
644 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
646 Ncurses.refresh if opts[:refresh]
647 Ncurses.mutex.unlock unless opts[:sync] == false
653 @minibuf_mutex.synchronize do
655 id ||= @minibuf_stack.length
656 @minibuf_stack[id] = s
660 draw_screen :refresh => true
662 draw_minibuf :refresh => true
675 def erase_flash; @flash = nil; end
679 draw_screen :refresh => true
682 ## a little tricky because we can't just delete_at id because ids
683 ## are relative (they're positions into the array).
685 @minibuf_mutex.synchronize do
686 @minibuf_stack[id] = nil
687 if id == @minibuf_stack.length - 1
689 break if @minibuf_stack[i]
690 @minibuf_stack.delete_at i
695 draw_screen :refresh => true
698 def shell_out command
710 def default_status_bar buf
711 " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
714 def default_terminal_title buf
715 "Sup #{Redwood::VERSION} :: #{buf.title}"
718 def get_status_and_title buf
720 :num_inbox => lambda { Index.num_results_for :label => :inbox },
721 :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
722 :num_total => lambda { Index.size },
723 :num_spam => lambda { Index.num_results_for :label => :spam },
725 :mode => buf.mode.name,
726 :status => buf.mode.status
729 statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
730 term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
732 [statusbar_text, term_title_text]
738 while(u = Etc.getpwent)