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,941 @@
|
|
|
1
|
+
module Redwood
|
|
2
|
+
|
|
3
|
+
class ThreadViewMode < LineCursorMode
|
|
4
|
+
## this holds all info we need to lay out a message
|
|
5
|
+
class MessageLayout
|
|
6
|
+
attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new, :toggled_state
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class ChunkLayout
|
|
10
|
+
attr_accessor :state
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
INDENT_SPACES = 2 # how many spaces to indent child messages
|
|
14
|
+
|
|
15
|
+
HookManager.register "detailed-headers", <<EOS
|
|
16
|
+
Add or remove headers from the detailed header display of a message.
|
|
17
|
+
Variables:
|
|
18
|
+
message: The message whose headers are to be formatted.
|
|
19
|
+
headers: A hash of header (name, value) pairs, initialized to the default
|
|
20
|
+
headers.
|
|
21
|
+
Return value:
|
|
22
|
+
None. The variable 'headers' should be modified in place.
|
|
23
|
+
EOS
|
|
24
|
+
|
|
25
|
+
HookManager.register "bounce-command", <<EOS
|
|
26
|
+
Determines the command used to bounce a message.
|
|
27
|
+
Variables:
|
|
28
|
+
from: The From header of the message being bounced
|
|
29
|
+
(eg: likely _not_ your address).
|
|
30
|
+
to: The addresses you asked the message to be bounced to as an array.
|
|
31
|
+
Return value:
|
|
32
|
+
A string representing the command to pipe the mail into. This
|
|
33
|
+
should include the entire command except for the destination addresses,
|
|
34
|
+
which will be appended by sup.
|
|
35
|
+
EOS
|
|
36
|
+
|
|
37
|
+
HookManager.register "publish", <<EOS
|
|
38
|
+
Executed when a message or a chunk is requested to be published.
|
|
39
|
+
Variables:
|
|
40
|
+
chunk: Redwood::Message or Redwood::Chunk::* to be published.
|
|
41
|
+
Return value:
|
|
42
|
+
None.
|
|
43
|
+
EOS
|
|
44
|
+
|
|
45
|
+
register_keymap do |k|
|
|
46
|
+
k.add :toggle_detailed_header, "Toggle detailed header", 'h'
|
|
47
|
+
k.add :show_header, "Show full message header", 'H'
|
|
48
|
+
k.add :show_message, "Show full message (raw form)", 'V'
|
|
49
|
+
k.add :activate_chunk, "Expand/collapse or activate item", :enter
|
|
50
|
+
k.add :expand_all_messages, "Expand/collapse all messages", 'E'
|
|
51
|
+
k.add :edit_draft, "Edit draft", 'e'
|
|
52
|
+
k.add :send_draft, "Send draft", 'y'
|
|
53
|
+
k.add :edit_labels, "Edit or add labels for a thread", 'l'
|
|
54
|
+
k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
|
|
55
|
+
k.add :jump_to_next_open, "Jump to next open message", 'n'
|
|
56
|
+
k.add :jump_to_next_and_open, "Jump to next message and open", "\C-n"
|
|
57
|
+
k.add :jump_to_prev_open, "Jump to previous open message", 'p'
|
|
58
|
+
k.add :jump_to_prev_and_open, "Jump to previous message and open", "\C-p"
|
|
59
|
+
k.add :align_current_message, "Align current message in buffer", 'z'
|
|
60
|
+
k.add :toggle_starred, "Star or unstar message", '*'
|
|
61
|
+
k.add :toggle_new, "Toggle unread/read status of message", 'N'
|
|
62
|
+
# k.add :collapse_non_new_messages, "Collapse all but unread messages", 'N'
|
|
63
|
+
k.add :reply, "Reply to a message", 'r'
|
|
64
|
+
k.add :reply_all, "Reply to all participants of this message", 'G'
|
|
65
|
+
k.add :forward, "Forward a message or attachment", 'f'
|
|
66
|
+
k.add :bounce, "Bounce message to other recipient(s)", '!'
|
|
67
|
+
k.add :alias, "Edit alias/nickname for a person", 'i'
|
|
68
|
+
k.add :edit_as_new, "Edit message as new", 'D'
|
|
69
|
+
k.add :save_to_disk, "Save message/attachment to disk", 's'
|
|
70
|
+
k.add :save_all_to_disk, "Save all attachments to disk", 'A'
|
|
71
|
+
k.add :publish, "Publish message/attachment using publish-hook", 'P'
|
|
72
|
+
k.add :search, "Search for messages from particular people", 'S'
|
|
73
|
+
k.add :compose, "Compose message to person", 'm'
|
|
74
|
+
k.add :subscribe_to_list, "Subscribe to/unsubscribe from mailing list", "("
|
|
75
|
+
k.add :unsubscribe_from_list, "Subscribe to/unsubscribe from mailing list", ")"
|
|
76
|
+
k.add :pipe_message, "Pipe message or attachment to a shell command", '|'
|
|
77
|
+
|
|
78
|
+
k.add :archive_and_next, "Archive this thread, kill buffer, and view next", 'a'
|
|
79
|
+
k.add :delete_and_next, "Delete this thread, kill buffer, and view next", 'd'
|
|
80
|
+
k.add :kill_and_next, "Kill this thread, kill buffer, and view next", '&'
|
|
81
|
+
k.add :toggle_wrap, "Toggle wrapping of text", 'w'
|
|
82
|
+
|
|
83
|
+
k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read:", '.' do |kk|
|
|
84
|
+
kk.add :archive_and_kill, "Archive this thread and kill buffer", 'a'
|
|
85
|
+
kk.add :delete_and_kill, "Delete this thread and kill buffer", 'd'
|
|
86
|
+
kk.add :kill_and_kill, "Kill this thread and kill buffer", '&'
|
|
87
|
+
kk.add :spam_and_kill, "Mark this thread as spam and kill buffer", 's'
|
|
88
|
+
kk.add :unread_and_kill, "Mark this thread as unread and kill buffer", 'N'
|
|
89
|
+
kk.add :do_nothing_and_kill, "Just kill this buffer", '.'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read/do (n)othing:", ',' do |kk|
|
|
93
|
+
kk.add :archive_and_next, "Archive this thread, kill buffer, and view next", 'a'
|
|
94
|
+
kk.add :delete_and_next, "Delete this thread, kill buffer, and view next", 'd'
|
|
95
|
+
kk.add :kill_and_next, "Kill this thread, kill buffer, and view next", '&'
|
|
96
|
+
kk.add :spam_and_next, "Mark this thread as spam, kill buffer, and view next", 's'
|
|
97
|
+
kk.add :unread_and_next, "Mark this thread as unread, kill buffer, and view next", 'N'
|
|
98
|
+
kk.add :do_nothing_and_next, "Kill buffer, and view next", 'n', ','
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read/do (n)othing:", ']' do |kk|
|
|
102
|
+
kk.add :archive_and_prev, "Archive this thread, kill buffer, and view previous", 'a'
|
|
103
|
+
kk.add :delete_and_prev, "Delete this thread, kill buffer, and view previous", 'd'
|
|
104
|
+
kk.add :kill_and_prev, "Kill this thread, kill buffer, and view previous", '&'
|
|
105
|
+
kk.add :spam_and_prev, "Mark this thread as spam, kill buffer, and view previous", 's'
|
|
106
|
+
kk.add :unread_and_prev, "Mark this thread as unread, kill buffer, and view previous", 'N'
|
|
107
|
+
kk.add :do_nothing_and_prev, "Kill buffer, and view previous", 'n', ']'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
## there are a couple important instance variables we hold to format
|
|
112
|
+
## the thread and to provide line-based functionality. @layout is a
|
|
113
|
+
## map from Messages to MessageLayouts, and @chunk_layout from
|
|
114
|
+
## Chunks to ChunkLayouts. @message_lines is a map from row #s to
|
|
115
|
+
## Message objects. @chunk_lines is a map from row #s to Chunk
|
|
116
|
+
## objects. @person_lines is a map from row #s to Person objects.
|
|
117
|
+
|
|
118
|
+
def initialize thread, hidden_labels=[], index_mode=nil
|
|
119
|
+
super :slip_rows => $config[:slip_rows]
|
|
120
|
+
@thread = thread
|
|
121
|
+
@hidden_labels = hidden_labels
|
|
122
|
+
|
|
123
|
+
## used for dispatch-and-next
|
|
124
|
+
@index_mode = index_mode
|
|
125
|
+
@dying = false
|
|
126
|
+
|
|
127
|
+
@layout = SavingHash.new { MessageLayout.new }
|
|
128
|
+
@chunk_layout = SavingHash.new { ChunkLayout.new }
|
|
129
|
+
earliest, latest = nil, nil
|
|
130
|
+
latest_date = nil
|
|
131
|
+
altcolor = false
|
|
132
|
+
|
|
133
|
+
@thread.each do |m, d, p|
|
|
134
|
+
next unless m
|
|
135
|
+
earliest ||= m
|
|
136
|
+
@layout[m].state = initial_state_for m
|
|
137
|
+
@layout[m].toggled_state = false
|
|
138
|
+
@layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
|
|
139
|
+
@layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color
|
|
140
|
+
@layout[m].orig_new = m.has_label? :read
|
|
141
|
+
altcolor = !altcolor
|
|
142
|
+
if latest_date.nil? || m.date > latest_date
|
|
143
|
+
latest_date = m.date
|
|
144
|
+
latest = m
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
@wrap = true
|
|
149
|
+
|
|
150
|
+
@layout[latest].state = :open if @layout[latest].state == :closed
|
|
151
|
+
@layout[earliest].state = :detailed if earliest.has_label?(:unread) || @thread.size == 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def toggle_wrap
|
|
155
|
+
@wrap = !@wrap
|
|
156
|
+
regen_text
|
|
157
|
+
buffer.mark_dirty if buffer
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def draw_line ln, opts={}
|
|
161
|
+
if ln == curpos
|
|
162
|
+
super ln, :highlight => true
|
|
163
|
+
else
|
|
164
|
+
super
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
def lines; @text.length; end
|
|
168
|
+
def [] i; @text[i]; end
|
|
169
|
+
|
|
170
|
+
## a little hacky---since regen_text can depend on buffer features like the
|
|
171
|
+
## content_width, we don't call it in the constructor, and instead call it
|
|
172
|
+
## here, which is set before we're responsible for drawing ourself.
|
|
173
|
+
def buffer= b
|
|
174
|
+
super
|
|
175
|
+
regen_text
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def show_header
|
|
179
|
+
m = @message_lines[curpos] or return
|
|
180
|
+
BufferManager.spawn_unless_exists("Full header for #{m.id}") do
|
|
181
|
+
TextMode.new m.raw_header.ascii
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def show_message
|
|
186
|
+
m = @message_lines[curpos] or return
|
|
187
|
+
BufferManager.spawn_unless_exists("Raw message for #{m.id}") do
|
|
188
|
+
TextMode.new m.raw_message.ascii
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def toggle_detailed_header
|
|
193
|
+
m = @message_lines[curpos] or return
|
|
194
|
+
@layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
|
|
195
|
+
update
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def reply type_arg=nil
|
|
199
|
+
m = @message_lines[curpos] or return
|
|
200
|
+
mode = ReplyMode.new m, type_arg
|
|
201
|
+
BufferManager.spawn "Reply to #{m.subj}", mode
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def reply_all; reply :all; end
|
|
205
|
+
|
|
206
|
+
def subscribe_to_list
|
|
207
|
+
m = @message_lines[curpos] or return
|
|
208
|
+
if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
|
|
209
|
+
ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "subscribe")
|
|
210
|
+
else
|
|
211
|
+
BufferManager.flash "Can't find List-Subscribe header for this message."
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def unsubscribe_from_list
|
|
216
|
+
m = @message_lines[curpos] or return
|
|
217
|
+
if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
|
|
218
|
+
ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "unsubscribe")
|
|
219
|
+
else
|
|
220
|
+
BufferManager.flash "Can't find List-Unsubscribe header for this message."
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def forward
|
|
225
|
+
if(chunk = @chunk_lines[curpos]) && chunk.is_a?(Chunk::Attachment)
|
|
226
|
+
ForwardMode.spawn_nicely :attachments => [chunk]
|
|
227
|
+
elsif(m = @message_lines[curpos])
|
|
228
|
+
ForwardMode.spawn_nicely :message => m
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def bounce
|
|
233
|
+
m = @message_lines[curpos] or return
|
|
234
|
+
to = BufferManager.ask_for_contacts(:people, "Bounce To: ") or return
|
|
235
|
+
|
|
236
|
+
defcmd = AccountManager.default_account.bounce_sendmail
|
|
237
|
+
|
|
238
|
+
cmd = case (hookcmd = HookManager.run "bounce-command", :from => m.from, :to => to)
|
|
239
|
+
when nil, /^$/ then defcmd
|
|
240
|
+
else hookcmd
|
|
241
|
+
end + ' ' + to.map { |t| t.email }.join(' ')
|
|
242
|
+
|
|
243
|
+
bt = to.size > 1 ? "#{to.size} recipients" : to[0].to_s
|
|
244
|
+
|
|
245
|
+
if BufferManager.ask_yes_or_no "Really bounce to #{bt}?"
|
|
246
|
+
debug "bounce command: #{cmd}"
|
|
247
|
+
begin
|
|
248
|
+
IO.popen(cmd, 'w') do |sm|
|
|
249
|
+
sm.puts m.raw_message
|
|
250
|
+
end
|
|
251
|
+
raise SendmailCommandFailed, "Couldn't execute #{cmd}" unless $? == 0
|
|
252
|
+
m.add_label :forwarded
|
|
253
|
+
Index.save_message m
|
|
254
|
+
rescue SystemCallError, SendmailCommandFailed => e
|
|
255
|
+
warn "problem sending mail: #{e.message}"
|
|
256
|
+
BufferManager.flash "Problem sending mail: #{e.message}"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
include CanAliasContacts
|
|
262
|
+
def alias
|
|
263
|
+
p = @person_lines[curpos] or return
|
|
264
|
+
alias_contact p
|
|
265
|
+
update
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def search
|
|
269
|
+
p = @person_lines[curpos] or return
|
|
270
|
+
mode = PersonSearchResultsMode.new [p]
|
|
271
|
+
BufferManager.spawn "Search for #{p.name}", mode
|
|
272
|
+
mode.load_threads :num => mode.buffer.content_height
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def compose
|
|
276
|
+
p = @person_lines[curpos]
|
|
277
|
+
if p
|
|
278
|
+
ComposeMode.spawn_nicely :to_default => p
|
|
279
|
+
else
|
|
280
|
+
ComposeMode.spawn_nicely
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def edit_labels
|
|
285
|
+
old_labels = @thread.labels
|
|
286
|
+
reserved_labels = old_labels.select { |l| LabelManager::RESERVED_LABELS.include? l }
|
|
287
|
+
new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels.sort_by {|x| x.to_s}
|
|
288
|
+
|
|
289
|
+
return unless new_labels
|
|
290
|
+
@thread.labels = Set.new(reserved_labels) + new_labels
|
|
291
|
+
new_labels.each { |l| LabelManager << l }
|
|
292
|
+
update
|
|
293
|
+
UpdateManager.relay self, :labeled, @thread.first
|
|
294
|
+
Index.save_thread @thread
|
|
295
|
+
UndoManager.register "labeling thread" do
|
|
296
|
+
@thread.labels = old_labels
|
|
297
|
+
Index.save_thread @thread
|
|
298
|
+
UpdateManager.relay self, :labeled, @thread.first
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def toggle_starred
|
|
303
|
+
m = @message_lines[curpos] or return
|
|
304
|
+
toggle_label m, :starred
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def toggle_new
|
|
308
|
+
m = @message_lines[curpos] or return
|
|
309
|
+
toggle_label m, :unread
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def toggle_label m, label
|
|
313
|
+
if m.has_label? label
|
|
314
|
+
m.remove_label label
|
|
315
|
+
else
|
|
316
|
+
m.add_label label
|
|
317
|
+
end
|
|
318
|
+
## TODO: don't recalculate EVERYTHING just to add a stupid little
|
|
319
|
+
## star to the display
|
|
320
|
+
update
|
|
321
|
+
UpdateManager.relay self, :single_message_labeled, m
|
|
322
|
+
Index.save_thread @thread
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
## called when someone presses enter when the cursor is highlighting
|
|
326
|
+
## a chunk. for expandable chunks (including messages) we toggle
|
|
327
|
+
## open/closed state; for viewable chunks (like attachments) we
|
|
328
|
+
## view.
|
|
329
|
+
def activate_chunk
|
|
330
|
+
chunk = @chunk_lines[curpos] or return
|
|
331
|
+
if chunk.is_a? Chunk::Text
|
|
332
|
+
## if the cursor is over a text region, expand/collapse the
|
|
333
|
+
## entire message
|
|
334
|
+
chunk = @message_lines[curpos]
|
|
335
|
+
end
|
|
336
|
+
layout = if chunk.is_a?(Message)
|
|
337
|
+
@layout[chunk]
|
|
338
|
+
elsif chunk.expandable?
|
|
339
|
+
@chunk_layout[chunk]
|
|
340
|
+
end
|
|
341
|
+
if layout
|
|
342
|
+
layout.state = (layout.state != :closed ? :closed : :open)
|
|
343
|
+
#cursor_down if layout.state == :closed # too annoying
|
|
344
|
+
update
|
|
345
|
+
elsif chunk.viewable?
|
|
346
|
+
view chunk
|
|
347
|
+
end
|
|
348
|
+
if chunk.is_a?(Message) && $config[:jump_to_open_message]
|
|
349
|
+
jump_to_message chunk
|
|
350
|
+
jump_to_next_open if layout.state == :closed
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def edit_as_new
|
|
355
|
+
m = @message_lines[curpos] or return
|
|
356
|
+
mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc, :refs => m.refs, :replytos => m.replytos)
|
|
357
|
+
BufferManager.spawn "edit as new", mode
|
|
358
|
+
mode.default_edit_message
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def save_to_disk
|
|
362
|
+
chunk = @chunk_lines[curpos] or return
|
|
363
|
+
case chunk
|
|
364
|
+
when Chunk::Attachment
|
|
365
|
+
default_dir = $config[:default_attachment_save_dir]
|
|
366
|
+
default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
|
|
367
|
+
default_fn = File.expand_path File.join(default_dir, chunk.filename)
|
|
368
|
+
fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
|
|
369
|
+
|
|
370
|
+
# if user selects directory use file name from message
|
|
371
|
+
if fn and File.directory? fn
|
|
372
|
+
fn = File.join(fn, chunk.filename)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
save_to_file(fn) { |f| f.print chunk.raw_content } if fn
|
|
376
|
+
else
|
|
377
|
+
m = @message_lines[curpos]
|
|
378
|
+
fn = BufferManager.ask_for_filename :filename, "Save message to file: "
|
|
379
|
+
return unless fn
|
|
380
|
+
save_to_file(fn) do |f|
|
|
381
|
+
m.each_raw_message_line { |l| f.print l }
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def save_all_to_disk
|
|
387
|
+
m = @message_lines[curpos] or return
|
|
388
|
+
default_dir = ($config[:default_attachment_save_dir] || ".")
|
|
389
|
+
folder = BufferManager.ask_for_filename :filename, "Save all attachments to folder: ", default_dir, true
|
|
390
|
+
return unless folder
|
|
391
|
+
|
|
392
|
+
num = 0
|
|
393
|
+
num_errors = 0
|
|
394
|
+
m.chunks.each do |chunk|
|
|
395
|
+
next unless chunk.is_a?(Chunk::Attachment)
|
|
396
|
+
fn = File.join(folder, chunk.filename)
|
|
397
|
+
num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
|
|
398
|
+
num += 1
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
if num == 0
|
|
402
|
+
BufferManager.flash "Didn't find any attachments!"
|
|
403
|
+
else
|
|
404
|
+
if num_errors == 0
|
|
405
|
+
BufferManager.flash "Wrote #{num.pluralize 'attachment'} to #{folder}."
|
|
406
|
+
else
|
|
407
|
+
BufferManager.flash "Wrote #{(num - num_errors).pluralize 'attachment'} to #{folder}; couldn't write #{num_errors} of them (see log)."
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def publish
|
|
413
|
+
chunk = @chunk_lines[curpos] or return
|
|
414
|
+
if HookManager.enabled? "publish"
|
|
415
|
+
HookManager.run "publish", :chunk => chunk
|
|
416
|
+
else
|
|
417
|
+
BufferManager.flash "Publishing hook not defined."
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def edit_draft
|
|
422
|
+
m = @message_lines[curpos] or return
|
|
423
|
+
if m.is_draft?
|
|
424
|
+
mode = ResumeMode.new m
|
|
425
|
+
BufferManager.spawn "Edit message", mode
|
|
426
|
+
BufferManager.kill_buffer self.buffer
|
|
427
|
+
mode.default_edit_message
|
|
428
|
+
else
|
|
429
|
+
BufferManager.flash "Not a draft message!"
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def send_draft
|
|
434
|
+
m = @message_lines[curpos] or return
|
|
435
|
+
if m.is_draft?
|
|
436
|
+
mode = ResumeMode.new m
|
|
437
|
+
BufferManager.spawn "Send message", mode
|
|
438
|
+
BufferManager.kill_buffer self.buffer
|
|
439
|
+
mode.send_message
|
|
440
|
+
else
|
|
441
|
+
BufferManager.flash "Not a draft message!"
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def jump_to_first_open
|
|
446
|
+
m = @message_lines[0] or return
|
|
447
|
+
if @layout[m].state != :closed
|
|
448
|
+
jump_to_message m#, true
|
|
449
|
+
else
|
|
450
|
+
jump_to_next_open #true
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def jump_to_next_and_open
|
|
455
|
+
return continue_search_in_buffer if in_search? # err.. don't know why im doing this
|
|
456
|
+
|
|
457
|
+
m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
|
|
458
|
+
return unless m
|
|
459
|
+
|
|
460
|
+
nextm = @layout[m].next
|
|
461
|
+
return unless nextm
|
|
462
|
+
|
|
463
|
+
if @layout[m].toggled_state == true
|
|
464
|
+
@layout[m].state = :closed
|
|
465
|
+
@layout[m].toggled_state = false
|
|
466
|
+
update
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
if @layout[nextm].state == :closed
|
|
470
|
+
@layout[nextm].state = :open
|
|
471
|
+
@layout[nextm].toggled_state = true
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
jump_to_message nextm if nextm
|
|
475
|
+
|
|
476
|
+
update if @layout[nextm].toggled_state
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def jump_to_next_open force_alignment=nil
|
|
480
|
+
return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations
|
|
481
|
+
m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
|
|
482
|
+
return unless m
|
|
483
|
+
while nextm = @layout[m].next
|
|
484
|
+
break if @layout[nextm].state != :closed
|
|
485
|
+
m = nextm
|
|
486
|
+
end
|
|
487
|
+
jump_to_message nextm, force_alignment if nextm
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def align_current_message
|
|
491
|
+
m = @message_lines[curpos] or return
|
|
492
|
+
jump_to_message m, true
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def jump_to_prev_and_open force_alignment=nil
|
|
496
|
+
m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] }
|
|
497
|
+
return unless m
|
|
498
|
+
|
|
499
|
+
nextm = @layout[m].prev
|
|
500
|
+
return unless nextm
|
|
501
|
+
|
|
502
|
+
if @layout[m].toggled_state == true
|
|
503
|
+
@layout[m].state = :closed
|
|
504
|
+
@layout[m].toggled_state = false
|
|
505
|
+
update
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
if @layout[nextm].state == :closed
|
|
509
|
+
@layout[nextm].state = :open
|
|
510
|
+
@layout[nextm].toggled_state = true
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
jump_to_message nextm if nextm
|
|
514
|
+
update if @layout[nextm].toggled_state
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def jump_to_prev_open
|
|
518
|
+
m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] } # bah, .to_a
|
|
519
|
+
return unless m
|
|
520
|
+
## jump to the top of the current message if we're in the body;
|
|
521
|
+
## otherwise, to the previous message
|
|
522
|
+
|
|
523
|
+
top = @layout[m].top
|
|
524
|
+
if curpos == top
|
|
525
|
+
while(prevm = @layout[m].prev)
|
|
526
|
+
break if @layout[prevm].state != :closed
|
|
527
|
+
m = prevm
|
|
528
|
+
end
|
|
529
|
+
jump_to_message prevm if prevm
|
|
530
|
+
else
|
|
531
|
+
jump_to_message m
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def jump_to_message m, force_alignment=false
|
|
536
|
+
l = @layout[m]
|
|
537
|
+
|
|
538
|
+
## boundaries of the message
|
|
539
|
+
message_left = l.depth * INDENT_SPACES
|
|
540
|
+
message_right = message_left + l.width
|
|
541
|
+
|
|
542
|
+
## calculate leftmost colum
|
|
543
|
+
left = if force_alignment # force mode: align exactly
|
|
544
|
+
message_left
|
|
545
|
+
else # regular: minimize cursor movement
|
|
546
|
+
## leftmost and rightmost are boundaries of all valid left-column
|
|
547
|
+
## alignments.
|
|
548
|
+
leftmost = [message_left, message_right - buffer.content_width + 1].min
|
|
549
|
+
rightmost = message_left
|
|
550
|
+
leftcol.clamp(leftmost, rightmost)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
jump_to_line l.top # move vertically
|
|
554
|
+
jump_to_col left # move horizontally
|
|
555
|
+
set_cursor_pos l.top # set cursor pos
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def expand_all_messages
|
|
559
|
+
@global_message_state ||= :closed
|
|
560
|
+
@global_message_state = (@global_message_state == :closed ? :open : :closed)
|
|
561
|
+
@layout.each { |m, l| l.state = @global_message_state }
|
|
562
|
+
update
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def collapse_non_new_messages
|
|
566
|
+
@layout.each { |m, l| l.state = l.orig_new ? :open : :closed }
|
|
567
|
+
update
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def expand_all_quotes
|
|
571
|
+
if(m = @message_lines[curpos])
|
|
572
|
+
quotes = m.chunks.select { |c| (c.is_a?(Chunk::Quote) || c.is_a?(Chunk::Signature)) && c.lines.length > 1 }
|
|
573
|
+
numopen = quotes.inject(0) { |s, c| s + (@chunk_layout[c].state == :open ? 1 : 0) }
|
|
574
|
+
newstate = numopen > quotes.length / 2 ? :closed : :open
|
|
575
|
+
quotes.each { |c| @chunk_layout[c].state = newstate }
|
|
576
|
+
update
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def cleanup
|
|
581
|
+
@layout = @chunk_layout = @text = nil # for good luck
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def archive_and_kill; archive_and_then :kill end
|
|
585
|
+
def spam_and_kill; spam_and_then :kill end
|
|
586
|
+
def delete_and_kill; delete_and_then :kill end
|
|
587
|
+
def kill_and_kill; kill_and_then :kill end
|
|
588
|
+
def unread_and_kill; unread_and_then :kill end
|
|
589
|
+
def do_nothing_and_kill; do_nothing_and_then :kill end
|
|
590
|
+
|
|
591
|
+
def archive_and_next; archive_and_then :next end
|
|
592
|
+
def spam_and_next; spam_and_then :next end
|
|
593
|
+
def delete_and_next; delete_and_then :next end
|
|
594
|
+
def kill_and_next; kill_and_then :next end
|
|
595
|
+
def unread_and_next; unread_and_then :next end
|
|
596
|
+
def do_nothing_and_next; do_nothing_and_then :next end
|
|
597
|
+
|
|
598
|
+
def archive_and_prev; archive_and_then :prev end
|
|
599
|
+
def spam_and_prev; spam_and_then :prev end
|
|
600
|
+
def delete_and_prev; delete_and_then :prev end
|
|
601
|
+
def kill_and_prev; kill_and_then :prev end
|
|
602
|
+
def unread_and_prev; unread_and_then :prev end
|
|
603
|
+
def do_nothing_and_prev; do_nothing_and_then :prev end
|
|
604
|
+
|
|
605
|
+
def archive_and_then op
|
|
606
|
+
dispatch op do
|
|
607
|
+
@thread.remove_label :inbox
|
|
608
|
+
UpdateManager.relay self, :archived, @thread.first
|
|
609
|
+
Index.save_thread @thread
|
|
610
|
+
UndoManager.register "archiving 1 thread" do
|
|
611
|
+
@thread.apply_label :inbox
|
|
612
|
+
Index.save_thread @thread
|
|
613
|
+
UpdateManager.relay self, :unarchived, @thread.first
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def spam_and_then op
|
|
619
|
+
dispatch op do
|
|
620
|
+
@thread.apply_label :spam
|
|
621
|
+
UpdateManager.relay self, :spammed, @thread.first
|
|
622
|
+
Index.save_thread @thread
|
|
623
|
+
UndoManager.register "marking 1 thread as spam" do
|
|
624
|
+
@thread.remove_label :spam
|
|
625
|
+
Index.save_thread @thread
|
|
626
|
+
UpdateManager.relay self, :unspammed, @thread.first
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def delete_and_then op
|
|
632
|
+
dispatch op do
|
|
633
|
+
@thread.apply_label :deleted
|
|
634
|
+
UpdateManager.relay self, :deleted, @thread.first
|
|
635
|
+
Index.save_thread @thread
|
|
636
|
+
UndoManager.register "deleting 1 thread" do
|
|
637
|
+
@thread.remove_label :deleted
|
|
638
|
+
Index.save_thread @thread
|
|
639
|
+
UpdateManager.relay self, :undeleted, @thread.first
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def kill_and_then op
|
|
645
|
+
dispatch op do
|
|
646
|
+
@thread.apply_label :killed
|
|
647
|
+
UpdateManager.relay self, :killed, @thread.first
|
|
648
|
+
Index.save_thread @thread
|
|
649
|
+
UndoManager.register "killed 1 thread" do
|
|
650
|
+
@thread.remove_label :killed
|
|
651
|
+
Index.save_thread @thread
|
|
652
|
+
UpdateManager.relay self, :unkilled, @thread.first
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def unread_and_then op
|
|
658
|
+
dispatch op do
|
|
659
|
+
@thread.apply_label :unread
|
|
660
|
+
UpdateManager.relay self, :unread, @thread.first
|
|
661
|
+
Index.save_thread @thread
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def do_nothing_and_then op
|
|
666
|
+
dispatch op
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def dispatch op
|
|
670
|
+
return if @dying
|
|
671
|
+
@dying = true
|
|
672
|
+
|
|
673
|
+
l = lambda do
|
|
674
|
+
yield if block_given?
|
|
675
|
+
BufferManager.kill_buffer_safely buffer
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
case op
|
|
679
|
+
when :next
|
|
680
|
+
@index_mode.launch_next_thread_after @thread, &l
|
|
681
|
+
when :prev
|
|
682
|
+
@index_mode.launch_prev_thread_before @thread, &l
|
|
683
|
+
when :kill
|
|
684
|
+
l.call
|
|
685
|
+
else
|
|
686
|
+
raise ArgumentError, "unknown thread dispatch operation #{op.inspect}"
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
private :dispatch
|
|
690
|
+
|
|
691
|
+
def pipe_message
|
|
692
|
+
chunk = @chunk_lines[curpos]
|
|
693
|
+
chunk = nil unless chunk.is_a?(Chunk::Attachment)
|
|
694
|
+
message = @message_lines[curpos] unless chunk
|
|
695
|
+
|
|
696
|
+
return unless chunk || message
|
|
697
|
+
|
|
698
|
+
command = BufferManager.ask(:shell, "pipe command: ")
|
|
699
|
+
return if command.nil? || command.empty?
|
|
700
|
+
|
|
701
|
+
output = pipe_to_process(command) do |stream|
|
|
702
|
+
if chunk
|
|
703
|
+
stream.print chunk.raw_content
|
|
704
|
+
else
|
|
705
|
+
message.each_raw_message_line { |l| stream.print l }
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
if output
|
|
710
|
+
BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
|
|
711
|
+
else
|
|
712
|
+
BufferManager.flash "'#{command}' done!"
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def status
|
|
718
|
+
user_labels = @thread.labels.to_a.map do |l|
|
|
719
|
+
l.to_s if LabelManager.user_defined_labels.member?(l)
|
|
720
|
+
end.compact.join(",")
|
|
721
|
+
user_labels = (user_labels.empty? and "" or "<#{user_labels}>")
|
|
722
|
+
[user_labels, super].join(" -- ")
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
private
|
|
726
|
+
|
|
727
|
+
def initial_state_for m
|
|
728
|
+
if m.has_label?(:starred) || m.has_label?(:unread)
|
|
729
|
+
:open
|
|
730
|
+
else
|
|
731
|
+
:closed
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def update
|
|
736
|
+
regen_text
|
|
737
|
+
buffer.mark_dirty if buffer
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
## here we generate the actual content lines. we accumulate
|
|
741
|
+
## everything into @text, and we set @chunk_lines and
|
|
742
|
+
## @message_lines, and we update @layout.
|
|
743
|
+
def regen_text
|
|
744
|
+
@text = []
|
|
745
|
+
@chunk_lines = []
|
|
746
|
+
@message_lines = []
|
|
747
|
+
@person_lines = []
|
|
748
|
+
|
|
749
|
+
prevm = nil
|
|
750
|
+
@thread.each do |m, depth, parent|
|
|
751
|
+
unless m.is_a? Message # handle nil and :fake_root
|
|
752
|
+
@text += chunk_to_lines m, nil, @text.length, depth, parent
|
|
753
|
+
next
|
|
754
|
+
end
|
|
755
|
+
l = @layout[m]
|
|
756
|
+
|
|
757
|
+
## is this still necessary?
|
|
758
|
+
next unless @layout[m].state # skip discarded drafts
|
|
759
|
+
|
|
760
|
+
## build the patina
|
|
761
|
+
text = chunk_to_lines m, l.state, @text.length, depth, parent, l.color, l.star_color
|
|
762
|
+
|
|
763
|
+
l.top = @text.length
|
|
764
|
+
l.bot = @text.length + text.length # updated below
|
|
765
|
+
l.prev = prevm
|
|
766
|
+
l.next = nil
|
|
767
|
+
l.depth = depth
|
|
768
|
+
# l.state we preserve
|
|
769
|
+
l.width = 0 # updated below
|
|
770
|
+
@layout[l.prev].next = m if l.prev
|
|
771
|
+
|
|
772
|
+
(0 ... text.length).each do |i|
|
|
773
|
+
@chunk_lines[@text.length + i] = m
|
|
774
|
+
@message_lines[@text.length + i] = m
|
|
775
|
+
lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
@text += text
|
|
779
|
+
prevm = m
|
|
780
|
+
if l.state != :closed
|
|
781
|
+
m.chunks.each do |c|
|
|
782
|
+
cl = @chunk_layout[c]
|
|
783
|
+
|
|
784
|
+
## set the default state for chunks
|
|
785
|
+
cl.state ||=
|
|
786
|
+
if c.expandable? && c.respond_to?(:initial_state)
|
|
787
|
+
c.initial_state
|
|
788
|
+
else
|
|
789
|
+
:closed
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
text = chunk_to_lines c, cl.state, @text.length, depth
|
|
793
|
+
(0 ... text.length).each do |i|
|
|
794
|
+
@chunk_lines[@text.length + i] = c
|
|
795
|
+
@message_lines[@text.length + i] = m
|
|
796
|
+
lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * INDENT_SPACES)
|
|
797
|
+
l.width = lw if lw > l.width
|
|
798
|
+
end
|
|
799
|
+
@text += text
|
|
800
|
+
end
|
|
801
|
+
@layout[m].bot = @text.length
|
|
802
|
+
end
|
|
803
|
+
end
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def message_patina_lines m, state, start, parent, prefix, color, star_color
|
|
807
|
+
prefix_widget = [color, prefix]
|
|
808
|
+
|
|
809
|
+
open_widget = [color, (state == :closed ? "+ " : "- ")]
|
|
810
|
+
new_widget = [color, (m.has_label?(:unread) ? "N" : " ")]
|
|
811
|
+
starred_widget = if m.has_label?(:starred)
|
|
812
|
+
[star_color, "*"]
|
|
813
|
+
else
|
|
814
|
+
[color, " "]
|
|
815
|
+
end
|
|
816
|
+
attach_widget = [color, (m.has_label?(:attachment) ? "@" : " ")]
|
|
817
|
+
|
|
818
|
+
case state
|
|
819
|
+
when :open
|
|
820
|
+
@person_lines[start] = m.from
|
|
821
|
+
[[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
|
|
822
|
+
[color,
|
|
823
|
+
"#{m.from ? m.from.mediumname.fix_encoding! : '?'} to #{m.recipients.map { |l| l.shortname.fix_encoding! }.join(', ')} #{m.date.to_nice_s.fix_encoding!} (#{m.date.to_nice_distance_s.fix_encoding!})"]]]
|
|
824
|
+
|
|
825
|
+
when :closed
|
|
826
|
+
@person_lines[start] = m.from
|
|
827
|
+
[[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
|
|
828
|
+
[color,
|
|
829
|
+
"#{m.from ? m.from.mediumname.fix_encoding! : '?'}, #{m.date.to_nice_s.fix_encoding!} (#{m.date.to_nice_distance_s.fix_encoding!}) #{m.snippet ? m.snippet.fix_encoding! : ''}"]]]
|
|
830
|
+
|
|
831
|
+
when :detailed
|
|
832
|
+
@person_lines[start] = m.from
|
|
833
|
+
from_line = [[prefix_widget, open_widget, new_widget, attach_widget, starred_widget,
|
|
834
|
+
[color, "From: #{m.from ? format_person(m.from) : '?'}"]]]
|
|
835
|
+
|
|
836
|
+
addressee_lines = []
|
|
837
|
+
unless m.to.empty?
|
|
838
|
+
m.to.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
|
|
839
|
+
addressee_lines += format_person_list " To: ", m.to
|
|
840
|
+
end
|
|
841
|
+
unless m.cc.empty?
|
|
842
|
+
m.cc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
|
|
843
|
+
addressee_lines += format_person_list " Cc: ", m.cc
|
|
844
|
+
end
|
|
845
|
+
unless m.bcc.empty?
|
|
846
|
+
m.bcc.each_with_index { |p, i| @person_lines[start + addressee_lines.length + from_line.length + i] = p }
|
|
847
|
+
addressee_lines += format_person_list " Bcc: ", m.bcc
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
headers = OrderedHash.new
|
|
851
|
+
headers["Date"] = "#{m.date.to_message_nice_s} (#{m.date.to_nice_distance_s})"
|
|
852
|
+
headers["Subject"] = m.subj
|
|
853
|
+
|
|
854
|
+
show_labels = @thread.labels - LabelManager::HIDDEN_RESERVED_LABELS
|
|
855
|
+
unless show_labels.empty?
|
|
856
|
+
headers["Labels"] = show_labels.map { |x| x.to_s }.sort.join(', ')
|
|
857
|
+
end
|
|
858
|
+
if parent
|
|
859
|
+
headers["In reply to"] = "#{parent.from.mediumname}'s message of #{parent.date.to_message_nice_s}"
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
HookManager.run "detailed-headers", :message => m, :headers => headers
|
|
863
|
+
|
|
864
|
+
from_line + (addressee_lines + headers.map { |k, v| " #{k}: #{v}" }).map { |l| [[color, prefix + " " + l]] }
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def format_person_list prefix, people
|
|
869
|
+
ptext = people.map { |p| format_person p }
|
|
870
|
+
pad = " " * prefix.display_length
|
|
871
|
+
[prefix + ptext.first + (ptext.length > 1 ? "," : "")] +
|
|
872
|
+
ptext[1 .. -1].map_with_index do |e, i|
|
|
873
|
+
pad + e + (i == ptext.length - 1 ? "" : ",")
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
def format_person p
|
|
878
|
+
p.longname + (ContactManager.is_aliased_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
def maybe_wrap_text lines
|
|
882
|
+
if @wrap
|
|
883
|
+
config_width = $config[:wrap_width]
|
|
884
|
+
if config_width and config_width != 0
|
|
885
|
+
width = [config_width, buffer.content_width].min
|
|
886
|
+
else
|
|
887
|
+
width = buffer.content_width
|
|
888
|
+
end
|
|
889
|
+
# lines can apparently be both String and Array, convert to Array for map.
|
|
890
|
+
if lines.kind_of? String
|
|
891
|
+
lines = lines.lines.to_a
|
|
892
|
+
end
|
|
893
|
+
lines = lines.map { |l| l.chomp.wrap width if l }.flatten
|
|
894
|
+
end
|
|
895
|
+
return lines
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
## todo: check arguments on this overly complex function
|
|
899
|
+
def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
|
|
900
|
+
prefix = " " * INDENT_SPACES * depth
|
|
901
|
+
case chunk
|
|
902
|
+
when :fake_root
|
|
903
|
+
[[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]
|
|
904
|
+
when nil
|
|
905
|
+
[[[:missing_message_color, "#{prefix}<an unreceived message>"]]]
|
|
906
|
+
when Message
|
|
907
|
+
message_patina_lines(chunk, state, start, parent, prefix, color, star_color) +
|
|
908
|
+
(chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. Hit 'e' to edit, 'y' to send. <<<"]]] : [])
|
|
909
|
+
|
|
910
|
+
else
|
|
911
|
+
raise "Bad chunk: #{chunk.inspect}" unless chunk.respond_to?(:inlineable?) ## debugging
|
|
912
|
+
if chunk.inlineable?
|
|
913
|
+
lines = maybe_wrap_text(chunk.lines)
|
|
914
|
+
lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
|
|
915
|
+
elsif chunk.expandable?
|
|
916
|
+
case state
|
|
917
|
+
when :closed
|
|
918
|
+
[[[chunk.patina_color, "#{prefix}+ #{chunk.patina_text}"]]]
|
|
919
|
+
when :open
|
|
920
|
+
lines = maybe_wrap_text(chunk.lines)
|
|
921
|
+
[[[chunk.patina_color, "#{prefix}- #{chunk.patina_text}"]]] + lines.map { |line| [[chunk.color, "#{prefix}#{line}"]] }
|
|
922
|
+
end
|
|
923
|
+
else
|
|
924
|
+
[[[chunk.patina_color, "#{prefix}x #{chunk.patina_text}"]]]
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
def view chunk
|
|
930
|
+
BufferManager.flash "viewing #{chunk.content_type} attachment..."
|
|
931
|
+
success = chunk.view!
|
|
932
|
+
BufferManager.erase_flash
|
|
933
|
+
BufferManager.completely_redraw_screen
|
|
934
|
+
unless success
|
|
935
|
+
BufferManager.spawn "Attachment: #{chunk.filename}", TextMode.new(chunk.to_s.ascii, chunk.filename)
|
|
936
|
+
BufferManager.flash "Couldn't execute view command, viewing as text."
|
|
937
|
+
end
|
|
938
|
+
end
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
end
|