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
@@ -74,9 +74,13 @@ class Buffer
74
74
  mode.resize rows, cols
75
75
  end
76
76
 
77
- def redraw
78
- draw if @dirty
79
- draw_status
77
+ def redraw status
78
+ if @dirty
79
+ draw status
80
+ else
81
+ draw_status status
82
+ end
83
+
80
84
  commit
81
85
  end
82
86
 
@@ -87,9 +91,9 @@ class Buffer
87
91
  @w.noutrefresh
88
92
  end
89
93
 
90
- def draw
94
+ def draw status
91
95
  @mode.draw
92
- draw_status
96
+ draw_status status
93
97
  commit
94
98
  end
95
99
 
@@ -110,9 +114,8 @@ class Buffer
110
114
  @w.clear
111
115
  end
112
116
 
113
- def draw_status
114
- write @height - 1, 0, " [#{mode.name}] #{title} #{mode.status}",
115
- :color => :status_color
117
+ def draw_status status
118
+ write @height - 1, 0, status, :color => :status_color
116
119
  end
117
120
 
118
121
  def focus
@@ -133,6 +136,35 @@ class BufferManager
133
136
 
134
137
  attr_reader :focus_buf
135
138
 
139
+ ## we have to define the key used to continue in-buffer search here, because
140
+ ## it has special semantics that BufferManager deals with---current searches
141
+ ## are canceled by any keypress except this one.
142
+ CONTINUE_IN_BUFFER_SEARCH_KEY = "n"
143
+
144
+ HookManager.register "status-bar-text", <<EOS
145
+ Sets the status bar. The default status bar contains the mode name, the buffer
146
+ title, and the mode status. Note that this will be called at least once per
147
+ keystroke, so excessive computation is discouraged.
148
+
149
+ Variables:
150
+ num_inbox: number of messages in inbox
151
+ num_inbox_unread: total number of messages marked as unread
152
+ num_total: total number of messages in the index
153
+ num_spam: total number of messages marked as spam
154
+ title: title of the current buffer
155
+ mode: current mode name (string)
156
+ status: current mode status (string)
157
+ Return value: a string to be used as the status bar.
158
+ EOS
159
+
160
+ HookManager.register "terminal-title-text", <<EOS
161
+ Sets the title of the current terminal, if applicable. Note that this will be
162
+ called at least once per keystroke, so excessive computation is discouraged.
163
+
164
+ Variables: the same as status-bar-text hook.
165
+ Return value: a string to be used as the terminal title.
166
+ EOS
167
+
136
168
  def initialize
137
169
  @name_map = {}
138
170
  @buffers = []
@@ -150,7 +182,8 @@ class BufferManager
150
182
  def buffers; @name_map.to_a; end
151
183
 
152
184
  def focus_on buf
153
- raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
185
+ return unless @buffers.member? buf
186
+
154
187
  return if buf == @focus_buf
155
188
  @focus_buf.blur if @focus_buf
156
189
  @focus_buf = buf
@@ -158,7 +191,7 @@ class BufferManager
158
191
  end
159
192
 
160
193
  def raise_to_front buf
161
- raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
194
+ return unless @buffers.member? buf
162
195
 
163
196
  @buffers.delete buf
164
197
  if @buffers.length > 0 && @buffers.last.force_to_top?
@@ -190,7 +223,13 @@ class BufferManager
190
223
  end
191
224
 
192
225
  def handle_input c
193
- @focus_buf && @focus_buf.mode.handle_input(c)
226
+ if @focus_buf
227
+ if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY[0]
228
+ @focus_buf.mode.cancel_search!
229
+ @focus_buf.mark_dirty
230
+ end
231
+ @focus_buf.mode.handle_input c
232
+ end
194
233
  end
195
234
 
196
235
  def exists? n; @name_map.member? n; end
@@ -204,16 +243,27 @@ class BufferManager
204
243
  def completely_redraw_screen
205
244
  return if @shelled
206
245
 
246
+ status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
247
+
207
248
  Ncurses.sync do
208
249
  @dirty = true
209
250
  Ncurses.clear
210
- draw_screen :sync => false
251
+ draw_screen :sync => false, :status => status, :title => title
211
252
  end
212
253
  end
213
254
 
214
255
  def draw_screen opts={}
215
256
  return if @shelled
216
257
 
