sup 0.2 → 0.3

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 (43) hide show
  1. data/History.txt +10 -0
  2. data/bin/sup +50 -68
  3. data/doc/NewUserGuide.txt +11 -7
  4. data/doc/TODO +34 -22
  5. data/lib/sup.rb +30 -24
  6. data/lib/sup/buffer.rb +124 -39
  7. data/lib/sup/colormap.rb +4 -4
  8. data/lib/sup/draft.rb +1 -1
  9. data/lib/sup/hook.rb +18 -5
  10. data/lib/sup/imap.rb +11 -13
  11. data/lib/sup/index.rb +52 -14
  12. data/lib/sup/keymap.rb +1 -1
  13. data/lib/sup/logger.rb +1 -0
  14. data/lib/sup/maildir.rb +9 -0
  15. data/lib/sup/mbox.rb +3 -1
  16. data/lib/sup/message-chunks.rb +21 -7
  17. data/lib/sup/message.rb +31 -15
  18. data/lib/sup/mode.rb +2 -0
  19. data/lib/sup/modes/buffer-list-mode.rb +7 -3
  20. data/lib/sup/modes/compose-mode.rb +14 -16
  21. data/lib/sup/modes/contact-list-mode.rb +2 -2
  22. data/lib/sup/modes/edit-message-mode.rb +55 -23
  23. data/lib/sup/modes/forward-mode.rb +22 -5
  24. data/lib/sup/modes/inbox-mode.rb +3 -7
  25. data/lib/sup/modes/label-list-mode.rb +30 -10
  26. data/lib/sup/modes/label-search-results-mode.rb +12 -0
  27. data/lib/sup/modes/line-cursor-mode.rb +13 -0
  28. data/lib/sup/modes/log-mode.rb +0 -6
  29. data/lib/sup/modes/poll-mode.rb +0 -3
  30. data/lib/sup/modes/reply-mode.rb +19 -11
  31. data/lib/sup/modes/scroll-mode.rb +111 -20
  32. data/lib/sup/modes/search-results-mode.rb +21 -0
  33. data/lib/sup/modes/text-mode.rb +10 -2
  34. data/lib/sup/modes/thread-index-mode.rb +200 -90
  35. data/lib/sup/modes/thread-view-mode.rb +27 -10
  36. data/lib/sup/person.rb +1 -0
  37. data/lib/sup/poll.rb +15 -7
  38. data/lib/sup/source.rb +6 -1
  39. data/lib/sup/suicide.rb +1 -1
  40. data/lib/sup/textfield.rb +14 -14
  41. data/lib/sup/thread.rb +6 -2
  42. data/lib/sup/util.rb +111 -9
  43. metadata +13 -6
@@ -1,8 +1,6 @@
1
1
  module Redwood
2
2
 
3
3
  class ThreadViewMode < LineCursorMode
4
- include CanSpawnComposeMode
5
-
6
4
  ## this holds all info we need to lay out a message
7
5
  class MessageLayout
8
6
  attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new
@@ -27,7 +25,7 @@ class ThreadViewMode < LineCursorMode
27
25
  k.add :jump_to_prev_open, "Jump to previous open message", 'p'
28
26
  k.add :toggle_starred, "Star or unstar message", '*'
29
27
  k.add :toggle_new, "Toggle new/read status of message", 'N'
30
- # k.add :collapse_non_new_messages, "Collapse all but new messages", 'N'
28
+ # k.add :collapse_non_new_messages, "Collapse all but unread messages", 'N'
31
29
  k.add :reply, "Reply to a message", 'r'
32
30
  k.add :forward, "Forward a message", 'f'
33
31
  k.add :alias, "Edit alias/nickname for a person", 'i'
@@ -37,6 +35,8 @@ class ThreadViewMode < LineCursorMode
37
35
  k.add :compose, "Compose message to person", 'm'
38
36
  k.add :archive_and_kill, "Archive thread and kill buffer", 'a'
39
37
  k.add :delete_and_kill, "Delete thread and kill buffer", 'd'
38
+ k.add :subscribe_to_list, "Subscribe to/unsubscribe from mailing list", "("
39
+ k.add :unsubscribe_from_list, "Subscribe to/unsubscribe from mailing list", ")"
40
40
  end
41
41
 
42
42
  ## there are a couple important instance variables we hold to format
