sup 0.0.8 → 0.1

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 (57) hide show
  1. data/HACKING +6 -36
  2. data/History.txt +11 -0
  3. data/Manifest.txt +5 -0
  4. data/README.txt +13 -31
  5. data/Rakefile +3 -3
  6. data/bin/sup +167 -89
  7. data/bin/sup-add +39 -29
  8. data/bin/sup-config +57 -31
  9. data/bin/sup-sync +60 -54
  10. data/bin/sup-sync-back +143 -0
  11. data/doc/FAQ.txt +56 -19
  12. data/doc/Philosophy.txt +34 -33
  13. data/doc/TODO +76 -46
  14. data/doc/UserGuide.txt +142 -122
  15. data/lib/sup.rb +76 -36
  16. data/lib/sup/account.rb +27 -19
  17. data/lib/sup/buffer.rb +130 -44
  18. data/lib/sup/contact.rb +1 -1
  19. data/lib/sup/draft.rb +1 -2
  20. data/lib/sup/imap.rb +64 -19
  21. data/lib/sup/index.rb +95 -16
  22. data/lib/sup/keymap.rb +1 -1
  23. data/lib/sup/label.rb +31 -5
  24. data/lib/sup/maildir.rb +7 -5
  25. data/lib/sup/mbox.rb +34 -15
  26. data/lib/sup/mbox/loader.rb +30 -12
  27. data/lib/sup/mbox/ssh-loader.rb +7 -5
  28. data/lib/sup/message.rb +93 -44
  29. data/lib/sup/modes/buffer-list-mode.rb +1 -1
  30. data/lib/sup/modes/completion-mode.rb +55 -0
  31. data/lib/sup/modes/compose-mode.rb +6 -25
  32. data/lib/sup/modes/contact-list-mode.rb +1 -1
  33. data/lib/sup/modes/edit-message-mode.rb +119 -29
  34. data/lib/sup/modes/file-browser-mode.rb +108 -0
  35. data/lib/sup/modes/forward-mode.rb +3 -20
  36. data/lib/sup/modes/inbox-mode.rb +9 -12
  37. data/lib/sup/modes/label-list-mode.rb +28 -46
  38. data/lib/sup/modes/label-search-results-mode.rb +1 -16
  39. data/lib/sup/modes/line-cursor-mode.rb +44 -5
  40. data/lib/sup/modes/person-search-results-mode.rb +1 -16
  41. data/lib/sup/modes/reply-mode.rb +18 -31
  42. data/lib/sup/modes/resume-mode.rb +6 -6
  43. data/lib/sup/modes/scroll-mode.rb +6 -5
  44. data/lib/sup/modes/search-results-mode.rb +6 -17
  45. data/lib/sup/modes/thread-index-mode.rb +70 -28
  46. data/lib/sup/modes/thread-view-mode.rb +65 -29
  47. data/lib/sup/person.rb +71 -30
  48. data/lib/sup/poll.rb +13 -4
  49. data/lib/sup/rfc2047.rb +61 -0
  50. data/lib/sup/sent.rb +7 -5
  51. data/lib/sup/source.rb +12 -9
  52. data/lib/sup/suicide.rb +36 -0
  53. data/lib/sup/tagger.rb +6 -6
  54. data/lib/sup/textfield.rb +76 -14
  55. data/lib/sup/thread.rb +97 -123
  56. data/lib/sup/util.rb +167 -1
  57. metadata +30 -5
@@ -1,14 +1,14 @@
1
1
  module Redwood
2
2
 
3
- class ResumeMode < ComposeMode
3
+ class ResumeMode < EditMessageMode
4
4
  def initialize m
5
- super()
6
5
  @id = m.id
7
- @header, @body = parse_file m.draft_filename
8
- @header.delete "Date"
9
- @header["Message-Id"] = gen_message_id # generate a new'n
10
- regen_text
11
6
  @safe = false
7
+
8
+ header, body = parse_file m.draft_filename
9
+ header.delete "Date"
10
+
11
+ super :header => header, :body => body
12
12
  end
13
13
 
14
14
  def killable?
@@ -21,7 +21,7 @@ class ScrollMode < Mode
21
21
  k.add :col_right, "Right one column", :right, 'l'
