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.

Files changed (57) hide show
  1. data/HACKING +6 -36
  2. data/History.txt +11 -0
  3. data/Manifest.txt +5 -0
  4. data/README.txt +13 -31
  5. data/Rakefile +3 -3
  6. data/bin/sup +167 -89
  7. data/bin/sup-add +39 -29
  8. data/bin/sup-config +57 -31
  9. data/bin/sup-sync +60 -54
  10. data/bin/sup-sync-back +143 -0
  11. data/doc/FAQ.txt +56 -19
  12. data/doc/Philosophy.txt +34 -33
  13. data/doc/TODO +76 -46
  14. data/doc/UserGuide.txt +142 -122
  15. data/lib/sup.rb +76 -36
  16. data/lib/sup/account.rb +27 -19
  17. data/lib/sup/buffer.rb +130 -44
  18. data/lib/sup/contact.rb +1 -1
  19. data/lib/sup/draft.rb +1 -2
  20. data/lib/sup/imap.rb +64 -19
  21. data/lib/sup/index.rb +95 -16
  22. data/lib/sup/keymap.rb +1 -1
  23. data/lib/sup/label.rb +31 -5
  24. data/lib/sup/maildir.rb +7 -5
  25. data/lib/sup/mbox.rb +34 -15
  26. data/lib/sup/mbox/loader.rb +30 -12
  27. data/lib/sup/mbox/ssh-loader.rb +7 -5
  28. data/lib/sup/message.rb +93 -44
  29. data/lib/sup/modes/buffer-list-mode.rb +1 -1
  30. data/lib/sup/modes/completion-mode.rb +55 -0
  31. data/lib/sup/modes/compose-mode.rb +6 -25
  32. data/lib/sup/modes/contact-list-mode.rb +1 -1
  33. data/lib/sup/modes/edit-message-mode.rb +119 -29
  34. data/lib/sup/modes/file-browser-mode.rb +108 -0
  35. data/lib/sup/modes/forward-mode.rb +3 -20
  36. data/lib/sup/modes/inbox-mode.rb +9 -12
  37. data/lib/sup/modes/label-list-mode.rb +28 -46
  38. data/lib/sup/modes/label-search-results-mode.rb +1 -16
  39. data/lib/sup/modes/line-cursor-mode.rb +44 -5
  40. data/lib/sup/modes/person-search-results-mode.rb +1 -16
  41. data/lib/sup/modes/reply-mode.rb +18 -31
  42. data/lib/sup/modes/resume-mode.rb +6 -6
  43. data/lib/sup/modes/scroll-mode.rb +6 -5
  44. data/lib/sup/modes/search-results-mode.rb +6 -17
  45. data/lib/sup/modes/thread-index-mode.rb +70 -28
  46. data/lib/sup/modes/thread-view-mode.rb +65 -29
  47. data/lib/sup/person.rb +71 -30
  48. data/lib/sup/poll.rb +13 -4
  49. data/lib/sup/rfc2047.rb +61 -0
  50. data/lib/sup/sent.rb +7 -5
  51. data/lib/sup/source.rb +12 -9
  52. data/lib/sup/suicide.rb +36 -0
  53. data/lib/sup/tagger.rb +6 -6
  54. data/lib/sup/textfield.rb +76 -14
  55. data/lib/sup/thread.rb +97 -123
  56. data/lib/sup/util.rb +167 -1
  57. 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.map { |name, buf| buf.mode.name.length }.max
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
- super()
8
- @header = {
5
+ header = {
9
6
  "From" => AccountManager.default_account.full_address,
10
- "Message-Id" => gen_message_id,
11
7
  }
12
8
 
13
- @header["To"] = opts[:to].map { |p| p.full_address }.join(", ") if opts[:to]
14
- @header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
15
- @header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
16
- @header["Subject"] = opts[:subj] if opts[:subj]
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
- def regen_text
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
 
@@ -23,7 +23,7 @@ class ContactListMode < LineCursorMode
23
23
  k.add :search, "Search for messages from particular people", 'S'
24
24
  end
25
25
 
26
- def initialize mode = :regular
26
+ def initialize mode=:regular
27
27
  @mode = mode
28
28
  @tags = Tagger.new self
29
29
  @num = nil
@@ -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 *a
21
- super
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
- new_header, new_body = parse_file(@file.path)
40
- NON_EDITABLE_HEADERS.each { |h| new_header[h] = header[h] if header[h] }
41
- handle_new_text new_header, new_body
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
- protected
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 gen_message_id
52
- "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
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
- IO.popen(acct.sendmail, "w") { |p| write_message p, true, date }
125
-
126
- BufferManager.kill_buffer buffer
127
- BufferManager.flash "Message sent!"
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 sig_lines
137
- sigfn = (AccountManager.account_for(header["From"]) ||
138
- AccountManager.default_account).sig_file
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
- if sigfn && File.exists?(sigfn)
141
- ["", "-- "] + File.readlines(sigfn).map { |l| l.chomp }
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
- def write_message f, full_header=true, date=Time.now
148
- raise ArgumentError, "no pre-defined date: header allowed" if header["Date"]
149
- f.puts header_lines(header)
150
- f.puts "Date: #{date.rfc2822}"
151
- if full_header
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