]> git.cworth.org Git - sup/blobdiff - lib/sup/buffer.rb
Merge branch 'index-locking' into next
[sup] / lib / sup / buffer.rb
index 21e55ed839310526d5d683bd75fe202238d4d7eb..ebc3587bb44ea547ce2ef665fdf893b10296e23e 100644 (file)
@@ -48,6 +48,8 @@ end
 
 module Redwood
 
+class InputSequenceAborted < StandardError; end
+
 class Buffer
   attr_reader :mode, :x, :y, :width, :height, :title
   bool_reader :dirty
@@ -74,9 +76,13 @@ class Buffer
     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
 
@@ -87,9 +93,9 @@ class Buffer
     @w.noutrefresh
   end
 
-  def draw
+  def draw status
     @mode.draw
-    draw_status
+    draw_status status
     commit
   end
 
@@ -110,9 +116,8 @@ class Buffer
     @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
@@ -138,6 +143,39 @@ class BufferManager
   ## 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
+
+  HookManager.register "extra-contact-addresses", <<EOS
+A list of extra addresses to propose for tab completion, etc. when the
+user is entering an email address. Can be plain email addresses or can
+be full "User Name <email@domain.tld>" entries.
+
+Variables: none
+Return value: an array of email address strings.
+EOS
+
   def initialize
     @name_map = {}
     @buffers = []
@@ -148,6 +186,7 @@ class BufferManager
     @textfields = {}
     @flash = nil
     @shelled = @asking = false
+    @in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
 
     self.class.i_am_the_instance self
   end
@@ -156,7 +195,6 @@ class BufferManager
 
   def focus_on buf
     return unless @buffers.member? buf
-
     return if buf == @focus_buf 
     @focus_buf.blur if @focus_buf
     @focus_buf = buf
@@ -164,15 +202,13 @@ class BufferManager
   end
 
   def raise_to_front buf
-    return unless @buffers.member? buf
-
-    @buffers.delete buf
+    @buffers.delete(buf) or return
     if @buffers.length > 0 && @buffers.last.force_to_top?
       @buffers.insert(-2, buf)
     else
       @buffers.push buf
-      focus_on buf
     end
+    focus_on @buffers.last
     @dirty = true
   end
 
@@ -216,16 +252,29 @@ class BufferManager
   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
+        raise "status must be supplied if draw_screen is called within a sync" if opts[:sync] == false
+        get_status_and_title @focus_buf # must be called outside of the ncurses lock
+      end
+
+    ## http://rtfm.etla.org/xterm/ctlseq.html (see Operating System Controls)
+    print "\033]0;#{title}\07" if title && @in_x
+
     Ncurses.mutex.lock unless opts[:sync] == false
 
     ## disabling this for the time being, to help with debugging
@@ -234,7 +283,7 @@ class BufferManager
     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
 
@@ -242,7 +291,7 @@ class BufferManager
     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]
@@ -253,18 +302,21 @@ class BufferManager
     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]
-      nil
-    else
-      mode = yield
-      spawn title, mode, opts
-      @name_map[title]
-    end
+    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={}
@@ -308,7 +360,10 @@ class BufferManager
       c = Ncurses.nonblocking_getch
       next unless c # getch timeout
       break if c == Ncurses::KEY_CANCEL
-      mode.handle_input c
+      begin
+        mode.handle_input c
+      rescue InputSequenceAborted # do nothing
+      end
       draw_screen
       erase_flash
     end
@@ -337,7 +392,7 @@ class BufferManager
   end
 
   def kill_buffer buf
-    raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
+    raise ArgumentError, "buffer not on stack: #{buf}: #{buf.title.inspect}" unless @buffers.member? buf
 
     buf.mode.cleanup
     @buffers.delete buf
@@ -347,15 +402,13 @@ class BufferManager
       ## TODO: something intelligent here
       ## for now I will simply prohibit killing the inbox buffer.
     else
