]> git.cworth.org Git - sup/commitdiff
refactor message chunk & chunk layout
authorwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Sun, 30 Sep 2007 20:04:58 +0000 (20:04 +0000)
committerwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Sun, 30 Sep 2007 20:04:58 +0000 (20:04 +0000)
git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@598 5c8cc53c-5e98-4d25-b20a-d8db53a31250

Manifest.txt
lib/sup.rb
lib/sup/crypto.rb
lib/sup/message-chunks.rb [new file with mode: 0644]
lib/sup/message.rb
lib/sup/modes/thread-view-mode.rb

index 7e1af191357404337a28113525ec327c79d01df3..7353416c89b0f971fc95ed380dab60ab1685602c 100644 (file)
@@ -34,6 +34,7 @@ lib/sup/mbox/loader.rb
 lib/sup/mbox/ssh-file.rb
 lib/sup/mbox/ssh-loader.rb
 lib/sup/message.rb
+lib/sup/message-chunks.rb
 lib/sup/mode.rb
 lib/sup/modes/buffer-list-mode.rb
 lib/sup/modes/completion-mode.rb
index f1bada6b062a33bd0d1184fcdd33908a5edd6d8a..6cab46d5b41b75a14f867b84c2f5ffb53a7e6616 100644 (file)
@@ -218,6 +218,7 @@ Redwood::HookManager.new Redwood::HOOK_DIR
 
 require "sup/update"
 require "sup/suicide"
+require "sup/message-chunks"
 require "sup/message"
 require "sup/source"
 require "sup/mbox"
index 69b940816ee65c1f8110d5afec01c4448607f3e3..148456d62f59e4269be3683a1290e642b37c7e0d 100644 (file)
@@ -1,25 +1,5 @@
 module Redwood
 
-class CryptoSignature
-  attr_reader :lines, :status, :description
-
-  def initialize status, description, lines
-    @status = status
-    @description = description
-    @lines = lines
-  end
-end
-
-class CryptoDecryptedNotice
-  attr_reader :lines, :status, :description
-
-  def initialize status, description, lines=[]
-    @status = status
-    @description = description
-    @lines = lines
-  end
-end
-
 class CryptoManager
   include Singleton
 
@@ -60,9 +40,9 @@ class CryptoManager
 
     if gpg_output =~ /^gpg: (.* signature from .*$)/
       if $? == 0
-        CryptoSignature.new :valid, $1, output_lines
+        Chunk::CryptoNotice.new :valid, $1, output_lines
       else
-        CryptoSignature.new :invalid, $1, output_lines
+        Chunk::CryptoNotice.new :invalid, $1, output_lines
       end
     else
       unknown_status output_lines
@@ -103,16 +83,16 @@ class CryptoManager
       sig = 
         if sig_lines # encrypted & signed
           if sig_lines =~ /^gpg: (Good signature from .*$)/
-            CryptoSignature.new :valid, $1, sig_lines.split("\n")
+            Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
           else
-            CryptoSignature.new :invalid, $1, sig_lines.split("\n")
+            Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
           end
         end
 
-      notice = CryptoDecryptedNotice.new :valid, "This message has been decrypted for display"
+      notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
       [RMail::Parser.read(decrypted_payload), sig, notice]
     else
-      notice = CryptoDecryptedNotice.new :invalid, "This message could not be decrypted", gpg_output.split("\n")
+      notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", gpg_output.split("\n")
       [nil, nil, notice]
     end
   end
@@ -120,7 +100,7 @@ class CryptoManager
 private
 
   def unknown_status lines=[]
-    CryptoSignature.new :unknown, "Unable to determine validity of cryptographic signature", lines
+    Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
   end
   
   def cant_find_binary
@@ -128,10 +108,3 @@ private
   end
 end
 end
