sup 0.7 → 0.8
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/CONTRIBUTORS +8 -3
- data/History.txt +19 -0
- data/README.txt +45 -44
- data/ReleaseNotes +6 -0
- data/bin/sup +36 -5
- data/bin/sup-add +0 -0
- data/bin/sup-config +0 -0
- data/bin/sup-dump +0 -0
- data/bin/sup-recover-sources +8 -12
- data/bin/sup-sync +22 -16
- data/bin/sup-sync-back +1 -1
- data/bin/sup-tweak-labels +8 -8
- data/lib/sup.rb +3 -17
- data/lib/sup/account.rb +2 -3
- data/lib/sup/buffer.rb +21 -10
- data/lib/sup/colormap.rb +30 -27
- data/lib/sup/contact.rb +1 -1
- data/lib/sup/draft.rb +1 -3
- data/lib/sup/imap.rb +1 -1
- data/lib/sup/index.rb +70 -48
- data/lib/sup/label.rb +12 -10
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +1 -1
- data/lib/sup/mbox.rb +13 -70
- data/lib/sup/mbox/loader.rb +26 -15
- data/lib/sup/message-chunks.rb +18 -6
- data/lib/sup/message.rb +56 -67
- data/lib/sup/mode.rb +2 -1
- data/lib/sup/modes/buffer-list-mode.rb +6 -2
- data/lib/sup/modes/compose-mode.rb +0 -1
- data/lib/sup/modes/contact-list-mode.rb +1 -1
- data/lib/sup/modes/edit-message-mode.rb +37 -9
- data/lib/sup/modes/inbox-mode.rb +34 -0
- data/lib/sup/modes/label-list-mode.rb +10 -3
- data/lib/sup/modes/reply-mode.rb +24 -13
- data/lib/sup/modes/resume-mode.rb +2 -0
- data/lib/sup/modes/scroll-mode.rb +10 -9
- data/lib/sup/modes/search-results-mode.rb +2 -2
- data/lib/sup/modes/thread-index-mode.rb +157 -38
- data/lib/sup/modes/thread-view-mode.rb +27 -11
- data/lib/sup/person.rb +22 -73
- data/lib/sup/poll.rb +18 -20
- data/lib/sup/source.rb +44 -0
- data/lib/sup/undo.rb +39 -0
- data/lib/sup/util.rb +25 -16
- metadata +46 -45
data/lib/sup/mode.rb
CHANGED
@@ -24,6 +24,7 @@ class Mode
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def killable?; true; end
|
27
|
+
def unsaved?; false end
|
27
28
|
def draw; end
|
28
29
|
def focus; end
|
29
30
|
def blur; end
|
@@ -57,7 +58,7 @@ class Mode
|
|
57
58
|
title = "Keybindings from #{Mode.make_name klass.name}"
|
58
59
|
s = <<EOS
|
59
60
|
#{title}
|
60
|
-
#{'-' * title.
|
61
|
+
#{'-' * title.display_length}
|
61
62
|
|
62
63
|
#{km.help_text used_keys}
|
63
64
|
EOS
|
@@ -16,6 +16,7 @@ class BufferListMode < LineCursorMode
|
|
16
16
|
|
17
17
|
def focus
|
18
18
|
reload # buffers may have been killed or created since last view
|
19
|
+
set_cursor_pos 0
|
19
20
|
end
|
20
21
|
|
21
22
|
protected
|
@@ -26,10 +27,13 @@ protected
|
|
26
27
|
end
|
27
28
|
|
28
29
|
def regen_text
|
29
|
-
@bufs = BufferManager.buffers.
|
30
|
+
@bufs = BufferManager.buffers.reject { |name, buf| buf.mode == self }.sort_by { |name, buf| buf.atime }.reverse
|
30
31
|
width = @bufs.max_of { |name, buf| buf.mode.name.length }
|
31
32
|
@text = @bufs.map do |name, buf|
|
32
|
-
|
33
|
+
base_color = buf.system? ? :system_buf_color : :regular_buf_color
|
34
|
+
[[base_color, sprintf("%#{width}s ", buf.mode.name)],
|
35
|
+
[:modified_buffer_color, (buf.mode.unsaved? ? '*' : ' ')],
|
36
|
+
[base_color, " " + name]]
|
33
37
|
end
|
34
38
|
end
|
35
39
|
|
@@ -5,7 +5,6 @@ class ComposeMode < EditMessageMode
|
|
5
5
|
header = {}
|
6
6
|
header["From"] = (opts[:from] || AccountManager.default_account).full_address
|
7
7
|
header["To"] = opts[:to].map { |p| p.full_address }.join(", ") if opts[:to]
|
8
|
-
header["To"] = opts[:to].map { |p| p.full_address }.join(", ") if opts[:to]
|
9
8
|
header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
|
10
9
|
header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
|
11
10
|
header["Subject"] = opts[:subj] if opts[:subj]
|
@@ -23,7 +23,7 @@ class ContactListMode < LineCursorMode
|
|
23
23
|
k.add :reload, "Drop contact list and reload", 'D'
|
24
24
|
k.add :alias, "Edit alias/or name for contact", 'a', 'i'
|
25
25
|
k.add :toggle_tagged, "Tag/untag current line", 't'
|
26
|
-
k.add :apply_to_tagged, "Apply next command to all tagged items", '
|
26
|
+
k.add :apply_to_tagged, "Apply next command to all tagged items", '+'
|
27
27
|
k.add :search, "Search for messages from particular people", 'S'
|
28
28
|
end
|
29
29
|
|
@@ -2,6 +2,7 @@ require 'tempfile'
|
|
2
2
|
require 'socket' # just for gethostname!
|
3
3
|
require 'pathname'
|
4
4
|
require 'rmail'
|
5
|
+
require 'jcode' # for RE_UTF8
|
5
6
|
|
6
7
|
module Redwood
|
7
8
|
|
@@ -12,7 +13,7 @@ class EditMessageMode < LineCursorMode
|
|
12
13
|
|
13
14
|
FORCE_HEADERS = %w(From To Cc Bcc Subject)
|
14
15
|
MULTI_HEADERS = %w(To Cc Bcc)
|
15
|
-
NON_EDITABLE_HEADERS = %w(Message-
|
16
|
+
NON_EDITABLE_HEADERS = %w(Message-id Date)
|
16
17
|
|
17
18
|
HookManager.register "signature", <<EOS
|
18
19
|
Generates a message signature.
|
@@ -145,6 +146,8 @@ EOS
|
|
145
146
|
!edited? || BufferManager.ask_yes_or_no("Discard message?")
|
146
147
|
end
|
147
148
|
|
149
|
+
def unsaved?; edited? end
|
150
|
+
|
148
151
|
def attach_file
|
149
152
|
fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
|
150
153
|
return unless fn
|
@@ -168,6 +171,29 @@ EOS
|
|
168
171
|
|
169
172
|
protected
|
170
173
|
|
174
|
+
def mime_encode string
|
175
|
+
string = [string].pack('M') # basic quoted-printable
|
176
|
+
string.gsub!(/=\n/,'') # .. remove trailing newline
|
177
|
+
string.gsub!(/_/,'=96') # .. encode underscores
|
178
|
+
string.gsub!(/\?/,'=3F') # .. encode question marks
|
179
|
+
string.gsub!(/ /,'_') # .. translate space to underscores
|
180
|
+
"=?utf-8?q?#{string}?="
|
181
|
+
end
|
182
|
+
|
183
|
+
def mime_encode_subject string
|
184
|
+
return string unless string.match(String::RE_UTF8)
|
185
|
+
mime_encode string
|
186
|
+
end
|
187
|
+
|
188
|
+
RE_ADDRESS = /(.+)( <.*@.*>)/
|
189
|
+
|
190
|
+
# Encode "bælammet mitt <user@example.com>" into
|
191
|
+
# "=?utf-8?q?b=C3=A6lammet_mitt?= <user@example.com>
|
192
|
+
def mime_encode_address string
|
193
|
+
return string unless string.match(String::RE_UTF8)
|
194
|
+
string.sub(RE_ADDRESS) { |match| mime_encode($1) + $2 }
|
195
|
+
end
|
196
|
+
|
171
197
|
def move_cursor_left
|
172
198
|
if curpos < @selectors.length
|
173
199
|
@selectors[curpos].roll_left
|
@@ -212,7 +238,7 @@ protected
|
|
212
238
|
|
213
239
|
def parse_file fn
|
214
240
|
File.open(fn) do |f|
|
215
|
-
header =
|
241
|
+
header = Source.parse_raw_email_header(f).inject({}) { |h, (k, v)| h[k.capitalize] = v; h } # lousy HACK
|
216
242
|
body = f.readlines.map { |l| l.chomp }
|
217
243
|
|
218
244
|
header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
|
@@ -257,7 +283,7 @@ protected
|
|
257
283
|
if i == 0
|
258
284
|
header + " " + name
|
259
285
|
else
|
260
|
-
(" " * (header.
|
286
|
+
(" " * (header.display_length + 1)) + name
|
261
287
|
end + (i == things.length - 1 ? "" : ",")
|
262
288
|
end
|
263
289
|
end
|
@@ -321,8 +347,8 @@ protected
|
|
321
347
|
|
322
348
|
## do whatever crypto transformation is necessary
|
323
349
|
if @crypto_selector && @crypto_selector.val != :none
|
324
|
-
from_email =
|
325
|
-
to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p|
|
350
|
+
from_email = Person.from_address(@header["From"]).email
|
351
|
+
to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| Person.from_address(p).email }
|
326
352
|
|
327
353
|
m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
|
328
354
|
end
|
@@ -333,14 +359,16 @@ protected
|
|
333
359
|
m.header[k] =
|
334
360
|
case v
|
335
361
|
when String
|
336
|
-
v
|
362
|
+
k.match(/subject/i) ? mime_encode_subject(v) : mime_encode_address(v)
|
337
363
|
when Array
|
338
|
-
v.join ", "
|
364
|
+
v.map { |v| mime_encode_address v }.join ", "
|
339
365
|
end
|
340
366
|
end
|
367
|
+
|
341
368
|
m.header["Date"] = date.rfc2822
|
342
369
|
m.header["Message-Id"] = @message_id
|
343
370
|
m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
|
371
|
+
m.header["Content-Transfer-Encoding"] = '8bit'
|
344
372
|
m
|
345
373
|
end
|
346
374
|
|
@@ -390,7 +418,7 @@ protected
|
|
390
418
|
|
391
419
|
contacts = BufferManager.ask_for_contacts :people, "#{field}: ", default
|
392
420
|
if contacts
|
393
|
-
text = contacts.map { |s| s.
|
421
|
+
text = contacts.map { |s| s.full_address }.join(", ")
|
394
422
|
@header[field] = parse_header field, text
|
395
423
|
update
|
396
424
|
end
|
@@ -412,7 +440,7 @@ private
|
|
412
440
|
end
|
413
441
|
|
414
442
|
def sig_lines
|
415
|
-
p =
|
443
|
+
p = Person.from_address(@header["From"])
|
416
444
|
from_email = p && p.email
|
417
445
|
|
418
446
|
## first run the hook
|
data/lib/sup/modes/inbox-mode.rb
CHANGED
@@ -26,12 +26,27 @@ class InboxMode < ThreadIndexMode
|
|
26
26
|
|
27
27
|
def archive
|
28
28
|
return unless cursor_thread
|
29
|
+
thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
|
30
|
+
|
31
|
+
UndoManager.register "archiving thread" do
|
32
|
+
thread.apply_label :inbox
|
33
|
+
add_or_unhide thread.first
|
34
|
+
end
|
35
|
+
|
29
36
|
cursor_thread.remove_label :inbox
|
30
37
|
hide_thread cursor_thread
|
31
38
|
regen_text
|
32
39
|
end
|
33
40
|
|
34
41
|
def multi_archive threads
|
42
|
+
UndoManager.register "archiving #{threads.size.pluralize 'thread'}" do
|
43
|
+
threads.map do |t|
|
44
|
+
t.apply_label :inbox
|
45
|
+
add_or_unhide t.first
|
46
|
+
end
|
47
|
+
regen_text
|
48
|
+
end
|
49
|
+
|
35
50
|
threads.each do |t|
|
36
51
|
t.remove_label :inbox
|
37
52
|
hide_thread t
|
@@ -41,6 +56,14 @@ class InboxMode < ThreadIndexMode
|
|
41
56
|
|
42
57
|
def read_and_archive
|
43
58
|
return unless cursor_thread
|
59
|
+
thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
|
60
|
+
|
61
|
+
UndoManager.register "reading and archiving thread" do
|
62
|
+
thread.apply_label :inbox
|
63
|
+
thread.apply_label :unread
|
64
|
+
add_or_unhide thread.first
|
65
|
+
end
|
66
|
+
|
44
67
|
cursor_thread.remove_label :unread
|
45
68
|
cursor_thread.remove_label :inbox
|
46
69
|
hide_thread cursor_thread
|
@@ -48,12 +71,23 @@ class InboxMode < ThreadIndexMode
|
|
48
71
|
end
|
49
72
|
|
50
73
|
def multi_read_and_archive threads
|
74
|
+
old_labels = threads.map { |t| t.labels.dup }
|
75
|
+
|
51
76
|
threads.each do |t|
|
52
77
|
t.remove_label :unread
|
53
78
|
t.remove_label :inbox
|
54
79
|
hide_thread t
|
55
80
|
end
|
56
81
|
regen_text
|
82
|
+
|
83
|
+
UndoManager.register "reading and archiving #{threads.size.pluralize 'thread'}" do
|
84
|
+
threads.zip(old_labels).each do |t, l|
|
85
|
+
t.labels = l
|
86
|
+
add_or_unhide t.first
|
87
|
+
end
|
88
|
+
regen_text
|
89
|
+
end
|
90
|
+
|
57
91
|
end
|
58
92
|
|
59
93
|
def handle_unarchived_update sender, m
|
@@ -48,12 +48,12 @@ protected
|
|
48
48
|
|
49
49
|
def regen_text
|
50
50
|
@text = []
|
51
|
-
labels = LabelManager.
|
51
|
+
labels = LabelManager.all_labels
|
52
52
|
|
53
53
|
counts = labels.map do |label|
|
54
54
|
string = LabelManager.string_for label
|
55
55
|
total = Index.num_results_for :label => label
|
56
|
-
unread = Index.num_results_for
|
56
|
+
unread = (label == :unread)? total : Index.num_results_for(:labels => [label, :unread])
|
57
57
|
[label, string, total, unread]
|
58
58
|
end.sort_by { |l, s, t, u| s.downcase }
|
59
59
|
|
@@ -65,7 +65,14 @@ protected
|
|
65
65
|
|
66
66
|
@labels = []
|
67
67
|
counts.map do |label, string, total, unread|
|
68
|
-
if
|
68
|
+
## if we've done a search and there are no messages for this label, we can delete it from the
|
69
|
+
## list. BUT if it's a brand-new label, the user may not have sync'ed it to the index yet, so
|
70
|
+
## don't delete it in this case.
|
71
|
+
##
|
72
|
+
## this is all a hack. what should happen is:
|
73
|
+
## TODO make the labelmanager responsible for label counts
|
74
|
+
## and then it can listen to labeled and unlabeled events, etc.
|
75
|
+
if total == 0 && !LabelManager::RESERVED_LABELS.include?(label) && !LabelManager.new_label?(label)
|
69
76
|
Redwood::log "no hits for label #{label}, deleting"
|
70
77
|
LabelManager.delete label
|
71
78
|
next
|
data/lib/sup/modes/reply-mode.rb
CHANGED
@@ -53,22 +53,33 @@ EOS
|
|
53
53
|
hook_reply_from = HookManager.run "reply-from", :message => @m
|
54
54
|
|
55
55
|
## sanity check that selection is a Person (or we'll fail below)
|
56
|
-
## don't check that it's an Account, though; assume they know what they're
|
56
|
+
## don't check that it's an Account, though; assume they know what they're
|
57
|
+
## doing.
|
57
58
|
if hook_reply_from && !(hook_reply_from.is_a? Person)
|
58
|
-
|
59
|
-
|
59
|
+
Redwood::log "reply-from returned non-Person, using default from."
|
60
|
+
hook_reply_from = nil
|
60
61
|
end
|
61
62
|
|
62
|
-
from
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
63
|
+
## determine the from address of a reply.
|
64
|
+
## if we have a value from a hook, use it.
|
65
|
+
from = if hook_reply_from
|
66
|
+
hook_reply_from
|
67
|
+
## otherwise, if the original email had an envelope-to header, try and use
|
68
|
+
## it, and look up the corresponding name form the list of accounts.
|
69
|
+
##
|
70
|
+
## this is for the case where mail is received from a mailing lists (so the
|
71
|
+
## To: is the list id itself). if the user subscribes via a particular
|
72
|
+
## alias, we want to use that alias in the reply.
|
73
|
+
elsif @m.recipient_email && (a = AccountManager.account_for(@m.recipient_email))
|
74
|
+
Person.new a.name, @m.recipient_email
|
75
|
+
## otherwise, try and find an account somewhere in the list of to's
|
76
|
+
## and cc's.
|
77
|
+
elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
|
78
|
+
b
|
79
|
+
## if all else fails, use the default
|
80
|
+
else
|
81
|
+
AccountManager.default_account
|
82
|
+
end
|
72
83
|
|
73
84
|
## now, determine to: and cc: addressess. we ignore reply-to for list
|
74
85
|
## messages because it's typically set to the list address, which we
|
@@ -73,7 +73,7 @@ class ScrollMode < Mode
|
|
73
73
|
end
|
74
74
|
if line
|
75
75
|
@search_line = line + 1
|
76
|
-
search_goto_pos line, col, col + @search_query.
|
76
|
+
search_goto_pos line, col, col + @search_query.display_length
|
77
77
|
buffer.mark_dirty
|
78
78
|
else
|
79
79
|
BufferManager.flash "Not found!"
|
@@ -164,7 +164,7 @@ protected
|
|
164
164
|
if match
|
165
165
|
return [i, offset + match]
|
166
166
|
else
|
167
|
-
offset += string.
|
167
|
+
offset += string.display_length
|
168
168
|
end
|
169
169
|
end
|
170
170
|
end
|
@@ -219,24 +219,25 @@ protected
|
|
219
219
|
|
220
220
|
def draw_line_from_array ln, a, opts
|
221
221
|
xpos = 0
|
222
|
-
a.
|
222
|
+
a.each_with_index do |(color, text), i|
|
223
223
|
raise "nil text for color '#{color}'" if text.nil? # good for debugging
|
224
|
+
l = text.display_length
|
225
|
+
no_fill = i != a.size - 1
|
224
226
|
|
225
|
-
if xpos +
|
227
|
+
if xpos + l < @leftcol
|
226
228
|
buffer.write ln - @topline, 0, "", :color => color,
|
227
229
|
:highlight => opts[:highlight]
|
228
|
-
xpos += text.length
|
229
230
|
elsif xpos < @leftcol
|
230
231
|
## partial
|
231
232
|
buffer.write ln - @topline, 0, text[(@leftcol - xpos) .. -1],
|
232
233
|
:color => color,
|
233
|
-
:highlight => opts[:highlight]
|
234
|
-
xpos += text.length
|
234
|
+
:highlight => opts[:highlight], :no_fill => no_fill
|
235
235
|
else
|
236
236
|
buffer.write ln - @topline, xpos - @leftcol, text,
|
237
|
-
:color => color, :highlight => opts[:highlight]
|
238
|
-
|
237
|
+
:color => color, :highlight => opts[:highlight],
|
238
|
+
:no_fill => no_fill
|
239
239
|
end
|
240
|
+
xpos += l
|
240
241
|
end
|
241
242
|
end
|
242
243
|
|
@@ -32,8 +32,8 @@ class SearchResultsMode < ThreadIndexMode
|
|
32
32
|
mode = SearchResultsMode.new qobj, extraopts
|
33
33
|
BufferManager.spawn "search: \"#{short_text}\"", mode
|
34
34
|
mode.load_threads :num => mode.buffer.content_height
|
35
|
-
rescue
|
36
|
-
BufferManager.flash "
|
35
|
+
rescue Index::ParseError => e
|
36
|
+
BufferManager.flash "Problem: #{e.message}!"
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
@@ -42,8 +42,9 @@ EOS
|
|
42
42
|
k.add :toggle_tagged, "Tag/untag selected thread", 't'
|
43
43
|
k.add :toggle_tagged_all, "Tag/untag all threads", 'T'
|
44
44
|
k.add :tag_matching, "Tag matching threads", 'g'
|
45
|
-
k.add :apply_to_tagged, "Apply next command to all tagged threads", '
|
45
|
+
k.add :apply_to_tagged, "Apply next command to all tagged threads", '+', '='
|
46
46
|
k.add :join_threads, "Force tagged threads to be joined into the same thread", '#'
|
47
|
+
k.add :undo, "Undo the previous action", 'u'
|
47
48
|
end
|
48
49
|
|
49
50
|
def initialize hidden_labels=[], load_thread_opts={}
|
@@ -68,6 +69,8 @@ EOS
|
|
68
69
|
|
69
70
|
UpdateManager.register self
|
70
71
|
|
72
|
+
@save_thread_mutex = Mutex.new
|
73
|
+
|
71
74
|
@last_load_more_size = nil
|
72
75
|
to_load_more do |size|
|
73
76
|
next if @last_load_more_size == 0
|
@@ -77,12 +80,14 @@ EOS
|
|
77
80
|
end
|
78
81
|
end
|
79
82
|
|
83
|
+
def unsaved?; dirty? end
|
80
84
|
def lines; @text.length; end
|
81
85
|
def [] i; @text[i]; end
|
82
86
|
def contains_thread? t; @threads.include?(t) end
|
83
87
|
|
84
88
|
def reload
|
85
89
|
drop_all_threads
|
90
|
+
UndoManager.clear
|
86
91
|
BufferManager.draw_screen
|
87
92
|
load_threads :num => buffer.content_height
|
88
93
|
end
|
@@ -208,12 +213,16 @@ EOS
|
|
208
213
|
add_or_unhide m
|
209
214
|
end
|
210
215
|
|
216
|
+
def undo
|
217
|
+
UndoManager.undo
|
218
|
+
end
|
219
|
+
|
211
220
|
def update
|
212
221
|
@mutex.synchronize do
|
213
222
|
## let's see you do THIS in python
|
214
223
|
@threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| [t.date, t.first.id] }.reverse
|
215
224
|
@size_widgets = @threads.map { |t| size_widget_for_thread t }
|
216
|
-
@size_widget_width = @size_widgets.max_of { |w| w.
|
225
|
+
@size_widget_width = @size_widgets.max_of { |w| w.display_length }
|
217
226
|
end
|
218
227
|
|
219
228
|
regen_text
|
@@ -230,66 +239,122 @@ EOS
|
|
230
239
|
end
|
231
240
|
end
|
232
241
|
|
242
|
+
## returns an undo lambda
|
233
243
|
def actually_toggle_starred t
|
244
|
+
pos = curpos
|
234
245
|
if t.has_label? :starred # if ANY message has a star
|
235
246
|
t.remove_label :starred # remove from all
|
236
247
|
UpdateManager.relay self, :unstarred, t.first
|
248
|
+
lambda do
|
249
|
+
t.first.add_label :starred
|
250
|
+
UpdateManager.relay self, :starred, t.first
|
251
|
+
regen_text
|
252
|
+
end
|
237
253
|
else
|
238
254
|
t.first.add_label :starred # add only to first
|
239
255
|
UpdateManager.relay self, :starred, t.first
|
256
|
+
lambda do
|
257
|
+
t.remove_label :starred
|
258
|
+
UpdateManager.relay self, :unstarred, t.first
|
259
|
+
regen_text
|
260
|
+
end
|
240
261
|
end
|
241
262
|
end
|
242
263
|
|
243
264
|
def toggle_starred
|
244
265
|
t = cursor_thread or return
|
245
|
-
actually_toggle_starred t
|
266
|
+
undo = actually_toggle_starred t
|
267
|
+
UndoManager.register "toggling thread starred status", undo
|
246
268
|
update_text_for_line curpos
|
247
269
|
cursor_down
|
248
270
|
end
|
249
271
|
|
250
272
|
def multi_toggle_starred threads
|
251
|
-
|
273
|
+
UndoManager.register "toggling #{threads.size.pluralize 'thread'} starred status",
|
274
|
+
threads.map { |t| actually_toggle_starred t }
|
252
275
|
regen_text
|
253
276
|
end
|
254
277
|
|
278
|
+
## returns an undo lambda
|
255
279
|
def actually_toggle_archived t
|
280
|
+
thread = t
|
281
|
+
pos = curpos
|
256
282
|
if t.has_label? :inbox
|
257
283
|
t.remove_label :inbox
|
258
284
|
UpdateManager.relay self, :archived, t.first
|
285
|
+
lambda do
|
286
|
+
thread.apply_label :inbox
|
287
|
+
update_text_for_line pos
|
288
|
+
UpdateManager.relay self,:unarchived, thread.first
|
289
|
+
end
|
259
290
|
else
|
260
291
|
t.apply_label :inbox
|
261
292
|
UpdateManager.relay self, :unarchived, t.first
|
293
|
+
lambda do
|
294
|
+
thread.remove_label :inbox
|
295
|
+
update_text_for_line pos
|
296
|
+
UpdateManager.relay self, :unarchived, thread.first
|
297
|
+
end
|
262
298
|
end
|
263
299
|
end
|
264
300
|
|
301
|
+
## returns an undo lambda
|
265
302
|
def actually_toggle_spammed t
|
303
|
+
thread = t
|
266
304
|
if t.has_label? :spam
|
267
305
|
t.remove_label :spam
|
306
|
+
add_or_unhide t.first
|
268
307
|
UpdateManager.relay self, :unspammed, t.first
|
308
|
+
lambda do
|
309
|
+
thread.apply_label :spam
|
310
|
+
self.hide_thread thread
|
311
|
+
UpdateManager.relay self,:spammed, thread.first
|
312
|
+
end
|
269
313
|
else
|
270
314
|
t.apply_label :spam
|
315
|
+
hide_thread t
|
271
316
|
UpdateManager.relay self, :spammed, t.first
|
317
|
+
lambda do
|
318
|
+
thread.remove_label :spam
|
319
|
+
add_or_unhide thread.first
|
320
|
+
UpdateManager.relay self,:unspammed, thread.first
|
321
|
+
end
|
272
322
|
end
|
273
323
|
end
|
274
324
|
|
325
|
+
## returns an undo lambda
|
275
326
|
def actually_toggle_deleted t
|
276
327
|
if t.has_label? :deleted
|
277
328
|
t.remove_label :deleted
|
329
|
+
add_or_unhide t.first
|
278
330
|
UpdateManager.relay self, :undeleted, t.first
|
331
|
+
lambda do
|
332
|
+
t.apply_label :deleted
|
333
|
+
hide_thread t
|
334
|
+
UpdateManager.relay self, :deleted, t.first
|
335
|
+
end
|
279
336
|
else
|
280
337
|
t.apply_label :deleted
|
338
|
+
hide_thread t
|
281
339
|
UpdateManager.relay self, :deleted, t.first
|
340
|
+
lambda do
|
341
|
+
t.remove_label :deleted
|
342
|
+
add_or_unhide t.first
|
343
|
+
UpdateManager.relay self, :undeleted, t.first
|
344
|
+
end
|
282
345
|
end
|
283
346
|
end
|
284
347
|
|
285
348
|
def toggle_archived
|
286
349
|
t = cursor_thread or return
|
287
|
-
actually_toggle_archived t
|
350
|
+
undo = actually_toggle_archived t
|
351
|
+
UndoManager.register "deleting/undeleting thread #{t.first.id}", undo, lambda { update_text_for_line curpos }
|
288
352
|
update_text_for_line curpos
|
289
353
|
end
|
290
354
|
|
291
355
|
def multi_toggle_archived threads
|
292
|
-
threads.
|
356
|
+
undos = threads.map { |t| actually_toggle_archived t }
|
357
|
+
UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", undos, lambda { regen_text }
|
293
358
|
regen_text
|
294
359
|
end
|
295
360
|
|
@@ -350,10 +415,9 @@ EOS
|
|
350
415
|
## see deleted or spam emails, and when you undelete or unspam them
|
351
416
|
## you also want them to disappear immediately.
|
352
417
|
def multi_toggle_spam threads
|
353
|
-
threads.
|
354
|
-
|
355
|
-
|
356
|
-
end
|
418
|
+
undos = threads.map { |t| actually_toggle_spammed t }
|
419
|
+
UndoManager.register "marking/unmarking #{threads.size.pluralize 'thread'} as spam",
|
420
|
+
undos, lambda { regen_text }
|
357
421
|
regen_text
|
358
422
|
end
|
359
423
|
|
@@ -364,10 +428,9 @@ EOS
|
|
364
428
|
|
365
429
|
## see comment for multi_toggle_spam
|
366
430
|
def multi_toggle_deleted threads
|
367
|
-
threads.
|
368
|
-
|
369
|
-
|
370
|
-
end
|
431
|
+
undos = threads.map { |t| actually_toggle_deleted t }
|
432
|
+
UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}",
|
433
|
+
undos, lambda { regen_text }
|
371
434
|
regen_text
|
372
435
|
end
|
373
436
|
|
@@ -376,24 +439,44 @@ EOS
|
|
376
439
|
multi_kill [t]
|
377
440
|
end
|
378
441
|
|
442
|
+
## m-m-m-m-MULTI-KILL
|
379
443
|
def multi_kill threads
|
444
|
+
UndoManager.register "killing #{threads.size.pluralize 'thread'}" do
|
445
|
+
threads.each do |t|
|
446
|
+
t.remove_label :killed
|
447
|
+
add_or_unhide t.first
|
448
|
+
end
|
449
|
+
regen_text
|
450
|
+
end
|
451
|
+
|
380
452
|
threads.each do |t|
|
381
453
|
t.apply_label :killed
|
382
454
|
hide_thread t
|
383
455
|
end
|
456
|
+
|
384
457
|
regen_text
|
385
|
-
BufferManager.flash "#{threads.size.pluralize '
|
458
|
+
BufferManager.flash "#{threads.size.pluralize 'thread'} killed."
|
386
459
|
end
|
387
460
|
|
388
|
-
def save
|
389
|
-
|
390
|
-
|
391
|
-
|
461
|
+
def save background=true
|
462
|
+
if background
|
463
|
+
Redwood::reporting_thread("saving thread") { actually_save }
|
464
|
+
else
|
465
|
+
actually_save
|
466
|
+
end
|
467
|
+
end
|
392
468
|
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
469
|
+
def actually_save
|
470
|
+
@save_thread_mutex.synchronize do
|
471
|
+
BufferManager.say("Saving contacts...") { ContactManager.instance.save }
|
472
|
+
dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
|
473
|
+
next if dirty_threads.empty?
|
474
|
+
|
475
|
+
BufferManager.say("Saving threads...") do |say_id|
|
476
|
+
dirty_threads.each_with_index do |t, i|
|
477
|
+
BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
|
478
|
+
t.save Index
|
479
|
+
end
|
397
480
|
end
|
398
481
|
end
|
399
482
|
end
|
@@ -407,7 +490,7 @@ EOS
|
|
407
490
|
sleep 0.1 # TODO: necessary?
|
408
491
|
BufferManager.erase_flash
|
409
492
|
end
|
410
|
-
save
|
493
|
+
save false
|
411
494
|
super
|
412
495
|
end
|
413
496
|
|
@@ -424,9 +507,14 @@ EOS
|
|
424
507
|
end
|
425
508
|
|
426
509
|
def tag_matching
|
427
|
-
query = BufferManager.ask :search, "tag threads matching: "
|
510
|
+
query = BufferManager.ask :search, "tag threads matching (regex): "
|
428
511
|
return if query.nil? || query.empty?
|
429
|
-
query =
|
512
|
+
query = begin
|
513
|
+
/#{query}/i
|
514
|
+
rescue RegexpError => e
|
515
|
+
BufferManager.flash "error interpreting '#{query}': #{e.message}"
|
516
|
+
return
|
517
|
+
end
|
430
518
|
@mutex.synchronize { @threads.each { |t| @tags.tag t if thread_matches?(t, query) } }
|
431
519
|
regen_text
|
432
520
|
end
|
@@ -436,6 +524,10 @@ EOS
|
|
436
524
|
def edit_labels
|
437
525
|
thread = cursor_thread or return
|
438
526
|
speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
|
527
|
+
|
528
|
+
old_labels = thread.labels
|
529
|
+
pos = curpos
|
530
|
+
|
439
531
|
keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
|
440
532
|
|
441
533
|
user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl, @hidden_labels
|
@@ -444,21 +536,49 @@ EOS
|
|
444
536
|
thread.labels = keepl + user_labels
|
445
537
|
user_labels.each { |l| LabelManager << l }
|
446
538
|
update_text_for_line curpos
|
539
|
+
|
540
|
+
UndoManager.register "labeling thread" do
|
541
|
+
thread.labels = old_labels
|
542
|
+
update_text_for_line pos
|
543
|
+
UpdateManager.relay self, :labeled, thread.first
|
544
|
+
end
|
545
|
+
|
447
546
|
UpdateManager.relay self, :labeled, thread.first
|
448
547
|
end
|
449
548
|
|
450
549
|
def multi_edit_labels threads
|
451
|
-
user_labels = BufferManager.ask_for_labels :
|
550
|
+
user_labels = BufferManager.ask_for_labels :labels, "Add/remove labels (use -label to remove): ", [], @hidden_labels
|
452
551
|
return unless user_labels
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
user_labels.each { |l| LabelManager << l }
|
458
|
-
else
|
552
|
+
|
553
|
+
user_labels.map! { |l| (l.to_s =~ /^-/)? [l.to_s.gsub(/^-?/, '').to_sym, true] : [l, false] }
|
554
|
+
hl = user_labels.select { |(l,_)| @hidden_labels.member? l }
|
555
|
+
unless hl.empty?
|
459
556
|
BufferManager.flash "'#{hl}' is a reserved label!"
|
557
|
+
return
|
460
558
|
end
|
559
|
+
|
560
|
+
old_labels = threads.map { |t| t.labels.dup }
|
561
|
+
|
562
|
+
threads.each do |t|
|
563
|
+
user_labels.each do |(l, to_remove)|
|
564
|
+
if to_remove
|
565
|
+
t.remove_label l
|
566
|
+
else
|
567
|
+
t.apply_label l
|
568
|
+
LabelManager << l
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
461
573
|
regen_text
|
574
|
+
|
575
|
+
UndoManager.register "labeling #{threads.size.pluralize 'thread'}" do
|
576
|
+
threads.zip(old_labels).map do |t, old_labels|
|
577
|
+
t.labels = old_labels
|
578
|
+
UpdateManager.relay self, :labeled, t.first
|
579
|
+
end
|
580
|
+
regen_text
|
581
|
+
end
|
462
582
|
end
|
463
583
|
|
464
584
|
def reply
|
@@ -662,7 +782,7 @@ protected
|
|
662
782
|
|
663
783
|
date = t.date.to_nice_s
|
664
784
|
|
665
|
-
starred = t.has_label?
|
785
|
+
starred = t.has_label? :starred
|
666
786
|
|
667
787
|
## format the from column
|
668
788
|
cur_width = 0
|
@@ -673,9 +793,9 @@ protected
|
|
673
793
|
last = i == ann.length - 1
|
674
794
|
|
675
795
|
abbrev =
|
676
|
-
if cur_width + name.
|
796
|
+
if cur_width + name.display_length > from_width
|
677
797
|
name[0 ... (from_width - cur_width - 1)] + "."
|
678
|
-
elsif cur_width + name.
|
798
|
+
elsif cur_width + name.display_length == from_width
|
679
799
|
name[0 ... (from_width - cur_width)]
|
680
800
|
else
|
681
801
|
if last
|
@@ -685,7 +805,7 @@ protected
|
|
685
805
|
end
|
686
806
|
end
|
687
807
|
|
688
|
-
cur_width += abbrev.
|
808
|
+
cur_width += abbrev.display_length
|
689
809
|
|
690
810
|
if last && from_width > cur_width
|
691
811
|
abbrev += " " * (from_width - cur_width)
|
@@ -727,7 +847,6 @@ protected
|
|
727
847
|
(t.labels - @hidden_labels).map { |label| [:label_color, "+#{label} "] } +
|
728
848
|
[[:snippet_color, snippet]
|
729
849
|
]
|
730
|
-
|
731
850
|
end
|
732
851
|
|
733
852
|
def dirty?; @mutex.synchronize { (@hidden_threads.keys + @threads).any? { |t| t.dirty? } } end
|