]> git.cworth.org Git - sup/blob - lib/sup/modes/thread-view-mode.rb
Merge commit 'origin/color'
[sup] / lib / sup / modes / thread-view-mode.rb
1 require 'open3'
2 module Redwood
3
4 class ThreadViewMode < LineCursorMode
5   ## this holds all info we need to lay out a message
6   class MessageLayout
7     attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new
8   end
9
10   class ChunkLayout
11     attr_accessor :state
12   end
13
14   DATE_FORMAT = "%B %e %Y %l:%M%P"
15   INDENT_SPACES = 2 # how many spaces to indent child messages
16
17   HookManager.register "detailed-headers", <<EOS
18 Add or remove headers from the detailed header display of a message.
19 Variables:
20   message: The message whose headers are to be formatted.
21   headers: A hash of header (name, value) pairs, initialized to the default
22            headers.
23 Return value:
24   None. The variable 'headers' should be modified in place.
25 EOS
26
27   register_keymap do |k|
28     k.add :toggle_detailed_header, "Toggle detailed header", 'h'
29     k.add :show_header, "Show full message header", 'H'
30     k.add :activate_chunk, "Expand/collapse or activate item", :enter
31     k.add :expand_all_messages, "Expand/collapse all messages", 'E'
32     k.add :edit_draft, "Edit draft", 'e'
33     k.add :edit_labels, "Edit or add labels for a thread", 'l'
34     k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
35     k.add :jump_to_next_open, "Jump to next open message", 'n'
36     k.add :jump_to_prev_open, "Jump to previous open message", 'p'
37     k.add :align_current_message, "Align current message in buffer", 'z'
38     k.add :toggle_starred, "Star or unstar message", '*'
39     k.add :toggle_new, "Toggle unread/read status of message", 'N'
40 #    k.add :collapse_non_new_messages, "Collapse all but unread messages", 'N'
41     k.add :reply, "Reply to a message", 'r'
42     k.add :forward, "Forward a message or attachment", 'f'
43     k.add :alias, "Edit alias/nickname for a person", 'i'
44     k.add :edit_as_new, "Edit message as new", 'D'
45     k.add :save_to_disk, "Save message/attachment to disk", 's'
46     k.add :search, "Search for messages from particular people", 'S'
47     k.add :compose, "Compose message to person", 'm'
48     k.add :subscribe_to_list, "Subscribe to/unsubscribe from mailing list", "("
49     k.add :unsubscribe_from_list, "Subscribe to/unsubscribe from mailing list", ")"
50     k.add :pipe_message, "Pipe message or attachment to a shell command", '|'
51
52     k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read:", '.' do |kk|
53       kk.add :archive_and_kill, "Archive this thread and kill buffer", 'a'
54       kk.add :delete_and_kill, "Delete this thread and kill buffer", 'd'
55       kk.add :spam_and_kill, "Mark this thread as spam and kill buffer", 's'
56       kk.add :unread_and_kill, "Mark this thread as unread and kill buffer", 'N'
57     end
58
59     k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read/do (n)othing:", ',' do |kk|
60       kk.add :archive_and_next, "Archive this thread, kill buffer, and view next", 'a'
61       kk.add :delete_and_next, "Delete this thread, kill buffer, and view next", 'd'
62       kk.add :spam_and_next, "Mark this thread as spam, kill buffer, and view next", 's'
63       kk.add :unread_and_next, "Mark this thread as unread, kill buffer, and view next", 'N'
64       kk.add :do_nothing_and_next, "Kill buffer, and view next", 'n'
65     end
66
67     k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read/do (n)othing:", ']' do |kk|
68       kk.add :archive_and_prev, "Archive this thread, kill buffer, and view previous", 'a'
69       kk.add :delete_and_prev, "Delete this thread, kill buffer, and view previous", 'd'
70       kk.add :spam_and_prev, "Mark this thread as spam, kill buffer, and view previous", 's'
71       kk.add :unread_and_prev, "Mark this thread as unread, kill buffer, and view previous", 'N'
72       kk.add :do_nothing_and_prev, "Kill buffer, and view previous", 'n'
73     end
74   end
75
76   ## there are a couple important instance variables we hold to format
77   ## the thread and to provide line-based functionality. @layout is a
78   ## map from Messages to MessageLayouts, and @chunk_layout from
79   ## Chunks to ChunkLayouts.  @message_lines is a map from row #s to
80   ## Message objects.  @chunk_lines is a map from row #s to Chunk
81   ## objects. @person_lines is a map from row #s to Person objects.
82
83   def initialize thread, hidden_labels=[], index_mode=nil
84     super()
85     @thread = thread
86     @hidden_labels = hidden_labels
87
88     ## used for dispatch-and-next
89     @index_mode = index_mode
90     @dying = false
91
92     @layout = SavingHash.new { MessageLayout.new }
93     @chunk_layout = SavingHash.new { ChunkLayout.new }
94     earliest, latest = nil, nil
95     latest_date = nil
96     altcolor = false
97
98     @thread.each do |m, d, p|
99       next unless m
100       earliest ||= m
101       @layout[m].state = initial_state_for m
102       @layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
103       @layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color
104       @layout[m].orig_new = m.has_label? :read
105       altcolor = !altcolor
106       if latest_date.nil? || m.date > latest_date
107         latest_date = m.date
108         latest = m
109       end
110     end
111
112     @layout[latest].state = :open if @layout[latest].state == :closed
113     @layout[earliest].state = :detailed if earliest.has_label?(:unread) || @thread.size == 1
114
115     @thread.remove_label :unread
116     regen_text
117   end
118
119   def draw_line ln, opts={}
120     if ln == curpos
121       super ln, :highlight => true
122     else
123       super
124     end
125   end
126   def lines; @text.length; end
127   def [] i; @text[i]; end
128
129   def show_header
130     m = @message_lines[curpos] or return
131     BufferManager.spawn_unless_exists("Full header for #{m.id}") do
132       TextMode.new m.raw_header
133     end
134   end
135
136   def toggle_detailed_header
137     m = @message_lines[curpos] or return
138     @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
139     update
140   end
141
142   def reply
143     m = @message_lines[curpos] or return
144     mode = ReplyMode.new m
145     BufferManager.spawn "Reply to #{m.subj}", mode
146   end
147
148   def subscribe_to_list
149     m = @message_lines[curpos] or return
150     if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
151       ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
152     else
153       BufferManager.flash "Can't find List-Subscribe header for this message."
154     end
155   end
156
157   def unsubscribe_from_list
158     m = @message_lines[curpos] or return
159     if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
160       ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
161     else
162       BufferManager.flash "Can't find List-Unsubscribe header for this message."
163     end
164   end
165
166   def forward
167     if(chunk = @chunk_lines[curpos]) && chunk.is_a?(Chunk::Attachment)
168       ForwardMode.spawn_nicely :attachments => [chunk]
169     elsif(m = @message_lines[curpos])
170       ForwardMode.spawn_nicely :message => m
171     end
172   end
173
174   include CanAliasContacts
175   def alias
176     p = @person_lines[curpos] or return
177     alias_contact p
178     update
179   end
180
181   def search
182     p = @person_lines[curpos] or return
183     mode = PersonSearchResultsMode.new [p]
184     BufferManager.spawn "Search for #{p.name}", mode
185     mode.load_threads :num => mode.buffer.content_height
186   end    
187
188   def compose
189     p = @person_lines[curpos]
190     if p
191       ComposeMode.spawn_nicely :to_default => p
192     else
193       ComposeMode.spawn_nicely
194     end
195   end    
196
197   def edit_labels
198     reserved_labels = @thread.labels.select { |l| LabelManager::RESERVED_LABELS.include? l }
199     new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels
200
201     return unless new_labels
202     @thread.labels = (reserved_labels + new_labels).uniq
203     new_labels.each { |l| LabelManager << l }
204     update
205     UpdateManager.relay self, :labeled, @thread.first
206   end
207
208   def toggle_starred
209     m = @message_lines[curpos] or return
210     toggle_label m, :starred
211   end
212
213   def toggle_new
214     m = @message_lines[curpos] or return
215     toggle_label m, :unread
216   end
217
218   def toggle_label m, label
219     if m.has_label? label
220       m.remove_label label
221     else
222       m.add_label label
223     end
224     ## TODO: don't recalculate EVERYTHING just to add a stupid little
225     ## star to the display
226     update
227     UpdateManager.relay self, :single_message_labeled, m
228   end
229
230   ## called when someone presses enter when the cursor is highlighting
231   ## a chunk. for expandable chunks (including messages) we toggle
232   ## open/closed state; for viewable chunks (like attachments) we
233   ## view.
234   def activate_chunk
235     chunk = @chunk_lines[curpos] or return
236     layout = 
237       if chunk.is_a?(Message)
238         @layout[chunk]
239       elsif chunk.expandable?
240         @chunk_layout[chunk]
241       end
242     if layout
243       layout.state = (layout.state != :closed ? :closed : :open)
244       #cursor_down if layout.state == :closed # too annoying
245       update
246     elsif chunk.viewable?
247       view chunk
248     end
249   end
250
251   def edit_as_new
252     m = @message_lines[curpos] or return
253     mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc, :refs => m.refs, :replytos => m.replytos)
254     BufferManager.spawn "edit as new", mode
255     mode.edit_message
256   end
257
258   def save_to_disk
259     chunk = @chunk_lines[curpos] or return
260     case chunk
261     when Chunk::Attachment
262       fn = BufferManager.ask_for_filename :filename, "Save attachment to file: ", chunk.filename
263       save_to_file(fn) { |f| f.print chunk.raw_content } if fn
264     else
265       m = @message_lines[curpos]
266       fn = BufferManager.ask_for_filename :filename, "Save message to file: "
267       return unless fn
268       save_to_file(fn) do |f|
269         m.each_raw_message_line { |l| f.print l }
270       end
271     end
272   end
273
274   def edit_draft
275     m = @message_lines[curpos] or return
276     if m.is_draft?
277       mode = ResumeMode.new m
278       BufferManager.spawn "Edit message", mode
279       BufferManager.kill_buffer self.buffer
280       mode.edit_message
281     else
282       BufferManager.flash "Not a draft message!"
283     end
284   end
285
286   def jump_to_first_open loose_alignment=false
287     m = @message_lines[0] or return
288     if @layout[m].state != :closed
289       jump_to_message m, loose_alignment
290     else
291       jump_to_next_open loose_alignment
292     end
293   end
294
295   def jump_to_next_open loose_alignment=false
296     return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations
297     m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
298     return unless m
299     while nextm = @layout[m].next
300       break if @layout[nextm].state != :closed
301       m = nextm
302     end
303     jump_to_message nextm, loose_alignment if nextm
304   end
305
306   def align_current_message
307     m = @message_lines[curpos] or return
308     jump_to_message m
309   end
310
311   def jump_to_prev_open loose_alignment=false
312     m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] } # bah, .to_a
313     return unless m
314     ## jump to the top of the current message if we're in the body;
315     ## otherwise, to the previous message
316     
317     top = @layout[m].top
318     if curpos == top
319       while(prevm = @layout[m].prev)
320         break if @layout[prevm].state != :closed
321         m = prevm
322       end
323       jump_to_message prevm, loose_alignment if prevm
324     else
325       jump_to_message m, loose_alignment
326     end
327   end
328
329   IDEAL_TOP_CONTEXT = 3 # try and give 3 rows of top context
330   IDEAL_LEFT_CONTEXT = 4 # try and give 4 columns of left context
331   def jump_to_message m, loose_alignment=false
332     l = @layout[m]
333     left = l.depth * INDENT_SPACES
334     right = left + l.width
335
336     ## jump to the top line
337     if loose_alignment
338       jump_to_line [l.top - IDEAL_TOP_CONTEXT, 0].max # give 3 lines of top context
339     else
340       jump_to_line l.top
341     end
342
343     ## jump to the left column
344     ideal_left = left +
345       if loose_alignment
346         -IDEAL_LEFT_CONTEXT + (l.width - buffer.content_width + IDEAL_LEFT_CONTEXT + 1).clamp(0, IDEAL_LEFT_CONTEXT)
347       else
348         0
349       end
350
351     jump_to_col [ideal_left, 0].max
352
353     ## either way, move the cursor to the first line
354     set_cursor_pos l.top
355   end
356
357   def expand_all_messages
358     @global_message_state ||= :closed
359     @global_message_state = (@global_message_state == :closed ? :open : :closed)
360     @layout.each { |m, l| l.state = @global_message_state }
361     update
362   end
363
364   def collapse_non_new_messages
365     @layout.each { |m, l| l.state = l.orig_new ? :open : :closed }
366     update
367   end
368
369   def expand_all_quotes
370     if(m = @message_lines[curpos])
371       quotes = m.chunks.select { |c| (c.is_a?(Chunk::Quote) || c.is_a?(Chunk::Signature)) && c.lines.length > 1 }
372       numopen = quotes.inject(0) { |s, c| s + (@chunk_layout[c].state == :open ? 1 : 0) }
373       newstate = numopen > quotes.length / 2 ? :closed : :open
374       quotes.each { |c| @chunk_layout[c].state = newstate }
375       update
376     end
377   end
378
379   def cleanup
380     @layout = @chunk_layout = @text = nil # for good luck
381   end
382
383   def archive_and_kill; archive_and_then :kill end
384   def spam_and_kill; spam_and_then :kill end
385   def delete_and_kill; delete_and_then :kill end
386   def unread_and_kill; unread_and_then :kill end
387
388   def archive_and_next; archive_and_then :next end
389   def spam_and_next; spam_and_then :next end
390   def delete_and_next; delete_and_then :next end
391   def unread_and_next; unread_and_then :next end
392   def do_nothing_and_next; do_nothing_and_then :next end
393
394   def archive_and_prev; archive_and_then :prev end
395   def spam_and_prev; spam_and_then :prev end
396   def delete_and_prev; delete_and_then :prev end
397   def unread_and_prev; unread_and_then :prev end
398   def do_nothing_and_prev; do_nothing_and_then :prev end
399
400   def archive_and_then op
401     dispatch op do
402       @thread.remove_label :inbox
403       UpdateManager.relay self, :archived, @thread.first
404     end
405   end
406
407   def spam_and_then op
408     dispatch op do
409       @thread.apply_label :spam
410       UpdateManager.relay self, :spammed, @thread.first
411     end
412   end
413
414   def delete_and_then op
415     dispatch op do
416       @thread.apply_label :deleted
417       UpdateManager.relay self, :deleted, @thread.first
418     end
419   end
420
421   def unread_and_then op
422     dispatch op do
423       @thread.apply_label :unread
424       UpdateManager.relay self, :unread, @thread.first
425     end
426   end
427
428   def do_nothing_and_then op
429     dispatch op
430   end
431
432   def dispatch op
433     return if @dying
434     @dying = true
435
436     l = lambda do
437       yield if block_given?
438       BufferManager.kill_buffer_safely buffer
439     end
440
441     case op
442     when :next
443       @index_mode.launch_next_thread_after @thread, &l
444     when :prev
445       @index_mode.launch_prev_thread_before @thread, &l
446     when :kill
447       l.call
448     else
449       raise ArgumentError, "unknown thread dispatch operation #{op.inspect}"
450     end
451   end
452   private :dispatch
453
454   def pipe_message
455     chunk = @chunk_lines[curpos]
456     chunk = nil unless chunk.is_a?(Chunk::Attachment)
457     message = @message_lines[curpos] unless chunk
458
459     return unless chunk || message
460
461     command = BufferManager.ask(:shell, "pipe command: ")
462     return if command.nil? || command.empty?
463
464     output = pipe_to_process(command) do |stream|
465       if chunk
466         stream.print chunk.raw_content
467       else
468         message.each_raw_message_line { |l| stream.print l }
469       end
470     end
471
472     if output
473       BufferManager.spawn "Output of '#{command}'", TextMode.new(output)
474     else
475       BufferManager.flash "'#{command}' done!"
476     end
477   end
478
479 private
480
481   def initial_state_for m
482     if m.has_label?(:starred) || m.has_label?(:unread)
483       :open
484     else
485       :closed
486     end
487   end
488
489   def update
490     regen_text
491     buffer.mark_dirty if buffer
492   end
493
494   ## here we generate the actual content lines. we accumulate
495   ## everything into @text, and we set @chunk_lines and
496   ## @message_lines, and we update @layout.
497   def regen_text
498     @text = []
499     @chunk_lines = []
500     @message_lines = []
501     @person_lines = []
502
503     prevm = nil
504     @thread.each do |m, depth, parent|
505       unless m.is_a? Message # handle nil and :fake_root
506         @text += chunk_to_lines m, nil, @text.length, depth, parent
507         next
508       end
509       l = @layout[m]
510
511       ## is this still necessary?
512       next unless @layout[m].state # skip discarded drafts
513
514       ## build the patina
515       text = chunk_to_lines m, l.state, @text.length, depth, parent, l.color, l.star_color
516       
517       l.top = @text.length
518       l.bot = @text.length + text.length # updated below
519       l.prev = prevm
520       l.next = nil
521       l.depth = depth
522       # l.state we preserve
523       l.width = 0 # updated below
524       @layout[l.prev].next = m if l.prev
525
526       (0 ... text.length).each do |i|
527         @chunk_lines[@text.length + i] = m
528         @message_lines[@text.length + i] = m
529         lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum
530       end
531
532       @text += text
533       prevm = m 
534       if l.state != :closed
535         m.chunks.each do |c|
536           cl = @chunk_layout[c]
537
538           ## set the default state for chunks
539           cl.state ||=
540             if c.expandable? && c.respond_to?(:initial_state)
541               c.initial_state
542             else
543               :closed
544             end
545
546           text = chunk_to_lines c, cl.state, @text.length, depth
547           (0 ... text.length).each do |i|
548             @chunk_lines[@text.length + i] = c
549             @message_lines[@text.length + i] = m
550             lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum - (depth * INDENT_SPACES)
551             l.width = lw if lw > l.width
552           end
553           @text += text
554         end
555         @layout[m].bot = @text.length
556       end
557     end
558   end
559
560   def message_patina_lines m, state, start, parent, prefix, color, star_color
561     prefix_widget = [color, prefix]
562
563     open_widget = [color, (state == :closed ? "+ " : "- ")]
564     new_widget = [color, (m.has_label?(:unread) ? "N" : " ")]
565     starred_widget = if m.has_label?(:starred)
566         [star_color, "*"]
567       else
568         [color, " "]
569       end
570     attach_widget = [color, (m.has_label?(:attachment) ? "@" : " ")]
571
572     case state
573     when :open
574       @person_lines[start] = m.from
575       [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
576         [color, 
577             "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
578
579     when :closed
580       @person_lines[start] = m.from
581       [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
582         [color, 
583         "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})  #{m.snippet}"]]]
584
585     when :detailed
586       @person_lines[start] = m.from
587       from_line = [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
588           [color, "From: #{m.from ? format_person(m.from) : '?'}"]]]
589
590       addressee_lines = []
591       unless m.to.empty?
592         m.to.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
593         addressee_lines += format_person_list "   To: ", m.to
594       end
595       unless m.cc.empty?
596         m.cc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
597         addressee_lines += format_person_list "   Cc: ", m.cc
598       end
599       unless m.bcc.empty?
600         m.bcc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
601         addressee_lines += format_person_list "   Bcc: ", m.bcc
602       end
603
604       headers = OrderedHash.new
605       headers["Date"] = "#{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"
606       headers["Subject"] = m.subj
607
608       show_labels = @thread.labels - LabelManager::HIDDEN_RESERVED_LABELS
609       unless show_labels.empty?
610         headers["Labels"] = show_labels.map { |x| x.to_s }.sort.join(', ')
611       end
612       if parent
613         headers["In reply to"] = "#{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}"
614       end
615
616       HookManager.run "detailed-headers", :message => m, :headers => headers
617       
618       from_line + (addressee_lines + headers.map { |k, v| "   #{k}: #{v}" }).map { |l| [[color, prefix + "  " + l]] }
619     end
620   end
621
622   def format_person_list prefix, people
623     ptext = people.map { |p| format_person p }
624     pad = " " * prefix.length
625     [prefix + ptext.first + (ptext.length > 1 ? "," : "")] + 
626       ptext[1 .. -1].map_with_index do |e, i|
627         pad + e + (i == ptext.length - 1 ? "" : ",")
628       end
629   end
630
631   def format_person p
632     p.longname + (ContactManager.is_aliased_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
633   end
634
635   ## todo: check arguments on this overly complex function
636   def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
637     prefix = " " * INDENT_SPACES * depth
638     case chunk
639     when :fake_root
640       [[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]
641     when nil
642       [[[:missing_message_color, "#{prefix}<an unreceived message>"]]]
643     when Message
644       message_patina_lines(chunk, state, start, parent, prefix, color, star_color) +
645         (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
646
647     else
648       raise "Bad chunk: #{chunk.inspect}" unless chunk.respond_to?(:inlineable?) ## debugging
649       if chunk.inlineable?
650         chunk.lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
651       elsif chunk.expandable?
652         case state
653         when :closed
654           [[[chunk.patina_color, "#{prefix}+ #{chunk.patina_text}"]]]
655         when :open
656           [[[chunk.patina_color, "#{prefix}- #{chunk.patina_text}"]]] + chunk.lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
657         end
658       else
659         [[[chunk.patina_color, "#{prefix}x #{chunk.patina_text}"]]]
660       end
661     end
662   end
663
664   def view chunk
665     BufferManager.flash "viewing #{chunk.content_type} attachment..."
666     success = chunk.view!
667     BufferManager.erase_flash
668     BufferManager.completely_redraw_screen
669     unless success
670       BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s, chunk.filename)
671       BufferManager.flash "Couldn't execute view command, viewing as text."
672     end
673   end
674 end
675
676 end