Class: Redwood::ThreadIndexMode

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

Overview

subclasses should implement:

  • is_relevant?

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

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

Returns a new instance of ThreadIndexMode



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

#[](i) ⇒ Object



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

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

#actually_saveObject



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

#actually_toggle_archived(t) ⇒ Object



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

#actually_toggle_deleted(t) ⇒ Object



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

#actually_toggle_spammed(t) ⇒ Object



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

#actually_toggle_starred(t) ⇒ Object



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

#apply_to_taggedObject



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

def apply_to_tagged; @tags.apply_to_tagged; end

#cancel_searchObject



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

def cancel_search
  @interrupt_search = true
end

#cleanupObject



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

#contains_thread?(t) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#edit_labelsObject



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

#edit_messageObject



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

#forwardObject



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

#handle_added_update(sender, m) ⇒ Object



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

#handle_deleted_update(sender, m) ⇒ Object



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

#handle_labeled_update(sender, m) ⇒ Object



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

#handle_simple_update(sender, m) ⇒ Object



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

#handle_single_message_deleted_update(sender, m) ⇒ Object



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

#handle_single_message_labeled_update(sender, m) ⇒ Object



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

#handle_spammed_update(sender, m) ⇒ Object



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

#handle_undeleted_update(sender, m) ⇒ Object



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

def handle_undeleted_update sender, m
  add_or_unhide m
end

#is_relevant?(m) ⇒ Boolean

overwrite me!

Returns:

  • (Boolean)


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

def is_relevant? m; false; end

#join_threadsObject



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

#jump_to_next_newObject



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

#killObject



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

#launch_another_thread(thread, direction, &b) ⇒ Object



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

#launch_next_thread_after(thread, &b) ⇒ Object

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

#launch_prev_thread_before(thread, &b) ⇒ Object



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

#linesObject



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

def lines; @text.length; end

#load_all_threadsObject



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

def load_all_threads
  load_threads :num => -1
end

#load_n_threads(n = LOAD_MORE_THREAD_NUM, opts = {}) ⇒ Object

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

#load_n_threads_background(n = LOAD_MORE_THREAD_NUM, opts = {}) ⇒ Object



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

#load_threads(opts = {}) ⇒ Object



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

#multi_edit_labels(threads) ⇒ Object



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

#multi_join_threads(threads) ⇒ Object



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

#multi_kill(threads) ⇒ Object



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

#multi_select(threads) ⇒ Object



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

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

#multi_toggle_archived(threads) ⇒ Object



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

#multi_toggle_deleted(threads) ⇒ Object

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

#multi_toggle_new(threads) ⇒ Object



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

#multi_toggle_spam(threads) ⇒ Object

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

#multi_toggle_starred(threads) ⇒ Object



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

#multi_toggle_tagged(threads) ⇒ Object



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

#reloadObject



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

#replyObject



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

#resize(rows, cols) ⇒ Object



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

def resize rows, cols
  regen_text
  super
end

#save(background = true) ⇒ Object



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

#select(t = nil, when_done = nil) ⇒ Object

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

#statusObject



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

#tag_matchingObject



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

#toggle_archivedObject



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

#toggle_deletedObject



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

#toggle_newObject



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

#toggle_spamObject



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

#toggle_starredObject



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

#toggle_taggedObject



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

#toggle_tagged_allObject



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

#updateObject



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