Class: Redwood::ThreadViewMode

Inherits:
LineCursorMode show all
Includes:
CanAliasContacts
Defined in:
lib/sup/modes/thread-view-mode.rb

Defined Under Namespace

Classes: ChunkLayout, MessageLayout

Constant Summary

DATE_FORMAT =
"%B %e %Y %l:%M%P"
INDENT_SPACES =

how many spaces to indent child messages

2
IDEAL_TOP_CONTEXT =

try and give 3 rows of top context

3
IDEAL_LEFT_CONTEXT =

try and give 4 columns of left context

4

Constants inherited from ScrollMode

ScrollMode::COL_JUMP

Instance Attribute Summary

Attributes inherited from LineCursorMode

#curpos

Attributes inherited from ScrollMode

#botline, #leftcol, #status, #topline

Attributes inherited from Mode

#buffer

Instance Method Summary collapse

Methods included from CanAliasContacts

#alias_contact

Methods inherited from LineCursorMode

#draw

Methods inherited from ScrollMode

#at_bottom?, #at_top?, #cancel_search!, #col_left, #col_right, #continue_search_in_buffer, #draw, #ensure_mode_validity, #half_page_down, #half_page_up, #in_search?, #jump_to_col, #jump_to_end, #jump_to_left, #jump_to_line, #jump_to_start, #line_down, #line_up, #page_down, #page_up, #resize, #rightcol, #search_goto_line, #search_goto_pos, #search_in_buffer, #search_start_line

Methods inherited from Mode

#blur, #cancel_search!, #draw, #focus, #handle_input, #help_text, #in_search?, #killable?, load_all_modes, make_name, #name, #pipe_to_process, register_keymap, #resize, #resolve_input, #save_to_file, #status

Constructor Details

#initialize(thread, hidden_labels = [], index_mode = nil) ⇒ ThreadViewMode

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.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/sup/modes/thread-view-mode.rb', line 84

def initialize thread, hidden_labels=[], index_mode=nil
  super()
  @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].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

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

  @thread.remove_label :unread
  regen_text
end

Instance Method Details

#[](i) ⇒ Object



128
# File 'lib/sup/modes/thread-view-mode.rb', line 128

def [] i; @text[i]; end

#activate_chunkObject

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.



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/sup/modes/thread-view-mode.rb', line 235

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 layout
    layout.state = (layout.state != :closed ? :closed : :open)
    #cursor_down if layout.state == :closed # too annoying
    update
  elsif chunk.viewable?
    view chunk
  end
end

#aliasObject



176
177
178
179
180
# File 'lib/sup/modes/thread-view-mode.rb', line 176

def alias
  p = @person_lines[curpos] or return
  alias_contact p
  update
end

#align_current_messageObject



320
321
322
323
# File 'lib/sup/modes/thread-view-mode.rb', line 320

def align_current_message
  m = @message_lines[curpos] or return
  jump_to_message m
end

#archive_and_killObject



397
# File 'lib/sup/modes/thread-view-mode.rb', line 397

def archive_and_kill; archive_and_then :kill end

#archive_and_nextObject



402
# File 'lib/sup/modes/thread-view-mode.rb', line 402

def archive_and_next; archive_and_then :next end

#archive_and_prevObject



408
# File 'lib/sup/modes/thread-view-mode.rb', line 408

def archive_and_prev; archive_and_then :prev end

#archive_and_then(op) ⇒ Object



414
415
416
417
418
419
# File 'lib/sup/modes/thread-view-mode.rb', line 414

def archive_and_then op
  dispatch op do
    @thread.remove_label :inbox
    UpdateManager.relay self, :archived, @thread.first
  end
end

#cleanupObject



393
394
395
# File 'lib/sup/modes/thread-view-mode.rb', line 393

def cleanup
  @layout = @chunk_layout = @text = nil # for good luck
end

#collapse_non_new_messagesObject



378
379
380
381
# File 'lib/sup/modes/thread-view-mode.rb', line 378

def collapse_non_new_messages
  @layout.each { |m, l| l.state = l.orig_new ? :open : :closed }
  update
end

#composeObject



189
190
191
192
193
194
195
196
# File 'lib/sup/modes/thread-view-mode.rb', line 189

def compose
  p = @person_lines[curpos]
  if p
    ComposeMode.spawn_nicely :to_default => p
  else
    ComposeMode.spawn_nicely
  end
