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.
- data/HACKING +31 -9
- data/History.txt +7 -0
- data/Manifest.txt +2 -0
- data/Rakefile +9 -5
- data/bin/sup +81 -57
- data/bin/sup-config +1 -1
- data/bin/sup-sync +3 -0
- data/bin/sup-tweak-labels +127 -0
- data/doc/TODO +23 -12
- data/lib/sup.rb +13 -11
- data/lib/sup/account.rb +25 -12
- data/lib/sup/buffer.rb +61 -41
- data/lib/sup/colormap.rb +2 -0
- data/lib/sup/contact.rb +28 -18
- data/lib/sup/crypto.rb +86 -31
- data/lib/sup/draft.rb +12 -6
- data/lib/sup/horizontal-selector.rb +47 -0
- data/lib/sup/imap.rb +50 -37
- data/lib/sup/index.rb +76 -13
- data/lib/sup/keymap.rb +27 -8
- data/lib/sup/maildir.rb +1 -1
- data/lib/sup/mbox/loader.rb +1 -1
- data/lib/sup/message-chunks.rb +43 -15
- data/lib/sup/message.rb +67 -31
- data/lib/sup/mode.rb +40 -9
- data/lib/sup/modes/completion-mode.rb +1 -1
- data/lib/sup/modes/compose-mode.rb +3 -3
- data/lib/sup/modes/contact-list-mode.rb +12 -8
- data/lib/sup/modes/edit-message-mode.rb +100 -36
- data/lib/sup/modes/file-browser-mode.rb +1 -0
- data/lib/sup/modes/forward-mode.rb +43 -8
- data/lib/sup/modes/inbox-mode.rb +8 -5
- data/lib/sup/modes/label-search-results-mode.rb +12 -1
- data/lib/sup/modes/line-cursor-mode.rb +4 -7
- data/lib/sup/modes/reply-mode.rb +59 -54
- data/lib/sup/modes/resume-mode.rb +6 -6
- data/lib/sup/modes/scroll-mode.rb +4 -3
- data/lib/sup/modes/search-results-mode.rb +8 -5
- data/lib/sup/modes/text-mode.rb +19 -2
- data/lib/sup/modes/thread-index-mode.rb +109 -40
- data/lib/sup/modes/thread-view-mode.rb +180 -49
- data/lib/sup/person.rb +3 -3
- data/lib/sup/poll.rb +9 -8
- data/lib/sup/rfc2047.rb +7 -1
- data/lib/sup/sent.rb +1 -1
- data/lib/sup/tagger.rb +10 -4
- data/lib/sup/textfield.rb +7 -7
- data/lib/sup/thread.rb +86 -49
- data/lib/sup/update.rb +11 -0
- data/lib/sup/util.rb +74 -34
- data/test/test_message.rb +441 -0
- 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.
|
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"
|
data/lib/sup/account.rb
CHANGED
@@ -3,8 +3,10 @@ module Redwood
|
|
3
3
|
class Account < Person
|
4
4
|
attr_accessor :sendmail, :signature
|
5
5
|
|
6
|
-
def initialize
|
7
|
-
|
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
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
data/lib/sup/buffer.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
508
|
+
@textfields[domain] ||= TextField.new
|
508
509
|
tf = @textfields[domain]
|
509
510
|
completion_buf = nil
|
510
511
|
|
511
|
-
|
512
|
-
|
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 :
|
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
|
-
|
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
|
-
|
564
|
+
status, title = get_status_and_title @focus_buf
|
562
565
|
Ncurses.sync do
|
563
|
-
|
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
|
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
|
-
@
|
582
|
-
|
585
|
+
@asking = false
|
583
586
|
Ncurses.sync do
|
584
587
|
Ncurses.curs_set 0
|
585
|
-
|
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
|
data/lib/sup/colormap.rb
CHANGED
data/lib/sup/contact.rb
CHANGED
@@ -5,44 +5,54 @@ class ContactManager
|
|
5
5
|
|
6
6
|
def initialize fn
|
7
7
|
@fn = fn
|
8
|
-
|
9
|
-
|
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 =~ /^([^:]
|
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
|
25
|
-
def
|
26
|
-
|
27
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
def contact_for aalias; @a2p[aalias]
|
39
|
-
def alias_for person; @p2a[person]
|
40
|
-
def
|
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
|