sup 0.2 → 0.3

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 (43) hide show
  1. data/History.txt +10 -0
  2. data/bin/sup +50 -68
  3. data/doc/NewUserGuide.txt +11 -7
  4. data/doc/TODO +34 -22
  5. data/lib/sup.rb +30 -24
  6. data/lib/sup/buffer.rb +124 -39
  7. data/lib/sup/colormap.rb +4 -4
  8. data/lib/sup/draft.rb +1 -1
  9. data/lib/sup/hook.rb +18 -5
  10. data/lib/sup/imap.rb +11 -13
  11. data/lib/sup/index.rb +52 -14
  12. data/lib/sup/keymap.rb +1 -1
  13. data/lib/sup/logger.rb +1 -0
  14. data/lib/sup/maildir.rb +9 -0
  15. data/lib/sup/mbox.rb +3 -1
  16. data/lib/sup/message-chunks.rb +21 -7
  17. data/lib/sup/message.rb +31 -15
  18. data/lib/sup/mode.rb +2 -0
  19. data/lib/sup/modes/buffer-list-mode.rb +7 -3
  20. data/lib/sup/modes/compose-mode.rb +14 -16
  21. data/lib/sup/modes/contact-list-mode.rb +2 -2
  22. data/lib/sup/modes/edit-message-mode.rb +55 -23
  23. data/lib/sup/modes/forward-mode.rb +22 -5
  24. data/lib/sup/modes/inbox-mode.rb +3 -7
  25. data/lib/sup/modes/label-list-mode.rb +30 -10
  26. data/lib/sup/modes/label-search-results-mode.rb +12 -0
  27. data/lib/sup/modes/line-cursor-mode.rb +13 -0
  28. data/lib/sup/modes/log-mode.rb +0 -6
  29. data/lib/sup/modes/poll-mode.rb +0 -3
  30. data/lib/sup/modes/reply-mode.rb +19 -11
  31. data/lib/sup/modes/scroll-mode.rb +111 -20
  32. data/lib/sup/modes/search-results-mode.rb +21 -0
  33. data/lib/sup/modes/text-mode.rb +10 -2
  34. data/lib/sup/modes/thread-index-mode.rb +200 -90
  35. data/lib/sup/modes/thread-view-mode.rb +27 -10
  36. data/lib/sup/person.rb +1 -0
  37. data/lib/sup/poll.rb +15 -7
  38. data/lib/sup/source.rb +6 -1
  39. data/lib/sup/suicide.rb +1 -1
  40. data/lib/sup/textfield.rb +14 -14
  41. data/lib/sup/thread.rb +6 -2
  42. data/lib/sup/util.rb +111 -9
  43. metadata +13 -6
@@ -1,19 +1,36 @@
1
1
  module Redwood
2
2
 
3
3
  class ForwardMode < EditMessageMode
4
- def initialize m
5
- super :header => {
4
+
5
+ ## todo: share some of this with reply-mode
6
+ def initialize m, opts={}
7
+ header = {
6
8
  "From" => AccountManager.default_account.full_address,
7
9
  "Subject" => "Fwd: #{m.subj}",
8
- },
9
- :body => forward_body_lines(m)
10
+ }
11
+
12
+ header["To"] = opts[:to].map { |p| p.full_address }.join(", ") if opts[:to]
13
+ header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
14
+ header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
15
+
16
+ super :header => header, :body => forward_body_lines(m)
17
+ end
18
+
19
+ def self.spawn_nicely m, opts={}
20
+ to = opts[:to] || BufferManager.ask_for_contacts(:people, "To: ") or return
21
+ cc = opts[:cc] || BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc]
22
+ bcc = opts[:bcc] || BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc]
23
+
24
+ mode = ForwardMode.new m, :to => to, :cc => cc, :bcc => bcc
25
+ BufferManager.spawn "Forwarding #{m.subj}", mode
26
+ mode.edit_message
10
27
  end
11
28
 
12
29
  protected
13
30
 