258
+ status, title =
259
+ if opts.member? :status
260
+ [opts[:status], opts[:title]]
261
+ else
262
+ get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
263
+ end
264
+
265
+ print "\033]2;#{title}\07" if title
266
+
217
267
  Ncurses.mutex.lock unless opts[:sync] == false
218
268
 
219
269
  ## disabling this for the time being, to help with debugging
@@ -222,7 +272,7 @@ class BufferManager
222
272
  false && @buffers.inject(@dirty) do |dirty, buf|
223
273
  buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
224
274
  #dirty ? buf.draw : buf.redraw
225
- buf.draw
275
+ buf.draw status
226
276
  dirty
227
277
  end
228
278
 
@@ -230,7 +280,7 @@ class BufferManager
230
280
  if true
231
281
  buf = @buffers.last
232
282
  buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
233
- @dirty ? buf.draw : buf.redraw
283
+ @dirty ? buf.draw(status) : buf.redraw(status)
234
284
  end
235
285
 
236
286
  draw_minibuf :sync => false unless opts[:skip_minibuf]
@@ -241,17 +291,21 @@ class BufferManager
241
291
  Ncurses.mutex.unlock unless opts[:sync] == false
242
292
  end
243
293
 
244
- ## gets the mode from the block, which is only called if the buffer
245
- ## doesn't already exist. this is useful in the case that generating
246
- ## the mode is expensive, as it often is.
294
+ ## if the named buffer already exists, pops it to the front without
295
+ ## calling the block. otherwise, gets the mode from the block and
296
+ ## creates a new buffer. returns two things: the buffer, and a boolean
297
+ ## indicating whether it's a new buffer or not.
247
298
  def spawn_unless_exists title, opts={}
248
- if @name_map.member? title
249
- raise_to_front @name_map[title] unless opts[:hidden]
250
- else
251
- mode = yield
252
- spawn title, mode, opts
253
- end
254
- @name_map[title]
299
+ new =
300
+ if @name_map.member? title
301
+ raise_to_front @name_map[title] unless opts[:hidden]
302
+ false
303
+ else
304
+ mode = yield
305
+ spawn title, mode, opts
306
+ true
307
+ end
308
+ [@name_map[title], new]
255
309
  end
256
310
 
257
311
  def spawn title, mode, opts={}
@@ -334,7 +388,9 @@ class BufferManager
334
388
  ## TODO: something intelligent here
335
389
  ## for now I will simply prohibit killing the inbox buffer.
336
390
  else
337
- raise_to_front @buffers.last
391
+ last = @buffers.last
392
+ @focus_buf ||= last
393
+ raise_to_front last
338
394
  end
339
395
  end
340
396
 
@@ -344,15 +400,13 @@ class BufferManager
344
400
  end
345
401
  end
346
402
 
347
- def ask_many_with_completions domain, question, completions, default=nil, sep=" "
403
+ def ask_many_with_completions domain, question, completions, default=nil
348
404
  ask domain, question, default do |partial|
349
405
  prefix, target =
350
- case partial#.gsub(/#{sep}+/, sep)
406
+ case partial
351
407
  when /^\s*$/
352
408
  ["", ""]
353
- when /^(.+#{sep})$/
354
- [$1, ""]
355
- when /^(.*#{sep})?(.+?)$/
409
+ when /^(.*\s+)?(.*?)$/
356
410
  [$1 || "", $2]
357
411
  else
358
412
  raise "william screwed up completion: #{partial.inspect}"
@@ -362,6 +416,17 @@ class BufferManager
362
416
  end
363
417
  end
364
418
 
419
+ def ask_many_emails_with_completions domain, question, completions, default=nil
420
+ ask domain, question, default do |partial|
421
+ prefix, target = partial.split_on_commas_with_remainder
422
+ Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}"
423
+ target ||= prefix.pop || ""
424
+ prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
425
+ Redwood::log "after: prefix #{prefix.inspect}, target #{target.inspect}"
426
+ completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
427
+ end
428
+ end
429
+
365
430
  def ask_for_filename domain, question, default=nil
366
431
  answer = ask domain, question, default do |s|
367
432
  if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
@@ -423,13 +488,11 @@ class BufferManager
423
488
  default = default_contacts.map { |s| s.to_s }.join(" ")
424
489
  default += " " unless default.empty?
425
490
 
426
- recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.longname, c.email] }
427
- contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.longname, c.email] }
428
-
429
- Redwood::log "recent: #{recent.inspect}"
491
+ recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
492
+ contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
430
493
 
