Class: Redwood::ThreadIndexMode

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

Overview

subclasses should implement:

  • is_relevant?

Direct Known Subclasses

InboxMode, LabelSearchResultsMode, PersonSearchResultsMode, SearchResultsMode

Constant Summary

DATE_WIDTH =
Time::TO_NICE_S_MAX_LEN
MIN_FROM_WIDTH =
15
LOAD_MORE_THREAD_NUM =
20

Constants inherited from ScrollMode

ScrollMode::COL_JUMP

Instance Attribute Summary

Attributes inherited from LineCursorMode

#curpos

Attributes inherited from ScrollMode

#botline, #leftcol, #topline

Attributes inherited from Mode

#buffer

Instance Method Summary (collapse)

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, #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, #resolve_input, #save_to_file

Constructor Details

- (ThreadIndexMode) initialize(hidden_labels = [], load_thread_opts = {})



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/sup/modes/thread-index-mode.rb', line 49

def initialize hidden_labels=[], load_thread_opts={}
  super()
  @mutex = Mutex.new # covers the following variables:
  @threads = {}
  @hidden_threads = {}
  @size_widget_width = nil
  @size_widgets = {}
  @tags = Tagger.new self

  ## these guys, and @text and @lines, are not covered
  @load_thread = nil
  @load_thread_opts = load_thread_opts
  @hidden_labels = hidden_labels + LabelManager::HIDDEN_RESERVED_LABELS
  @date_width = DATE_WIDTH

  @interrupt_search = false
  
  initialize_threads # defines @ts and @ts_mutex
  update # defines @text and @lines

  UpdateManager.register self

  @save_thread_mutex = Mutex.new

  @last_load_more_size = nil
  to_load_more do |size|
    next if @last_load_more_size == 0
    load_threads :num => 1, :background => false
    load_threads :num => (size - 1),
                 :when_done => lambda { |num| @last_load_more_size = num }
  end
end

Instance Method Details

- (Object) [](i)



83
# File 'lib/sup/modes/thread-index-mode.rb', line 83

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

- (Object) actually_save



398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/sup/modes/thread-index-mode.rb', line 398

def actually_save
  @save_thread_mutex.synchronize do
    BufferManager.say("Saving contacts...") { ContactManager.instance.save }
    dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
    next if dirty_threads.empty?

    BufferManager.say("Saving threads...") do |say_id|
      dirty_threads.each_with_index do |t, i|
        BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
        t.save Index
      end
    end
  end
end

- (Object) actually_toggle_archived(t)



257
258
259
260
261
262
263
264
265
# File 'lib/sup/modes/thread-index-mode.rb', line 257

def actually_toggle_archived t
  if t.has_label? :inbox
    t.remove_label :inbox
    UpdateManager.relay self, :archived, t.first
  else
    t.apply_label :inbox
    UpdateManager.relay self, :unarchived, t.first
  end
end

- (Object) actually_toggle_deleted(t)



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

def actually_toggle_deleted t
  if t.has_label? :deleted
    t.remove_label :deleted
    UpdateManager.relay self, :undeleted, t.first
  else
    t.apply_label :deleted
    UpdateManager.relay self, :deleted, t.first
  end
end

- (Object) actually_toggle_spammed(t)



267
268
269
270
271
272
273
274
275
# File 'lib/sup/modes/thread-index-mode.rb', line 267

def actually_toggle_spammed t
  if t.has_label? :spam
    t.remove_label :spam
    UpdateManager.relay self, :unspammed, t.first
  else
    t.apply_label :spam
    UpdateManager.relay self, :spammed, t.first
  end
end

- (Object) actually_toggle_starred(t)



235
236
237
238
239
240
241
242
243
# File 'lib/sup/modes/thread-index-mode.rb', line 235

def actually_toggle_starred t
  if t.has_label? :starred # if ANY message has a star
    t.remove_label :starred # remove from all
    UpdateManager.relay self, :unstarred, t.first
  else
    t.first.add_label :starred # add only to first
    UpdateManager.relay self, :starred, t.first
  end
end

- (Object) apply_to_tagged



451
# File 'lib/sup/modes/thread-index-mode.rb', line 451

def apply_to_tagged; @tags.apply_to_tagged; end