14
31
  def forward_body_lines m
15
32
  ["--- Begin forwarded message from #{m.from.mediumname} ---"] +
16
- m.basic_header_lines + [""] + m.basic_body_lines +
33
+ m.quotable_header_lines + [""] + m.quotable_body_lines +
17
34
  ["--- End forwarded message ---"]
18
35
  end
19
36
  end
@@ -14,7 +14,9 @@ class InboxMode < ThreadIndexMode
14
14
  @@instance = self
15
15
  end
16
16
 
17
- def is_relevant? m; m.has_label? :inbox; end
17
+ def is_relevant? m
18
+ m.has_label?(:inbox) && ([:spam, :deleted, :killed] & m.labels).empty?
19
+ end
18
20
 
19
21
  ## label-list-mode wants to be able to raise us if the user selects
20
22
  ## the "inbox" label, so we need to keep our singletonness around
@@ -43,12 +45,6 @@ class InboxMode < ThreadIndexMode
43
45
  end
44
46
  end
45
47
 
46
- # not quite working, and not sure if i like it anyways
47
- # def handle_unarchived_update sender, t
48
- # Redwood::log "unarchived #{t.subj}"
49
- # show_thread t
50
- # end
51
-
52
48
  def status
53
49
  super + " #{Index.size} messages in index"
54
50
  end
@@ -2,18 +2,16 @@ module Redwood
2
2
 
3
3
  class LabelListMode < LineCursorMode
4
4
  register_keymap do |k|
5
- k.add :select_label, "Select label", :enter
5
+ k.add :select_label, "Search by label", :enter
6
6
  k.add :reload, "Discard label list and reload", '@'
7
+ k.add :jump_to_next_new, "Jump to next new thread", :tab
8
+ k.add :toggle_show_unread_only, "Toggle between showing all labels and those with unread mail", 'u'
7
9
  end
8
10
 
9
- bool_reader :done
10
- attr_reader :value
11
-
12
11
  def initialize
13
12
  @labels = []
14
13
  @text = []
15
- @done = false
16
- @value = nil
14
+ @unread_only = false
17
15
  super
18
16
  regen_text
19
17
  end
@@ -21,13 +19,28 @@ class LabelListMode < LineCursorMode
21
19
  def lines; @text.length end
22
20
  def [] i; @text[i] end
23
21
 
22
+ def jump_to_next_new
23
+ n = ((curpos + 1) ... lines).find { |i| @labels[i][1] > 0 } || (0 ... curpos).find { |i| @labels[i][1] > 0 }
24
+ if n
25
+ ## jump there if necessary
26
+ jump_to_line n unless n >= topline && n < botline
27
+ set_cursor_pos n
28
+ else
29
+ BufferManager.flash "No labels messages with unread messages."
30
+ end
31
+ end
24
32
  protected
25
33
 
34
+ def toggle_show_unread_only
35
+ @unread_only = !@unread_only
36
+ reload
37
+ end
38
+
26
39
  def reload
27
40
  regen_text
28
41
  buffer.mark_dirty if buffer
29
42
  end
30
-
43
+
31
44
  def regen_text
32
45
  @text = []
33
46
  labels = LabelManager.listable_labels
@@ -41,6 +54,10 @@ protected
41
54
 
42
55
  width = counts.max_of { |l, s, t, u| s.length }
43
56
 
57
+ if @unread_only
58
+ counts.delete_if { | l, s, t, u | u == 0 }
59
+ end
60
+
44
61
  @labels = []
45
62
  counts.map do |label, string, total, unread|
46
63
  if total == 0 && !LabelManager::RESERVED_LABELS.include?(label)
@@ -51,14 +68,17 @@ protected
51
68
 
52
69
  @text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color),
53
70
  sprintf("%#{width + 1}s %5d %s, %5d unread", string, total, total == 1 ? " message" : "messages", unread)]]
54
- @labels << label
71
+ @labels << [label, unread]
55
72
  yield i if block_given?
56
73
  end.compact
