sup 0.11 → 0.12

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.

@@ -66,6 +66,7 @@ protected
66
66
  return if @curpos == p
67
67
  @curpos = p.clamp @cursor_top, lines
68
68
  buffer.mark_dirty
69
+ set_status
69
70
  end
70
71
 
71
72
  ## override search behavior to be cursor-based. this is a stupid
@@ -42,6 +42,7 @@ EOS
42
42
 
43
43
  def initialize message, type_arg=nil
44
44
  @m = message
45
+ @edited = false
45
46
 
46
47
  ## it's important to put this early because it forces a read of
47
48
  ## the full headers (most importantly the list-post header, if
@@ -64,18 +65,15 @@ EOS
64
65
  ## if we have a value from a hook, use it.
65
66
  from = if hook_reply_from
66
67
  hook_reply_from
67
- ## otherwise, if the original email had an envelope-to header, try and use
68
- ## it, and look up the corresponding name form the list of accounts.
69
- ##
68
+ ## otherwise, try and find an account somewhere in the list of to's
69
+ ## and cc's and look up the corresponding name form the list of accounts.
70
+ ## if this does not succeed use the recipient_email (=envelope-to) instead.
70
71
  ## this is for the case where mail is received from a mailing lists (so the
71
72
  ## To: is the list id itself). if the user subscribes via a particular
72
73
  ## alias, we want to use that alias in the reply.
73
- elsif @m.recipient_email && (a = AccountManager.account_for(@m.recipient_email))
74
- Person.new a.name, @m.recipient_email
75
- ## otherwise, try and find an account somewhere in the list of to's
76
- ## and cc's.
77
- elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
78
- b
74
+ elsif(b = (@m.to.collect {|t| t.email} + @m.cc.collect {|c| c.email} + [@m.recipient_email] ).find { |p| AccountManager.is_account_email? p })
75
+ a = AccountManager.account_for(b)
76
+ Person.new a.name, b
79
77
  ## if all else fails, use the default
80
78
  else
81
79
  AccountManager.default_account
@@ -150,11 +148,13 @@ EOS
150
148
  :recipient
151
149
  end)
152
150
 
151
+ @bodies = {}
153
152
  @headers.each do |k, v|
154
- HookManager.run "before-edit", :header => v, :body => body
153
+ @bodies[k] = body
154
+ HookManager.run "before-edit", :header => v, :body => @bodies[k]
155
155
  end
156
156
 
157
- super :header => @headers[@type_selector.val], :body => body, :twiddles => false
157
+ super :header => @headers[@type_selector.val], :body => @bodies[@type_selector.val], :twiddles => false
158
158
  add_selector @type_selector
159
159
  end
160
160
 
@@ -164,6 +164,7 @@ protected
164
164
  super
165
165
  if @headers[@type_selector.val] != self.header
166
166
  self.header = @headers[@type_selector.val]
167
+ self.body = @bodies[@type_selector.val] unless @edited
167
168
  update
168
169
  end
169
170
  end
@@ -172,6 +173,7 @@ protected
172
173
  super
173
174
  if @headers[@type_selector.val] != self.header
174
175
  self.header = @headers[@type_selector.val]
176
+ self.body = @bodies[@type_selector.val] unless @edited
175
177
  update
176
178
  end
177
179
  end
@@ -188,6 +190,10 @@ protected
188
190
  end
189
191
 
190
192
  def handle_new_text new_header, new_body
193
+ if new_body != @bodies[@type_selector.val]
194
+ @bodies[@type_selector.val] = new_body
195
+ @edited = true
196
+ end
191
197
  old_header = @headers[@type_selector.val]
192
198
  if new_header.size != old_header.size || old_header.any? { |k, v| new_header[k] != v }
193
199
  @type_selector.set_to :user
@@ -231,7 +231,7 @@ EOS
231
231
  old_cursor_thread = cursor_thread
232
232
  @mutex.synchronize do
233
233
  ## let's see you do THIS in python
234
- @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| [t.date, t.first.id] }.reverse
234
+ @threads = @ts.threads.select { |t| !@hidden_threads.member?(t) }.select(&:has_message?).sort_by(&:sort_key)
235
235
  @size_widgets = @threads.map { |t| size_widget_for_thread t }
236
236
  @size_widget_width = @size_widgets.max_of { |w| w.display_length }
237
237
  @date_widgets = @threads.map { |t| date_widget_for_thread t }
@@ -792,8 +792,8 @@ protected
792
792
  authors = t.map do |m, *o|