22
22
  k.add :page_down, "Down one page", :page_down, 'n', ' '
23
23
  k.add :page_up, "Up one page", :page_up, 'p', :backspace
24
- k.add :jump_to_home, "Jump to top", :home, '^', '1'
24
+ k.add :jump_to_start, "Jump to top", :home, '^', '1'
25
25
  k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
26
26
  k.add :jump_to_left, "Jump to the left", '['
27
27
  end
@@ -76,17 +76,18 @@ class ScrollMode < Mode
76
76
  buffer.mark_dirty
77
77
  end
78
78
 
79
+ def at_top?; @topline == 0 end
80
+ def at_bottom?; @botline == lines end
81
+
79
82
  def line_down; jump_to_line @topline + 1; end
80
83
  def line_up; jump_to_line @topline - 1; end
81
84
  def page_down; jump_to_line @topline + buffer.content_height - @slip_rows; end
82
85
  def page_up; jump_to_line @topline - buffer.content_height + @slip_rows; end
83
- def jump_to_home; jump_to_line 0; end
86
+ def jump_to_start; jump_to_line 0; end
84
87
  def jump_to_end; jump_to_line lines - buffer.content_height; end
85
88
 
86
-
87
89
  def ensure_mode_validity
88
- @topline = @topline.clamp 0, lines - 1
89
- @topline = 0 if @topline < 0 # empty
90
+ @topline = @topline.clamp 0, [lines - 1, 0].max
90
91
  @botline = [@topline + buffer.content_height, lines].min
91
92
  end
92
93
 
@@ -3,26 +3,15 @@ module Redwood
3
3
  class SearchResultsMode < ThreadIndexMode
4
4
  def initialize qobj
5
5
  @qobj = qobj
6
- super
6
+ super [], { :qobj => @qobj, :load_killed => true, :load_spam => false }
7
7
  end
8
8
 
9
- ## TODO: think about this
10
- def is_relevant? m; super; end
9
+ ## a proper is_relevant? method requires some way of asking ferret
10
+ ## if an in-memory object satisfies a query. i'm not sure how to do
11
+ ## that yet. in the worst case i can make an in-memory index, add
12
+ ## the message, and search against it to see if i have > 0 results,
13
+ ## but that seems pretty insane.
11
14
 
12
- def load_threads opts={}
13
- n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
14
- load_n_threads_background n, :qobj => @qobj,
15
- :load_killed => true,
16
- :load_spam => false,
17
- :when_done =>(lambda do |num|
18
- opts[:when_done].call if opts[:when_done]
19
- if num > 0
20
- BufferManager.flash "Found #{num} threads"
21
- else
22
- BufferManager.flash "No matches"
23
- end
24
- end)
25
- end
26
15
  end
27
16
 
28
17
  end
@@ -1,7 +1,7 @@
1
- require 'thread'
2
1
  module Redwood
3
2
 
4
- ## subclasses should implement load_threads
3
+ ## subclasses should implement:
4
+ ## - is_relevant?
5
5
 
6
6
  class ThreadIndexMode < LineCursorMode
7
7
  DATE_WIDTH = Time::TO_NICE_S_MAX_LEN
@@ -27,21 +27,30 @@ class ThreadIndexMode < LineCursorMode
27
27
  k.add :apply_to_tagged, "Apply next command to all tagged threads", ';'
28
28
  end
29
29
 
30
- def initialize required_labels=[], hidden_labels=[]
30
+ def initialize hidden_labels=[], load_thread_opts={}
31
31
  super()
32
+ @mutex = Mutex.new
32
33
  @load_thread = nil
33
- @required_labels = required_labels
34
- @hidden_labels = hidden_labels + LabelManager::HIDDEN_LABELS
34
+ @load_thread_opts = load_thread_opts
35
+ @hidden_labels = hidden_labels + LabelManager::HIDDEN_RESERVED_LABELS
35
36
  @date_width = DATE_WIDTH
36
37
  @from_width = FROM_WIDTH
37
38
  @size_width = nil
38
-
39
+
39
40
  @tags = Tagger.new self
40
41
 
41
42
  initialize_threads
42
43
  update
43
44
 
44
45
  UpdateManager.register self
