sup 0.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 (53) hide show
  1. data/History.txt +5 -0
  2. data/LICENSE +280 -0
  3. data/Manifest.txt +52 -0
  4. data/README.txt +119 -0
  5. data/Rakefile +45 -0
  6. data/bin/sup +229 -0
  7. data/bin/sup-import +162 -0
  8. data/doc/FAQ.txt +38 -0
  9. data/doc/Philosophy.txt +59 -0
  10. data/doc/TODO +31 -0
  11. data/lib/sup.rb +141 -0
  12. data/lib/sup/account.rb +53 -0
  13. data/lib/sup/buffer.rb +391 -0
  14. data/lib/sup/colormap.rb +118 -0
  15. data/lib/sup/contact.rb +40 -0
  16. data/lib/sup/draft.rb +105 -0
  17. data/lib/sup/index.rb +353 -0
  18. data/lib/sup/keymap.rb +89 -0
  19. data/lib/sup/label.rb +41 -0
  20. data/lib/sup/logger.rb +42 -0
  21. data/lib/sup/mbox.rb +51 -0
  22. data/lib/sup/mbox/loader.rb +116 -0
  23. data/lib/sup/message.rb +302 -0
  24. data/lib/sup/mode.rb +79 -0
  25. data/lib/sup/modes/buffer-list-mode.rb +37 -0
  26. data/lib/sup/modes/compose-mode.rb +33 -0
  27. data/lib/sup/modes/contact-list-mode.rb +121 -0
  28. data/lib/sup/modes/edit-message-mode.rb +162 -0
  29. data/lib/sup/modes/forward-mode.rb +38 -0
  30. data/lib/sup/modes/help-mode.rb +19 -0
  31. data/lib/sup/modes/inbox-mode.rb +45 -0
  32. data/lib/sup/modes/label-list-mode.rb +89 -0
  33. data/lib/sup/modes/label-search-results-mode.rb +29 -0
  34. data/lib/sup/modes/line-cursor-mode.rb +133 -0
  35. data/lib/sup/modes/log-mode.rb +44 -0
  36. data/lib/sup/modes/person-search-results-mode.rb +29 -0
  37. data/lib/sup/modes/poll-mode.rb +24 -0
  38. data/lib/sup/modes/reply-mode.rb +136 -0
  39. data/lib/sup/modes/resume-mode.rb +18 -0
  40. data/lib/sup/modes/scroll-mode.rb +106 -0
  41. data/lib/sup/modes/search-results-mode.rb +31 -0
  42. data/lib/sup/modes/text-mode.rb +51 -0
  43. data/lib/sup/modes/thread-index-mode.rb +389 -0
  44. data/lib/sup/modes/thread-view-mode.rb +338 -0
  45. data/lib/sup/person.rb +120 -0
  46. data/lib/sup/poll.rb +80 -0
  47. data/lib/sup/sent.rb +46 -0
  48. data/lib/sup/tagger.rb +40 -0
  49. data/lib/sup/textfield.rb +83 -0
  50. data/lib/sup/thread.rb +358 -0
  51. data/lib/sup/update.rb +21 -0
  52. data/lib/sup/util.rb +260 -0
  53. metadata +123 -0
