sup 0.3 → 0.4
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/HACKING +31 -9
- data/History.txt +7 -0
- data/Manifest.txt +2 -0
- data/Rakefile +9 -5
- data/bin/sup +81 -57
- data/bin/sup-config +1 -1
- data/bin/sup-sync +3 -0
- data/bin/sup-tweak-labels +127 -0
- data/doc/TODO +23 -12
- data/lib/sup.rb +13 -11
- data/lib/sup/account.rb +25 -12
- data/lib/sup/buffer.rb +61 -41
- data/lib/sup/colormap.rb +2 -0
- data/lib/sup/contact.rb +28 -18
- data/lib/sup/crypto.rb +86 -31
- data/lib/sup/draft.rb +12 -6
- data/lib/sup/horizontal-selector.rb +47 -0
- data/lib/sup/imap.rb +50 -37
- data/lib/sup/index.rb +76 -13
- data/lib/sup/keymap.rb +27 -8
- data/lib/sup/maildir.rb +1 -1
- data/lib/sup/mbox/loader.rb +1 -1
- data/lib/sup/message-chunks.rb +43 -15
- data/lib/sup/message.rb +67 -31
- data/lib/sup/mode.rb +40 -9
- data/lib/sup/modes/completion-mode.rb +1 -1
- data/lib/sup/modes/compose-mode.rb +3 -3
- data/lib/sup/modes/contact-list-mode.rb +12 -8
- data/lib/sup/modes/edit-message-mode.rb +100 -36
- data/lib/sup/modes/file-browser-mode.rb +1 -0
- data/lib/sup/modes/forward-mode.rb +43 -8
- data/lib/sup/modes/inbox-mode.rb +8 -5
- data/lib/sup/modes/label-search-results-mode.rb +12 -1
- data/lib/sup/modes/line-cursor-mode.rb +4 -7
- data/lib/sup/modes/reply-mode.rb +59 -54
- data/lib/sup/modes/resume-mode.rb +6 -6
- data/lib/sup/modes/scroll-mode.rb +4 -3
- data/lib/sup/modes/search-results-mode.rb +8 -5
- data/lib/sup/modes/text-mode.rb +19 -2
- data/lib/sup/modes/thread-index-mode.rb +109 -40
- data/lib/sup/modes/thread-view-mode.rb +180 -49
- data/lib/sup/person.rb +3 -3
- data/lib/sup/poll.rb +9 -8
- data/lib/sup/rfc2047.rb +7 -1
- data/lib/sup/sent.rb +1 -1
- data/lib/sup/tagger.rb +10 -4
- data/lib/sup/textfield.rb +7 -7
- data/lib/sup/thread.rb +86 -49
- data/lib/sup/update.rb +11 -0
- data/lib/sup/util.rb +74 -34
- data/test/test_message.rb +441 -0
- metadata +136 -117
@@ -9,7 +9,18 @@ class LabelSearchResultsMode < ThreadIndexMode
|
|
9
9
|
super [], opts
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
register_keymap do |k|
|
13
|
+
k.add :refine_search, "Refine search", '.'
|
14
|
+
end
|
15
|
+
|
16
|
+
def refine_search
|
17
|
+
label_query = @labels.size > 1 ? "(#{@labels.join('||')})" : @labels.first
|
18
|
+
query = BufferManager.ask :search, "query: ", "+label:#{label_query} "
|
19
|
+
return unless query && query !~ /^\s*$/
|
20
|
+
SearchResultsMode.spawn_from_query query
|
21
|
+
end
|
22
|
+
|
23
|
+
def is_relevant? m; @labels.all? { |l| m.has_label? l } end
|
13
24
|
|
14
25
|
def self.spawn_nicely label
|
15
26
|
label = LabelManager.label_for(label) unless label.is_a?(Symbol)
|
@@ -55,14 +55,11 @@ protected
|
|
55
55
|
buffer.mark_dirty
|
56
56
|
end
|
57
57
|
|
58
|
-
## override search behavior to be cursor-based
|
58
|
+
## override search behavior to be cursor-based. this is a stupid
|
59
|
+
## implementation and should be made better. TODO: improve.
|
59
60
|
def search_goto_line line
|
60
|
-
while line
|
61
|
-
|
62
|
-
end
|
63
|
-
while line < topline
|
64
|
-
page_up
|
65
|
-
end
|
61
|
+
page_down while line >= botline
|
62
|
+
page_up while line < topline
|
66
63
|
set_cursor_pos line
|
67
64
|
end
|
68
65
|
|
data/lib/sup/modes/reply-mode.rb
CHANGED
@@ -3,17 +3,21 @@ module Redwood
|
|
3
3
|
class ReplyMode < EditMessageMode
|
4
4
|
REPLY_TYPES = [:sender, :recipient, :list, :all, :user]
|
5
5
|
TYPE_DESCRIPTIONS = {
|
6
|
-
:sender => "
|
7
|
-
:recipient => "
|
8
|
-
:all => "
|
9
|
-
:list => "
|
10
|
-
:user => "Customized
|
6
|
+
:sender => "Sender",
|
7
|
+
:recipient => "Recipient",
|
8
|
+
:all => "All",
|
9
|
+
:list => "Mailing list",
|
10
|
+
:user => "Customized"
|
11
11
|
}
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
HookManager.register "attribution", <<EOS
|
14
|
+
Generates an attribution ("Excerpts from Joe Bloggs's message of Fri Jan 11 09:54:32 -0500 2008:").
|
15
|
+
Variables:
|
16
|
+
message: a message object representing the message being replied to
|
17
|
+
(useful values include message.from.name and message.date)
|
18
|
+
Return value:
|
19
|
+
A string containing the text of the quote line (can be multi-line)
|
20
|
+
EOS
|
17
21
|
|
18
22
|
def initialize message
|
19
23
|
@m = message
|
@@ -23,27 +27,33 @@ class ReplyMode < EditMessageMode
|
|
23
27
|
## any)
|
24
28
|
body = reply_body_lines message
|
25
29
|
|
30
|
+
## first, determine the address at which we received this email. this will
|
31
|
+
## become our From: address in the reply.
|
26
32
|
from =
|
27
|
-
if @m.recipient_email &&
|
28
|
-
|
33
|
+
if @m.recipient_email && AccountManager.is_account_email?(@m.recipient_email)
|
34
|
+
PersonManager.person_for(@m.recipient_email)
|
29
35
|
elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
|
30
36
|
b
|
31
37
|
else
|
32
38
|
AccountManager.default_account
|
33
39
|
end
|
34
40
|
|
35
|
-
##
|
36
|
-
## the list address, which we
|
41
|
+
## now, determine to: and cc: addressess. we ignore reply-to for list
|
42
|
+
## messages because it's typically set to the list address, which we
|
43
|
+
## explicitly treat with reply type :list
|
37
44
|
to = @m.is_list_message? ? @m.from : (@m.replyto || @m.from)
|
38
|
-
cc = (@m.to + @m.cc - [from, to]).uniq
|
39
45
|
|
40
|
-
|
46
|
+
## next, cc:
|
47
|
+
cc = (@m.to + @m.cc - [from, to]).uniq
|
41
48
|
|
49
|
+
## one potential reply type is "reply to recipient". this only happens
|
50
|
+
## in certain cases:
|
42
51
|
## if there's no cc, then the sender is the person you want to reply
|
43
52
|
## to. if it's a list message, then the list address is. otherwise,
|
44
53
|
## the cc contains a recipient.
|
45
54
|
useful_recipient = !(cc.empty? || @m.is_list_message?)
|
46
55
|
|
56
|
+
@headers = {}
|
47
57
|
@headers[:recipient] = {
|
48
58
|
"To" => cc.map { |p| p.full_address },
|
49
59
|
} if useful_recipient
|
@@ -55,10 +65,11 @@ class ReplyMode < EditMessageMode
|
|
55
65
|
|
56
66
|
@headers[:user] = {}
|
57
67
|
|
68
|
+
not_me_ccs = cc.select { |p| !AccountManager.is_account?(p) }
|
58
69
|
@headers[:all] = {
|
59
70
|
"To" => [to.full_address],
|
60
|
-
"Cc" =>
|
61
|
-
} unless
|
71
|
+
"Cc" => not_me_ccs.map { |p| p.full_address },
|
72
|
+
} unless not_me_ccs.empty?
|
62
73
|
|
63
74
|
@headers[:list] = {
|
64
75
|
"To" => [@m.list_address.full_address],
|
@@ -68,7 +79,7 @@ class ReplyMode < EditMessageMode
|
|
68
79
|
|
69
80
|
@headers.each do |k, v|
|
70
81
|
@headers[k] = {
|
71
|
-
"From" =>
|
82
|
+
"From" => from.full_address,
|
72
83
|
"To" => [],
|
73
84
|
"Cc" => [],
|
74
85
|
"Bcc" => [],
|
@@ -78,47 +89,55 @@ class ReplyMode < EditMessageMode
|
|
78
89
|
}.merge v
|
79
90
|
end
|
80
91
|
|
81
|
-
|
82
|
-
@
|
92
|
+
types = REPLY_TYPES.select { |t| @headers.member?(t) }
|
93
|
+
@type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
|
94
|
+
|
95
|
+
@type_selector.set_to(
|
83
96
|
if @m.is_list_message?
|
84
97
|
:list
|
85
98
|
elsif @headers.member? :sender
|
86
99
|
:sender
|
87
100
|
else
|
88
101
|
:recipient
|
89
|
-
end
|
102
|
+
end)
|
90
103
|
|
91
|
-
super :header => @headers[@
|
92
|
-
|
104
|
+
super :header => @headers[@type_selector.val], :body => body, :twiddles => false
|
105
|
+
add_selector @type_selector
|
93
106
|
end
|
94
107
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
end + [[:none, ""]]
|
103
|
-
when 1
|
104
|
-
""
|
105
|
-
else
|
106
|
-
super(i - 2)
|
108
|
+
protected
|
109
|
+
|
110
|
+
def move_cursor_right
|
111
|
+
super
|
112
|
+
if @headers[@type_selector.val] != self.header
|
113
|
+
self.header = @headers[@type_selector.val]
|
114
|
+
update
|
107
115
|
end
|
108
116
|
end
|
109
117
|
|
110
|
-
|
118
|
+
def move_cursor_left
|
119
|
+
super
|
120
|
+
if @headers[@type_selector.val] != self.header
|
121
|
+
self.header = @headers[@type_selector.val]
|
122
|
+
update
|
123
|
+
end
|
124
|
+
end
|
111
125
|
|
112
126
|
def reply_body_lines m
|
113
|
-
|
127
|
+
attribution = HookManager.run("attribution", :message => m) || default_attribution(m)
|
128
|
+
lines = attribution.split("\n") + m.quotable_body_lines.map { |l| "> #{l}" }
|
114
129
|
lines.pop while lines.last =~ /^\s*$/
|
115
130
|
lines
|
116
131
|
end
|
117
132
|
|
133
|
+
def default_attribution m
|
134
|
+
"Excerpts from #{@m.from.name}'s message of #{@m.date}:"
|
135
|
+
end
|
136
|
+
|
118
137
|
def handle_new_text new_header, new_body
|
119
|
-
old_header = @headers[@
|
138
|
+
old_header = @headers[@type_selector.val]
|
120
139
|
if new_header.size != old_header.size || old_header.any? { |k, v| new_header[k] != v }
|
121
|
-
@
|
140
|
+
@type_selector.set_to :user
|
122
141
|
self.header = @headers[:user] = new_header
|
123
142
|
update
|
124
143
|
end
|
@@ -131,24 +150,10 @@ protected
|
|
131
150
|
def edit_field field
|
132
151
|
edited_field = super
|
133
152
|
if edited_field && edited_field != "Subject"
|
134
|
-
@
|
153
|
+
@type_selector.set_to :user
|
135
154
|
update
|
136
155
|
end
|
137
156
|
end
|
138
|
-
|
139
|
-
def move_cursor_left
|
140
|
-
i = @type_labels.index @selected_type
|
141
|
-
@selected_type = @type_labels[(i - 1) % @type_labels.length]
|
142
|
-
self.header = @headers[@selected_type]
|
143
|
-
update
|
144
|
-
end
|
145
|
-
|
146
|
-
def move_cursor_right
|
147
|
-
i = @type_labels.index @selected_type
|
148
|
-
@selected_type = @type_labels[(i + 1) % @type_labels.length]
|
149
|
-
self.header = @headers[@selected_type]
|
150
|
-
update
|
151
|
-
end
|
152
157
|
end
|
153
158
|
|
154
159
|
end
|
@@ -2,13 +2,13 @@ module Redwood
|
|
2
2
|
|
3
3
|
class ResumeMode < EditMessageMode
|
4
4
|
def initialize m
|
5
|
-
@
|
5
|
+
@m = m
|
6
6
|
@safe = false
|
7
7
|
|
8
8
|
header, body = parse_file m.draft_filename
|
9
9
|
header.delete "Date"
|
10
10
|
|
11
|
-
super :header => header, :body => body
|
11
|
+
super :header => header, :body => body, :have_signature => true
|
12
12
|
end
|
13
13
|
|
14
14
|
def killable?
|
@@ -16,13 +16,13 @@ class ResumeMode < EditMessageMode
|
|
16
16
|
|
17
17
|
case BufferManager.ask_yes_or_no "Discard draft?"
|
18
18
|
when true
|
19
|
-
DraftManager.discard @
|
19
|
+
DraftManager.discard @m
|
20
20
|
BufferManager.flash "Draft discarded."
|
21
21
|
true
|
22
22
|
when false
|
23
23
|
if edited?
|
24
24
|
DraftManager.write_draft { |f| write_message f, false }
|
25
|
-
DraftManager.discard @
|
25
|
+
DraftManager.discard @m
|
26
26
|
BufferManager.flash "Draft saved."
|
27
27
|
end
|
28
28
|
true
|
@@ -33,14 +33,14 @@ class ResumeMode < EditMessageMode
|
|
33
33
|
|
34
34
|
def send_message
|
35
35
|
if super
|
36
|
-
DraftManager.discard @
|
36
|
+
DraftManager.discard @m
|
37
37
|
@safe = true
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
41
|
def save_as_draft
|
42
42
|
@safe = true
|
43
|
-
DraftManager.discard @
|
43
|
+
DraftManager.discard @m if super
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -12,14 +12,14 @@ class ScrollMode < Mode
|
|
12
12
|
|
13
13
|
attr_reader :status, :topline, :botline, :leftcol
|
14
14
|
|
15
|
-
COL_JUMP =
|
15
|
+
COL_JUMP = 4
|
16
16
|
|
17
17
|
register_keymap do |k|
|
18
18
|
k.add :line_down, "Down one line", :down, 'j', 'J'
|
19
19
|
k.add :line_up, "Up one line", :up, 'k', 'K'
|
20
20
|
k.add :col_left, "Left one column", :left, 'h'
|
21
21
|
k.add :col_right, "Right one column", :right, 'l'
|
22
|
-
k.add :page_down, "Down one page", :page_down, '
|
22
|
+
k.add :page_down, "Down one page", :page_down, ' '
|
23
23
|
k.add :page_up, "Up one page", :page_up, 'p', :backspace
|
24
24
|
k.add :jump_to_start, "Jump to top", :home, '^', '1'
|
25
25
|
k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
|
@@ -101,6 +101,7 @@ class ScrollMode < Mode
|
|
101
101
|
end
|
102
102
|
|
103
103
|
def jump_to_col col
|
104
|
+
col = col - (col % COL_JUMP)
|
104
105
|
buffer.mark_dirty unless @leftcol == col
|
105
106
|
@leftcol = col
|
106
107
|
end
|
@@ -176,7 +177,7 @@ protected
|
|
176
177
|
draw_line_from_array ln, s, opts
|
177
178
|
end
|
178
179
|
else
|
179
|
-
raise "unknown drawable object: #{s.inspect}" # good for debugging
|
180
|
+
raise "unknown drawable object: #{s.inspect} in #{self} for line #{ln}" # good for debugging
|
180
181
|
end
|
181
182
|
|
182
183
|
## speed test
|
@@ -1,9 +1,11 @@
|
|
1
1
|
module Redwood
|
2
2
|
|
3
3
|
class SearchResultsMode < ThreadIndexMode
|
4
|
-
def initialize qobj
|
4
|
+
def initialize qobj, qopts = nil
|
5
5
|
@qobj = qobj
|
6
|
-
|
6
|
+
@qopts = qopts
|
7
|
+
|
8
|
+
super [], { :qobj => @qobj }.merge(@qopts)
|
7
9
|
end
|
8
10
|
|
9
11
|
register_keymap do |k|
|
@@ -13,7 +15,7 @@ class SearchResultsMode < ThreadIndexMode
|
|
13
15
|
def refine_search
|
14
16
|
query = BufferManager.ask :search, "query: ", (@qobj.to_s + " ")
|
15
17
|
return unless query && query !~ /^\s*$/
|
16
|
-
SearchResultsMode.spawn_from_query query
|
18
|
+
SearchResultsMode.spawn_from_query query, @qopts
|
17
19
|
end
|
18
20
|
|
19
21
|
## a proper is_relevant? method requires some way of asking ferret
|
@@ -24,9 +26,10 @@ class SearchResultsMode < ThreadIndexMode
|
|
24
26
|
|
25
27
|
def self.spawn_from_query text
|
26
28
|
begin
|
27
|
-
qobj = Index.parse_user_query_string(text)
|
29
|
+
qobj, extraopts = Index.parse_user_query_string(text)
|
30
|
+
return unless qobj
|
28
31
|
short_text = text.length < 20 ? text : text[0 ... 20] + "..."
|
29
|
-
mode = SearchResultsMode.new qobj
|
32
|
+
mode = SearchResultsMode.new qobj, extraopts
|
30
33
|
BufferManager.spawn "search: \"#{short_text}\"", mode
|
31
34
|
mode.load_threads :num => mode.buffer.content_height
|
32
35
|
rescue Ferret::QueryParser::QueryParseException => e
|
data/lib/sup/modes/text-mode.rb
CHANGED
@@ -4,20 +4,37 @@ class TextMode < ScrollMode
|
|
4
4
|
attr_reader :text
|
5
5
|
register_keymap do |k|
|
6
6
|
k.add :save_to_disk, "Save to disk", 's'
|
7
|
+
k.add :pipe, "Pipe to process", '|'
|
7
8
|
end
|
8
9
|
|
9
|
-
def initialize text=""
|
10
|
+
def initialize text="", filename=nil
|
10
11
|
@text = text
|
12
|
+
@filename = filename
|
11
13
|
update_lines
|
12
14
|
buffer.mark_dirty if buffer
|
13
15
|
super()
|
14
16
|
end
|
15
17
|
|
16
18
|
def save_to_disk
|
17
|
-
fn = BufferManager.ask_for_filename :filename, "Save to file: "
|
19
|
+
fn = BufferManager.ask_for_filename :filename, "Save to file: ", @filename
|
18
20
|
save_to_file(fn) { |f| f.puts text } if fn
|
19
21
|
end
|
20
22
|
|
23
|
+
def pipe
|
24
|
+
command = BufferManager.ask(:shell, "pipe command: ")
|
25
|
+
return if command.nil? || command.empty?
|
26
|
+
|
27
|
+
output = pipe_to_process(command) do |stream|
|
28
|
+
@text.each { |l| stream.puts l }
|
29
|
+
end
|
30
|
+
|
31
|
+
if output
|
32
|
+
BufferManager.spawn "Output of '#{command}'", TextMode.new(output)
|
33
|
+
else
|
34
|
+
BufferManager.flash "'#{command}' done!"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
21
38
|
def text= t
|
22
39
|
@text = t
|
23
40
|
update_lines
|
@@ -31,7 +31,9 @@ EOS
|
|
31
31
|
k.add :forward, "Forward latest message in a thread", 'f'
|
32
32
|
k.add :toggle_tagged, "Tag/untag selected thread", 't'
|
33
33
|
k.add :toggle_tagged_all, "Tag/untag all threads", 'T'
|
34
|
+
k.add :tag_matching, "Tag matching threads", 'g'
|
34
35
|
k.add :apply_to_tagged, "Apply next command to all tagged threads", ';'
|
36
|
+
k.add :join_threads, "Force tagged threads to be joined into the same thread", '#'
|
35
37
|
end
|
36
38
|
|
37
39
|
def initialize hidden_labels=[], load_thread_opts={}
|
@@ -74,7 +76,7 @@ EOS
|
|
74
76
|
end
|
75
77
|
|
76
78
|
## open up a thread view window
|
77
|
-
def select t=nil
|
79
|
+
def select t=nil, when_done=nil
|
78
80
|
t ||= cursor_thread or return
|
79
81
|
|
80
82
|
Redwood::reporting_thread("load messages for thread-view-mode") do
|
@@ -87,73 +89,98 @@ EOS
|
|
87
89
|
m.load_from_source!
|
88
90
|
end
|
89
91
|
end
|
90
|
-
mode = ThreadViewMode.new t, @hidden_labels
|
92
|
+
mode = ThreadViewMode.new t, @hidden_labels, self
|
91
93
|
BufferManager.spawn t.subj, mode
|
92
94
|
BufferManager.draw_screen
|
93
|
-
mode.jump_to_first_open
|
95
|
+
mode.jump_to_first_open true
|
94
96
|
BufferManager.draw_screen # lame TODO: make this unnecessary
|
95
97
|
## the first draw_screen is needed before topline and botline
|
96
98
|
## are set, and the second to show the cursor having moved
|
97
99
|
|
98
100
|
update_text_for_line curpos
|
99
|
-
UpdateManager.relay self, :read, t
|
101
|
+
UpdateManager.relay self, :read, t.first
|
102
|
+
when_done.call if when_done
|
100
103
|
end
|
101
104
|
end
|
102
105
|
|
103
106
|
def multi_select threads
|
104
107
|
threads.each { |t| select t }
|
105
108
|
end
|
106
|
-
|
107
|
-
def handle_label_update sender, m
|
108
|
-
t = @ts_mutex.synchronize { @ts.thread_for(m) } or return
|
109
|
-
handle_label_thread_update sender, t
|
110
|
-
end
|
111
109
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
110
|
+
## this is called by thread-view-modes when the user wants to view
|
111
|
+
## the next thread without going to index-mode. we update the cursor
|
112
|
+
## as a convenience.
|
113
|
+
def launch_next_thread_after thread, &b
|
114
|
+
l = @lines[thread] or return
|
115
|
+
t = @mutex.synchronize do
|
116
|
+
if l < @threads.length - 1
|
117
|
+
set_cursor_pos l + 1 # move out of mutex?
|
118
|
+
@threads[l + 1]
|
119
|
+
end
|
120
|
+
end or return
|
121
|
+
|
122
|
+
select t, b
|
123
|
+
end
|
124
|
+
|
125
|
+
def handle_single_message_labeled_update sender, m
|
126
|
+
## no need to do anything different here; we don't differentiate
|
127
|
+
## messages from their containing threads
|
128
|
+
handle_labeled_update sender, m
|
129
|
+
end
|
130
|
+
|
131
|
+
def handle_labeled_update sender, m
|
132
|
+
if(t = thread_containing(m))
|
133
|
+
l = @lines[t] or return
|
134
|
+
update_text_for_line l
|
135
|
+
elsif is_relevant?(m)
|
136
|
+
add_or_unhide m
|
137
|
+
end
|
116
138
|
end
|
117
139
|
|
118
|
-
def
|
140
|
+
def handle_simple_update sender, m
|
141
|
+
t = thread_containing(m) or return
|
119
142
|
l = @lines[t] or return
|
120
143
|
update_text_for_line l
|
121
|
-
BufferManager.draw_screen
|
122
144
|
end
|
123
145
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
hide_thread t
|
129
|
-
regen_text
|
146
|
+
%w(read unread archived starred unstarred).each do |state|
|
147
|
+
define_method "handle_#{state}_update" do |*a|
|
148
|
+
handle_simple_update(*a)
|
149
|
+
end
|
130
150
|
end
|
131
151
|
|
132
152
|
## overwrite me!
|
133
153
|
def is_relevant? m; false; end
|
134
154
|
|
135
|
-
def
|
155
|
+
def handle_added_update sender, m
|
156
|
+
add_or_unhide m
|
157
|
+
BufferManager.draw_screen
|
158
|
+
end
|
159
|
+
|
160
|
+
def handle_single_message_deleted_update sender, m
|
136
161
|
@ts_mutex.synchronize do
|
137
|
-
return unless
|
138
|
-
@ts.
|
162
|
+
return unless @ts.contains? m
|
163
|
+
@ts.remove_id m.id
|
139
164
|
end
|
140
165
|
update
|
141
|
-
BufferManager.draw_screen
|
142
166
|
end
|
143
167
|
|
144
|
-
def
|
168
|
+
def handle_deleted_update sender, m
|
145
169
|
@ts_mutex.synchronize do
|
146
|
-
return unless @ts.
|
147
|
-
@ts.
|
170
|
+
return unless @ts.contains? m
|
171
|
+
@ts.remove_thread_containing_id m.id
|
148
172
|
end
|
149
173
|
update
|
150
|
-
|
174
|
+
end
|
175
|
+
|
176
|
+
def handle_undeleted_update sender, m
|
177
|
+
add_or_unhide m
|
151
178
|
end
|
152
179
|
|
153
180
|
def update
|
154
181
|
@mutex.synchronize do
|
155
182
|
## let's see you do THIS in python
|
156
|
-
@threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| t.date }.reverse
|
183
|
+
@threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| [t.date, t.first.id] }.reverse
|
157
184
|
@size_widgets = @threads.map { |t| size_widget_for_thread t }
|
158
185
|
@size_widget_width = @size_widgets.max_of { |w| w.length }
|
159
186
|
end
|
@@ -175,10 +202,10 @@ EOS
|
|
175
202
|
def actually_toggle_starred t
|
176
203
|
if t.has_label? :starred # if ANY message has a star
|
177
204
|
t.remove_label :starred # remove from all
|
178
|
-
UpdateManager.relay self, :unstarred, t
|
205
|
+
UpdateManager.relay self, :unstarred, t.first
|
179
206
|
else
|
180
207
|
t.first.add_label :starred # add only to first
|
181
|
-
UpdateManager.relay self, :starred, t
|
208
|
+
UpdateManager.relay self, :starred, t.first
|
182
209
|
end
|
183
210
|
end
|
184
211
|
|
@@ -197,30 +224,30 @@ EOS
|
|
197
224
|
def actually_toggle_archived t
|
198
225
|
if t.has_label? :inbox
|
199
226
|
t.remove_label :inbox
|
200
|
-
UpdateManager.relay self, :archived, t
|
227
|
+
UpdateManager.relay self, :archived, t.first
|
201
228
|
else
|
202
229
|
t.apply_label :inbox
|
203
|
-
UpdateManager.relay self, :unarchived, t
|
230
|
+
UpdateManager.relay self, :unarchived, t.first
|
204
231
|
end
|
205
232
|
end
|
206
233
|
|
207
234
|
def actually_toggle_spammed t
|
208
235
|
if t.has_label? :spam
|
209
236
|
t.remove_label :spam
|
210
|
-
UpdateManager.relay self, :unspammed, t
|
237
|
+
UpdateManager.relay self, :unspammed, t.first
|
211
238
|
else
|
212
239
|
t.apply_label :spam
|
213
|
-
UpdateManager.relay self, :spammed, t
|
240
|
+
UpdateManager.relay self, :spammed, t.first
|
214
241
|
end
|
215
242
|
end
|
216
243
|
|
217
244
|
def actually_toggle_deleted t
|
218
245
|
if t.has_label? :deleted
|
219
246
|
t.remove_label :deleted
|
220
|
-
UpdateManager.relay self, :undeleted, t
|
247
|
+
UpdateManager.relay self, :undeleted, t.first
|
221
248
|
else
|
222
249
|
t.apply_label :deleted
|
223
|
-
UpdateManager.relay self, :deleted, t
|
250
|
+
UpdateManager.relay self, :deleted, t.first
|
224
251
|
end
|
225
252
|
end
|
226
253
|
|
@@ -252,6 +279,18 @@ EOS
|
|
252
279
|
regen_text
|
253
280
|
end
|
254
281
|
|
282
|
+
def join_threads
|
283
|
+
## this command has no non-tagged form. as a convenience, allow this
|
284
|
+
## command to be applied to tagged threads without hitting ';'.
|
285
|
+
@tags.apply_to_tagged :join_threads
|
286
|
+
end
|
287
|
+
|
288
|
+
def multi_join_threads threads
|
289
|
+
@ts.join_threads threads or return
|
290
|
+
@tags.drop_all_tags # otherwise we have tag pointers to invalid threads!
|
291
|
+
update
|
292
|
+
end
|
293
|
+
|
255
294
|
def jump_to_next_new
|
256
295
|
n = @mutex.synchronize do
|
257
296
|
((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread } ||
|
@@ -351,6 +390,14 @@ EOS
|
|
351
390
|
regen_text
|
352
391
|
end
|
353
392
|
|
393
|
+
def tag_matching
|
394
|
+
query = BufferManager.ask :search, "tag threads matching: "
|
395
|
+
return if query.nil? || query.empty?
|
396
|
+
query = /#{query}/i
|
397
|
+
@mutex.synchronize { @threads.each { |t| @tags.tag t if thread_matches?(t, query) } }
|
398
|
+
regen_text
|
399
|
+
end
|
400
|
+
|
354
401
|
def apply_to_tagged; @tags.apply_to_tagged; end
|
355
402
|
|
356
403
|
def edit_labels
|
@@ -363,7 +410,7 @@ EOS
|
|
363
410
|
return unless user_labels
|
364
411
|
thread.labels = keepl + user_labels
|
365
412
|
user_labels.each { |l| LabelManager << l }
|
366
|
-
|
413
|
+
UpdateManager.relay self, :labeled, thread.first
|
367
414
|
end
|
368
415
|
|
369
416
|
def multi_edit_labels threads
|
@@ -395,7 +442,7 @@ EOS
|
|
395
442
|
m = t.latest_message
|
396
443
|
return if m.nil? # probably won't happen
|
397
444
|
m.load_from_source!
|
398
|
-
ForwardMode.spawn_nicely m
|
445
|
+
ForwardMode.spawn_nicely :message => m
|
399
446
|
end
|
400
447
|
|
401
448
|
def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
|
@@ -465,6 +512,28 @@ EOS
|
|
465
512
|
|
466
513
|
protected
|
467
514
|
|
515
|
+
def add_or_unhide m
|
516
|
+
if @hidden_threads[m]
|
517
|
+
@hidden_threads.delete m
|
518
|
+
## now it will re-appear when #update is called
|
519
|
+
else
|
520
|
+
@ts_mutex.synchronize do
|
521
|
+
return unless is_relevant?(m) || @ts.is_relevant?(m)
|
522
|
+
@ts.load_thread_for_message m
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
update
|
527
|
+
end
|
528
|
+
|
529
|
+
def thread_containing m; @ts_mutex.synchronize { @ts.thread_for m } end
|
530
|
+
|
531
|
+
## used to tag threads by query. this can be made a lot more sophisticated,
|
532
|
+
## but for right now we'll do the obvious this.
|
533
|
+
def thread_matches? t, query
|
534
|
+
t.subj =~ query || t.snippet =~ query || t.participants.any? { |x| x.longname =~ query }
|
535
|
+
end
|
536
|
+
|
468
537
|
def size_widget_for_thread t
|
469
538
|
HookManager.run("index-mode-size-widget", :thread => t) || default_size_widget_for(t)
|
470
539
|
end
|