793
793
  next unless m && m.from
794
794
  new[m.from] ||= m.has_label?(:unread)
795
- next if seen[m.from]
796
- seen[m.from] = true
795
+ next if seen[m.from.mediumname]
796
+ seen[m.from.mediumname] = true
797
797
  m.from
798
798
  end.compact
799
799
 
@@ -876,15 +876,16 @@ protected
876
876
  :index_old_color
877
877
  end
878
878
 
879
- snippet = t.snippet + (t.snippet.empty? ? "" : "...")
879
+ size_padding = @size_widget_width - size_widget.display_length
880
+ size_widget_text = sprintf "%#{size_padding}s%s", "", size_widget
880
881
 
881
- size_widget_text = sprintf "%#{ @size_widget_width}s", size_widget
882
- date_widget_text = sprintf "%#{ @date_widget_width}s", date_widget
882
+ date_padding = @date_widget_width - date_widget.display_length
883
+ date_widget_text = sprintf "%#{date_padding}s%s", "", date_widget
883
884
 
884
885
  [
885
886
  [:tagged_color, @tags.tagged?(t) ? ">" : " "],
886
887
  [:date_color, date_widget_text],
887
- (starred ? [:starred_color, "*"] : [:none, " "]),
888
+ [:starred_color, (starred ? "*" : " ")],
888
889
  ] +
889
890
  from +
890
891
  [
@@ -895,7 +896,7 @@ protected
895
896
  (t.labels - @hidden_labels).map { |label| [:label_color, "#{label} "] } +
896
897
  [
897
898
  [subj_color, t.subj + (t.subj.empty? ? "" : " ")],
898
- [:snippet_color, snippet],
899
+ [:snippet_color, t.snippet],
899
900
  ]
900
901
  end
901
902
 
@@ -913,7 +914,7 @@ private
913
914
  end
914
915
 
915
916
  def default_date_widget_for t
916
- t.date.to_nice_s
917
+ t.date.getlocal.to_nice_s
917
918
  end
918
919
 
919
920
  def from_width
@@ -3,7 +3,7 @@ module Redwood
3
3
  class ThreadViewMode < LineCursorMode
4
4
  ## this holds all info we need to lay out a message
5
5
  class MessageLayout
6
- attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new
6
+ attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new, :toggled_state
7
7
  end
8
8
 
9
9
  class ChunkLayout
@@ -54,7 +54,9 @@ EOS
54
54
  k.add :edit_labels, "Edit or add labels for a thread", 'l'
55
55
  k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
56
56
  k.add :jump_to_next_open, "Jump to next open message", 'n'
57
+ k.add :jump_to_next_and_open, "Jump to next message and open", "\C-n"
57
58
  k.add :jump_to_prev_open, "Jump to previous open message", 'p'
59
+ k.add :jump_to_prev_and_open, "Jump to previous message and open", "\C-p"
58
60
  k.add :align_current_message, "Align current message in buffer", 'z'
59
61
  k.add :toggle_starred, "Star or unstar message", '*'
60
62
  k.add :toggle_new, "Toggle unread/read status of message", 'N'
@@ -111,7 +113,7 @@ EOS
111
113
  ## objects. @person_lines is a map from row #s to Person objects.
112
114
 
113
115
  def initialize thread, hidden_labels=[], index_mode=nil
114
- super()
116
+ super :slip_rows => $config[:slip_rows]
115
117
  @thread = thread
116
118
  @hidden_labels = hidden_labels
117
119
 
@@ -129,6 +131,7 @@ EOS
129
131
  next unless m
130
132
  earliest ||= m
131
133
  @layout[m].state = initial_state_for m
134
+ @layout[m].toggled_state = false
132
135
  @layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
133
136
  @layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color
134
137
  @layout[m].orig_new = m.has_label? :read
@@ -440,6 +443,29 @@ EOS
440
443
  end
441
444
  end
442
445
 
446
+ def jump_to_next_and_open
447
+ return continue_search_in_buffer if in_search? # err.. don't know why im doing this
448
+
449
+ m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
450
+ return unless m
451
+
452
+ if @layout[m].toggled_state == true
453
+ @layout[m].state = :closed
454
+ @layout[m].toggled_state = false
455
+ update
456
+ end
457
+
458
+ nextm = @layout[m].next
459
+ if @layout[nextm].state == :closed
460
+ @layout[nextm].state = :open
461
+ @layout[nextm].toggled_state = true
462
+ end
463
+
464
+ jump_to_message nextm if nextm
465
+
466
+ update if @layout[nextm].toggled_state
467
+ end
468
+
443
469
  def jump_to_next_open force_alignment=nil
444
470
  return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations
445
471
  m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
@@ -456,6 +482,26 @@ EOS
456
482
  jump_to_message m, true
457
483
  end
458
484
 
485
+ def jump_to_prev_and_open force_alignment=nil
486
+ m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] }
487
+ return unless m
488
+
489
+ if @layout[m].toggled_state == true
490
+ @layout[m].state = :closed
491
+ @layout[m].toggled_state = false
492
+ update
493
+ end
494
+
495
+ nextm = @layout[m].prev
496
+ if @layout[nextm].state == :closed
497
+ @layout[nextm].state = :open
498
+ @layout[nextm].toggled_state = true
499
+ end
500
+
501
+ jump_to_message nextm if nextm
502
+ update if @layout[nextm].toggled_state
503
+ end
504
+
459
505
  def jump_to_prev_open
