sup 0.0.6 → 0.0.7

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.

@@ -1,7 +1,13 @@
1
1
  module Redwood
2
2
 
3
3
  class ThreadViewMode < LineCursorMode
4
+ ## this holds all info we need to lay out a message
5
+ class Layout
6
+ attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color
7
+ end
8
+
4
9
  DATE_FORMAT = "%B %e %Y %l:%M%P"
10
+ INDENT_SPACES = 2 # how many spaces to indent child messages
5
11
 
6
12
  register_keymap do |k|
7
13
  k.add :toggle_detailed_header, "Toggle detailed header", 'd'
@@ -16,34 +22,47 @@ class ThreadViewMode < LineCursorMode
16
22
  k.add :collapse_non_new_messages, "Collapse all but new messages", 'N'
17
23
  k.add :reply, "Reply to a message", 'r'
18
24
  k.add :forward, "Forward a message", 'f'
25
+ k.add :alias, "Edit alias/nickname for a person", 'a'
26
+ k.add :edit_as_new, "Edit message as new", 'D'
19
27
  k.add :save_to_disk, "Save message/attachment to disk", 's'
28
+ k.add :search, "Search for messages from particular people", 'S'
29
+ k.add :archive_and_kill, "Archive thread and kill buffer", 'A'
20
30
  end
21
31
 
32
+ ## there are a couple important instance variables we hold to lay
33
+ ## out the thread and to provide line-based functionality. @layout
34
+ ## is a map from Message and Chunk objects to Layout objects. (for
35
+ ## chunks, we only use the state field right now.) @message_lines is
36
+ ## a map from row #s to Message objects. @chunk_lines is a map from
37
+ ## row #s to Chunk objects. @person_lines is a map from row #s to
38
+ ## Person objects.
39
+
22
40
  def initialize thread, hidden_labels=[]
23
41
  super()
24
42
  @thread = thread
25
- @state = {}
26
43
  @hidden_labels = hidden_labels
27
44
 
45
+ @layout = {}
28
46
  earliest, latest = nil, nil
29
47
  latest_date = nil
48
+ altcolor = false
30
49
  @thread.each do |m, d, p|
31
50
  next unless m
32
51
  earliest ||= m
33
- @state[m] = initial_state_for m
52
+ @layout[m] = Layout.new
53
+ @layout[m].state = initial_state_for m
54
+ @layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
55
+ altcolor = !altcolor
34
56
  if latest_date.nil? || m.date > latest_date
35
57
  latest_date = m.date
36
58
  latest = m
37
59
  end
38
60
  end
39
61
 
40
- @state[latest] = :open if @state[latest] == :closed
41
- @state[earliest] = :detailed if earliest.has_label?(:unread) || @thread.size == 1
62
+ @layout[latest].state = :open if @layout[latest].state == :closed
63
+ @layout[earliest].state = :detailed if earliest.has_label?(:unread) || @thread.size == 1
42
64
 
43
- BufferManager.say "Loading message bodies..." do
44
- regen_chunks
45
- regen_text
46
- end
65
+ regen_text
47
66
  end
48
67
 
49
68
  def draw_line ln, opts={}
@@ -57,33 +76,47 @@ class ThreadViewMode < LineCursorMode
57
76
  def [] i; @text[i]; end
58
77
 
59
78
  def show_header
60
- return unless(m = @message_lines[curpos])
79
+ m = @message_lines[curpos] or return
61
80
  BufferManager.spawn_unless_exists("Full header") do
62
81
  TextMode.new m.raw_header
63
82
  end
64
83
  end
65
84
 
66
85
  def toggle_detailed_header
67
- return unless(m = @message_lines[curpos])
68
- @state[m] = (@state[m] == :detailed ? :open : :detailed)
86
+ m = @message_lines[curpos] or return
87
+ @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
69
88
  update
70
89
  end
71
90
 
72
91
  def reply
