]> git.cworth.org Git - sup/commitdiff
finally, attachment support\!
authorwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Fri, 6 Jul 2007 14:20:19 +0000 (14:20 +0000)
committerwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Fri, 6 Jul 2007 14:20:19 +0000 (14:20 +0000)
git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@473 5c8cc53c-5e98-4d25-b20a-d8db53a31250

15 files changed:
Manifest.txt
Rakefile
bin/sup
doc/TODO
lib/sup.rb
lib/sup/buffer.rb
lib/sup/index.rb
lib/sup/modes/completion-mode.rb [new file with mode: 0644]
lib/sup/modes/edit-message-mode.rb
lib/sup/modes/file-browser-mode.rb [new file with mode: 0644]
lib/sup/modes/line-cursor-mode.rb
lib/sup/modes/scroll-mode.rb
lib/sup/modes/thread-view-mode.rb
lib/sup/textfield.rb
lib/sup/util.rb

index 440f0bba8cd8501a995593d0ceb3cb59883b09df..fdaebdee058b2dd432160e501b8ac4acdd88dba3 100644 (file)
@@ -34,6 +34,7 @@ lib/sup/mbox/ssh-loader.rb
 lib/sup/message.rb
 lib/sup/mode.rb
 lib/sup/modes/buffer-list-mode.rb
+lib/sup/modes/completion-mode.rb
 lib/sup/modes/compose-mode.rb
 lib/sup/modes/contact-list-mode.rb
 lib/sup/modes/edit-message-mode.rb
@@ -53,6 +54,7 @@ lib/sup/modes/search-results-mode.rb
 lib/sup/modes/text-mode.rb
 lib/sup/modes/thread-index-mode.rb
 lib/sup/modes/thread-view-mode.rb
+lib/sup/modes/file-browser-mode.rb
 lib/sup/person.rb
 lib/sup/poll.rb
 lib/sup/rfc2047.rb
index 180f0afa0cccffafbde5c606f64f42819dfcc49e..41225eb9d781f8c61680aa0339fcff525cac2db2 100644 (file)
--- a/Rakefile
+++ b/Rakefile
@@ -16,7 +16,7 @@ Hoe.new('sup', Redwood::VERSION) do |p|
   p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[2].gsub(/^\s+/, "")
   p.changes = p.paragraphs_of('History.txt', 0..0).join("\n\n")
   p.email = "wmorgan-sup@masanjin.net"
-  p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline', 'net-ssh', ['trollop', '>= 1.7'], 'lockfile']
+  p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline', 'net-ssh', ['trollop', '>= 1.7'], 'lockfile', 'mime-types']
 end
 
 rule 'ss?.png' => 'ss?-small.png' do |t|
diff --git a/bin/sup b/bin/sup
index 83f1e1d8614f4948e99a7eabc4b74e89ef8aceb9..52de3fc7b2dd6435ea2c6d4dd1df6b48dfd5a522 100644 (file)
--- a/bin/sup
+++ b/bin/sup
@@ -119,7 +119,7 @@ begin
     c.add :message_patina_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_GREEN
     c.add :alternate_patina_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_BLUE
     c.add :missing_message_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_RED
-    c.add :mime_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK
+    c.add :attachment_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK
     c.add :quote_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
     c.add :sig_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
     c.add :quote_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
@@ -137,6 +137,8 @@ begin
           Ncurses::A_BOLD
     c.add :draft_notification_color, Ncurses::COLOR_RED, Ncurses::COLOR_BLACK,
           Ncurses::A_BOLD
+    c.add :completion_character_color, Ncurses::COLOR_WHITE,
+          Ncurses::COLOR_BLACK, Ncurses::A_BOLD
   end
 
   log "initializing buffer manager"
@@ -261,14 +263,15 @@ ensure
 
   Redwood::finish
   stop_cursing
+  Redwood::log "stopped cursing"
 
   if SuicideManager.instantiated? && SuicideManager.die?
-    Redwood::log "I've been asked to commit sepuku. I obey!"
+    Redwood::log "I've been ordered to commit sepuku. I obey!"
   end
 
   case $exception
   when nil
-    Redwood::log "good night, sweet prince!"
+    Redwood::log "no fatal errors. good job, william."
     Index.save
   else
     Redwood::log "oh crap, an exception"
