sup 0.9.1 → 0.10

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (46) hide show
  1. data/CONTRIBUTORS +10 -6
  2. data/History.txt +11 -0
  3. data/ReleaseNotes +10 -0
  4. data/bin/sup +55 -19
  5. data/bin/sup-add +18 -8
  6. data/bin/sup-config +2 -2
  7. data/bin/sup-convert-ferret-index +84 -0
  8. data/bin/sup-dump +4 -3
  9. data/bin/sup-sync +4 -3
  10. data/bin/sup-sync-back +3 -2
  11. data/bin/sup-tweak-labels +3 -3
  12. data/lib/sup.rb +35 -4
  13. data/lib/sup/buffer.rb +12 -6
  14. data/lib/sup/colormap.rb +1 -0
  15. data/lib/sup/crypto.rb +76 -55
  16. data/lib/sup/ferret_index.rb +6 -1
  17. data/lib/sup/index.rb +62 -8
  18. data/lib/sup/logger.rb +2 -1
  19. data/lib/sup/maildir.rb +4 -2
  20. data/lib/sup/mbox/loader.rb +4 -3
  21. data/lib/sup/message-chunks.rb +9 -7
  22. data/lib/sup/message.rb +29 -27
  23. data/lib/sup/mode.rb +11 -4
  24. data/lib/sup/modes/buffer-list-mode.rb +5 -0
  25. data/lib/sup/modes/console-mode.rb +4 -0
  26. data/lib/sup/modes/edit-message-mode.rb +4 -2
  27. data/lib/sup/modes/file-browser-mode.rb +1 -1
  28. data/lib/sup/modes/inbox-mode.rb +18 -1
  29. data/lib/sup/modes/label-list-mode.rb +44 -3
  30. data/lib/sup/modes/text-mode.rb +1 -1
  31. data/lib/sup/modes/thread-index-mode.rb +63 -52
  32. data/lib/sup/modes/thread-view-mode.rb +68 -7
  33. data/lib/sup/poll.rb +20 -5
  34. data/lib/sup/source.rb +1 -0
  35. data/lib/sup/thread.rb +1 -1
  36. data/lib/sup/util.rb +49 -11
  37. data/lib/sup/xapian_index.rb +151 -112
  38. metadata +4 -10
  39. data/lib/sup/hook.rb.BACKUP.8625.rb +0 -158
  40. data/lib/sup/hook.rb.BACKUP.8681.rb +0 -158
  41. data/lib/sup/hook.rb.BASE.8625.rb +0 -155
  42. data/lib/sup/hook.rb.BASE.8681.rb +0 -155
  43. data/lib/sup/hook.rb.LOCAL.8625.rb +0 -142
  44. data/lib/sup/hook.rb.LOCAL.8681.rb +0 -142
  45. data/lib/sup/hook.rb.REMOTE.8625.rb +0 -145
  46. data/lib/sup/hook.rb.REMOTE.8681.rb +0 -145
@@ -29,7 +29,7 @@ class TextMode < ScrollMode
29
29
  end
30
30
 
31
31
  if output
32
- BufferManager.spawn "Output of '#{command}'", TextMode.new(output)
32
+ BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
33
33
  else
34
34
  BufferManager.flash "'#{command}' done!"
35
35
  end
@@ -37,7 +37,7 @@ EOS
37
37
  k.add :toggle_spam, "Mark/unmark thread as spam", 'S'
38
38
  k.add :toggle_deleted, "Delete/undelete thread", 'd'
39
39
  k.add :kill, "Kill thread (never to be seen in inbox again)", '&'
40
- k.add :save, "Save changes now", '$'
40
+ k.add :flush_index, "Flush all changes now", '$'
41
41
  k.add :jump_to_next_new, "Jump to next new thread", :tab
42
42
  k.add :reply, "Reply to latest message in a thread", 'r'
43
43
  k.add :reply_all, "Reply to all participants of the latest message in a thread", 'G'
@@ -66,7 +66,7 @@ EOS
66
66
  @date_width = DATE_WIDTH
67
67
 
68
68
  @interrupt_search = false
69
-
69
+
70
70
  initialize_threads # defines @ts and @ts_mutex
71
71
  update # defines @text and @lines
72
72
 
@@ -220,12 +220,14 @@ EOS
220
220
  end
221
221
 
222
222
  def update
223
+ old_cursor_thread = cursor_thread
223
224
  @mutex.synchronize do
224
225
  ## let's see you do THIS in python
225
226
  @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| [t.date, t.first.id] }.reverse
