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 raise "status must be supplied if draw_screen is called within a sync" if opts[:sync] == false
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
354 rescue InputSequenceAborted # do nothing
364 def kill_all_buffers_safely
365 until @buffers.empty?
366 ## inbox mode always claims it's unkillable. we'll ignore it.
367 return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
368 kill_buffer @buffers.last
373 def kill_buffer_safely buf
374 return false unless buf.mode.killable?
380 kill_buffer @buffers.first until @buffers.empty?
384 raise ArgumentError, "buffer not on stack: #{buf}: #{buf.title.inspect}" unless @buffers.member? buf
388 @name_map.delete buf.title
389 @focus_buf = nil if @focus_buf == buf
391 ## TODO: something intelligent here
392 ## for now I will simply prohibit killing the inbox buffer.
394 raise_to_front @buffers.last
398 def ask_with_completions domain, question, completions, default=nil
399 ask domain, question, default do |s|
400 completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] }
404 def ask_many_with_completions domain, question, completions, default=nil
405 ask domain, question, default do |partial|
410 when /^(.*\s+)?(.*?)$/
413 raise "william screwed up completion: #{partial.inspect}"
416 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
420 def ask_many_emails_with_completions domain, question, completions, default=nil
421 ask domain, question, default do |partial|
422 prefix, target = partial.split_on_commas_with_remainder
423 Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}"
424 target ||= prefix.pop || ""
425 prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
426 Redwood::log "after: prefix #{prefix.inspect}, target #{target.inspect}"
427 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
431 def ask_for_filename domain, question, default=nil
432 answer = ask domain, question, default do |s|
433 if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
435 name = $2.empty? ? Etc.getlogin : $2
436 dir = Etc.getpwnam(name).dir rescue nil
438 [[s.sub(full, dir), "~#{name}"]]
440 users.select { |u| u =~ /^#{name}/ }.map do |u|
441 [s.sub("~#{name}", "~#{u}"), "~#{u}"]
444 else # regular filename completion
445 Dir["#{s}*"].sort.map do |fn|
446 suffix = File.directory?(fn) ? "/" : ""
447 [fn + suffix, File.basename(fn) + suffix]
455 spawn_modal "file browser", FileBrowserMode.new
456 elsif File.directory?(answer)
457 spawn_modal "file browser", FileBrowserMode.new(answer)
466 ## returns an array of labels
467 def ask_for_labels domain, question, default_labels, forbidden_labels=[]
468 default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
469 default = default_labels.join(" ")
470 default += " " unless default.empty?
472 applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
474 answer = ask_many_with_completions domain, question, applyable_labels, default
478 user_labels = answer.split(/\s+/).map { |l| l.intern }
479 user_labels.each do |l|
480 if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
481 BufferManager.flash "'#{l}' is a reserved label!"
488 def ask_for_contacts domain, question, default_contacts=[]
489 default = default_contacts.map { |s| s.to_s }.join(" ")
490 default += " " unless default.empty?
492 recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
493 contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
495 completions = (recent + contacts).flatten.uniq.sort
496 answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
499 answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
503 ## for simplicitly, we always place the question at the very bottom of the
505 def ask domain, question, default=nil, &block
506 raise "impossible!" if @asking
509 @textfields[domain] ||= TextField.new
510 tf = @textfields[domain]
513 status, title = get_status_and_title @focus_buf
516 tf.activate Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols, question, default, &block
517 @dirty = true # for some reason that blanks the whole fucking screen
518 draw_screen :sync => false, :status => status, :title => title
524 c = Ncurses.nonblocking_getch
525 next unless c # getch timeout
526 break unless tf.handle_input c # process keystroke
528 if tf.new_completions?
529 kill_buffer completion_buf if completion_buf
531 shorts = tf.completions.map { |full, short| short }
532 prefix_len = shorts.shared_prefix.length
534 mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
535 completion_buf = spawn "<completions>", mode, :height => 10
537 draw_screen :skip_minibuf => true
539 elsif tf.roll_completions?
540 completion_buf.mode.roll
541 draw_screen :skip_minibuf => true
545 Ncurses.sync { Ncurses.refresh }
548 kill_buffer completion_buf if completion_buf
554 draw_screen :sync => false, :status => status, :title => title
559 def ask_getch question, accept=nil
560 raise "impossible!" if @asking
563 accept = accept.split(//).map { |x| x[0] } if accept
565 status, title = get_status_and_title @focus_buf
567 draw_screen :sync => false, :status => status, :title => title
568 Ncurses.mvaddstr Ncurses.rows - 1, 0, question
569 Ncurses.move Ncurses.rows - 1, question.length + 1
577 key = Ncurses.nonblocking_getch or next
578 if key == Ncurses::KEY_CANCEL
580 elsif accept.nil? || accept.empty? || accept.member?(key)
589 draw_screen :sync => false, :status => status, :title => title
595 ## returns true (y), false (n), or nil (ctrl-g / cancel)
596 def ask_yes_or_no question
597 case(r = ask_getch question, "ynYN")
607 ## turns an input keystroke into an action symbol. returns the action
608 ## if found, nil if not found, and throws InputSequenceAborted if
609 ## the user aborted a multi-key sequence. (Because each of those cases
610 ## should be handled differently.)
612 ## this is in BufferManager because multi-key sequences require prompting.
613 def resolve_input_with_keymap c, keymap
614 action, text = keymap.action_for c
615 while action.is_a? Keymap # multi-key commands, prompt
616 key = BufferManager.ask_getch text
617 unless key # user canceled, abort
619 raise InputSequenceAborted
621 action, text = action.action_for(key) if action.has_key?(key)
627 @minibuf_mutex.synchronize do
630 @minibuf_stack.compact.size, 1].max
634 def draw_minibuf opts={}
636 @minibuf_mutex.synchronize do
637 m = @minibuf_stack.compact
638 m << @flash if @flash
639 m << "" if m.empty? unless @asking # to clear it
642 Ncurses.mutex.lock unless opts[:sync] == false
643 Ncurses.attrset Colormap.color_for(:none)
644 adj = @asking ? 2 : 1
645 m.each_with_index do |s, i|
646 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
648 Ncurses.refresh if opts[:refresh]
649 Ncurses.mutex.unlock unless opts[:sync] == false
655 @minibuf_mutex.synchronize do
657 id ||= @minibuf_stack.length
658 @minibuf_stack[id] = s
662 draw_screen :refresh => true
664 draw_minibuf :refresh => true
677 def erase_flash; @flash = nil; end
681 draw_screen :refresh => true
684 ## a little tricky because we can't just delete_at id because ids
685 ## are relative (they're positions into the array).
687 @minibuf_mutex.synchronize do
688 @minibuf_stack[id] = nil
689 if id == @minibuf_stack.length - 1
691 break if @minibuf_stack[i]
692 @minibuf_stack.delete_at i
697 draw_screen :refresh => true
700 def shell_out command
712 def default_status_bar buf
713 " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
716 def default_terminal_title buf
717 "Sup #{Redwood::VERSION} :: #{buf.title}"
720 def get_status_and_title buf
722 :num_inbox => lambda { Index.num_results_for :label => :inbox },
723 :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
724 :num_total => lambda { Index.size },
725 :num_spam => lambda { Index.num_results_for :label => :spam },
727 :mode => buf.mode.name,
728 :status => buf.mode.status
731 statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
732 term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
734 [statusbar_text, term_title_text]
740 while(u = Etc.getpwent)