From f07de32c8629a9523175d31028a869986e873bd2 Mon Sep 17 00:00:00 2001 From: wmorgan Date: Sun, 30 Sep 2007 20:04:58 +0000 Subject: [PATCH] refactor message chunk & chunk layout git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@598 5c8cc53c-5e98-4d25-b20a-d8db53a31250 --- Manifest.txt | 1 + lib/sup.rb | 1 + lib/sup/crypto.rb | 41 ++------ lib/sup/message-chunks.rb | 168 ++++++++++++++++++++++++++++++ lib/sup/message.rb | 108 ++----------------- lib/sup/modes/thread-view-mode.rb | 120 +++++++-------------- 6 files changed, 225 insertions(+), 214 deletions(-) create mode 100644 lib/sup/message-chunks.rb diff --git a/Manifest.txt b/Manifest.txt index 7e1af19..7353416 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -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 diff --git a/lib/sup.rb b/lib/sup.rb index f1bada6..6cab46d 100644 --- a/lib/sup.rb +++ b/lib/sup.rb @@ -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" diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb index 69b9408..148456d 100644 --- a/lib/sup/crypto.rb +++ b/lib/sup/crypto.rb @@ -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 index 0000000..276a61d --- /dev/null +++ b/lib/sup/message-chunks.rb @@ -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", < 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 diff --git a/lib/sup/message.rb b/lib/sup/message.rb index 7583b7a..963805b 100644 --- a/lib/sup/message.rb +++ b/lib/sup/message.rb @@ -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", < 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 diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb index f2ad547..d9c3f8c 100644 --- a/lib/sup/modes/thread-view-mode.rb +++ b/lib/sup/modes/thread-view-mode.rb @@ -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 -- 2.45.2