-
-
-## to check:
-## failed decryption
-## decryption but failed signature
-## no gpg found
-## multiple private keys
diff --git a/lib/sup/message-chunks.rb b/lib/sup/message-chunks.rb
new file mode 100644 (file)
index 0000000..276a61d
--- /dev/null
@@ -0,0 +1,168 @@
+## Here we define all the "chunks" that a message is parsed
+## into. Chunks are used by ThreadViewMode to render a message. Chunks
+## are used for both MIME stuff like attachments, for Sup's parsing of
+## the message body into text, quote, and signature regions, and for
+## notices like "this message was decrypted" or "this message contains
+## a valid signature"---basically, anything we want to differentiate
+## at display time.
+
+## A chunk can be inlineable, expandable, or viewable. If it's
+## inlineable, #color and #lines are called and the output is treated
+## as part of the message text. This is how Text and one-line Quotes
+## and Signatures work.
+
+## If it's expandable, #patina_color and #patina_text are called to
+## generate a "patina" (a one-line widget, basically), and the user
+## can press enter to toggle the display of the chunk content, which
+## is generated from #color and #lines). This is how Quote, Signature,
+## and most widgets work.
+
+## If it's viewable, a patina is displayed using #patina_color and
+## #patina_text, but no toggling is allowed. Instead, if #view! is
+## defined, pressing enter on the widget calls view! and (if that
+## returns false) #to_s. Otherwise enter does nothing. This is how
+## non-inlineable attachments work.
+
+module Redwood
+module Chunk
+  class Attachment
+    HookManager.register "mime-decode", <<EOS
+Executes when decoding a MIME attachment.
+Variables:
+   content_type: the content-type of the message
+       filename: the filename of the attachment as saved to disk (generated
+                 on the fly, so don't call more than once)
+  sibling_types: if this attachment is part of a multipart MIME attachment,
+                 an array of content-types for all attachments. Otherwise,
+                 the empty array.
+Return value:
+  The decoded text of the attachment, or nil if not decoded.
+EOS
+#' stupid ruby-mode
+
+    ## raw_content is the post-MIME-decode content. this is used for
+    ## saving the attachment to disk.
+    attr_reader :content_type, :filename, :lines, :raw_content
+
+    def initialize content_type, filename, encoded_content, sibling_types
+      @content_type = content_type
+      @filename = filename
+      @raw_content = encoded_content.decode
+
+      @lines =
+        case @content_type
+        when /^text\/plain\b/
+          Message.convert_from(@raw_content, encoded_content.charset).split("\n")
+        else
+          text = HookManager.run "mime-decode", :content_type => content_type,
+                                 :filename => lambda { write_to_disk },
+                                 :sibling_types => sibling_types
+          text.split("\n") if text
+        end
+    end
+
+    def color; :none end
+    def patina_color; :attachment_color end
+    def patina_text
+      if expandable?
+        "Attachment: #{filename} (#{lines.length} lines)"
+      else
+        "Attachment: #{filename} (#{content_type})"
+      end
+    end
+
+    ## an attachment is exapndable if we've managed to decode it into
+    ## something we can display inline. otherwise, it's viewable.
+    def inlineable?; false end
+    def expandable?; !viewable? end
+    def viewable?; @lines.nil? end
+    def view!
+      file = Tempfile.new "redwood.attachment"
+      file.print @raw_content
+      file.close
+      system "/usr/bin/run-mailcap --action=view #{@content_type}:#{file.path} >& /dev/null"
+      $? == 0
+    end
+    
+    ## used when viewing the attachment as text
+    def to_s
+      @lines || @raw_content
+    end
+  end
+
+  class Text
+    WRAP_LEN = 80 # wrap at this width
+
+    attr_reader :lines
+    def initialize lines
+      @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten # wrap
+
+      ## trim off all empty lines except one
+      lines.pop while lines.last =~ /^\s*$/ 
+    end
+
+    def color; :none end
+    def inlineable?; true end
+  end
+
+  class Quote
+    attr_reader :lines
+    def initialize lines
+      @lines = lines
+    end
+    
+    def inlineable?; @lines.length == 1 end
+    def expandable?; !inlineable? end
+    def patina_color; :quote_patina_color end
+    def patina_text; "(#{lines.length} quoted lines)" end
+    def color; :quote_color end
+  end
+
+  class Signature
+    attr_reader :lines
+    def initialize lines
+      @lines = lines
+    end
+
+    def inlineable?; @lines.length == 1 end
+    def expandable?; !inlineable? end
+    def patina_color; :sig_patina_color end
+    def patina_text; "(#{lines.length}-line signature)" end
+    def color; :sig_color end
+  end
+
+  class EnclosedMessageNotice
+    attr_reader :from
+    def initialize from
+      @from = from
+    end
+
+    def to_s
+      "Begin enclosed message from #{@from.longname}"
+    end
+  end
+
+  class CryptoNotice
+    attr_reader :lines, :status, :patina_text
+
+    def initialize status, description, lines=[]
+      @status = status
+      @patina_text = description
+      @lines = lines
+    end
+
+    def patina_color
+      case status
+      when :valid: :cryptosig_valid_color
+      when :invalid: :cryptosig_invalid_color
+      else :cryptosig_unknown_color
+      end
+    end
+    def color; patina_color end
+
+    def inlineable?; false end
+    def expandable?; !@lines.empty? end
+    def viewable?; false end
+  end
+end
+end
index 7583b7a5d5760f03a7945c5b8abf45e928332fdc..963805b9893202a364ebfa5d453075ccf082f473 100644 (file)
@@ -15,23 +15,8 @@ class MessageFormatError < StandardError; end
 ## sequences in the text of an email. (how sweet would that be?)
 class Message
   SNIPPET_LEN = 80
-  WRAP_LEN = 80 # wrap at this width
   RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i
 
-  HookManager.register "mime-decode", <<EOS
-Executes when decoding a MIME attachment.
-Variables:
-   content_type: the content-type of the message
-       filename: the filename of the attachment as saved to disk (generated
-                 on the fly, so don't call more than once)
-  sibling_types: if this attachment is part of a multipart MIME attachment,
-                 an array of content-types for all attachments. Otherwise,
-                 the empty array.
-Return value:
-  The decoded text of the attachment, or nil if not decoded.
-EOS
-#' stupid ruby-mode
-
   ## some utility methods
   class << self
     def normalize_subj s; s.gsub(RE_PATTERN, ""); end
@@ -39,79 +24,6 @@ EOS
     def reify_subj s; subj_is_reply?(s) ? s : "Re: " + s; end
   end
 
-  class Attachment
-    ## encoded_content is still possible MIME-encoded
-    ##
-    ## raw_content is after decoding but before being turned into
-    ## inlineable text.
-    ##
-    ## lines is array of inlineable text.
-
-    attr_reader :content_type, :filename, :lines, :raw_content
-
-    def initialize content_type, filename, encoded_content, sibling_types
-      @content_type = content_type
-      @filename = filename
-      @raw_content = encoded_content.decode
-
-      @lines = 
-        case @content_type
-        when /^text\/plain\b/
-          Message.convert_from(@raw_content, encoded_content.charset).split("\n")
-        else
-          text = HookManager.run "mime-decode", :content_type => content_type,
-                                 :filename => lambda { write_to_disk },
-                                 :sibling_types => sibling_types
-          text.split("\n") if text
-          
-        end
-    end
-
-    def inlineable?; !@lines.nil? end
-
-    def view!
-      path = write_to_disk
-      system "/usr/bin/run-mailcap --action=view #{@content_type}:#{path} >& /dev/null"
-      $? == 0
-    end
-    
-    ## used when viewing the attachment as text
-    def to_s
-      @lines || @raw_content
-    end
-
-  private
-
-    def write_to_disk
-      file = Tempfile.new "redwood.attachment"
-      file.print @raw_content
-      file.close
-      file.path
-    end
-  end
-
-  class Text
-    attr_reader :lines
-    def initialize lines
-      ## do some wrapping
-      @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten
-    end
-  end
-
-  class Quote
-    attr_reader :lines
-    def initialize lines
-      @lines = lines
-    end
-  end
-
-  class Signature
-    attr_reader :lines
-    def initialize lines
-      @lines = lines
-    end
-  end
-
   QUOTE_PATTERN = /^\s{0,4}[>|\}]/
   BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
   QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
@@ -225,7 +137,7 @@ EOS
   def load_from_source!
     @chunks ||=
       if @source.has_errors?
-        [Text.new(error_message(@source.error.message.split("\n")))]
+        [Chunk::Text.new(error_message(@source.error.message.split("\n")))]
       else
         begin
           ## we need to re-read the header because it contains information
@@ -243,7 +155,7 @@ EOS
           ## we need force_to_top here otherwise this window will cover
           ## up the error message one
           Redwood::report_broken_sources :force_to_top => true
-          [Text.new(error_message(e.message))]
+          [Chunk::Text.new(error_message(e.message))]
         end
       end
   end
@@ -296,13 +208,13 @@ EOS
       to.map { |p| "#{p.name} #{p.email}" },
       cc.map { |p| "#{p.name} #{p.email}" },
       bcc.map { |p| "#{p.name} #{p.email}" },
-      chunks.select { |c| c.is_a? Text }.map { |c| c.lines },
+      chunks.select { |c| c.is_a? Chunk::Text }.map { |c| c.lines },
       Message.normalize_subj(subj),
     ].flatten.compact.join " "
   end
 
   def basic_body_lines
