class Redwood::ThreadViewMode

Public Class Methods

new(thread, hidden_labels=[], index_mode=nil) click to toggle source

there are a couple important instance variables we hold to format the thread and to provide line-based functionality. @layout is a map from Messages to MessageLayouts, and @chunk_layout from Chunks to ChunkLayouts. @message_lines is a map from row s to Message objects. @chunk_lines is a map from row s to Chunk objects. @person_lines is a map from row s to Person objects.

Calls superclass method Redwood::LineCursorMode::new
# File lib/sup/modes/thread_view_mode.rb, line 128
def initialize thread, hidden_labels=[], index_mode=nil
  @indent_spaces = $config[:indent_spaces]
  super :slip_rows => $config[:slip_rows]
  @thread = thread
  @hidden_labels = hidden_labels

  ## used for dispatch-and-next
  @index_mode = index_mode
  @dying = false

  @layout = SavingHash.new { MessageLayout.new }
  @chunk_layout = SavingHash.new { ChunkLayout.new }
  earliest, latest = nil, nil
  latest_date = nil
  altcolor = false

  @thread.each do |m, d, p|
    next unless m
    earliest ||= m
    @layout[m].state = initial_state_for m
    @layout[m].toggled_state = false
    @layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
    @layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color
    @layout[m].orig_new = m.has_label? :read
    altcolor = !altcolor
    if latest_date.nil? || m.date > latest_date
      latest_date = m.date
      latest = m
    end
  end

  @wrap = true

  @layout[latest].state = :open if @layout[latest].state == :closed
  @layout[earliest].state = :detailed if earliest.has_label?(:unread) || @thread.size == 1
end

Public Instance Methods

[](i;) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 179
def [] i; @text[i]; end
activate_chunk() click to toggle source

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.

# File lib/sup/modes/thread_view_mode.rb, line 358
def activate_chunk
  chunk = @chunk_lines[curpos] or return
  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
    update
  elsif chunk.viewable?
    view chunk
  end
  if chunk.is_a?(Message) && $config[:jump_to_open_message]
    jump_to_message chunk
    jump_to_next_open if layout.state == :closed
  end
end
alias() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 291
def alias
  p = @person_lines[curpos] or return
  alias_contact p
  update
end
align_current_message() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 519
def align_current_message
  m = @message_lines[curpos] or return
  jump_to_message m, true
end
archive_and_kill() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 613
def archive_and_kill; archive_and_then :kill end
archive_and_next() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 620
def archive_and_next; archive_and_then :next end
archive_and_prev() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 627
def archive_and_prev; archive_and_then :prev end
archive_and_then(op) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 634
def archive_and_then op
  dispatch op do
    @thread.remove_label :inbox
    UpdateManager.relay self, :archived, @thread.first
    Index.save_thread @thread
    UndoManager.register "archiving 1 thread" do
      @thread.apply_label :inbox
      Index.save_thread @thread
      UpdateManager.relay self, :unarchived, @thread.first
    end
  end
end
bounce() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 261
def bounce
  m = @message_lines[curpos] or return
  to = BufferManager.ask_for_contacts(:people, "Bounce To: ") or return

  defcmd = AccountManager.default_account.bounce_sendmail

  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[0].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
      m.add_label :forwarded
      Index.save_message m
    rescue SystemCallError, SendmailCommandFailed => e
      warn "problem sending mail: #{e.message}"
      BufferManager.flash "Problem sending mail: #{e.message}"
    end
  end
end
buffer=(b) click to toggle source

a little hacky—since regen_text can depend on buffer features like the content_width, we don't call it in the constructor, and instead call it here, which is set before we're responsible for drawing ourself.

Calls superclass method
# File lib/sup/modes/thread_view_mode.rb, line 184
def buffer= b
  super
  regen_text
end
cleanup() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 609
def cleanup
  @layout = @chunk_layout = @text = nil # for good luck
end
collapse_non_new_messages() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 594
def collapse_non_new_messages
  @layout.each { |m, l| l.state = l.orig_new ? :open : :closed }
  update
end
compose() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 304
def compose
  p = @person_lines[curpos]
  if p
    ComposeMode.spawn_nicely :to_default => p
  else
    ComposeMode.spawn_nicely
  end
