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
data/lib/sup/person.rb CHANGED
@@ -5,42 +5,89 @@ class PersonManager
5
5
 
6
6
  def initialize fn
7
7
  @fn = fn
8
- @names = {}
9
- IO.readlines(fn).map { |l| l =~ /^(.*)?:\s+(\d+)\s+(.*)$/ && @names[$1] = [$2.to_i, $3] } if File.exists? fn
8
+ @@people = {}
9
+
10
+ ## read in stored people
11
+ IO.readlines(fn).map do |l|
12
+ l =~ /^(.*)?:\s+(\d+)\s+(.*)$/ or raise "can't parse: #{l}"
13
+ email, time, name = $1, $2, $3
14
+ @@people[email] = Person.new name, email, time, false
15
+ end if File.exists? fn
16
+
10
17
  self.class.i_am_the_instance self
11
18
  end
12
19
 
13
- def name_for email; @names.member?(email) ? @names[email][1] : nil; end
14
- def register email, name
15
- return unless name
20
+ def save
21
+ File.open(@fn, "w") do |f|
22
+ @@people.each do |email, p|
23
+ f.puts "#{p.email}: #{p.timestamp} #{p.name}"
24
+ end
25
+ end
26
+ end
16
27
 
17
- name = name.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ").gsub(/^['"]|['"]$/, "")
28
+ def self.people_for s, opts={}
29
+ return [] if s.nil?
30
+ s.split_on_commas.map { |ss| self.person_for ss, opts }
31
+ end
18
32
 
19
- ## all else being equal, prefer longer names, unless the prior name
20
- ## doesn't contain any capitalization
21
- oldcount, oldname = @names[email]
22
- @names[email] = [0, name] if oldname.nil? || oldname.length < name.length || (oldname !~ /[A-Z]/ && name =~ /[A-Z]/)
23
- @names[email][0] = Time.now.to_i
33
+ def self.person_for s, opts={}
34
+ p = Person.from_address(s) or return nil
35
+ p.definitive = true if opts[:definitive]
36
+ register p
24
37
  end
38
+
39
+ def self.register p
40
+ oldp = @@people[p.email]
25
41
 
26
- def save; File.open(@fn, "w") { |f| @names.each { |email, (time, name)| f.puts "#{email}: #{time} #{name}" } }; end
27
- end
42
+ if oldp.nil? || p.better_than?(oldp)
43
+ @@people[p.email] = p
44
+ end
28
45
 
29
- class Person
30
- @@email_map = {}
46
+ @@people[p.email].touch!
47
+ @@people[p.email]
48
+ end
49
+ end
31
50
 
32
- attr_accessor :name, :email
51
+ ## don't create these by hand. rather, go through personmanager, to
52
+ ## ensure uniqueness and overriding.
53
+ class Person
54
+ attr_accessor :name, :email, :timestamp
55
+ bool_accessor :definitive
33
56
 
34
- def initialize name, email
57
+ def initialize name, email, timestamp=0, definitive=false
35
58
  raise ArgumentError, "email can't be nil" unless email
59
+
60
+ if name
61
+ @name = name.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
62
+ if @name =~ /^(['"]\s*)(.*?)(\s*["'])$/
63
+ @name = $2
64
+ end
65
+ end
66
+
36
67
  @email = email.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ").downcase
37
- PersonManager.register @email, name
38
- @name = PersonManager.name_for @email
68
+ @definitive = definitive
69
+ @timestamp = timestamp
70
+ end
71
+
72
+ ## heuristic: whether the name attached to this email is "real", i.e.
73
+ ## we should bother to store it.
74
+ def generic?
75
+ @email =~ /no\-?reply/
76
+ end
77
+
78
+ def better_than? o
79
+ return false if o.definitive? || generic?
80
+ return true if definitive?
81
+ o.name.nil? || (name && name.length > o.name.length && name =~ /[a-z]/)
39
82
  end
40
83
 
41
- def == o; o && o.email == email; end
42
- alias :eql? :==
43
- def hash; [name, email].hash; end
84
+ def to_s; "#@name <#@email>" end
85
+
86
+ def touch!; @timestamp = Time.now.to_i end
87
+
88
+ # def == o; o && o.email == email; end
89
+ # alias :eql? :==
90
+ # def hash; [name, email].hash; end
44
91
 
45
92
  def shortname
46
93
  case @name
@@ -93,7 +140,7 @@ class Person
93
140
  end.downcase
94
141
  end
95
142
 
96
- def self.for s
143
+ def self.from_address s
97
144
  return nil if s.nil?
98
145
 
99
146
  ## try and parse an email address and name
@@ -110,13 +157,7 @@ class Person
110
157
  [nil, s]
111
158
  end
112
159
 
113
- @@email_map[email] ||= Person.new name, email
114
- end
115
-
116
- def self.for_several s
117
- return [] if s.nil?
118
-
119
- s.split_on_commas.map { |ss| self.for ss }
160
+ Person.new name, email
120
161
  end
121
162
  end
122
163
 
data/lib/sup/poll.rb CHANGED
@@ -9,6 +9,7 @@ class PollManager
9
9
 
10
10
  def initialize
11
11
  @mutex = Mutex.new
12
+ @thread = nil
12
13
  @last_poll = nil
13
14
 
14
15
  self.class.i_am_the_instance self
@@ -29,8 +30,8 @@ class PollManager
29
30
  [num, numi]
30
31
  end
31
32
 
32
- def start_thread
33
- Redwood::reporting_thread do
33
+ def start
34
+ @thread = Redwood::reporting_thread do
34
35
  while true
35
36
  sleep DELAY / 2
36
37
  poll if @last_poll.nil? || (Time.now - @last_poll) >= DELAY
@@ -38,6 +39,11 @@ class PollManager
38
39
  end
39
40
  end
40
41
 
42
+ def stop
43
+ @thread.kill if @thread
44
+ @thread = nil
45
+ end
46
+
41
47
  def do_poll
42
48
  total_num = total_numi = 0
43
49
  @mutex.synchronize do
@@ -94,18 +100,21 @@ class PollManager
94
100
 
95
101
  source.each do |offset, labels|
96
102
  if source.has_errors?
97
- Redwood::log "error loading messages from #{source}: #{source.broken_msg}"
103
+ Redwood::log "error loading messages from #{source}: #{source.error.message}"
98
104
  return
99
105
  end
100
106
 
101
107
  labels.each { |l| LabelManager << l }
102
- labels += [:inbox] unless source.archived?
108
+ labels = labels + (source.archived? ? [] : [:inbox])
103
109
 
104
110
  begin
105
111
  m = Message.new :source => source, :source_info => offset, :labels => labels
106
112
  if m.source_marked_read?
107
113
  m.remove_label :unread
108
114
  labels.delete :unread
115
+ else
116
+ m.add_label :unread
117
+ labels << :unread
109
118
  end
110
119
 
111
120
  docid, entry = Index.load_entry_for_id m.id
@@ -0,0 +1,61 @@
1
+ ## from: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/101949
2
+
3
+ # $Id: rfc2047.rb,v 1.4 2003/04/18 20:55:56 sam Exp $
4
+ # MODIFIED slightly by William Morgan
5
+ #
6
+ # An implementation of RFC 2047 decoding.
7
+ #
8
+ # This module depends on the iconv library by Nobuyoshi Nakada, which I've
9
+ # heard may be distributed as a standard part of Ruby 1.8. Many thanks to him
10
+ # for helping with building and using iconv.
11
+ #
12
+ # Thanks to "Josef 'Jupp' Schugt" <jupp / gmx.de> for pointing out an error with
13
+ # stateful character sets.
14
+ #
15
+ # Copyright (c) Sam Roberts <sroberts / uniserve.com> 2004
16
+ #
17
+ # This file is distributed under the same terms as Ruby.
18
+
19
+ require 'iconv'
20
+
21
+ module Rfc2047
22
+ WORD = %r{=\?([!\#$%&'*+-/0-9A-Z\\^\`a-z{|}~]+)\?([BbQq])\?([!->@-~]+)\?=} # :nodoc: 'stupid ruby-mode
23
+ WORDSEQ = %r{(#{WORD.source})\s+(?=#{WORD.source})}
24
+
25
+ def Rfc2047.is_encoded? s; s =~ WORD end
26
+
27
+ # Decodes a string, +from+, containing RFC 2047 encoded words into a target
28
+ # character set, +target+. See iconv_open(3) for information on the
29
+ # supported target encodings. If one of the encoded words cannot be
30
+ # converted to the target encoding, it is left in its encoded form.
31
+ def Rfc2047.decode_to(target, from)
32
+ from = from.gsub(WORDSEQ, '\1')
33
+ out = from.gsub(WORD) do
34
+ |word|
35
+ charset, encoding, text = $1, $2, $3
36
+
37
+ # B64 or QP decode, as necessary:
38
+ case encoding
39
+ when 'b', 'B'
40
+ #puts text
41
+ text = text.unpack('m*')[0]
42
+ #puts text.dump
43
+
44
+ when 'q', 'Q'
45
+ # RFC 2047 has a variant of quoted printable where a ' ' character
46
+ # can be represented as an '_', rather than =32, so convert
47
+ # any of these that we find before doing the QP decoding.
48
+ text = text.tr("_", " ")
49
+ text = text.unpack('M*')[0]
50
+
51
+ # Don't need an else, because no other values can be matched in a
52
+ # WORD.
53
+ end
54
+
55
+ # Convert:
56
+ #
57
+ # Remember - Iconv.open(to, from)!
58
+ text = Iconv.iconv(target, charset, text).join
59
+ end
60
+ end
61
+ end
data/lib/sup/sent.rb CHANGED
@@ -30,18 +30,20 @@ class SentManager
30
30
  end
31
31
 
32
32
  class SentLoader < MBox::Loader
33
+ yaml_properties :cur_offset
34
+
33
35
  def initialize cur_offset=0
34
- filename = Redwood::SENT_FN
35
- File.open(filename, "w") { } unless File.exists? filename
36
- super "mbox://" + filename, cur_offset, true, true
36
+ @filename = Redwood::SENT_FN
37
+ File.open(@filename, "w") { } unless File.exists? @filename
38
+ super "mbox://" + @filename, cur_offset, true, true
37
39
  end
38
40
 
41
+ def file_path; @filename end
42
+
39
43
  def uri; SentManager.source_name; end
40
44
  def to_s; SentManager.source_name; end
41
45
  def id; SentManager.source_id; end
42
46
  def labels; [:sent, :inbox]; end
43
47
  end
44
48
 
45
- Redwood::register_yaml(SentLoader, %w(cur_offset))
46
-
47
49
  end
data/lib/sup/source.rb CHANGED
@@ -5,15 +5,15 @@ class OutOfSyncSourceError < SourceError; end
5
5
  class FatalSourceError < SourceError; end
6
6
 
7
7
  class Source
8
- ## Implementing a new source is typically quite easy, because Sup
9
- ## only needs to be able to:
8
+ ## Implementing a new source should be easy, because Sup only needs
9
+ ## to be able to:
10
10
  ## 1. See how many messages it contains
11
- ## 2. Get an arbirtrary message
11
+ ## 2. Get an arbitrary message
12
12
  ## 3. (optional) see whether the source has marked it read or not
13
13
  ##
14
14
  ## In particular, Sup doesn't need to move messages, mark them as
15
- ## read, delete them, or anything else. (Well, at some point it will
16
- ## need to delete them, but that will be an optional capability.)
15
+ ## read, delete them, or anything else. (Well, it's nice to be able
16
+ ## to delete them, but that is optional.)
17
17
  ##
18
18
  ## On the other hand, Sup assumes that you can assign each message a
19
19
  ## unique integer id, such that newer messages have higher ids than
@@ -33,7 +33,8 @@ class Source
33
33
  ## - raw_header offset
34
34
  ## - raw_full_message offset
35
35
  ## - check
36
- ## - next (or each, if you prefer)
36
+ ## - next (or each, if you prefer): should return a message and an
37
+ ## array of labels.
37
38
  ##
38
39
  ## ... where "offset" really means unique id. (You can tell I
39
40
  ## started with mbox.)
@@ -46,7 +47,7 @@ class Source
46
47
  ## else (e.g. the imap server is down or the maildir is missing.)
47
48
  ##
48
49
  ## Finally, be sure the source is thread-safe, since it WILL be
49
- ## pummeled from multiple threads at once.
50
+ ## pummelled from multiple threads at once.
50
51
  ##
51
52
  ## Examples for you to look at: mbox/loader.rb, imap.rb, and
52
53
  ## maildir.rb.
@@ -60,6 +61,8 @@ class Source
60
61
  attr_accessor :id
61
62
 
62
63
  def initialize uri, initial_offset=nil, usual=true, archived=false, id=nil
64
+ raise ArgumentError, "id must be an integer: #{id.inspect}" unless id.is_a? Fixnum if id
65
+
63
66
  @uri = uri
64
67
  @cur_offset = initial_offset
65
68
  @usual = usual
@@ -68,6 +71,8 @@ class Source
68
71
  @dirty = false
69
72
  end
70
73
 
74
+ def file_path; nil end
75
+
71
76
  def to_s; @uri.to_s; end
72
77
  def seek_to! o; self.cur_offset = o; end
73
78
  def reset!; seek_to! start_offset; end
@@ -97,6 +102,4 @@ protected
97
102
  end
98
103
  end
99
104
 
100
- Redwood::register_yaml(Source, %w(uri cur_offset usual archived id))
101
-
102
105
  end
@@ -0,0 +1,36 @@
1
+ module Redwood
2
+
3
+ class SuicideManager
4
+ include Singleton
5
+
6
+ DELAY = 5
7
+
8
+ def initialize fn
9
+ @fn = fn
10
+ @die = false
11
+ @thread = nil
12
+ self.class.i_am_the_instance self
13
+ FileUtils.rm_f @fn
14
+ end
15
+
16
+ bool_reader :die
17
+
18
+ def start
19
+ @thread = Redwood::reporting_thread do
20
+ while true
21
+ sleep DELAY
22
+ if File.exists? @fn
23
+ FileUtils.rm_f @fn
24
+ @die = true
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def stop
31
+ @thread.kill if @thread
32
+ @thread = nil
33
+ end
34
+ end
35
+
36
+ end
data/lib/sup/tagger.rb CHANGED
@@ -12,23 +12,23 @@ class Tagger
12
12
  def drop_tag_for o; @tagged.delete o; end
13
13
 
14
14
  def apply_to_tagged
15
- num_tagged = @tagged.map { |t| t ? 1 : 0 }.sum
15
+ targets = @tagged.select_by_value
16
+ num_tagged = targets.size
16
17
  if num_tagged == 0
17
- BufferManager.flash "No tagged messages!"
18
+ BufferManager.flash "No tagged threads!"
18
19
  return
19
20
  end
20
21
 
21
- noun = num_tagged == 1 ? "message" : "messages"
22
+ noun = num_tagged == 1 ? "thread" : "threads"
22
23
  c = BufferManager.ask_getch "apply to #{num_tagged} tagged #{noun}:"
23
24
  return if c.nil? # user cancelled
24
25
 
25
- if(action = @mode.resolve_input c)
26
+ if(action = @mode.resolve_input(c))
26
27
  tagged_sym = "multi_#{action}".intern
27
28
  if @mode.respond_to? tagged_sym
28
- targets = @tagged.select_by_value
29
29
  @mode.send tagged_sym, targets
30
30
  else
31
- BufferManager.flash "That command cannot be applied to multiple messages."
31
+ BufferManager.flash "That command cannot be applied to multiple threads."
32
32
  end
33
33
  else
34
34
  BufferManager.flash "Unknown command #{c.to_character}."
data/lib/sup/textfield.rb CHANGED
@@ -2,26 +2,47 @@ require 'curses'
2
2
 
3
3
  module Redwood
4
4
 
5
+ ## a fully-functional text field supporting completions, expansions,
6
+ ## history--everything!
7
+ ##
8
+ ## completion is done emacs-style, and mostly depends on outside
9
+ ## support, as we merely signal the existence of a new set of
10
+ ## completions to show (#new_completions?) or that the current list
11
+ ## of completions should be rolled if they're too large to fill the
12
+ ## screen (#roll_completions?).
13
+ ##
14
+ ## in sup, completion support is implemented through BufferManager#ask
15
+ ## and CompletionMode.
5
16
  class TextField
6
- attr_reader :value
7
-
8
17
  def initialize window, y, x, width
9
18
  @w, @x, @y = window, x, y
10
19
  @width = width
11
20
  @i = nil
12
21
  @history = []
22
+
23
+ @completion_block = nil
24
+ reset_completion_state
13
25
  end
14
26
 
15
- def activate question, default=nil
27
+ bool_reader :new_completions, :roll_completions
28
+ attr_reader :completions
29
+
30
+ ## when the user presses enter, we store the value in @value and
31
+ ## clean up all the ncurses cruft. before @value is set, we can
32
+ ## get the current value from ncurses.
33
+ def value; @field ? get_cur_value : @value end
34
+
35
+ def activate question, default=nil, &block
16
36
  @question = question
17
37
  @value = nil
38
+ @completion_block = block
18
39
  @field = Ncurses::Form.new_field 1, @width - question.length,
19
40
  @y, @x + question.length, 0, 0
20
41
  @form = Ncurses::Form.new_form [@field]
21
42
 
22
43
  @history[@i = @history.size] = default || ""
23
44
  Ncurses::Form.post_form @form
24
- @field.set_field_buffer 0, @history[@i]
45
+ set_cur_value @history[@i]
25
46
  end
26
47
 
27
48
  def position_cursor
@@ -33,24 +54,47 @@ class TextField
33
54
  end
34
55
 
35
56
  def deactivate
57
+ reset_completion_state
36
58
  @form.unpost_form
37
59
  @form.free_form
38
60
  @field.free_field
61
+ @field = nil
39
62
  Ncurses.curs_set 0
40
63
  end
41
64
 
42
65
  def handle_input c
43
- if c == 10 # Ncurses::KEY_ENTER
44
- Ncurses::Form.form_driver @form, Ncurses::Form::REQ_VALIDATION
45
- @value = @history[@i] = @field.field_buffer(0).gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
66
+ ## short-circuit exit paths
67
+ case c
68
+ when Ncurses::KEY_ENTER # submit!
69
+ @value = @history[@i] = get_cur_value
46
70
  return false
47
- elsif c == Ncurses::KEY_CANCEL
71
+ when Ncurses::KEY_CANCEL # cancel
48
72
  @history.delete_at @i
49
73
  @i = @history.empty? ? nil : (@i - 1) % @history.size
50
74
  @value = nil
51
75
  return false
76
+ when Ncurses::KEY_TAB # completion
77
+ return true unless @completion_block
78
+ if @completions.empty?
79
+ v = get_cur_value
80
+ c = @completion_block.call v
81
+ if c.size > 0
82
+ set_cur_value c.map { |full, short| full }.shared_prefix
83
+ end
84
+ if c.size > 1
85
+ @completions = c
86
+ @new_completions = true
87
+ @roll_completions = false
88
+ end
89
+ else
90
+ @new_completions = false
91
+ @roll_completions = true
92
+ end
93
+ return true
52
94
  end
53
95
 
96
+ reset_completion_state
97
+
54
98
  d =
55
99
  case c
56
100
  when Ncurses::KEY_LEFT
@@ -64,21 +108,39 @@ class TextField
64
108
  when ?\005
65
109
  Ncurses::Form::REQ_END_FIELD
66
110
  when Ncurses::KEY_UP
67
- @history[@i] = @field.field_buffer(0)
111
+ @history[@i] = @field.field_buffer 0
68
112
  @i = (@i - 1) % @history.size
69
- @field.set_field_buffer 0, @history[@i]
113
+ set_cur_value @history[@i]
70
114
  when Ncurses::KEY_DOWN
71
- @history[@i] = @field.field_buffer(0)
115
+ @history[@i] = @field.field_buffer 0
72
116
  @i = (@i + 1) % @history.size
73
- @field.set_field_buffer 0, @history[@i]
117
+ set_cur_value @history[@i]
74
118
  else
75
119
  c
76
120
  end
77
121
 
78
122
  Ncurses::Form.form_driver @form, d
79
- Ncurses.refresh
80
-
81
123
  true
82
124
  end
125
+
126
+ private
127
+
128
+ def reset_completion_state
129
+ @completions = []
130
+ @new_completions = @roll_completions = @clear_completions = false
131
+ end
132
+
133
+ ## ncurses inanity wrapper
134
+ def get_cur_value
135
+ Ncurses::Form.form_driver @form, Ncurses::Form::REQ_VALIDATION
136
+ @field.field_buffer(0).gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
137
+ end
138
+
139
+ ## ncurses inanity wrapper
140
+ def set_cur_value v
141
+ @field.set_field_buffer 0, v
142
+ Ncurses::Form.form_driver @form, Ncurses::Form::REQ_END_FIELD
143
+ end
144
+
83
145
  end
84
146
  end