]> git.cworth.org Git - sup/blob - lib/sup/modes/thread-view-mode.rb
added a nice "loading" message
[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..." 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     else
147       BufferManager.flash "Not a draft message!"
148     end
149   end
150
151   def jump_to_next_open
152     return unless(m = @message_lines[curpos])
153     while nextm = @messages[m][3]
154       break if @state[nextm] == :open
155       m = nextm
156     end
157     jump_to_message nextm if nextm
158   end
159
160   def jump_to_prev_open
161     return unless(m = @message_lines[curpos])
162     ## jump to the top of the current message if we're in the body;
163     ## otherwise, to the previous message
164     top = @messages[m][0]
165     if curpos == top
166       while prevm = @messages[m][2]
167         break if @state[prevm] == :open
168         m = prevm
169       end
170       jump_to_message prevm if prevm
171     else
172       jump_to_message m
173     end
174   end
175
176   def jump_to_message m
177     top, bot, prevm, nextm = @messages[m]
178     jump_to_line top unless top >= topline &&
179       top <= botline && bot >= topline && bot <= botline
180     set_cursor_pos top
181   end
182
183   def expand_all_messages
184     @global_message_state ||= :closed
185     @global_message_state = (@global_message_state == :closed ? :open : :closed)
186     @state.each { |m, v| @state[m] = @global_message_state if m.is_a? Message }
187     update
188   end
189
190
191   def collapse_non_new_messages
192     @messages.each { |m, v| @state[m] = m.has_label?(:unread) ? :open : :closed }
193     update
194   end
195
196   def expand_all_quotes
197     if(m = @message_lines[curpos])
198       quotes = @chunks[m].select { |c| c.is_a?(Message::Quote) || c.is_a?(Message::Signature) }
199       open, closed = quotes.partition { |c| @state[c] == :open }
200       newstate = open.length > closed.length ? :closed : :open
201       Redwood::log "#{open.length} opened, #{closed.length} closed, new state is thus #{newstate}"
202       quotes.each { |c| @state[c] = newstate }
203       update
204     end
205   end
206
207   ## not sure if this is really necessary but we might as well...
208   def cleanup
209     @thread.each do |m, d, p|
210       if m && m.has_label?(:unread)
211         m.remove_label :unread 
212         UpdateManager.relay :read, m
213       end
214     end
215
216     Redwood::log "releasing chunks and text from \"#{buffer.title}\""
217     @messages = @chunks = @text = nil
218   end
219
220 private 
221
222   def update
223     regen_text
224     buffer.mark_dirty if buffer
225   end
226
227   def regen_chunks
228     @chunks = {}
229     @thread.each { |m, d, p| @chunks[m] = m.to_chunks if m.is_a?(Message) }
230   end
231   
232   def regen_text
233     @text = []
234     @chunk_lines = []
235     @message_lines = []
236     @messages = {}
237
238     prev_m = nil
239     @thread.each do |m, depth, parent|
240       text = chunk_to_lines m, @state[m], @text.length, depth, parent
241       (0 ... text.length).each do |i|
242         @chunk_lines[@text.length + i] = m
243         @message_lines[@text.length + i] = m
244       end
245
246       @messages[m] = [@text.length, @text.length + text.length, prev_m, nil]
247       @messages[prev_m][3] = m if prev_m
248       prev_m = m
249
250       @text += text
251       if @state[m] != :closed && @chunks.member?(m)
252         @chunks[m].each do |c|
253           @state[c] ||= :closed
254           text = chunk_to_lines c, @state[c], @text.length, depth
255           (0 ... text.length).each do |i|
256             @chunk_lines[@text.length + i] = c
257             @message_lines[@text.length + i] = m
258           end
259           @text += text
260         end
261         @messages[m][1] = @text.length
262       end
263     end
264   end
265
266   def message_patina_lines m, state, parent, prefix
267     prefix_widget = [:message_patina_color, prefix]
268     widget = 
269       case state
270       when :closed
271         [:message_patina_color, "+ "]
272       when :open, :detailed
273         [:message_patina_color, "- "]
274       end
275     imp_widget = 
276       if m.has_label?(:starred)
277         [:starred_patina_color, "* "]
278       else
279         [:message_patina_color, "  "]
280       end
281
282     case state
283     when :open
284       [[prefix_widget, widget, imp_widget,
285         [:message_patina_color, 
286             "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
287 #        (m.to.empty? ? [] : [[[:message_patina_color, prefix + "    To: " + m.recipients.map { |x| x.mediumname }.join(", ")]]]) +
288     when :closed
289       [[prefix_widget, widget, imp_widget,
290         [:message_patina_color, 
291         "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})  #{m.snippet}"]]]
292     when :detailed
293       labels = m.labels# - @hidden_labels
294       x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] +
295         ((m.to.empty? ? [] : break_into_lines("  To: ", m.to.map { |x| x.longname })) +
296               (m.cc.empty? ? [] : break_into_lines("  Cc: ", m.cc.map { |x| x.longname })) +
297               (m.bcc.empty? ? [] : break_into_lines("  Bcc: ", m.bcc.map { |x| x.longname })) +
298               ["  Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] +
299               ["  Subject: #{m.subj}"] +
300               [(parent ? "  In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] +
301               [labels.empty? ? nil : "  Labels: #{labels.join(', ')}"]
302             ).flatten.compact.map { |l| [[:message_patina_color, prefix + "  " + l]] }
303       #raise x.inspect
304       x
305     end
306   end
307
308   def break_into_lines prefix, list
309     pad = " " * prefix.length
310     [prefix + list.first + (list.length > 1 ? "," : "")] + 
311       list[1 .. -1].map_with_index do |e, i|
312         pad + e + (i == list.length - 1 ? "" : ",")
313       end
314   end
315
316
317   def chunk_to_lines chunk, state, start, depth, parent=nil
318     prefix = "  " * depth
319     case chunk
320     when :fake_root
321       [[[:message_patina_color, "#{prefix}<one or more unreceived messages>"]]]
322     when nil
323       [[[:message_patina_color, "#{prefix}<an unreceived message>"]]]
324     when Message
325       message_patina_lines(chunk, state, parent, prefix) +
326         (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
327
328     when Message::Attachment
329       [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
330     when Message::Text
331       t = chunk.lines
332       if t.last =~ /^\s*$/
333         t.pop while t[t.length - 2] =~ /^\s*$/
334       end
335       t.map { |line| [[:none, "#{prefix}#{line}"]] }
336     when Message::Quote
337       case state
338       when :closed
339         [[[:quote_patina_color, "#{prefix}+ #{chunk.lines.length} quoted lines"]]]
340       when :open
341         t = chunk.lines
342         [[[:quote_patina_color, "#{prefix}- #{chunk.lines.length} quoted lines"]]] +
343            t.map { |line| [[:quote_color, "#{prefix}#{line}"]] }
344       end
345     when Message::Signature
346       case state
347       when :closed
348         [[[:sig_patina_color, "#{prefix}+ #{chunk.lines.length}-line signature"]]]
349       when :open
350         t = chunk.lines
351         [[[:sig_patina_color, "#{prefix}- #{chunk.lines.length}-line signature"]]] +
352            t.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
353       end
354     else
355       raise "unknown chunk type #{chunk.class.name}"
356     end
357   end
358
359   def view_attachment a
360     BufferManager.flash "viewing #{a.content_type} attachment..."
361     a.view!
362     BufferManager.erase_flash
363     BufferManager.completely_redraw_screen
364   end
365
366 end
367
368 end