-    chunks.find_all { |c| c.is_a?(Text) || c.is_a?(Quote) }.map { |c| c.lines }.flatten
+    chunks.find_all { |c| c.is_a?(Chunk::Text) || c.is_a?(Chunk::Quote) }.map { |c| c.lines }.flatten
   end
 
   def basic_header_lines
@@ -427,7 +339,7 @@ private
 
       ## if there's a filename, we'll treat it as an attachment.
       if filename
-        [Attachment.new(m.header.content_type, filename, m, sibling_types)]
+        [Chunk::Attachment.new(m.header.content_type, filename, m, sibling_types)]
 
       ## otherwise, it's body text
       else
@@ -473,7 +385,7 @@ private
         end
 
         if newstate
-          chunks << Text.new(chunk_lines) unless chunk_lines.empty?
+          chunks << Chunk::Text.new(chunk_lines) unless chunk_lines.empty?
           chunk_lines = [line]
           state = newstate
         else
@@ -495,7 +407,7 @@ private
           if chunk_lines.empty?
             # nothing
           else
-            chunks << Quote.new(chunk_lines)
+            chunks << Chunk::Quote.new(chunk_lines)
           end
           chunk_lines = [line]
           state = newstate
@@ -515,11 +427,11 @@ private
     ## final object
     case state
     when :quote, :block_quote