index 1ca6d43122ff97796ab5133b7f1e4630c2068143..e84af4eabcf44e88da6d2e5b7ece81afd580619a 100644 (file)
--- a/doc/TODO
+++ b/doc/TODO
@@ -1,5 +1,6 @@
 for 0.0.9
 ---------
+_ bugfix: screwing the headers when editing causes a crash
 _ bugfix: when one new message comes into an imap folder, we don't
    catch it until a reload (sometimes?)
   message indicating they're loaded to inbox (imap only?)
index 6ca881f72e023a180724c9201da247beeb924f7d..953aa5d297d7d0b3b398ef7d389e48c11a29e73c 100644 (file)
@@ -244,6 +244,8 @@ require "sup/modes/inbox-mode"
 require "sup/modes/buffer-list-mode"
 require "sup/modes/log-mode"
 require "sup/modes/poll-mode"
+require "sup/modes/file-browser-mode"
+require "sup/modes/completion-mode"
 require "sup/logger"
 require "sup/sent"
 
index ac7575fd36890828f5f11654e8cbabeac384f163..198d4d190ac742cd7cacd55af5920d30e148edaf 100644 (file)
@@ -1,3 +1,4 @@
+require 'etc'
 require 'thread'
 
 module Ncurses
@@ -28,7 +29,9 @@ module Ncurses
 
   module_function :rows, :cols, :nonblocking_getch, :mutex, :sync
 
-  KEY_CANCEL = "\a"[0] # ctrl-g
+  KEY_ENTER = 10
+  KEY_CANCEL = ?\a # ctrl-g
+  KEY_TAB = 9
 end
 
 module Redwood
@@ -204,19 +207,22 @@ class BufferManager
     ## 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|
+    true && @buffers.inject(@dirty) do |dirty, buf|
       buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
-      @dirty ? buf.draw : buf.redraw
+      #dirty ? buf.draw : buf.redraw
+      buf.draw
+      dirty
     end
 
     ## quick hack
-    if true
+    if false
       buf = @buffers.last
       buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
       @dirty ? buf.draw : buf.redraw
     end
 
     draw_minibuf :sync => false unless opts[:skip_minibuf]
+
     @dirty = false
     Ncurses.doupdate
     Ncurses.refresh if opts[:refresh]
@@ -268,6 +274,24 @@ class BufferManager
     b
   end
 
+  ## requires the mode to have #done? and #value methods
+  def spawn_modal title, mode, opts={}
+    b = spawn title, mode, opts
+    draw_screen
+
+    until mode.done?
+      c = Ncurses.nonblocking_getch
+      next unless c # getch timeout
+      break if c == Ncurses::KEY_CANCEL
+      mode.handle_input c
+      draw_screen
+      erase_flash
+    end
+
+    kill_buffer b
+    mode.value
+  end
+
   def kill_all_buffers_safely
     until @buffers.empty?
       ## inbox mode always claims it's unkillable. we'll ignore it.
@@ -302,20 +326,56 @@ class BufferManager
     end
   end
 
