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