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
@@ -1,22 +1,14 @@
|
|
1
1
|
module Redwood
|
2
2
|
|
3
3
|
class ForwardMode < EditMessageMode
|
4
|
-
attr_reader :body, :header
|
5
|
-
|
6
4
|
def initialize m
|
7
|
-
super
|
8
|
-
@header = {
|
5
|
+
super :header => {
|
9
6
|
"From" => AccountManager.default_account.full_address,
|
10
7
|
"Subject" => "Fwd: #{m.subj}",
|
11
|
-
|
12
|
-
|
13
|
-
@body = forward_body_lines(m) + sig_lines
|
14
|
-
regen_text
|
8
|
+
},
|
9
|
+
:body => forward_body_lines(m)
|
15
10
|
end
|
16
11
|
|
17
|
-
def lines; @text.length; end
|
18
|
-
def [] i; @text[i]; end
|
19
|
-
|
20
12
|
protected
|
21
13
|
|
22
14
|
def forward_body_lines m
|
@@ -24,15 +16,6 @@ protected
|
|
24
16
|
m.basic_header_lines + [""] + m.basic_body_lines +
|
25
17
|
["--- End forwarded message ---"]
|
26
18
|
end
|
27
|
-
|
28
|
-
def handle_new_text new_header, new_body
|
29
|
-
@header = new_header
|
30
|
-
@body = new_body
|
31
|
-
end
|
32
|
-
|
33
|
-
def regen_text
|
34
|
-
@text = header_lines(@header - NON_EDITABLE_HEADERS) + [""] + @body
|
35
|
-
end
|
36
19
|
end
|
37
20
|
|
38
21
|
end
|
data/lib/sup/modes/inbox-mode.rb
CHANGED
@@ -9,12 +9,20 @@ class InboxMode < ThreadIndexMode
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def initialize
|
12
|
-
super [:inbox],
|
12
|
+
super [:inbox, :sent], { :label => :inbox }
|
13
|
+
raise "can't have more than one!" if defined? @@instance
|
14
|
+
@@instance = self
|
13
15
|
end
|
14
16
|
|
17
|
+
def is_relevant? m; m.has_label? :inbox; end
|
18
|
+
|
19
|
+
## label-list-mode wants to be able to raise us if the user selects
|
20
|
+
## the "inbox" label, so we need to keep our singletonness around
|
21
|
+
def self.instance; @@instance; end
|
15
22
|
def killable?; false; end
|
16
23
|
|
17
24
|
def archive
|
25
|
+
return unless cursor_thread
|
18
26
|
cursor_thread.remove_label :inbox
|
19
27
|
hide_thread cursor_thread
|
20
28
|
regen_text
|
@@ -44,17 +52,6 @@ class InboxMode < ThreadIndexMode
|
|
44
52
|
def status
|
45
53
|
super + " #{Index.size} messages in index"
|
46
54
|
end
|
47
|
-
|
48
|
-
def is_relevant? m; m.has_label? :inbox; end
|
49
|
-
|
50
|
-
def load_threads opts={}
|
51
|
-
n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
|
52
|
-
load_n_threads_background n, :label => :inbox,
|
53
|
-
:when_done => (lambda do |num|
|
54
|
-
opts[:when_done].call if opts[:when_done]
|
55
|
-
BufferManager.flash "Added #{num} threads."
|
56
|
-
end)
|
57
|
-
end
|
58
55
|
end
|
59
56
|
|
60
57
|
end
|
@@ -2,79 +2,61 @@ module Redwood
|
|
2
2
|
|
3
3
|
class LabelListMode < LineCursorMode
|
4
4
|
register_keymap do |k|
|
5
|
-
k.add :
|
6
|
-
k.add :reload, "Discard
|
5
|
+
k.add :select_label, "Select label", :enter
|
6
|
+
k.add :reload, "Discard label list and reload", 'D'
|
7
7
|
end
|
8
8
|
|
9
|
+
bool_reader :done
|
10
|
+
attr_reader :value
|
11
|
+
|
9
12
|
def initialize
|
10
13
|
@labels = []
|
11
14
|
@text = []
|
12
|
-
|
15
|
+
@done = false
|
16
|
+
@value = nil
|
17
|
+
super
|
18
|
+
regen_text
|
13
19
|
end
|
14
20
|
|
15
|
-
def lines; @text.length
|
16
|
-
def [] i; @text[i]
|
17
|
-
|
18
|
-
def load_in_background
|
19
|
-
Redwood::reporting_thread do
|
20
|
-
BufferManager.say("Counting labels...") { regen_text }
|
21
|
-
BufferManager.draw_screen
|
22
|
-
end
|
23
|
-
end
|
21
|
+
def lines; @text.length end
|
22
|
+
def [] i; @text[i] end
|
24
23
|
|
25
24
|
protected
|
26
25
|
|
27
26
|
def reload
|
28
|
-
|
29
|
-
|
30
|
-
load_in_background
|
27
|
+
regen_text
|
28
|
+
buffer.mark_dirty if buffer
|
31
29
|
end
|
32
30
|
|
33
31
|
def regen_text
|
34
32
|
@text = []
|
35
|
-
@labels =
|
33
|
+
@labels = LabelManager.listable_label_strings
|
36
34
|
|
37
|
-
counts = @labels.map do |
|
38
|
-
|
39
|
-
|
40
|
-
[
|
35
|
+
counts = @labels.map do |string|
|
36
|
+
label = LabelManager.label_for string
|
37
|
+
total = Index.num_results_for :label => label
|
38
|
+
unread = Index.num_results_for :labels => [label, :unread]
|
39
|
+
[label, string, total, unread]
|
41
40
|
end
|
42
41
|
|
43
|
-
width = @labels.
|
42
|
+
width = @labels.max_of { |string| string.length }
|
44
43
|
|
45
|
-
counts.
|
46
|
-
if total == 0 && !LabelManager::
|
47
|
-
Redwood::log "no hits for label #{
|
48
|
-
LabelManager.delete
|
49
|
-
@labels.delete t
|
44
|
+
counts.map do |label, string, total, unread|
|
45
|
+
if total == 0 && !LabelManager::RESERVED_LABELS.include?(label)
|
46
|
+
Redwood::log "no hits for label #{label}, deleting"
|
47
|
+
LabelManager.delete label
|
50
48
|
next
|
51
49
|
end
|
52
50
|
|
53
|
-
label =
|
54
|
-
case t
|
55
|
-
when *LabelManager::LISTABLE_LABELS
|
56
|
-
t.to_s.ucfirst
|
57
|
-
else
|
58
|
-
t.to_s
|
59
|
-
end
|
60
51
|
@text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color),
|
61
|
-
sprintf("%#{width + 1}s %5d %s, %5d unread",
|
52
|
+
sprintf("%#{width + 1}s %5d %s, %5d unread", string, total, total == 1 ? " message" : "messages", unread)]]
|
62
53
|
yield i if block_given?
|
63
54
|
end.compact
|
64
|
-
|
65
|
-
buffer.mark_dirty
|
66
55
|
end
|
67
56
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
BufferManager.raise_to_front BufferManager["inbox"]
|
72
|
-
else
|
73
|
-
b = BufferManager.spawn_unless_exists(label) do
|
74
|
-
mode = LabelSearchResultsMode.new [label]
|
75
|
-
end
|
76
|
-
b.mode.load_threads :num => b.content_height
|
77
|
-
end
|
57
|
+
def select_label
|
58
|
+
@value, string = @labels[curpos]
|
59
|
+
@done = true if @value
|
78
60
|
end
|
79
61
|
end
|
80
62
|
|
@@ -3,25 +3,10 @@ module Redwood
|
|
3
3
|
class LabelSearchResultsMode < ThreadIndexMode
|
4
4
|
def initialize labels
|
5
5
|
@labels = labels
|
6
|
-
super
|
6
|
+
super [], { :labels => @labels }
|
7
7
|
end
|
8
8
|
|
9
9
|
def is_relevant? m; @labels.all? { |l| m.has_label? l }; end
|
10
|
-
|
11
|
-
def load_threads opts={}
|
12
|
-
n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
|
13
|
-
load_n_threads_background n, :labels => @labels,
|
14
|
-
:load_killed => true,
|
15
|
-
:load_spam => false,
|
16
|
-
:when_done =>(lambda do |num|
|
17
|
-
opts[:when_done].call if opts[:when_done]
|
18
|
-
if num > 0
|
19
|
-
BufferManager.flash "Found #{num} threads"
|
20
|
-
else
|
21
|
-
BufferManager.flash "No matches"
|
22
|
-
end
|
23
|
-
end)
|
24
|
-
end
|
25
10
|
end
|
26
11
|
|
27
12
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
module Redwood
|
2
2
|
|
3
|
+
## extends ScrollMode to have a line-based cursor.
|
3
4
|
class LineCursorMode < ScrollMode
|
4
5
|
register_keymap do |k|
|
5
6
|
## overwrite scrollmode binding on arrow keys for cursor movement
|
@@ -11,9 +12,11 @@ class LineCursorMode < ScrollMode
|
|
11
12
|
|
12
13
|
attr_reader :curpos
|
13
14
|
|
14
|
-
def initialize
|
15
|
-
@cursor_top =
|
16
|
-
@
|
15
|
+
def initialize opts={}
|
16
|
+
@cursor_top = @curpos = opts.delete(:skip_top_rows) || 0
|
17
|
+
@load_more_callbacks = []
|
18
|
+
@load_more_callbacks_m = Mutex.new
|
19
|
+
@load_more_callbacks_active = false
|
17
20
|
super opts
|
18
21
|
end
|
19
22
|
|
@@ -24,6 +27,11 @@ class LineCursorMode < ScrollMode
|
|
24
27
|
|
25
28
|
protected
|
26
29
|
|
30
|
+
## callbacks when the cursor is asked to go beyond the bottom
|
31
|
+
def to_load_more &b
|
32
|
+
@load_more_callbacks << b
|
33
|
+
end
|
34
|
+
|
27
35
|
def draw_line ln, opts={}
|
28
36
|
if ln == @curpos
|
29
37
|
super ln, :highlight => true, :debug => opts[:debug]
|
@@ -49,6 +57,7 @@ protected
|
|
49
57
|
|
50
58
|
def line_down # overwrite scrollmode
|
51
59
|
super
|
60
|
+
call_load_more_callbacks([topline + buffer.content_height - lines, 10].max) if topline + buffer.content_height > lines
|
52
61
|
set_cursor_pos topline if @curpos < topline
|
53
62
|
end
|
54
63
|
|
@@ -58,7 +67,9 @@ protected
|
|
58
67
|
end
|
59
68
|
|
60
69
|
def cursor_down
|
70
|
+
call_load_more_callbacks buffer.content_height if @curpos == lines - 1
|
61
71
|
return false unless @curpos < lines - 1
|
72
|
+
|
62
73
|
if @curpos >= botline - 1
|
63
74
|
page_down
|
64
75
|
set_cursor_pos topline
|
@@ -102,9 +113,21 @@ protected
|
|
102
113
|
end
|
103
114
|
end
|
104
115
|
|
116
|
+
## more complicated than one might think. three behaviors.
|
105
117
|
def page_down
|
106
|
-
if
|
118
|
+
## if we're on the last page, and it's not a full page, just move
|
119
|
+
## the cursor down to the bottom and assume we can't load anything
|
120
|
+
## else via the callbacks.
|
121
|
+
if topline > lines - buffer.content_height
|
107
122
|
set_cursor_pos(lines - 1)
|
123
|
+
|
124
|
+
## if we're on the last page, and it's a full page, try and load
|
125
|
+
## more lines via the callbacks and then shift the page down
|
126
|
+
elsif topline == lines - buffer.content_height
|
127
|
+
call_load_more_callbacks buffer.content_height
|
128
|
+
super
|
129
|
+
|
130
|
+
## otherwise, just move down
|
108
131
|
else
|
109
132
|
relpos = @curpos - topline
|
110
133
|
super
|
@@ -112,7 +135,7 @@ protected
|
|
112
135
|
end
|
113
136
|
end
|
114
137
|
|
115
|
-
def
|
138
|
+
def jump_to_start
|
116
139
|
super
|
117
140
|
set_cursor_pos @cursor_top
|
118
141
|
end
|
@@ -129,6 +152,22 @@ private
|
|
129
152
|
@status = l > 0 ? "line #{@curpos + 1} of #{l}" : ""
|
130
153
|
end
|
131
154
|
|
155
|
+
def call_load_more_callbacks size
|
156
|
+
go =
|
157
|
+
@load_more_callbacks_m.synchronize do
|
158
|
+
if @load_more_callbacks_active
|
159
|
+
false
|
160
|
+
else
|
161
|
+
@load_more_callbacks_active = true
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
return unless go
|
166
|
+
|
167
|
+
@load_more_callbacks.each { |c| c.call size }
|
168
|
+
@load_more_callbacks_active = false
|
169
|
+
end
|
170
|
+
|
132
171
|
end
|
133
172
|
|
134
173
|
end
|
@@ -3,25 +3,10 @@ module Redwood
|
|
3
3
|
class PersonSearchResultsMode < ThreadIndexMode
|
4
4
|
def initialize people
|
5
5
|
@people = people
|
6
|
-
super
|
6
|
+
super [], { :participants => @people }
|
7
7
|
end
|
8
8
|
|
9
9
|
def is_relevant? m; @people.any? { |p| m.from == p }; end
|
10
|
-
|
11
|
-
def load_threads opts={}
|
12
|
-
n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
|
13
|
-
load_n_threads_background n, :participants => @people,
|
14
|
-
:load_killed => true,
|
15
|
-
:load_spam => false,
|
16
|
-
:when_done =>(lambda do |num|
|
17
|
-
opts[:when_done].call if opts[:when_done]
|
18
|
-
if num > 0
|
19
|
-
BufferManager.flash "Found #{num} threads"
|
20
|
-
else
|
21
|
-
BufferManager.flash "No matches"
|
22
|
-
end
|
23
|
-
end)
|
24
|
-
end
|
25
10
|
end
|
26
11
|
|
27
12
|
end
|
data/lib/sup/modes/reply-mode.rb
CHANGED
@@ -16,26 +16,24 @@ class ReplyMode < EditMessageMode
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def initialize message
|
19
|
-
super 2, :twiddles => false
|
20
19
|
@m = message
|
21
20
|
|
22
21
|
## it's important to put this early because it forces a read of
|
23
22
|
## the full headers (most importantly the list-post header, if
|
24
23
|
## any)
|
25
|
-
|
24
|
+
body = reply_body_lines message
|
26
25
|
|
27
26
|
from =
|
28
27
|
if @m.recipient_email
|
29
|
-
AccountManager.account_for
|
28
|
+
AccountManager.account_for @m.recipient_email
|
30
29
|
else
|
31
30
|
(@m.to + @m.cc).find { |p| AccountManager.is_account? p }
|
32
31
|
end || AccountManager.default_account
|
33
32
|
|
34
|
-
#from_email = @m.recipient_email || from.email
|
35
33
|
from_email = from.email
|
36
34
|
|
37
35
|
## ignore reply-to for list messages because it's typically set to
|
38
|
-
## the list address
|
36
|
+
## the list address, which we explicitly treat with :list
|
39
37
|
to = @m.is_list_message? ? @m.from : (@m.replyto || @m.from)
|
40
38
|
cc = (@m.to + @m.cc - [from, to]).uniq
|
41
39
|
|
@@ -57,7 +55,7 @@ class ReplyMode < EditMessageMode
|
|
57
55
|
@headers[:all] = {
|
58
56
|
"From" => "#{from.name} <#{from_email}>",
|
59
57
|
"To" => [to.full_address],
|
60
|
-
"Cc" => cc.map { |p| p.full_address },
|
58
|
+
"Cc" => cc.select { |p| !AccountManager.is_account?(p) }.map { |p| p.full_address },
|
61
59
|
} unless cc.empty?
|
62
60
|
|
63
61
|
@headers[:list] = {
|
@@ -66,7 +64,6 @@ class ReplyMode < EditMessageMode
|
|
66
64
|
} if @m.is_list_message?
|
67
65
|
|
68
66
|
refs = gen_references
|
69
|
-
mid = gen_message_id
|
70
67
|
@headers.each do |k, v|
|
71
68
|
@headers[k] = {
|
72
69
|
"To" => "",
|
@@ -74,7 +71,6 @@ class ReplyMode < EditMessageMode
|
|
74
71
|
"Bcc" => "",
|
75
72
|
"In-Reply-To" => "<#{@m.id}>",
|
76
73
|
"Subject" => Message.reify_subj(@m.subj),
|
77
|
-
"Message-Id" => mid,
|
78
74
|
"References" => refs,
|
79
75
|
}.merge v
|
80
76
|
end
|
@@ -89,33 +85,27 @@ class ReplyMode < EditMessageMode
|
|
89
85
|
:recipient
|
90
86
|
end
|
91
87
|
|
92
|
-
@body
|
93
|
-
|
88
|
+
super :header => @headers[@selected_type], :body => body,
|
89
|
+
:skip_top_rows => 2, :twiddles => false
|
94
90
|
end
|
95
91
|
|
96
|
-
def lines;
|
92
|
+
def lines; super + 2; end
|
97
93
|
def [] i
|
98
94
|
case i
|
99
95
|
when 0
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
lame << [:none, " "]
|
105
|
-
end
|
106
|
-
lame + [[:none, ""]]
|
96
|
+
@type_labels.inject([]) do |array, t|
|
97
|
+
array + [[(t == @selected_type ? :none_highlight : :none),
|
98
|
+
"#{TYPE_DESCRIPTIONS[t]}"], [:none, " "]]
|
99
|
+
end + [[:none, ""]]
|
107
100
|
when 1
|
108
101
|
""
|
109
102
|
else
|
110
|
-
|
103
|
+
super(i - 2)
|
111
104
|
end
|
112
105
|
end
|
113
106
|
|
114
107
|
protected
|
115
108
|
|
116
|
-
def body; @body; end
|
117
|
-
def header; @headers[@selected_type]; end
|
118
|
-
|
119
109
|
def reply_body_lines m
|
120
110
|
lines = ["Excerpts from #{@m.from.name}'s message of #{@m.date}:"] +
|
121
111
|
m.basic_body_lines.map { |l| "> #{l}" }
|
@@ -124,19 +114,14 @@ protected
|
|
124
114
|
end
|
125
115
|
|
126
116
|
def handle_new_text new_header, new_body
|
127
|
-
|
128
|
-
|
129
|
-
if new_header.size != header.size ||
|
130
|
-
header.any? { |k, v| new_header[k] != v }
|
117
|
+
old_header = @headers[@selected_type]
|
118
|
+
if new_header.size != old_header.size || old_header.any? { |k, v| new_header[k] != v }
|
131
119
|
@selected_type = :user
|
132
|
-
@headers[:user] = new_header
|
120
|
+
self.header = @headers[:user] = new_header
|
121
|
+
update
|
133
122
|
end
|
134
123
|
end
|
135
124
|
|
136
|
-
def regen_text
|
137
|
-
@text = header_lines(header - NON_EDITABLE_HEADERS) + [""] + body
|
138
|
-
end
|
139
|
-
|
140
125
|
def gen_references
|
141
126
|
(@m.refs + [@m.id]).map { |x| "<#{x}>" }.join(" ")
|
142
127
|
end
|
@@ -144,12 +129,14 @@ protected
|
|
144
129
|
def move_cursor_left
|
145
130
|
i = @type_labels.index @selected_type
|
146
131
|
@selected_type = @type_labels[(i - 1) % @type_labels.length]
|
132
|
+
self.header = @headers[@selected_type]
|
147
133
|
update
|
148
134
|
end
|
149
135
|
|
150
136
|
def move_cursor_right
|
151
137
|
i = @type_labels.index @selected_type
|
152
138
|
@selected_type = @type_labels[(i + 1) % @type_labels.length]
|
139
|
+
self.header = @headers[@selected_type]
|
153
140
|
update
|
154
141
|
end
|
155
142
|
end
|