- (Object) cancel_search



553
554
555
# File 'lib/sup/modes/thread-index-mode.rb', line 553

def cancel_search
  @interrupt_search = true
end

- (Object) cleanup



413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/sup/modes/thread-index-mode.rb', line 413

def cleanup
  UpdateManager.unregister self

  if @load_thread
    @load_thread.kill 
    BufferManager.clear @mbid if @mbid
    sleep 0.1 # TODO: necessary?
    BufferManager.erase_flash
  end
  save false
  super
end

- (Boolean) contains_thread?(t)



84
# File 'lib/sup/modes/thread-index-mode.rb', line 84

def contains_thread? t; @threads.include?(t) end

- (Object) edit_labels



453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'lib/sup/modes/thread-index-mode.rb', line 453

def edit_labels
  thread = cursor_thread or return
  speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
  keepl, modifyl = thread.labels.partition { |t| speciall.member? t }

  user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl, @hidden_labels

  return unless user_labels
  thread.labels = keepl + user_labels
  user_labels.each { |l| LabelManager << l }
  update_text_for_line curpos
  UpdateManager.relay self, :labeled, thread.first
end

- (Object) edit_message



224
225
226
227
228
229
230
231
232
233
# File 'lib/sup/modes/thread-index-mode.rb', line 224

def edit_message
  return unless(t = cursor_thread)
  message, *crap = t.find { |m, *o| m.has_label? :draft }
  if message
    mode = ResumeMode.new message
    BufferManager.spawn "Edit message", mode
  else
    BufferManager.flash "Not a draft message!"
  end
end

- (Object) forward



499
500
501
502
503
504
505
# File 'lib/sup/modes/thread-index-mode.rb', line 499

def forward
  t = cursor_thread or return
  m = t.latest_message
  return if m.nil? # probably won't happen
  m.load_from_source!
  ForwardMode.spawn_nicely :message => m
end

- (Object) handle_added_update(sender, m)



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

def handle_added_update sender, m
  add_or_unhide m
  BufferManager.draw_screen
end

- (Object) handle_deleted_update(sender, m)



195
196
197
198
199
200
# File 'lib/sup/modes/thread-index-mode.rb', line 195

def handle_deleted_update sender, m
  t = @ts_mutex.synchronize { @ts.thread_for m }
  return unless t
  hide_thread t
  update
end

- (Object) handle_labeled_update(sender, m)



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

def handle_labeled_update sender, m
  if(t = thread_containing(m)) 
    l = @lines[t] or return
    update_text_for_line l
  elsif is_relevant?(m)
    add_or_unhide m
  end
end

- (Object) handle_simple_update(sender, m)



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

def handle_simple_update sender, m
  t = thread_containing(m) or return
  l = @lines[t] or return
  update_text_for_line l
end

- (Object) handle_single_message_deleted_update(sender, m)



187
188
189
190
191
192
193
# File 'lib/sup/modes/thread-index-mode.rb', line 187

def handle_single_message_deleted_update sender, m
  @ts_mutex.synchronize do
    return unless @ts.contains? m
    @ts.remove_id m.id
  end
  update
end

- (Object) handle_single_message_labeled_update(sender, m)



152
153
154
155
156
# File 'lib/sup/modes/thread-index-mode.rb', line 152

def handle_single_message_labeled_update sender, m
  ## no need to do anything different here; we don't differentiate 
  ## messages from their containing threads
  handle_labeled_update sender, m
end

- (Object) handle_spammed_update(sender, m)



202
203
204
205
206
207
# File 'lib/sup/modes/thread-index-mode.rb', line 202

def handle_spammed_update sender, m
  t = @ts_mutex.synchronize { @ts.thread_for m }
  return unless t
  hide_thread t
  update
end

- (Object) handle_undeleted_update(sender, m)



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

def handle_undeleted_update sender, m
  add_or_unhide m
end

- (Boolean) is_relevant?(m)

overwrite me!



180
# File 'lib/sup/modes/thread-index-mode.rb', line 180

def is_relevant? m; false; end

- (Object) join_threads



315
316
317
318
319
# File 'lib/sup/modes/thread-index-mode.rb', line 315

def join_threads
  ## this command has no non-tagged form. as a convenience, allow this
  ## command to be applied to tagged threads without hitting ';'.
  @tags.apply_to_tagged :join_threads