end

#delete_and_killObject



399
# File 'lib/sup/modes/thread-view-mode.rb', line 399

def delete_and_kill; delete_and_then :kill end

#delete_and_nextObject



404
# File 'lib/sup/modes/thread-view-mode.rb', line 404

def delete_and_next; delete_and_then :next end

#delete_and_prevObject



410
# File 'lib/sup/modes/thread-view-mode.rb', line 410

def delete_and_prev; delete_and_then :prev end

#delete_and_then(op) ⇒ Object



428
429
430
431
432
433
# File 'lib/sup/modes/thread-view-mode.rb', line 428

def delete_and_then op
  dispatch op do
    @thread.apply_label :deleted
    UpdateManager.relay self, :deleted, @thread.first
  end
end

#do_nothing_and_nextObject



406
# File 'lib/sup/modes/thread-view-mode.rb', line 406

def do_nothing_and_next; do_nothing_and_then :next end

#do_nothing_and_prevObject



412
# File 'lib/sup/modes/thread-view-mode.rb', line 412

def do_nothing_and_prev; do_nothing_and_then :prev end

#do_nothing_and_then(op) ⇒ Object



442
443
444
# File 'lib/sup/modes/thread-view-mode.rb', line 442

def do_nothing_and_then op
  dispatch op
end

#draw_line(ln, opts = {}) ⇒ Object



120
121
122
123
124
125
126
# File 'lib/sup/modes/thread-view-mode.rb', line 120

def draw_line ln, opts={}
  if ln == curpos
    super ln, :highlight => true
  else
    super
  end
end

#edit_as_newObject



252
253
254
255
256
257
# File 'lib/sup/modes/thread-view-mode.rb', line 252

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.edit_message
end

#edit_draftObject



276
277
278
279
280
281
282
283
284
285
286
# File 'lib/sup/modes/thread-view-mode.rb', line 276

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.edit_message
  else
    BufferManager.flash "Not a draft message!"
  end
end

#edit_labelsObject



198
199
200
201
202
203
204
205
206
207
# File 'lib/sup/modes/thread-view-mode.rb', line 198

def edit_labels
  reserved_labels = @thread.labels.select { |l| LabelManager::RESERVED_LABELS.include? l }
  new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels

  return unless new_labels
  @thread.labels = (reserved_labels + new_labels).uniq
  new_labels.each { |l| LabelManager << l }
  update
  UpdateManager.relay self, :labeled, @thread.first
end

#expand_all_messagesObject



371
372
373
374
375
376
# File 'lib/sup/modes/thread-view-mode.rb', line 371

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_quotesObject



383
384
385
386
387
388
389
390
391
# File 'lib/sup/modes/thread-view-mode.rb', line 383

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

#forwardObject



167
168
169
170
171
172
173
# File 'lib/sup/modes/thread-view-mode.rb', line 167

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

#jump_to_first_open(loose_alignment = false) ⇒ Object



300
301
302
303
304
305
306
307
# File 'lib/sup/modes/thread-view-mode.rb', line 300

def jump_to_first_open loose_alignment=false
  m = @message_lines[0] or return
  if @layout[m].state != :closed
    jump_to_message m, loose_alignment
  else
    jump_to_next_open loose_alignment
  end
end

#jump_to_message(m, loose_alignment = false) ⇒ Object



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/sup/modes/thread-view-mode.rb', line 345

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
  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_col [ideal_left, 0].max

  ## either way, move the cursor to the first line
  set_cursor_pos l.top
end

#jump_to_next_open(loose_alignment = false) ⇒ Object



309
310
311
312
313
314
315
316
317
318
# File 'lib/sup/modes/thread-view-mode.rb', line 309

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 = (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, loose_alignment if nextm
end

#jump_to_prev_open(loose_alignment = false) ⇒ Object



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/sup/modes/thread-view-mode.rb', line 325

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
  
  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, loose_alignment if prevm
  else
    jump_to_message m, loose_alignment
  end
end

#linesObject



127
# File 'lib/sup/modes/thread-view-mode.rb', line 127

def lines; @text.length; end

#pipe_messageObject



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/sup/modes/thread-view-mode.rb', line 468

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

  if output
    BufferManager.spawn "Output of '#{command}'", TextMode.new(output)
  else
    BufferManager.flash "'#{command}' done!"
  end
end

#replyObject



143
144
145
146
147
# File 'lib/sup/modes/thread-view-mode.rb', line 143

def reply
  m = @message_lines[curpos] or return
  mode = ReplyMode.new m
  BufferManager.spawn "Reply to #{m.subj}", mode
end

#save_to_diskObject



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/sup/modes/thread-view-mode.rb', line 259

def save_to_disk
  chunk = @chunk_lines[curpos] or return
  case chunk
  when Chunk::Attachment
    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]
    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