73
- return unless(m = @message_lines[curpos])
92
+ m = @message_lines[curpos] or return
74
93
  mode = ReplyMode.new m
75
94
  BufferManager.spawn "Reply to #{m.subj}", mode
76
95
  end
77
96
 
78
97
  def forward
79
- return unless(m = @message_lines[curpos])
98
+ m = @message_lines[curpos] or return
80
99
  mode = ForwardMode.new m
81
100
  BufferManager.spawn "Forward of #{m.subj}", mode
82
101
  mode.edit
83
102
  end
84
103
 
104
+ include CanAliasContacts
105
+ def alias
106
+ p = @person_lines[curpos] or return
107
+ alias_contact p
108
+ update
109
+ end
110
+
111
+ def search
112
+ p = @person_lines[curpos] or return
113
+ mode = PersonSearchResultsMode.new [p]
114
+ BufferManager.spawn "search for #{p.name}", mode
115
+ mode.load_threads :num => mode.buffer.content_height
116
+ end
117
+
85
118
  def toggle_starred
86
- return unless(m = @message_lines[curpos])
119
+ m = @message_lines[curpos] or return
87
120
  if m.has_label? :starred
88
121
  m.remove_label :starred
89
122
  else
@@ -92,49 +125,45 @@ class ThreadViewMode < LineCursorMode
92
125
  ## TODO: don't recalculate EVERYTHING just to add a stupid little
93
126
  ## star to the display
94
127
  update
95
- UpdateManager.relay :starred, m
128
+ UpdateManager.relay self, :starred, m
96
129
  end
97
130
 
98
131
  def toggle_expanded
99
- return unless(chunk = @chunk_lines[curpos])
132
+ chunk = @chunk_lines[curpos] or return
100
133
  case chunk
101
134
  when Message, Message::Quote, Message::Signature
102
- @state[chunk] = (@state[chunk] != :closed ? :closed : :open)
103
- cursor_down if @state[chunk] == :closed
135
+ return if chunk.lines.length == 1 unless chunk.is_a? Message # too small to expand/close
136
+ l = @layout[chunk]
137
+ l.state = (l.state != :closed ? :closed : :open)
138
+ cursor_down if l.state == :closed
104
139
  when Message::Attachment
105
140
  view_attachment chunk
106
141
  end
107
142
  update
108
143
  end
109
144
 
110
- def save fn
111
- if File.exists? fn
112
- return unless BufferManager.ask_yes_or_no "File exists. Overwrite?"
113
- end
114
- begin
115
- File.open(fn, "w") { |f| yield f }
116
- BufferManager.flash "Successfully wrote #{fn}."
117
- rescue SystemCallError => e
118
- BufferManager.flash "Error writing to file: #{e.message}"
119
- end
145
+ def edit_as_new
146
+ m = @message_lines[curpos] or return
147
+ mode = ComposeMode.new(:body => m.basic_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc)
148
+ BufferManager.spawn "edit as new", mode
149
+ mode.edit
120
150
  end
121
- private :save
122
151
 
123
152
  def save_to_disk
124
- return unless(chunk = @chunk_lines[curpos])
153
+ chunk = @chunk_lines[curpos] or return
125
154
  case chunk
126
155
  when Message::Attachment
127
- fn = BufferManager.ask :filename, "save attachment to file: ", chunk.filename
128
- save(fn) { |f| f.print chunk } if fn
156
+ fn = BufferManager.ask :filename, "Save attachment to file: ", chunk.filename
157
+ save_to_file(fn) { |f| f.print chunk } if fn
129
158
  else
130
159
  m = @message_lines[curpos]
131
- fn = BufferManager.ask :filename, "save message to file: "
132
- save(fn) { |f| f.print m.raw_full_message } if fn
160
+ fn = BufferManager.ask :filename, "Save message to file: "
161
+ save_to_file(fn) { |f| f.print m.raw_full_message } if fn
133
162
  end
134
163
  end
135
164
 
136
165
  def edit_message
