sup 0.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (53) hide show
  1. data/History.txt +5 -0
  2. data/LICENSE +280 -0
  3. data/Manifest.txt +52 -0
  4. data/README.txt +119 -0
  5. data/Rakefile +45 -0
  6. data/bin/sup +229 -0
  7. data/bin/sup-import +162 -0
  8. data/doc/FAQ.txt +38 -0
  9. data/doc/Philosophy.txt +59 -0
  10. data/doc/TODO +31 -0
  11. data/lib/sup.rb +141 -0
  12. data/lib/sup/account.rb +53 -0
  13. data/lib/sup/buffer.rb +391 -0
  14. data/lib/sup/colormap.rb +118 -0
  15. data/lib/sup/contact.rb +40 -0
  16. data/lib/sup/draft.rb +105 -0
  17. data/lib/sup/index.rb +353 -0
  18. data/lib/sup/keymap.rb +89 -0
  19. data/lib/sup/label.rb +41 -0
  20. data/lib/sup/logger.rb +42 -0
  21. data/lib/sup/mbox.rb +51 -0
  22. data/lib/sup/mbox/loader.rb +116 -0
  23. data/lib/sup/message.rb +302 -0
  24. data/lib/sup/mode.rb +79 -0
  25. data/lib/sup/modes/buffer-list-mode.rb +37 -0
  26. data/lib/sup/modes/compose-mode.rb +33 -0
  27. data/lib/sup/modes/contact-list-mode.rb +121 -0
  28. data/lib/sup/modes/edit-message-mode.rb +162 -0
  29. data/lib/sup/modes/forward-mode.rb +38 -0
  30. data/lib/sup/modes/help-mode.rb +19 -0
  31. data/lib/sup/modes/inbox-mode.rb +45 -0
  32. data/lib/sup/modes/label-list-mode.rb +89 -0
  33. data/lib/sup/modes/label-search-results-mode.rb +29 -0
  34. data/lib/sup/modes/line-cursor-mode.rb +133 -0
  35. data/lib/sup/modes/log-mode.rb +44 -0
  36. data/lib/sup/modes/person-search-results-mode.rb +29 -0
  37. data/lib/sup/modes/poll-mode.rb +24 -0
  38. data/lib/sup/modes/reply-mode.rb +136 -0
  39. data/lib/sup/modes/resume-mode.rb +18 -0
  40. data/lib/sup/modes/scroll-mode.rb +106 -0
  41. data/lib/sup/modes/search-results-mode.rb +31 -0
  42. data/lib/sup/modes/text-mode.rb +51 -0
  43. data/lib/sup/modes/thread-index-mode.rb +389 -0
  44. data/lib/sup/modes/thread-view-mode.rb +338 -0
  45. data/lib/sup/person.rb +120 -0
  46. data/lib/sup/poll.rb +80 -0
  47. data/lib/sup/sent.rb +46 -0
  48. data/lib/sup/tagger.rb +40 -0
  49. data/lib/sup/textfield.rb +83 -0
  50. data/lib/sup/thread.rb +358 -0
  51. data/lib/sup/update.rb +21 -0
  52. data/lib/sup/util.rb +260 -0
  53. metadata +123 -0
