lib/sup/buffer.rb in sup-0.2 vs lib/sup/buffer.rb in sup-0.3

- old
+ new

@@ -72,26 +72,30 @@ @height = rows @dirty = true mode.resize rows, cols end - def redraw - draw if @dirty - draw_status + def redraw status + if @dirty + draw status + else + draw_status status + end + commit end def mark_dirty; @dirty = true; end def commit @dirty = false @w.noutrefresh end - def draw + def draw status @mode.draw - draw_status + draw_status status commit end ## s nil means a blank line! def write y, x, s, opts={} @@ -108,13 +112,12 @@ def clear @w.clear end - def draw_status - write @height - 1, 0, " [#{mode.name}] #{title} #{mode.status}", - :color => :status_color + def draw_status status + write @height - 1, 0, status, :color => :status_color end def focus @focus = true @dirty = true @@ -131,10 +134,39 @@ class BufferManager include Singleton attr_reader :focus_buf + ## we have to define the key used to continue in-buffer search here, because + ## it has special semantics that BufferManager deals with---current searches + ## are canceled by any keypress except this one. + CONTINUE_IN_BUFFER_SEARCH_KEY = "n" + + HookManager.register "status-bar-text", <<EOS +Sets the status bar. The default status bar contains the mode name, the buffer +title, and the mode status. Note that this will be called at least once per +keystroke, so excessive computation is discouraged. + +Variables: + num_inbox: number of messages in inbox + num_inbox_unread: total number of messages marked as unread + num_total: total number of messages in the index + num_spam: total number of messages marked as spam + title: title of the current buffer + mode: current mode name (string) + status: current mode status (string) +Return value: a string to be used as the status bar. +EOS + + HookManager.register "terminal-title-text", <<EOS +Sets the title of the current terminal, if applicable. Note that this will be +called at least once per keystroke, so excessive computation is discouraged. + +Variables: the same as status-bar-text hook. +Return value: a string to be used as the terminal title. +EOS + def initialize @name_map = {} @buffers = [] @focus_buf = nil @dirty = true @@ -148,19 +180,20 @@ end def buffers; @name_map.to_a; end def focus_on buf - raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf + return unless @buffers.member? buf + return if buf == @focus_buf @focus_buf.blur if @focus_buf @focus_buf = buf @focus_buf.focus end def raise_to_front buf - raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf + return unless @buffers.member? buf @buffers.delete buf if @buffers.length > 0 && @buffers.last.force_to_top? @buffers.insert(-2, buf) else @@ -188,11 +221,17 @@ @buffers.last.force_to_top = false raise_to_front @buffers[@buffers.length - 2] end def handle_input c - @focus_buf && @focus_buf.mode.handle_input(c) + if @focus_buf + if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY[0] + @focus_buf.mode.cancel_search! + @focus_buf.mark_dirty + end + @focus_buf.mode.handle_input c + end end def exists? n; @name_map.member? n; end def [] n; @name_map[n]; end def []= n, b @@ -202,58 +241,73 @@ end def completely_redraw_screen return if @shelled + status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock + Ncurses.sync do @dirty = true Ncurses.clear - draw_screen :sync => false + draw_screen :sync => false, :status => status, :title => title end end def draw_screen opts={} return if @shelled + status, title = + if opts.member? :status + [opts[:status], opts[:title]] + else + get_status_and_title(@focus_buf) # must be called outside of the ncurses lock + end + + print "\033]2;#{title}\07" if title + Ncurses.mutex.lock unless opts[:sync] == false ## disabling this for the time being, to help with debugging ## (currently we only have one buffer visible at a time). ## TODO: reenable this if we allow multiple buffers false && @buffers.inject(@dirty) do |dirty, buf| buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols #dirty ? buf.draw : buf.redraw - buf.draw + buf.draw status dirty end ## quick hack if true buf = @buffers.last buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols - @dirty ? buf.draw : buf.redraw + @dirty ? buf.draw(status) : buf.redraw(status) end draw_minibuf :sync => false unless opts[:skip_minibuf] @dirty = false Ncurses.doupdate Ncurses.refresh if opts[:refresh] Ncurses.mutex.unlock unless opts[:sync] == false end - ## gets the mode from the block, which is only called if the buffer - ## doesn't already exist. this is useful in the case that generating - ## the mode is expensive, as it often is. + ## if the named buffer already exists, pops it to the front without + ## calling the block. otherwise, gets the mode from the block and + ## creates a new buffer. returns two things: the buffer, and a boolean + ## indicating whether it's a new buffer or not. def spawn_unless_exists title, opts={} - if @name_map.member? title - raise_to_front @name_map[title] unless opts[:hidden] - else - mode = yield - spawn title, mode, opts - end - @name_map[title] + new = + if @name_map.member? title + raise_to_front @name_map[title] unless opts[:hidden] + false + else + mode = yield + spawn title, mode, opts + true + end + [@name_map[title], new] end def spawn title, mode, opts={} raise ArgumentError, "title must be a string" unless title.is_a? String realtitle = title @@ -332,38 +386,49 @@ @focus_buf = nil if @focus_buf == buf if @buffers.empty? ## TODO: something intelligent here ## for now I will simply prohibit killing the inbox buffer. else - raise_to_front @buffers.last + last = @buffers.last + @focus_buf ||= last + raise_to_front last end end def ask_with_completions domain, question, completions, default=nil ask domain, question, default do |s| completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] } end end - def ask_many_with_completions domain, question, completions, default=nil, sep=" " + def ask_many_with_completions domain, question, completions, default=nil ask domain, question, default do |partial| prefix, target = - case partial#.gsub(/#{sep}+/, sep) + case partial when /^\s*$/ ["", ""] - when /^(.+#{sep})$/ - [$1, ""] - when /^(.*#{sep})?(.+?)$/ + when /^(.*\s+)?(.*?)$/ [$1 || "", $2] else raise "william screwed up completion: #{partial.inspect}" end completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] } end end + def ask_many_emails_with_completions domain, question, completions, default=nil + ask domain, question, default do |partial| + prefix, target = partial.split_on_commas_with_remainder + Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}" + target ||= prefix.pop || "" + prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ") + Redwood::log "after: prefix #{prefix.inspect}, target #{target.inspect}" + completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] } + end + end + def ask_for_filename domain, question, default=nil answer = ask domain, question, default do |s| if s =~ /(~([^\s\/]*))/ # twiddle directory expansion full = $1 name = $2.empty? ? Etc.getlogin : $2 @@ -421,17 +486,15 @@ def ask_for_contacts domain, question, default_contacts=[] default = default_contacts.map { |s| s.to_s }.join(" ") default += " " unless default.empty? - recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.longname, c.email] } - contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.longname, c.email] } + recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] } + contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] } - Redwood::log "recent: #{recent.inspect}" - completions = (recent + contacts).flatten.uniq.sort - answer = BufferManager.ask_many_with_completions domain, question, completions, default, /\s*,\s*/ + answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default if answer answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) } end end @@ -452,16 +515,14 @@ ## FUCKING COMPLICATED. Ncurses.sync do tf.activate question, default, &block @dirty = true draw_screen :skip_minibuf => true, :sync => false + tf.position_cursor + Ncurses.refresh end - ret = nil - tf.position_cursor - Ncurses.sync { Ncurses.refresh } - while true c = Ncurses.nonblocking_getch next unless c # getch timeout break unless tf.handle_input c # process keystroke @@ -625,9 +686,33 @@ end @shelled = false end private + def default_status_bar buf + " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}" + end + + def default_terminal_title buf + "Sup #{Redwood::VERSION} :: #{buf.title}" + end + + def get_status_and_title buf + opts = { + :num_inbox => lambda { Index.num_results_for :label => :inbox }, + :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] }, + :num_total => lambda { Index.size }, + :num_spam => lambda { Index.num_results_for :label => :spam }, + :title => buf.title, + :mode => buf.mode.name, + :status => buf.mode.status + } + + statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf) + term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf) + + [statusbar_text, term_title_text] + end def users unless @users @users = [] while(u = Etc.getpwent)