137
- return unless(m = @message_lines[curpos])
166
+ m = @message_lines[curpos] or return
138
167
  if m.is_draft?
139
168
  mode = ResumeMode.new m
140
169
  BufferManager.spawn "Edit message", mode
@@ -144,23 +173,33 @@ class ThreadViewMode < LineCursorMode
144
173
  end
145
174
  end
146
175
 
176
+ def jump_to_first_open
177
+ m = @message_lines[0] or return
178
+ if @layout[m].state != :closed
179
+ jump_to_message m
180
+ else
181
+ jump_to_next_open
182
+ end
183
+ end
184
+
147
185
  def jump_to_next_open
148
- return unless(m = @message_lines[curpos])
149
- while nextm = @messages[m][3]
150
- break if @state[nextm] == :open
186
+ m = @message_lines[curpos] or return
187
+ while nextm = @layout[m].next
188
+ break if @layout[nextm].state != :closed
151
189
  m = nextm
152
190
  end
153
191
  jump_to_message nextm if nextm
154
192
  end
155
193
 
156
194
  def jump_to_prev_open
157
- return unless(m = @message_lines[curpos])
195
+ m = @message_lines[curpos] or return
158
196
  ## jump to the top of the current message if we're in the body;
159
197
  ## otherwise, to the previous message
160
- top = @messages[m][0]
198
+
199
+ top = @layout[m].top
161
200
  if curpos == top
162
- while(prevm = @messages[m][2])
163
- break if @state[prevm] != :closed
201
+ while(prevm = @layout[m].prev)
202
+ break if @layout[prevm].state != :closed
164
203
  m = prevm
165
204
  end
166
205
  jump_to_message prevm if prevm
@@ -170,47 +209,52 @@ class ThreadViewMode < LineCursorMode
170
209
  end
171
210
 
172
211
  def jump_to_message m
173
- top, bot, prevm, nextm, depth = @messages[m]
174
- jump_to_line top unless top >= topline &&
175
- top <= botline && bot >= topline && bot <= botline
176
- jump_to_col depth * 2 # sorry!!!! TODO: make this a constant
177
- set_cursor_pos top
212
+ l = @layout[m]
213
+ left = l.depth * INDENT_SPACES
214
+ right = left + l.width
215
+
216
+ ## jump to the top line unless both top and bottom fit in the current view
217
+ jump_to_line l.top unless l.top >= topline && l.top <= botline && l.bot >= topline && l.bot <= botline
218
+
219
+ ## jump to the left columns unless both left and right fit in the current view
220
+ jump_to_col left unless left >= leftcol && left <= rightcol && right >= leftcol && right <= rightcol
221
+
222
+ ## either way, move the cursor to the first line
223
+ set_cursor_pos l.top
178
224
  end
179
225
 
180
226
  def expand_all_messages
181
227
  @global_message_state ||= :closed
182
228
  @global_message_state = (@global_message_state == :closed ? :open : :closed)
183
- @state.each { |m, v| @state[m] = @global_message_state if m.is_a? Message }
229
+ @layout.each { |m, l| l.state = @global_message_state if m.is_a? Message }
184
230
  update
185
231
  end
186
232
 
187
-
188
233
  def collapse_non_new_messages
189
- @messages.each { |m, v| @state[m] = m.has_label?(:unread) ? :open : :closed }
234
+ @layout.each { |m, l| l.state = m.has_label?(:unread) ? :open : :closed }
190
235
  update
191
236
  end
192
237
 
193
238
  def expand_all_quotes
194
239
  if(m = @message_lines[curpos])