460
506
  m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] } # bah, .to_a
461
507
  return unless m
@@ -28,9 +28,8 @@ num_inbox_total_unread: the total number of unread messages in the inbox
28
28
  only those messages appearing in the inbox
29
29
  EOS
30
30
 
31
- DELAY = $config[:poll_interval] || 300
32
-
33
31
  def initialize
32
+ @delay = $config[:poll_interval] || 300
34
33
  @mutex = Mutex.new
35
34
  @thread = nil
36
35
  @last_poll = nil
@@ -83,8 +82,8 @@ EOS
83
82
  def start
84
83
  @thread = Redwood::reporting_thread("periodic poll") do
85
84
  while true
86
- sleep DELAY / 2
87
- poll if @last_poll.nil? || (Time.now - @last_poll) >= DELAY
85
+ sleep @delay / 2
86
+ poll if @last_poll.nil? || (Time.now - @last_poll) >= @delay
88
87
  end
89
88
  end
90
89
  end
@@ -102,43 +101,37 @@ EOS
102
101
 
103
102
  @mutex.synchronize do
104
103
  @poll_sources.each do |source|
105
- # yield "source #{source} is done? #{source.done?} (cur_offset #{source.cur_offset} >= #{source.end_offset})"
106
104
  begin
107
- yield "Loading from #{source}... " unless source.done? || (source.respond_to?(:has_errors?) && source.has_errors?)
105
+ yield "Loading from #{source}... "
108
106
  rescue SourceError => e
109
107
  warn "problem getting messages from #{source}: #{e.message}"
110
- Redwood::report_broken_sources :force_to_top => true
111
108
  next
112
109
  end
113
110
 
114
111
  num = 0
115
112
  numi = 0
116
- each_message_from source do |m|
117
- old_m = Index.build_message m.id
118
- if old_m
119
- if old_m.source.id != source.id || old_m.source_info != m.source_info
120
- ## here we merge labels between new and old versions, but we don't let the new
121
- ## message add :unread or :inbox labels. (they can exist in the old version,
122
- ## just not be added.)
123
- new_labels = old_m.labels + (m.labels - [:unread, :inbox])
124
- yield "Message at #{m.source_info} is an updated of an old message. Updating labels from #{m.labels.to_a * ','} => #{new_labels.to_a * ','}"
125
- m.labels = new_labels
126
- Index.update_message m
113
+ poll_from source do |action,m,old_m,progress|
114
+ if action == :delete
115
+ yield "Deleting #{m.id}"
116
+ elsif action == :add
117
+ if old_m
118
+ if not old_m.locations.member? m.location
119
+ yield "Message at #{m.source_info} is an updated of an old message. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
120
+ else
121
+ yield "Skipping already-imported message at #{m.source_info}"
122
+ end
127
123
  else
128
- yield "Skipping already-imported message at #{m.source_info}"
129
- end
130
- else
131
- yield "Found new message at #{m.source_info} with labels #{m.labels.to_a * ','}"
132
- add_new_message m
133
- loaded_labels.merge m.labels
134
- num += 1
135
- from_and_subj << [m.from && m.from.longname, m.subj]
136
- if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
137
- from_and_subj_inbox << [m.from && m.from.longname, m.subj]
138
- numi += 1
124
+ yield "Found new message at #{m.source_info} with labels #{m.labels.to_a * ','}"
125
+ loaded_labels.merge m.labels
126
+ num += 1
127
+ from_and_subj << [m.from && m.from.longname, m.subj]
128
+ if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
129
+ from_and_subj_inbox << [m.from && m.from.longname, m.subj]
130
+ numi += 1
131
+ end
139
132
  end