-  ## not really thread safe.
-  def ask domain, question, default=nil
+  ## returns an ARRAY of filenames!
+  def ask_for_filenames 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
+        dir = Etc.getpwnam(name).dir rescue nil
+        if dir
+          [[s.sub(full, dir), "~#{name}"]]
+        else
+          users.select { |u| u =~ /^#{name}/ }.map do |u|
+            [s.sub("~#{name}", "~#{u}"), "~#{u}"]
+          end
+        end
+      else # regular filename completion
+        Dir["#{s}*"].sort.map do |fn|
+          suffix = File.directory?(fn) ? "/" : ""
+          [fn + suffix, File.basename(fn) + suffix]
+        end
+      end
+    end
+
+    if answer
+      if answer.empty?
+        spawn_modal "file browser", FileBrowserMode.new
+      elsif File.directory?(answer)
+        spawn_modal "file browser", FileBrowserMode.new(answer)
+      else
+        [answer]
+      end
+    else
+      []
+    end
+  end
+
+  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
     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.
+    ## 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.
     Ncurses.sync do
-      tf.activate question, default
+      tf.activate question, default, &block
       @dirty = true
       draw_screen :skip_minibuf => true, :sync => false
     end
@@ -324,15 +384,42 @@ class BufferManager
     tf.position_cursor
     Ncurses.sync { Ncurses.refresh }
 
-    @asking = true
-    while tf.handle_input(Ncurses.nonblocking_getch); end
-    @asking = false
+    while true
+      c = Ncurses.nonblocking_getch
+      next unless c  # getch timeout
+      break unless tf.handle_input c # process keystroke
+
+      if tf.new_completions?
+        kill_buffer completion_buf if completion_buf
+        
+        prefix_len =
+          if tf.value =~ /\/$/
+            0
+          else
+            File.basename(tf.value).length
+          end
+
+        mode = CompletionMode.new tf.completions.map { |full, short| short }, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
+        completion_buf = spawn "<completions>", mode, :height => 10
+
+        draw_screen :skip_minibuf => true
+        tf.position_cursor
+      elsif tf.roll_completions?
+        completion_buf.mode.roll
+
+        draw_screen :skip_minibuf => true
+        tf.position_cursor
+      end
 
-    ret = tf.value
+      Ncurses.sync { Ncurses.refresh }
+    end
+    
     Ncurses.sync { tf.deactivate }
+    kill_buffer completion_buf if completion_buf
     @dirty = true
-
-    ret
+    @asking = false
+    draw_screen
+    tf.value
   end
 
   ## some pretty lame code in here!
@@ -467,5 +554,17 @@ class BufferManager
     end
     @shelled = false
   end
+
+private
+
+  def users
+    unless @users
+      @users = []
+      while(u = Etc.getpwent)
+        @users << u.name
+      end
+    end
+    @users
+  end
 end
 end
index 1181e80eafd5bf0f7e512f056eb244ef3e2e385c..c436ffdbe69654fe296d123db1285ef6ba0deee4 100644 (file)
@@ -189,7 +189,7 @@ EOS
   end
 
   def save_index fn=File.join(@dir, "ferret")
-    # don't have to do anything,  apparently
+    # don't have to do anything, apparently
   end
 
   def contains_id? id
diff --git a/lib/sup/modes/completion-mode.rb b/lib/sup/modes/completion-mode.rb
new file mode 100644 (file)
index 0000000..e96b935
--- /dev/null
@@ -0,0 +1,55 @@
+module Redwood
+
+class CompletionMode < ScrollMode
+  INTERSTITIAL = "  "
+
+  def initialize list, opts={}
+    @list = list
+    @header = opts[:header]
+    @prefix_len = opts[:prefix_len]
+    @lines = nil
+    super :slip_rows => 1, :twiddles => false
+  end
+
+  def lines
+    update_lines unless @lines
+    @lines.length
+  end
+
+  def [] i
+    update_lines unless @lines
+    @lines[i]
+  end
+
+  def roll; if at_bottom? then jump_to_start else page_down end end
+
+private
+
+  def update_lines
+    width = buffer.content_width
+    max_length = @list.map { |s| s.length }.max
+    num_per = buffer.content_width / (max_length + INTERSTITIAL.length)
+    @lines = [@header].compact
+    @list.each_with_index do |s, i|
+      if @prefix_len
+        @lines << [] if i % num_per == 0
+        if @prefix_len < s.length
+          prefix = s[0 ... @prefix_len]
+          suffix = s[(@prefix_len + 1) .. -1]
+          char = s[@prefix_len].chr
+
+          @lines.last += [[:none, sprintf("%#{max_length - suffix.length - 1}s", prefix)],
+                          [:completion_character_color, char],
+                          [:none, suffix + INTERSTITIAL]]
+        else
+          @lines.last += [[:none, sprintf("%#{max_length}s#{INTERSTITIAL}", s)]]
+        end
+      else
+        @lines << "" if i % num_per == 0
+        @lines.last += sprintf "%#{max_length}s#{INTERSTITIAL}", s
+      end
+    end
+  end
+end
+
+end
index 67af505c9b9d8be4001bd297a0bc6cd8f2a985dc..5285279dc4ac7c2860c69f243f8cc73a07d6b493 100644 (file)
@@ -1,5 +1,7 @@
 require 'tempfile'
 require 'socket' # just for gethostname!
+require 'pathname'
+require 'rmail'
 
 module Redwood
 
@@ -16,6 +18,8 @@ class EditMessageMode < LineCursorMode
     k.add :send_message, "Send message", 'y'
     k.add :edit, "Edit message", 'e', :enter
     k.add :save_as_draft, "Save as draft", 'P'
+    k.add :attach_file, "Attach a file", 'a'
+    k.add :delete_attachment, "Delete an attachment", 'd'
   end
 
   def initialize opts={}
@@ -23,11 +27,13 @@ class EditMessageMode < LineCursorMode
     @body = opts.delete(:body) || []
     @body += sig_lines if $config[:edit_signature]
     @attachments = []
+    @attachment_lines = {}
     @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
 
     @edited = false
+    @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
     super opts
-    update
+    regen_text
   end
 
   def lines; @text.length end
@@ -59,6 +65,20 @@ class EditMessageMode < LineCursorMode
     !edited? || BufferManager.ask_yes_or_no("Discard message?")
   end
 
+  def attach_file
+    fn = BufferManager.ask_for_filenames :attachment, "File name (enter for browser): "
+    fn.each { |f| @attachments << Pathname.new(f) }
+    update
+  end
+
+  def delete_attachment
+    i = curpos - @top_lines
+    if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachments[i]}?")
+      @attachments.delete_at i
+      update
+    end
+  end
+
 protected
 
   def update