195
- quotes = @chunks[m].select { |c| c.is_a?(Message::Quote) || c.is_a?(Message::Signature) }
196
- open, closed = quotes.partition { |c| @state[c] == :open }
197
- newstate = open.length > closed.length ? :closed : :open
198
- quotes.each { |c| @state[c] = newstate }
240
+ quotes = m.chunks.select { |c| (c.is_a?(Message::Quote) || c.is_a?(Message::Signature)) && c.lines.length > 1 }
241
+ numopen = quotes.inject(0) { |s, c| s + (@layout[c].state == :open ? 1 : 0) }
242
+ newstate = numopen > quotes.length / 2 ? :closed : :open
243
+ quotes.each { |c| @layout[c].state = newstate }
199
244
  update
200
245
  end
201
246
  end
202
247
 
203
- ## kinda slow for large threads. TODO: fasterify
204
248
  def cleanup
205
- BufferManager.say "Marking messages as read..." do
206
- @thread.each do |m, d, p|
207
- if m && m.has_label?(:unread)
208
- m.remove_label :unread
209
- UpdateManager.relay :read, m
210
- end
211
- end
212
- end
213
- @messages = @chunks = @text = nil
249
+ @thread.remove_label :unread
250
+ UpdateManager.relay self, :read, @thread
251
+ @layout = @text = nil
252
+ end
253
+
254
+ def archive_and_kill
255
+ @thread.remove_label :inbox
256
+ UpdateManager.relay self, :archived, @thread
257
+ BufferManager.kill_buffer_safely buffer
214
258
  end
215
259
 
216
260
  private
@@ -228,116 +272,141 @@ private
228
272
  buffer.mark_dirty if buffer
229
273
  end
230
274
 
231
- def regen_chunks
232
- @chunks = {}
233
- @thread.each { |m, d, p| @chunks[m] = m.to_chunks if m.is_a?(Message) }
234
- end
235
-
275
+ ## here we generate the actual content lines. we accumulate
276
+ ## everything into @text, and we set @chunk_lines and
277
+ ## @message_lines, and we update @layout.
236
278
  def regen_text
237
279
  @text = []
238
280
  @chunk_lines = []
239
281
  @message_lines = []
240
- @messages = {}
282
+ @person_lines = []
241
283
 
242
- prev_m = nil
284
+ prevm = nil
243
285
  @thread.each do |m, depth, parent|
244
- ## we're occasionally called on @threads that have had messages
245
- ## added to them since initialization. luckily we regen_text on
246
- ## the entire thread every time the user does anything besides
247
- ## scrolling (basically), so we can just slap this on here.
248
- ##
249
- ## to pick nits, the niceness that i do in the constructor with
250
- ## 'latest' might not be valid, but i don't see that as a huge
251
- ## issue.
252
- @state[m] ||= initial_state_for m if m
253
-
254
- text = chunk_to_lines m, @state[m], @text.length, depth, parent
286
+ unless m.is_a? Message # handle nil and :fake_root
287
+ @text += chunk_to_lines m, nil, @text.length, depth, parent
288
+ next
289
+ end
290
+ l = @layout[m] or next # TODO: figure out why this is nil sometimes
291
+
292
+ ## build the patina
293
+ text = chunk_to_lines m, l.state, @text.length, depth, parent, @layout[m].color
294
+
295
+ l.top = @text.length
296
+ l.bot = @text.length + text.length # updated below
297
+ l.prev = prevm
298
+ l.next = nil
299
+ l.depth = depth
300
+ # l.state we preserve
301
+ l.width = 0 # updated below
302
+ @layout[l.prev].next = m if l.prev
303
+
255
304
  (0 ... text.length).each do |i|
256
305
  @chunk_lines[@text.length + i] = m
257
306
  @message_lines[@text.length + i] = m
307
+ lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum
258
308
  end
259
309
 
260
- ## sorry i store all this shit in an array. very, very sorry.
261
- ## also sorry about the * 2. very, very sorry.
262
- @messages[m] = [@text.length, @text.length + text.length, prev_m, nil, depth]
263
- @messages[prev_m][3] = m if prev_m
264
- prev_m = m if m.is_a? Message
265
-
266
310
  @text += text