431
494
  completions = (recent + contacts).flatten.uniq.sort
432
- answer = BufferManager.ask_many_with_completions domain, question, completions, default, /\s*,\s*/
495
+ answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
433
496
 
434
497
  if answer
435
498
  answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
@@ -454,12 +517,10 @@ class BufferManager
454
517
  tf.activate question, default, &block
455
518
  @dirty = true
456
519
  draw_screen :skip_minibuf => true, :sync => false
520
+ tf.position_cursor
521
+ Ncurses.refresh
457
522
  end
458
523
 
459
- ret = nil
460
- tf.position_cursor
461
- Ncurses.sync { Ncurses.refresh }
462
-
463
524
  while true
464
525
  c = Ncurses.nonblocking_getch
465
526
  next unless c # getch timeout
@@ -627,6 +688,30 @@ class BufferManager
627
688
  end
628
689
 
629
690
  private
691
+ def default_status_bar buf
692
+ " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
693
+ end
694
+
695
+ def default_terminal_title buf
696
+ "Sup #{Redwood::VERSION} :: #{buf.title}"
697
+ end
698
+
699
+ def get_status_and_title buf
700
+ opts = {
701
+ :num_inbox => lambda { Index.num_results_for :label => :inbox },
702
+ :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
703
+ :num_total => lambda { Index.size },
704
+ :num_spam => lambda { Index.num_results_for :label => :spam },
705
+ :title => buf.title,
706
+ :mode => buf.mode.name,
707
+ :status => buf.mode.status
708
+ }
709
+
710
+ statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
711
+ term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
712
+
713
+ [statusbar_text, term_title_text]
714
+ end
630
715
 
631
716
  def users
632
717
  unless @users
@@ -22,14 +22,14 @@ class Colormap
22
22
  []) + [nil]
23
23
  end
24
24
 
25
- def add sym, fg, bg, *attrs
26
- raise ArgumentError, "color for #{sym} already defined" if
27
- @entries.member? sym
25
+ def add sym, fg, bg, attr=nil, opts={}
26
+ raise ArgumentError, "color for #{sym} already defined" if @entries.member? sym
28
27
  raise ArgumentError, "color '#{fg}' unknown" unless CURSES_COLORS.include? fg
29
28
  raise ArgumentError, "color '#{bg}' unknown" unless CURSES_COLORS.include? bg
29
+ attrs = [attr].flatten.compact
30
30
 
31
31
  @entries[sym] = [fg, bg, attrs, nil]
32
- @entries[highlight_sym(sym)] = highlight_for(fg, bg, attrs) + [nil]
32
+ @entries[highlight_sym(sym)] = opts[:highlight] ? @entries[opts[:highlight]] : highlight_for(fg, bg, attrs) + [nil]
33
33
  end
34
34
 
35
35
  def highlight_sym sym
@@ -47,7 +47,7 @@ class DraftLoader < Source
47
47
  def initialize cur_offset=0
48
48
  dir = Redwood::DRAFT_DIR
49
49
  Dir.mkdir dir unless File.exists? dir
50
- super "draft://#{dir}", cur_offset, true, false
50
+ super DraftManager.source_name, cur_offset, true, false
51
51
  @dir = dir
52
52
  end
53
53
 
@@ -9,9 +9,10 @@ class HookManager
9
9
  ##
10
10
  ## i don't bother providing setters, since i'm pretty sure the
11
11
  ## charade will fall apart pretty quickly with respect to scoping.
12
- ## this is basically fail-fast.
12
+ ## "fail-fast", we'll call it.
13
13
  class HookContext
14
14
  def initialize name
15
+ @__say_id = nil
15
16
  @__name = name
16
17
  @__locals = {}
17
18
  end
@@ -21,7 +22,7 @@ class HookManager
21
22
  def method_missing m, *a
22
23
  case @__locals[m]
23
24
  when Proc
24
- @__locals[m].call(*a)
25
+ @__locals[m] = @__locals[m].call(*a) # only call the proc once
25
26
  when nil
26
27
  super
27
28
  else
@@ -30,8 +31,12 @@ class HookManager
30
31
  end
31
32
 
32
33
  def say s
33
- @__say_id = BufferManager.say s, @__say_id
34
- BufferManager.draw_screen
34
+ if BufferManager.instantiated?
35
+ @__say_id = BufferManager.say s, @__say_id
36
+ BufferManager.draw_screen
37
+ else
38
+ log s
39
+ end
35
40
  end