@@ -107,11 +107,27 @@ class ThreadViewMode < LineCursorMode
107
107
  BufferManager.spawn "Reply to #{m.subj}", mode
108
108
  end
109
109
 
110
+ def subscribe_to_list
111
+ m = @message_lines[curpos] or return
112
+ if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
113
+ ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
114
+ else
115
+ BufferManager.flash "Can't find List-Subscribe header for this message."
116
+ end
117
+ end
118
+
119
+ def unsubscribe_from_list
120
+ m = @message_lines[curpos] or return
121
+ if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
122
+ ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
123
+ else
124
+ BufferManager.flash "Can't find List-Unsubscribe header for this message."
125
+ end
126
+ end
127
+
110
128
  def forward
111
129
  m = @message_lines[curpos] or return
112
- mode = ForwardMode.new m
113
- BufferManager.spawn "Forward of #{m.subj}", mode
114
- mode.edit_message
130
+ ForwardMode.spawn_nicely m
115
131
  end
116
132
 
117
133
  include CanAliasContacts
@@ -131,9 +147,9 @@ class ThreadViewMode < LineCursorMode
131
147
  def compose
132
148
  p = @person_lines[curpos]
133
149
  if p
134
- spawn_compose_mode :to => [p]
150
+ ComposeMode.spawn_nicely :to => [p]
135
151
  else
136
- spawn_compose_mode
152
+ ComposeMode.spawn_nicely
137
153
  end
138
154
  end
139
155
 
@@ -145,7 +161,7 @@ class ThreadViewMode < LineCursorMode
145
161
  @thread.labels = (reserved_labels + new_labels).uniq
146
162
  new_labels.each { |l| LabelManager << l }
147
163
  update
148
- UpdateManager.relay self, :label, m
164
+ UpdateManager.relay self, :label_thread, @thread
149
165
  end
150
166
 
151
167
  def toggle_starred
@@ -193,7 +209,7 @@ class ThreadViewMode < LineCursorMode
193
209
 
194
210
  def edit_as_new
195
211
  m = @message_lines[curpos] or return
196
- mode = ComposeMode.new(:body => m.basic_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc)
212
+ mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc)
197
213
  BufferManager.spawn "edit as new", mode
198
214
  mode.edit_message
199
215
  end
@@ -236,6 +252,7 @@ class ThreadViewMode < LineCursorMode
236
252
  end
237
253
 
238
254
  def jump_to_next_open
255
+ return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations
239
256
  m = @message_lines[curpos] or return
240
257
  while nextm = @layout[m].next
241
258
  break if @layout[nextm].state != :closed
@@ -21,6 +21,7 @@ class PersonManager
21
21
  File.open(@fn, "w") do |f|
22
22
  @@people.each do |email, p|
23
23
  next if p.email == p.name
24
+ next if p.email =~ /=/ # drop rfc2047-encoded, and lots of other useless emails. definitely a heuristic.
24
25
  f.puts "#{p.email}: #{p.timestamp} #{p.name}"
25
26
  end
26
27
  end
@@ -5,6 +5,12 @@ module Redwood
5
5
  class PollManager
6
6
  include Singleton
7
7
 
8
+ HookManager.register "before-add-message", <<EOS
9
+ Executes immediately before a message is added to the index.
10
+ Variables:
11
+ message: the new message
12
+ EOS
13
+
8
14
  HookManager.register "before-poll", <<EOS
9
15
  Executes immediately before a poll for new messages commences.
10
16
  No variables.
@@ -18,7 +24,7 @@ Variables:
18
24
  not auto-archived).
19
25
  from_and_subj: an array of (from email address, subject) pairs
20
26
  from_and_subj_inbox: an array of (from email address, subject) pairs for
21
- messages appearing in the inbox
27
+ only those messages appearing in the inbox
22
28
  EOS
23
29
 
24
30
  DELAY = 300
@@ -33,7 +39,8 @@ EOS
33
39
  end
34
40
 
35
41
  def buffer
36
- BufferManager.spawn_unless_exists("<poll for new messages>", :hidden => true) { PollMode.new }
42
+ b, new = BufferManager.spawn_unless_exists("<poll for new messages>", :hidden => true) { PollMode.new }
43
+ b
37
44
  end
38
45
 
39
46
  def poll
@@ -44,7 +51,7 @@ EOS
44
51
  BufferManager.flash "Polling for new messages..."