74
+
75
+ BufferManager.flash "No labels with unread messages!" if counts.empty? && @unread_only
57
76
  end
58
77
 
59
78
  def select_label
60
- @value, string = @labels[curpos]
61
- @done = true if @value
79
+ label, num_unread = @labels[curpos]
80
+ return unless label
81
+ LabelSearchResultsMode.spawn_nicely label
62
82
  end
63
83
  end
64
84
 
@@ -10,6 +10,18 @@ class LabelSearchResultsMode < ThreadIndexMode
10
10
  end
11
11
 
12
12
  def is_relevant? m; @labels.all? { |l| m.has_label? l }; end
13
+
14
+ def self.spawn_nicely label
15
+ label = LabelManager.label_for(label) unless label.is_a?(Symbol)
16
+ case label
17
+ when nil
18
+ when :inbox
19
+ BufferManager.raise_to_front InboxMode.instance.buffer
20
+ else
21
+ b, new = BufferManager.spawn_unless_exists("All threads with label '#{label}'") { LabelSearchResultsMode.new [label] }
22
+ b.mode.load_threads :num => b.content_height if new
23
+ end
24
+ end
13
25
  end
14
26
 
15
27
  end
@@ -55,6 +55,19 @@ protected
55
55
  buffer.mark_dirty
56
56
  end
57
57
 
58
+ ## override search behavior to be cursor-based
59
+ def search_goto_line line
60
+ while line > botline
61
+ page_down
62
+ end
63
+ while line < topline
64
+ page_up
65
+ end
66
+ set_cursor_pos line
67
+ end
68
+
69
+ def search_start_line; @curpos end
70
+
58
71
  def line_down # overwrite scrollmode
59
72
  super
60
73
  call_load_more_callbacks([topline + buffer.content_height - lines, 10].max) if topline + buffer.content_height > lines
@@ -3,7 +3,6 @@ module Redwood
3
3
  class LogMode < TextMode
4
4
  register_keymap do |k|
5
5
  k.add :toggle_follow, "Toggle follow mode", 'f'
6
- k.add :save_to_disk, "Save log to disk", 's'
7
6
  end
8
7
 
9
8
  def initialize
@@ -37,11 +36,6 @@ class LogMode < TextMode
37
36
  end
38
37
  end
39
38
 
40
- def save_to_disk
41
- fn = BufferManager.ask_for_filename :filename, "Save log to file: "
42
- save_to_file(fn) { |f| f.puts text } if fn
43
- end
44
-
45
39
  def status
46
40
  super + " (follow: #@follow)"
47
41
  end
@@ -8,9 +8,6 @@ class PollMode < LogMode
8
8
 
9
9
  def puts s=""
10
10
  self << s + "\n"
11
- # if lines % 5 == 0
12
- BufferManager.draw_screen
13
- # end
14
11
  end
15
12
 
16
13
  def poll
@@ -38,13 +38,20 @@ class ReplyMode < EditMessageMode
38
38
  cc = (@m.to + @m.cc - [from, to]).uniq
39
39
 
40
40
  @headers = {}
41
- @headers[:sender] = {
42
- "To" => [to.full_address],
43
- } unless AccountManager.is_account? to
44
41
 
42
+ ## if there's no cc, then the sender is the person you want to reply
43
+ ## to. if it's a list message, then the list address is. otherwise,
44
+ ## the cc contains a recipient.
45
+ useful_recipient = !(cc.empty? || @m.is_list_message?)
46
+
45
47
  @headers[:recipient] = {
46
48
  "To" => cc.map { |p| p.full_address },
47
- } unless cc.empty? || @m.is_list_message?
49
+ } if useful_recipient
50
+
51
+ ## typically we don't want to have a reply-to-sender option if the sender
52
+ ## is a user account. however, if the cc is empty, it's a message to
53
+ ## ourselves, so for the lack of any other options, we'll add it.
54
+ @headers[:sender] = { "To" => [to.full_address], } if !AccountManager.is_account?(to) || !useful_recipient
48
55
 
