sup 0.3 → 0.4

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 (52) hide show
  1. data/HACKING +31 -9
  2. data/History.txt +7 -0
  3. data/Manifest.txt +2 -0
  4. data/Rakefile +9 -5
  5. data/bin/sup +81 -57
  6. data/bin/sup-config +1 -1
  7. data/bin/sup-sync +3 -0
  8. data/bin/sup-tweak-labels +127 -0
  9. data/doc/TODO +23 -12
  10. data/lib/sup.rb +13 -11
  11. data/lib/sup/account.rb +25 -12
  12. data/lib/sup/buffer.rb +61 -41
  13. data/lib/sup/colormap.rb +2 -0
  14. data/lib/sup/contact.rb +28 -18
  15. data/lib/sup/crypto.rb +86 -31
  16. data/lib/sup/draft.rb +12 -6
  17. data/lib/sup/horizontal-selector.rb +47 -0
  18. data/lib/sup/imap.rb +50 -37
  19. data/lib/sup/index.rb +76 -13
  20. data/lib/sup/keymap.rb +27 -8
  21. data/lib/sup/maildir.rb +1 -1
  22. data/lib/sup/mbox/loader.rb +1 -1
  23. data/lib/sup/message-chunks.rb +43 -15
  24. data/lib/sup/message.rb +67 -31
  25. data/lib/sup/mode.rb +40 -9
  26. data/lib/sup/modes/completion-mode.rb +1 -1
  27. data/lib/sup/modes/compose-mode.rb +3 -3
  28. data/lib/sup/modes/contact-list-mode.rb +12 -8
  29. data/lib/sup/modes/edit-message-mode.rb +100 -36
  30. data/lib/sup/modes/file-browser-mode.rb +1 -0
  31. data/lib/sup/modes/forward-mode.rb +43 -8
  32. data/lib/sup/modes/inbox-mode.rb +8 -5
  33. data/lib/sup/modes/label-search-results-mode.rb +12 -1
  34. data/lib/sup/modes/line-cursor-mode.rb +4 -7
  35. data/lib/sup/modes/reply-mode.rb +59 -54
  36. data/lib/sup/modes/resume-mode.rb +6 -6
  37. data/lib/sup/modes/scroll-mode.rb +4 -3
  38. data/lib/sup/modes/search-results-mode.rb +8 -5
  39. data/lib/sup/modes/text-mode.rb +19 -2
  40. data/lib/sup/modes/thread-index-mode.rb +109 -40
  41. data/lib/sup/modes/thread-view-mode.rb +180 -49
  42. data/lib/sup/person.rb +3 -3
  43. data/lib/sup/poll.rb +9 -8
  44. data/lib/sup/rfc2047.rb +7 -1
  45. data/lib/sup/sent.rb +1 -1
  46. data/lib/sup/tagger.rb +10 -4
  47. data/lib/sup/textfield.rb +7 -7
  48. data/lib/sup/thread.rb +86 -49
  49. data/lib/sup/update.rb +11 -0
  50. data/lib/sup/util.rb +74 -34
  51. data/test/test_message.rb +441 -0
  52. metadata +136 -117
data/lib/sup.rb CHANGED
@@ -3,6 +3,7 @@ require 'yaml'
3
3
  require 'zlib'
4
4
  require 'thread'
5
5
  require 'fileutils'
6
+ require 'gettext'
6
7
  require 'curses'
7
8
 
8
9
  class Object
@@ -32,7 +33,7 @@ class Module
32
33
  end
33
34
 
34
35
  module Redwood
35
- VERSION = "0.3"
36
+ VERSION = "0.4"
36
37
 
37
38
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
38
39
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
@@ -49,16 +50,6 @@ module Redwood
49
50
  YAML_DOMAIN = "masanjin.net"
50
51
  YAML_DATE = "2006-10-01"
51
52
 
52
- ## determine encoding and character set
53
- ## probably a better way to do this
54
- $ctype = ENV["LC_CTYPE"] || ENV["LANG"] || "en-US.utf-8"
55
- $encoding =
56
- if $ctype =~ /\.(.*)?/
57
- $1
58
- else
59
- "utf-8"
60
- end
61
-
62
53
  ## record exceptions thrown in threads nicely