45
52
  num, numi, from_and_subj, from_and_subj_inbox = buffer.mode.poll
46
53
  if num > 0
47
- BufferManager.flash "Loaded #{num} new messages, #{numi} to inbox."
54
+ BufferManager.flash "Loaded #{num.pluralize 'new message'}, #{numi} to inbox."
48
55
  else
49
56
  BufferManager.flash "No new messages."
50
57
  end
@@ -56,7 +63,7 @@ EOS
56
63
  end
57
64
 
58
65
  def start
59
- @thread = Redwood::reporting_thread do
66
+ @thread = Redwood::reporting_thread("periodic poll") do
60
67
  while true
61
68
  sleep DELAY / 2
62
69
  poll if @last_poll.nil? || (Time.now - @last_poll) >= DELAY
@@ -81,7 +88,7 @@ EOS
81
88
  yield "Loading from #{source}... " unless source.done? || source.has_errors?
82
89
  rescue SourceError => e
83
90
  Redwood::log "problem getting messages from #{source}: #{e.message}"
84
- Redwood::report_broken_sources
91
+ Redwood::report_broken_sources :force_to_top => true
85
92
  next
86
93
  end
87
94
 
@@ -94,7 +101,7 @@ EOS
94
101
  unless entry
95
102
  num += 1
96
103
  from_and_subj << [m.from.longname, m.subj]
97
- if m.labels.include? :inbox
104
+ if m.has_label?(:inbox) && ([:spam, :deleted, :killed] & m.labels).empty?
98
105
  from_and_subj_inbox << [m.from.longname, m.subj]
99
106
  numi += 1
100
107
  end
@@ -147,6 +154,7 @@ EOS
147
154
  end
148
155
 
149
156
  docid, entry = Index.load_entry_for_id m.id
157
+ HookManager.run "before-add-message", :message => m
150
158
  m = yield(m, offset, entry) or next
151
159
  Index.sync_message m, docid, entry
152
160
  UpdateManager.relay self, :add, m unless entry
@@ -156,7 +164,7 @@ EOS
156
164
  end
157
165
  rescue SourceError => e
158
166
  Redwood::log "problem getting messages from #{source}: #{e.message}"
159
- Redwood::report_broken_sources
167
+ Redwood::report_broken_sources :force_to_top => true
160
168
  end
161
169
  end
162
170
  end
@@ -1,6 +1,11 @@
1
1
  module Redwood
2
2
 
3
- class SourceError < StandardError; end
3
+ class SourceError < StandardError
4
+ def initialize *a
5
+ raise "don't instantiate me!" if SourceError.is_a?(self.class)
6
+ super
7
+ end
8
+ end
4
9
  class OutOfSyncSourceError < SourceError; end
5
10
  class FatalSourceError < SourceError; end
6
11
 
@@ -16,7 +16,7 @@ class SuicideManager
16
16
  bool_reader :die
17
17
 
18
18
  def start
19
- @thread = Redwood::reporting_thread do
19
+ @thread = Redwood::reporting_thread("suicide watch") do
20
20
  while true
21
21
  sleep DELAY
22
22
  if File.exists? @fn
@@ -64,7 +64,7 @@ class TextField
64
64
  case c
65
65
  when Ncurses::KEY_ENTER # submit!
66
66
  @value = get_cursed_value
67
- @history.push @value
67
+ @history.push @value unless @value =~ /^\s*$/
68
68
  return false
69
69
  when Ncurses::KEY_CANCEL # cancel
70
70
  @value = nil
@@ -108,23 +108,23 @@ class TextField
108
108
  Ncurses::Form::REQ_END_FIELD
109
109
  when 11 # ctrl-k
110
110
  Ncurses::Form::REQ_CLR_EOF