@@ -0,0 +1,18 @@
1
+ module Redwood
2
+
3
+ class ResumeMode < ComposeMode
4
+ def initialize m
5
+ super()
6
+ @id = m.id
7
+ @header, @body = parse_file m.draft_filename
8
+ @header.delete "Date"
9
+ @header["Message-Id"] = gen_message_id # generate a new'n
10
+ regen_text
11
+ end
12
+
13
+ def send_message
14
+ DraftManager.discard @id if super
15
+ end
16
+ end
17
+
18
+ end
@@ -0,0 +1,106 @@
1
+ module Redwood
2
+
3
+ class ScrollMode < Mode
4
+ attr_reader :status, :topline, :botline
5
+
6
+ COL_JUMP = 2
7
+
8
+ register_keymap do |k|
9
+ k.add :line_down, "Down one line", :down, 'j', 'J'
10
+ k.add :line_up, "Up one line", :up, 'k', 'K'
11
+ k.add :col_left, "Left one column", :left, 'h'
12
+ k.add :col_right, "Right one column", :right, 'l'
13
+ k.add :page_down, "Down one page", :page_down, 'n', ' '
14
+ k.add :page_up, "Up one page", :page_up, 'p', :backspace
15
+ k.add :jump_to_home, "Jump to top", :home, '^', '1'
16
+ k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
17
+ end
18
+
19
+ def initialize opts={}
20
+ @topline, @botline, @leftcol = 0, 0, 0
21
+ @slip_rows = opts[:slip_rows] || 0 # when we pgup/pgdown,
22
+ # how many lines do we keep?
23
+ @twiddles = opts.member?(:twiddles) ? opts[:twiddles] : true
24
+ super()
25
+ end
26
+
27
+ def draw
28
+ ensure_mode_validity
29
+ (@topline ... @botline).each { |ln| draw_line ln }
30
+ ((@botline - @topline) ... buffer.content_height).each do |ln|
31
+ if @twiddles
32
+ buffer.write ln, 0, "~", :color => :twiddle_color
33
+ else
34
+ buffer.write ln, 0, ""
35
+ end
36
+ end
37
+ @status = "lines #{@topline + 1}:#{@botline}/#{lines}"
38
+ end
39
+
40
+ def col_left
41
+ return unless @leftcol > 0
42
+ @leftcol -= COL_JUMP
43
+ buffer.mark_dirty
44
+ end
45
+
46
+ def col_right
47
+ @leftcol += COL_JUMP
48
+ buffer.mark_dirty
49
+ end
50
+
51
+ ## set top line to l
52
+ def jump_to_line l
53
+ l = l.clamp 0, lines - 1
54
+ return if @topline == l
55
+ @topline = l
56
+ @botline = [l + buffer.content_height, lines].min
57
+ buffer.mark_dirty
58
+ end
59
+
60
+ def line_down; jump_to_line @topline + 1; end
61
+ def line_up; jump_to_line @topline - 1; end
62
+ def page_down; jump_to_line @topline + buffer.content_height - @slip_rows; end
63
+ def page_up; jump_to_line @topline - buffer.content_height + @slip_rows; end
64
+ def jump_to_home; jump_to_line 0; end
65
+ def jump_to_end; jump_to_line lines - buffer.content_height; end
66
+
67
+ def ensure_mode_validity
68
+ @topline = @topline.clamp 0, lines - 1
69
+ @topline = 0 if @topline < 0 # empty
70
+ @botline = [@topline + buffer.content_height, lines].min
71
+ end
72
+
73
+ protected
74
+
75
+ def draw_line ln, opts={}
76
+ case(s = self[ln])
77
+ when String
78
+ buffer.write ln - @topline, 0, s[@leftcol .. -1],
79
+ :highlight => opts[:highlight]
80
+ when Array
81
+ xpos = 0
82
+ s.each do |color, text|
83
+ raise "nil text for color '#{color}'" if text.nil?
84
+ if xpos + text.length < @leftcol
85
+ buffer.write ln - @topline, 0, "", :color => color,
86
+ :highlight => opts[:highlight]
87
+ xpos += text.length
88
+ ## nothing
89
+ elsif xpos < @leftcol
90
+ ## partial
91
+ buffer.write ln - @topline, 0, text[(@leftcol - xpos) .. -1],
92
+ :color => color,
93
+ :highlight => opts[:highlight]
94
+ xpos += text.length
95
+ else
96
+ buffer.write ln - @topline, xpos - @leftcol, text,
97
+ :color => color, :highlight => opts[:highlight]
98
+ xpos += text.length
99
+ end
100
+
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ end
@@ -0,0 +1,31 @@
1
+ module Redwood
2
+
3
+ class SearchResultsMode < ThreadIndexMode
4
+ register_keymap do |k|
5
+ k.add :load_more_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
6
+ end
7
+
8
+ def initialize content
9
+ raise ArgumentError, "no content" if content =~ /^\s*$/
10
+ @content = content.gsub(/[\(\)]/) { |x| "\\" + x }
11
+ super
12
+ end
13
+
14
+ ## TODO: think about this
15
+ def is_relevant? m; super; end
16
+
17
+ def load_more_threads n=ThreadIndexMode::LOAD_MORE_THREAD_NUM
18
+ load_n_threads_background n, :content => @content,
19
+ :load_killed => true,
20
+ :load_spam => false,
21
+ :when_done =>(lambda do |num|
22
+ if num > 0
23
+ BufferManager.flash "Found #{num} threads"
24
+ else
25
+ BufferManager.flash "No matches"
26
+ end
27
+ end)
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,51 @@
1
+ module Redwood
2
+
3
+ class TextMode < ScrollMode
4
+ attr_reader :text
5
+
6
+ def initialize text=""
7
+ @text = text
8
+ update_lines
9
+ buffer.mark_dirty if buffer
10
+ super()
11
+ end
12
+
13
+ def text= t
14
+ @text = t
15
+ update_lines
16
+ if buffer
17
+ ensure_mode_validity
18
+ buffer.mark_dirty
19
+ end
20
+ end
21
+
22
+ def << line
23
+ @lines = [0] if @text.empty?
24
+ @text << line
25
+ @lines << @text.length
26
+ if buffer
27
+ ensure_mode_validity
28
+ buffer.mark_dirty
29
+ end
30
+ end
31
+
32
+ def lines
33
+ @lines.length - 1
34
+ end
35
+
36
+ def [] i
37
+ return nil unless i < @lines.length
38
+ @text[@lines[i] ... (i + 1 < @lines.length ? @lines[i + 1] - 1 : @text.length)]
39
+ # (@lines[i] ... (i + 1 < @lines.length ? @lines[i + 1] - 1 : @text.length)).inspect
40
+ end
41
+
42
+ private
43
+
44
+ def update_lines
45
+ pos = @text.find_all_positions("\n")
46
+ pos.push @text.length unless pos.last == @text.length - 1
47
+ @lines = [0] + pos.map { |x| x + 1 }
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,389 @@
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