@@ -67,7 +87,10 @@ protected
   end
 
   def regen_text
-    @text = header_lines(@header - NON_EDITABLE_HEADERS) + [""] + @body 
+    top = header_lines(@header - NON_EDITABLE_HEADERS) + [""]
+    @text = top + @body + 
+      @attachments.map { |f| [[:attachment_color, "+ Attachment: #{f} (#{f.human_size})"]] }
+    @top_lines = top.size
     @text += sig_lines unless $config[:edit_signature]
   end
 
@@ -134,7 +157,7 @@ protected
     BufferManager.flash "Sending..."
 
     begin
-      IO.popen(acct.sendmail, "w") { |p| write_message p, true, date }
+      IO.popen(acct.sendmail, "w") { |p| write_full_message_to p }
     rescue SystemCallError
     end
     if $? == 0
@@ -153,6 +176,28 @@ protected
     BufferManager.flash "Saved for later editing."
   end
 
+  def write_full_message_to f
+    m = RMail::Message.new
+    @header.each { |k, v| m.header[k] = v.to_s unless v.to_s.empty? }
+    m.header["Date"] = Time.now.rfc2822
+    m.header["Message-Id"] = @message_id
+    m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
+    if @attachments.empty?
+      m.header["Content-Disposition"] = "inline"
+      m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
+      m.body = @body.join "\n"
+      m.body += sig_lines.join("\n") unless $config[:edit_signature]
+    else
+      body_m = RMail::Message.new
+      body_m.body = @body.join "\n"
+      body_m.body += sig_lines.join("\n") unless $config[:edit_signature]
+      
+      m.add_part body_m
+      @attachments.each { |fn| m.add_attachment fn.to_s }
+    end
+    f.puts m.to_s
+  end
+
   ## this is going to change soon: draft messages (currently written
   ## with full=false) will be output as yaml.
   def write_message f, full=true, date=Time.now
