]> git.cworth.org Git - sup/blob - lib/sup/modes/thread-index-mode.rb
84c3441e8dc219b6747d5274caffaa860155e459
[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       if i % 5 == 0
184         BufferManager.say "Saving thread #{i + 1} of #{threads.length}...",
185                           mbid
186       end
187       t.save Index
188     end
189     BufferManager.clear mbid
190   end
191
192   def cleanup
193     UpdateManager.unregister self
194
195     if @load_thread
196       @load_thread.kill 
197       BufferManager.clear @mbid if @mbid
198       sleep 0.1 # TODO: necessary?
199       BufferManager.erase_flash
200     end
201     save
202     super
203   end
204
205   def toggle_tagged
206     t = @threads[curpos] or return
207     @tags.toggle_tag_for t
208     update_text_for_line curpos
209     cursor_down
210   end
211
212   def apply_to_tagged; @tags.apply_to_tagged; end
213
214   def edit_labels
215     thread = @threads[curpos]
216     speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
217     keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
218     label_string = modifyl.join(" ")
219
220     answer = BufferManager.ask :edit_labels, "edit labels: ", label_string
221     return unless answer
222     user_labels = answer.split(/\s+/).map { |l| l.intern }
223     
224     hl = user_labels.select { |l| speciall.member? l }
225     if hl.empty?
226       thread.labels = keepl + user_labels
227       user_labels.each { |l| LabelManager << l }
228     else
229       BufferManager.flash "'#{hl}' is a reserved label!"
230     end
231     update_text_for_line curpos
232   end
233
234   def multi_edit_labels threads
235     answer = BufferManager.ask :add_labels, "add labels: "
236     return unless answer
237     user_labels = answer.split(/\s+/).map { |l| l.intern }
238     
239     hl = user_labels.select { |l| @hidden_labels.member? l }
240     if hl.empty?
241       threads.each { |t| user_labels.each { |l| t.apply_label l } }
242       user_labels.each { |l| LabelManager << l }
243     else
244       BufferManager.flash "'#{hl}' is a reserved label!"
245     end
246     regen_text
247   end
248
249   def reply
250     t = @threads[curpos] or return
251     m = t.latest_message
252     return if m.nil? # probably won't happen
253     mode = ReplyMode.new m
254     BufferManager.spawn "Reply to #{m.subj}", mode
255   end
256
257   def forward
258     t = @threads[curpos] or return
259     m = t.latest_message
260     return if m.nil? # probably won't happen
261     mode = ForwardMode.new m
262     BufferManager.spawn "Forward of #{m.subj}", mode
263     mode.edit
264   end
265
266   def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
267     return if @load_thread
268     @load_thread = Redwood::reporting_thread do 
269       num = load_n_threads n, opts
270       opts[:when_done].call(num) if opts[:when_done]
271       @load_thread = nil
272     end
273   end
274
275   def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
276     @mbid = BufferManager.say "Searching for threads..."
277     orig_size = @ts.size
278     @ts.load_n_threads(@ts.size + n, opts) do |i|
279       BufferManager.say "Loaded #{i} threads...", @mbid
280       if i % 5 == 0
281         update
282         BufferManager.draw_screen
283       end
284     end
285     @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
286
287     update
288     BufferManager.clear @mbid
289     @mbid = nil
290     BufferManager.draw_screen
291     @ts.size - orig_size
292   end
293
294   def status
295     "line #{curpos + 1} of #{lines} #{dirty? ? '*modified*' : ''}"
296   end
297
298 protected
299
300   def cursor_thread; @threads[curpos]; end
301
302   def drop_all_threads
303     @tags.drop_all_tags
304     initialize_threads
305     update
306   end
307
308   def remove_label_and_hide_thread t, label
309     t.remove_label label
310     hide_thread t
311   end
312
313   def hide_thread t
314     raise "already hidden" if @hidden_threads[t]
315     @hidden_threads[t] = true
316     @threads.delete t
317     @tags.drop_tag_for t
318   end
319
320   def update_text_for_line l
321     return unless l # not sure why this happens, but it does, occasionally
322     @text[l] = text_for_thread @threads[l]
323     buffer.mark_dirty if buffer
324   end
325
326   def regen_text
327     @text = @threads.map_with_index { |t, i| text_for_thread t }
328     @lines = @threads.map_with_index { |t, i| [t, i] }.to_h
329     buffer.mark_dirty if buffer
330   end
331   
332   def author_text_for_thread t
333     if t.authors.size == 1
334       t.authors.first.mediumname
335     else
336       t.authors.map { |p| AccountManager.is_account?(p) ? "me" : p.shortname }.join ", "
337     end
338   end
339
340   def text_for_thread t
341     date = (@date_cache[t] ||= t.date.to_nice_s(Time.now)) 
342     from = (@who_cache[t] ||= author_text_for_thread(t))
343     if from.length > @from_width
344       from = from[0 ... (@from_width - 1)]
345       from += "." unless from[-1] == ?\s
346     end
347
348     new = @new_cache.member?(t) ? @new_cache[t] : @new_cache[t] = t.has_label?(:unread)
349     starred = @starred_cache.member?(t) ? @starred_cache[t] : @starred_cache[t] = t.has_label?(:starred)
350
351     dp = (@dp_cache[t] ||= t.direct_participants.any? { |p| AccountManager.is_account? p })
352     p = (@p_cache[t] ||= (dp || t.participants.any? { |p| AccountManager.is_account? p }))
353
354     base_color = (new ? :index_new_color : :index_old_color)
355     [ 
356       [:tagged_color, @tags.tagged?(t) ? ">" : " "],
357       [:none, sprintf("%#{@date_width}s ", date)],
358       [base_color, sprintf("%-#{@from_width}s", from)],
359       [:starred_color, starred ? "*" : " "],
360       [:none, t.size == 1 ? " " * (@size_width + 2) : sprintf("(%#{@size_width}d)", t.size)],
361       [:to_me_color, dp ? " >" : (p ? ' +' : "  ")],
362       [base_color, t.subj + (t.subj.empty? ? "" : " ")],
363     ] +
364       (t.labels - @hidden_labels).map { |label| [:label_color, "+#{label} "] } +
365       [[:snippet_color, t.snippet]
366     ]
367   end
368
369   def dirty?; (@hidden_threads.keys + @threads).any? { |t| t.dirty? }; end
370
371 private
372
373   def initialize_threads
374     @ts = ThreadSet.new Index.instance
375     @date_cache = {}
376     @who_cache = {}
377     @dp_cache = {}
378     @p_cache = {}
379     @new_cache = {}
380     @starred_cache = {}
381     @hidden_threads = {}
382   end
383 end
384
385 end