sup 0.0.6 → 0.0.7

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.

@@ -9,6 +9,7 @@ class EditMessageMode < LineCursorMode
9
9
  NON_EDITABLE_HEADERS = %w(Message-Id Date)
10
10
 
11
11
  attr_reader :status
12
+ bool_reader :edited
12
13
 
13
14
  register_keymap do |k|
14
15
  k.add :send_message, "Send message", 'y'
@@ -23,7 +24,7 @@ class EditMessageMode < LineCursorMode
23
24
  end
24
25
 
25
26
  def edit
26
- @file = Tempfile.new "redwood.#{self.class.name.camel_to_hyphy}"
27
+ @file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
27
28
  @file.puts header_lines(header - NON_EDITABLE_HEADERS)
28
29
  @file.puts
29
30
  @file.puts body
@@ -41,10 +42,14 @@ class EditMessageMode < LineCursorMode
41
42
  update
42
43
  end
43
44
 
45
+ def killable?
46
+ !edited? || BufferManager.ask_yes_or_no("Discard message?")
47
+ end
48
+
44
49
  protected
45
50
 
46
51
  def gen_message_id
47
- "<#{Time.now.to_i}-redwood-#{rand 10000}@#{Socket.gethostname}>"
52
+ "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
48
53
  end
49
54
 
50
55
  def update
@@ -55,13 +60,13 @@ protected
55
60
  def parse_file fn
56
61
  File.open(fn) do |f|
57
62
  header = MBox::read_header f
58
- body = MBox::read_body f
63
+ body = f.readlines
59
64
 
60
65
  header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
61
66
  header.each do |k, v|
62
67
  next unless MULTI_HEADERS.include?(k) && !v.empty?
63
68
  header[k] = v.split_on_commas.map do |name|
64
- (p = ContactManager.resolve(name)) && p.full_address || name
69
+ (p = ContactManager.person_with(name)) && p.full_address || name
65
70
  end
66
71
  end
67
72
 
@@ -101,7 +106,7 @@ protected
101
106
  end
102
107
 
103
108
  def send_message
104
- return false unless @edited || BufferManager.ask_yes_or_no("Message unedited. Really send?")
109
+ return unless edited? || BufferManager.ask_yes_or_no("Message unedited. Really send?")
105
110
 
106
111
  raise "no message id!" unless header["Message-Id"]
107
112
  date = Time.now
@@ -114,20 +119,18 @@ protected
114
119
 
115
120
  acct = AccountManager.account_for(from_email) || AccountManager.default_account
116
121
  SentManager.write_sent_message(date, from_email) { |f| write_message f, true, date }
117
- BufferManager.flash "sending..."
122
+ BufferManager.flash "Sending..."
118
123
 
119
124
  IO.popen(acct.sendmail, "w") { |p| write_message p, true, date }
120
125
 
121
126
  BufferManager.kill_buffer buffer
122
127
  BufferManager.flash "Message sent!"
123
- true
124
128
  end
125
129
 
126
130
  def save_as_draft
127
131
  DraftManager.write_draft { |f| write_message f, false }
128
132
  BufferManager.kill_buffer buffer
129
133
  BufferManager.flash "Saved for later editing."
130
- true
131
134
  end
132
135
 
133
136
  def sig_lines
@@ -15,7 +15,8 @@ class InboxMode < ThreadIndexMode
15
15
  def killable?; false; end
16
16
 
17
17
  def archive
18
- remove_label_and_hide_thread cursor_thread, :inbox
18
+ cursor_thread.remove_label :inbox
19
+ hide_thread cursor_thread
19
20
  regen_text
20
21
  end
21
22
 
@@ -24,13 +25,28 @@ class InboxMode < ThreadIndexMode
24
25
  regen_text
25
26
  end
26
27
 
28
+ def handle_archived_update sender, t
29
+ if contains_thread? t
30
+ hide_thread t
31
+ regen_text
32
+ end
33
+ end
34
+
35
+ # not quite working, and not sure if i like it anyways
36
+ # def handle_unarchived_update sender, t
37
+ # Redwood::log "unarchived #{t.subj}"
38
+ # show_thread t
39
+ # end
40
+
41
+ def status
42
+ super + " #{Index.size} messages in index"
43
+ end
44
+
27
45
  def is_relevant? m; m.has_label? :inbox; end