267
- if @state[m] != :closed && @chunks.member?(m)
268
- @chunks[m].each do |c|
269
- @state[c] ||= :closed
270
- text = chunk_to_lines c, @state[c], @text.length, depth
311
+ prevm = m
312
+ if @layout[m].state != :closed
313
+ m.chunks.each do |c|
314
+ cl = (@layout[c] ||= Layout.new)
315
+ cl.state ||= :closed
316
+ text = chunk_to_lines c, cl.state, @text.length, depth
271
317
  (0 ... text.length).each do |i|
272
318
  @chunk_lines[@text.length + i] = c
273
319
  @message_lines[@text.length + i] = m
320
+ lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum - (depth * INDENT_SPACES)
321
+ l.width = lw if lw > l.width
274
322
  end
275
323
  @text += text
276
324
  end
277
- @messages[m][1] = @text.length
325
+ @layout[m].bot = @text.length
278
326
  end
279
327
  end
280
328
  end
281
329
 
282
- def message_patina_lines m, state, parent, prefix
283
- prefix_widget = [:message_patina_color, prefix]
330
+ def message_patina_lines m, state, start, parent, prefix, color
331
+ prefix_widget = [color, prefix]
284
332
  widget =
285
333
  case state
286
334
  when :closed
287
- [:message_patina_color, "+ "]
335
+ [color, "+ "]
288
336
  when :open, :detailed
289
- [:message_patina_color, "- "]
337
+ [color, "- "]
290
338
  end
291
339
  imp_widget =
292
340
  if m.has_label?(:starred)
293
341
  [:starred_patina_color, "* "]
294
342
  else
295
- [:message_patina_color, " "]
343
+ [color, " "]
296
344
  end
297
345
 
298
346
  case state
299
347
  when :open