226
227
  @size_widgets = @threads.map { |t| size_widget_for_thread t }
227
228
  @size_widget_width = @size_widgets.max_of { |w| w.display_length }
228
229
  end
230
+ set_cursor_pos @threads.index(old_cursor_thread)||curpos
229
231
 
230
232
  regen_text
231
233
  end
@@ -266,15 +268,18 @@ EOS
266
268
  def toggle_starred
267
269
  t = cursor_thread or return
268
270
  undo = actually_toggle_starred t
269
- UndoManager.register "toggling thread starred status", undo
271
+ UndoManager.register "toggling thread starred status", undo, lambda { Index.save_thread t }
270
272
  update_text_for_line curpos
271
273
  cursor_down
274
+ Index.save_thread t
272
275
  end
273
276
 
274
277
  def multi_toggle_starred threads
275
278
  UndoManager.register "toggling #{threads.size.pluralize 'thread'} starred status",
276
- threads.map { |t| actually_toggle_starred t }
279
+ threads.map { |t| actually_toggle_starred t },
280
+ lambda { threads.each { |t| Index.save_thread t } }
277
281
  regen_text
282
+ threads.each { |t| Index.save_thread t }
278
283
  end
279
284
 
280
285
  ## returns an undo lambda
@@ -350,14 +355,18 @@ EOS
350
355
  def toggle_archived
351
356
  t = cursor_thread or return
352
357
  undo = actually_toggle_archived t
353
- UndoManager.register "deleting/undeleting thread #{t.first.id}", undo, lambda { update_text_for_line curpos }
358
+ UndoManager.register "deleting/undeleting thread #{t.first.id}", undo, lambda { update_text_for_line curpos },
359
+ lambda { Index.save_thread t }
354
360
  update_text_for_line curpos
361
+ Index.save_thread t
355
362
  end
356
363
 
357
364
  def multi_toggle_archived threads
358
365
  undos = threads.map { |t| actually_toggle_archived t }
359
- UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", undos, lambda { regen_text }
366
+ UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", undos, lambda { regen_text },
367
+ lambda { threads.each { |t| Index.save_thread t } }
360
368
  regen_text
369
+ threads.each { |t| Index.save_thread t }
361
370
  end
362
371
 
363
372
  def toggle_new
@@ -365,11 +374,13 @@ EOS
365
374
  t.toggle_label :unread
366
375
  update_text_for_line curpos
367
376
  cursor_down
377
+ Index.save_thread t
368
378
  end
369
379
 
370
380
  def multi_toggle_new threads
371
381
  threads.each { |t| t.toggle_label :unread }
372
382
  regen_text
383
+ threads.each { |t| Index.save_thread t }
373
384
  end
374
385
 
375
386
  def multi_toggle_tagged threads
@@ -385,6 +396,7 @@ EOS
385
396
 
386
397
  def multi_join_threads threads
387
398
  @ts.join_threads threads or return
399
+ threads.each { |t| Index.save_thread t }
388
400
  @tags.drop_all_tags # otherwise we have tag pointers to invalid threads!
389
401
  update
390
402
  end
@@ -399,14 +411,13 @@ EOS
399
411
  jump_to_line n unless n >= topline && n < botline
400
412
  set_cursor_pos n
401
413
  else
402
- BufferManager.flash "No new messages"
414
+ BufferManager.flash "No new messages."
403
415
  end
404
416
  end
405
417
 
406
418
  def toggle_spam
407
419
  t = cursor_thread or return
408
420
  multi_toggle_spam [t]
409
- HookManager.run("mark-as-spam", :thread => t)
410
421
  end
411
422
 
412
423
  ## both spam and deleted have the curious characteristic that you
@@ -418,9 +429,11 @@ EOS
418
429
  ## you also want them to disappear immediately.
419
430
  def multi_toggle_spam threads
420
431
  undos = threads.map { |t| actually_toggle_spammed t }
432
+ threads.each { |t| HookManager.run("mark-as-spam", :thread => t) }
421
433
  UndoManager.register "marking/unmarking #{threads.size.pluralize 'thread'} as spam",
422
- undos, lambda { regen_text }
434
+ undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
423
435
  regen_text
436
+ threads.each { |t| Index.save_thread t }
424
437
  end
425
438
 
426
439
  def toggle_deleted
@@ -432,8 +445,9 @@ EOS
432
445
  def multi_toggle_deleted threads
433
446
  undos = threads.map { |t| actually_toggle_deleted t }
434
447
  UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}",
435
- undos, lambda { regen_text }
448
+ undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
436
449
  regen_text