28
46
 
29
47
  def load_threads opts={}
30
48
  n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
31
49
  load_n_threads_background n, :label => :inbox,
32
- :load_killed => false,
33
- :load_spam => false,
34
50
  :when_done => (lambda do |num|
35
51
  opts[:when_done].call if opts[:when_done]
36
52
  BufferManager.flash "Added #{num} threads."
@@ -15,18 +15,9 @@ class LabelListMode < LineCursorMode
15
15
  def lines; @text.length; end
16
16
  def [] i; @text[i]; end
17
17
 
18
- def load; regen_text; end
19
-
20
18
  def load_in_background
21
19
  Redwood::reporting_thread 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
20
+ BufferManager.say("Counting labels...") { regen_text }
30
21
  BufferManager.draw_screen
31
22
  end
32
23
  end
@@ -41,8 +32,7 @@ protected
41
32
 
42
33
  def regen_text
43
34
  @text = []
44
- @labels = LabelManager::LISTABLE_LABELS.sort_by { |t| t.to_s } +
45
- LabelManager.user_labels.sort_by { |t| t.to_s }
35
+ @labels = (LabelManager::LISTABLE_LABELS + LabelManager.user_labels).sort_by { |t| t.to_s }
46
36
 
47
37
  counts = @labels.map do |t|
48
38
  total = Index.num_results_for :label => t
@@ -71,6 +61,8 @@ protected
71
61
  sprintf("%#{width + 1}s %5d %s, %5d unread", label, total, total == 1 ? " message" : "messages", unread)]]
72
62
  yield i if block_given?
73
63
  end.compact
64
+
65
+ buffer.mark_dirty
74
66
  end
75
67
 
76
68
  def view_results
@@ -3,6 +3,7 @@ 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'
6
7
  end
7
8
 
8
9
  def initialize
@@ -36,6 +37,11 @@ class LogMode < TextMode
36
37
  end
37
38
  end
38
39
 
40
+ def save_to_disk
41
+ fn = BufferManager.ask :filename, "Save log to file: "
42
+ save_to_file(fn) { |f| f.puts text } if fn
43
+ end
44
+
39
45
  def status
40
46
  super + " (follow: #@follow)"
41
47
  end
@@ -1,9 +1,10 @@
1
1
  module Redwood
2
2
 
3
3
  class ReplyMode < EditMessageMode
4
- REPLY_TYPES = [:sender, :list, :all, :user]
4
+ REPLY_TYPES = [:sender, :recipient, :list, :all, :user]
5
5
  TYPE_DESCRIPTIONS = {
6
6
  :sender => "Reply to sender",
7
+ :recipient => "Reply to recipient",
7
8
  :all => "Reply to all",
8
9
  :list => "Reply to mailing list",
9
10
  :user => "Customized reply"
@@ -30,7 +31,8 @@ class ReplyMode < EditMessageMode
30
31
  (@m.to + @m.cc).find { |p| AccountManager.is_account? p }
31
32
  end || AccountManager.default_account
32
33
 
33
- from_email = @m.recipient_email || from.email
34
+ #from_email = @m.recipient_email || from.email
35
+ from_email = from.email
34
36
 
35
37
  ## ignore reply-to for list messages because it's typically set to
36
38
  ## the list address anyways
@@ -41,7 +43,12 @@ class ReplyMode < EditMessageMode
41
43
  @headers[:sender] = {
42
44
  "From" => "#{from.name} <#{from_email}>",
43
45
  "To" => [to.full_address],
44
- }
46
+ } unless AccountManager.is_account? to
47
+
48
+ @headers[:recipient] = {
49
+ "From" => "#{from.name} <#{from_email}>",
50
+ "To" => cc.map { |p| p.full_address },
51
+ } unless cc.empty? || @m.is_list_message?
45
52
 