46
+
47
+ @last_load_more_size = nil
48
+ to_load_more do |size|
49
+ next if @last_load_more_size == 0
50
+ load_threads :num => 1, :background => false
51
+ load_threads :num => (size - 1),
52
+ :when_done => lambda { |num| @last_load_more_size = num }
53
+ end
45
54
  end
46
55
 
47
56
  def lines; @text.length; end
@@ -117,8 +126,8 @@ class ThreadIndexMode < LineCursorMode
117
126
 
118
127
  def update
119
128
  ## let's see you do THIS in python
120
- @threads = @ts.threads.select { |t| !@hidden_threads[t] && !t.has_label?(:killed) }.sort_by { |t| t.date }.reverse
121
- @size_width = (@threads.map { |t| t.size }.max || 0).num_digits
129
+ @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| t.date }.reverse
130
+ @size_width = (@threads.max_of { |t| t.size } || 0).num_digits
122
131
  regen_text
123
132
  end
124
133
 
@@ -192,9 +201,10 @@ class ThreadIndexMode < LineCursorMode
192
201
  end
193
202
 
194
203
  def jump_to_next_new
195
- n = ((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread }
196
- n = (0 ... curpos).find { |i| @threads[i].has_label? :unread } unless n
204
+ n = ((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread } || (0 ... curpos).find { |i| @threads[i].has_label? :unread }
197
205
  if n
206
+ ## jump there if necessary
207
+ jump_to_line n unless n >= topline && n < botline
198
208
  set_cursor_pos n
199
209
  else
200
210
  BufferManager.flash "No new messages"
@@ -241,12 +251,12 @@ class ThreadIndexMode < LineCursorMode
241
251
  end
242
252
 
243
253
  def save
244
- threads = @threads + @hidden_threads.keys
254
+ dirty_threads = (@threads + @hidden_threads.keys).select { |t| t.dirty? }
255
+ return if dirty_threads.empty?
245
256
 
246
257
  BufferManager.say("Saving threads...") do |say_id|
247
- threads.each_with_index do |t, i|
248
- next unless t.dirty?
249
- BufferManager.say "Saving thread #{i +1} of #{threads.length}...", say_id
258
+ dirty_threads.each_with_index do |t, i|
259
+ BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
250
260
  t.save Index
251
261
  end
252
262
  end
@@ -330,8 +340,8 @@ class ThreadIndexMode < LineCursorMode
330
340
  end
331
341
 
332
342
  def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
333
- return if @load_thread
334
- @load_thread = Redwood::reporting_thread do
343
+ return if @load_thread # todo: wrap in mutex
344
+ @load_thread = Redwood::reporting_thread do
335
345
  num = load_n_threads n, opts
336
346
  opts[:when_done].call(num) if opts[:when_done]
337
347
  @load_thread = nil
@@ -358,15 +368,35 @@ class ThreadIndexMode < LineCursorMode
358
368
  BufferManager.draw_screen
359
369
  @ts.size - orig_size
360
370
  end
371
+ synchronized :load_n_threads
361
372
 
362
373
  def status
363
374
  if (l = lines) == 0
364
- ""
375
+ "line 0 of 0"
365
376
  else
366
377
  "line #{curpos + 1} of #{l} #{dirty? ? '*modified*' : ''}"
367
378
  end
368
379
  end
369
380
 
381
+ def load_threads opts={}
382
+ n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
383
+
384
+ myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
385
+ opts[:when_done].call(num) if opts[:when_done]
386
+ if num > 0
387
+ BufferManager.flash "Found #{num} threads"
388
+ else
389
+ BufferManager.flash "No matches"
390
+ end
391
+ end)})
392
+
393
+ if opts[:background] || opts[:background].nil?
394
+ load_n_threads_background n, myopts
395
+ else
396
+ load_n_threads n, myopts
397
+ end
398
+ end
399
+
370
400
  protected
371
401
 
372
402
  def cursor_thread; @threads[curpos]; end
@@ -406,16 +436,20 @@ protected
406
436
  end
407
437
 
408
438
  def author_text_for_thread t