450
+ threads.each { |t| Index.save_thread t }
437
451
  end
438
452
 
439
453
  def kill
@@ -441,12 +455,19 @@ EOS
441
455
  multi_kill [t]
442
456
  end
443
457
 
458
+ def flush_index
459
+ @flush_id = BufferManager.say "Flushing index..."
460
+ Index.save_index
461
+ BufferManager.clear @flush_id
462
+ end
463
+
444
464
  ## m-m-m-m-MULTI-KILL
445
465
  def multi_kill threads
446
466
  UndoManager.register "killing #{threads.size.pluralize 'thread'}" do
447
467
  threads.each do |t|
448
468
  t.remove_label :killed
449
469
  add_or_unhide t.first
470
+ Index.save_thread t
450
471
  end
451
472
  regen_text
452
473
  end
@@ -458,29 +479,7 @@ EOS
458
479
 
459
480
  regen_text
460
481
  BufferManager.flash "#{threads.size.pluralize 'thread'} killed."
461
- end
462
-
463
- def save background=true
464
- if background
465
- Redwood::reporting_thread("saving thread") { actually_save }
466
- else
467
- actually_save
468
- end
469
- end
470
-
471
- def actually_save
472
- @save_thread_mutex.synchronize do
473
- BufferManager.say("Saving contacts...") { ContactManager.instance.save }
474
- dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
475
- next if dirty_threads.empty?
476
-
477
- BufferManager.say("Saving threads...") do |say_id|
478
- dirty_threads.each_with_index do |t, i|
479
- BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
480
- t.save_state Index
481
- end
482
- end
483
- end
482
+ threads.each { |t| Index.save_thread t }
484
483
  end
485
484
 
486
485
  def cleanup
@@ -492,7 +491,8 @@ EOS
492
491
  sleep 0.1 # TODO: necessary?
493
492
  BufferManager.erase_flash
494
493
  end
495
- save false
494
+ dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
495
+ fail "dirty threads remain" unless dirty_threads.empty?
496
496
  super
497
497
  end
498
498
 
@@ -543,9 +543,11 @@ EOS
543
543
  thread.labels = old_labels
544
544
  update_text_for_line pos
545
545
  UpdateManager.relay self, :labeled, thread.first
546
+ Index.save_thread thread
546
547
  end
547
548
 
548
549
  UpdateManager.relay self, :labeled, thread.first
550
+ Index.save_thread thread
549
551
  end
550
552
 
551
553
  def multi_edit_labels threads
@@ -579,9 +581,12 @@ EOS
579
581
  threads.zip(old_labels).map do |t, old_labels|
580
582
  t.labels = old_labels
581
583
  UpdateManager.relay self, :labeled, t.first
584
+ Index.save_thread t
582
585
  end
583
586
  regen_text
584
587
  end
588
+
589
+ threads.each { |t| Index.save_thread t }
585
590
  end
586
591
 
587
592
  def reply type_arg=nil
@@ -759,30 +764,36 @@ protected
759
764
  @lines = threads.map_with_index { |t, i| [t, i] }.to_h
760
765
  buffer.mark_dirty if buffer
761
766
  end
762
-
767
+
763
768
  def authors; map { |m, *o| m.from if m }.compact.uniq; end
764
769
 
770
+ ## preserve author order from the thread
765
771
  def author_names_and_newness_for_thread t, limit=nil
766
772
  new = {}
767
- authors = Set.new
768
- t.each do |m, *o|
769
- next unless m
770
- break if limit and authors.size >= limit
771
-
772
- name =
773
- if AccountManager.is_account?(m.from)
774
- "me"
775
- elsif t.authors.size == 1
776
- m.from.mediumname
777
- else
778
- m.from.shortname
779
- end
773
+ seen = {}
774
+ authors = t.map do |m, *o|
775
+ next unless m && m.from
776
+ new[m.from] ||= m.has_label?(:unread)
777
+ next if seen[m.from]
778
+ seen[m.from] = true
779
+ m.from
780
+ end.compact
781
+
782
+ result = []
783
+ authors.each do |a|
784
+ break if limit && result.size >= limit
785
+ name = if AccountManager.is_account?(a)
786
+ "me"
787
+ elsif t.authors.size == 1
788
+ a.mediumname
789
+ else
790
+ a.shortname
791
+ end
780
792
 
781
- new[name] ||= m.has_label?(:unread)
782
- authors << name
793
+ result << [name, new[a]]
783
794
  end
784
795
 
785
- authors.to_a.map { |a| [a, new[a]] }
796
+ result
786
797
  end
