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.
- data/History.txt +5 -0
- data/LICENSE +280 -0
- data/Manifest.txt +52 -0
- data/README.txt +119 -0
- data/Rakefile +45 -0
- data/bin/sup +229 -0
- data/bin/sup-import +162 -0
- data/doc/FAQ.txt +38 -0
- data/doc/Philosophy.txt +59 -0
- data/doc/TODO +31 -0
- data/lib/sup.rb +141 -0
- data/lib/sup/account.rb +53 -0
- data/lib/sup/buffer.rb +391 -0
- data/lib/sup/colormap.rb +118 -0
- data/lib/sup/contact.rb +40 -0
- data/lib/sup/draft.rb +105 -0
- data/lib/sup/index.rb +353 -0
- data/lib/sup/keymap.rb +89 -0
- data/lib/sup/label.rb +41 -0
- data/lib/sup/logger.rb +42 -0
- data/lib/sup/mbox.rb +51 -0
- data/lib/sup/mbox/loader.rb +116 -0
- data/lib/sup/message.rb +302 -0
- data/lib/sup/mode.rb +79 -0
- data/lib/sup/modes/buffer-list-mode.rb +37 -0
- data/lib/sup/modes/compose-mode.rb +33 -0
- data/lib/sup/modes/contact-list-mode.rb +121 -0
- data/lib/sup/modes/edit-message-mode.rb +162 -0
- data/lib/sup/modes/forward-mode.rb +38 -0
- data/lib/sup/modes/help-mode.rb +19 -0
- data/lib/sup/modes/inbox-mode.rb +45 -0
- data/lib/sup/modes/label-list-mode.rb +89 -0
- data/lib/sup/modes/label-search-results-mode.rb +29 -0
- data/lib/sup/modes/line-cursor-mode.rb +133 -0
- data/lib/sup/modes/log-mode.rb +44 -0
- data/lib/sup/modes/person-search-results-mode.rb +29 -0
- data/lib/sup/modes/poll-mode.rb +24 -0
- data/lib/sup/modes/reply-mode.rb +136 -0
- data/lib/sup/modes/resume-mode.rb +18 -0
- data/lib/sup/modes/scroll-mode.rb +106 -0
- data/lib/sup/modes/search-results-mode.rb +31 -0
- data/lib/sup/modes/text-mode.rb +51 -0
- data/lib/sup/modes/thread-index-mode.rb +389 -0
- data/lib/sup/modes/thread-view-mode.rb +338 -0
- data/lib/sup/person.rb +120 -0
- data/lib/sup/poll.rb +80 -0
- data/lib/sup/sent.rb +46 -0
- data/lib/sup/tagger.rb +40 -0
- data/lib/sup/textfield.rb +83 -0
- data/lib/sup/thread.rb +358 -0
- data/lib/sup/update.rb +21 -0
- data/lib/sup/util.rb +260 -0
- metadata +123 -0
data/lib/sup/mode.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class Mode
|
4
|
+
attr_accessor :buffer
|
5
|
+
@@keymaps = {}
|
6
|
+
|
7
|
+
def self.register_keymap keymap=nil, &b
|
8
|
+
keymap = Keymap.new(&b) if keymap.nil?
|
9
|
+
@@keymaps[self] = keymap
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@buffer = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.make_name s; s.gsub(/.*::/, "").camel_to_hyphy; end
|
17
|
+
def name; Mode.make_name self.class.name; end
|
18
|
+
|
19
|
+
def self.load_all_modes dir
|
20
|
+
Dir[File.join(dir, "*.rb")].each do |f|
|
21
|
+
$stderr.puts "## loading mode #{f}"
|
22
|
+
require f
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def draw; end
|
27
|
+
def focus; end
|
28
|
+
def blur; end
|
29
|
+
def status; ""; end
|
30
|
+
def resize rows, cols; end
|
31
|
+
def cleanup
|
32
|
+
@buffer = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
## turns an input keystroke into an action symbol
|
36
|
+
def resolve_input c
|
37
|
+
## try all keymaps in order of age
|
38
|
+
action = nil
|
39
|
+
klass = self.class
|
40
|
+
|
41
|
+
ancestors.each do |klass|
|
42
|
+
action = @@keymaps.member?(klass) && @@keymaps[klass].action_for(c)
|
43
|
+
return action if action
|
44
|
+
end
|
45
|
+
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_input c
|
50
|
+
if(action = resolve_input c)
|
51
|
+
send action
|
52
|
+
true
|
53
|
+
else
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def help_text
|
59
|
+
used_keys = {}
|
60
|
+
ancestors.map do |klass|
|
61
|
+
km = @@keymaps[klass] or next
|
62
|
+
title = "Keybindings from #{Mode.make_name klass.name}"
|
63
|
+
s = <<EOS
|
64
|
+
#{title}
|
65
|
+
#{'-' * title.length}
|
66
|
+
|
67
|
+
#{km.help_text used_keys}
|
68
|
+
EOS
|
69
|
+
begin
|
70
|
+
used_keys.merge! km.keysyms.to_boolean_h
|
71
|
+
rescue ArgumentError
|
72
|
+
raise km.keysyms.inspect
|
73
|
+
end
|
74
|
+
s
|
75
|
+
end.compact.join "\n"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class BufferListMode < LineCursorMode
|
4
|
+
register_keymap do |k|
|
5
|
+
k.add :jump_to_buffer, "Jump to that buffer", :enter
|
6
|
+
k.add :reload, "Reload", "R"
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
regen_text
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def lines; @text.length; end
|
15
|
+
def [] i; @text[i]; end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def reload
|
20
|
+
regen_text
|
21
|
+
buffer.mark_dirty
|
22
|
+
end
|
23
|
+
|
24
|
+
def regen_text
|
25
|
+
@bufs = BufferManager.buffers.sort_by { |name, buf| name }
|
26
|
+
width = @bufs.map { |name, buf| name.length }.max
|
27
|
+
@text = @bufs.map do |name, buf|
|
28
|
+
sprintf "%#{width}s %s", name, buf.mode.name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def jump_to_buffer
|
33
|
+
BufferManager.raise_to_front @bufs[curpos][1]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class ComposeMode < EditMessageMode
|
4
|
+
attr_reader :body, :header
|
5
|
+
|
6
|
+
def initialize h={}
|
7
|
+
super()
|
8
|
+
@header = {
|
9
|
+
"From" => AccountManager.default_account.full_address,
|
10
|
+
"Message-Id" => gen_message_id,
|
11
|
+
}
|
12
|
+
|
13
|
+
@header["To"] = [h[:to]].flatten.compact.map { |p| p.full_address }
|
14
|
+
@body = sig_lines
|
15
|
+
regen_text
|
16
|
+
end
|
17
|
+
|
18
|
+
def lines; @text.length; end
|
19
|
+
def [] i; @text[i]; end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def handle_new_text new_header, new_body
|
24
|
+
@header = new_header
|
25
|
+
@body = new_body
|
26
|
+
end
|
27
|
+
|
28
|
+
def regen_text
|
29
|
+
@text = header_lines(@header - EditMessageMode::NON_EDITABLE_HEADERS) + [""] + @body
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class ContactListMode < LineCursorMode
|
4
|
+
LOAD_MORE_CONTACTS_NUM = 10
|
5
|
+
|
6
|
+
register_keymap do |k|
|
7
|
+
k.add :load_more, "Load #{LOAD_MORE_CONTACTS_NUM} more contacts", 'M'
|
8
|
+
k.add :reload, "Reload contacts", 'R'
|
9
|
+
k.add :alias, "Edit alias for contact", 'a'
|
10
|
+
k.add :toggle_tagged, "Tag/untag current line", 't'
|
11
|
+
k.add :apply_to_tagged, "Apply next command to all tagged items", ';'
|
12
|
+
k.add :search, "Search for messages from particular people", 'S'
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize mode = :regular
|
16
|
+
@mode = mode
|
17
|
+
@tags = Tagger.new self
|
18
|
+
reload
|
19
|
+
super()
|
20
|
+
end
|
21
|
+
|
22
|
+
def lines; @text.length; end
|
23
|
+
def [] i; @text[i]; end
|
24
|
+
|
25
|
+
def toggle_tagged
|
26
|
+
p = @contacts[curpos] or return
|
27
|
+
@tags.toggle_tag_for p
|
28
|
+
update_text_for_line curpos
|
29
|
+
cursor_down
|
30
|
+
end
|
31
|
+
|
32
|
+
def multi_toggle_tagged threads
|
33
|
+
@tags.drop_all_tags
|
34
|
+
regen_text
|
35
|
+
end
|
36
|
+
|
37
|
+
def apply_to_tagged; @tags.apply_to_tagged; end
|
38
|
+
|
39
|
+
def load; regen_text; end
|
40
|
+
def load_more
|
41
|
+
@num += LOAD_MORE_CONTACTS_NUM
|
42
|
+
regen_text
|
43
|
+
BufferManager.flash "Loaded #{LOAD_MORE_CONTACTS_NUM} more contacts."
|
44
|
+
end
|
45
|
+
|
46
|
+
def multi_select people
|
47
|
+
case @mode
|
48
|
+
when :regular
|
49
|
+
mode = ComposeMode.new :to => people
|
50
|
+
BufferManager.spawn "new message", mode
|
51
|
+
mode.edit
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def select
|
56
|
+
p = @contacts[curpos] or return
|
57
|
+
multi_select [p]
|
58
|
+
end
|
59
|
+
|
60
|
+
def multi_search people
|
61
|
+
mode = PersonSearchResultsMode.new people
|
62
|
+
BufferManager.spawn "personal search results", mode
|
63
|
+
mode.load_more_threads mode.buffer.content_height
|
64
|
+
end
|
65
|
+
|
66
|
+
def search
|
67
|
+
p = @contacts[curpos] or return
|
68
|
+
multi_search [p]
|
69
|
+
end
|
70
|
+
|
71
|
+
def reload
|
72
|
+
@tags.drop_all_tags
|
73
|
+
@num = LOAD_MORE_CONTACTS_NUM
|
74
|
+
load
|
75
|
+
end
|
76
|
+
|
77
|
+
def alias
|
78
|
+
p = @contacts[curpos] or return
|
79
|
+
a = BufferManager.ask(:alias, "alias for #{p.longname}: ", @user_contacts[p]) or return
|
80
|
+
if a.empty?
|
81
|
+
ContactManager.drop_contact p
|
82
|
+
else
|
83
|
+
ContactManager.set_contact p, a
|
84
|
+
@user_contacts[p] = a
|
85
|
+
update_text_for_line curpos
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
def update_text_for_line line
|
92
|
+
@text[line] = text_for_contact @contacts[line]
|
93
|
+
buffer.mark_dirty
|
94
|
+
end
|
95
|
+
|
96
|
+
def text_for_contact p
|
97
|
+
aalias = @user_contacts[p] || ""
|
98
|
+
[[:tagged_color, @tags.tagged?(p) ? ">" : " "],
|
99
|
+
[:none, sprintf("%-#{@awidth}s %-#{@nwidth}s %s", aalias, p.name, p.email)]]
|
100
|
+
end
|
101
|
+
|
102
|
+
def regen_text
|
103
|
+
@user_contacts = ContactManager.contacts.invert
|
104
|
+
recent = Index.load_contacts AccountManager.user_emails,
|
105
|
+
:num => @num
|
106
|
+
|
107
|
+
@contacts = (@user_contacts.keys + recent.select { |p| !@user_contacts[p] }).sort_by { |p| p.sort_by_me + (p.name || "") + p.email }.remove_successive_dupes
|
108
|
+
|
109
|
+
@awidth, @nwidth = 0, 0
|
110
|
+
@contacts.each do |p|
|
111
|
+
aalias = @user_contacts[p]
|
112
|
+
@awidth = aalias.length if aalias && aalias.length > @awidth
|
113
|
+
@nwidth = p.name.length if p.name && p.name.length > @nwidth
|
114
|
+
end
|
115
|
+
|
116
|
+
@text = @contacts.map { |p| text_for_contact p }
|
117
|
+
buffer.mark_dirty if buffer
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'socket' # just for gethostname!
|
3
|
+
|
4
|
+
module Redwood
|
5
|
+
|
6
|
+
class EditMessageMode < LineCursorMode
|
7
|
+
FORCE_HEADERS = %w(From To Cc Bcc Subject)
|
8
|
+
MULTI_HEADERS = %w(To Cc Bcc)
|
9
|
+
NON_EDITABLE_HEADERS = %w(Message-Id Date)
|
10
|
+
|
11
|
+
attr_reader :status
|
12
|
+
|
13
|
+
register_keymap do |k|
|
14
|
+
k.add :send_message, "Send message", 'y'
|
15
|
+
k.add :edit, "Edit message", 'e', :enter
|
16
|
+
k.add :save_as_draft, "Save as draft", 'P'
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize *a
|
20
|
+
super
|
21
|
+
@attachments = []
|
22
|
+
@edited = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def edit
|
26
|
+
@file = Tempfile.new "redwood.#{self.class.name.camel_to_hyphy}"
|
27
|
+
@file.puts header_lines(header - NON_EDITABLE_HEADERS)
|
28
|
+
@file.puts
|
29
|
+
@file.puts body
|
30
|
+
@file.close
|
31
|
+
|
32
|
+
editor = $config[:editor] || ENV['EDITOR'] || "/usr/bin/vi"
|
33
|
+
|
34
|
+
mtime = File.mtime @file.path
|
35
|
+
BufferManager.shell_out "#{editor} #{@file.path}"
|
36
|
+
@edited = true if File.mtime(@file.path) > mtime
|
37
|
+
|
38
|
+
new_header, new_body = parse_file(@file.path)
|
39
|
+
NON_EDITABLE_HEADERS.each { |h| new_header[h] = header[h] if header[h] }
|
40
|
+
handle_new_text new_header, new_body
|
41
|
+
update
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def gen_message_id
|
47
|
+
"<#{Time.now.to_i}-redwood-#{rand 10000}@#{Socket.gethostname}>"
|
48
|
+
end
|
49
|
+
|
50
|
+
def update
|
51
|
+
regen_text
|
52
|
+
buffer.mark_dirty
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_file fn
|
56
|
+
File.open(fn) do |f|
|
57
|
+
header = MBox::read_header f
|
58
|
+
body = MBox::read_body f
|
59
|
+
|
60
|
+
header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
|
61
|
+
header.each do |k, v|
|
62
|
+
next unless MULTI_HEADERS.include?(k) && !v.empty?
|
63
|
+
header[k] = v.split_on_commas.map do |name|
|
64
|
+
(p = ContactManager.resolve(name)) && p.full_address || name
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
[header, body]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def header_lines header
|
73
|
+
force_headers = FORCE_HEADERS.map { |h| make_lines "#{h}:", header[h] }
|
74
|
+
other_headers = (header.keys - FORCE_HEADERS).map do |h|
|
75
|
+
make_lines "#{h}:", header[h]
|
76
|
+
end
|
77
|
+
|
78
|
+
(force_headers + other_headers).flatten.compact
|
79
|
+
end
|
80
|
+
|
81
|
+
def make_lines header, things
|
82
|
+
case things
|
83
|
+
when nil, []
|
84
|
+
[header + " "]
|
85
|
+
when String
|
86
|
+
[header + " " + things]
|
87
|
+
else
|
88
|
+
if things.empty?
|
89
|
+
[header]
|
90
|
+
else
|
91
|
+
things.map_with_index do |name, i|
|
92
|
+
raise "an array: #{name.inspect} (things #{things.inspect})" if Array === name
|
93
|
+
if i == 0
|
94
|
+
header + " " + name
|
95
|
+
else
|
96
|
+
(" " * (header.length + 1)) + name
|
97
|
+
end + (i == things.length - 1 ? "" : ",")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def send_message
|
104
|
+
return false unless @edited || BufferManager.ask_yes_or_no("message unedited---really send?")
|
105
|
+
|
106
|
+
raise "no message id!" unless header["Message-Id"]
|
107
|
+
date = Time.now
|
108
|
+
from_email =
|
109
|
+
if header["From"] =~ /<?(\S+@(\S+?))>?$/
|
110
|
+
$1
|
111
|
+
else
|
112
|
+
AccountManager.default_account.email
|
113
|
+
end
|
114
|
+
|
115
|
+
sendmail = AccountManager.account_for(from_email).sendmail
|
116
|
+
raise "nil sendmail" unless sendmail
|
117
|
+
SentManager.write_sent_message(date, from_email) { |f| write_message f, true, date }
|
118
|
+
BufferManager.flash "sending..."
|
119
|
+
|
120
|
+
IO.popen(sendmail, "w") { |p| write_message p, true, date }
|
121
|
+
|
122
|
+
BufferManager.kill_buffer buffer
|
123
|
+
BufferManager.flash "Message sent!"
|
124
|
+
true
|
125
|
+
end
|
126
|
+
|
127
|
+
def save_as_draft
|
128
|
+
DraftManager.write_draft { |f| write_message f, false }
|
129
|
+
BufferManager.kill_buffer buffer
|
130
|
+
BufferManager.flash "Saved for later editing."
|
131
|
+
end
|
132
|
+
|
133
|
+
def sig_lines
|
134
|
+
sigfn = (AccountManager.account_for(header["From"]) ||
|
135
|
+
AccountManager.default_account).sig_file
|
136
|
+
|
137
|
+
if sigfn && File.exists?(sigfn)
|
138
|
+
["", "-- "] + File.readlines(sigfn).map { |l| l.chomp }
|
139
|
+
else
|
140
|
+
[]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def write_message f, full_header=true, date=Time.now
|
145
|
+
raise ArgumentError, "no pre-defined date: header allowed" if header["Date"]
|
146
|
+
f.puts header_lines(header)
|
147
|
+
f.puts "Date: #{date.rfc2822}"
|
148
|
+
if full_header
|
149
|
+
f.puts <<EOS
|
150
|
+
Mime-Version: 1.0
|
151
|
+
Content-Type: text/plain; charset=us-ascii
|
152
|
+
Content-Disposition: inline
|
153
|
+
User-Agent: Redwood/#{Redwood::VERSION}
|
154
|
+
EOS
|
155
|
+
end
|
156
|
+
|
157
|
+
f.puts
|
158
|
+
f.puts @body
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class ForwardMode < EditMessageMode
|
4
|
+
attr_reader :body, :header
|
5
|
+
|
6
|
+
def initialize m
|
7
|
+
super()
|
8
|
+
@header = {
|
9
|
+
"From" => AccountManager.default_account.full_address,
|
10
|
+
"Subject" => "Fwd: #{m.subj}",
|
11
|
+
"Message-Id" => gen_message_id,
|
12
|
+
}
|
13
|
+
@body = forward_body_lines(m) + sig_lines
|
14
|
+
regen_text
|
15
|
+
end
|
16
|
+
|
17
|
+
def lines; @text.length; end
|
18
|
+
def [] i; @text[i]; end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def forward_body_lines m
|
23
|
+
["--- Begin forwarded message from #{m.from.mediumname} ---"] +
|
24
|
+
m.basic_header_lines + [""] + m.basic_body_lines +
|
25
|
+
["--- End forwarded message ---"]
|
26
|
+
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
|
+
end
|
37
|
+
|
38
|
+
end
|