sup 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTORS +84 -0
- data/Gemfile +3 -0
- data/HACKING +42 -0
- data/History.txt +361 -0
- data/LICENSE +280 -0
- data/README.md +70 -0
- data/Rakefile +12 -0
- data/ReleaseNotes +231 -0
- data/bin/sup +434 -0
- data/bin/sup-add +118 -0
- data/bin/sup-config +243 -0
- data/bin/sup-dump +43 -0
- data/bin/sup-import-dump +101 -0
- data/bin/sup-psych-ify-config-files +21 -0
- data/bin/sup-recover-sources +87 -0
- data/bin/sup-sync +210 -0
- data/bin/sup-sync-back-maildir +127 -0
- data/bin/sup-tweak-labels +140 -0
- data/contrib/colorpicker.rb +100 -0
- data/contrib/completion/_sup.zsh +114 -0
- data/devel/console.sh +3 -0
- data/devel/count-loc.sh +3 -0
- data/devel/load-index.rb +9 -0
- data/devel/profile.rb +12 -0
- data/devel/start-console.rb +5 -0
- data/doc/FAQ.txt +119 -0
- data/doc/Hooks.txt +79 -0
- data/doc/Philosophy.txt +69 -0
- data/lib/sup.rb +467 -0
- data/lib/sup/account.rb +90 -0
- data/lib/sup/buffer.rb +768 -0
- data/lib/sup/colormap.rb +239 -0
- data/lib/sup/contact.rb +67 -0
- data/lib/sup/crypto.rb +461 -0
- data/lib/sup/draft.rb +119 -0
- data/lib/sup/hook.rb +159 -0
- data/lib/sup/horizontal_selector.rb +59 -0
- data/lib/sup/idle.rb +42 -0
- data/lib/sup/index.rb +882 -0
- data/lib/sup/interactive_lock.rb +89 -0
- data/lib/sup/keymap.rb +140 -0
- data/lib/sup/label.rb +87 -0
- data/lib/sup/logger.rb +77 -0
- data/lib/sup/logger/singleton.rb +10 -0
- data/lib/sup/maildir.rb +257 -0
- data/lib/sup/mbox.rb +187 -0
- data/lib/sup/message.rb +803 -0
- data/lib/sup/message_chunks.rb +328 -0
- data/lib/sup/mode.rb +140 -0
- data/lib/sup/modes/buffer_list_mode.rb +50 -0
- data/lib/sup/modes/completion_mode.rb +55 -0
- data/lib/sup/modes/compose_mode.rb +38 -0
- data/lib/sup/modes/console_mode.rb +125 -0
- data/lib/sup/modes/contact_list_mode.rb +148 -0
- data/lib/sup/modes/edit_message_async_mode.rb +110 -0
- data/lib/sup/modes/edit_message_mode.rb +728 -0
- data/lib/sup/modes/file_browser_mode.rb +109 -0
- data/lib/sup/modes/forward_mode.rb +82 -0
- data/lib/sup/modes/help_mode.rb +19 -0
- data/lib/sup/modes/inbox_mode.rb +85 -0
- data/lib/sup/modes/label_list_mode.rb +138 -0
- data/lib/sup/modes/label_search_results_mode.rb +38 -0
- data/lib/sup/modes/line_cursor_mode.rb +203 -0
- data/lib/sup/modes/log_mode.rb +57 -0
- data/lib/sup/modes/person_search_results_mode.rb +12 -0
- data/lib/sup/modes/poll_mode.rb +19 -0
- data/lib/sup/modes/reply_mode.rb +228 -0
- data/lib/sup/modes/resume_mode.rb +52 -0
- data/lib/sup/modes/scroll_mode.rb +252 -0
- data/lib/sup/modes/search_list_mode.rb +204 -0
- data/lib/sup/modes/search_results_mode.rb +59 -0
- data/lib/sup/modes/text_mode.rb +76 -0
- data/lib/sup/modes/thread_index_mode.rb +1033 -0
- data/lib/sup/modes/thread_view_mode.rb +941 -0
- data/lib/sup/person.rb +134 -0
- data/lib/sup/poll.rb +272 -0
- data/lib/sup/rfc2047.rb +56 -0
- data/lib/sup/search.rb +110 -0
- data/lib/sup/sent.rb +58 -0
- data/lib/sup/service/label_service.rb +45 -0
- data/lib/sup/source.rb +244 -0
- data/lib/sup/tagger.rb +50 -0
- data/lib/sup/textfield.rb +253 -0
- data/lib/sup/thread.rb +452 -0
- data/lib/sup/time.rb +93 -0
- data/lib/sup/undo.rb +38 -0
- data/lib/sup/update.rb +30 -0
- data/lib/sup/util.rb +747 -0
- data/lib/sup/util/ncurses.rb +274 -0
- data/lib/sup/util/path.rb +9 -0
- data/lib/sup/util/query.rb +17 -0
- data/lib/sup/util/uri.rb +15 -0
- data/lib/sup/version.rb +3 -0
- data/sup.gemspec +53 -0
- data/test/dummy_source.rb +61 -0
- data/test/gnupg_test_home/gpg.conf +1 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
- data/test/gnupg_test_home/trustdb.gpg +0 -0
- data/test/integration/test_label_service.rb +18 -0
- data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
- data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
- data/test/messages/missing-line.eml +9 -0
- data/test/test_crypto.rb +109 -0
- data/test/test_header_parsing.rb +168 -0
- data/test/test_helper.rb +7 -0
- data/test/test_message.rb +532 -0
- data/test/test_messages_dir.rb +147 -0
- data/test/test_yaml_migration.rb +85 -0
- data/test/test_yaml_regressions.rb +17 -0
- data/test/unit/service/test_label_service.rb +19 -0
- data/test/unit/test_horizontal_selector.rb +40 -0
- data/test/unit/util/test_query.rb +46 -0
- data/test/unit/util/test_string.rb +57 -0
- data/test/unit/util/test_uri.rb +19 -0
- metadata +423 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Redwood
|
|
2
|
+
|
|
3
|
+
class SearchResultsMode < ThreadIndexMode
|
|
4
|
+
def initialize query
|
|
5
|
+
@query = query
|
|
6
|
+
super [], query
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
register_keymap do |k|
|
|
10
|
+
k.add :refine_search, "Refine search", '|'
|
|
11
|
+
k.add :save_search, "Save search", '%'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def refine_search
|
|
15
|
+
text = BufferManager.ask :search, "refine query: ", (@query[:text] + " ")
|
|
16
|
+
return unless text && text !~ /^\s*$/
|
|
17
|
+
SearchResultsMode.spawn_from_query text
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def save_search
|
|
21
|
+
name = BufferManager.ask :save_search, "Name this search: "
|
|
22
|
+
return unless name && name !~ /^\s*$/
|
|
23
|
+
name.strip!
|
|
24
|
+
unless SearchManager.valid_name? name
|
|
25
|
+
BufferManager.flash "Not saved: " + SearchManager.name_format_hint
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
if SearchManager.all_searches.include? name
|
|
29
|
+
BufferManager.flash "Not saved: \"#{name}\" already exists"
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
BufferManager.flash "Search saved as \"#{name}\"" if SearchManager.add name, @query[:text].strip
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
## a proper is_relevant? method requires some way of asking the index
|
|
36
|
+
## if an in-memory object satisfies a query. i'm not sure how to do
|
|
37
|
+
## that yet. in the worst case i can make an in-memory index, add
|
|
38
|
+
## the message, and search against it to see if i have > 0 results,
|
|
39
|
+
## but that seems pretty insane.
|
|
40
|
+
|
|
41
|
+
def self.spawn_from_query text
|
|
42
|
+
begin
|
|
43
|
+
if SearchManager.predefined_queries.has_key? text
|
|
44
|
+
query = SearchManager.predefined_queries[text]
|
|
45
|
+
else
|
|
46
|
+
query = Index.parse_query(text)
|
|
47
|
+
end
|
|
48
|
+
return unless query
|
|
49
|
+
short_text = text.length < 20 ? text : text[0 ... 20] + "..."
|
|
50
|
+
mode = SearchResultsMode.new query
|
|
51
|
+
BufferManager.spawn "search: \"#{short_text}\"", mode
|
|
52
|
+
mode.load_threads :num => mode.buffer.content_height
|
|
53
|
+
rescue Index::ParseError => e
|
|
54
|
+
BufferManager.flash "Problem: #{e.message}!"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Redwood
|
|
2
|
+
|
|
3
|
+
class TextMode < ScrollMode
|
|
4
|
+
attr_reader :text
|
|
5
|
+
register_keymap do |k|
|
|
6
|
+
k.add :save_to_disk, "Save to disk", 's'
|
|
7
|
+
k.add :pipe, "Pipe to process", '|'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize text="", filename=nil
|
|
11
|
+
@text = text
|
|
12
|
+
@filename = filename
|
|
13
|
+
update_lines
|
|
14
|
+
buffer.mark_dirty if buffer
|
|
15
|
+
super()
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def save_to_disk
|
|
19
|
+
fn = BufferManager.ask_for_filename :filename, "Save to file: ", @filename
|
|
20
|
+
save_to_file(fn) { |f| f.puts text } if fn
|
|
21
|
+
end
|
|
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.ascii)
|
|
33
|
+
else
|
|
34
|
+
BufferManager.flash "'#{command}' done!"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def text= t
|
|
39
|
+
@text = t
|
|
40
|
+
update_lines
|
|
41
|
+
if buffer
|
|
42
|
+
ensure_mode_validity
|
|
43
|
+
buffer.mark_dirty
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def << line
|
|
48
|
+
@lines = [0] if @text.empty?
|
|
49
|
+
@text << line.fix_encoding!
|
|
50
|
+
@lines << @text.length
|
|
51
|
+
if buffer
|
|
52
|
+
ensure_mode_validity
|
|
53
|
+
buffer.mark_dirty
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def lines
|
|
58
|
+
@lines.length - 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def [] i
|
|
62
|
+
return nil unless i < @lines.length
|
|
63
|
+
@text[@lines[i] ... (i + 1 < @lines.length ? @lines[i + 1] - 1 : @text.length)].normalize_whitespace
|
|
64
|
+
# (@lines[i] ... (i + 1 < @lines.length ? @lines[i + 1] - 1 : @text.length)).inspect
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def update_lines
|
|
70
|
+
pos = @text.find_all_positions("\n")
|
|
71
|
+
pos.push @text.length unless pos.last == @text.length - 1
|
|
72
|
+
@lines = [0] + pos.map { |x| x + 1 }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
end
|
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module Redwood
|
|
4
|
+
|
|
5
|
+
## subclasses should implement:
|
|
6
|
+
## - is_relevant?
|
|
7
|
+
|
|
8
|
+
class ThreadIndexMode < LineCursorMode
|
|
9
|
+
DATE_WIDTH = Time::TO_NICE_S_MAX_LEN
|
|
10
|
+
MIN_FROM_WIDTH = 15
|
|
11
|
+
LOAD_MORE_THREAD_NUM = 20
|
|
12
|
+
|
|
13
|
+
HookManager.register "index-mode-size-widget", <<EOS
|
|
14
|
+
Generates the per-thread size widget for each thread.
|
|
15
|
+
Variables:
|
|
16
|
+
thread: The message thread to be formatted.
|
|
17
|
+
EOS
|
|
18
|
+
|
|
19
|
+
HookManager.register "index-mode-date-widget", <<EOS
|
|
20
|
+
Generates the per-thread date widget for each thread.
|
|
21
|
+
Variables:
|
|
22
|
+
thread: The message thread to be formatted.
|
|
23
|
+
EOS
|
|
24
|
+
|
|
25
|
+
HookManager.register "mark-as-spam", <<EOS
|
|
26
|
+
This hook is run when a thread is marked as spam
|
|
27
|
+
Variables:
|
|
28
|
+
thread: The message thread being marked as spam.
|
|
29
|
+
EOS
|
|
30
|
+
|
|
31
|
+
register_keymap do |k|
|
|
32
|
+
k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
|
|
33
|
+
k.add_multi "Load all threads (! to confirm) :", '!' do |kk|
|
|
34
|
+
kk.add :load_all_threads, "Load all threads (may list a _lot_ of threads)", '!'
|
|
35
|
+
end
|
|
36
|
+
k.add :read_and_archive, "Archive thread (remove from inbox) and mark read", 'A'
|
|
37
|
+
k.add :cancel_search, "Cancel current search", :ctrl_g
|
|
38
|
+
k.add :reload, "Refresh view", '@'
|
|
39
|
+
k.add :toggle_archived, "Toggle archived status", 'a'
|
|
40
|
+
k.add :toggle_starred, "Star or unstar all messages in thread", '*'
|
|
41
|
+
k.add :toggle_new, "Toggle new/read status of all messages in thread", 'N'
|
|
42
|
+
k.add :edit_labels, "Edit or add labels for a thread", 'l'
|
|
43
|
+
k.add :edit_message, "Edit message (drafts only)", 'e'
|
|
44
|
+
k.add :toggle_spam, "Mark/unmark thread as spam", 'S'
|
|
45
|
+
k.add :toggle_deleted, "Delete/undelete thread", 'd'
|
|
46
|
+
k.add :kill, "Kill thread (never to be seen in inbox again)", '&'
|
|
47
|
+
k.add :flush_index, "Flush all changes now", '$'
|
|
48
|
+
k.add :jump_to_next_new, "Jump to next new thread", :tab
|
|
49
|
+
k.add :reply, "Reply to latest message in a thread", 'r'
|
|
50
|
+
k.add :reply_all, "Reply to all participants of the latest message in a thread", 'G'
|
|
51
|
+
k.add :forward, "Forward latest message in a thread", 'f'
|
|
52
|
+
k.add :toggle_tagged, "Tag/untag selected thread", 't'
|
|
53
|
+
k.add :toggle_tagged_all, "Tag/untag all threads", 'T'
|
|
54
|
+
k.add :tag_matching, "Tag matching threads", 'g'
|
|
55
|
+
k.add :apply_to_tagged, "Apply next command to all tagged threads", '+', '='
|
|
56
|
+
k.add :join_threads, "Force tagged threads to be joined into the same thread", '#'
|
|
57
|
+
k.add :undo, "Undo the previous action", 'u'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def initialize hidden_labels=[], load_thread_opts={}
|
|
61
|
+
super()
|
|
62
|
+
@mutex = Mutex.new # covers the following variables:
|
|
63
|
+
@threads = []
|
|
64
|
+
@hidden_threads = {}
|
|
65
|
+
@size_widget_width = nil
|
|
66
|
+
@size_widgets = []
|
|
67
|
+
@date_widget_width = nil
|
|
68
|
+
@date_widgets = []
|
|
69
|
+
@tags = Tagger.new self
|
|
70
|
+
|
|
71
|
+
## these guys, and @text and @lines, are not covered
|
|
72
|
+
@load_thread = nil
|
|
73
|
+
@load_thread_opts = load_thread_opts
|
|
74
|
+
@hidden_labels = hidden_labels + LabelManager::HIDDEN_RESERVED_LABELS
|
|
75
|
+
@date_width = DATE_WIDTH
|
|
76
|
+
|
|
77
|
+
@interrupt_search = false
|
|
78
|
+
|
|
79
|
+
initialize_threads # defines @ts and @ts_mutex
|
|
80
|
+
update # defines @text and @lines
|
|
81
|
+
|
|
82
|
+
UpdateManager.register self
|
|
83
|
+
|
|
84
|
+
@save_thread_mutex = Mutex.new
|
|
85
|
+
|
|
86
|
+
@last_load_more_size = nil
|
|
87
|
+
to_load_more do |size|
|
|
88
|
+
next if @last_load_more_size == 0
|
|
89
|
+
load_threads :num => size,
|
|
90
|
+
:when_done => lambda { |num| @last_load_more_size = num }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def unsaved?; dirty? end
|
|
95
|
+
def lines; @text.length; end
|
|
96
|
+
def [] i; @text[i]; end
|
|
97
|
+
def contains_thread? t; @threads.include?(t) end
|
|
98
|
+
|
|
99
|
+
def reload
|
|
100
|
+
drop_all_threads
|
|
101
|
+
UndoManager.clear
|
|
102
|
+
BufferManager.draw_screen
|
|
103
|
+
load_threads :num => buffer.content_height
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
## open up a thread view window
|
|
107
|
+
def select t=nil, when_done=nil
|
|
108
|
+
t ||= cursor_thread or return
|
|
109
|
+
|
|
110
|
+
Redwood::reporting_thread("load messages for thread-view-mode") do
|
|
111
|
+
num = t.size
|
|
112
|
+
message = "Loading #{num.pluralize 'message body'}..."
|
|
113
|
+
BufferManager.say(message) do |sid|
|
|
114
|
+
t.each_with_index do |(m, *_), i|
|
|
115
|
+
next unless m
|
|
116
|
+
BufferManager.say "#{message} (#{i}/#{num})", sid if t.size > 1
|
|
117
|
+
m.load_from_source!
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
mode = ThreadViewMode.new t, @hidden_labels, self
|
|
121
|
+
BufferManager.spawn t.subj, mode
|
|
122
|
+
BufferManager.draw_screen
|
|
123
|
+
mode.jump_to_first_open if $config[:jump_to_open_message]
|
|
124
|
+
BufferManager.draw_screen # lame TODO: make this unnecessary
|
|
125
|
+
## the first draw_screen is needed before topline and botline
|
|
126
|
+
## are set, and the second to show the cursor having moved
|
|
127
|
+
|
|
128
|
+
t.remove_label :unread
|
|
129
|
+
Index.save_thread t
|
|
130
|
+
|
|
131
|
+
update_text_for_line curpos
|
|
132
|
+
UpdateManager.relay self, :read, t.first
|
|
133
|
+
when_done.call if when_done
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def multi_select threads
|
|
138
|
+
threads.each { |t| select t }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
## these two methods are called by thread-view-modes when the user
|
|
142
|
+
## wants to view the previous/next thread without going back to
|
|
143
|
+
## index-mode. we update the cursor as a convenience.
|
|
144
|
+
def launch_next_thread_after thread, &b
|
|
145
|
+
launch_another_thread thread, 1, &b
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def launch_prev_thread_before thread, &b
|
|
149
|
+
launch_another_thread thread, -1, &b
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def launch_another_thread thread, direction, &b
|
|
153
|
+
l = @lines[thread] or return
|
|
154
|
+
target_l = l + direction
|
|
155
|
+
t = @mutex.synchronize do
|
|
156
|
+
if target_l >= 0 && target_l < @threads.length
|
|
157
|
+
@threads[target_l]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if t # there's a next thread
|
|
162
|
+
set_cursor_pos target_l # move out of mutex?
|
|
163
|
+
select t, b
|
|
164
|
+
elsif b # no next thread. call the block anyways
|
|
165
|
+
b.call
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def handle_single_message_labeled_update sender, m
|
|
170
|
+
## no need to do anything different here; we don't differentiate
|
|
171
|
+
## messages from their containing threads
|
|
172
|
+
handle_labeled_update sender, m
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def handle_labeled_update sender, m
|
|
176
|
+
if(t = thread_containing(m))
|
|
177
|
+
l = @lines[t] or return
|
|
178
|
+
update_text_for_line l
|
|
179
|
+
elsif is_relevant?(m)
|
|
180
|
+
add_or_unhide m
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def handle_simple_update sender, m
|
|
185
|
+
t = thread_containing(m) or return
|
|
186
|
+
l = @lines[t] or return
|
|
187
|
+
update_text_for_line l
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
%w(read unread archived starred unstarred).each do |state|
|
|
191
|
+
define_method "handle_#{state}_update" do |*a|
|
|
192
|
+
handle_simple_update(*a)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
## overwrite me!
|
|
197
|
+
def is_relevant? m; false; end
|
|
198
|
+
|
|
199
|
+
def handle_added_update sender, m
|
|
200
|
+
add_or_unhide m
|
|
201
|
+
BufferManager.draw_screen
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def handle_updated_update sender, m
|
|
205
|
+
t = thread_containing(m) or return
|
|
206
|
+
l = @lines[t] or return
|
|
207
|
+
@ts_mutex.synchronize do
|
|
208
|
+
@ts.delete_message m
|
|
209
|
+
@ts.add_message m
|
|
210
|
+
end
|
|
211
|
+
Index.save_thread t, sync_back = false
|
|
212
|
+
update_text_for_line l
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def handle_location_deleted_update sender, m
|
|
216
|
+
t = thread_containing(m)
|
|
217
|
+
delete_thread t if t and t.first.id == m.id
|
|
218
|
+
@ts_mutex.synchronize do
|
|
219
|
+
@ts.delete_message m if t
|
|
220
|
+
end
|
|
221
|
+
update
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def handle_single_message_deleted_update sender, m
|
|
225
|
+
@ts_mutex.synchronize do
|
|
226
|
+
return unless @ts.contains? m
|
|
227
|
+
@ts.remove_id m.id
|
|
228
|
+
end
|
|
229
|
+
update
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def handle_deleted_update sender, m
|
|
233
|
+
t = @ts_mutex.synchronize { @ts.thread_for m }
|
|
234
|
+
return unless t
|
|
235
|
+
hide_thread t
|
|
236
|
+
update
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def handle_killed_update sender, m
|
|
240
|
+
t = @ts_mutex.synchronize { @ts.thread_for m }
|
|
241
|
+
return unless t
|
|
242
|
+
hide_thread t
|
|
243
|
+
update
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def handle_spammed_update sender, m
|
|
247
|
+
t = @ts_mutex.synchronize { @ts.thread_for m }
|
|
248
|
+
return unless t
|
|
249
|
+
hide_thread t
|
|
250
|
+
update
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def handle_undeleted_update sender, m
|
|
254
|
+
add_or_unhide m
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def handle_unkilled_update sender, m
|
|
258
|
+
add_or_unhide m
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def undo
|
|
262
|
+
UndoManager.undo
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def update
|
|
266
|
+
old_cursor_thread = cursor_thread
|
|
267
|
+
@mutex.synchronize do
|
|
268
|
+
## let's see you do THIS in python
|
|
269
|
+
@threads = @ts.threads.select { |t| !@hidden_threads.member?(t) }.select(&:has_message?).sort_by(&:sort_key)
|
|
270
|
+
@size_widgets = @threads.map { |t| size_widget_for_thread t }
|
|
271
|
+
@size_widget_width = @size_widgets.max_of { |w| w.display_length }
|
|
272
|
+
@date_widgets = @threads.map { |t| date_widget_for_thread t }
|
|
273
|
+
@date_widget_width = @date_widgets.max_of { |w| w.display_length }
|
|
274
|
+
end
|
|
275
|
+
set_cursor_pos @threads.index(old_cursor_thread)||curpos
|
|
276
|
+
|
|
277
|
+
regen_text
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def edit_message
|
|
281
|
+
return unless(t = cursor_thread)
|
|
282
|
+
message, *_ = t.find { |m, *o| m.has_label? :draft }
|
|
283
|
+
if message
|
|
284
|
+
mode = ResumeMode.new message
|
|
285
|
+
BufferManager.spawn "Edit message", mode
|
|
286
|
+
else
|
|
287
|
+
BufferManager.flash "Not a draft message!"
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
## returns an undo lambda
|
|
292
|
+
def actually_toggle_starred t
|
|
293
|
+
if t.has_label? :starred # if ANY message has a star
|
|
294
|
+
t.remove_label :starred # remove from all
|
|
295
|
+
UpdateManager.relay self, :unstarred, t.first
|
|
296
|
+
lambda do
|
|
297
|
+
t.first.add_label :starred
|
|
298
|
+
UpdateManager.relay self, :starred, t.first
|
|
299
|
+
regen_text
|
|
300
|
+
end
|
|
301
|
+
else
|
|
302
|
+
t.first.add_label :starred # add only to first
|
|
303
|
+
UpdateManager.relay self, :starred, t.first
|
|
304
|
+
lambda do
|
|
305
|
+
t.remove_label :starred
|
|
306
|
+
UpdateManager.relay self, :unstarred, t.first
|
|
307
|
+
regen_text
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def toggle_starred
|
|
313
|
+
t = cursor_thread or return
|
|
314
|
+
undo = actually_toggle_starred t
|
|
315
|
+
UndoManager.register "toggling thread starred status", undo, lambda { Index.save_thread t }
|
|
316
|
+
update_text_for_line curpos
|
|
317
|
+
cursor_down
|
|
318
|
+
Index.save_thread t
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def multi_toggle_starred threads
|
|
322
|
+
UndoManager.register "toggling #{threads.size.pluralize 'thread'} starred status",
|
|
323
|
+
threads.map { |t| actually_toggle_starred t },
|
|
324
|
+
lambda { threads.each { |t| Index.save_thread t } }
|
|
325
|
+
regen_text
|
|
326
|
+
threads.each { |t| Index.save_thread t }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
## returns an undo lambda
|
|
330
|
+
def actually_toggle_archived t
|
|
331
|
+
thread = t
|
|
332
|
+
pos = curpos
|
|
333
|
+
if t.has_label? :inbox
|
|
334
|
+
t.remove_label :inbox
|
|
335
|
+
UpdateManager.relay self, :archived, t.first
|
|
336
|
+
lambda do
|
|
337
|
+
thread.apply_label :inbox
|
|
338
|
+
update_text_for_line pos
|
|
339
|
+
UpdateManager.relay self,:unarchived, thread.first
|
|
340
|
+
end
|
|
341
|
+
else
|
|
342
|
+
t.apply_label :inbox
|
|
343
|
+
UpdateManager.relay self, :unarchived, t.first
|
|
344
|
+
lambda do
|
|
345
|
+
thread.remove_label :inbox
|
|
346
|
+
update_text_for_line pos
|
|
347
|
+
UpdateManager.relay self, :unarchived, thread.first
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
## returns an undo lambda
|
|
353
|
+
def actually_toggle_spammed t
|
|
354
|
+
thread = t
|
|
355
|
+
if t.has_label? :spam
|
|
356
|
+
t.remove_label :spam
|
|
357
|
+
add_or_unhide t.first
|
|
358
|
+
UpdateManager.relay self, :unspammed, t.first
|
|
359
|
+
lambda do
|
|
360
|
+
thread.apply_label :spam
|
|
361
|
+
self.hide_thread thread
|
|
362
|
+
UpdateManager.relay self,:spammed, thread.first
|
|
363
|
+
end
|
|
364
|
+
else
|
|
365
|
+
t.apply_label :spam
|
|
366
|
+
hide_thread t
|
|
367
|
+
UpdateManager.relay self, :spammed, t.first
|
|
368
|
+
lambda do
|
|
369
|
+
thread.remove_label :spam
|
|
370
|
+
add_or_unhide thread.first
|
|
371
|
+
UpdateManager.relay self,:unspammed, thread.first
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
## returns an undo lambda
|
|
377
|
+
def actually_toggle_deleted t
|
|
378
|
+
if t.has_label? :deleted
|
|
379
|
+
t.remove_label :deleted
|
|
380
|
+
add_or_unhide t.first
|
|
381
|
+
UpdateManager.relay self, :undeleted, t.first
|
|
382
|
+
lambda do
|
|
383
|
+
t.apply_label :deleted
|
|
384
|
+
hide_thread t
|
|
385
|
+
UpdateManager.relay self, :deleted, t.first
|
|
386
|
+
end
|
|
387
|
+
else
|
|
388
|
+
t.apply_label :deleted
|
|
389
|
+
hide_thread t
|
|
390
|
+
UpdateManager.relay self, :deleted, t.first
|
|
391
|
+
lambda do
|
|
392
|
+
t.remove_label :deleted
|
|
393
|
+
add_or_unhide t.first
|
|
394
|
+
UpdateManager.relay self, :undeleted, t.first
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def toggle_archived
|
|
400
|
+
t = cursor_thread or return
|
|
401
|
+
undo = actually_toggle_archived t
|
|
402
|
+
UndoManager.register "deleting/undeleting thread #{t.first.id}", undo, lambda { update_text_for_line curpos },
|
|
403
|
+
lambda { Index.save_thread t }
|
|
404
|
+
update_text_for_line curpos
|
|
405
|
+
Index.save_thread t
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def multi_toggle_archived threads
|
|
409
|
+
undos = threads.map { |t| actually_toggle_archived t }
|
|
410
|
+
UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", undos, lambda { regen_text },
|
|
411
|
+
lambda { threads.each { |t| Index.save_thread t } }
|
|
412
|
+
regen_text
|
|
413
|
+
threads.each { |t| Index.save_thread t }
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def toggle_new
|
|
417
|
+
t = cursor_thread or return
|
|
418
|
+
t.toggle_label :unread
|
|
419
|
+
update_text_for_line curpos
|
|
420
|
+
cursor_down
|
|
421
|
+
Index.save_thread t
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def multi_toggle_new threads
|
|
425
|
+
threads.each { |t| t.toggle_label :unread }
|
|
426
|
+
regen_text
|
|
427
|
+
threads.each { |t| Index.save_thread t }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def multi_toggle_tagged threads
|
|
431
|
+
@mutex.synchronize { @tags.drop_all_tags }
|
|
432
|
+
regen_text
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def join_threads
|
|
436
|
+
## this command has no non-tagged form. as a convenience, allow this
|
|
437
|
+
## command to be applied to tagged threads without hitting ';'.
|
|
438
|
+
@tags.apply_to_tagged :join_threads
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def multi_join_threads threads
|
|
442
|
+
@ts.join_threads threads or return
|
|
443
|
+
threads.each { |t| Index.save_thread t }
|
|
444
|
+
@tags.drop_all_tags # otherwise we have tag pointers to invalid threads!
|
|
445
|
+
update
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def jump_to_next_new
|
|
449
|
+
n = @mutex.synchronize do
|
|
450
|
+
((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread } ||
|
|
451
|
+
(0 ... curpos).find { |i| @threads[i].has_label? :unread }
|
|
452
|
+
end
|
|
453
|
+
if n
|
|
454
|
+
## jump there if necessary
|
|
455
|
+
jump_to_line n unless n >= topline && n < botline
|
|
456
|
+
set_cursor_pos n
|
|
457
|
+
else
|
|
458
|
+
BufferManager.flash "No new messages."
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def toggle_spam
|
|
463
|
+
t = cursor_thread or return
|
|
464
|
+
multi_toggle_spam [t]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
## both spam and deleted have the curious characteristic that you
|
|
468
|
+
## always want to hide the thread after either applying or removing
|
|
469
|
+
## that label. in all thread-index-views except for
|
|
470
|
+
## label-search-results-mode, when you mark a message as spam or
|
|
471
|
+
## deleted, you want it to disappear immediately; in LSRM, you only
|
|
472
|
+
## see deleted or spam emails, and when you undelete or unspam them
|
|
473
|
+
## you also want them to disappear immediately.
|
|
474
|
+
def multi_toggle_spam threads
|
|
475
|
+
undos = threads.map { |t| actually_toggle_spammed t }
|
|
476
|
+
threads.each { |t| HookManager.run("mark-as-spam", :thread => t) }
|
|
477
|
+
UndoManager.register "marking/unmarking #{threads.size.pluralize 'thread'} as spam",
|
|
478
|
+
undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
|
|
479
|
+
regen_text
|
|
480
|
+
threads.each { |t| Index.save_thread t }
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def toggle_deleted
|
|
484
|
+
t = cursor_thread or return
|
|
485
|
+
multi_toggle_deleted [t]
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
## see comment for multi_toggle_spam
|
|
489
|
+
def multi_toggle_deleted threads
|
|
490
|
+
undos = threads.map { |t| actually_toggle_deleted t }
|
|
491
|
+
UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}",
|
|
492
|
+
undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
|
|
493
|
+
regen_text
|
|
494
|
+
threads.each { |t| Index.save_thread t }
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def kill
|
|
498
|
+
t = cursor_thread or return
|
|
499
|
+
multi_kill [t]
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def flush_index
|
|
503
|
+
@flush_id = BufferManager.say "Flushing index..."
|
|
504
|
+
Index.save_index
|
|
505
|
+
BufferManager.clear @flush_id
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
## m-m-m-m-MULTI-KILL
|
|
509
|
+
def multi_kill threads
|
|
510
|
+
UndoManager.register "killing/unkilling #{threads.size.pluralize 'threads'}" do
|
|
511
|
+
threads.each do |t|
|
|
512
|
+
if t.toggle_label :killed
|
|
513
|
+
add_or_unhide t.first
|
|
514
|
+
else
|
|
515
|
+
hide_thread t
|
|
516
|
+
end
|
|
517
|
+
end.each do |t|
|
|
518
|
+
UpdateManager.relay self, :labeled, t.first
|
|
519
|
+
Index.save_thread t
|
|
520
|
+
end
|
|
521
|
+
regen_text
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
threads.each do |t|
|
|
525
|
+
if t.toggle_label :killed
|
|
526
|
+
hide_thread t
|
|
527
|
+
else
|
|
528
|
+
add_or_unhide t.first
|
|
529
|
+
end
|
|
530
|
+
end.each do |t|
|
|
531
|
+
# send 'labeled'... this might be more specific
|
|
532
|
+
UpdateManager.relay self, :labeled, t.first
|
|
533
|
+
Index.save_thread t
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
killed, unkilled = threads.partition { |t| t.has_label? :killed }.map(&:size)
|
|
537
|
+
BufferManager.flash "#{killed.pluralize 'thread'} killed, #{unkilled} unkilled"
|
|
538
|
+
regen_text
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def cleanup
|
|
542
|
+
UpdateManager.unregister self
|
|
543
|
+
|
|
544
|
+
if @load_thread
|
|
545
|
+
@load_thread.kill
|
|
546
|
+
BufferManager.clear @mbid if @mbid
|
|
547
|
+
sleep 0.1 # TODO: necessary?
|
|
548
|
+
BufferManager.erase_flash
|
|
549
|
+
end
|
|
550
|
+
dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
|
|
551
|
+
fail "dirty threads remain" unless dirty_threads.empty?
|
|
552
|
+
super
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def toggle_tagged
|
|
556
|
+
t = cursor_thread or return
|
|
557
|
+
@mutex.synchronize { @tags.toggle_tag_for t }
|
|
558
|
+
update_text_for_line curpos
|
|
559
|
+
cursor_down
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def toggle_tagged_all
|
|
563
|
+
@mutex.synchronize { @threads.each { |t| @tags.toggle_tag_for t } }
|
|
564
|
+
regen_text
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def tag_matching
|
|
568
|
+
query = BufferManager.ask :search, "tag threads matching (regex): "
|
|
569
|
+
return if query.nil? || query.empty?
|
|
570
|
+
query = begin
|
|
571
|
+
/#{query}/i
|
|
572
|
+
rescue RegexpError => e
|
|
573
|
+
BufferManager.flash "error interpreting '#{query}': #{e.message}"
|
|
574
|
+
return
|
|
575
|
+
end
|
|
576
|
+
@mutex.synchronize { @threads.each { |t| @tags.tag t if thread_matches?(t, query) } }
|
|
577
|
+
regen_text
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def apply_to_tagged; @tags.apply_to_tagged; end
|
|
581
|
+
|
|
582
|
+
def edit_labels
|
|
583
|
+
thread = cursor_thread or return
|
|
584
|
+
speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
|
|
585
|
+
|
|
586
|
+
old_labels = thread.labels
|
|
587
|
+
pos = curpos
|
|
588
|
+
|
|
589
|
+
keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
|
|
590
|
+
|
|
591
|
+
user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl.sort_by {|x| x.to_s}, @hidden_labels
|
|
592
|
+
return unless user_labels
|
|
593
|
+
|
|
594
|
+
thread.labels = Set.new(keepl) + user_labels
|
|
595
|
+
user_labels.each { |l| LabelManager << l }
|
|
596
|
+
update_text_for_line curpos
|
|
597
|
+
|
|
598
|
+
UndoManager.register "labeling thread" do
|
|
599
|
+
thread.labels = old_labels
|
|
600
|
+
update_text_for_line pos
|
|
601
|
+
UpdateManager.relay self, :labeled, thread.first
|
|
602
|
+
Index.save_thread thread
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
UpdateManager.relay self, :labeled, thread.first
|
|
606
|
+
Index.save_thread thread
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def multi_edit_labels threads
|
|
610
|
+
user_labels = BufferManager.ask_for_labels :labels, "Add/remove labels (use -label to remove): ", [], @hidden_labels
|
|
611
|
+
return unless user_labels
|
|
612
|
+
|
|
613
|
+
user_labels.map! { |l| (l.to_s =~ /^-/)? [l.to_s.gsub(/^-?/, '').to_sym, true] : [l, false] }
|
|
614
|
+
hl = user_labels.select { |(l,_)| @hidden_labels.member? l }
|
|
615
|
+
unless hl.empty?
|
|
616
|
+
BufferManager.flash "'#{hl}' is a reserved label!"
|
|
617
|
+
return
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
old_labels = threads.map { |t| t.labels.dup }
|
|
621
|
+
|
|
622
|
+
threads.each do |t|
|
|
623
|
+
user_labels.each do |(l, to_remove)|
|
|
624
|
+
if to_remove
|
|
625
|
+
t.remove_label l
|
|
626
|
+
else
|
|
627
|
+
t.apply_label l
|
|
628
|
+
LabelManager << l
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
UpdateManager.relay self, :labeled, t.first
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
regen_text
|
|
635
|
+
|
|
636
|
+
UndoManager.register "labeling #{threads.size.pluralize 'thread'}" do
|
|
637
|
+
threads.zip(old_labels).map do |t, old_labels|
|
|
638
|
+
t.labels = old_labels
|
|
639
|
+
UpdateManager.relay self, :labeled, t.first
|
|
640
|
+
Index.save_thread t
|
|
641
|
+
end
|
|
642
|
+
regen_text
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
threads.each { |t| Index.save_thread t }
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def reply type_arg=nil
|
|
649
|
+
t = cursor_thread or return
|
|
650
|
+
m = t.latest_message
|
|
651
|
+
return if m.nil? # probably won't happen
|
|
652
|
+
m.load_from_source!
|
|
653
|
+
mode = ReplyMode.new m, type_arg
|
|
654
|
+
BufferManager.spawn "Reply to #{m.subj}", mode
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def reply_all; reply :all; end
|
|
658
|
+
|
|
659
|
+
def forward
|
|
660
|
+
t = cursor_thread or return
|
|
661
|
+
m = t.latest_message
|
|
662
|
+
return if m.nil? # probably won't happen
|
|
663
|
+
m.load_from_source!
|
|
664
|
+
ForwardMode.spawn_nicely :message => m
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
|
|
668
|
+
return if @load_thread # todo: wrap in mutex
|
|
669
|
+
@load_thread = Redwood::reporting_thread("load threads for thread-index-mode") do
|
|
670
|
+
num = load_n_threads n, opts
|
|
671
|
+
opts[:when_done].call(num) if opts[:when_done]
|
|
672
|
+
@load_thread = nil
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
## TODO: figure out @ts_mutex in this method
|
|
677
|
+
def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
|
|
678
|
+
@interrupt_search = false
|
|
679
|
+
@mbid = BufferManager.say "Searching for threads..."
|
|
680
|
+
|
|
681
|
+
ts_to_load = n
|
|
682
|
+
ts_to_load = ts_to_load + @ts.size unless n == -1 # -1 means all threads
|
|
683
|
+
|
|
684
|
+
orig_size = @ts.size
|
|
685
|
+
last_update = Time.now
|
|
686
|
+
@ts.load_n_threads(ts_to_load, opts) do |i|
|
|
687
|
+
if (Time.now - last_update) >= 0.25
|
|
688
|
+
BufferManager.say "Loaded #{i.pluralize 'thread'}...", @mbid
|
|
689
|
+
update
|
|
690
|
+
BufferManager.draw_screen
|
|
691
|
+
last_update = Time.now
|
|
692
|
+
end
|
|
693
|
+
::Thread.pass
|
|
694
|
+
break if @interrupt_search
|
|
695
|
+
end
|
|
696
|
+
@ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
|
|
697
|
+
|
|
698
|
+
update
|
|
699
|
+
BufferManager.clear @mbid
|
|
700
|
+
@mbid = nil
|
|
701
|
+
BufferManager.draw_screen
|
|
702
|
+
@ts.size - orig_size
|
|
703
|
+
end
|
|
704
|
+
ignore_concurrent_calls :load_n_threads
|
|
705
|
+
|
|
706
|
+
def status
|
|
707
|
+
if (l = lines) == 0
|
|
708
|
+
"line 0 of 0"
|
|
709
|
+
else
|
|
710
|
+
"line #{curpos + 1} of #{l}"
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def cancel_search
|
|
715
|
+
@interrupt_search = true
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def load_all_threads
|
|
719
|
+
load_threads :num => -1
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def load_threads opts={}
|
|
723
|
+
if opts[:num].nil?
|
|
724
|
+
n = ThreadIndexMode::LOAD_MORE_THREAD_NUM
|
|
725
|
+
else
|
|
726
|
+
n = opts[:num]
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
|
|
730
|
+
opts[:when_done].call(num) if opts[:when_done]
|
|
731
|
+
|
|
732
|
+
if num > 0
|
|
733
|
+
BufferManager.flash "Found #{num.pluralize 'thread'}."
|
|
734
|
+
else
|
|
735
|
+
BufferManager.flash "No matches."
|
|
736
|
+
end
|
|
737
|
+
end)})
|
|
738
|
+
|
|
739
|
+
if opts[:background] || opts[:background].nil?
|
|
740
|
+
load_n_threads_background n, myopts
|
|
741
|
+
else
|
|
742
|
+
load_n_threads n, myopts
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
ignore_concurrent_calls :load_threads
|
|
746
|
+
|
|
747
|
+
def read_and_archive
|
|
748
|
+
return unless cursor_thread
|
|
749
|
+
thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
|
|
750
|
+
|
|
751
|
+
was_unread = thread.labels.member? :unread
|
|
752
|
+
UndoManager.register "reading and archiving thread" do
|
|
753
|
+
thread.apply_label :inbox
|
|
754
|
+
thread.apply_label :unread if was_unread
|
|
755
|
+
add_or_unhide thread.first
|
|
756
|
+
Index.save_thread thread
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
cursor_thread.remove_label :unread
|
|
760
|
+
cursor_thread.remove_label :inbox
|
|
761
|
+
hide_thread cursor_thread
|
|
762
|
+
regen_text
|
|
763
|
+
Index.save_thread thread
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def multi_read_and_archive threads
|
|
767
|
+
old_labels = threads.map { |t| t.labels.dup }
|
|
768
|
+
|
|
769
|
+
threads.each do |t|
|
|
770
|
+
t.remove_label :unread
|
|
771
|
+
t.remove_label :inbox
|
|
772
|
+
hide_thread t
|
|
773
|
+
end
|
|
774
|
+
regen_text
|
|
775
|
+
|
|
776
|
+
UndoManager.register "reading and archiving #{threads.size.pluralize 'thread'}" do
|
|
777
|
+
threads.zip(old_labels).each do |t, l|
|
|
778
|
+
t.labels = l
|
|
779
|
+
add_or_unhide t.first
|
|
780
|
+
Index.save_thread t
|
|
781
|
+
end
|
|
782
|
+
regen_text
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
threads.each { |t| Index.save_thread t }
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def resize rows, cols
|
|
789
|
+
regen_text
|
|
790
|
+
super
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
protected
|
|
794
|
+
|
|
795
|
+
def add_or_unhide m
|
|
796
|
+
@ts_mutex.synchronize do
|
|
797
|
+
if (is_relevant?(m) || @ts.is_relevant?(m)) && !@ts.contains?(m)
|
|
798
|
+
@ts.load_thread_for_message m, @load_thread_opts
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
@hidden_threads.delete @ts.thread_for(m)
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
update
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def thread_containing m; @ts_mutex.synchronize { @ts.thread_for m } end
|
|
808
|
+
|
|
809
|
+
## used to tag threads by query. this can be made a lot more sophisticated,
|
|
810
|
+
## but for right now we'll do the obvious this.
|
|
811
|
+
def thread_matches? t, query
|
|
812
|
+
t.subj =~ query || t.snippet =~ query || t.participants.any? { |x| x.longname =~ query }
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def size_widget_for_thread t
|
|
816
|
+
HookManager.run("index-mode-size-widget", :thread => t) || default_size_widget_for(t)
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def date_widget_for_thread t
|
|
820
|
+
HookManager.run("index-mode-date-widget", :thread => t) || default_date_widget_for(t)
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def cursor_thread; @mutex.synchronize { @threads[curpos] }; end
|
|
824
|
+
|
|
825
|
+
def drop_all_threads
|
|
826
|
+
@tags.drop_all_tags
|
|
827
|
+
initialize_threads
|
|
828
|
+
update
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def delete_thread t
|
|
832
|
+
@mutex.synchronize do
|
|
833
|
+
i = @threads.index(t) or return
|
|
834
|
+
@threads.delete_at i
|
|
835
|
+
@size_widgets.delete_at i
|
|
836
|
+
@date_widgets.delete_at i
|
|
837
|
+
@tags.drop_tag_for t
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def hide_thread t
|
|
842
|
+
@mutex.synchronize do
|
|
843
|
+
i = @threads.index(t) or return
|
|
844
|
+
raise "already hidden" if @hidden_threads[t]
|
|
845
|
+
@hidden_threads[t] = true
|
|
846
|
+
@threads.delete_at i
|
|
847
|
+
@size_widgets.delete_at i
|
|
848
|
+
@date_widgets.delete_at i
|
|
849
|
+
@tags.drop_tag_for t
|
|
850
|
+
end
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
def update_text_for_line l
|
|
854
|
+
return unless l # not sure why this happens, but it does, occasionally
|
|
855
|
+
|
|
856
|
+
need_update = false
|
|
857
|
+
|
|
858
|
+
@mutex.synchronize do
|
|
859
|
+
@size_widgets[l] = size_widget_for_thread @threads[l]
|
|
860
|
+
@date_widgets[l] = date_widget_for_thread @threads[l]
|
|
861
|
+
|
|
862
|
+
## if a widget size has increased, we need to redraw everyone
|
|
863
|
+
need_update =
|
|
864
|
+
(@size_widgets[l].size > @size_widget_width) or
|
|
865
|
+
(@date_widgets[l].size > @date_widget_width)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
if need_update
|
|
869
|
+
update
|
|
870
|
+
else
|
|
871
|
+
@text[l] = text_for_thread_at l
|
|
872
|
+
buffer.mark_dirty if buffer
|
|
873
|
+
end
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def regen_text
|
|
877
|
+
threads = @mutex.synchronize { @threads }
|
|
878
|
+
@text = threads.map_with_index { |t, i| text_for_thread_at i }
|
|
879
|
+
@lines = threads.map_with_index { |t, i| [t, i] }.to_h
|
|
880
|
+
buffer.mark_dirty if buffer
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def authors; map { |m, *o| m.from if m }.compact.uniq; end
|
|
884
|
+
|
|
885
|
+
## preserve author order from the thread
|
|
886
|
+
def author_names_and_newness_for_thread t, limit=nil
|
|
887
|
+
new = {}
|
|
888
|
+
seen = {}
|
|
889
|
+
authors = t.map do |m, *o|
|
|
890
|
+
next unless m && m.from
|
|
891
|
+
new[m.from] ||= m.has_label?(:unread)
|
|
892
|
+
next if seen[m.from]
|
|
893
|
+
seen[m.from] = true
|
|
894
|
+
m.from
|
|
895
|
+
end.compact
|
|
896
|
+
|
|
897
|
+
result = []
|
|
898
|
+
authors.each do |a|
|
|
899
|
+
break if limit && result.size >= limit
|
|
900
|
+
name = if AccountManager.is_account?(a)
|
|
901
|
+
"me"
|
|
902
|
+
elsif t.authors.size == 1
|
|
903
|
+
a.mediumname
|
|
904
|
+
else
|
|
905
|
+
a.shortname
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
result << [name, new[a]]
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
if result.size == 1 && (author_and_newness = result.assoc("me"))
|
|
912
|
+
unless (recipients = t.participants - t.authors).empty?
|
|
913
|
+
result = recipients.collect do |r|
|
|
914
|
+
break if limit && result.size >= limit
|
|
915
|
+
name = (recipients.size == 1) ? r.mediumname : r.shortname
|
|
916
|
+
["(#{name})", author_and_newness[1]]
|
|
917
|
+
end
|
|
918
|
+
end
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
result
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
AUTHOR_LIMIT = 5
|
|
925
|
+
def text_for_thread_at line
|
|
926
|
+
t, size_widget, date_widget = @mutex.synchronize do
|
|
927
|
+
[@threads[line], @size_widgets[line], @date_widgets[line]]
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
starred = t.has_label? :starred
|
|
931
|
+
|
|
932
|
+
## format the from column
|
|
933
|
+
cur_width = 0
|
|
934
|
+
ann = author_names_and_newness_for_thread t, AUTHOR_LIMIT
|
|
935
|
+
from = []
|
|
936
|
+
ann.each_with_index do |(name, newness), i|
|
|
937
|
+
break if cur_width >= from_width
|
|
938
|
+
last = i == ann.length - 1
|
|
939
|
+
|
|
940
|
+
abbrev =
|
|
941
|
+
if cur_width + name.display_length > from_width
|
|
942
|
+
name.slice_by_display_length(from_width - cur_width - 1) + "."
|
|
943
|
+
elsif cur_width + name.display_length == from_width
|
|
944
|
+
name.slice_by_display_length(from_width - cur_width)
|
|
945
|
+
else
|
|
946
|
+
if last
|
|
947
|
+
name.slice_by_display_length(from_width - cur_width)
|
|
948
|
+
else
|
|
949
|
+
name.slice_by_display_length(from_width - cur_width - 1) + ","
|
|
950
|
+
end
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
cur_width += abbrev.display_length
|
|
954
|
+
|
|
955
|
+
if last && from_width > cur_width
|
|
956
|
+
abbrev += " " * (from_width - cur_width)
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
from << [(newness ? :index_new_color : (starred ? :index_starred_color : :index_old_color)), abbrev]
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
is_me = AccountManager.method(:is_account?)
|
|
963
|
+
directly_participated = t.direct_participants.any?(&is_me)
|
|
964
|
+
participated = directly_participated || t.participants.any?(&is_me)
|
|
965
|
+
|
|
966
|
+
subj_color =
|
|
967
|
+
if t.has_label?(:draft)
|
|
968
|
+
:index_draft_color
|
|
969
|
+
elsif t.has_label?(:unread)
|
|
970
|
+
:index_new_color
|
|
971
|
+
elsif starred
|
|
972
|
+
:index_starred_color
|
|
973
|
+
elsif Colormap.sym_is_defined(:index_subject_color)
|
|
974
|
+
:index_subject_color
|
|
975
|
+
else
|
|
976
|
+
:index_old_color
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
size_padding = @size_widget_width - size_widget.display_length
|
|
980
|
+
size_widget_text = sprintf "%#{size_padding}s%s", "", size_widget
|
|
981
|
+
|
|
982
|
+
date_padding = @date_widget_width - date_widget.display_length
|
|
983
|
+
date_widget_text = sprintf "%#{date_padding}s%s", "", date_widget
|
|
984
|
+
|
|
985
|
+
[
|
|
986
|
+
[:tagged_color, @tags.tagged?(t) ? ">" : " "],
|
|
987
|
+
[:date_color, date_widget_text],
|
|
988
|
+
[:starred_color, (starred ? "*" : " ")],
|
|
989
|
+
] +
|
|
990
|
+
from +
|
|
991
|
+
[
|
|
992
|
+
[:size_widget_color, size_widget_text],
|
|
993
|
+
[:with_attachment_color , t.labels.member?(:attachment) ? "@" : " "],
|
|
994
|
+
[:to_me_color, directly_participated ? ">" : (participated ? '+' : " ")],
|
|
995
|
+
] +
|
|
996
|
+
(t.labels - @hidden_labels).sort_by {|x| x.to_s}.map {
|
|
997
|
+
|label| [Colormap.sym_is_defined("label_#{label}_color".to_sym) || :label_color, "#{label} "]
|
|
998
|
+
} +
|
|
999
|
+
[
|
|
1000
|
+
[subj_color, t.subj + (t.subj.empty? ? "" : " ")],
|
|
1001
|
+
[:snippet_color, t.snippet],
|
|
1002
|
+
]
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
def dirty?; @mutex.synchronize { (@hidden_threads.keys + @threads).any? { |t| t.dirty? } } end
|
|
1006
|
+
|
|
1007
|
+
private
|
|
1008
|
+
|
|
1009
|
+
def default_size_widget_for t
|
|
1010
|
+
case t.size
|
|
1011
|
+
when 1
|
|
1012
|
+
""
|
|
1013
|
+
else
|
|
1014
|
+
"(#{t.size})"
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
def default_date_widget_for t
|
|
1019
|
+
t.date.getlocal.to_nice_s
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
def from_width
|
|
1023
|
+
[(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
def initialize_threads
|
|
1027
|
+
@ts = ThreadSet.new Index.instance, $config[:thread_by_subject]
|
|
1028
|
+
@ts_mutex = Mutex.new
|
|
1029
|
+
@hidden_threads = {}
|
|
1030
|
+
end
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
end
|