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.
- data/History.txt +5 -0
- data/LICENSE +280 -0
- data/Manifest.txt +52 -0
- data/README.txt +119 -0
- data/Rakefile +45 -0
- data/bin/sup +229 -0
- data/bin/sup-import +162 -0
- data/doc/FAQ.txt +38 -0
- data/doc/Philosophy.txt +59 -0
- data/doc/TODO +31 -0
- data/lib/sup.rb +141 -0
- data/lib/sup/account.rb +53 -0
- data/lib/sup/buffer.rb +391 -0
- data/lib/sup/colormap.rb +118 -0
- data/lib/sup/contact.rb +40 -0
- data/lib/sup/draft.rb +105 -0
- data/lib/sup/index.rb +353 -0
- data/lib/sup/keymap.rb +89 -0
- data/lib/sup/label.rb +41 -0
- data/lib/sup/logger.rb +42 -0
- data/lib/sup/mbox.rb +51 -0
- data/lib/sup/mbox/loader.rb +116 -0
- data/lib/sup/message.rb +302 -0
- data/lib/sup/mode.rb +79 -0
- data/lib/sup/modes/buffer-list-mode.rb +37 -0
- data/lib/sup/modes/compose-mode.rb +33 -0
- data/lib/sup/modes/contact-list-mode.rb +121 -0
- data/lib/sup/modes/edit-message-mode.rb +162 -0
- data/lib/sup/modes/forward-mode.rb +38 -0
- data/lib/sup/modes/help-mode.rb +19 -0
- data/lib/sup/modes/inbox-mode.rb +45 -0
- data/lib/sup/modes/label-list-mode.rb +89 -0
- data/lib/sup/modes/label-search-results-mode.rb +29 -0
- data/lib/sup/modes/line-cursor-mode.rb +133 -0
- data/lib/sup/modes/log-mode.rb +44 -0
- data/lib/sup/modes/person-search-results-mode.rb +29 -0
- data/lib/sup/modes/poll-mode.rb +24 -0
- data/lib/sup/modes/reply-mode.rb +136 -0
- data/lib/sup/modes/resume-mode.rb +18 -0
- data/lib/sup/modes/scroll-mode.rb +106 -0
- data/lib/sup/modes/search-results-mode.rb +31 -0
- data/lib/sup/modes/text-mode.rb +51 -0
- data/lib/sup/modes/thread-index-mode.rb +389 -0
- data/lib/sup/modes/thread-view-mode.rb +338 -0
- data/lib/sup/person.rb +120 -0
- data/lib/sup/poll.rb +80 -0
- data/lib/sup/sent.rb +46 -0
- data/lib/sup/tagger.rb +40 -0
- data/lib/sup/textfield.rb +83 -0
- data/lib/sup/thread.rb +358 -0
- data/lib/sup/update.rb +21 -0
- data/lib/sup/util.rb +260 -0
- 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
|
data/lib/sup/person.rb
ADDED
@@ -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
|