787
798
 
788
799
  AUTHOR_LIMIT = 5
@@ -843,7 +854,7 @@ protected
843
854
 
844
855
  [
845
856
  [:tagged_color, @tags.tagged?(t) ? ">" : " "],
846
- [:none, sprintf("%#{@date_width}s", date)],
857
+ [:date_color, sprintf("%#{@date_width}s", date)],
847
858
  (starred ? [:starred_color, "*"] : [:none, " "]),
848
859
  ] +
849
860
  from +
@@ -58,6 +58,7 @@ EOS
58
58
  k.add :alias, "Edit alias/nickname for a person", 'i'
59
59
  k.add :edit_as_new, "Edit message as new", 'D'
60
60
  k.add :save_to_disk, "Save message/attachment to disk", 's'
61
+ k.add :save_all_to_disk, "Save all attachments to disk", 'A'
61
62
  k.add :search, "Search for messages from particular people", 'S'
62
63
  k.add :compose, "Compose message to person", 'm'
63
64
  k.add :subscribe_to_list, "Subscribe to/unsubscribe from mailing list", "("
@@ -66,6 +67,7 @@ EOS
66
67
 
67
68
  k.add :archive_and_next, "Archive this thread, kill buffer, and view next", 'a'
68
69
  k.add :delete_and_next, "Delete this thread, kill buffer, and view next", 'd'
70
+ k.add :toggle_wrap, "Toggle wrapping of text", 'w'
69
71
 
70
72
  k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read:", '.' do |kk|
71
73
  kk.add :archive_and_kill, "Archive this thread and kill buffer", 'a'
@@ -127,11 +129,19 @@ EOS
127
129
  end
128
130
  end
129
131
 
132
+ @wrap = true
133
+
130
134
  @layout[latest].state = :open if @layout[latest].state == :closed
131
135
  @layout[earliest].state = :detailed if earliest.has_label?(:unread) || @thread.size == 1
132
136
 
133
137
  @thread.remove_label :unread
138
+ Index.save_thread @thread
139
+ end
140
+
141
+ def toggle_wrap
142
+ @wrap = !@wrap
134
143
  regen_text
144
+ buffer.mark_dirty if buffer
135
145
  end
136
146
 
137
147
  def draw_line ln, opts={}
@@ -144,17 +154,25 @@ EOS
144
154
  def lines; @text.length; end
145
155
  def [] i; @text[i]; end
146
156
 
157
+ ## a little hacky---since regen_text can depend on buffer features like the
158
+ ## content_width, we don't call it in the constructor, and instead call it
159
+ ## here, which is set before we're responsible for drawing ourself.
160
+ def buffer= b
161
+ super
162
+ regen_text
163
+ end
164
+
147
165
  def show_header
148
166
  m = @message_lines[curpos] or return
149
167
  BufferManager.spawn_unless_exists("Full header for #{m.id}") do
150
- TextMode.new m.raw_header
168
+ TextMode.new m.raw_header.ascii
151
169
  end
152
170
  end
153
171
 
154
172
  def show_message
155
173
  m = @message_lines[curpos] or return
156
174
  BufferManager.spawn_unless_exists("Raw message for #{m.id}") do
157
- TextMode.new m.raw_message
175
+ TextMode.new m.raw_message.ascii
158
176
  end
159
177
  end
160
178
 
@@ -258,8 +276,10 @@ EOS
258
276
  new_labels.each { |l| LabelManager << l }
259
277
  update
260
278
  UpdateManager.relay self, :labeled, @thread.first
279
+ Index.save_thread @thread
261
280
  UndoManager.register "labeling thread" do
262
281
  @thread.labels = old_labels
282
+ Index.save_thread @thread
263
283
  UpdateManager.relay self, :labeled, @thread.first
264
284
  end
265
285
  end
@@ -284,6 +304,7 @@ EOS
284
304
  ## star to the display
285
305
  update
286
306
  UpdateManager.relay self, :single_message_labeled, m
307
+ Index.save_thread @thread
287
308
  end
288
309
 
289
310
  ## called when someone presses enter when the cursor is highlighting
@@ -326,8 +347,10 @@ EOS
326
347
  chunk = @chunk_lines[curpos] or return
327
348
  case chunk
328
349
  when Chunk::Attachment