63
54
  def reporting_thread name
64
55
  if $opts[:no_threads]
@@ -204,6 +195,7 @@ else
204
195
  :ask_for_subject => true,
205
196
  :confirm_no_attachments => true,
206
197
  :confirm_top_posting => true,
198
+ :discard_snippets_from_encrypted_messages => false,
207
199
  }
208
200
  begin
209
201
  FileUtils.mkdir_p Redwood::BASE_DIR
@@ -234,6 +226,15 @@ module Redwood
234
226
  module_function :log
235
227
  end
236
228
 
229
+ ## determine encoding and character set
230
+ $encoding = Locale.current.charset
231
+ if $encoding
232
+ Redwood::log "using character set encoding #{$encoding.inspect}"
233
+ else
234
+ Redwood::log "warning: can't find character set by using locale, defaulting to utf-8"
235
+ $encoding = "utf-8"
236
+ end
237
+
237
238
  ## now everything else (which can feel free to call Redwood::log at load time)
238
239
  require "sup/update"
239
240
  require "sup/suicide"
@@ -255,6 +256,7 @@ require "sup/tagger"
255
256
  require "sup/draft"
256
257
  require "sup/poll"
257
258
  require "sup/crypto"
259
+ require "sup/horizontal-selector"
258
260
  require "sup/modes/line-cursor-mode"
259
261
  require "sup/modes/help-mode"
260
262
  require "sup/modes/edit-message-mode"
@@ -3,8 +3,10 @@ module Redwood
3
3
  class Account < Person
4
4
  attr_accessor :sendmail, :signature
5
5
 
6
- def initialize email, h
7
- super h[:name], email, 0, true
6
+ def initialize h
7
+ raise ArgumentError, "no name for account" unless h[:name]
8
+ raise ArgumentError, "no name for email" unless h[:name]
9
+ super h[:name], h[:email], 0, true
8
10
  @sendmail = h[:sendmail]
9
11
  @signature = h[:signature]
10
12
  end
@@ -18,10 +20,11 @@ class AccountManager
18
20
  def initialize accounts
19
21
  @email_map = {}
20
22
  @accounts = {}
23
+ @regexen = {}
21
24
  @default_account = nil
22
25
 
23
26
  add_account accounts[:default], true
24
- accounts.each { |k, v| add_account v unless k == :default }
27
+ accounts.each { |k, v| add_account v, false unless k == :default }
25
28
 
26
29
  self.class.i_am_the_instance self
27
30
  end
@@ -38,24 +41,34 @@ class AccountManager
38
41
  end
39
42
  hash[:alternates] ||= []
40
43
 
41
- main_email = hash[:email]
44
+ a = Account.new hash
45
+ PersonManager.register a
46
+ @accounts[a] = true
47
+
48
+ if default
49
+ raise ArgumentError, "multiple default accounts" if @default_account
50
+ @default_account = a
51
+ end
52
+
42
53
  ([hash[:email]] + hash[:alternates]).each do |email|
43
54
  next if @email_map.member? email
44
- a = Account.new main_email, hash
45
- PersonManager.register a
46
- @accounts[a] = true
47
55
  @email_map[email] = a
48
56
  end
49
57
 
50
- if default
51
- raise ArgumentError, "multiple default accounts" if @default_account
52
- @default_account = @email_map[main_email]
53
- end
58
+ hash[:regexen].each do |re|
59
+ @regexen[Regexp.new(re)] = a
60
+ end if hash[:regexen]
54
61
  end
55
62
 
56
63
  def is_account? p; is_account_email? p.email end
57
- def account_for email; @email_map[email] end
58
64
  def is_account_email? email; !account_for(email).nil? end
65
+ def account_for email
66
+ if(a = @email_map[email])
67
+ a
68
+ else
69
+ @regexen.argfind { |re, a| re =~ email && a }
70
+ end
71
+ end
59
72
  end
60
73
 
61
74
  end
@@ -48,6 +48,8 @@ end
48
48
 
49
49
  module Redwood
50
50
 
51
+ class InputSequenceAborted < StandardError; end
52
+
51
53
  class Buffer