36
41
 
37
42
  def log s
@@ -39,7 +44,12 @@ class HookManager
39
44
  end
40
45
 
41
46
  def ask_yes_or_no q
42
- BufferManager.ask_yes_or_no q
47
+ if BufferManager.instantiated?
48
+ BufferManager.ask_yes_or_no q
49
+ else
50
+ print q
51
+ gets.chomp.downcase == 'y'
52
+ end
43
53
  end
44
54
 
45
55
  def __binding
@@ -75,6 +85,7 @@ class HookManager
75
85
  rescue Exception => e
76
86
  log "error running hook: #{e.message}"
77
87
  log e.backtrace.join("\n")
88
+ @hooks[name] = nil # disable it
78
89
  BufferManager.flash "Error running hook: #{e.message}"
79
90
  end
80
91
  context.__cleanup
@@ -101,6 +112,8 @@ EOS
101
112
  end
102
113
  end
103
114
 
115
+ def enabled? name; !hook_for(name).nil? end
116
+
104
117
  private
105
118
 
106
119
  def hook_for name
@@ -87,17 +87,8 @@ class IMAP < Source
87
87
  end
88
88
  def ssl?; @parsed_uri.scheme == 'imaps' end
89
89
 
90
- def check
91
- return unless start_offset
92
-
93
- ids =
94
- @mutex.synchronize do
95
- unsynchronized_scan_mailbox
96
- @ids
97
- end
98
-
99
- start = ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}."
100
- end
90
+ def check; end # do nothing because anything we do will be too slow,
91
+ # and we'll catch the errors later.
101
92
 
102
93
  ## is this necessary? TODO: remove maybe
103
94
  def == o; o.is_a?(IMAP) && o.uri == self.uri && o.username == self.username; end
@@ -109,6 +100,10 @@ class IMAP < Source
109
100
  def load_message id
110
101
  RMail::Parser.read raw_message(id)
111
102
  end
103
+
104
+ def each_raw_message_line id
105
+ StringIO.new(raw_message(id)).each { |l| yield l }
106
+ end
112
107
 
113
108
  def raw_header id
114
109
  unsynchronized_scan_mailbox
@@ -164,13 +159,14 @@ class IMAP < Source
164
159
  id = ids[i]
165
160
  state = @mutex.synchronize { @imap_state[id] } or next
166
161
  self.cur_offset = id
167
- labels = { :Seen => :unread,
168
- :Flagged => :starred,
162
+ labels = { :Flagged => :starred,
169
163
  :Deleted => :deleted
170
164
  }.inject(@labels) do |cur, (imap, sup)|
171
165
  cur + (state[:flags].include?(imap) ? [sup] : [])
172
166
  end
173
167
 
168
+ labels += [:unread] unless state[:flags].include?(:Seen)
169
+
174
170
  yield id, labels
175
171
  end
176
172
  end
@@ -228,10 +224,12 @@ private
228
224
  ## fails with a NO response, the client may try another", in
229
225
  ## practice it seems like they can also send a BAD response.
230
226
  begin
227
+ raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=CRAM-MD5"
231
228
  @imap.authenticate 'CRAM-MD5', @username, @password
232
229
  rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
233
230
  Redwood::log "CRAM-MD5 authentication failed: #{e.class}. Trying LOGIN auth..."
234
231
  begin
232
+ raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=LOGIN"
235
233
  @imap.authenticate 'LOGIN', @username, @password
236
234
  rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
237
235
  Redwood::log "LOGIN authentication failed: #{e.class}. Trying plain-text LOGIN..."
@@ -1,8 +1,14 @@
1
1
  ## the index structure for redwood. interacts with ferret.
2
2
 
3
- require 'thread'
4
3
  require 'fileutils'
5
4
  require 'ferret'
5
+ begin
6
+ require 'chronic'
7
+ $have_chronic = true
8
+ rescue LoadError => e
9
+ Redwood::log "optional 'chronic' library not found (run 'gem install chronic' to install)"
10
+ $have_chronic = false
11
+ end
6
12
 
7
13
  module Redwood
8
14
 
@@ -18,6 +24,7 @@ class Index
18
24
  include Singleton
19
25
 
20
26
  attr_reader :index
27
+ alias ferret index
21
28
  def initialize dir=BASE_DIR
22
29
  @dir = dir
23
30
  @sources = {}
@@ -46,7 +53,7 @@ class Index
46
53
  end
47
54
 
48
55
  def start_lock_update_thread
49
- @lock_update_thread = Redwood::reporting_thread do
56
+ @lock_update_thread = Redwood::reporting_thread("lock update") do
50
57
  while true
51
58
  sleep 30
52
59
  @lock.touch_yourself
@@ -112,8 +119,8 @@ EOS
112
119
  def add_source source
113
120
  raise "duplicate source!" if @sources.include? source
114
121
  @sources_dirty = true
115
- source.id ||= @sources.size
116
- ##TODO: why was this necessary?
122
+ max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id }
123
+ source.id ||= (max || 0) + 1
117
124
  ##source.id += 1 while @sources.member? source.id