-      chunks << Quote.new(chunk_lines) unless chunk_lines.empty?
+      chunks << Chunk::Quote.new(chunk_lines) unless chunk_lines.empty?
     when :text
-      chunks << Text.new(chunk_lines) unless chunk_lines.empty?
+      chunks << Chunk::Text.new(chunk_lines) unless chunk_lines.empty?
     when :sig
-      chunks << Signature.new(chunk_lines) unless chunk_lines.empty?
+      chunks << Chunk::Signature.new(chunk_lines) unless chunk_lines.empty?
     end
     chunks
   end
index f2ad5474ce18216f878817d59fa77c13fb943ff8..d9c3f8c07d669e8dd3a7f524c08615517f6eb8bb 100644 (file)
@@ -16,7 +16,7 @@ class ThreadViewMode < LineCursorMode
   register_keymap do |k|
     k.add :toggle_detailed_header, "Toggle detailed header", 'h'
     k.add :show_header, "Show full message header", 'H'
-    k.add :toggle_expanded, "Expand/collapse item", :enter
+    k.add :activate_chunk, "Expand/collapse or activate item", :enter
     k.add :expand_all_messages, "Expand/collapse all messages", 'E'
     k.add :edit_draft, "Edit draft", 'e'
     k.add :edit_labels, "Edit or add labels for a thread", 'l'