111
- when Ncurses::KEY_UP
112
- @i ||= @history.size
113
- @history[@i] = get_cursed_value
114
- @i = (@i - 1) % @history.size
115
- @value = @history[@i]
116
- set_cursed_value @value
117
- when Ncurses::KEY_DOWN
118
- @i ||= @history.size
119
- @history[@i] = get_cursed_value
120
- @i = (@i + 1) % @history.size
121
- @value = @history[@i]
122
- set_cursed_value @value
111
+ when Ncurses::KEY_UP, Ncurses::KEY_DOWN
112
+ unless @history.empty?
113
+ value = get_cursed_value
114
+ @i ||= @history.size
115
+ #Redwood::log "history before #{@history.inspect}"
116
+ @history[@i] = value #unless value =~ /^\s*$/
117
+ @i = (@i + (c == Ncurses::KEY_UP ? -1 : 1)) % @history.size
118
+ @value = @history[@i]
119
+ #Redwood::log "history after #{@history.inspect}"
120
+ set_cursed_value @value
121
+ Ncurses::Form::REQ_END_FIELD
122
+ end
123
123
  else
124
124
  c
125
125
  end
126
126
 
127
- Ncurses::Form.form_driver @form, d
127
+ Ncurses::Form.form_driver @form, d if d
128
128
  true
129
129
  end
130
130
 
@@ -86,8 +86,12 @@ class Thread
86
86
  def dirty?; any? { |m, *o| m && m.dirty? }; end
87
87
  def date; map { |m, *o| m.date if m }.compact.max; end
88
88
  def snippet
89
- last_m, last_stuff = select { |m, *o| m && m.snippet && !m.snippet.empty? }.sort_by { |m, *o| m.date }.last
90
- last_m ? last_m.snippet : ""
89
+ with_snippets = select { |m, *o| m && m.snippet && !m.snippet.empty? }
90
+ first_unread, * = with_snippets.select { |m, *o| m.has_label?(:unread) }.sort_by { |m, *o| m.date }.first
91
+ return first_unread.snippet if first_unread
92
+ last_read, * = with_snippets.sort_by { |m, *o| m.date }.last
93
+ return last_read.snippet if last_read
94
+ ""
91
95
  end
92
96
  def authors; map { |m, *o| m.from if m }.compact.uniq; end
93
97
 
@@ -136,6 +136,7 @@ class Object
136
136
 
137
137
  ## clone of java-style whole-method synchronization
138
138
  ## assumes a @mutex variable
139
+ ## TODO: clean up, try harder to avoid namespace collisions
139
140
  def synchronized *meth
140
141
  meth.each do
141
142
  class_eval <<-EOF
@@ -146,6 +147,32 @@ class Object
146
147
  EOF
147
148
  end
148
149
  end
150
+
151
+ def ignore_concurrent_calls *meth
152
+ meth.each do
153
+ mutex = "@__concurrent_protector_#{meth}"
154
+ flag = "@__concurrent_flag_#{meth}"
155
+ oldmeth = "__unprotected_#{meth}"
156
+ class_eval <<-EOF
157
+ alias #{oldmeth} #{meth}
158
+ def #{meth}(*a, &b)
159
+ #{mutex} = Mutex.new unless defined? #{mutex}
160
+ #{flag} = true unless defined? #{flag}
161
+ run = #{mutex}.synchronize do
162
+ if #{flag}
163
+ #{flag} = false
164
+ true
165
+ end
166
+ end
167
+ if run
168
+ ret = #{oldmeth}(*a, &b)
169
+ #{mutex}.synchronize { #{flag} = true }
170
+ ret
171
+ end
172
+ end
173
+ EOF
174
+ end
175
+ end
149
176
  end
150
177
 
151
178
  class String
@@ -165,6 +192,7 @@ class String
165
192
  ret
166
193
  end
167
194
 
195
+ ## one of the few things i miss from perl
168
196
  def ucfirst
169
197
  self[0 .. 0].upcase + self[1 .. -1]
170
198
  end
