]> git.cworth.org Git - sup/blob - lib/sup/modes/thread-view-mode.rb
many changes (while on the airplane).
[sup] / lib / sup / modes / thread-view-mode.rb
1 module Redwood
2
3 class ThreadViewMode < LineCursorMode
4   DATE_FORMAT = "%B %e %Y %l:%M%P"
5
6   register_keymap do |k|
7     k.add :toggle_detailed_header, "Toggle detailed header", 'd'
8     k.add :show_header, "Show full message header", 'H'
9     k.add :toggle_expanded, "Expand/collapse item", :enter
10     k.add :expand_all_messages, "Expand/collapse all messages", 'E'
11     k.add :edit_message, "Edit message (drafts only)", 'e'
12     k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
13     k.add :jump_to_next_open, "Jump to next open message", 'n'
14     k.add :jump_to_prev_open, "Jump to previous open message", 'p'
15     k.add :toggle_starred, "Star or unstar message", '*'
16     k.add :collapse_non_new_messages, "Collapse all but new messages", 'N'
17     k.add :reply, "Reply to a message", 'r'
18     k.add :forward, "Forward a message", 'f'
19     k.add :save_to_disk, "Save message/attachment to disk", 's'
20   end
21
22   def initialize thread, hidden_labels=[]
23     super()
24     @thread = thread
25     @state = {}
26     @hidden_labels = hidden_labels
27
28     earliest = nil
29     latest = nil
30     latest_date = nil
31     @thread.each do |m, d, p|
32       next unless m
33       earliest ||= m
34       @state[m] = 
35         if m.has_label?(:unread) && m == earliest
36           :detailed
37         elsif m.has_label?(:starred) || m.has_label?(:unread)
38           :open
39         else
40           :closed
41         end
42       if latest_date.nil? || m.date > latest_date
43         latest_date = m.date
44         latest = m
45       end
46     end
47     @state[latest] = :open if @state[latest] == :closed
48
49     BufferManager.say "Loading message bodies..." do
50       regen_chunks
51       regen_text
52     end
53   end
54
55   def draw_line ln, opts={}
56     if ln == curpos
57       super ln, :highlight => true
58     else
59       super
60     end
61   end
62   def lines; @text.length; end
63   def [] i; @text[i]; end
64
65   def show_header
66     return unless(m = @message_lines[curpos])
67     BufferManager.spawn_unless_exists("Full header") do
68       TextMode.new m.raw_header
69     end
70   end
71
72   def toggle_detailed_header
73     return unless(m = @message_lines[curpos])
74     @state[m] = (@state[m] == :detailed ? :open : :detailed)
75     update
76   end
77
78   def reply
79     return unless(m = @message_lines[curpos])
80     mode = ReplyMode.new m
81     BufferManager.spawn "Reply to #{m.subj}", mode
82   end
83
84   def forward
85     return unless(m = @message_lines[curpos])
86     mode = ForwardMode.new m
87     BufferManager.spawn "Forward of #{m.subj}", mode
88     mode.edit
89   end
90
91   def toggle_starred
92     return unless(m = @message_lines[curpos])
93     if m.has_label? :starred
94       m.remove_label :starred
95     else
96       m.add_label :starred
97     end
98     ## TODO: don't recalculate EVERYTHING just to add a stupid little
99     ## star to the display
100     update
101     UpdateManager.relay :starred, m
102   end
103
104   def toggle_expanded
105     return unless(chunk = @chunk_lines[curpos])
106     case chunk
107     when Message, Message::Quote, Message::Signature
108       @state[chunk] = (@state[chunk] != :closed ? :closed : :open)
109     when Message::Attachment
110       view_attachment chunk
111     end
112     update
113   end
114
115   def save fn
116     if File.exists? fn
117       return unless BufferManager.ask_yes_or_no "File exists. Overwrite?"
118     end
119     begin
120       File.open(fn, "w") { |f| yield f }
121       BufferManager.flash "Successfully wrote #{fn}."
122     rescue SystemCallError => e
123       BufferManager.flash "Error writing to file: #{e.message}"
124     end
125   end
126   private :save
127
128   def save_to_disk
129     return unless(chunk = @chunk_lines[curpos])
130     case chunk
131     when Message::Attachment
132       fn = BufferManager.ask :filename, "save attachment to file: ", chunk.filename
133       save(fn) { |f| f.print chunk } if fn
134     else
135       m = @message_lines[curpos]
136       fn = BufferManager.ask :filename, "save message to file: "
137       save(fn) { |f| f.print m.raw_full_message } if fn
138     end
139   end
140
141   def edit_message
142     return unless(m = @message_lines[curpos])
143     if m.is_draft?
144       mode = ResumeMode.new m
145       BufferManager.spawn "Edit message", mode
146       mode.edit
147     else
148       BufferManager.flash "Not a draft message!"
149     end
150   end
151
152   def jump_to_next_open
153     return unless(m = @message_lines[curpos])
154     while nextm = @messages[m][3]
155       break if @state[nextm] == :open
156       m = nextm
157     end
158     jump_to_message nextm if nextm
159   end
160
161   def jump_to_prev_open
162     return unless(m = @message_lines[curpos])
163     ## jump to the top of the current message if we're in the body;
164     ## otherwise, to the previous message
165     top = @messages[m][0]
166     if curpos == top
167       while prevm = @messages[m][2]
168         break if @state[prevm] == :open
169         m = prevm
170       end
171       jump_to_message prevm if prevm
172     else
173       jump_to_message m
174     end
175   end
176
177   def jump_to_message m
178     top, bot, prevm, nextm = @messages[m]
179     jump_to_line top unless top >= topline &&
180       top <= botline && bot >= topline && bot <= botline
181     set_cursor_pos top
182   end
183
184   def expand_all_messages
185     @global_message_state ||= :closed
186     @global_message_state = (@global_message_state == :closed ? :open : :closed)
187     @state.each { |m, v| @state[m] = @global_message_state if m.is_a? Message }
188     update
189   end
190
191
192   def collapse_non_new_messages
193     @messages.each { |m, v| @state[m] = m.has_label?(:unread) ? :open : :closed }
194     update
195   end
196
197   def expand_all_quotes
198     if(m = @message_lines[curpos])
199       quotes = @chunks[m].select { |c| c.is_a?(Message::Quote) || c.is_a?(Message::Signature) }
200       open, closed = quotes.partition { |c| @state[c] == :open }
201       newstate = open.length > closed.length ? :closed : :open
202       Redwood::log "#{open.length} opened, #{closed.length} closed, new state is thus #{newstate}"
203       quotes.each { |c| @state[c] = newstate }
204       update
205     end
206   end
207
208   ## not sure if this is really necessary but we might as well...
209   def cleanup
210     @thread.each do |m, d, p|
211       if m && m.has_label?(:unread)
212         m.remove_label :unread 
213         UpdateManager.relay :read, m
214       end
215     end
216     @messages = @chunks = @text = nil
217   end
218
219 private 
220
221   def update
222     regen_text
223     buffer.mark_dirty if buffer
224   end
225
226   def regen_chunks
227     @chunks = {}
228     @thread.each { |m, d, p| @chunks[m] = m.to_chunks if m.is_a?(Message) }
229   end
230   
231   def regen_text
232     @text = []
233     @chunk_lines = []
234     @message_lines = []
235     @messages = {}
236
237     prev_m = nil
238     @thread.each do |m, depth, parent|
239       text = chunk_to_lines m, @state[m], @text.length, depth, parent
240       (0 ... text.length).each do |i|
241         @chunk_lines[@text.length + i] = m
242         @message_lines[@text.length + i] = m
243       end
244
245       @messages[m] = [@text.length, @text.length + text.length, prev_m, nil]
246       @messages[prev_m][3] = m if prev_m
247       prev_m = m
248
249       @text += text
250       if @state[m] != :closed && @chunks.member?(m)
251         @chunks[m].each do |c|
252           @state[c] ||= :closed
253           text = chunk_to_lines c, @state[c], @text.length, depth
254           (0 ... text.length).each do |i|
255             @chunk_lines[@text.length + i] = c
256             @message_lines[@text.length + i] = m
257           end
258           @text += text
259         end
260         @messages[m][1] = @text.length
261       end
262     end
263   end
264
265   def message_patina_lines m, state, parent, prefix
266     prefix_widget = [:message_patina_color, prefix]
267     widget = 
268       case state
269       when :closed
270         [:message_patina_color, "+ "]
271       when :open, :detailed
272         [:message_patina_color, "- "]
273       end
274     imp_widget = 
275       if m.has_label?(:starred)
276         [:starred_patina_color, "* "]
277       else
278         [:message_patina_color, "  "]
279       end
280
281     case state
282     when :open
283       [[prefix_widget, widget, imp_widget,
284         [:message_patina_color, 
285             "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
286 #        (m.to.empty? ? [] : [[[:message_patina_color, prefix + "    To: " + m.recipients.map { |x| x.mediumname }.join(", ")]]]) +
287     when :closed
288       [[prefix_widget, widget, imp_widget,
289         [:message_patina_color, 
290         "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})  #{m.snippet}"]]]
291     when :detailed
292       labels = m.labels# - @hidden_labels
293       x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] +
294         ((m.to.empty? ? [] : break_into_lines("  To: ", m.to.map { |x| x.longname })) +
295            (m.cc.empty? ? [] : break_into_lines("  Cc: ", m.cc.map { |x| x.longname })) +
296            (m.bcc.empty? ? [] : break_into_lines("  Bcc: ", m.bcc.map { |x| x.longname })) +
297            ["  Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] +
298            ["  Subject: #{m.subj}"] +
299            [(parent ? "  In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] +
300            [labels.empty? ? nil : "  Labels: #{labels.join(', ')}"]
301         ).flatten.compact.map { |l| [[:message_patina_color, prefix + "  " + l]] }
302       #raise x.inspect
303       x
304     end
305   end
306
307   def break_into_lines prefix, list
308     pad = " " * prefix.length
309     [prefix + list.first + (list.length > 1 ? "," : "")] + 
310       list[1 .. -1].map_with_index do |e, i|
311         pad + e + (i == list.length - 1 ? "" : ",")
312       end
313   end
314
315
316   def chunk_to_lines chunk, state, start, depth, parent=nil
317     prefix = "  " * depth
318     case chunk
319     when :fake_root
320       [[[:message_patina_color, "#{prefix}<one or more unreceived messages>"]]]
321     when nil
322       [[[:message_patina_color, "#{prefix}<an unreceived message>"]]]
323     when Message
324       message_patina_lines(chunk, state, parent, prefix) +
325         (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
326
327     when Message::Attachment
328       [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
329     when Message::Text
330       t = chunk.lines
331       if t.last =~ /^\s*$/
332         t.pop while t[t.length - 2] =~ /^\s*$/
333       end
334       t.map { |line| [[:none, "#{prefix}#{line}"]] }
335     when Message::Quote
336       case state
337       when :closed
338         [[[:quote_patina_color, "#{prefix}+ #{chunk.lines.length} quoted lines"]]]
339       when :open
340         t = chunk.lines
341         [[[:quote_patina_color, "#{prefix}- #{chunk.lines.length} quoted lines"]]] +
342            t.map { |line| [[:quote_color, "#{prefix}#{line}"]] }
343       end
344     when Message::Signature
345       case state
346       when :closed
347         [[[:sig_patina_color, "#{prefix}+ #{chunk.lines.length}-line signature"]]]
348       when :open
349         t = chunk.lines
350         [[[:sig_patina_color, "#{prefix}- #{chunk.lines.length}-line signature"]]] +
351            t.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
352       end
353     else
354       raise "unknown chunk type #{chunk.class.name}"
355     end
356   end
357
358   def view_attachment a
359     BufferManager.flash "viewing #{a.content_type} attachment..."
360     a.view!
361     BufferManager.erase_flash
362     BufferManager.completely_redraw_screen
363   end
364
365 end
366
367 end