52
54
  attr_reader :mode, :x, :y, :width, :height, :title
53
55
  bool_reader :dirty
@@ -175,6 +177,7 @@ EOS
175
177
  @textfields = {}
176
178
  @flash = nil
177
179
  @shelled = @asking = false
180
+ @in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
178
181
 
179
182
  self.class.i_am_the_instance self
180
183
  end
@@ -183,7 +186,6 @@ EOS
183
186
 
184
187
  def focus_on buf
185
188
  return unless @buffers.member? buf
186
-
187
189
  return if buf == @focus_buf
188
190
  @focus_buf.blur if @focus_buf
189
191
  @focus_buf = buf
@@ -191,15 +193,13 @@ EOS
191
193
  end
192
194
 
193
195
  def raise_to_front buf
194
- return unless @buffers.member? buf
195
-
196
- @buffers.delete buf
196
+ @buffers.delete(buf) or return
197
197
  if @buffers.length > 0 && @buffers.last.force_to_top?
198
198
  @buffers.insert(-2, buf)
199
199
  else
200
200
  @buffers.push buf
201
- focus_on buf
202
201
  end
202
+ focus_on @buffers.last
203
203
  @dirty = true
204
204
  end
205
205
 
@@ -259,10 +259,11 @@ EOS
259
259
  if opts.member? :status
260
260
  [opts[:status], opts[:title]]
261
261
  else
262
- get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
262
+ raise "status must be supplied if draw_screen is called within a sync" if opts[:sync] == false
263
+ get_status_and_title @focus_buf # must be called outside of the ncurses lock
263
264
  end
264
265
 
265
- print "\033]2;#{title}\07" if title
266
+ print "\033]2;#{title}\07" if title && @in_x
266
267
 
267
268
  Ncurses.mutex.lock unless opts[:sync] == false
268
269
 
@@ -349,7 +350,10 @@ EOS
349
350
  c = Ncurses.nonblocking_getch
350
351
  next unless c # getch timeout
351
352
  break if c == Ncurses::KEY_CANCEL
352
- mode.handle_input c
353
+ begin
354
+ mode.handle_input c
355
+ rescue InputSequenceAborted # do nothing
356
+ end
353
357
  draw_screen
354
358
  erase_flash
355
359
  end
@@ -378,7 +382,7 @@ EOS
378
382
  end
379
383
 
380
384
  def kill_buffer buf
381
- raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
385
+ raise ArgumentError, "buffer not on stack: #{buf}: #{buf.title.inspect}" unless @buffers.member? buf
382
386
 
383
387
  buf.mode.cleanup
384
388
  @buffers.delete buf
@@ -388,15 +392,13 @@ EOS
388
392
  ## TODO: something intelligent here
389
393
  ## for now I will simply prohibit killing the inbox buffer.
390
394
  else
391
- last = @buffers.last
392
- @focus_buf ||= last
393
- raise_to_front last
395
+ raise_to_front @buffers.last
394
396
  end
395
397
  end
396
398
 
397
399
  def ask_with_completions domain, question, completions, default=nil
398
400
  ask domain, question, default do |s|
399
- completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] }
401
+ completions.select { |x| x =~ /^#{Regexp::escape s}/i }.map { |x| [x, x] }
400
402
  end
401
403
  end
402
404
 
@@ -412,18 +414,16 @@ EOS
412
414
  raise "william screwed up completion: #{partial.inspect}"
413
415
  end
414
416
 
415
- completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
417
+ completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
416
418
  end
417
419
  end
418
420
 
419
421
  def ask_many_emails_with_completions domain, question, completions, default=nil
420
422
  ask domain, question, default do |partial|
421
423
  prefix, target = partial.split_on_commas_with_remainder
422
- Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}"
423
424
  target ||= prefix.pop || ""
424
425
  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] }
426
+ completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
427
427
  end
428
428
  end
429
429
 
@@ -436,7 +436,7 @@ EOS
436
436
  if dir
437
437
  [[s.sub(full, dir), "~#{name}"]]
438
438
  else