end

- (Object) jump_to_next_new



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

def jump_to_next_new
  n = @mutex.synchronize do
    ((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread } ||
      (0 ... curpos).find { |i| @threads[i].has_label? :unread }
  end
  if n
    ## jump there if necessary
    jump_to_line n unless n >= topline && n < botline
    set_cursor_pos n
  else
    BufferManager.flash "No new messages"
  end
end

- (Object) kill



376
377
378
379
# File 'lib/sup/modes/thread-index-mode.rb', line 376

def kill
  t = cursor_thread or return
  multi_kill [t]
end

- (Object) launch_another_thread(thread, direction, &b)



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/sup/modes/thread-index-mode.rb', line 135

def launch_another_thread thread, direction, &b
  l = @lines[thread] or return
  target_l = l + direction
  t = @mutex.synchronize do
    if target_l >= 0 && target_l < @threads.length
      @threads[target_l]
    end
  end

  if t # there's a next thread
    set_cursor_pos target_l # move out of mutex?
    select t, b
  elsif b # no next thread. call the block anyways
    b.call
  end
end

- (Object) launch_next_thread_after(thread, &b)

these two methods are called by thread-view-modes when the user wants to view the previous/next thread without going back to index-mode. we update the cursor as a convenience.



127
128
129
# File 'lib/sup/modes/thread-index-mode.rb', line 127

def launch_next_thread_after thread, &b
  launch_another_thread thread, 1, &b
end

- (Object) launch_prev_thread_before(thread, &b)



131
132
133
# File 'lib/sup/modes/thread-index-mode.rb', line 131

def launch_prev_thread_before thread, &b
  launch_another_thread thread, -1, &b
end

- (Object) lines



82
# File 'lib/sup/modes/thread-index-mode.rb', line 82

def lines; @text.length; end

- (Object) load_all_threads



557
558
559
# File 'lib/sup/modes/thread-index-mode.rb', line 557

def load_all_threads
  load_threads :num => -1
end

- (Object) load_n_threads(n = LOAD_MORE_THREAD_NUM, opts = {})

TODO: figure out @ts_mutex in this method



517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
# File 'lib/sup/modes/thread-index-mode.rb', line 517

def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
  @interrupt_search = false
  @mbid = BufferManager.say "Searching for threads..."

  ts_to_load = n
  ts_to_load = ts_to_load + @ts.size unless n == -1 # -1 means all threads

  orig_size = @ts.size
  last_update = Time.now
  @ts.load_n_threads(ts_to_load, opts) do |i|
    if (Time.now - last_update) >= 0.25
      BufferManager.say "Loaded #{i.pluralize 'thread'}...", @mbid
      update
      BufferManager.draw_screen
      last_update = Time.now
    end
    break if @interrupt_search
  end
  @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }

  update
  BufferManager.clear @mbid
  @mbid = nil
  BufferManager.draw_screen
  @ts.size - orig_size
end

- (Object) load_n_threads_background(n = LOAD_MORE_THREAD_NUM, opts = {})



507
508
509
510
511
512
513
514
# File 'lib/sup/modes/thread-index-mode.rb', line 507

def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
  return if @load_thread # todo: wrap in mutex
  @load_thread = Redwood::reporting_thread("load threads for thread-index-mode") do
    num = load_n_threads n, opts
    opts[:when_done].call(num) if opts[:when_done]
    @load_thread = nil
  end
end

- (Object) load_threads(opts = {})



561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/sup/modes/thread-index-mode.rb', line 561

def load_threads opts={}
  if opts[:num].nil?
    n = ThreadIndexMode::LOAD_MORE_THREAD_NUM
  else
    n = opts[:num]
  end

  myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
    opts[:when_done].call(num) if opts[:when_done]

    if num > 0
      BufferManager.flash "Found #{num.pluralize 'thread'}."
    else
      BufferManager.flash "No matches."
    end
  end)})

  if opts[:background] || opts[:background].nil?
    load_n_threads_background n, myopts
  else
    load_n_threads n, myopts
  end
end

- (Object) multi_edit_labels(threads)



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