118
125
  @sources[source.id] = source
119
126
  end
@@ -171,7 +178,7 @@ EOS
171
178
  :date => m.date.to_indexable_s,
172
179
  :body => m.content,
173
180
  :snippet => m.snippet,
174
- :label => m.labels.join(" "),
181
+ :label => m.labels.uniq.join(" "),
175
182
  :from => m.from ? m.from.email : "",
176
183
  :to => (m.to + m.cc + m.bcc).map { |x| x.email }.join(" "),
177
184
  :subject => wrap_subj(Message.normalize_subj(m.subj)),
@@ -256,12 +263,14 @@ EOS
256
263
  end
257
264
 
258
265
  until pending.empty? || (opts[:limit] && messages.size >= opts[:limit])
259
- id = pending.pop
260
- next if searched.member? id
261
- searched[id] = true
262
266
  q = Ferret::Search::BooleanQuery.new true
263
- q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
264
- q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
267
+
268
+ pending.each do |id|
269
+ searched[id] = true
270
+ q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
271
+ q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
272
+ end
273
+ pending = []
265
274
 
266
275
  q = build_query :qobj => q
267
276
 
@@ -278,10 +287,11 @@ EOS
278
287
  #Redwood::log "got #{mid} as a child of #{id}"
279
288
  messages[mid] ||= lambda { build_message docid }
280
289
  refs = @index[docid][:refs].split(" ")
281
- pending += refs
290
+ pending += refs.select { |id| !searched[id] }
282
291
  end
283
292
  end
284
293
  end
294
+
285
295
  if killed
286
296
  Redwood::log "thread for #{m.id} is killed, ignoring"
287
297
  false
@@ -370,8 +380,10 @@ EOS
370
380
 
371
381
  protected
372
382
 
383
+ ## do any specialized parsing
384
+ ## returns nil and flashes error message if parsing failed
373
385
  def parse_user_query_string str
374
- str2 = str.gsub(/(to|from):(\S+)/) do
386
+ result = str.gsub(/\b(to|from):(\S+)\b/) do
375
387
  field, name = $1, $2
376
388
  if(p = ContactManager.contact_for(name))
377
389
  [field, p.email]
@@ -380,8 +392,34 @@ protected
380
392
  end.join(":")
381
393
  end
382
394
 
383
- Redwood::log "translated #{str} to #{str2}" unless str2 == str
384
- @qparser.parse str2
395
+ if $have_chronic
396
+ chronic_failure = false
397
+ result = result.gsub(/\b(before|on|in|after):(\((.+?)\)\B|(\S+)\b)/) do
398
+ break if chronic_failure
399
+ field, datestr = $1, ($3 || $4)
400
+ realdate = Chronic.parse(datestr, :guess => false, :context => :none)
401
+ if realdate
402
+ case field
403
+ when "after"
404
+ Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.end}"
405
+ "date:(>= #{sprintf "%012d", realdate.end.to_i})"
406
+ when "before"
407
+ Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
408
+ "date:(<= #{sprintf "%012d", realdate.begin.to_i})"
409
+ else
410
+ Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate}"
411
+ "date:(<= #{sprintf "%012d", realdate.end.to_i}) date:(>= #{sprintf "%012d", realdate.begin.to_i})"
412
+ end
413
+ else
414
+ BufferManager.flash "Don't understand date #{datestr.inspect}!"
415
+ chronic_failure = true
416
+ end
417
+ end
418
+ result = nil if chronic_failure
419
+ end
420
+
421
+ Redwood::log "translated #{str.inspect} to #{result}" unless result == str
422
+ @qparser.parse result if result
385
423
  end
386
424
 
387
425
  def build_query opts