439
- users.select { |u| u =~ /^#{name}/ }.map do |u|
439
+ users.select { |u| u =~ /^#{Regexp::escape name}/ }.map do |u|
440
440
  [s.sub("~#{name}", "~#{u}"), "~#{u}"]
441
441
  end
442
442
  end
@@ -499,24 +499,22 @@ EOS
499
499
  end
500
500
  end
501
501
 
502
-
502
+ ## for simplicitly, we always place the question at the very bottom of the
503
+ ## screen
503
504
  def ask domain, question, default=nil, &block
504
505
  raise "impossible!" if @asking
505
506
  @asking = true
506
507
 
507
- @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
508
+ @textfields[domain] ||= TextField.new
508
509
  tf = @textfields[domain]
509
510
  completion_buf = nil
510
511
 
511
- ## this goddamn ncurses form shit is a fucking 1970's nightmare.
512
- ## jesus christ. the exact sequence of ncurses events that needs
513
- ## to happen in order to display a form and have the entire screen
514
- ## not disappear and have the cursor in the right place is TOO
515
- ## FUCKING COMPLICATED.
512
+ status, title = get_status_and_title @focus_buf
513
+
516
514
  Ncurses.sync do
517
- tf.activate question, default, &block
518
- @dirty = true
519
- draw_screen :skip_minibuf => true, :sync => false
515
+ tf.activate Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols, question, default, &block
516
+ @dirty = true # for some reason that blanks the whole fucking screen
517
+ draw_screen :sync => false, :status => status, :title => title
520
518
  tf.position_cursor
521
519
  Ncurses.refresh
522
520
  end
@@ -546,45 +544,48 @@ EOS
546
544
  Ncurses.sync { Ncurses.refresh }
547
545
  end
548
546
 
549
- Ncurses.sync { tf.deactivate }
550
547
  kill_buffer completion_buf if completion_buf
548
+
551
549
  @dirty = true
552
550
  @asking = false
553
- draw_screen
551
+ Ncurses.sync do
552
+ tf.deactivate
553
+ draw_screen :sync => false, :status => status, :title => title
554
+ end
554
555
  tf.value
555
556
  end
556
557
 
557
- ## some pretty lame code in here!
558
558
  def ask_getch question, accept=nil
559
+ raise "impossible!" if @asking
560
+ @asking = true
561
+
559
562
  accept = accept.split(//).map { |x| x[0] } if accept
560
563
 
561
- flash question
564
+ status, title = get_status_and_title @focus_buf
562
565
  Ncurses.sync do
563
- Ncurses.curs_set 1
566
+ draw_screen :sync => false, :status => status, :title => title
567
+ Ncurses.mvaddstr Ncurses.rows - 1, 0, question
564
568
  Ncurses.move Ncurses.rows - 1, question.length + 1
569
+ Ncurses.curs_set 1
565
570
  Ncurses.refresh
566
571
  end
567
572
 
568
573
  ret = nil
569
574
  done = false
570
- @shelled = true
571
575
  until done
572
576
  key = Ncurses.nonblocking_getch or next
573
577
  if key == Ncurses::KEY_CANCEL
574
578
  done = true
575
- elsif (accept && accept.member?(key)) || !accept
579
+ elsif accept.nil? || accept.empty? || accept.member?(key)
576
580
  ret = key
577
581
  done = true
578
582
  end
579
583
  end
580
584
 
581
- @shelled = false
582
-
585
+ @asking = false
583
586
  Ncurses.sync do
584
587
  Ncurses.curs_set 0
585
- erase_flash
586
- draw_screen :sync => false
587
- Ncurses.curs_set 0
588
+ draw_screen :sync => false, :status => status, :title => title
588
589
  end
589
590
 
590
591
  ret
@@ -602,6 +603,25 @@ EOS
602
603
  end
603
604
  end
604
605
 
606
+ ## turns an input keystroke into an action symbol. returns the action
607
+ ## if found, nil if not found, and throws InputSequenceAborted if
608
+ ## the user aborted a multi-key sequence. (Because each of those cases
609
+ ## should be handled differently.)
610
+ ##
611
+ ## this is in BufferManager because multi-key sequences require prompting.
612
+ def resolve_input_with_keymap c, keymap
613
+ action, text = keymap.action_for c
614
+ while action.is_a? Keymap # multi-key commands, prompt
615
+ key = BufferManager.ask_getch text
616
+ unless key # user canceled, abort
617
+ erase_flash
618
+ raise InputSequenceAborted
619
+ end
620
+ action, text = action.action_for(key) if action.has_key?(key)
621
+ end
622
+ action
623
+ end
624
+
605
625
  def minibuf_lines
606
626
  @minibuf_mutex.synchronize do
607
627
  [(@flash ? 1 : 0) +
@@ -615,7 +635,7 @@ EOS
615
635
  @minibuf_mutex.synchronize do
616
636
  m = @minibuf_stack.compact
617
637
  m << @flash if @flash
618
- m << "" if m.empty?
638
+ m << "" if m.empty? unless @asking # to clear it
619
639
  end
620
640
 
621
641
  Ncurses.mutex.lock unless opts[:sync] == false
@@ -51,6 +51,8 @@ class Colormap
51
51
  case bg
52
52
  when Curses::COLOR_CYAN
53
53
  Curses::COLOR_YELLOW
54
+ when Curses::COLOR_YELLOW
55
+ Curses::COLOR_BLUE
54
56
  else
55
57
  Curses::COLOR_CYAN
56
58
  end
@@ -5,44 +5,54 @@ class ContactManager
5
5
 
6
6
  def initialize fn
7
7
  @fn = fn
8
- @p2a = {} # person to alias map
9
- @a2p = {} # alias to person map
8
+
9
+ ## maintain the mapping between people and aliases. for contacts without
10
+ ## aliases, there will be no @a2p entry, so @p2a.keys should be treated
11
+ ## as the canonical list of contacts.
12
+
13
+ @p2a = {} # person to alias
14
+ @a2p = {} # alias to person
10
15
 
11
16
  if File.exists? fn
12
17
  IO.foreach(fn) do |l|
13
- l =~ /^([^:]+): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
18
+ l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
14
19
  aalias, addr = $1, $2
15
20
  p = PersonManager.person_for addr, :definitive => true
16
21
  @p2a[p] = aalias
17
- @a2p[aalias] = p
22
+ @a2p[aalias] = p unless aalias.nil? || aalias.empty?
18
23
  end
19
24
  end
20
25
 
21
26
  self.class.i_am_the_instance self
22
27
  end
23
28
 
24
- def contacts; @p2a.keys; end
25
- def set_contact person, aalias
26
- if(pold = @a2p[aalias]) && (pold != person)
27
- drop_contact pold
29
+ def contacts; @p2a.keys end
30
+ def contacts_with_aliases; @a2p.values.uniq end
31
+
32
+ def update_alias person, aalias=nil
33
+ if(old_aalias = @p2a[person]) # remove old alias
34
+ @a2p.delete old_aalias
28
35
  end
29
36
  @p2a[person] = aalias
30
- @a2p[aalias] = person
37
+ @a2p[aalias] = person unless aalias.nil? || aalias.empty?
31
38
  end
39
+
40
+ ## this may not actually be called anywhere, since we still keep contacts
41
+ ## around without aliases to override any fullname changes.
32
42
  def drop_contact person
33
- if(aalias = @p2a[person])
34
- @p2a.delete person
35
- @a2p.delete aalias
36
- end
37
- end
38
- def contact_for aalias; @a2p[aalias]; end
39
- def alias_for person; @p2a[person]; end
40
- def is_contact? person; @p2a.member? person; end
43
+ aalias = @p2a[person]
44
+ @p2a.delete person
45
+ @a2p.delete aalias if aalias
46
+ end
47
+
48
+ def contact_for aalias; @a2p[aalias] end
49
+ def alias_for person; @p2a[person] end
50
+ def is_aliased_contact? person; !@p2a[person].nil? end
41
51
 
42
52
  def save
43
53
  File.open(@fn, "w") do |f|
44
54
  @p2a.each do |p, a|
45
- f.puts "#{a}: #{p.full_address}"
55
+ f.puts "#{a || ''}: #{p.full_address}"
46
56
  end
47
57
  end
48
58
  end