end
delete_and_kill() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 615
def delete_and_kill; delete_and_then :kill end
delete_and_next() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 622
def delete_and_next; delete_and_then :next end
delete_and_prev() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 629
def delete_and_prev; delete_and_then :prev end
delete_and_then(op) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 660
def delete_and_then op
  dispatch op do
    @thread.apply_label :deleted
    UpdateManager.relay self, :deleted, @thread.first
    Index.save_thread @thread
    UndoManager.register "deleting 1 thread" do
      @thread.remove_label :deleted
      Index.save_thread @thread
      UpdateManager.relay self, :undeleted, @thread.first
    end
  end
end
do_nothing_and_kill() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 618
def do_nothing_and_kill; do_nothing_and_then :kill end
do_nothing_and_next() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 625
def do_nothing_and_next; do_nothing_and_then :next end
do_nothing_and_prev() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 632
def do_nothing_and_prev; do_nothing_and_then :prev end
do_nothing_and_then(op) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 694
def do_nothing_and_then op
  dispatch op
end
draw_line(ln, opts={}) click to toggle source
Calls superclass method Redwood::LineCursorMode#draw_line
# File lib/sup/modes/thread_view_mode.rb, line 171
def draw_line ln, opts={}
  if ln == curpos
    super ln, :highlight => true
  else
    super
  end
end
edit_as_new() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 383
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, :refs => m.refs, :replytos => m.replytos)
  BufferManager.spawn "edit as new", mode
  mode.default_edit_message
end
edit_draft() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 450
def edit_draft
  m = @message_lines[curpos] or return
  if m.is_draft?
    mode = ResumeMode.new m
    BufferManager.spawn "Edit message", mode
    BufferManager.kill_buffer self.buffer
    mode.default_edit_message
  else
    BufferManager.flash "Not a draft message!"
  end
end
edit_labels() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 313
def edit_labels
  old_labels = @thread.labels
  reserved_labels = old_labels.select { |l| LabelManager::RESERVED_LABELS.include? l }
  new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels.sort_by {|x| x.to_s}

  return unless new_labels
  @thread.labels = Set.new(reserved_labels) + new_labels
  new_labels.each { |l| LabelManager << l }
  update
  UpdateManager.relay self, :labeled, @thread.first
  Index.save_thread @thread
  UndoManager.register "labeling thread" do
    @thread.labels = old_labels
    Index.save_thread @thread
    UpdateManager.relay self, :labeled, @thread.first
  end
end
expand_all_messages() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 587
def expand_all_messages
  @global_message_state ||= :closed
  @global_message_state = (@global_message_state == :closed ? :open : :closed)
  @layout.each { |m, l| l.state = @global_message_state }
  update
end
expand_all_quotes() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 599
def expand_all_quotes
  if(m = @message_lines[curpos])
    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 }
    update
  end
end
fetch_and_verify() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 800
def fetch_and_verify
  message = @message_lines[curpos]
  crypto_chunk = message.chunks.select {|chunk| chunk.is_a?(Chunk::CryptoNotice)}.first
  return unless crypto_chunk
  return unless crypto_chunk.unknown_fingerprint

  BufferManager.flash "Retrieving key #{crypto_chunk.unknown_fingerprint} ..."

  error = CryptoManager.retrieve crypto_chunk.unknown_fingerprint

  if error
    BufferManager.flash "Couldn't retrieve key: #{error.to_s}"
  else
    BufferManager.flash "Key #{crypto_chunk.unknown_fingerprint} successfully retrieved !"
  end

  # Re-trigger gpg verification
  message.reload_from_source!
  update
end
forward() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 253
def forward
  if(chunk = @chunk_lines[curpos]) && chunk.is_a?(Chunk::Attachment)
    ForwardMode.spawn_nicely :attachments => [chunk]
  elsif(m = @message_lines[curpos])
    ForwardMode.spawn_nicely :message => m
  end