49
56
  @headers[:user] = {}
50
57
 
@@ -58,6 +65,7 @@ class ReplyMode < EditMessageMode
58
65
  } if @m.is_list_message?
59
66
 
60
67
  refs = gen_references
68
+
61
69
  @headers.each do |k, v|
62
70
  @headers[k] = {
63
71
  "From" => "#{from.name} <#{from.email}>",
@@ -102,8 +110,7 @@ class ReplyMode < EditMessageMode
102
110
  protected
103
111
 
104
112
  def reply_body_lines m
105
- lines = ["Excerpts from #{@m.from.name}'s message of #{@m.date}:"] +
106
- m.basic_body_lines.map { |l| "> #{l}" }
113
+ lines = ["Excerpts from #{@m.from.name}'s message of #{@m.date}:"] + m.quotable_body_lines.map { |l| "> #{l}" }
107
114
  lines.pop while lines.last =~ /^\s*$/
108
115
  lines
109
116
  end
@@ -121,11 +128,12 @@ protected
121
128
  (@m.refs + [@m.id]).map { |x| "<#{x}>" }.join(" ")
122
129
  end
123
130
 
124
- def edit_field
125
- @selected_type = :user
126
- self.header = @headers[:user]
127
- update
128
- super
131
+ def edit_field field
132
+ edited_field = super
133
+ if edited_field && edited_field != "Subject"
134
+ @selected_type = :user
135
+ update
136
+ end
129
137
  end
130
138
 
131
139
  def move_cursor_left
@@ -24,6 +24,8 @@ class ScrollMode < Mode
24
24
  k.add :jump_to_start, "Jump to top", :home, '^', '1'
25
25
  k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
26
26
  k.add :jump_to_left, "Jump to the left", '['
27
+ k.add :search_in_buffer, "Search in current buffer", '/'
28
+ k.add :continue_search_in_buffer, "Jump to next search occurrence in buffer", BufferManager::CONTINUE_IN_BUFFER_SEARCH_KEY
27
29
  end
28
30
 
29
31
  def initialize opts={}
@@ -31,6 +33,9 @@ class ScrollMode < Mode
31
33
  @slip_rows = opts[:slip_rows] || 0 # when we pgup/pgdown,
32
34
  # how many lines do we keep?
33
35
  @twiddles = opts.member?(:twiddles) ? opts[:twiddles] : true
36
+ @search_query = nil
37
+ @search_line = nil
38
+ @status = ""
34
39
  super()
35
40
  end
36
41
 
@@ -49,6 +54,41 @@ class ScrollMode < Mode
49
54
  @status = "lines #{@topline + 1}:#{@botline}/#{lines}"
50
55
  end
51
56
 
57
+ def in_search?; @search_line end
58
+ def cancel_search!; @search_line = nil end
59
+
60
+ def continue_search_in_buffer
61
+ unless @search_query
62
+ BufferManager.flash "No current search!"
63
+ return
64
+ end
65
+
66
+ start = @search_line || search_start_line
67
+ line = find_text @search_query, start
68
+ if line.nil? && (start > 0)
69
+ line = find_text @search_query, 0
70
+ BufferManager.flash "Search wrapped to top!" if line
71
+ end
72
+ if line
73
+ @search_line = line + 1
74
+ search_goto_line line
75
+ buffer.mark_dirty
76
+ else
77
+ BufferManager.flash "Not found!"
78
+ end
79
+ end
80
+
81
+ def search_in_buffer
82
+ query = BufferManager.ask :search, "search in buffer: "
83
+ return if query.nil? || query.empty?
84
+ @search_query = Regexp.escape query
85
+ continue_search_in_buffer
86
+ end
87
+
88
+ ## subclasses can override these two!
89
+ def search_goto_line line; jump_to_line line end
90
+ def search_start_line; @topline end
91
+
52
92
  def col_left
53
93
  return unless @leftcol > 0
54
94
  @leftcol -= COL_JUMP
@@ -98,40 +138,91 @@ class ScrollMode < Mode
98
138
 
99
139
  protected
100
140
 
141
+ def find_text query, start_line
142
+ regex = /#{query}/i
143
+ (start_line ... lines).each do |i|
144
+ case(s = self[i])
145
+ when String
146
+ return i if s =~ regex
147
+ when Array
148
+ return i if s.any? { |color, string| string =~ regex }
149
+ end
150
+ end
151
+ nil
152
+ end
153
+
101
154
  def draw_line ln, opts={}
155
+ regex = /(#{@search_query})/i
102
156
  case(s = self[ln])
103
157
  when String
104
- buffer.write ln - @topline, 0, s[@leftcol .. -1],
105
- :highlight => opts[:highlight]
158
+ if in_search?
159
+ draw_line_from_array ln, matching_text_array(s, regex), opts
160
+ else
161
+ draw_line_from_string ln, s, opts
162
+ end
106
163
  when Array
107
- xpos = 0
164
+ if in_search?
165
+ ## seems like there ought to be a better way of doing this
166
+ array = []
167
+ s.each do |color, text|
168
+ if text =~ regex
169
+ array += matching_text_array text, regex, color
170
+ else
171
+ array << [color, text]
172
+ end
173
+ end
174
+ draw_line_from_array ln, array, opts
175
+ else
176
+ draw_line_from_array ln, s, opts
177
+ end
178
+ else
179
+ raise "unknown drawable object: #{s.inspect}" # good for debugging
180
+ end
108
181
 
109
182
  ## speed test
110
183
  # str = s.map { |color, text| text }.join
111
184
  # buffer.write ln - @topline, 0, str, :color => :none, :highlight => opts[:highlight]
112
185
  # return
186
+ end
113
187
 
114
- s.each do |color, text|
115
- raise "nil text for color '#{color}'" if text.nil? # good for debugging
116
- if xpos + text.length < @leftcol
117
- buffer.write ln - @topline, 0, "", :color => color,
118
- :highlight => opts[:highlight]
119
- xpos += text.length
120
- elsif xpos < @leftcol
121
- ## partial
122
- buffer.write ln - @topline, 0, text[(@leftcol - xpos) .. -1],
123
- :color => color,
124
- :highlight => opts[:highlight]
125
- xpos += text.length
126
- else
127
- buffer.write ln - @topline, xpos - @leftcol, text,
128
- :color => color, :highlight => opts[:highlight]
129
- xpos += text.length
130
- end
188
+ def matching_text_array s, regex, oldcolor=:none
189
+ s.split(regex).map do |text|
190
+ next if text.empty?
191
+ if text =~ regex
192
+ [:search_highlight_color, text]
193
+ else
194
+ [oldcolor, text]
195
+ end
196
+ end.compact + [[oldcolor, ""]]
197
+ end
131
198
 
199
+ def draw_line_from_array ln, a, opts
200
+ xpos = 0
201
+ a.each do |color, text|
202
+ raise "nil text for color '#{color}'" if text.nil? # good for debugging
203
+
204
+ if xpos + text.length < @leftcol
205
+ buffer.write ln - @topline, 0, "", :color => color,
206
+ :highlight => opts[:highlight]
207
+ xpos += text.length
208
+ elsif xpos < @leftcol
209
+ ## partial
210
+ buffer.write ln - @topline, 0, text[(@leftcol - xpos) .. -1],
211
+ :color => color,
212
+ :highlight => opts[:highlight]
213
+ xpos += text.length
214
+ else
215
+ buffer.write ln - @topline, xpos - @leftcol, text,
216
+ :color => color, :highlight => opts[:highlight]
217
+ xpos += text.length
132
218
  end
133
219
  end
134
220
  end
221
+
222
+ def draw_line_from_string ln, s, opts
223
+ buffer.write ln - @topline, 0, s[@leftcol .. -1], :highlight => opts[:highlight]
224
+ end
135
225
  end
136
226
 
137
227
  end
228
+