@@ -172,31 +172,25 @@ class ThreadViewMode < LineCursorMode
     UpdateManager.relay self, :label, m
   end
 
-  ## a little overly complicated. for quotes and signatures, if it's
-  ## one line, we just display it and don't allow for
-  ## collapsing/expanding. for crypto notices, we allow
-  ## expanding/collapsing iff the # of notice lines is > 0.
-  def toggle_expanded
+  ## called when someone presses enter when the cursor is highlighting
+  ## a chunk. for expandable chunks (including messages) we toggle
+  ## open/closed state; for viewable chunks (like attachments) we
+  ## view.
+  def activate_chunk
     chunk = @chunk_lines[curpos] or return
-    case chunk
-    when Message
-      l = @layout[chunk]
-      l.state = (l.state != :closed ? :closed : :open)
-      cursor_down if l.state == :closed
-    when CryptoSignature, CryptoDecryptedNotice
-      return if chunk.lines.empty?
-      toggle_chunk_expansion chunk
-    when Message::Quote, Message::Signature
-      return if chunk.lines.length <= 1
-      toggle_chunk_expansion chunk
-    when Message::Attachment
-      if chunk.inlineable?
-        toggle_chunk_expansion chunk
-      else
-        view_attachment chunk
+    layout = 
+      if chunk.is_a?(Message)
+        @layout[chunk]
+      elsif chunk.expandable?
+        @chunk_layout[chunk]
       end
+    if layout
+      layout.state = (layout.state != :closed ? :closed : :open)
+      #cursor_down if layout.state == :closed # too annoying
+      update
+    elsif chunk.viewable?
+      view chunk
     end
-    update
   end
 
   def edit_as_new
@@ -209,7 +203,7 @@ class ThreadViewMode < LineCursorMode
   def save_to_disk
     chunk = @chunk_lines[curpos] or return
     case chunk
-    when Message::Attachment
+    when Chunk::Attachment
       fn = BufferManager.ask_for_filename :filename, "Save attachment to file: ", chunk.filename
       save_to_file(fn) { |f| f.print chunk.raw_content } if fn
     else
@@ -298,7 +292,7 @@ class ThreadViewMode < LineCursorMode
 
   def expand_all_quotes
     if(m = @message_lines[curpos])
-      quotes = m.chunks.select { |c| (c.is_a?(Message::Quote) || c.is_a?(Message::Signature)) && c.lines.length > 1 }
+      quotes = m.chunks.select { |c| (c.is_a?(Chunk::Quote) || c.is_a?(Chunk::Signature)) && c.lines.length > 1 }
       numopen = quotes.inject(0) { |s, c| s + (@chunk_layout[c].state == :open ? 1 : 0) }
       newstate = numopen > quotes.length / 2 ? :closed : :open
       quotes.each { |c| @chunk_layout[c].state = newstate }
@@ -324,12 +318,6 @@ class ThreadViewMode < LineCursorMode
 
 private
 
-  def toggle_chunk_expansion chunk
-    l = @chunk_layout[chunk]
-    l.state = (l.state != :closed ? :closed : :open)
-    cursor_down if l.state == :closed
-  end
-
   def initial_state_for m
     if m.has_label?(:starred) || m.has_label?(:unread)
       :open
@@ -389,7 +377,7 @@ private
 
           ## set the default state for chunks
           cl.state ||=
-            if c.is_a?(Message::Attachment) && c.inlineable?
+            if c.is_a?(Chunk::Attachment) && c.expandable?
               :open
             else
               :closed