end
goto_uri() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 759
def goto_uri
  unless (chunk = @chunk_lines[curpos])
    BufferManager.flash "No URI found."
    return
  end
  unless HookManager.enabled? "goto"
    BufferManager.flash "You must add a goto.rb hook before you can goto a URI."
    return
  end

  # @text is a list of lines with this format:
  # [
  #   [[:text_color, "Some text"]]
  #   [[:text_color, " continued here"]]
  # ]

  linetext = @text.slice(curpos, @text.length).flatten(1)
    .take_while{|d| [:text_color, :sig_color].include?(d[0]) and d[1].strip != ""} # Only take up to the first "" alone on its line
    .map{|d| d[1].strip}.join("").strip

  found = false
  URI.extract(linetext || "").each do |match|
    begin
      u = URI.parse(match)
      next unless u.absolute?
      next unless ["http", "https"].include?(u.scheme)

      reallink = Shellwords.escape(u.to_s)
      BufferManager.flash "Going to #{reallink} ..."
      HookManager.run "goto", :uri => reallink
      BufferManager.completely_redraw_screen
      found = true

    rescue URI::InvalidURIError => e
      debug "not a uri: #{e}"
      # Do nothing, this is an ok flow
    end
  end
  BufferManager.flash "No URI found." unless found
end
jump_to_first_open() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 474
def jump_to_first_open
  m = @message_lines[0] or return
  if @layout[m].state != :closed
    jump_to_message m#, true
  else
    jump_to_next_open #true
  end
end
jump_to_message(m, force_alignment=false) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 564
def jump_to_message m, force_alignment=false
  l = @layout[m]

  ## boundaries of the message
  message_left = l.depth * @indent_spaces
  message_right = message_left + l.width

  ## calculate leftmost colum
  left = if force_alignment # force mode: align exactly
    message_left
  else # regular: minimize cursor movement
    ## leftmost and rightmost are boundaries of all valid left-column
    ## alignments.
    leftmost = [message_left, message_right - buffer.content_width + 1].min
    rightmost = message_left
    leftcol.clamp(leftmost, rightmost)
  end

  jump_to_line l.top    # move vertically
  jump_to_col left      # move horizontally
  set_cursor_pos l.top  # set cursor pos
end
jump_to_next_and_open() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 483
def jump_to_next_and_open
  return continue_search_in_buffer if in_search? # err.. don't know why im doing this

  m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
  return unless m

  nextm = @layout[m].next
  return unless nextm

  if @layout[m].toggled_state == true
    @layout[m].state = :closed
    @layout[m].toggled_state = false
    update
  end

  if @layout[nextm].state == :closed
    @layout[nextm].state = :open
    @layout[nextm].toggled_state = true
  end

  jump_to_message nextm if nextm

  update if @layout[nextm].toggled_state
end
jump_to_next_open(force_alignment=nil) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 508
def jump_to_next_open force_alignment=nil
  return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations
  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, force_alignment if nextm
end
jump_to_prev_and_open(force_alignment=nil) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 524
def jump_to_prev_and_open force_alignment=nil
  m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] }
  return unless m

  nextm = @layout[m].prev
  return unless nextm

  if @layout[m].toggled_state == true
    @layout[m].state = :closed
    @layout[m].toggled_state = false
    update
  end

  if @layout[nextm].state == :closed
    @layout[nextm].state = :open
    @layout[nextm].toggled_state = true
  end

  jump_to_message nextm if nextm
  update if @layout[nextm].toggled_state
end
jump_to_prev_open() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 546
def jump_to_prev_open
  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

  top = @layout[m].top
  if curpos == top
    while(prevm = @layout[m].prev)
      break if @layout[prevm].state != :closed
      m = prevm
    end
    jump_to_message prevm if prevm
  else
    jump_to_message m
  end
end
kill_and_kill() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 616
def kill_and_kill; kill_and_then :kill end
kill_and_next() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 623
def kill_and_next; kill_and_then :next end
kill_and_prev() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 630
def kill_and_prev; kill_and_then :prev end
kill_and_then(op) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 673
def kill_and_then op
  dispatch op do
    @thread.apply_label :killed
    UpdateManager.relay self, :killed, @thread.first
    Index.save_thread @thread
    UndoManager.register "killed 1 thread" do
      @thread.remove_label :killed
      Index.save_thread @thread
      UpdateManager.relay self, :unkilled, @thread.first
    end
  end