46
53
  @headers[:user] = {
47
54
  "From" => "#{from.name} <#{from_email}>",
@@ -73,7 +80,14 @@ class ReplyMode < EditMessageMode
73
80
  end
74
81
 
75
82
  @type_labels = REPLY_TYPES.select { |t| @headers.member?(t) }
76
- @selected_type = @m.is_list_message? ? :list : :sender
83
+ @selected_type =
84
+ if @m.is_list_message?
85
+ :list
86
+ elsif @headers.member? :sender
87
+ :sender
88
+ else
89
+ :recipient
90
+ end
77
91
 
78
92
  @body += sig_lines
79
93
  regen_text
@@ -114,14 +128,13 @@ protected
114
128
 
115
129
  if new_header.size != header.size ||
116
130
  header.any? { |k, v| new_header[k] != v }
117
- #raise "nhs: #{new_header.size} hs: #{header.size} new: #{new_header.inspect} old: #{header.inspect}"
118
131
  @selected_type = :user
119
132
  @headers[:user] = new_header
120
133
  end
121
134
  end
122
135
 
123
136
  def regen_text
124
- @text = header_lines(@headers[@selected_type] - NON_EDITABLE_HEADERS) + [""] + @body
137
+ @text = header_lines(header - NON_EDITABLE_HEADERS) + [""] + body
125
138
  end
126
139
 
127
140
  def gen_references
@@ -12,18 +12,22 @@ class ResumeMode < ComposeMode
12
12
  end
13
13
 
14
14
  def killable?
15
- unless @safe
16
- case BufferManager.ask_yes_or_no "Discard draft?"
17
- when true
15
+ return true if @safe
16
+
17
+ case BufferManager.ask_yes_or_no "Discard draft?"
18
+ when true
19
+ DraftManager.discard @id
20
+ BufferManager.flash "Draft discarded."
21
+ true
22
+ when false
23
+ if edited?
24
+ DraftManager.write_draft { |f| write_message f, false }
18
25
  DraftManager.discard @id
19
- BufferManager.flash "Draft discarded."
20
- true
21
- when false
22
26
  BufferManager.flash "Draft saved."
23
- true
24
- else
25
- false
26
27
  end
28
+ true
29
+ else
30
+ false
27
31
  end
28
32
  end
29
33
 
@@ -1,7 +1,16 @@
1
1
  module Redwood
2
2
 
3
3
  class ScrollMode < Mode
4
- attr_reader :status, :topline, :botline
4
+ ## we define topline and botline as the top and bottom lines of any
5
+ ## content in the currentview.
6
+
7
+ ## we left leftcol and rightcol as the left and right columns of any
8
+ ## content in the current view. but since we're operating in a
9
+ ## line-centric fashion, rightcol is always leftcol + the buffer
10
+ ## width. (whereas botline is topline + at most the buffer height,
11
+ ## and can be == to topline in the case that there's no content.)
12
+
13
+ attr_reader :status, :topline, :botline, :leftcol
5
14
 
6
15
  COL_JUMP = 2
7
16
 
@@ -25,6 +34,8 @@ class ScrollMode < Mode
25
34
  super()
26
35
  end
27
36
 
37
+ def rightcol; @leftcol + buffer.content_width; end
38
+
28
39
  def draw
29
40
  ensure_mode_validity
30
41
  (@topline ... @botline).each { |ln| draw_line ln }
@@ -80,7 +91,7 @@ class ScrollMode < Mode
80
91
  end
81
92
 
82
93
  def resize *a
83
- super *a
94
+ super(*a)
84
95
  ensure_mode_validity
85
96
  end
86
97
 
@@ -1,3 +1,4 @@
1
+ require 'thread'
1
2
  module Redwood
2
3
 
3
4
  ## subclasses should implement load_threads
@@ -16,6 +17,7 @@ class ThreadIndexMode < LineCursorMode
16
17
  k.add :edit_labels, "Edit or add labels for a thread", 'l'
17
18
  k.add :edit_message, "Edit message (drafts only)", 'e'
18
19
  k.add :mark_as_spam, "Mark thread as spam", 'S'
20
+ k.add :delete, "Mark thread for deletion", 'd'
19
21
  k.add :kill, "Kill thread (never to be seen in inbox again)", '&'
20
22
  k.add :save, "Save changes now", '$'
21
23
  k.add :jump_to_next_new, "Jump to next new thread", :tab
@@ -44,6 +46,7 @@ class ThreadIndexMode < LineCursorMode
44
46
 
45
47
  def lines; @text.length; end
46
48
  def [] i; @text[i]; end
49
+ def contains_thread? t; !@lines[t].nil?; end
47
50
 
48
51
  def reload
49
52
  drop_all_threads
@@ -55,11 +58,22 @@ class ThreadIndexMode < LineCursorMode
55
58
  def select t=nil
56
59
  t ||= @threads[curpos]
57
60
 
61
+ ## this isn't working entirely. TODO:figure out why
62
+ # t = t.clone # required so that messages added later on don't completely
63
+ # screw everything up
64
+
58
65
  ## TODO: don't regen text completely
59
66
  Redwood::reporting_thread do
67
+ BufferManager.say("Loading message bodies...") do |sid|
68
+ t.each { |m, *o| m.load_from_source! if m }
69
+ end
60
70
  mode = ThreadViewMode.new t, @hidden_labels
61
71
  BufferManager.spawn t.subj, mode
62
72
  BufferManager.draw_screen
73
+ mode.jump_to_first_open
74
+ BufferManager.draw_screen # lame TODO: make this unnecessary
75
+ ## the first draw_screen is needed before topline and botline
76
+ ## are set, and the second to show the cursor having moved
63
77
  end
64
78
  end
65
79
 
@@ -67,45 +81,49 @@ class ThreadIndexMode < LineCursorMode
67
81
  threads.each { |t| select t }
68
82
  end
69
83
 
70
- def handle_starred_update m
71
- return unless(t = @ts.thread_for m)
72
- @starred_cache[t] = t.has_label? :starred
73
- update_text_for_line @lines[t]
84
+ def handle_starred_update sender, m
85
+ t = @ts.thread_for(m) or return
86
+ l = @lines[t] or return
87
+ update_text_for_line l
88
+ BufferManager.draw_screen
74
89
  end
75
90
 
76
- def handle_read_update m
77
- return unless(t = @ts.thread_for m)
78
- @new_cache[t] = false
91
+ def handle_read_update sender, t
92
+ l = @lines[t] or return
79
93
  update_text_for_line @lines[t]
94
+ BufferManager.draw_screen
80
95
  end
81
96
 
97
+ def handle_archived_update *a; handle_read_update(*a); end
98
+
82
99
  ## overwrite me!
83
100
  def is_relevant? m; false; end
84
101
 
85
- def handle_add_update m
102
+ def handle_add_update sender, m
86
103
  if is_relevant?(m) || @ts.is_relevant?(m)
87
104
  @ts.load_thread_for_message m
88
- @new_cache.delete @ts.thread_for(m) # force recalculation of newness
89
105
  update
106
+ BufferManager.draw_screen
90
107
  end
91
108
  end
92
109
 
93
- def handle_delete_update mid
110
+ def handle_delete_update sender, mid
94
111
  if @ts.contains_id? mid
95
112
  @ts.remove mid
96
113
  update
114
+ BufferManager.draw_screen
97
115
  end
98
116
  end
99
117
 
100
118
  def update
101
119
  ## let's see you do THIS in python
102
- @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| t.date }.reverse
120
+ @threads = @ts.threads.select { |t| !@hidden_threads[t] && !t.has_label?(:killed) }.sort_by { |t| t.date }.reverse
103
121
  @size_width = (@threads.map { |t| t.size }.max || 0).num_digits
