]> git.cworth.org Git - sup/blob - lib/sup/modes/thread-index-mode.rb
052b9730ecfe0e6cac14ed8769c73fb408cc52f1
[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     Redwood::reporting_thread do
51       Redwood::log "loading messages for thread"
52       mode = ThreadViewMode.new t, @hidden_labels
53       BufferManager.spawn t.subj, mode
54       BufferManager.draw_screen
55     end
56   end
57   
58   def handle_starred_update m
59     return unless(t = @ts.thread_for m)
60     @starred_cache[t] = t.has_label? :starred
61     update_text_for_line @lines[t]
62   end
63
64   def handle_read_update m
65     return unless(t = @ts.thread_for m)
66     @new_cache[t] = false
67     update_text_for_line @lines[t]
68   end
69
70   ## overwrite me!
71   def is_relevant? m; false; end
72
73   def handle_add_update m
74     if is_relevant?(m) || @ts.is_relevant?(m)
75       @ts.load_thread_for_message m
76       update
77     end
78   end
79
80   def handle_delete_update mid
81     if @ts.contains_id? mid
82       @ts.remove mid
83       update
84     end
85   end
86
87   def update
88     ## let's see you do THIS in python
89     @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| t.date }.reverse
90     @size_width = (@threads.map { |t| t.size }.max || 0).num_digits
91     regen_text
92   end
93
94   def edit_message
95     t = @threads[curpos] or return
96     message, *crap = t.find { |m, *o| m.has_label? :draft }
97     if message
98       mode = ResumeMode.new message
99       BufferManager.spawn "Edit message", mode
100     else
101       BufferManager.flash "Not a draft message!"
102     end
103   end
104
105   def toggle_starred
106     t = @threads[curpos] or return
107     @starred_cache[t] = t.toggle_label :starred
108     update_text_for_line curpos
109     cursor_down
110   end
111
112   def multi_toggle_starred threads
113     threads.each { |t| @starred_cache[t] = t.toggle_label :starred }
114     regen_text
115   end
116
117   def toggle_archived
118     return unless(t = @threads[curpos])
119     t.toggle_label :inbox
120     update_text_for_line curpos
121     cursor_down
122   end
123
124   def multi_toggle_archived threads
125     threads.each { |t| t.toggle_label :inbox }
126     regen_text
127   end
128
129   def toggle_new
130     t = @threads[curpos] or return
131     @new_cache[t] = t.toggle_label :unread
132     update_text_for_line curpos
133     cursor_down
134   end
135
136   def multi_toggle_new threads
137     threads.each { |t| @new_cache[t] = t.toggle_label :unread }
138     regen_text
139   end
140
141   def multi_toggle_tagged threads
142     @tags.drop_all_tags
143     regen_text
144   end
145
146   def jump_to_next_new
147     t = @threads[curpos] or return
148     n = ((curpos + 1) .. lines).find { |i| @new_cache[@threads[i]] }
149     n = (0 ... curpos).find { |i| @new_cache[@threads[i]] } unless n
150     if n
151       set_cursor_pos n
152     else
153       BufferManager.flash "No new messages"
154     end
155   end
156
157   def mark_as_spam
158     t = @threads[curpos] or return
159     multi_mark_as_spam [t]
160   end
161
162   def multi_mark_as_spam threads
163     threads.each do |t|
164       t.apply_label :spam
165       hide_thread t
166     end
167     regen_text
168   end
169
170   def kill
171     t = @threads[curpos] or return
172     multi_kill [t]
173   end
174
175   def multi_kill threads
176     threads.each do |t|
177       t.apply_label :killed
178       hide_thread t
179     end
180     regen_text
181   end
182
183   def save
184     threads = @threads + @hidden_threads.keys
185     mbid = BufferManager.say "Saving threads..."
186     threads.each_with_index do |t, i|
187       if i % 5 == 0
188         BufferManager.say "Saving thread #{i + 1} of #{threads.length}...",
189                           mbid
190       end
191       t.save Index
192     end
193     BufferManager.clear mbid
194   end
195
196   def cleanup
197     UpdateManager.unregister self
198
199     if @load_thread
200       @load_thread.kill 
201       BufferManager.clear @mbid if @mbid
202       sleep 0.1 # TODO: necessary?
203       BufferManager.erase_flash
204     end
205     save
206     super
207   end
208
209   def toggle_tagged
210     t = @threads[curpos] or return
211     @tags.toggle_tag_for t
212     update_text_for_line curpos
213     cursor_down
214   end
215
216   def apply_to_tagged; @tags.apply_to_tagged; end
217
218   def edit_labels
219     thread = @threads[curpos]
220     speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
221     keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
222     label_string = modifyl.join(" ")
223
224     answer = BufferManager.ask :edit_labels, "edit labels: ", label_string
225     return unless answer
226     user_labels = answer.split(/\s+/).map { |l| l.intern }
227     
228     hl = user_labels.select { |l| speciall.member? l }
229     if hl.empty?
230       thread.labels = keepl + user_labels
231       user_labels.each { |l| LabelManager << l }
232     else
233       BufferManager.flash "'#{hl}' is a reserved label!"
234     end
235     update_text_for_line curpos
236   end
237
238   def multi_edit_labels threads
239     answer = BufferManager.ask :add_labels, "add labels: "
240     return unless answer
241     user_labels = answer.split(/\s+/).map { |l| l.intern }
242     
243     hl = user_labels.select { |l| @hidden_labels.member? l }
244     if hl.empty?
245       threads.each { |t| user_labels.each { |l| t.apply_label l } }
246       user_labels.each { |l| LabelManager << l }
247     else
248       BufferManager.flash "'#{hl}' is a reserved label!"
249     end
250     regen_text
251   end
252
253   def reply
254     t = @threads[curpos] or return
255     m = t.latest_message
256     return if m.nil? # probably won't happen
257     mode = ReplyMode.new m
258     BufferManager.spawn "Reply to #{m.subj}", mode
259   end
260
261   def forward
262     t = @threads[curpos] or return
263     m = t.latest_message
264     return if m.nil? # probably won't happen
265     mode = ForwardMode.new m
266     BufferManager.spawn "Forward of #{m.subj}", mode
267     mode.edit
268   end
269
270   def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
271     return if @load_thread
272     @load_thread = Redwood::reporting_thread do 
273       num = load_n_threads n, opts
274       opts[:when_done].call(num) if opts[:when_done]
275       @load_thread = nil
276     end
277   end
278
279   def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
280     @mbid = BufferManager.say "Searching for threads..."
281     orig_size = @ts.size
282     @ts.load_n_threads(@ts.size + n, opts) do |i|
283       BufferManager.say "Loaded #{i} threads...", @mbid
284       if i % 5 == 0
285         update
286         BufferManager.draw_screen
287       end
288     end
289     @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
290
291     update
292     BufferManager.clear @mbid
293     @mbid = nil
294     BufferManager.draw_screen
295     @ts.size - orig_size
296   end
297
298   def status
299     "line #{curpos + 1} of #{lines} #{dirty? ? '*modified*' : ''}"
300   end
301
302 protected
303
304   def cursor_thread; @threads[curpos]; end
305
306   def drop_all_threads
307     @tags.drop_all_tags
308     initialize_threads
309     update
310   end
311
312   def remove_label_and_hide_thread t, label
313     t.remove_label label
314     hide_thread t
315   end
316
317   def hide_thread t
318     raise "already hidden" if @hidden_threads[t]
319     @hidden_threads[t] = true
320     @threads.delete t
321     @tags.drop_tag_for t
322   end
323
324   def update_text_for_line l
325     return unless l # not sure why this happens, but it does, occasionally
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 + (t.subj.empty? ? "" : " ")],
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