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,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