104
122
  regen_text
105
123
  end
106
124
 
107
125
  def edit_message
108
- t = @threads[curpos] or return
126
+ return unless(t = @threads[curpos])
109
127
  message, *crap = t.find { |m, *o| m.has_label? :draft }
110
128
  if message
111
129
  mode = ResumeMode.new message
@@ -115,39 +133,56 @@ class ThreadIndexMode < LineCursorMode
115
133
  end
116
134
  end
117
135
 
118
- def toggle_starred
136
+ def actually_toggle_starred t
137
+ if t.has_label? :starred # if ANY message has a star
138
+ t.remove_label :starred # remove from all
139
+ else
140
+ t.first.add_label :starred # add only to first
141
+ end
142
+ end
143
+
144
+ def toggle_starred
119
145
  t = @threads[curpos] or return
120
- @starred_cache[t] = t.toggle_label :starred
146
+ actually_toggle_starred t
121
147
  update_text_for_line curpos
122
148
  cursor_down
123
149
  end
124
150
 
125
151
  def multi_toggle_starred threads
126
- threads.each { |t| @starred_cache[t] = t.toggle_label :starred }
152
+ threads.each { |t| actually_toggle_starred t }
127
153
  regen_text
128
154
  end
129
155
 
130
- def toggle_archived
131
- return unless(t = @threads[curpos])
132
- t.toggle_label :inbox
156
+ def actually_toggle_archived t
157
+ if t.has_label? :inbox
158
+ t.remove_label :inbox
159
+ UpdateManager.relay self, :archived, t
160
+ else
161
+ t.apply_label :inbox
162
+ UpdateManager.relay self, :unarchived, t
163
+ end
164
+ end
165
+
166
+ def toggle_archived
167
+ t = @threads[curpos] or return
168
+ actually_toggle_archived t
133
169
  update_text_for_line curpos