end
lines() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 178
def lines; @text.length; end
pipe_message() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 720
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, success = 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

  unless success
    BufferManager.flash "Invalid command: '#{command}' is not an executable"
    return
  end

  if output
    BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
  else
    BufferManager.flash "'#{command}' done!"
  end
end
publish() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 441
def publish
  chunk = @chunk_lines[curpos] or return
  if HookManager.enabled? "publish"
    HookManager.run "publish", :chunk => chunk
  else
    BufferManager.flash "Publishing hook not defined."
  end
end
reload() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 209
def reload
  update
end
reply(type_arg=nil) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 213
def reply type_arg=nil
  m = @message_lines[curpos] or return
  mode = ReplyMode.new m, type_arg
  BufferManager.spawn "Reply to #{m.subj}", mode
end
reply_all() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 219
def reply_all; reply :all; end
save_all_to_disk() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 415
def save_all_to_disk
  m = @message_lines[curpos] or return
  default_dir = ($config[:default_attachment_save_dir] || ".")
  folder = BufferManager.ask_for_filename :filename, "Save all attachments to folder: ", default_dir, true
  return unless folder

  num = 0
  num_errors = 0
  m.chunks.each do |chunk|
    next unless chunk.is_a?(Chunk::Attachment)
    fn = File.join(folder, chunk.filesafe_filename)
    num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
    num += 1
  end

  if num == 0
    BufferManager.flash "Didn't find any attachments!"
  else
    if num_errors == 0
      BufferManager.flash "Wrote #{num.pluralize 'attachment'} to #{folder}."
    else
      BufferManager.flash "Wrote #{(num - num_errors).pluralize 'attachment'} to #{folder}; couldn't write #{num_errors} of them (see log)."
    end
  end
end
save_to_disk() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 390
def save_to_disk
  chunk = @chunk_lines[curpos] or return
  case chunk
  when Chunk::Attachment
    default_dir = $config[:default_attachment_save_dir]
    default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
    default_fn = File.expand_path File.join(default_dir, chunk.filesafe_filename)
    fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true

    # if user selects directory use file name from message
    if fn and File.directory? fn
      fn = File.join(fn, chunk.filename)
    end

    save_to_file(fn) { |f| f.print chunk.raw_content } if fn
  else
    m = @message_lines[curpos]
    fn = BufferManager.ask_for_filename :filename, "Save message to file: "
    return unless fn
    save_to_file(fn) do |f|
      m.each_raw_message_line { |l| f.print l }
    end
  end
end
send_draft() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 462
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
show_header() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 189
def show_header
  m = @message_lines[curpos] or return
  BufferManager.spawn_unless_exists("Full header for #{m.id}") do
    TextMode.new m.raw_header.ascii
  end
end
show_message() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 196
def show_message
  m = @message_lines[curpos] or return
  BufferManager.spawn_unless_exists("Raw message for #{m.id}") do
    TextMode.new m.raw_message.ascii
  end
end
spam_and_kill() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 614
def spam_and_kill; spam_and_then :kill end
spam_and_next() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 621
def spam_and_next; spam_and_then :next end
spam_and_prev() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 628
def spam_and_prev; spam_and_then :prev end
spam_and_then(op) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 647
def spam_and_then op
  dispatch op do
    @thread.apply_label :spam
    UpdateManager.relay self, :spammed, @thread.first
    Index.save_thread @thread
    UndoManager.register "marking 1 thread as spam" do
      @thread.remove_label :spam
      Index.save_thread @thread
      UpdateManager.relay self, :unspammed, @thread.first
    end
  end
end
status() click to toggle source
Calls superclass method
# File lib/sup/modes/thread_view_mode.rb, line 751
def status
  user_labels = @thread.labels.to_a.map do |l|
    l.to_s if LabelManager.user_defined_labels.member?(l)
  end.compact.join(",")
  user_labels = (user_labels.empty? and "" or "<#{user_labels}>")
  [user_labels, super].join(" -- ")
end
subscribe_to_list() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 221
def subscribe_to_list
  m = @message_lines[curpos] or return
  if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
    ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "subscribe")
  else
    BufferManager.flash "Can't find List-Subscribe header for this message."
  end
end
toggle_detailed_header() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 203
def toggle_detailed_header
  m = @message_lines[curpos] or return
  @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
  update
