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,338 @@
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
@@ -0,0 +1,120 @@
1
+ module Redwood
2
+
3
+ class Person
4
+ @@email_map = {}
5
+
6
+ attr_accessor :name, :email
7
+
8
+ def initialize name, email
9
+ raise ArgumentError, "email can't be nil" unless email
10
+ @name =
11
+ if name
12
+ name.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
13
+ else
14
+ nil
15
+ end
16
+ @email = email.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ").downcase
17
+ @@email_map[@email] = self
18
+ end
19
+
20
+ def == o; o && o.email == email; end
21
+ alias :eql? :==
22
+
23
+ def hash
24
+ [name, email].hash
25
+ end
26
+
27
+ def shortname
28
+ case @name
29
+ when /\S+, (\S+)/
30
+ $1
31
+ when /(\S+) \S+/
32
+ $1
33
+ when nil
34
+ @email #[0 ... 10]
35
+ else
36
+ @name #[0 ... 10]
37
+ end
38
+ end
39
+
40
+ def longname
41
+ if @name && @email
42
+ "#@name <#@email>"
43
+ else
44
+ @email
45
+ end
46
+ end
47
+
48
+ def mediumname
49
+ if @name
50
+ name
51
+ else
52
+ @email
53
+ end
54
+ end
55
+
56
+ def full_address
57
+ if @name && @email
58
+ if @name =~ /"/
59
+ "#{@name.inspect} <#@email>"
60
+ else
61
+ "#@name <#@email>"
62
+ end
63
+ else
64
+ @email
65
+ end
66
+ end
67
+
68
+ def sort_by_me
69
+ case @name
70
+ when /^(\S+), \S+/
71
+ $1
72
+ when /^\S+ \S+ (\S+)/
73
+ $1
74
+ when /^\S+ (\S+)/
75
+ $1
76
+ when nil
77
+ @email
78
+ else
79
+ @name
80
+ end.downcase
81
+ end
82
+
83
+ def self.for_several s
84
+ return [] if s.nil?
85
+
86
+ begin
87
+ s.split_on_commas.map { |ss| self.for ss }
88
+ rescue StandardError => e
89
+ raise "#{e.message}: for #{s.inspect}"
90
+ end
91
+ end
92
+
93
+ def self.for s
94
+ return nil if s.nil?
95
+ name, email =
96
+ case s
97
+ when /["'](.*?)["'] <(.*?)>/, /([^,]+) <(.*?)>/
98
+ a, b = $1, $2
99
+ [a.gsub('\"', '"'), b]
100
+ when /<((\S+?)@\S+?)>/
101
+ [$2, $1]
102
+ when /((\S+?)@\S+)/
103
+ [$2, $1]
104
+ else
105
+ [nil, s]
106
+ end
107
+
108
+ if name && (p = @@email_map[email])
109
+ ## all else being equal, prefer longer names, unless the prior name
110
+ ## doesn't contain any capitalization
111
+ p.name = name if (p.name.nil? || p.name.length < name.length) unless
112
+ p.name =~ /[A-Z]/ || (AccountManager.instantiated? && AccountManager.is_account?(p))
113
+ p
114
+ else
115
+ Person.new name, email
116
+ end
117
+ end
118
+ end
119
+
120
+ end