348
+ @person_lines[start] = m.from
300
349
  [[prefix_widget, widget, imp_widget,
301
- [:message_patina_color,
350
+ [color,
302
351
  "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
352
+
303
353
  when :closed
354
+ @person_lines[start] = m.from
304
355
  [[prefix_widget, widget, imp_widget,
305
- [:message_patina_color,
356
+ [color,
306
357
  "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s}) #{m.snippet}"]]]
358
+
307
359
  when :detailed
308
- labels = m.labels# - @hidden_labels
309
- x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] +
310
- ((m.to.empty? ? [] : break_into_lines(" To: ", m.to.map { |x| x.longname })) +
311
- (m.cc.empty? ? [] : break_into_lines(" Cc: ", m.cc.map { |x| x.longname })) +
312
- (m.bcc.empty? ? [] : break_into_lines(" Bcc: ", m.bcc.map { |x| x.longname })) +
313
- [" Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] +
314
- [" Subject: #{m.subj}"] +
315
- [(parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] +
316
- [labels.empty? ? nil : " Labels: #{labels.join(', ')}"]
317
- ).flatten.compact.map { |l| [[:message_patina_color, prefix + " " + l]] }
318
- #raise x.inspect
319
- x
360
+ @person_lines[start] = m.from
361
+ from = [[prefix_widget, widget, imp_widget, [color, "From: #{m.from ? format_person(m.from) : '?'}"]]]
362
+
363
+ rest = []
364
+ unless m.to.empty?
365
+ m.to.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
366
+ rest += format_person_list " To: ", m.to
367
+ end
368
+ unless m.cc.empty?
369
+ m.cc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
370
+ rest += format_person_list " Cc: ", m.cc
371
+ end
372
+ unless m.bcc.empty?
373
+ m.bcc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
374
+ rest += format_person_list " Bcc: ", m.bcc
375
+ end
376
+
377
+ rest += [
378
+ " Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})",
379
+ " Subject: #{m.subj}",
380
+ (parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil),
381
+ m.labels.empty? ? nil : " Labels: #{m.labels.join(', ')}",
382
+ ].compact
383
+
384
+ from + rest.map { |l| [[color, prefix + " " + l]] }
320
385
  end
321
386
  end
322
387
 
323
- def break_into_lines prefix, list
388
+ def format_person_list prefix, people
389
+ ptext = people.map { |p| format_person p }
324
390
  pad = " " * prefix.length
325
- [prefix + list.first + (list.length > 1 ? "," : "")] +
326
- list[1 .. -1].map_with_index do |e, i|
327
- pad + e + (i == list.length - 1 ? "" : ",")
391
+ [prefix + ptext.first + (ptext.length > 1 ? "," : "")] +
392
+ ptext[1 .. -1].map_with_index do |e, i|
393
+ pad + e + (i == ptext.length - 1 ? "" : ",")
328
394
  end
329
395
  end
330
396
 
397
+ def format_person p
398
+ p.longname + (ContactManager.is_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
399
+ end
331
400
 
332
- def chunk_to_lines chunk, state, start, depth, parent=nil
333
- prefix = " " * depth
401
+ def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil
402
+ prefix = " " * INDENT_SPACES * depth
334
403
  case chunk
335
404
  when :fake_root
336
- [[[:message_patina_color, "#{prefix}<one or more unreceived messages>"]]]
405
+ [[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]
337
406
  when nil
338
- [[[:message_patina_color, "#{prefix}<an unreceived message>"]]]
407
+ [[[:missing_message_color, "#{prefix}<an unreceived message>"]]]
339
408
  when Message
340
- message_patina_lines(chunk, state, parent, prefix) +
409
+ message_patina_lines(chunk, state, start, parent, prefix, color) +
341
410
  (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
342
411
  when Message::Attachment
343
412
  [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
@@ -348,22 +417,20 @@ private
348
417
  end
349
418
  t.map { |line| [[:none, "#{prefix}#{line}"]] }
350
419
  when Message::Quote
420
+ return [[[:quote_color, "#{prefix}#{chunk.lines.first}"]]] if chunk.lines.length == 1
351
421
  case state
352
422
  when :closed
353
- [[[:quote_patina_color, "#{prefix}+ #{chunk.lines.length} quoted lines"]]]
423
+ [[[:quote_patina_color, "#{prefix}+ (#{chunk.lines.length} quoted lines)"]]]
354
424
  when :open
355
- t = chunk.lines
356
- [[[:quote_patina_color, "#{prefix}- #{chunk.lines.length} quoted lines"]]] +
357
- t.map { |line| [[:quote_color, "#{prefix}#{line}"]] }
425
+ [[[:quote_patina_color, "#{prefix}- (#{chunk.lines.length} quoted lines)"]]] + chunk.lines.map { |line| [[:quote_color, "#{prefix}#{line}"]] }
358
426
  end
359
427
  when Message::Signature
428
+ return [[[:sig_patina_color, "#{prefix}#{chunk.lines.first}"]]] if chunk.lines.length == 1
360
429
  case state
361
430
  when :closed
362
- [[[:sig_patina_color, "#{prefix}+ #{chunk.lines.length}-line signature"]]]
431
+ [[[:sig_patina_color, "#{prefix}+ (#{chunk.lines.length}-line signature)"]]]
363
432
  when :open
364
- t = chunk.lines
365
- [[[:sig_patina_color, "#{prefix}- #{chunk.lines.length}-line signature"]]] +
366
- t.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
433
+ [[[:sig_patina_color, "#{prefix}- (#{chunk.lines.length}-line signature)"]]] + chunk.lines.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
367
434
  end
368
435
  else
369
436
  raise "unknown chunk type #{chunk.class.name}"
@@ -372,9 +439,10 @@ private
372
439
 
373
440
  def view_attachment a
374
441
  BufferManager.flash "viewing #{a.content_type} attachment..."
375
- a.view!
442
+ success = a.view!
376
443
  BufferManager.erase_flash
377
444
  BufferManager.completely_redraw_screen
445
+ BufferManager.flash "Couldn't execute view command." unless success
378
446
  end
379
447
 
380
448
  end