]> git.cworth.org Git - sup/blob - lib/sup/modes/thread-index-mode.rb
moved evertying to devel
[sup] / lib / sup / modes / thread-index-mode.rb
1 module Redwood
2
3 class ThreadIndexMode < LineCursorMode
4   DATE_WIDTH = Time::TO_NICE_S_MAX_LEN
5   FROM_WIDTH = 15
6   LOAD_MORE_THREAD_NUM = 20
7
8   register_keymap do |k|
9     k.add :toggle_archived, "Toggle archived status", 'a'
10     k.add :toggle_starred, "Star or unstar all messages in thread", '*'
11     k.add :toggle_new, "Toggle new/read status of all messages in thread", 'N'
12     k.add :edit_labels, "Edit or add labels for a thread", 'l'
13     k.add :edit_message, "Edit message (drafts only)", 'e'
14     k.add :mark_as_spam, "Mark thread as spam", 'S'
15     k.add :kill, "Kill thread (never to be seen in inbox again)", 'K'
16     k.add :save, "Save changes now", '$'
17     k.add :jump_to_next_new, "Jump to next new thread", :tab
18     k.add :reply, "Reply to a thread", 'r'
19     k.add :forward, "Forward a thread", 'f'
20     k.add :toggle_tagged, "Tag/untag current line", 't'
21     k.add :apply_to_tagged, "Apply next command to all tagged threads", ';'
22   end
23
24   def initialize required_labels=[], hidden_labels=[]
25     super()
26     @load_thread = nil
27     @required_labels = required_labels
28     @hidden_labels = hidden_labels + LabelManager::HIDDEN_LABELS
29     @date_width = DATE_WIDTH
30     @from_width = FROM_WIDTH
31     @size_width = nil
32
33     @tags = Tagger.new self
34     
35     initialize_threads
36     update
37
38     UpdateManager.register self
39   end
40
41   def lines; @text.length; end
42   def [] i; @text[i]; end
43
44   ## open up a thread view window
45   def select
46     this_curpos = curpos
47     t = @threads[this_curpos]
48
49     ## TODO: don't regen text completely
50     mode = ThreadViewMode.new t, @hidden_labels
51     BufferManager.spawn t.subj, mode
52   end
53   
54   def handle_starred_update m
55     return unless(t = @ts.thread_for m)
56     @starred_cache[t] = t.has_label? :starred
57     update_text_for_line @lines[t]
58   end
59
60   def handle_read_update m
61     return unless(t = @ts.thread_for m)
62     @new_cache[t] = false
63     update_text_for_line @lines[t]
64   end
65
66   ## overwrite me!
67   def is_relevant? m; false; end
68
69   def handle_add_update m
70     if is_relevant?(m) || @ts.is_relevant?(m)
71       @ts.load_thread_for_message m
72       update
73     end
74   end
75
76   def handle_delete_update mid
77     if @ts.contains_id? mid
78       @ts.remove mid
79       update
80     end
81   end
82
83   def update
84     ## let's see you do THIS in python
85     @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| t.date }.reverse
86     @size_width = (@threads.map { |t| t.size }.max || 0).num_digits
87     regen_text
88   end
89
90   def edit_message
91     t = @threads[curpos] or return
92     message, *crap = t.find { |m, *o| m.has_label? :draft }
93     if message
94       mode = ResumeMode.new message
95       BufferManager.spawn "Edit message", mode
96     else
97       BufferManager.flash "Not a draft message!"
98     end
99   end
100
101   def toggle_starred
102     t = @threads[curpos] or return
103     @starred_cache[t] = t.toggle_label :starred
104     update_text_for_line curpos
105     cursor_down
106   end
107
108   def multi_toggle_starred threads
109     threads.each { |t| @starred_cache[t] = t.toggle_label :starred }
110     regen_text
111   end
112
113   def toggle_archived
114     return unless(t = @threads[curpos])
115     t.toggle_label :inbox
116     update_text_for_line curpos
117     cursor_down
118   end
119
120   def multi_toggle_archived threads
121     threads.each { |t| t.toggle_label :inbox }
122     regen_text
123   end
124
125   def toggle_new
126     t = @threads[curpos] or return
127     @new_cache[t] = t.toggle_label :unread
128     update_text_for_line curpos
129     cursor_down
130   end
131
132   def multi_toggle_new threads
133     threads.each { |t| @new_cache[t] = t.toggle_label :unread }
134     regen_text
135   end
136
137   def multi_toggle_tagged threads
138     @tags.drop_all_tags
139     regen_text
140   end
141
142   def jump_to_next_new
143     t = @threads[curpos] or return
144     n = ((curpos + 1) .. lines).find { |i| @new_cache[@threads[i]] }
145     n = (0 ... curpos).find { |i| @new_cache[@threads[i]] } unless n
146     if n
147       set_cursor_pos n
148     else
149       BufferManager.flash "No new messages"
150     end
151   end
152
153   def mark_as_spam
154     t = @threads[curpos] or return
155     multi_mark_as_spam [t]
156   end
157
158   def multi_mark_as_spam threads
159     threads.each do |t|
160       t.apply_label :spam
161       hide_thread t
162     end
163     regen_text
164   end
165
166   def kill
167     t = @threads[curpos] or return
168     multi_kill [t]
169   end
170
171   def multi_kill threads
172     threads.each do |t|
173       t.apply_label :killed
174       hide_thread t
175     end
176     regen_text
177   end
178
179   def save
180     threads = @threads + @hidden_threads.keys
181     mbid = BufferManager.say "Saving threads..."
182     threads.each_with_index do |t, i|
183       BufferManager.say "Saving thread #{i + 1} of #{threads.length}...",
184                         mbid
185       t.save Index
186     end
187     BufferManager.clear mbid
188   end
189
190   def cleanup
191     UpdateManager.unregister self
192
193     if @load_thread
194       @load_thread.kill 
195       BufferManager.clear @mbid if @mbid
196       sleep 0.1 # TODO: necessary?
197       BufferManager.erase_flash
198     end
199     save
200     super
201   end
202
203   def toggle_tagged
204     t = @threads[curpos] or return
205     @tags.toggle_tag_for t
206     update_text_for_line curpos
207     cursor_down
208   end
209
210   def apply_to_tagged; @tags.apply_to_tagged; end
211
212   def edit_labels
213     thread = @threads[curpos]
214     speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
215     keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
216     label_string = modifyl.join(" ")
217
218     answer = BufferManager.ask :edit_labels, "edit labels: ", label_string
219     return unless answer
220     user_labels = answer.split(/\s+/).map { |l| l.intern }
221     
222     hl = user_labels.select { |l| speciall.member? l }
223     if hl.empty?
224       thread.labels = keepl + user_labels
225       user_labels.each { |l| LabelManager << l }
226     else
227       BufferManager.flash "'#{hl}' is a reserved label!"
228     end
229     update_text_for_line curpos
230   end
231
232   def multi_edit_labels threads
233     answer = BufferManager.ask :add_labels, "add labels: "
234     return unless answer
235     user_labels = answer.split(/\s+/).map { |l| l.intern }
236     
237     hl = user_labels.select { |l| @hidden_labels.member? l }
238     if hl.empty?
239       threads.each { |t| user_labels.each { |l| t.apply_label l } }
240       user_labels.each { |l| LabelManager << l }
241     else
242       BufferManager.flash "'#{hl}' is a reserved label!"
243     end
244     regen_text
245   end
246
247   def reply
248     t = @threads[curpos] or return
249     m = t.latest_message
250     return if m.nil? # probably won't happen
251     mode = ReplyMode.new m
252     BufferManager.spawn "Reply to #{m.subj}", mode
253   end
254
255   def forward
256     t = @threads[curpos] or return
257     m = t.latest_message
258     return if m.nil? # probably won't happen
259     mode = ForwardMode.new m
260     BufferManager.spawn "Forward of #{m.subj}", mode
261     mode.edit
262   end
263
264   def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
265     return if @load_thread
266     @load_thread = ::Thread.new do 
267       begin
268         num = load_n_threads n, opts
269         opts[:when_done].call(num) if opts[:when_done]
270       rescue Exception => e
271         $exception ||= e
272         raise
273       end
274       @load_thread = nil
275     end
276   end
277
278   def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
279     @mbid = BufferManager.say "Searching for threads..."
280     orig_size = @ts.size
281     @ts.load_n_threads(@ts.size + n, opts) do |i|
282       BufferManager.say "Loaded #{i} threads...", @mbid
283       if i % 5 == 0
284         update
285         BufferManager.draw_screen
286       end
287     end
288     @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
289
290     update
291     BufferManager.clear @mbid
292     @mbid = nil
293
294     BufferManager.draw_screen
295
296     @ts.size - orig_size
297   end
298
299   def status
300     "line #{curpos + 1} of #{lines} #{dirty? ? '*modified*' : ''}"
301   end
302
303 protected
304
305   def cursor_thread; @threads[curpos]; end
306
307   def drop_all_threads
308     @tags.drop_all_tags
309     initialize_threads
310     update
311   end
312
313   def remove_label_and_hide_thread t, label
314     t.remove_label label
315     hide_thread t
316   end
317
318   def hide_thread t
319     raise "already hidden" if @hidden_threads[t]
320     @hidden_threads[t] = true
321     @threads.delete t
322     @tags.drop_tag_for t
323   end
324
325   def update_text_for_line l
326     @text[l] = text_for_thread @threads[l]
327     buffer.mark_dirty if buffer
328   end
329
330   def regen_text
331     @text = @threads.map_with_index { |t, i| text_for_thread t }
332     @lines = @threads.map_with_index { |t, i| [t, i] }.to_h
333     buffer.mark_dirty if buffer
334   end
335   
336   def author_text_for_thread t
337     if t.authors.size == 1
338       t.authors.first.mediumname
339     else
340       t.authors.map { |p| AccountManager.is_account?(p) ? "me" : p.shortname }.join ", "
341     end
342   end
343
344   def text_for_thread t
345     date = (@date_cache[t] ||= t.date.to_nice_s(Time.now)) 
346     from = (@who_cache[t] ||= author_text_for_thread(t))
347     if from.length > @from_width
348       from = from[0 ... (@from_width - 1)]
349       from += "." unless from[-1] == ?\s
350     end
351
352     new = @new_cache.member?(t) ? @new_cache[t] : @new_cache[t] = t.has_label?(:unread)
353     starred = @starred_cache.member?(t) ? @starred_cache[t] : @starred_cache[t] = t.has_label?(:starred)
354
355     dp = (@dp_cache[t] ||= t.direct_participants.any? { |p| AccountManager.is_account? p })
356     p = (@p_cache[t] ||= (dp || t.participants.any? { |p| AccountManager.is_account? p }))
357
358     base_color = (new ? :index_new_color : :index_old_color)
359     [ 
360       [:tagged_color, @tags.tagged?(t) ? ">" : " "],
361       [:none, sprintf("%#{@date_width}s ", date)],
362       [base_color, sprintf("%-#{@from_width}s ", from)],
363       [:starred_color, starred ? "*" : " "],
364       [:none, t.size == 1 ? " " * (@size_width + 2) : sprintf("(%#{@size_width}d)", t.size)],
365       [:to_me_color, dp ? " >" : (p ? ' -' : "  ")],
366       [base_color, t.subj]
367     ] +
368       (t.labels - @hidden_labels).map { |label| [:label_color, " +#{label}"] } +
369       [[:snippet_color, " " + t.snippet]
370     ]
371   end
372
373   def dirty?; (@hidden_threads.keys + @threads).any? { |t| t.dirty? }; end
374
375 private
376
377   def initialize_threads
378     @ts = ThreadSet.new Index.instance
379     @date_cache = {}
380     @who_cache = {}
381     @dp_cache = {}
382     @p_cache = {}
383     @new_cache = {}
384     @starred_cache = {}
385     @hidden_threads = {}
386   end
387 end
388
389 end