133
+ else fail
140
134
  end
141
- m
142
135
  end
143
136
  yield "Found #{num} messages, #{numi} to inbox." unless num == 0
144
137
  total_num += num
@@ -153,44 +146,47 @@ EOS
153
146
  [total_num, total_numi, from_and_subj, from_and_subj_inbox, loaded_labels]
154
147
  end
155
148
 
156
- ## like Source#each, but yields successive Message objects, which have their
157
- ## labels and offsets set correctly.
158
- ##
159
- ## this is the primary mechanism for iterating over messages from a source.
160
- def each_message_from source, opts={}
149
+ ## like Source#poll, but yields successive Message objects, which have their
150
+ ## labels and locations set correctly. The Messages are saved to or removed
151
+ ## from the index after being yielded.
152
+ def poll_from source, opts={}
161
153
  begin
162
- return if source.done? || source.has_errors?
163
-
164
- source.each do |offset, source_labels|
165
- if source.has_errors?
166
- warn "error loading messages from #{source}: #{source.error.message}"
167
- return
154
+ source.poll do |sym, args|
155
+ case sym
156
+ when :add
157
+ m = Message.build_from_source source, args[:info]
158
+ old_m = Index.build_message m.id
159
+ m.labels += args[:labels]
160
+ m.labels.delete :inbox if source.archived?
161
+ m.labels.delete :unread if source.read?
162
+ m.labels.delete :unread if m.source_marked_read? # preserve read status if possible
163
+ m.labels.each { |l| LabelManager << l }
164
+ m.labels = old_m.labels + (m.labels - [:unread, :inbox]) if old_m
165
+ m.locations = old_m.locations + m.locations if old_m
166
+ HookManager.run "before-add-message", :message => m
167
+ yield :add, m, old_m, args[:progress] if block_given?
168
+ Index.sync_message m, true
169
+
170
+ ## We need to add or unhide the message when it either did not exist
171
+ ## before at all or when it was updated. We do *not* add/unhide when
172
+ ## the same message was found at a different location
173
+ if !old_m or not old_m.locations.member? m.location
174
+ UpdateManager.relay self, :added, m
175
+ end
176
+ when :delete
177
+ Index.each_message :location => [source.id, args[:info]] do |m|
178
+ m.locations.delete Location.new(source, args[:info])
179
+ yield :delete, m, [source,args[:info]], args[:progress] if block_given?
180
+ Index.sync_message m, false
181
+ #UpdateManager.relay self, :deleted, m
182
+ end
168
183
  end
169
-
170
- m = Message.build_from_source source, offset
171
- m.labels += source_labels + (source.archived? ? [] : [:inbox])
172
- m.labels.delete :unread if m.source_marked_read? # preserve read status if possible
173
- m.labels.each { |l| LabelManager << l }
174
-
175
- HookManager.run "before-add-message", :message => m
176
- yield m
177
184
  end
178
185
  rescue SourceError => e
179
186
  warn "problem getting messages from #{source}: #{e.message}"
180
- Redwood::report_broken_sources :force_to_top => true
181
187
  end
182
188
  end
183
189
 
184
- ## TODO: see if we can do this within PollMode rather than by calling this
185
- ## method.
186
- ##
187
- ## a wrapper around Index.add_message that calls the proper hooks,
188
- ## does the gui callback stuff, etc.
189
- def add_new_message m
190
- Index.add_message m
191
- UpdateManager.relay self, :added, m
192
- end
193
-
194
190
  def handle_idle_update sender, idle_since; @should_clear_running_totals = false; end
195
191
  def handle_unidle_update sender, idle_since; @should_clear_running_totals = true; clear_running_totals; end
196
192
  def clear_running_totals; @running_totals = {:num => 0, :numi => 0, :loaded_labels => Set.new}; end