#searchObject



182
183
184
185
186
187
# File 'lib/sup/modes/thread-view-mode.rb', line 182

def search
  p = @person_lines[curpos] or return
  mode = PersonSearchResultsMode.new [p]
  BufferManager.spawn "Search for #{p.name}", mode
  mode.load_threads :num => mode.buffer.content_height
end

#send_draftObject



288
289
290
291
292
293
294
295
296
297
298
# File 'lib/sup/modes/thread-view-mode.rb', line 288

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_headerObject



130
131
132
133
134
135
# File 'lib/sup/modes/thread-view-mode.rb', line 130

def show_header
  m = @message_lines[curpos] or return
  BufferManager.spawn_unless_exists("Full header for #{m.id}") do
    TextMode.new m.raw_header
  end
end

#spam_and_killObject



398
# File 'lib/sup/modes/thread-view-mode.rb', line 398

def spam_and_kill; spam_and_then :kill end

#spam_and_nextObject



403
# File 'lib/sup/modes/thread-view-mode.rb', line 403

def spam_and_next; spam_and_then :next end

#spam_and_prevObject



409
# File 'lib/sup/modes/thread-view-mode.rb', line 409

def spam_and_prev; spam_and_then :prev end

#spam_and_then(op) ⇒ Object



421
422
423
424
425
426
# File 'lib/sup/modes/thread-view-mode.rb', line 421

def spam_and_then op
  dispatch op do
    @thread.apply_label :spam
    UpdateManager.relay self, :spammed, @thread.first
  end
end

#subscribe_to_listObject



149
150
151
152
153
154
155
156
# File 'lib/sup/modes/thread-view-mode.rb', line 149

def subscribe_to_list
  m = @message_lines[curpos] or return
  if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
    ComposeMode.spawn_nicely :from => AccountManager.(m.recipient_email), :to => [Person.from_address($1)], :subj => $3
  else
    BufferManager.flash "Can't find List-Subscribe header for this message."
  end
end

#toggle_detailed_headerObject



137
138
139
140
141
# File 'lib/sup/modes/thread-view-mode.rb', line 137

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) ⇒ Object



219
220
221
222
223
224
225
226
227
228
229
# File 'lib/sup/modes/thread-view-mode.rb', line 219

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
end

#toggle_newObject



214
215
216
217
# File 'lib/sup/modes/thread-view-mode.rb', line 214

def toggle_new
  m = @message_lines[curpos] or return
  toggle_label m, :unread
end

#toggle_starredObject



209
210
211
212
# File 'lib/sup/modes/thread-view-mode.rb', line 209

def toggle_starred
  m = @message_lines[curpos] or return
  toggle_label m, :starred
end

#unread_and_killObject



400
# File 'lib/sup/modes/thread-view-mode.rb', line 400

def unread_and_kill; unread_and_then :kill end

#unread_and_nextObject



405
# File 'lib/sup/modes/thread-view-mode.rb', line 405

def unread_and_next; unread_and_then :next end

#unread_and_prevObject



411
# File 'lib/sup/modes/thread-view-mode.rb', line 411

def unread_and_prev; unread_and_then :prev end

#unread_and_then(op) ⇒ Object



435
436
437
438
439
440
# File 'lib/sup/modes/thread-view-mode.rb', line 435

def unread_and_then op
  dispatch op do
    @thread.apply_label :unread
    UpdateManager.relay self, :unread, @thread.first
  end
end

#unsubscribe_from_listObject



158
159
160
161
162
163
164
165
# File 'lib/sup/modes/thread-view-mode.rb', line 158

def unsubscribe_from_list
  m = @message_lines[curpos] or return
  if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
    ComposeMode.spawn_nicely :from => AccountManager.(m.recipient_email), :to => [Person.from_address($1)], :subj => $3
  else
    BufferManager.flash "Can't find List-Unsubscribe header for this message."
  end
end