sup 0.0.8 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sup might be problematic. Click here for more details.
- data/HACKING +6 -36
- data/History.txt +11 -0
- data/Manifest.txt +5 -0
- data/README.txt +13 -31
- data/Rakefile +3 -3
- data/bin/sup +167 -89
- data/bin/sup-add +39 -29
- data/bin/sup-config +57 -31
- data/bin/sup-sync +60 -54
- data/bin/sup-sync-back +143 -0
- data/doc/FAQ.txt +56 -19
- data/doc/Philosophy.txt +34 -33
- data/doc/TODO +76 -46
- data/doc/UserGuide.txt +142 -122
- data/lib/sup.rb +76 -36
- data/lib/sup/account.rb +27 -19
- data/lib/sup/buffer.rb +130 -44
- data/lib/sup/contact.rb +1 -1
- data/lib/sup/draft.rb +1 -2
- data/lib/sup/imap.rb +64 -19
- data/lib/sup/index.rb +95 -16
- data/lib/sup/keymap.rb +1 -1
- data/lib/sup/label.rb +31 -5
- data/lib/sup/maildir.rb +7 -5
- data/lib/sup/mbox.rb +34 -15
- data/lib/sup/mbox/loader.rb +30 -12
- data/lib/sup/mbox/ssh-loader.rb +7 -5
- data/lib/sup/message.rb +93 -44
- data/lib/sup/modes/buffer-list-mode.rb +1 -1
- data/lib/sup/modes/completion-mode.rb +55 -0
- data/lib/sup/modes/compose-mode.rb +6 -25
- data/lib/sup/modes/contact-list-mode.rb +1 -1
- data/lib/sup/modes/edit-message-mode.rb +119 -29
- data/lib/sup/modes/file-browser-mode.rb +108 -0
- data/lib/sup/modes/forward-mode.rb +3 -20
- data/lib/sup/modes/inbox-mode.rb +9 -12
- data/lib/sup/modes/label-list-mode.rb +28 -46
- data/lib/sup/modes/label-search-results-mode.rb +1 -16
- data/lib/sup/modes/line-cursor-mode.rb +44 -5
- data/lib/sup/modes/person-search-results-mode.rb +1 -16
- data/lib/sup/modes/reply-mode.rb +18 -31
- data/lib/sup/modes/resume-mode.rb +6 -6
- data/lib/sup/modes/scroll-mode.rb +6 -5
- data/lib/sup/modes/search-results-mode.rb +6 -17
- data/lib/sup/modes/thread-index-mode.rb +70 -28
- data/lib/sup/modes/thread-view-mode.rb +65 -29
- data/lib/sup/person.rb +71 -30
- data/lib/sup/poll.rb +13 -4
- data/lib/sup/rfc2047.rb +61 -0
- data/lib/sup/sent.rb +7 -5
- data/lib/sup/source.rb +12 -9
- data/lib/sup/suicide.rb +36 -0
- data/lib/sup/tagger.rb +6 -6
- data/lib/sup/textfield.rb +76 -14
- data/lib/sup/thread.rb +97 -123
- data/lib/sup/util.rb +167 -1
- metadata +30 -5
@@ -23,7 +23,7 @@ protected
|
|
23
23
|
|
24
24
|
def regen_text
|
25
25
|
@bufs = BufferManager.buffers.sort_by { |name, buf| name }
|
26
|
-
width = @bufs.
|
26
|
+
width = @bufs.max_of { |name, buf| buf.mode.name.length }
|
27
27
|
@text = @bufs.map do |name, buf|
|
28
28
|
sprintf "%#{width}s %s", buf.mode.name, name
|
29
29
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class CompletionMode < ScrollMode
|
4
|
+
INTERSTITIAL = " "
|
5
|
+
|
6
|
+
def initialize list, opts={}
|
7
|
+
@list = list
|
8
|
+
@header = opts[:header]
|
9
|
+
@prefix_len = opts[:prefix_len]
|
10
|
+
@lines = nil
|
11
|
+
super :slip_rows => 1, :twiddles => false
|
12
|
+
end
|
13
|
+
|
14
|
+
def lines
|
15
|
+
update_lines unless @lines
|
16
|
+
@lines.length
|
17
|
+
end
|
18
|
+
|
19
|
+
def [] i
|
20
|
+
update_lines unless @lines
|
21
|
+
@lines[i]
|
22
|
+
end
|
23
|
+
|
24
|
+
def roll; if at_bottom? then jump_to_start else page_down end end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def update_lines
|
29
|
+
width = buffer.content_width
|
30
|
+
max_length = @list.max_of { |s| s.length }
|
31
|
+
num_per = buffer.content_width / (max_length + INTERSTITIAL.length)
|
32
|
+
@lines = [@header].compact
|
33
|
+
@list.each_with_index do |s, i|
|
34
|
+
if @prefix_len
|
35
|
+
@lines << [] if i % num_per == 0
|
36
|
+
if @prefix_len < s.length
|
37
|
+
prefix = s[0 ... @prefix_len]
|
38
|
+
suffix = s[(@prefix_len + 1) .. -1]
|
39
|
+
char = s[@prefix_len].chr
|
40
|
+
|
41
|
+
@lines.last += [[:none, sprintf("%#{max_length - suffix.length - 1}s", prefix)],
|
42
|
+
[:completion_character_color, char],
|
43
|
+
[:none, suffix + INTERSTITIAL]]
|
44
|
+
else
|
45
|
+
@lines.last += [[:none, sprintf("%#{max_length}s#{INTERSTITIAL}", s)]]
|
46
|
+
end
|
47
|
+
else
|
48
|
+
@lines << "" if i % num_per == 0
|
49
|
+
@lines.last += sprintf "%#{max_length}s#{INTERSTITIAL}", s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -1,36 +1,17 @@
|
|
1
1
|
module Redwood
|
2
2
|
|
3
3
|
class ComposeMode < EditMessageMode
|
4
|
-
attr_reader :body, :header
|
5
|
-
|
6
4
|
def initialize opts={}
|
7
|
-
|
8
|
-
@header = {
|
5
|
+
header = {
|
9
6
|
"From" => AccountManager.default_account.full_address,
|
10
|
-
"Message-Id" => gen_message_id,
|
11
7
|
}
|
12
8
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@body = (opts[:body] || []) + sig_lines
|
19
|
-
regen_text
|
20
|
-
end
|
21
|
-
|
22
|
-
def lines; @text.length; end
|
23
|
-
def [] i; @text[i]; end
|
24
|
-
|
25
|
-
protected
|
26
|
-
|
27
|
-
def handle_new_text new_header, new_body
|
28
|
-
@header = new_header
|
29
|
-
@body = new_body
|
30
|
-
end
|
9
|
+
header["To"] = opts[:to].map { |p| p.full_address }.join(", ") if opts[:to]
|
10
|
+
header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
|
11
|
+
header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
|
12
|
+
header["Subject"] = opts[:subj] if opts[:subj]
|
31
13
|
|
32
|
-
|
33
|
-
@text = header_lines(@header - EditMessageMode::NON_EDITABLE_HEADERS) + [""] + @body
|
14
|
+
super :header => header, :body => (opts[:body] || [])
|
34
15
|
end
|
35
16
|
end
|
36
17
|
|
@@ -1,33 +1,53 @@
|
|
1
1
|
require 'tempfile'
|
2
2
|
require 'socket' # just for gethostname!
|
3
|
+
require 'pathname'
|
4
|
+
require 'rmail'
|
3
5
|
|
4
6
|
module Redwood
|
5
7
|
|
8
|
+
class SendmailCommandFailed < StandardError; end
|
9
|
+
|
6
10
|
class EditMessageMode < LineCursorMode
|
7
11
|
FORCE_HEADERS = %w(From To Cc Bcc Subject)
|
8
12
|
MULTI_HEADERS = %w(To Cc Bcc)
|
9
13
|
NON_EDITABLE_HEADERS = %w(Message-Id Date)
|
10
14
|
|
11
15
|
attr_reader :status
|
16
|
+
attr_accessor :body, :header
|
12
17
|
bool_reader :edited
|
13
18
|
|
14
19
|
register_keymap do |k|
|
15
20
|
k.add :send_message, "Send message", 'y'
|
16
21
|
k.add :edit, "Edit message", 'e', :enter
|
17
22
|
k.add :save_as_draft, "Save as draft", 'P'
|
23
|
+
k.add :attach_file, "Attach a file", 'a'
|
24
|
+
k.add :delete_attachment, "Delete an attachment", 'd'
|
18
25
|
end
|
19
26
|
|
20
|
-
def initialize
|
21
|
-
|
27
|
+
def initialize opts={}
|
28
|
+
@header = opts.delete(:header) || {}
|
29
|
+
@body = opts.delete(:body) || []
|
30
|
+
@body += sig_lines if $config[:edit_signature]
|
22
31
|
@attachments = []
|
32
|
+
@attachment_lines = {}
|
33
|
+
@message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
|
23
34
|
@edited = false
|
35
|
+
|
36
|
+
super opts
|
37
|
+
regen_text
|
24
38
|
end
|
25
39
|
|
40
|
+
def lines; @text.length end
|
41
|
+
def [] i; @text[i] end
|
42
|
+
|
43
|
+
## a hook
|
44
|
+
def handle_new_text header, body; end
|
45
|
+
|
26
46
|
def edit
|
27
47
|
@file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
|
28
|
-
@file.puts header_lines(header - NON_EDITABLE_HEADERS)
|
48
|
+
@file.puts header_lines(@header - NON_EDITABLE_HEADERS)
|
29
49
|
@file.puts
|
30
|
-
@file.puts body
|
50
|
+
@file.puts @body
|
31
51
|
@file.close
|
32
52
|
|
33
53
|
editor = $config[:editor] || ENV['EDITOR'] || "/usr/bin/vi"
|
@@ -36,9 +56,11 @@ class EditMessageMode < LineCursorMode
|
|
36
56
|
BufferManager.shell_out "#{editor} #{@file.path}"
|
37
57
|
@edited = true if File.mtime(@file.path) > mtime
|
38
58
|
|
39
|
-
|
40
|
-
|
41
|
-
|
59
|
+
BufferManager.kill_buffer self.buffer unless @edited
|
60
|
+
|
61
|
+
header, @body = parse_file @file.path
|
62
|
+
@header = header - NON_EDITABLE_HEADERS
|
63
|
+
handle_new_text @header, @body
|
42
64
|
update
|
43
65
|
end
|
44
66
|
|
@@ -46,15 +68,37 @@ class EditMessageMode < LineCursorMode
|
|
46
68
|
!edited? || BufferManager.ask_yes_or_no("Discard message?")
|
47
69
|
end
|
48
70
|
|
49
|
-
|
71
|
+
def attach_file
|
72
|
+
fn = BufferManager.ask_for_filenames :attachment, "File name (enter for browser): "
|
73
|
+
fn.each { |f| @attachments << Pathname.new(f) }
|
74
|
+
update
|
75
|
+
end
|
50
76
|
|
51
|
-
def
|
52
|
-
|
77
|
+
def delete_attachment
|
78
|
+
i = curpos - @attachment_lines_offset
|
79
|
+
if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachments[i]}?")
|
80
|
+
@attachments.delete_at i
|
81
|
+
update
|
82
|
+
end
|
53
83
|
end
|
54
84
|
|
85
|
+
protected
|
86
|
+
|
55
87
|
def update
|
56
88
|
regen_text
|
57
|
-
buffer.mark_dirty
|
89
|
+
buffer.mark_dirty if buffer
|
90
|
+
end
|
91
|
+
|
92
|
+
def regen_text
|
93
|
+
top = header_lines(@header - NON_EDITABLE_HEADERS) + [""]
|
94
|
+
@text = top + @body
|
95
|
+
@text += sig_lines unless $config[:edit_signature]
|
96
|
+
|
97
|
+
unless @attachments.empty?
|
98
|
+
@text += [""]
|
99
|
+
@attachment_lines_offset = @text.length
|
100
|
+
@text += @attachments.map { |f| [[:attachment_color, "+ Attachment: #{f} (#{f.human_size})"]] }
|
101
|
+
end
|
58
102
|
end
|
59
103
|
|
60
104
|
def parse_file fn
|
@@ -108,23 +152,27 @@ protected
|
|
108
152
|
def send_message
|
109
153
|
return unless edited? || BufferManager.ask_yes_or_no("Message unedited. Really send?")
|
110
154
|
|
111
|
-
raise "no message id!" unless header["Message-Id"]
|
112
155
|
date = Time.now
|
113
156
|
from_email =
|
114
|
-
if header["From"] =~ /<?(\S+@(\S+?))>?$/
|
157
|
+
if @header["From"] =~ /<?(\S+@(\S+?))>?$/
|
115
158
|
$1
|
116
159
|
else
|
117
160
|
AccountManager.default_account.email
|
118
161
|
end
|
119
162
|
|
120
163
|
acct = AccountManager.account_for(from_email) || AccountManager.default_account
|
121
|
-
SentManager.write_sent_message(date, from_email) { |f| write_message f, true, date }
|
122
164
|
BufferManager.flash "Sending..."
|
123
165
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
166
|
+
begin
|
167
|
+
IO.popen(acct.sendmail, "w") { |p| write_full_message_to p, date }
|
168
|
+
raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
|
169
|
+
SentManager.write_sent_message(date, from_email) { |f| write_full_message_to f, date }
|
170
|
+
BufferManager.kill_buffer buffer
|
171
|
+
BufferManager.flash "Message sent!"
|
172
|
+
rescue SystemCallError, SendmailCommandFailed => e
|
173
|
+
Redwood::log "Problem sending mail: #{e.message}"
|
174
|
+
BufferManager.flash "Problem sending mail: #{e.message}"
|
175
|
+
end
|
128
176
|
end
|
129
177
|
|
130
178
|
def save_as_draft
|
@@ -133,22 +181,49 @@ protected
|
|
133
181
|
BufferManager.flash "Saved for later editing."
|
134
182
|
end
|
135
183
|
|
136
|
-
def
|
137
|
-
|
138
|
-
|
184
|
+
def write_full_message_to f, date=Time.now
|
185
|
+
m = RMail::Message.new
|
186
|
+
@header.each do |k, v|
|
187
|
+
next if v.nil? || v.empty?
|
188
|
+
m.header[k] =
|
189
|
+
case v
|
190
|
+
when String
|
191
|
+
v
|
192
|
+
when Array
|
193
|
+
v.join ", "
|
194
|
+
end
|
195
|
+
end
|
139
196
|
|
140
|
-
|
141
|
-
|
197
|
+
m.header["Date"] = date.rfc2822
|
198
|
+
m.header["Message-Id"] = @message_id
|
199
|
+
m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
|
200
|
+
|
201
|
+
if @attachments.empty?
|
202
|
+
m.header["Content-Disposition"] = "inline"
|
203
|
+
m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
|
204
|
+
m.body = @body.join
|
205
|
+
m.body += sig_lines.join("\n") unless $config[:edit_signature]
|
142
206
|
else
|
143
|
-
|
207
|
+
body_m = RMail::Message.new
|
208
|
+
body_m.body = @body.join
|
209
|
+
body_m.body += sig_lines.join("\n") unless $config[:edit_signature]
|
210
|
+
|
211
|
+
m.add_part body_m
|
212
|
+
@attachments.each { |fn| m.add_attachment fn.to_s }
|
144
213
|
end
|
214
|
+
f.puts m.to_s
|
145
215
|
end
|
146
216
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
217
|
+
## this is going to change soon: draft messages (currently written
|
218
|
+
## with full=false) will be output as yaml.
|
219
|
+
def write_message f, full=true, date=Time.now
|
220
|
+
raise ArgumentError, "no pre-defined date: header allowed" if @header["Date"]
|
221
|
+
f.puts header_lines(@header)
|
222
|
+
f.puts <<EOS
|
223
|
+
Date: #{date.rfc2822}
|
224
|
+
Message-Id: #{@message_id}
|
225
|
+
EOS
|
226
|
+
if full
|
152
227
|
f.puts <<EOS
|
153
228
|
Mime-Version: 1.0
|
154
229
|
Content-Type: text/plain; charset=us-ascii
|
@@ -159,7 +234,22 @@ EOS
|
|
159
234
|
|
160
235
|
f.puts
|
161
236
|
f.puts @body.map { |l| l =~ /^From / ? ">#{l}" : l }
|
237
|
+
f.puts sig_lines if full unless $config[:edit_signature]
|
162
238
|
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
def sig_lines
|
243
|
+
p = PersonManager.person_for @header["From"]
|
244
|
+
sigfn = (AccountManager.account_for(p.email) ||
|
245
|
+
AccountManager.default_account).signature
|
246
|
+
|
247
|
+
if sigfn && File.exists?(sigfn)
|
248
|
+
["", "-- "] + File.readlines(sigfn).map { |l| l.chomp }
|
249
|
+
else
|
250
|
+
[]
|
251
|
+
end
|
252
|
+
end
|
163
253
|
end
|
164
254
|
|
165
255
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Redwood
|
4
|
+
|
5
|
+
## meant to be spawned via spawn_modal!
|
6
|
+
class FileBrowserMode < LineCursorMode
|
7
|
+
RESERVED_ROWS = 1
|
8
|
+
|
9
|
+
register_keymap do |k|
|
10
|
+
k.add :back, "Go back to previous directory", "B"
|
11
|
+
k.add :view, "View file", "v"
|
12
|
+
k.add :select_file_or_follow_directory, "Select the highlighted file, or follow the directory", :enter
|
13
|
+
k.add :reload, "Reload file list", "R"
|
14
|
+
end
|
15
|
+
|
16
|
+
bool_reader :done
|
17
|
+
attr_reader :value
|
18
|
+
|
19
|
+
def initialize dir="."
|
20
|
+
@dirs = [Pathname.new(dir).realpath]
|
21
|
+
@done = false
|
22
|
+
@value = nil
|
23
|
+
regen_text
|
24
|
+
super :skip_top_rows => RESERVED_ROWS
|
25
|
+
end
|
26
|
+
|
27
|
+
def cwd; @dirs.last end
|
28
|
+
def lines; @text.length; end
|
29
|
+
def [] i; @text[i]; end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def back
|
34
|
+
return if @dirs.size == 1
|
35
|
+
@dirs.pop
|
36
|
+
reload
|
37
|
+
end
|
38
|
+
|
39
|
+
def reload
|
40
|
+
regen_text
|
41
|
+
buffer.mark_dirty
|
42
|
+
end
|
43
|
+
|
44
|
+
def view
|
45
|
+
name, f = @files[curpos - RESERVED_ROWS]
|
46
|
+
return unless f && f.file?
|
47
|
+
|
48
|
+
begin
|
49
|
+
BufferManager.spawn f.to_s, TextMode.new(f.readlines.join)
|
50
|
+
rescue SystemCallError => e
|
51
|
+
BufferManager.flash e.message
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def select_file_or_follow_directory
|
56
|
+
name, f = @files[curpos - RESERVED_ROWS]
|
57
|
+
return unless f
|
58
|
+
|
59
|
+
if f.directory? && f.to_s != "."
|
60
|
+
if f.readable?
|
61
|
+
@dirs.push f
|
62
|
+
reload
|
63
|
+
else
|
64
|
+
BufferManager.flash "Permission denied - #{f.realpath}"
|
65
|
+
end
|
66
|
+
else
|
67
|
+
begin
|
68
|
+
@value = [f.realpath.to_s]
|
69
|
+
@done = true
|
70
|
+
rescue SystemCallError => e
|
71
|
+
BufferManager.flash e.message
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def regen_text
|
77
|
+
@files =
|
78
|
+
begin
|
79
|
+
cwd.entries.sort_by do |f|
|
80
|
+
[f.directory? ? 0 : 1, f.basename.to_s]
|
81
|
+
end
|
82
|
+
rescue SystemCallError => e
|
83
|
+
BufferManager.flash "Error: #{e.message}"
|
84
|
+
[Pathname.new("."), Pathname.new("..")]
|
85
|
+
end.map do |f|
|
86
|
+
real_f = cwd + f
|
87
|
+
name = f.basename.to_s +
|
88
|
+
case
|
89
|
+
when real_f.symlink?
|
90
|
+
"@"
|
91
|
+
when real_f.directory?
|
92
|
+
"/"
|
93
|
+
else
|
94
|
+
""
|
95
|
+
end
|
96
|
+
[name, real_f]
|
97
|
+
end
|
98
|
+
|
99
|
+
size_width = @files.max_of { |name, f| f.human_size.length }
|
100
|
+
time_width = @files.max_of { |name, f| f.human_time.length }
|
101
|
+
|
102
|
+
@text = ["#{cwd}:"] + @files.map do |name, f|
|
103
|
+
sprintf "%#{time_width}s %#{size_width}s %s", f.human_time, f.human_size, name
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|