def multi_edit_labels threads
  user_labels = BufferManager.ask_for_labels :labels, "Add/remove labels (use -label to remove): ", [], @hidden_labels
  return unless user_labels

  user_labels.map! { |l| (l.to_s =~ /^-/)? [l.to_s.gsub(/^-?/, '').to_sym, true] : [l, false] }
  hl = user_labels.select { |(l,_)| @hidden_labels.member? l }
  if hl.empty?
    threads.each do |t|
      user_labels.each do |(l, to_remove)|
        if to_remove
          t.remove_label l
        else
          t.apply_label l
        end
      end
    end
    user_labels.each { |(l,_)| LabelManager << l }
  else
    BufferManager.flash "'#{hl}' is a reserved label!"
  end
  regen_text
end

- (Object) multi_join_threads(threads)



321
322
323
324
325
# File 'lib/sup/modes/thread-index-mode.rb', line 321

def multi_join_threads threads
  @ts.join_threads threads or return
  @tags.drop_all_tags # otherwise we have tag pointers to invalid threads!
  update
end

- (Object) multi_kill(threads)



381
382
383
384
385
386
387
388
# File 'lib/sup/modes/thread-index-mode.rb', line 381

def multi_kill threads
  threads.each do |t|
    t.apply_label :killed
    hide_thread t
  end
  regen_text
  BufferManager.flash "#{threads.size.pluralize 'Thread'} killed."
end

- (Object) multi_select(threads)



120
121
122
# File 'lib/sup/modes/thread-index-mode.rb', line 120

def multi_select threads
  threads.each { |t| select t }
end

- (Object) multi_toggle_archived(threads)



293
294
295
296
# File 'lib/sup/modes/thread-index-mode.rb', line 293

def multi_toggle_archived threads
  threads.each { |t| actually_toggle_archived t }
  regen_text
end

- (Object) multi_toggle_deleted(threads)

see comment for multi_toggle_spam



368
369
370
371
372
373
374
# File 'lib/sup/modes/thread-index-mode.rb', line 368

def multi_toggle_deleted threads
  threads.each do |t|
    actually_toggle_deleted t
    hide_thread t 
  end
  regen_text
end

- (Object) multi_toggle_new(threads)



305
306
307
308
# File 'lib/sup/modes/thread-index-mode.rb', line 305

def multi_toggle_new threads
  threads.each { |t| t.toggle_label :unread }
  regen_text
end

- (Object) multi_toggle_spam(threads)

both spam and deleted have the curious characteristic that you always want to hide the thread after either applying or removing that label. in all thread-index-views except for label-search-results-mode, when you mark a message as spam or deleted, you want it to disappear immediately; in LSRM, you only see deleted or spam emails, and when you undelete or unspam them you also want them to disappear immediately.



354
355
356
357
358
359
360
# File 'lib/sup/modes/thread-index-mode.rb', line 354

def multi_toggle_spam threads
  threads.each do |t|
    actually_toggle_spammed t
    hide_thread t 
  end
  regen_text
end

- (Object) multi_toggle_starred(threads)



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

def multi_toggle_starred threads
  threads.each { |t| actually_toggle_starred t }
  regen_text
end

- (Object) multi_toggle_tagged(threads)



310
311
312
313
# File 'lib/sup/modes/thread-index-mode.rb', line 310

def multi_toggle_tagged threads
  @mutex.synchronize { @tags.drop_all_tags }
  regen_text
end

- (Object) reload



86
87
88
89
90
# File 'lib/sup/modes/thread-index-mode.rb', line 86

def reload
  drop_all_threads
  BufferManager.draw_screen
  load_threads :num => buffer.content_height
end

- (Object) reply



490
491
492
493
494
495
496
497
# File 'lib/sup/modes/thread-index-mode.rb', line 490

def reply
  t = cursor_thread or return
  m = t.latest_message
  return if m.nil? # probably won't happen
  m.load_from_source!
  mode = ReplyMode.new m
  BufferManager.spawn "Reply to #{m.subj}", mode
end

- (Object) resize(rows, cols)



586
587
588
589
# File 'lib/sup/modes/thread-index-mode.rb', line 586

def resize rows, cols
  regen_text
  super
end

- (Object) save(background = true)



390
391
392
393
394
395
396
# File 'lib/sup/modes/thread-index-mode.rb', line 390