@@ -0,0 +1,161 @@
1
+ require 'eventmachine'
2
+ require 'socket'
3
+ require 'stringio'
4
+ require 'yajl'
5
+
6
+ class EM::P::Redwood < EM::Connection
7
+ VERSION = 1
8
+ ENCODINGS = %w(marshal json)
9
+
10
+ attr_reader :debug
11
+
12
+ def initialize *args
13
+ @state = :negotiating
14
+ @version_buf = ""
15
+ @debug = false
16
+ super
17
+ end
18
+
19
+ def receive_data data
20
+ if @state == :negotiating
21
+ @version_buf << data
22
+ if i = @version_buf.index("\n")
23
+ l = @version_buf.slice!(0..i)
24
+ receive_version *parse_version(l.strip)
25
+ x = @version_buf
26
+ @version_buf = nil
27
+ @state = :established
28
+ connection_established
29
+ receive_data x
30
+ end
31
+ else
32
+ @filter.decode(data).each do |msg|
33
+ puts "#{self.class.name} received: #{msg.inspect}" if @debug
34
+ validate_message *msg
35
+ receive_message *msg
36
+ end
37
+ end
38
+ end
39
+
40
+ def connection_established
41
+ end
42
+
43
+ def send_version encodings, extensions
44
+ fail if encodings.empty?
45
+ send_data "Redwood #{VERSION} #{encodings * ','} #{extensions.empty? ? :none : (extensions * ',')}\n"
46
+ end
47
+
48
+ def send_message type, tag, params={}
49
+ fail "attempted to send message during negotiation" unless @state == :established
50
+ puts "#{self.class.name} sent: #{[type, tag, params].inspect}" if @debug
51
+ validate_message type, tag, params
52
+ send_data @filter.encode([type,tag,params])
53
+ end
54
+
55
+ def receive_version l
56
+ fail "unimplemented"
57
+ end
58
+
59
+ def receive_message type, tag, params
60
+ fail "unimplemented"
61
+ end
62
+
63
+ private
64
+
65
+ def validate_message type, tag, params
66
+ fail unless type.is_a? String or type.is_a? Symbol
67
+ fail unless tag.is_a? String or tag.is_a? Integer
68
+ fail unless params.is_a? Hash
69
+ end
70
+
71
+ def parse_version l
72
+ l =~ /^Redwood\s+(\d+)\s+([\w,]+)\s+([\w,]+)$/ or fail "unexpected banner #{l.inspect}"
73
+ version, encodings, extensions = $1.to_i, $2, $3
74
+ encodings = encodings.split ','
75
+ extensions = extensions.split ','
76
+ extensions = [] if extensions == ['none']
77
+ fail unless version == VERSION
78
+ fail if encodings.empty?
79
+ [encodings, extensions]
80
+ end
81
+
82
+ def create_filter encoding
83
+ case encoding
84
+ when 'json' then JSONFilter.new
85
+ when 'marshal' then MarshalFilter.new
86
+ else fail "unknown encoding #{encoding.inspect}"
87
+ end
88
+ end
89
+
90
+ class JSONFilter
91
+ def initialize
92
+ @parser = Yajl::Parser.new :check_utf8 => false
93
+ end
94
+
95
+ def decode chunk
96
+ parsed = []
97
+ @parser.on_parse_complete = lambda { |o| parsed << o }
98
+ @parser << chunk
99
+ parsed
100
+ end
101
+
102
+ def encode *os
103
+ os.inject('') { |s, o| s << Yajl::Encoder.encode(o) }
104
+ end
105
+ end
106
+
107
+ class MarshalFilter
108
+ def initialize
109
+ @buf = ''
110
+ @state = :prefix
111
+ @size = 0
112
+ end
113
+
114
+ def decode chunk
115
+ received = []
116
+ @buf << chunk
117
+
118
+ begin
119
+ if @state == :prefix
120
+ break unless @buf.size >= 4
121
+ prefix = @buf.slice!(0...4)
122
+ @size = prefix.unpack('N')[0]
123
+ @state = :data
124
+ end
125
+
126
+ fail unless @state == :data
127
+ break if @buf.size < @size
128
+ received << Marshal.load(@buf.slice!(0...@size))
129
+ @state = :prefix
130
+ end until @buf.empty?
131
+
132
+ received
133
+ end
134
+
135
+ def encode o
136
+ data = Marshal.dump o
137
+ [data.size].pack('N') + data
138
+ end
139
+ end
140
+ end
141
+
142
+ class EM::P::RedwoodServer < EM::P::Redwood
143
+ def post_init
144
+ send_version ENCODINGS, []
145
+ end
146
+
147
+ def receive_version encodings, extensions
148
+ fail unless encodings.size == 1
149
+ fail unless ENCODINGS.member? encodings.first
150
+ @filter = create_filter encodings.first
151
+ end
152
+ end
153
+
154
+ class EM::P::RedwoodClient < EM::P::Redwood
155
+ def receive_version encodings, extensions
156
+ encoding = (ENCODINGS & encodings).first
157
+ fail unless encoding
158
+ @filter = create_filter encoding
159
+ send_version [encoding], []
160
+ end
161
+ end