409
- if t.authors.size == 1
410
- t.authors.first.mediumname
411
- else
412
- t.authors.map { |p| AccountManager.is_account?(p) ? "me" : p.shortname }.join ", "
413
- end
439
+ t.authors.map do |p|
440
+ if AccountManager.is_account?(p)
441
+ "me"
442
+ elsif t.authors.size == 1
443
+ p.mediumname
444
+ else
445
+ p.shortname
446
+ end
447
+ end.uniq.join ","
414
448
  end
415
449
 
416
450
  def text_for_thread t
417
- date = t.date.to_nice_s(Time.now)
418
- from = author_text_for_thread(t)
451
+ date = t.date.to_nice_s
452
+ from = author_text_for_thread t
419
453
  if from.length > @from_width
420
454
  from = from[0 ... (@from_width - 1)]
421
455
  from += "." unless from[-1] == ?\s
@@ -427,12 +461,20 @@ protected
427
461
  dp = t.direct_participants.any? { |p| AccountManager.is_account? p }
428
462
  p = dp || t.participants.any? { |p| AccountManager.is_account? p }
429
463
 
430
- base_color = (new ? :index_new_color : :index_old_color)
464
+ base_color =
465
+ if new
466
+ :index_new_color
467
+ elsif starred
468
+ :index_starred_color
469
+ else
470
+ :index_old_color
471
+ end
472
+
431
473
  [
432
474
  [:tagged_color, @tags.tagged?(t) ? ">" : " "],
433
- [:none, sprintf("%#{@date_width}s ", date)],
475
+ [:none, sprintf("%#{@date_width}s", date)],
476
+ (starred ? [:starred_color, "*"] : [:none, " "]),
434
477
  [base_color, sprintf("%-#{@from_width}s", from)],
435
- [:starred_color, starred ? "*" : " "],
436
478
  [:none, t.size == 1 ? " " * (@size_width + 2) : sprintf("(%#{@size_width}d)", t.size)],
437
479
  [:to_me_color, dp ? " >" : (p ? ' +' : " ")],
438
480
  [base_color, t.subj + (t.subj.empty? ? "" : " ")],
@@ -447,7 +489,7 @@ protected
447
489
  private
448
490
 
449
491
  def initialize_threads
450
- @ts = ThreadSet.new Index.instance
492
+ @ts = ThreadSet.new Index.instance, $config[:thread_by_subject]
451
493
  @ts_mutex = Mutex.new
452
494
  @hidden_threads = {}
453
495
  end
@@ -2,10 +2,14 @@ module Redwood
2
2
 
3
3
  class ThreadViewMode < LineCursorMode
4
4
  ## this holds all info we need to lay out a message
5
- class Layout
5
+ class MessageLayout
6
6
  attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new
7
7
  end
8
8
 
9
+ class ChunkLayout
10
+ attr_accessor :state
11
+ end
12
+
9
13
  DATE_FORMAT = "%B %e %Y %l:%M%P"
10
14
  INDENT_SPACES = 2 # how many spaces to indent child messages
11
15
 
@@ -14,7 +18,7 @@ class ThreadViewMode < LineCursorMode
14
18
  k.add :show_header, "Show full message header", 'H'
15
19
  k.add :toggle_expanded, "Expand/collapse item", :enter
16
20
  k.add :expand_all_messages, "Expand/collapse all messages", 'E'
17
- k.add :edit_message, "Edit message (drafts only)", 'e'
21
+ k.add :edit_draft, "Edit draft", 'e'
18
22
  k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
19
23
  k.add :jump_to_next_open, "Jump to next open message", 'n'
20
24
  k.add :jump_to_prev_open, "Jump to previous open message", 'p'
@@ -30,27 +34,27 @@ class ThreadViewMode < LineCursorMode
30
34
  k.add :archive_and_kill, "Archive thread and kill buffer", 'A'
31
35
  end
32
36
 
33
- ## there are a couple important instance variables we hold to lay
34
- ## out the thread and to provide line-based functionality. @layout
35
- ## is a map from Message and Chunk objects to Layout objects. (for
36
- ## chunks, we only use the state field right now.) @message_lines is
37
- ## a map from row #s to Message objects. @chunk_lines is a map from
38
- ## row #s to Chunk objects. @person_lines is a map from row #s to
39
- ## Person objects.
37
+ ## there are a couple important instance variables we hold to format
38
+ ## the thread and to provide line-based functionality. @layout is a
39
+ ## map from Messages to MessageLayouts, and @chunk_layout from
40
+ ## Chunks to ChunkLayouts. @message_lines is a map from row #s to
41
+ ## Message objects. @chunk_lines is a map from row #s to Chunk
42
+ ## objects. @person_lines is a map from row #s to Person objects.
40
43
 
41
44
  def initialize thread, hidden_labels=[]
42
45
  super()
43
46
  @thread = thread
44
47
  @hidden_labels = hidden_labels
45
48
 
46
- @layout = {}
49
+ @layout = SavingHash.new { MessageLayout.new }
50
+ @chunk_layout = SavingHash.new { ChunkLayout.new }
47
51
  earliest, latest = nil, nil
48
52
  latest_date = nil
49
53
  altcolor = false
54
+
50
55
  @thread.each do |m, d, p|
51
56
  next unless m
52
57
  earliest ||= m
53
- @layout[m] = Layout.new
54
58
  @layout[m].state = initial_state_for m
55
59
  @layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
56
60
  @layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color
@@ -146,13 +150,19 @@ class ThreadViewMode < LineCursorMode
146
150
  def toggle_expanded
147
151
  chunk = @chunk_lines[curpos] or return
148
152
  case chunk
149
- when Message, Message::Quote, Message::Signature
150
- return if chunk.lines.length == 1 unless chunk.is_a? Message # too small to expand/close
153
+ when Message
151
154
  l = @layout[chunk]
152
155
  l.state = (l.state != :closed ? :closed : :open)
153
156
  cursor_down if l.state == :closed
157
+ when Message::Quote, Message::Signature
158
+ return if chunk.lines.length == 1
159
+ toggle_chunk_expansion chunk
154
160
  when Message::Attachment
155
- view_attachment chunk
161
+ if chunk.inlineable?
162
+ toggle_chunk_expansion chunk
163
+ else
164
+ view_attachment chunk
165
+ end
156
166
  end
157
167
  update
158
168
  end
@@ -169,7 +179,7 @@ class ThreadViewMode < LineCursorMode
169
179
  case chunk
170
180
  when Message::Attachment
171
181
  fn = BufferManager.ask :filename, "Save attachment to file: ", chunk.filename
172
- save_to_file(fn) { |f| f.print chunk } if fn
182
+ save_to_file(fn) { |f| f.print chunk.raw_content } if fn
173
183
  else
174
184
  m = @message_lines[curpos]
175
185
  fn = BufferManager.ask :filename, "Save message to file: "
@@ -177,11 +187,12 @@ class ThreadViewMode < LineCursorMode
177
187
  end
178
188
  end
179
189
 
180
- def edit_message
190
+ def edit_draft
181
191
  m = @message_lines[curpos] or return
182
192
  if m.is_draft?
183
193
  mode = ResumeMode.new m
184
194
  BufferManager.spawn "Edit message", mode
195
+ BufferManager.kill_buffer self.buffer
185
196
  mode.edit
186
197
  else
187
198
  BufferManager.flash "Not a draft message!"
@@ -241,27 +252,27 @@ class ThreadViewMode < LineCursorMode
241
252
  def expand_all_messages
242
253
  @global_message_state ||= :closed
243
254
  @global_message_state = (@global_message_state == :closed ? :open : :closed)
244
- @layout.each { |m, l| l.state = @global_message_state if m.is_a? Message }
255
+ @layout.each { |m, l| l.state = @global_message_state }
245
256
  update
246
257
  end
247
258
 
248
259
  def collapse_non_new_messages
249
- @layout.each { |m, l| l.state = l.orig_new ? :open : :closed if m.is_a? Message }
260
+ @layout.each { |m, l| l.state = l.orig_new ? :open : :closed }
250
261
  update
251
262
  end
252
263
 
253
264
  def expand_all_quotes
254
265
  if(m = @message_lines[curpos])
255
266
  quotes = m.chunks.select { |c| (c.is_a?(Message::Quote) || c.is_a?(Message::Signature)) && c.lines.length > 1 }
256
- numopen = quotes.inject(0) { |s, c| s + (@layout[c].state == :open ? 1 : 0) }
267
+ numopen = quotes.inject(0) { |s, c| s + (@chunk_layout[c].state == :open ? 1 : 0) }
257
268
  newstate = numopen > quotes.length / 2 ? :closed : :open
258
- quotes.each { |c| @layout[c].state = newstate }
269
+ quotes.each { |c| @chunk_layout[c].state = newstate }
259
270
  update
260
271
  end
261
272
  end
262
273
 
263
274
  def cleanup
264
- @layout = @text = nil # for good luck
275
+ @layout = @chunk_layout = @text = nil # for good luck
265
276
  end
266
277
 
267
278
  def archive_and_kill
@@ -272,6 +283,12 @@ class ThreadViewMode < LineCursorMode
272
283
 
273
284
  private
274
285
 
286
+ def toggle_chunk_expansion chunk
287
+ l = @chunk_layout[chunk]
288
+ l.state = (l.state != :closed ? :closed : :open)
289
+ cursor_down if l.state == :closed
290
+ end
291
+
275
292
  def initial_state_for m
276
293
  if m.has_label?(:starred) || m.has_label?(:unread)
277
294
  :open
@@ -300,10 +317,13 @@ private
300
317
  @text += chunk_to_lines m, nil, @text.length, depth, parent
301
318
  next
302
319
  end
303
- l = @layout[m] or next # TODO: figure out why this is nil sometimes
320
+ l = @layout[m]
321
+
322
+ ## is this still necessary?
323
+ next unless @layout[m].state # skip discarded drafts
304
324
 
305
325
  ## build the patina
306
- text = chunk_to_lines m, l.state, @text.length, depth, parent, @layout[m].color, @layout[m].star_color
326
+ text = chunk_to_lines m, l.state, @text.length, depth, parent, l.color, l.star_color
307
327
 
308
328
  l.top = @text.length
309
329
  l.bot = @text.length + text.length # updated below
@@ -322,10 +342,18 @@ private
322
342
 
323
343
  @text += text
324
344
  prevm = m
325
- if @layout[m].state != :closed
345
+ if l.state != :closed
326
346
  m.chunks.each do |c|
327
- cl = (@layout[c] ||= Layout.new)
328
- cl.state ||= :closed
347
+ cl = @chunk_layout[c]
348
+
349
+ ## set the default state for chunks
350
+ cl.state ||=
351
+ if c.is_a?(Message::Attachment) && c.inlineable?
352
+ :open
353
+ else
354
+ :closed
355
+ end
356
+
329
357
  text = chunk_to_lines c, cl.state, @text.length, depth
330
358
  (0 ... text.length).each do |i|
331
359
  @chunk_lines[@text.length + i] = c
@@ -422,7 +450,13 @@ private
422
450
  message_patina_lines(chunk, state, start, parent, prefix, color, star_color) +
423
451
  (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
424
452
  when Message::Attachment
425
- [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
453
+ return [[[:attachment_color, "#{prefix}x Attachment: #{chunk.filename} (#{chunk.content_type})"]]] unless chunk.inlineable?
454
+ case state
455
+ when :closed
456
+ [[[:attachment_color, "#{prefix}+ Attachment: #{chunk.filename} (#{chunk.lines.length} lines)"]]]
457
+ when :open
458
+ [[[:attachment_color, "#{prefix}- Attachment: #{chunk.filename} (#{chunk.lines.length} lines)"]]] + chunk.lines.map { |line| [[:none, "#{prefix}#{line}"]] }
459
+ end
426
460
  when Message::Text
427
461
  t = chunk.lines
428
462
  if t.last =~ /^\s*$/ && t.length > 1
@@ -455,9 +489,11 @@ private
455
489
  success = a.view!
456
490
  BufferManager.erase_flash
457
491
  BufferManager.completely_redraw_screen
458
- BufferManager.flash "Couldn't execute view command." unless success
492
+ unless success
493
+ BufferManager.spawn "Attachment: #{a.filename}", TextMode.new(a.to_s)
494
+ BufferManager.flash "Couldn't execute view command, viewing as text."
495
+ end
459
496
  end
460
-
461
497
  end
462
498
 
463
499
  end