-      last = @buffers.last
-      @focus_buf ||= last
-      raise_to_front last
+      raise_to_front @buffers.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] }
+      completions.select { |x| x =~ /^#{Regexp::escape s}/i }.map { |x| [x, x] }
     end
   end
 
@@ -371,18 +424,16 @@ class BufferManager
           raise "william screwed up completion: #{partial.inspect}"
         end
 
-      completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
+      completions.select { |x| x =~ /^#{Regexp::escape 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] }
+      completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
     end
   end
 
@@ -395,7 +446,7 @@ class BufferManager
         if dir
           [[s.sub(full, dir), "~#{name}"]]
         else
-          users.select { |u| u =~ /^#{name}/ }.map do |u|
+          users.select { |u| u =~ /^#{Regexp::escape name}/ }.map do |u|
             [s.sub("~#{name}", "~#{u}"), "~#{u}"]
           end
         end
@@ -414,7 +465,7 @@ class BufferManager
         elsif File.directory?(answer)
           spawn_modal "file browser", FileBrowserMode.new(answer)
         else
-          answer
+          File.expand_path answer
         end
     end
 
@@ -451,6 +502,7 @@ class BufferManager
     contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
 
     completions = (recent + contacts).flatten.uniq.sort
+    completions += HookManager.run("extra-contact-addresses") || []
     answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
 
     if answer
@@ -458,30 +510,26 @@ class BufferManager
     end
   end
 
-
+  ## for simplicitly, we always place the question at the very bottom of the
+  ## screen
   def ask domain, question, default=nil, &block
     raise "impossible!" if @asking
     @asking = true
 
-    @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
+    @textfields[domain] ||= TextField.new
     tf = @textfields[domain]
     completion_buf = nil
 
-    ## this goddamn ncurses form shit is a fucking 1970's nightmare.
-    ## jesus christ. the exact sequence of ncurses events that needs
-    ## to happen in order to display a form and have the entire screen
-    ## not disappear and have the cursor in the right place is TOO
-    ## FUCKING COMPLICATED.
+    status, title = get_status_and_title @focus_buf
+
     Ncurses.sync do
-      tf.activate question, default, &block
-      @dirty = true
-      draw_screen :skip_minibuf => true, :sync => false
+      tf.activate Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols, question, default, &block
+      @dirty = true # for some reason that blanks the whole fucking screen
+      draw_screen :sync => false, :status => status, :title => title
+      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
@@ -507,45 +555,48 @@ class BufferManager
       Ncurses.sync { Ncurses.refresh }
     end
     
-    Ncurses.sync { tf.deactivate }
     kill_buffer completion_buf if completion_buf
+
     @dirty = true
     @asking = false
-    draw_screen
+    Ncurses.sync do
+      tf.deactivate
+      draw_screen :sync => false, :status => status, :title => title
+    end
     tf.value
   end
 
-  ## some pretty lame code in here!
   def ask_getch question, accept=nil
+    raise "impossible!" if @asking
+
     accept = accept.split(//).map { |x| x[0] } if accept
 
-    flash question
+    status, title = get_status_and_title @focus_buf
     Ncurses.sync do
-      Ncurses.curs_set 1
+      draw_screen :sync => false, :status => status, :title => title
+      Ncurses.mvaddstr Ncurses.rows - 1, 0, question
       Ncurses.move Ncurses.rows - 1, question.length + 1
+      Ncurses.curs_set 1
       Ncurses.refresh
     end
 
+    @asking = true
     ret = nil
     done = false
-    @shelled = true
     until done
       key = Ncurses.nonblocking_getch or next
       if key == Ncurses::KEY_CANCEL
         done = true
-      elsif (accept && accept.member?(key)) || !accept
+      elsif accept.nil? || accept.empty? || accept.member?(key)
         ret = key
         done = true
       end
     end
 
-    @shelled = false
-
+    @asking = false
     Ncurses.sync do
       Ncurses.curs_set 0
-      erase_flash
-      draw_screen :sync => false
-      Ncurses.curs_set 0
+      draw_screen :sync => false, :status => status, :title => title
     end
 
     ret
@@ -563,6 +614,25 @@ class BufferManager
     end
   end
 
+  ## turns an input keystroke into an action symbol. returns the action
+  ## if found, nil if not found, and throws InputSequenceAborted if
+  ## the user aborted a multi-key sequence. (Because each of those cases
+  ## should be handled differently.)
+  ##
+  ## this is in BufferManager because multi-key sequences require prompting.
+  def resolve_input_with_keymap c, keymap
+    action, text = keymap.action_for c
+    while action.is_a? Keymap # multi-key commands, prompt
+      key = BufferManager.ask_getch text
+      unless key # user canceled, abort
+        erase_flash
+        raise InputSequenceAborted
+      end
+      action, text = action.action_for(key) if action.has_key?(key)
+    end
+    action
+  end
+
   def minibuf_lines
     @minibuf_mutex.synchronize do
       [(@flash ? 1 : 0) + 
@@ -576,7 +646,7 @@ class BufferManager
     @minibuf_mutex.synchronize do
       m = @minibuf_stack.compact
       m << @flash if @flash
-      m << "" if m.empty?
+      m << "" if m.empty? unless @asking # to clear it
     end
 
     Ncurses.mutex.lock unless opts[:sync] == false
@@ -649,6 +719,30 @@ class BufferManager
   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