end
toggle_label(m, label) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 341
def toggle_label m, label
  if m.has_label? label
    m.remove_label label
  else
    m.add_label label
  end
  ## TODO: don't recalculate EVERYTHING just to add a stupid little
  ## star to the display
  update
  UpdateManager.relay self, :single_message_labeled, m
  Index.save_thread @thread
end
toggle_new() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 336
def toggle_new
  m = @message_lines[curpos] or return
  toggle_label m, :unread
end
toggle_starred() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 331
def toggle_starred
  m = @message_lines[curpos] or return
  toggle_label m, :starred
end
toggle_wrap() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 165
def toggle_wrap
  @wrap = !@wrap
  regen_text
  buffer.mark_dirty if buffer
end
unread_and_kill() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 617
def unread_and_kill; unread_and_then :kill end
unread_and_next() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 624
def unread_and_next; unread_and_then :next end
unread_and_prev() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 631
def unread_and_prev; unread_and_then :prev end
unread_and_then(op) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 686
def unread_and_then op
  dispatch op do
    @thread.apply_label :unread
    UpdateManager.relay self, :unread, @thread.first
    Index.save_thread @thread
  end
end
unsubscribe_from_list() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 230
def unsubscribe_from_list
  m = @message_lines[curpos] or return
  BufferManager.flash "Can't find List-Unsubscribe header for this message." unless m.list_unsubscribe

  if m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
    ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "unsubscribe")
  elsif m.list_unsubscribe =~ /<(http.*)?>/
    unless HookManager.enabled? "goto"
      BufferManager.flash "You must add a goto.rb hook before you can goto an unsubscribe URI."
      return
    end

    begin
      u = URI.parse($1)
    rescue URI::InvalidURIError => e
      BufferManager.flash("Invalid unsubscribe link")
      return
    end

    HookManager.run "goto", :uri => Shellwords.escape(u.to_s)
  end
end

Private Instance Methods

chunk_to_lines(chunk, state, start, depth, parent=nil, color=nil, star_color=nil) click to toggle source

todo: check arguments on this overly complex function