@@ -175,6 +203,64 @@ class String
175
203
  split(/,\s*(?=(?:[^"]*"[^"]*")*(?![^"]*"))/)
176
204
  end
177
205
 
206
+ ## ok, here we do it the hard way. got to have a remainder for purposes of
207
+ ## tab-completing full email addresses
208
+ def split_on_commas_with_remainder
209
+ ret = []
210
+ state = :outstring
211
+ pos = 0
212
+ region_start = 0
213
+ while pos <= length
214
+ newpos = case state
215
+ when :escaped_instring, :escaped_outstring: pos
216
+ else index(/[,"\\]/, pos)
217
+ end
218
+
219
+ if newpos
220
+ char = self[newpos]
221
+ else
222
+ char = nil
223
+ newpos = length
224
+ end
225
+
226
+ case char
227
+ when ?"
228
+ state = case state
229
+ when :outstring: :instring
230
+ when :instring: :outstring
231
+ when :escaped_instring: :instring
232
+ when :escaped_outstring: :outstring
233
+ end
234
+ when ?,, nil
235
+ state = case state
236
+ when :outstring, :escaped_outstring:
237
+ ret << self[region_start ... newpos].gsub(/^\s+|\s+$/, "")
238
+ region_start = newpos + 1
239
+ :outstring
240
+ when :instring: :instring
241
+ when :escaped_instring: :instring
242
+ end
243
+ when ?\\
244
+ state = case state
245
+ when :instring: :escaped_instring
246
+ when :outstring: :escaped_outstring
247
+ when :escaped_instring: :instring
248
+ when :escaped_outstring: :outstring
249
+ end
250
+ end
251
+ pos = newpos + 1
252
+ end
253
+
254
+ remainder = case state
255
+ when :instring
256
+ self[region_start .. -1].gsub(/^\s+/, "")
257
+ else
258
+ nil
259
+ end
260
+
261
+ [ret, remainder]
262
+ end
263
+
178
264
  def wrap len
179
265
  ret = []
180
266
  s = self
@@ -223,6 +309,20 @@ class Fixnum
223
309
  "<#{self}>"
224
310
  end
225
311
  end
312
+
313
+ ## hacking the english language
314
+ def pluralize s
315
+ to_s + " " +
316
+ if self == 1
317
+ s
318
+ else
319
+ if s =~ /(.*)y$/
320
+ $1 + "ies"
321
+ else
322
+ s + "s"
323
+ end
324
+ end
325
+ end
226
326
  end
227
327
 
228
328
  class Hash
@@ -299,6 +399,7 @@ class Array
299
399
  def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
300
400
 
301
401
  def last= e; self[-1] = e end
402
+ def nonempty?; !empty? end
302
403
  end
303
404
 
304
405
  class Time
@@ -405,19 +506,20 @@ module Singleton
405
506
  end
406
507
  end
407
508
 
408
- ## wraps an object. if it throws an exception, keeps a copy, and
409
- ## rethrows it for any further method calls.
509
+ ## wraps an object. if it throws an exception, keeps a copy.
410
510
  class Recoverable
411
511
  def initialize o
412
512
  @o = o
413
- @e = nil
513
+ @error = nil
514
+ @mutex = Mutex.new
414
515
  end
415
516
 
416
- def clear_error!; @e = nil; end
417
- def has_errors?; !@e.nil?; end
418
- def error; @e; end
517
+ attr_accessor :error
518
+
519
+ def clear_error!; @error = nil; end
520
+ def has_errors?; !@error.nil?; end
419
521
 
420
- def method_missing m, *a, &b; __pass m, *a, &b; end
522
+ def method_missing m, *a, &b; __pass m, *a, &b end
421
523
 
422
524
  def id; __pass :id; end
423
525
  def to_s; __pass :to_s; end
@@ -430,8 +532,8 @@ class Recoverable
430
532
  begin
431
533
  @o.send(m, *a, &b)
432
534
  rescue Exception => e
433
- @e = e
434
- raise e
535
+ @error ||= e
536
+ raise
435
537
  end
436
538
  end
437
539
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: sup
5
5
  version: !ruby/object:Gem::Version
6
- version: "0.2"
7
- date: 2007-10-29 00:00:00 -07:00
6
+ version: "0.3"
7
+ date: 2007-11-27 00:00:00 -08:00
8
8
  summary: A console-based email client with the best features of GMail, mutt, and emacs. Features full text search, labels, tagged operations, multiple buffers, recent contacts, and more.
9
9
  require_paths:
10
10
  - lib
@@ -103,10 +103,17 @@ files:
103
103
  - lib/sup/util.rb
104
104
  test_files: []
105
105
 
106
- rdoc_options: []
107
-
108
- extra_rdoc_files: []
109
-
106
+ rdoc_options:
107
+ - --main
108
+ - README.txt
109
+ extra_rdoc_files:
110
+ - History.txt
111
+ - Manifest.txt
112
+ - README.txt
113
+ - doc/FAQ.txt
114
+ - doc/Hooks.txt
115
+ - doc/NewUserGuide.txt
116
+ - doc/Philosophy.txt
110
117
  executables:
111
118
  - sup
112
119
  - sup-add