diff --git a/lib/sup/modes/file-browser-mode.rb b/lib/sup/modes/file-browser-mode.rb
new file mode 100644 (file)
index 0000000..d9af957
--- /dev/null
@@ -0,0 +1,108 @@
+require 'pathname'
+
+module Redwood
+
+## meant to be spawned via spawn_modal!
+class FileBrowserMode < LineCursorMode
+  RESERVED_ROWS = 1
+
+  register_keymap do |k|
+    k.add :back, "Go back to previous directory", "B"
+    k.add :view, "View file", "v"
+    k.add :select_file_or_follow_directory, "Select the highlighted file, or follow the directory", :enter
+    k.add :reload, "Reload file list", "R"
+  end
+
+  bool_reader :done
+  attr_reader :value
+
+  def initialize dir="."
+    @dirs = [Pathname.new(dir).realpath]
+    @done = false
+    @value = nil
+    regen_text
+    super :skip_top_rows => RESERVED_ROWS
+  end
+
+  def cwd; @dirs.last end
+  def lines; @text.length; end
+  def [] i; @text[i]; end
+
+protected
+  
+  def back
+    return if @dirs.size == 1
+    @dirs.pop
+    reload
+  end
+
+  def reload
+    regen_text
+    buffer.mark_dirty
+  end
+
+  def view
+     name, f = @files[curpos - RESERVED_ROWS]
+    return unless f && f.file?
+
+    begin
+      BufferManager.spawn f.to_s, TextMode.new(f.readlines.join)
+    rescue SystemCallError => e
+      BufferManager.flash e.message
+    end
+  end
+
+  def select_file_or_follow_directory
+    name, f = @files[curpos - RESERVED_ROWS]
+    return unless f
+
+    if f.directory? && f.to_s != "."
+      if f.readable?
+        @dirs.push f
+        reload
+      else
+        BufferManager.flash "Permission denied - #{f.realpath}"
+      end
+    else
+      begin
+        @value = [f.realpath.to_s]
+        @done = true
+      rescue SystemCallError => e
+        BufferManager.flash e.message
+      end
+    end
+  end
+
+  def regen_text
+    @files = 
+      begin
+        cwd.entries.sort_by do |f|
+          [f.directory? ? 0 : 1, f.basename.to_s]
+        end
+      rescue SystemCallError => e
+        BufferManager.flash "Error: #{e.message}"
+        [Pathname.new("."), Pathname.new("..")]
+      end.map do |f|
+      real_f = cwd + f
+      name = f.basename.to_s +
+        case
+        when real_f.symlink?
+          "@"
+        when real_f.directory?
+          "/"
+        else
+          ""
+        end
+      [name, real_f]
+    end
+
+    size_width = @files.map { |name, f| f.human_size.length }.max
+    time_width = @files.map { |name, f| f.human_time.length }.max
+
+    @text = ["#{cwd}:"] + @files.map do |name, f|
+      sprintf "%#{time_width}s %#{size_width}s %s", f.human_time, f.human_size, name
+    end
+  end
+end
+
+end
index a19fba192e818e0f29dc5566dd9459d39a9bb979..602e721515ab22331f60bf8951608565133917b0 100644 (file)
@@ -13,7 +13,7 @@ class LineCursorMode < ScrollMode
   attr_reader :curpos
 
   def initialize opts={}
-    @cursor_top = @curpos = opts[:skip_top_rows] || 0
+    @cursor_top = @curpos = opts.delete(:skip_top_rows) || 0
     @load_more_callbacks = []
     @load_more_callbacks_m = Mutex.new
     @load_more_callbacks_active = false
@@ -135,7 +135,7 @@ protected
     end
   end
 
-  def jump_to_home
+  def jump_to_start
     super
     set_cursor_pos @cursor_top
   end
index 31ccef0a17728144f40bbc7b6198252a75a6fd1f..513ecc90a030dc16f8a1dffb9633270f7a0e548d 100644 (file)
@@ -21,7 +21,7 @@ class ScrollMode < Mode
     k.add :col_right, "Right one column", :right, 'l'
     k.add :page_down, "Down one page", :page_down, 'n', ' '
     k.add :page_up, "Up one page", :page_up, 'p', :backspace
-    k.add :jump_to_home, "Jump to top", :home, '^', '1'
+    k.add :jump_to_start, "Jump to top", :home, '^', '1'
     k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
     k.add :jump_to_left, "Jump to the left", '['
   end
@@ -76,17 +76,18 @@ class ScrollMode < Mode
     buffer.mark_dirty
   end
 
+  def at_top?; @topline == 0 end
+  def at_bottom?; @botline == lines end
+
   def line_down; jump_to_line @topline + 1; end
   def line_up;  jump_to_line @topline - 1; end
   def page_down; jump_to_line @topline + buffer.content_height - @slip_rows; end
   def page_up; jump_to_line @topline - buffer.content_height + @slip_rows; end
-  def jump_to_home; jump_to_line 0; end
+  def jump_to_start; jump_to_line 0; end
   def jump_to_end; jump_to_line lines - buffer.content_height; end
 