134
- cursor_down
135
170
  end
136
171
 
137
172
  def multi_toggle_archived threads
138
- threads.each { |t| t.toggle_label :inbox }
173
+ threads.each { |t| actually_toggle_archived t }
139
174
  regen_text
140
175
  end
141
176
 
142
177
  def toggle_new
143
178
  t = @threads[curpos] or return
144
- @new_cache[t] = t.toggle_label :unread
179
+ t.toggle_label :unread
145
180
  update_text_for_line curpos
146
181
  cursor_down
147
182
  end
148
183
 
149
184
  def multi_toggle_new threads
150
- threads.each { |t| @new_cache[t] = t.toggle_label :unread }
185
+ threads.each { |t| t.toggle_label :unread }
151
186
  regen_text
152
187
  end
153
188
 
@@ -157,9 +192,8 @@ class ThreadIndexMode < LineCursorMode
157
192
  end
158
193
 
159
194
  def jump_to_next_new
160
- t = @threads[curpos] or return
161
- n = ((curpos + 1) .. lines).find { |i| @new_cache[@threads[i]] }
162
- n = (0 ... curpos).find { |i| @new_cache[@threads[i]] } unless n
195
+ n = ((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread }
196
+ n = (0 ... curpos).find { |i| @threads[i].has_label? :unread } unless n
163
197
  if n
164
198
  set_cursor_pos n
165
199
  else
@@ -180,6 +214,19 @@ class ThreadIndexMode < LineCursorMode
180
214
  regen_text
181
215
  end
182
216
 
217
+ def delete
218
+ t = @threads[curpos] or return
219
+ multi_delete [t]
220
+ end
221
+
222
+ def multi_delete threads
223
+ threads.each do |t|
224
+ t.toggle_label :deleted
225
+ hide_thread t
226
+ end
227
+ regen_text
228
+ end
229
+
183
230
  def kill
184
231
  t = @threads[curpos] or return
185
232
  multi_kill [t]
@@ -232,6 +279,7 @@ class ThreadIndexMode < LineCursorMode
232
279
  speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
233
280
  keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
234
281
  label_string = modifyl.join(" ")
282
+ label_string += " " unless label_string.empty?
235
283
 
236
284
  answer = BufferManager.ask :edit_labels, "edit labels: ", label_string
237
285
  return unless answer
@@ -266,6 +314,7 @@ class ThreadIndexMode < LineCursorMode
266
314
  t = @threads[curpos] or return
267
315
  m = t.latest_message
268
316
  return if m.nil? # probably won't happen
317
+ m.load_from_source!
269
318
  mode = ReplyMode.new m
270
319
  BufferManager.spawn "Reply to #{m.subj}", mode
271
320
  end
@@ -274,6 +323,7 @@ class ThreadIndexMode < LineCursorMode
274
323
  t = @threads[curpos] or return
275
324
  m = t.latest_message
276
325
  return if m.nil? # probably won't happen
326
+ m.load_from_source!
277
327
  mode = ForwardMode.new m
278
328
  BufferManager.spawn "Forward of #{m.subj}", mode
279
329
  mode.edit
@@ -291,11 +341,13 @@ class ThreadIndexMode < LineCursorMode
291
341
  def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
292
342
  @mbid = BufferManager.say "Searching for threads..."
293
343
  orig_size = @ts.size
344
+ last_update = Time.now - 9999 # oh yeah
294
345
  @ts.load_n_threads(@ts.size + n, opts) do |i|
295
346
  BufferManager.say "Loaded #{i} threads...", @mbid
296
- if i % 5 == 0
347
+ if (Time.now - last_update) >= 0.25
297
348
  update
298
349
  BufferManager.draw_screen
350
+ last_update = Time.now
299
351
  end
300
352
  end
301
353
  @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
@@ -325,11 +377,6 @@ protected
325
377
  update
326
378
  end
327
379
 
328
- def remove_label_and_hide_thread t, label
329
- t.remove_label label
330
- hide_thread t
331
- end
332
-
333
380
  def hide_thread t
334
381
  raise "already hidden" if @hidden_threads[t]
335
382
  @hidden_threads[t] = true
@@ -337,6 +384,15 @@ protected
337
384
  @tags.drop_tag_for t
338
385
  end
339
386
 
387
+ def show_thread t
388
+ if @hidden_threads[t]
389
+ @hidden_threads.delete t
390
+ else
391
+ @ts.add_thread t
392
+ end
393
+ update
394
+ end
395
+
340
396
  def update_text_for_line l
341
397
  return unless l # not sure why this happens, but it does, occasionally
342
398
  @text[l] = text_for_thread @threads[l]
@@ -358,18 +414,18 @@ protected
358
414
  end
359
415
 
360
416
  def text_for_thread t
361
- date = (@date_cache[t] ||= t.date.to_nice_s(Time.now))
362
- from = (@who_cache[t] ||= author_text_for_thread(t))
417
+ date = t.date.to_nice_s(Time.now)
418
+ from = author_text_for_thread(t)
363
419
  if from.length > @from_width
364
420
  from = from[0 ... (@from_width - 1)]
365
421
  from += "." unless from[-1] == ?\s
366
422
  end
367
423
 
368
- new = @new_cache.member?(t) ? @new_cache[t] : @new_cache[t] = t.has_label?(:unread)
369
- starred = @starred_cache.member?(t) ? @starred_cache[t] : @starred_cache[t] = t.has_label?(:starred)
424
+ new = t.has_label?(:unread)
425
+ starred = t.has_label?(:starred)
370
426
 
371
- dp = (@dp_cache[t] ||= t.direct_participants.any? { |p| AccountManager.is_account? p })
372
- p = (@p_cache[t] ||= (dp || t.participants.any? { |p| AccountManager.is_account? p }))
427
+ dp = t.direct_participants.any? { |p| AccountManager.is_account? p }
428
+ p = dp || t.participants.any? { |p| AccountManager.is_account? p }
373
429
 
374
430
  base_color = (new ? :index_new_color : :index_old_color)
375
431
  [
@@ -392,12 +448,7 @@ private
392
448
 
393
449
  def initialize_threads
394
450
  @ts = ThreadSet.new Index.instance
395
- @date_cache = {}
396
- @who_cache = {}
397
- @dp_cache = {}
398
- @p_cache = {}
399
- @new_cache = {}
400
- @starred_cache = {}
451
+ @ts_mutex = Mutex.new
401
452
  @hidden_threads = {}
402
453
  end
403
454
  end