@@ -0,0 +1,19 @@
1
+ module Redwood
2
+
3
+ class HelpMode < TextMode
4
+ def initialize mode, global_keymap
5
+ title = "Help for #{mode.name}"
6
+ super <<EOS
7
+ #{title}
8
+ #{'=' * title.length}
9
+
10
+ #{mode.help_text}
11
+ Global keybindings
12
+ ------------------
13
+ #{global_keymap.help_text}
14
+ EOS
15
+ end
16
+ end
17
+
18
+ end
19
+
@@ -0,0 +1,45 @@
1
+ require 'thread'
2
+
3
+ module Redwood
4
+
5
+ class InboxMode < ThreadIndexMode
6
+ register_keymap do |k|
7
+ ## overwrite toggle_archived with archive
8
+ k.add :archive, "Archive thread (remove from inbox)", 'a'
9
+ k.add :load_more_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
10
+ k.add :reload, "Discard threads and reload", 'R'
11
+ end
12
+
13
+ def initialize
14
+ super [:inbox], [:inbox]
15
+ end
16
+
17
+ def archive
18
+ remove_label_and_hide_thread cursor_thread, :inbox
19
+ regen_text
20
+ end
21
+
22
+ def multi_archive threads
23
+ threads.each { |t| remove_label_and_hide_thread t, :inbox }
24
+ regen_text
25
+ end
26
+
27
+ def is_relevant? m; m.has_label? :inbox; end
28
+
29
+ def load_more_threads n=ThreadIndexMode::LOAD_MORE_THREAD_NUM
30
+ load_n_threads_background n, :label => :inbox,
31
+ :load_killed => false,
32
+ :load_spam => false,
33
+ :when_done => lambda { |num|
34
+ BufferManager.flash "Added #{num} threads."
35
+ }
36
+ end
37
+
38
+ def reload
39
+ drop_all_threads
40
+ BufferManager.draw_screen
41
+ load_more_threads buffer.content_height
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,89 @@
1
+ module Redwood
2
+
3
+ class LabelListMode < LineCursorMode
4
+ register_keymap do |k|
5
+ k.add :view_results, "View messages with the selected label", :enter
6
+ k.add :reload, "Reload", "R"
7
+ end
8
+
9
+ def initialize
10
+ @labels = []
11
+ @text = []
12
+ super()
13
+ end
14
+
15
+ def lines; @text.length; end
16
+ def [] i; @text[i]; end
17
+
18
+ def load; regen_text; end
19
+
20
+ def load_in_background
21
+ ::Thread.new do
22
+ regen_text do |i|
23
+ if i % 10 == 0
24
+ buffer.mark_dirty
25
+ BufferManager.draw_screen
26
+ sleep 0.1 # ok, dirty trick.
27
+ end
28
+ end
29
+ buffer.mark_dirty
30
+ BufferManager.draw_screen
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ def reload
37
+ buffer.mark_dirty
38
+ BufferManager.draw_screen
39
+ load_in_background
40
+ end
41
+
42
+ def regen_text
43
+ @text = []
44
+ @labels = LabelManager::LISTABLE_LABELS.sort_by { |t| t.to_s } +
45
+ LabelManager.user_labels.sort_by { |t| t.to_s }
46
+
47
+ counts = @labels.map do |t|
48
+ total = Index.num_results_for :label => t
49
+ unread = Index.num_results_for :labels => [t, :unread]
50
+ [t, total, unread]
51
+ end
52
+
53
+ width = @labels.map { |t| t.to_s.length }.max
54
+
55
+ counts.map_with_index do |(t, total, unread), i|
56
+ if total == 0 && !LabelManager::LISTABLE_LABELS.include?(t)
57
+ Redwood::log "no hits for label #{t}, deleting"
58
+ LabelManager.delete t
59
+ @labels.delete t
60
+ next
61
+ end
62
+
63
+ label =
64
+ case t
65
+ when *LabelManager::LISTABLE_LABELS
66
+ t.to_s.ucfirst
67
+ else
68
+ t.to_s
69
+ end
70
+ @text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color),
71
+ sprintf("%#{width + 1}s %5d %s, %5d unread", label, total, total == 1 ? " message" : "messages", unread)]]
72
+ yield i if block_given?
73
+ end.compact
74
+ end
75
+
76
+ def view_results
77
+ label = @labels[curpos]
78
+ if label == :inbox
79
+ BufferManager.raise_to_front BufferManager["inbox"]
80
+ else
81
+ b = BufferManager.spawn_unless_exists(label) do
82
+ mode = LabelSearchResultsMode.new [label]
83
+ end
84
+ b.mode.load_more_threads b.content_height
85
+ end
86
+ end
87
+ end
88
+
89
+ end
@@ -0,0 +1,29 @@
1
+ module Redwood
2
+
3
+ class LabelSearchResultsMode < ThreadIndexMode
4
+ register_keymap do |k|
5
+ k.add :load_more_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
6
+ end
7
+
8
+ def initialize labels
9
+ @labels = labels
10
+ super
11
+ end
12
+
13
+ def is_relevant? m; @labels.all? { |l| m.has_label? l }; end
14
+
15
+ def load_more_threads n=ThreadIndexMode::LOAD_MORE_THREAD_NUM
16
+ load_n_threads_background n, :labels => @labels,
17
+ :load_killed => true,
18
+ :load_spam => false,
19
+ :when_done =>(lambda do |num|
20
+ if num > 0
21
+ BufferManager.flash "Found #{num} threads"
22
+ else
23
+ BufferManager.flash "No matches"
24
+ end
25
+ end)
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,133 @@
1
+ module Redwood
2
+
3
+ class LineCursorMode < ScrollMode
4
+ register_keymap do |k|
5
+ ## overwrite scrollmode binding on arrow keys for cursor movement
6
+ ## but j and k still scroll!
7
+ k.add :cursor_down, "Move cursor down one line", :down, 'j'
8
+ k.add :cursor_up, "Move cursor up one line", :up, 'k'
9
+ k.add :select, "Select this item", :enter
10
+ end
11
+
12
+ attr_reader :curpos
13
+
14
+ def initialize cursor_top=0, opts={}
15
+ @cursor_top = cursor_top
16
+ @curpos = cursor_top
17
+ super opts
18
+ end
19
+
20
+ def draw
21
+ super
22
+ set_status
23
+ end
24
+
25
+ protected
26
+
27
+ def draw_line ln, opts={}
28
+ if ln == @curpos
29
+ super ln, :highlight => true, :debug => opts[:debug]
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ def ensure_mode_validity
36
+ super
37
+ raise @curpos.inspect unless @curpos.is_a?(Integer)
38
+ c = @curpos.clamp topline, botline - 1
39
+ c = @cursor_top if c < @cursor_top
40
+ buffer.mark_dirty unless c == @curpos
41
+ @curpos = c
42
+ end
43
+
44
+ def set_cursor_pos p
45
+ return if @curpos == p
46
+ @curpos = p.clamp @cursor_top, lines
47
+ buffer.mark_dirty
48
+ end
49
+
50
+ def line_down # overwrite scrollmode
51
+ super
52
+ set_cursor_pos topline if @curpos < topline
53
+ end
54
+
55
+ def line_up # overwrite scrollmode
56
+ super
57
+ set_cursor_pos botline - 1 if @curpos > botline - 1
58
+ end
59
+
60
+ def cursor_down
61
+ return false unless @curpos < lines - 1
62
+ if @curpos >= botline - 1
63
+ page_down
64
+ set_cursor_pos [topline + 1, botline].min
65
+ else
66
+ @curpos += 1
67
+ unless buffer.dirty?
68
+ draw_line @curpos - 1
69
+ draw_line @curpos
70
+ set_status
71
+ buffer.commit
72
+ end
73
+ end
74
+ true
75
+ end
76
+
77
+ def cursor_up
78
+ return false unless @curpos > @cursor_top
79
+ if @curpos == topline
80
+ page_up
81
+ set_cursor_pos [botline - 2, topline].max
82
+ # raise "cursor position now #@curpos, topline #{topline} botline #{botline}"
83
+ else
84
+ @curpos -= 1
85
+ unless buffer.dirty?
86
+ draw_line @curpos + 1
87
+ draw_line @curpos
88
+ set_status
89
+ buffer.commit
90
+ end
91
+ end
92
+ true
93
+ end
94
+
95
+ def page_up # overwrite
96
+ if topline <= @cursor_top
97
+ set_cursor_pos @cursor_top
98
+ else
99
+ relpos = @curpos - topline
100
+ super
101
+ set_cursor_pos topline + relpos
102
+ end
103
+ end
104
+
105
+ def page_down
106
+ if topline >= lines - buffer.content_height
107
+ set_cursor_pos(lines - 1)
108
+ else
109
+ relpos = @curpos - topline
110
+ super
111
+ set_cursor_pos [topline + relpos, lines - 1].min
112
+ end
113
+ end
114
+
115
+ def jump_to_home
116
+ super
117
+ set_cursor_pos @cursor_top
118
+ end
119
+
120
+ def jump_to_end
121
+ super if topline < (lines - buffer.content_height)
122
+ set_cursor_pos(lines - 1)
123
+ end
124
+
125
+ private
126
+
127
+ def set_status
128
+ @status = "line #{@curpos + 1} of #{lines}"
129
+ end
130
+
131
+ end
132
+
133
+ end
@@ -0,0 +1,44 @@
1
+ module Redwood
2
+
3
+ class LogMode < TextMode
4
+ register_keymap do |k|
5
+ k.add :toggle_follow, "Toggle follow mode", 'f'
6
+ end
7
+
8
+ def initialize
9
+ @follow = true
10
+ super ""
11
+ end
12
+
13
+ def toggle_follow
14
+ @follow = !@follow
15
+ if buffer
16
+ if @follow
17
+ jump_to_line lines - buffer.content_height + 1 # leave an empty line at bottom
18
+ end
19
+ buffer.mark_dirty
20
+ end
21
+ end
22
+
23
+ def text= t
24
+ super
25
+ if buffer && @follow
26
+ follow_top = lines - buffer.content_height + 1
27
+ jump_to_line follow_top if topline < follow_top
28
+ end
29
+ end
30
+
31
+ def << line
32
+ super
33
+ if buffer && @follow
34
+ follow_top = lines - buffer.content_height + 1
35
+ jump_to_line follow_top if topline < follow_top
36
+ end
37
+ end
38
+
39
+ def status
40
+ super + " (follow: #@follow)"
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,29 @@
1
+ module Redwood
2
+
3
+ class PersonSearchResultsMode < ThreadIndexMode
4
+ register_keymap do |k|
5
+ k.add :load_more_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
6
+ end
7
+
8
+ def initialize people
9
+ @people = people
10
+ super
11
+ end
12
+
13
+ def is_relevant? m; @people.any? { |p| m.from == p }; end
14
+
15
+ def load_more_threads n=ThreadIndexMode::LOAD_MORE_THREAD_NUM
16
+ load_n_threads_background n, :participants => @people,
17
+ :load_killed => true,
18
+ :load_spam => false,
19
+ :when_done =>(lambda do |num|
20
+ if num > 0
21
+ BufferManager.flash "Found #{num} threads"
22
+ else
23
+ BufferManager.flash "No matches"
24
+ end
25
+ end)
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,24 @@
1
+ module Redwood
2
+
3
+ class PollMode < LogMode
4
+ def initialize
5
+ @new = true
6
+ super
7
+ end
8
+
9
+ def puts s=""
10
+ self << s + "\n"
11
+ # if lines % 5 == 0
12
+ BufferManager.draw_screen
13
+ # end
14
+ end
15
+
16
+ def poll
17
+ puts unless @new
18
+ @new = false
19
+ puts "poll started at #{Time.now}"
20
+ PollManager.poll { |s| puts s }
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,136 @@
1
+ module Redwood
2
+
3
+ class ReplyMode < EditMessageMode
4
+ REPLY_TYPES = [:sender, :list, :all, :user]
5
+ TYPE_DESCRIPTIONS = {
6
+ :sender => "Reply to sender",
7
+ :all => "Reply to all",
8
+ :list => "Reply to mailing list",
9
+ :user => "Customized reply"
10
+ }
11
+
12
+ register_keymap do |k|
13
+ k.add :move_cursor_right, "Move cursor to the right", :right
14
+ k.add :move_cursor_left, "Move cursor to the left", :left
15
+ end
16
+
17
+ def initialize message
18
+ super 2, :twiddles => false
19
+ @m = message
20
+
21
+ from =
22
+ if @m.recipient_email
23
+ AccountManager.account_for(@m.recipient_email)
24
+ else
25
+ (@m.to + @m.cc).argfind { |p| AccountManager.is_account? p }
26
+ end || AccountManager.default_account
27
+
28
+ from_email = @m.recipient_email || from.email
29
+
30
+ ## ignore reply-to for list messages because it's typically set to
31
+ ## the list address anyways
32
+ to = @m.is_list_message? ? @m.from : (@m.replyto || @m.from)
33
+ cc = (@m.to + @m.cc - [from, to]).uniq
34
+
35
+ @headers = {}
36
+ @headers[:sender] = {
37
+ "From" => "#{from.name} <#{from_email}>",
38
+ "To" => [to.full_address],
39
+ }
40
+
41
+ @headers[:user] = {
42
+ "From" => "#{from.name} <#{from_email}>",
43
+ "To" => "",
44
+ }
45
+
46
+ @headers[:all] = {
47
+ "From" => "#{from.name} <#{from_email}>",
48
+ "To" => [to.full_address],
49
+ "Cc" => cc.map { |p| p.full_address },
50
+ } unless cc.empty?
51
+
52
+ @headers[:list] = {
53
+ "From" => "#{from.name} <#{from_email}>",
54
+ "To" => [@m.list_address.full_address],
55
+ } if @m.is_list_message?
56
+
57
+ refs = gen_references
58
+ mid = gen_message_id
59
+ @headers.each do |k, v|
60
+ @headers[k] = v.merge({
61
+ "In-Reply-To" => "<#{@m.id}>",
62
+ "Subject" => Message.reify_subj(@m.subj),
63
+ "Message-Id" => mid,
64
+ "References" => refs,
65
+ })
66
+ end
67
+
68
+ @type_labels = REPLY_TYPES.select { |t| @headers.member?(t) }
69
+ @selected_type = @m.is_list_message? ? :list : :sender
70
+
71
+ @body = reply_body_lines(message) + sig_lines
72
+ regen_text
73
+ end
74
+
75
+ def lines; @text.length + 2; end
76
+ def [] i
77
+ case i
78
+ when 0
79
+ lame = []
80
+ @type_labels.each do |t|
81
+ lame << [(t == @selected_type ? :none_highlight : :none),
82
+ "#{TYPE_DESCRIPTIONS[t]}"]
83
+ lame << [:none, " "]
84
+ end
85
+ lame + [[:none, ""]]
86
+ when 1
87
+ ""
88
+ else
89
+ @text[i - 2]
90
+ end
91
+ end
92
+
93
+ protected
94
+
95
+ def body; @body; end
96
+ def header; @headers[@selected_type]; end
97
+
98
+ def reply_body_lines m
99
+ lines = ["Excerpts from #{@m.from.name}'s message of #{@m.date}:"] +
100
+ m.basic_body_lines.map { |l| "> #{l}" }
101
+ lines.pop while lines.last !~ /[:alpha:]/
102
+ lines
103
+ end
104
+
105
+ def handle_new_text new_header, new_body
106
+ @body = new_body
107
+
108
+ if new_header.size != header.size ||
109
+ header.any? { |k, v| new_header[k] != v }
110
+ @selected_type = :user
111
+ @headers[:user] = new_header
112
+ end
113
+ end
114
+
115
+ def regen_text
116
+ @text = header_lines(@headers[@selected_type] - NON_EDITABLE_HEADERS) + [""] + @body
117
+ end
118
+
119
+ def gen_references
120
+ (@m.refs + [@m.id]).map { |x| "<#{x}>" }.join(" ")
121
+ end
122
+
123
+ def move_cursor_left
124
+ i = @type_labels.index @selected_type
125
+ @selected_type = @type_labels[(i - 1) % @type_labels.length]
126
+ update
127
+ end
128
+
129
+ def move_cursor_right
130
+ i = @type_labels.index @selected_type
131
+ @selected_type = @type_labels[(i + 1) % @type_labels.length]
132
+ update
133
+ end
134
+ end
135
+
136
+ end