X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=lib%2Fsup%2Fmodes%2Fthread-view-mode.rb;h=dfe30ff7c4c49c4d163341aea1cdef87a85416ee;hb=aef0216d7f988ab87a3430c9a65210f0d55dfc64;hp=e974964e8456aa1990225a9a0d38438a1a1f2b6c;hpb=03763d91b333dbcdd36c2fe4557684c8f06e86cb;p=sup diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb index e974964..dfe30ff 100644 --- a/lib/sup/modes/thread-view-mode.rb +++ b/lib/sup/modes/thread-view-mode.rb @@ -1,3 +1,4 @@ +require 'open3' module Redwood class ThreadViewMode < LineCursorMode @@ -23,30 +24,68 @@ Return value: None. The variable 'headers' should be modified in place. EOS + HookManager.register "bounce-command", </ - ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3 + ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => $3 else BufferManager.flash "Can't find List-Subscribe header for this message." end @@ -129,7 +179,7 @@ EOS def unsubscribe_from_list m = @message_lines[curpos] or return if m.list_unsubscribe && m.list_unsubscribe =~ // - ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3 + ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => $3 else BufferManager.flash "Can't find List-Unsubscribe header for this message." end @@ -143,6 +193,38 @@ EOS end end + def bounce + m = @message_lines[curpos] or return + to = BufferManager.ask_for_contacts(:people, "Bounce To: ") or return + + defcmd = AccountManager.default_account.sendmail.sub(/\s(\-(ti|it|t))\b/) do |match| + case "$1" + when '-t' then '' + else ' -i' + end + end + + cmd = case (hookcmd = HookManager.run "bounce-command", :from => m.from, :to => to) + when nil, /^$/ then defcmd + else hookcmd + end + ' ' + to.map { |t| t.email }.join(' ') + + bt = to.size > 1 ? "#{to.size} recipients" : to.to_s + + if BufferManager.ask_yes_or_no "Really bounce to #{bt}?" + debug "bounce command: #{cmd}" + begin + IO.popen(cmd, 'w') do |sm| + sm.puts m.raw_message + end + raise SendmailCommandFailed, "Couldn't execute #{cmd}" unless $? == 0 + rescue SystemCallError, SendmailCommandFailed => e + warn "problem sending mail: #{e.message}" + BufferManager.flash "Problem sending mail: #{e.message}" + end + end + end + include CanAliasContacts def alias p = @person_lines[curpos] or return @@ -160,7 +242,7 @@ EOS def compose p = @person_lines[curpos] if p - ComposeMode.spawn_nicely :to => [p] + ComposeMode.spawn_nicely :to_default => p else ComposeMode.spawn_nicely end @@ -171,7 +253,7 @@ EOS new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels return unless new_labels - @thread.labels = (reserved_labels + new_labels).uniq + @thread.labels = Set.new(reserved_labels) + new_labels new_labels.each { |l| LabelManager << l } update UpdateManager.relay self, :labeled, @thread.first @@ -205,12 +287,16 @@ EOS ## view. def activate_chunk chunk = @chunk_lines[curpos] or return - layout = - if chunk.is_a?(Message) - @layout[chunk] - elsif chunk.expandable? - @chunk_layout[chunk] - end + if chunk.is_a? Chunk::Text + ## if the cursor is over a text region, expand/collapse the + ## entire message + chunk = @message_lines[curpos] + end + 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 @@ -218,11 +304,15 @@ EOS elsif chunk.viewable? view chunk end + if chunk.is_a?(Message) + jump_to_message chunk + jump_to_next_open if layout.state == :closed + end end def edit_as_new m = @message_lines[curpos] or return - mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc) + mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc, :refs => m.refs, :replytos => m.replytos) BufferManager.spawn "edit as new", mode mode.edit_message end @@ -231,7 +321,8 @@ EOS chunk = @chunk_lines[curpos] or return case chunk when Chunk::Attachment - fn = BufferManager.ask_for_filename :filename, "Save attachment to file: ", chunk.filename + default_dir = File.join(($config[:default_attachment_save_dir] || "."), chunk.filename) + fn = BufferManager.ask_for_filename :filename, "Save attachment to file: ", default_dir save_to_file(fn) { |f| f.print chunk.raw_content } if fn else m = @message_lines[curpos] @@ -255,27 +346,46 @@ EOS end end - def jump_to_first_open + def send_draft + m = @message_lines[curpos] or return + if m.is_draft? + mode = ResumeMode.new m + BufferManager.spawn "Send message", mode + BufferManager.kill_buffer self.buffer + mode.send_message + else + BufferManager.flash "Not a draft message!" + end + end + + def jump_to_first_open loose_alignment=false m = @message_lines[0] or return if @layout[m].state != :closed - jump_to_message m + jump_to_message m, loose_alignment else - jump_to_next_open + jump_to_next_open loose_alignment end end - def jump_to_next_open + def jump_to_next_open loose_alignment=false return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations - m = @message_lines[curpos] or return + m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] } + return unless m while nextm = @layout[m].next break if @layout[nextm].state != :closed m = nextm end - jump_to_message nextm if nextm + jump_to_message nextm, loose_alignment if nextm end - def jump_to_prev_open + def align_current_message m = @message_lines[curpos] or return + jump_to_message m + end + + def jump_to_prev_open loose_alignment=false + m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] } # bah, .to_a + return unless m ## jump to the top of the current message if we're in the body; ## otherwise, to the previous message @@ -285,22 +395,35 @@ EOS break if @layout[prevm].state != :closed m = prevm end - jump_to_message prevm if prevm + jump_to_message prevm, loose_alignment if prevm else - jump_to_message m + jump_to_message m, loose_alignment end end - def jump_to_message m + IDEAL_TOP_CONTEXT = 3 # try and give 3 rows of top context + IDEAL_LEFT_CONTEXT = 4 # try and give 4 columns of left context + def jump_to_message m, loose_alignment=false l = @layout[m] left = l.depth * INDENT_SPACES right = left + l.width - ## jump to the top line unless both top and bottom fit in the current view - jump_to_line l.top unless l.top >= topline && l.top <= botline && l.bot >= topline && l.bot <= botline + ## jump to the top line + if loose_alignment + jump_to_line [l.top - IDEAL_TOP_CONTEXT, 0].max # give 3 lines of top context + else + jump_to_line l.top + end + + ## jump to the left column + ideal_left = left + + if loose_alignment + -IDEAL_LEFT_CONTEXT + (l.width - buffer.content_width + IDEAL_LEFT_CONTEXT + 1).clamp(0, IDEAL_LEFT_CONTEXT) + else + 0 + end - ## jump to the left columns unless both left and right fit in the current view - jump_to_col left unless left >= leftcol && left <= rightcol && right >= leftcol && right <= rightcol + jump_to_col [ideal_left, 0].max ## either way, move the cursor to the first line set_cursor_pos l.top @@ -332,16 +455,100 @@ EOS @layout = @chunk_layout = @text = nil # for good luck end - def archive_and_kill - @thread.remove_label :inbox - UpdateManager.relay self, :archived, @thread.first - BufferManager.kill_buffer_safely buffer + def archive_and_kill; archive_and_then :kill end + def spam_and_kill; spam_and_then :kill end + def delete_and_kill; delete_and_then :kill end + def unread_and_kill; unread_and_then :kill end + + def archive_and_next; archive_and_then :next end + def spam_and_next; spam_and_then :next end + def delete_and_next; delete_and_then :next end + def unread_and_next; unread_and_then :next end + def do_nothing_and_next; do_nothing_and_then :next end + + def archive_and_prev; archive_and_then :prev end + def spam_and_prev; spam_and_then :prev end + def delete_and_prev; delete_and_then :prev end + def unread_and_prev; unread_and_then :prev end + def do_nothing_and_prev; do_nothing_and_then :prev end + + def archive_and_then op + dispatch op do + @thread.remove_label :inbox + UpdateManager.relay self, :archived, @thread.first + end + end + + def spam_and_then op + dispatch op do + @thread.apply_label :spam + UpdateManager.relay self, :spammed, @thread.first + end + end + + def delete_and_then op + dispatch op do + @thread.apply_label :deleted + UpdateManager.relay self, :deleted, @thread.first + end + end + + def unread_and_then op + dispatch op do + @thread.apply_label :unread + UpdateManager.relay self, :unread, @thread.first + end + end + + def do_nothing_and_then op + dispatch op + end + + def dispatch op + return if @dying + @dying = true + + l = lambda do + yield if block_given? + BufferManager.kill_buffer_safely buffer + end + + case op + when :next + @index_mode.launch_next_thread_after @thread, &l + when :prev + @index_mode.launch_prev_thread_before @thread, &l + when :kill + l.call + else + raise ArgumentError, "unknown thread dispatch operation #{op.inspect}" + end end + private :dispatch + + def pipe_message + chunk = @chunk_lines[curpos] + chunk = nil unless chunk.is_a?(Chunk::Attachment) + message = @message_lines[curpos] unless chunk + + return unless chunk || message + + command = BufferManager.ask(:shell, "pipe command: ") + return if command.nil? || command.empty? + + output = pipe_to_process(command) do |stream| + if chunk + stream.print chunk.raw_content + else + message.each_raw_message_line { |l| stream.print l } + end + end - def delete_and_kill - @thread.apply_label :deleted - UpdateManager.relay self, :deleted, @thread.first - BufferManager.kill_buffer_safely buffer + if output + BufferManager.spawn "Output of '#{command}'", TextMode.new(output) + else + BufferManager.flash "'#{command}' done!" + end end private @@ -394,7 +601,7 @@ private (0 ... text.length).each do |i| @chunk_lines[@text.length + i] = m @message_lines[@text.length + i] = m - lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum + lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum end @text += text @@ -415,7 +622,7 @@ private (0 ... text.length).each do |i| @chunk_lines[@text.length + i] = c @message_lines[@text.length + i] = m - lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum - (depth * INDENT_SPACES) + lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * INDENT_SPACES) l.width = lw if lw > l.width end @text += text @@ -430,29 +637,29 @@ private open_widget = [color, (state == :closed ? "+ " : "- ")] new_widget = [color, (m.has_label?(:unread) ? "N" : " ")] - starred_widget = - if m.has_label?(:starred) - [star_color, "* "] + starred_widget = if m.has_label?(:starred) + [star_color, "*"] else - [color, " "] + [color, " "] end + attach_widget = [color, (m.has_label?(:attachment) ? "@" : " ")] case state when :open @person_lines[start] = m.from - [[prefix_widget, open_widget, new_widget, starred_widget, + [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget, [color, "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]] when :closed @person_lines[start] = m.from - [[prefix_widget, open_widget, new_widget, starred_widget, + [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget, [color, "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]] when :detailed @person_lines[start] = m.from - from_line = [[prefix_widget, open_widget, new_widget, starred_widget, + from_line = [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget, [color, "From: #{m.from ? format_person(m.from) : '?'}"]]] addressee_lines = [] @@ -489,7 +696,7 @@ private def format_person_list prefix, people ptext = people.map { |p| format_person p } - pad = " " * prefix.length + pad = " " * prefix.display_length [prefix + ptext.first + (ptext.length > 1 ? "," : "")] + ptext[1 .. -1].map_with_index do |e, i| pad + e + (i == ptext.length - 1 ? "" : ",") @@ -497,7 +704,7 @@ private end def format_person p - p.longname + (ContactManager.is_contact?(p) ? " (#{ContactManager.alias_for p})" : "") + p.longname + (ContactManager.is_aliased_contact?(p) ? " (#{ContactManager.alias_for p})" : "") end ## todo: check arguments on this overly complex function @@ -510,7 +717,7 @@ private [[[:missing_message_color, "#{prefix}"]]] 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'. <<<"]]] : []) + (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. Hit 'e' to edit, 'y' to send. <<<"]]] : []) else raise "Bad chunk: #{chunk.inspect}" unless chunk.respond_to?(:inlineable?) ## debugging @@ -535,7 +742,7 @@ private BufferManager.erase_flash BufferManager.completely_redraw_screen unless success - BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s) + BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s, chunk.filename) BufferManager.flash "Couldn't execute view command, viewing as text." end end