# File lib/sup/modes/thread_view_mode.rb, line 996
def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
  prefix = " " * @indent_spaces * depth
  case chunk
  when :fake_root
    [[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]
  when nil
    [[[:missing_message_color, "#{prefix}<an unreceived message>"]]]
  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. Hit 'e' to edit, 'y' to send. <<<"]]] : [])

  else
    raise "Bad chunk: #{chunk.inspect}" unless chunk.respond_to?(:inlineable?) ## debugging
    if chunk.inlineable?
      lines = maybe_wrap_text(chunk.lines)
      lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
    elsif chunk.expandable?
      case state
      when :closed
        [[[chunk.patina_color, "#{prefix}+ #{chunk.patina_text}"]]]
      when :open
        lines = maybe_wrap_text(chunk.lines)
        [[[chunk.patina_color, "#{prefix}- #{chunk.patina_text}"]]] + lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
      end
    else
      [[[chunk.patina_color, "#{prefix}x #{chunk.patina_text}"]]]
    end
  end
end
dispatch(op) { || ... } click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 698
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
format_person(p) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 974
def format_person p
  p.longname + (ContactManager.is_aliased_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
end
format_person_list(prefix, people) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 965
def format_person_list prefix, people
  ptext = people.map { |p| format_person p }
  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 ? "" : ",")
    end
end
initial_state_for(m) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 823
def initial_state_for m
  if m.has_label?(:starred) || m.has_label?(:unread)
    :open
  else
    :closed
  end
end
maybe_wrap_text(lines) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 978
def maybe_wrap_text lines
  if @wrap
    config_width = $config[:wrap_width]
    if config_width and config_width != 0
      width = [config_width, buffer.content_width].min
    else
      width = buffer.content_width
    end
    # lines can apparently be both String and Array, convert to Array for map.
    if lines.kind_of? String
      lines = lines.lines.to_a
    end
    lines = lines.map { |l| l.chomp.wrap width if l }.flatten
  end
  return lines
end
message_patina_lines(m, state, start, parent, prefix, color, star_color) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 902
def message_patina_lines m, state, start, parent, prefix, color, star_color
  prefix_widget = [color, prefix]

  open_widget = [color, (state == :closed ? "+ " : "- ")]
  new_widget = [color, (m.has_label?(:unread) ? "N" : " ")]
  starred_widget = if m.has_label?(:starred)
      [star_color, "*"]
    else
      [color, " "]
    end
  attach_widget = [color, (m.has_label?(:attachment) ? "@" : " ")]

  case state
  when :open
    @person_lines[start] = m.from
    [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
      [color,
          "#{m.from ? m.from.mediumname.fix_encoding! : '?'} to #{m.recipients.map { |l| l.shortname.fix_encoding! }.join(', ')} #{m.date.to_nice_s.fix_encoding!} (#{m.date.to_nice_distance_s.fix_encoding!})"]]]

  when :closed
    @person_lines[start] = m.from
    [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
      [color,
      "#{m.from ? m.from.mediumname.fix_encoding! : '?'}, #{m.date.to_nice_s.fix_encoding!} (#{m.date.to_nice_distance_s.fix_encoding!})  #{m.snippet ? m.snippet.fix_encoding! : ''}"]]]

  when :detailed
    @person_lines[start] = m.from
    from_line = [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
        [color, "From: #{m.from ? format_person(m.from) : '?'}"]]]

    addressee_lines = []
    unless m.to.empty?
      m.to.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
      addressee_lines += format_person_list "   To: ", m.to
    end
    unless m.cc.empty?
      m.cc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
      addressee_lines += format_person_list "   Cc: ", m.cc
    end
    unless m.bcc.empty?
      m.bcc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
      addressee_lines += format_person_list "   Bcc: ", m.bcc
    end

    headers = {
      "Date" => "#{m.date.to_message_nice_s} (#{m.date.to_nice_distance_s})",
      "Subject" => m.subj
    }

    show_labels = @thread.labels - LabelManager::HIDDEN_RESERVED_LABELS
    unless show_labels.empty?
      headers["Labels"] = show_labels.map { |x| x.to_s }.sort.join(', ')
    end
    if parent
      headers["In reply to"] = "#{parent.from.mediumname}'s message of #{parent.date.to_message_nice_s}"
    end

    HookManager.run "detailed-headers", :message => m, :headers => headers

    from_line + (addressee_lines + headers.map { |k, v| "   #{k}: #{v}" }).map { |l| [[color, prefix + "  " + l]] }
  end
end
regen_text() click to toggle source

here we generate the actual content lines. we accumulate everything into @text, and we set @chunk_lines and @message_lines, and we update @layout.

# File lib/sup/modes/thread_view_mode.rb, line 839
def regen_text
  @text = []
  @chunk_lines = []
  @message_lines = []
  @person_lines = []

  prevm = nil
  @thread.each do |m, depth, parent|
    unless m.is_a? Message # handle nil and :fake_root
      @text += chunk_to_lines m, nil, @text.length, depth, parent
      next
    end
    l = @layout[m]

    ## is this still necessary?
    next unless @layout[m].state # skip discarded drafts

    ## build the patina
    text = chunk_to_lines m, l.state, @text.length, depth, parent, l.color, l.star_color

    l.top = @text.length
    l.bot = @text.length + text.length # updated below
    l.prev = prevm
    l.next = nil
    l.depth = depth
    # l.state we preserve
    l.width = 0 # updated below
    @layout[l.prev].next = m if l.prev

    (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.display_length }.sum
    end

    @text += text
    prevm = m
    if l.state != :closed
      m.chunks.each do |c|
        cl = @chunk_layout[c]

        ## set the default state for chunks
        cl.state ||=
          if c.expandable? && c.respond_to?(:initial_state)
            c.initial_state
          else
            :closed
          end

        text = chunk_to_lines c, cl.state, @text.length, depth
        (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.display_length }.sum - (depth * @indent_spaces)
          l.width = lw if lw > l.width
        end
        @text += text
      end
      @layout[m].bot = @text.length
    end
  end
end
update() click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 831
def update
  regen_text
  buffer.mark_dirty if buffer
end
view(chunk) click to toggle source
# File lib/sup/modes/thread_view_mode.rb, line 1026
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: #{chunk.filename}", TextMode.new(chunk.to_s.ascii, chunk.filename)
    BufferManager.flash "Couldn't execute view command, viewing as text."
  end
end