-
   def ensure_mode_validity
-    @topline = @topline.clamp 0, lines - 1
-    @topline = 0 if @topline < 0 # empty 
+    @topline = @topline.clamp 0, [lines - 1, 0].max
     @botline = [@topline + buffer.content_height, lines].min
   end
 
index 255b2bf8cc5cbbeb6b8503b4845f1f1d19828c19..b77f71b43d155b61856c71f3194cc96ec856f456 100644 (file)
@@ -432,7 +432,7 @@ private
       message_patina_lines(chunk, state, start, parent, prefix, color, star_color) +
         (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
     when Message::Attachment
-      [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
+      [[[:attachment_color, "#{prefix}+ Attachment: #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
     when Message::Text
       t = chunk.lines
       if t.last =~ /^\s*$/ && t.length > 1
index ad27c33f57fc1f24b2369f62b1a422cdc38c38b6..bdd1228882e0bc552cd4b855961d1b6864516795 100644 (file)
@@ -2,26 +2,47 @@ require 'curses'
 
 module Redwood
 
+## a fully-functional text field supporting completions, expansions,
+## history--everything!
+##
+## completion is done emacs-style, and mostly depends on outside
+## support, as we merely signal the existence of a new set of
+## completions to show (#new_completions?)  or that the current list
+## of completions should be rolled if they're too large to fill the
+## screen (#roll_completions?).
+##
+## in sup, completion support is implemented through BufferManager#ask
+## and CompletionMode.
 class TextField
-  attr_reader :value
-
   def initialize window, y, x, width
     @w, @x, @y = window, x, y
     @width = width
     @i = nil
     @history = []
+
+    @completion_block = nil
+    reset_completion_state
   end
 
-  def activate question, default=nil
+  bool_reader :new_completions, :roll_completions
+  attr_reader :completions
+
+  ## when the user presses enter, we store the value in @value and
+  ## clean up all the ncurses cruft. before @value is set, we can
+  ## get the current value from ncurses.
+  def value; @field ? get_cur_value : @value end
+
+  def activate question, default=nil, &block
     @question = question
     @value = nil
+    @completion_block = block
     @field = Ncurses::Form.new_field 1, @width - question.length,
                                      @y, @x + question.length, 0, 0
     @form = Ncurses::Form.new_form [@field]
 
     @history[@i = @history.size] = default || ""
     Ncurses::Form.post_form @form
-    @field.set_field_buffer 0, @history[@i]
+    set_cur_value @history[@i]
   end
 
   def position_cursor
@@ -33,24 +54,47 @@ class TextField
   end
 
   def deactivate
+    reset_completion_state
     @form.unpost_form
     @form.free_form
     @field.free_field
+    @field = nil
     Ncurses.curs_set 0
   end
 
   def handle_input c
-    if c == 10 # Ncurses::KEY_ENTER
-      Ncurses::Form.form_driver @form, Ncurses::Form::REQ_VALIDATION
-      @value = @history[@i] = @field.field_buffer(0).gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
+    ## short-circuit exit paths
+    case c
+    when Ncurses::KEY_ENTER # submit!
+      @value = @history[@i] = get_cur_value
       return false
-    elsif c == Ncurses::KEY_CANCEL
+    when Ncurses::KEY_CANCEL # cancel
       @history.delete_at @i
       @i = @history.empty? ? nil : (@i - 1) % @history.size 
       @value = nil
       return false
+    when Ncurses::KEY_TAB # completion
+      break unless @completion_block
+      if @completions.empty?
+        v = get_cur_value
+        c = @completion_block.call v
+        if c.size > 0
+          set_cur_value c.map { |full, short| full }.shared_prefix
+        end
+        if c.size > 1
+          @completions = c
+          @new_completions = true
+          @roll_completions = false
+        end
+      else
+        @new_completions = false
+        @roll_completions = true
+      end
+      return true
     end
 
+    reset_completion_state
+
     d =
       case c
       when Ncurses::KEY_LEFT
@@ -64,21 +108,39 @@ class TextField
       when ?\005
         Ncurses::Form::REQ_END_FIELD
       when Ncurses::KEY_UP
-        @history[@i] = @field.field_buffer(0)
+        @history[@i] = @field.field_buffer 0
         @i = (@i - 1) % @history.size
-        @field.set_field_buffer 0, @history[@i]
+        set_cur_value @history[@i]
       when Ncurses::KEY_DOWN
-        @history[@i] = @field.field_buffer(0)
+        @history[@i] = @field.field_buffer 0
         @i = (@i + 1) % @history.size
-        @field.set_field_buffer 0, @history[@i]
+        set_cur_value @history[@i]
       else
         c
       end
 
-    Ncurses::Form.form_driver @form, d if d
-    Ncurses.refresh
-
+    Ncurses::Form.form_driver @form, d
     true
   end
+
+private
+
+  def reset_completion_state
+    @completions = []
+    @new_completions = @roll_completions = @clear_completions = false
+  end
+
+  ## ncurses inanity wrapper
+  def get_cur_value
+    Ncurses::Form.form_driver @form, Ncurses::Form::REQ_VALIDATION
+    @field.field_buffer(0).gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
+  end
+  
+  ## ncurses inanity wrapper
+  def set_cur_value v
+    @field.set_field_buffer 0, v
+    Ncurses::Form.form_driver @form, Ncurses::Form::REQ_END_FIELD
+  end
+
 end
 end
index c2dd4b02071518c3ba8ba18b3f50709e72748568..2f89a40b218c52b3d9fa362641d1ab5915d1153c 100644 (file)
@@ -1,4 +1,6 @@
 require 'lockfile'
+require 'mime/types'
+require 'pathname'
 
 ## time for some monkeypatching!
 class Lockfile
@@ -27,6 +29,63 @@ class Lockfile
   def touch_yourself; touch path end
 end
 
+class Pathname
+  def human_size
+    s =
+      begin
+        size
+      rescue SystemCallError
+        return "?"
+      end
+
+    if s < 1024
+      s.to_s + "b"
+    elsif s < (1024 * 1024)
+      (s / 1024).to_s + "k"
+    elsif s < (1024 * 1024 * 1024)
+      (s / 1024 / 1024).to_s + "m"
+    else
+      (s / 1024 / 1024 / 1024).to_s + "g"
+    end
+  end
+
+  def human_time
+    begin
+      ctime.strftime("%Y-%m-%d %H:%M")
+    rescue SystemCallError
+      "?"
+    end
+  end
+end
+
+## more monkeypatching!
+module RMail
+  class EncodingUnsupportedError < StandardError; end
+
+  class Message
+    def add_attachment fn
+      bfn = File.basename fn
+      a = Message.new
+      t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
+
+      a.header.add "Content-Disposition", "attachment; filename=#{bfn}"
+      a.header.add "Content-Type", "#{t.content_type}; name=#{bfn}"
+      a.header.add "Content-Transfer-Encoding", t.encoding
+      a.body =
+        case t.encoding
+        when "base64"
+          [IO.read(fn)].pack "m"
+        when "quoted-printable"
+          [IO.read(fn)].pack "M"
+        else
+          raise EncodingUnsupportedError, t.encoding
+        end
+
+      add_part a
+    end
+  end
+end
+
 class Range
   ## only valid for integer ranges (unless I guess it's exclusive)
   def size 
@@ -77,6 +136,8 @@ class Object
   ##
   ## i'm sure there's pithy comment i could make here about the
   ## superiority of lisp, but fuck lisp.
+  ##
+  ## addendum: apparently this is a "k combinator". whoda thunk it?
   def returning x; yield x; x; end
 
   ## clone of java-style whole-method synchronization
@@ -213,6 +274,20 @@ module Enumerable
     end
     best
   end
+
+  ## returns the maximum shared prefix of an array of strings
+  ## optinally excluding a prefix
+  def shared_prefix exclude=""
+    return "" if empty?
+    prefix = ""
+    (0 ... first.length).each do |i|
+      c = first[i]
+      break unless all? { |s| s[i] == c }
+      next if exclude[i] == c
+      prefix += c.chr
+    end
+    prefix
+  end
 end
 
 class Array
@@ -224,6 +299,8 @@ class Array
   def rest; self[1..-1]; end
 
   def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
+
+  def last= e; self[-1] = e end
 end
 
 class Time