@@ -477,6 +465,7 @@ private
     p.longname + (ContactManager.is_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
   end
 
+  ## todo: check arguments on this overly complex function
   def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
     prefix = " " * INDENT_SPACES * depth
     case chunk
@@ -487,63 +476,30 @@ private
     when Message
       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
-      return [[[:attachment_color, "#{prefix}x Attachment: #{chunk.filename} (#{chunk.content_type})"]]] unless chunk.inlineable?
-      case state
-      when :closed
-        [[[:attachment_color, "#{prefix}+ Attachment: #{chunk.filename} (#{chunk.lines.length} lines)"]]]
-      when :open
-        [[[:attachment_color, "#{prefix}- Attachment: #{chunk.filename} (#{chunk.lines.length} lines)"]]] + chunk.lines.map { |line| [[:none, "#{prefix}#{line}"]] }
-      end
-    when Message::Text
-      t = chunk.lines
-      if t.last =~ /^\s*$/ && t.length > 1
-        t.pop while t[-2] =~ /^\s*$/ # pop until only one file empty line
-      end
-      t.map { |line| [[:none, "#{prefix}#{line}"]] }
-    when Message::Quote
-      return [[[:quote_color, "#{prefix}#{chunk.lines.first}"]]] if chunk.lines.length == 1
-      case state
-      when :closed
-        [[[:quote_patina_color, "#{prefix}+ (#{chunk.lines.length} quoted lines)"]]]
-      when :open
-        [[[:quote_patina_color, "#{prefix}- (#{chunk.lines.length} quoted lines)"]]] + chunk.lines.map { |line| [[:quote_color, "#{prefix}#{line}"]] }
-      end
-    when Message::Signature
-      return [[[:sig_patina_color, "#{prefix}#{chunk.lines.first}"]]] if chunk.lines.length == 1
-      case state
-      when :closed
-        [[[:sig_patina_color, "#{prefix}+ (#{chunk.lines.length}-line signature)"]]]
-      when :open
-        [[[:sig_patina_color, "#{prefix}- (#{chunk.lines.length}-line signature)"]]] + chunk.lines.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
-      end
-    when CryptoSignature, CryptoDecryptedNotice
-      color = 
-        case chunk.status
-          when :valid: :cryptosig_valid_color
-          when :invalid: :cryptosig_invalid_color
-          else :cryptosig_unknown_color
-        end
-      widget = chunk.lines.empty? ? "x" : (state == :closed ? "+" : "-")
-      case state
-      when :closed
-        [[[color, "#{prefix}#{widget} #{chunk.description}"]]] 
-      when :open
-        [[[color, "#{prefix}#{widget} #{chunk.description}"]]] +
-          chunk.lines.map { |line| [[color, "#{prefix}#{line}"]] }
-        end
+
     else
-      raise "unknown chunk type #{chunk.class.name}"
+      if chunk.inlineable?
+        chunk.lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
+      elsif chunk.expandable?
+        case state
+        when :closed
+          [[[chunk.patina_color, "#{prefix}+ #{chunk.patina_text}"]]]
+        when :open
+          [[[chunk.patina_color, "#{prefix}- #{chunk.patina_text}"]]] + chunk.lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
+        end
+      else
+        [[[chunk.patina_color, "#{prefix}x #{chunk.patina_text}"]]]
+      end
     end
   end
 
-  def view_attachment a
-    BufferManager.flash "viewing #{a.content_type} attachment..."
-    success = a.view!
+  def view chunk
+    BufferManager.flash "viewing #{chunk.content_type} attachment..."
+    success = chunk.view!
     BufferManager.erase_flash
     BufferManager.completely_redraw_screen
     unless success
-      BufferManager.spawn "Attachment: #{a.filename}", TextMode.new(a.to_s)
+      BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s)
       BufferManager.flash "Couldn't execute view command, viewing as text."
     end
   end