def save background=true
  if background
    Redwood::reporting_thread("saving thread") { actually_save }
  else
    actually_save
  end
end

- (Object) select(t = nil, when_done = nil)

open up a thread view window



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-index-mode.rb', line 93

def select t=nil, when_done=nil
  t ||= cursor_thread or return

  Redwood::reporting_thread("load messages for thread-view-mode") do
    num = t.size
    message = "Loading #{num.pluralize 'message body'}..."
    BufferManager.say(message) do |sid|
      t.each_with_index do |(m, *o), i|
        next unless m
        BufferManager.say "#{message} (#{i}/#{num})", sid if t.size > 1
        m.load_from_source! 
      end
    end
    mode = ThreadViewMode.new t, @hidden_labels, self
    BufferManager.spawn t.subj, mode
    BufferManager.draw_screen
    mode.jump_to_first_open true
    BufferManager.draw_screen # lame TODO: make this unnecessary
    ## the first draw_screen is needed before topline and botline
    ## are set, and the second to show the cursor having moved

    update_text_for_line curpos
    UpdateManager.relay self, :read, t.first
    when_done.call if when_done
  end
end

- (Object) status



545
546
547
548
549
550
551
# File 'lib/sup/modes/thread-index-mode.rb', line 545

def status
  if (l = lines) == 0
    "line 0 of 0"
  else
    "line #{curpos + 1} of #{l} #{dirty? ? '*modified*' : ''}"
  end
end

- (Object) tag_matching



438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/sup/modes/thread-index-mode.rb', line 438

def tag_matching
  query = BufferManager.ask :search, "tag threads matching (regex): "
  return if query.nil? || query.empty?
  query = begin
    /#{query}/i
  rescue RegexpError => e
    BufferManager.flash "error interpreting '#{query}': #{e.message}"
    return
  end
  @mutex.synchronize { @threads.each { |t| @tags.tag t if thread_matches?(t, query) } }
  regen_text
end

- (Object) toggle_archived



287
288
289
290
291
# File 'lib/sup/modes/thread-index-mode.rb', line 287

def toggle_archived 
  t = cursor_thread or return
  actually_toggle_archived t
  update_text_for_line curpos
end

- (Object) toggle_deleted



362
363
364
365
# File 'lib/sup/modes/thread-index-mode.rb', line 362

def toggle_deleted
  t = cursor_thread or return
  multi_toggle_deleted [t]
end

- (Object) toggle_new



298
299
300
301
302
303
# File 'lib/sup/modes/thread-index-mode.rb', line 298

def toggle_new
  t = cursor_thread or return
  t.toggle_label :unread
  update_text_for_line curpos
  cursor_down
end

- (Object) toggle_spam



341
342
343
344
345
# File 'lib/sup/modes/thread-index-mode.rb', line 341

def toggle_spam
  t = cursor_thread or return
  multi_toggle_spam [t]
  HookManager.run("mark-as-spam", :thread => t)
end

- (Object) toggle_starred



245
246
247
248
249
250
# File 'lib/sup/modes/thread-index-mode.rb', line 245

def toggle_starred 
  t = cursor_thread or return
  actually_toggle_starred t
  update_text_for_line curpos
  cursor_down
end

- (Object) toggle_tagged



426
427
428
429
430
431
# File 'lib/sup/modes/thread-index-mode.rb', line 426

def toggle_tagged
  t = cursor_thread or return
  @mutex.synchronize { @tags.toggle_tag_for t }
  update_text_for_line curpos
  cursor_down
end

- (Object) toggle_tagged_all



433
434
435
436
# File 'lib/sup/modes/thread-index-mode.rb', line 433

def toggle_tagged_all
  @mutex.synchronize { @threads.each { |t| @tags.toggle_tag_for t } }
  regen_text
end

- (Object) update



213
214
215
216
217
218
219
220
221
222
# File 'lib/sup/modes/thread-index-mode.rb', line 213

def update
  @mutex.synchronize do
    ## let's see you do THIS in python
    @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| [t.date, t.first.id] }.reverse
    @size_widgets = @threads.map { |t| size_widget_for_thread t }
    @size_widget_width = @size_widgets.max_of { |w| w.length }
  end

  regen_text
end