329
- default_dir = File.join(($config[:default_attachment_save_dir] || "."), chunk.filename)
330
- fn = BufferManager.ask_for_filename :filename, "Save attachment to file: ", default_dir
350
+ default_dir = $config[:default_attachment_save_dir]
351
+ default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
352
+ default_fn = File.expand_path File.join(default_dir, chunk.filename)
353
+ fn = BufferManager.ask_for_filename :filename, "Save attachment to file: ", default_fn
331
354
  save_to_file(fn) { |f| f.print chunk.raw_content } if fn
332
355
  else
333
356
  m = @message_lines[curpos]
@@ -339,6 +362,32 @@ EOS
339
362
  end
340
363
  end
341
364
 
365
+ def save_all_to_disk
366
+ m = @message_lines[curpos] or return
367
+ default_dir = ($config[:default_attachment_save_dir] || ".")
368
+ folder = BufferManager.ask_for_filename :filename, "Save all attachments to folder: ", default_dir, true
369
+ return unless folder
370
+
371
+ num = 0
372
+ num_errors = 0
373
+ m.chunks.each do |chunk|
374
+ next unless chunk.is_a?(Chunk::Attachment)
375
+ fn = File.join(folder, chunk.filename)
376
+ num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
377
+ num += 1
378
+ end
379
+
380
+ if num == 0
381
+ BufferManager.flash "Didn't find any attachments!"
382
+ else
383
+ if num_errors == 0
384
+ BufferManager.flash "Wrote #{num.pluralize 'attachment'} to #{folder}."
385
+ else
386
+ BufferManager.flash "Wrote #{(num - num_errors).pluralize 'attachment'} to #{folder}; couldn't write #{num_errors} of them (see log)."
387
+ end
388
+ end
389
+ end
390
+
342
391
  def edit_draft
343
392
  m = @message_lines[curpos] or return
344
393
  if m.is_draft?
@@ -476,8 +525,10 @@ EOS
476
525
  dispatch op do
477
526
  @thread.remove_label :inbox
478
527
  UpdateManager.relay self, :archived, @thread.first
528
+ Index.save_thread @thread
479
529
  UndoManager.register "archiving 1 thread" do
480
530
  @thread.apply_label :inbox
531
+ Index.save_thread @thread
481
532
  UpdateManager.relay self, :unarchived, @thread.first
482
533
  end
483
534
  end
@@ -487,8 +538,10 @@ EOS
487
538
  dispatch op do
488
539
  @thread.apply_label :spam
489
540
  UpdateManager.relay self, :spammed, @thread.first
541
+ Index.save_thread @thread
490
542
  UndoManager.register "marking 1 thread as spam" do
491
543
  @thread.remove_label :spam
544
+ Index.save_thread @thread
492
545
  UpdateManager.relay self, :unspammed, @thread.first
493
546
  end
494
547
  end
@@ -498,8 +551,10 @@ EOS
498
551
  dispatch op do
499
552
  @thread.apply_label :deleted
500
553
  UpdateManager.relay self, :deleted, @thread.first
554
+ Index.save_thread @thread
501
555
  UndoManager.register "deleting 1 thread" do
502
556
  @thread.remove_label :deleted
557
+ Index.save_thread @thread
503
558
  UpdateManager.relay self, :undeleted, @thread.first
504
559
  end
505
560
  end
@@ -509,6 +564,7 @@ EOS
509
564
  dispatch op do
510
565
  @thread.apply_label :unread
511
566
  UpdateManager.relay self, :unread, @thread.first
567
+ Index.save_thread @thread
512
568
  end
513
569
  end
514
570
 
@@ -557,7 +613,7 @@ EOS
557
613
  end
558
614
 
559
615
  if output
560
- BufferManager.spawn "Output of '#{command}'", TextMode.new(output)
616
+ BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
561
617
  else
562
618
  BufferManager.flash "'#{command}' done!"
563
619
  end
@@ -734,7 +790,12 @@ private
734
790
  else
735
791
  raise "Bad chunk: #{chunk.inspect}" unless chunk.respond_to?(:inlineable?) ## debugging
736
792
  if chunk.inlineable?
737
- chunk.lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
793
+ lines = chunk.lines
794
+ if @wrap
795
+ width = buffer.content_width
796
+ lines = lines.map { |l| l.chomp.wrap width }.flatten
797
+ end
798
+ lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
738
799
  elsif chunk.expandable?
739
800
  case state
740
801
  when :closed
@@ -754,7 +815,7 @@ private
754
815
  BufferManager.erase_flash
755
816
  BufferManager.completely_redraw_screen
756
817
  unless success
757
- BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s, chunk.filename)
818
+ BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s.ascii, chunk.filename)
758
819
  BufferManager.flash "Couldn't execute view command, viewing as text."
759
820
  end
760
821
  end