sup 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTORS +84 -0
- data/Gemfile +3 -0
- data/HACKING +42 -0
- data/History.txt +361 -0
- data/LICENSE +280 -0
- data/README.md +70 -0
- data/Rakefile +12 -0
- data/ReleaseNotes +231 -0
- data/bin/sup +434 -0
- data/bin/sup-add +118 -0
- data/bin/sup-config +243 -0
- data/bin/sup-dump +43 -0
- data/bin/sup-import-dump +101 -0
- data/bin/sup-psych-ify-config-files +21 -0
- data/bin/sup-recover-sources +87 -0
- data/bin/sup-sync +210 -0
- data/bin/sup-sync-back-maildir +127 -0
- data/bin/sup-tweak-labels +140 -0
- data/contrib/colorpicker.rb +100 -0
- data/contrib/completion/_sup.zsh +114 -0
- data/devel/console.sh +3 -0
- data/devel/count-loc.sh +3 -0
- data/devel/load-index.rb +9 -0
- data/devel/profile.rb +12 -0
- data/devel/start-console.rb +5 -0
- data/doc/FAQ.txt +119 -0
- data/doc/Hooks.txt +79 -0
- data/doc/Philosophy.txt +69 -0
- data/lib/sup.rb +467 -0
- data/lib/sup/account.rb +90 -0
- data/lib/sup/buffer.rb +768 -0
- data/lib/sup/colormap.rb +239 -0
- data/lib/sup/contact.rb +67 -0
- data/lib/sup/crypto.rb +461 -0
- data/lib/sup/draft.rb +119 -0
- data/lib/sup/hook.rb +159 -0
- data/lib/sup/horizontal_selector.rb +59 -0
- data/lib/sup/idle.rb +42 -0
- data/lib/sup/index.rb +882 -0
- data/lib/sup/interactive_lock.rb +89 -0
- data/lib/sup/keymap.rb +140 -0
- data/lib/sup/label.rb +87 -0
- data/lib/sup/logger.rb +77 -0
- data/lib/sup/logger/singleton.rb +10 -0
- data/lib/sup/maildir.rb +257 -0
- data/lib/sup/mbox.rb +187 -0
- data/lib/sup/message.rb +803 -0
- data/lib/sup/message_chunks.rb +328 -0
- data/lib/sup/mode.rb +140 -0
- data/lib/sup/modes/buffer_list_mode.rb +50 -0
- data/lib/sup/modes/completion_mode.rb +55 -0
- data/lib/sup/modes/compose_mode.rb +38 -0
- data/lib/sup/modes/console_mode.rb +125 -0
- data/lib/sup/modes/contact_list_mode.rb +148 -0
- data/lib/sup/modes/edit_message_async_mode.rb +110 -0
- data/lib/sup/modes/edit_message_mode.rb +728 -0
- data/lib/sup/modes/file_browser_mode.rb +109 -0
- data/lib/sup/modes/forward_mode.rb +82 -0
- data/lib/sup/modes/help_mode.rb +19 -0
- data/lib/sup/modes/inbox_mode.rb +85 -0
- data/lib/sup/modes/label_list_mode.rb +138 -0
- data/lib/sup/modes/label_search_results_mode.rb +38 -0
- data/lib/sup/modes/line_cursor_mode.rb +203 -0
- data/lib/sup/modes/log_mode.rb +57 -0
- data/lib/sup/modes/person_search_results_mode.rb +12 -0
- data/lib/sup/modes/poll_mode.rb +19 -0
- data/lib/sup/modes/reply_mode.rb +228 -0
- data/lib/sup/modes/resume_mode.rb +52 -0
- data/lib/sup/modes/scroll_mode.rb +252 -0
- data/lib/sup/modes/search_list_mode.rb +204 -0
- data/lib/sup/modes/search_results_mode.rb +59 -0
- data/lib/sup/modes/text_mode.rb +76 -0
- data/lib/sup/modes/thread_index_mode.rb +1033 -0
- data/lib/sup/modes/thread_view_mode.rb +941 -0
- data/lib/sup/person.rb +134 -0
- data/lib/sup/poll.rb +272 -0
- data/lib/sup/rfc2047.rb +56 -0
- data/lib/sup/search.rb +110 -0
- data/lib/sup/sent.rb +58 -0
- data/lib/sup/service/label_service.rb +45 -0
- data/lib/sup/source.rb +244 -0
- data/lib/sup/tagger.rb +50 -0
- data/lib/sup/textfield.rb +253 -0
- data/lib/sup/thread.rb +452 -0
- data/lib/sup/time.rb +93 -0
- data/lib/sup/undo.rb +38 -0
- data/lib/sup/update.rb +30 -0
- data/lib/sup/util.rb +747 -0
- data/lib/sup/util/ncurses.rb +274 -0
- data/lib/sup/util/path.rb +9 -0
- data/lib/sup/util/query.rb +17 -0
- data/lib/sup/util/uri.rb +15 -0
- data/lib/sup/version.rb +3 -0
- data/sup.gemspec +53 -0
- data/test/dummy_source.rb +61 -0
- data/test/gnupg_test_home/gpg.conf +1 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
- data/test/gnupg_test_home/trustdb.gpg +0 -0
- data/test/integration/test_label_service.rb +18 -0
- data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
- data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
- data/test/messages/missing-line.eml +9 -0
- data/test/test_crypto.rb +109 -0
- data/test/test_header_parsing.rb +168 -0
- data/test/test_helper.rb +7 -0
- data/test/test_message.rb +532 -0
- data/test/test_messages_dir.rb +147 -0
- data/test/test_yaml_migration.rb +85 -0
- data/test/test_yaml_regressions.rb +17 -0
- data/test/unit/service/test_label_service.rb +19 -0
- data/test/unit/test_horizontal_selector.rb +40 -0
- data/test/unit/util/test_query.rb +46 -0
- data/test/unit/util/test_string.rb +57 -0
- data/test/unit/util/test_uri.rb +19 -0
- metadata +423 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class BufferListMode < LineCursorMode
|
4
|
+
register_keymap do |k|
|
5
|
+
k.add :jump_to_buffer, "Jump to selected buffer", :enter
|
6
|
+
k.add :reload, "Reload buffer list", "@"
|
7
|
+
k.add :kill_selected_buffer, "Kill selected buffer", "X"
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
regen_text
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def lines; @text.length end
|
16
|
+
def [] i; @text[i] end
|
17
|
+
|
18
|
+
def focus
|
19
|
+
reload # buffers may have been killed or created since last view
|
20
|
+
set_cursor_pos 0
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def reload
|
26
|
+
regen_text
|
27
|
+
buffer.mark_dirty
|
28
|
+
end
|
29
|
+
|
30
|
+
def regen_text
|
31
|
+
@bufs = BufferManager.buffers.reject { |name, buf| buf.mode == self || buf.hidden? }.sort_by { |name, buf| buf.atime }.reverse
|
32
|
+
width = @bufs.max_of { |name, buf| buf.mode.name.length }
|
33
|
+
@text = @bufs.map do |name, buf|
|
34
|
+
base_color = buf.system? ? :system_buf_color : :regular_buf_color
|
35
|
+
[[base_color, sprintf("%#{width}s ", buf.mode.name)],
|
36
|
+
[:modified_buffer_color, (buf.mode.unsaved? ? '*' : ' ')],
|
37
|
+
[base_color, " " + name]]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def jump_to_buffer
|
42
|
+
BufferManager.raise_to_front @bufs[curpos][1]
|
43
|
+
end
|
44
|
+
|
45
|
+
def kill_selected_buffer
|
46
|
+
reload if BufferManager.kill_buffer_safely @bufs[curpos][1]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class CompletionMode < ScrollMode
|
4
|
+
INTERSTITIAL = " "
|
5
|
+
|
6
|
+
def initialize list, opts={}
|
7
|
+
@list = list
|
8
|
+
@header = opts[:header]
|
9
|
+
@prefix_len = opts[:prefix_len]
|
10
|
+
@lines = nil
|
11
|
+
super :slip_rows => 1, :twiddles => false
|
12
|
+
end
|
13
|
+
|
14
|
+
def lines
|
15
|
+
update_lines unless @lines
|
16
|
+
@lines.length
|
17
|
+
end
|
18
|
+
|
19
|
+
def [] i
|
20
|
+
update_lines unless @lines
|
21
|
+
@lines[i]
|
22
|
+
end
|
23
|
+
|
24
|
+
def roll; if at_bottom? then jump_to_start else page_down end end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def update_lines
|
29
|
+
width = buffer.content_width
|
30
|
+
max_length = @list.max_of { |s| s.length }
|
31
|
+
num_per = [1, buffer.content_width / (max_length + INTERSTITIAL.length)].max
|
32
|
+
@lines = [@header].compact
|
33
|
+
@list.each_with_index do |s, i|
|
34
|
+
if @prefix_len
|
35
|
+
@lines << [] if i % num_per == 0
|
36
|
+
if @prefix_len < s.length
|
37
|
+
prefix = s[0 ... @prefix_len]
|
38
|
+
suffix = s[(@prefix_len + 1) .. -1]
|
39
|
+
char = s[@prefix_len].chr
|
40
|
+
|
41
|
+
@lines.last += [[:text_color, sprintf("%#{max_length - suffix.length - 1}s", prefix)],
|
42
|
+
[:completion_character_color, char],
|
43
|
+
[:text_color, suffix + INTERSTITIAL]]
|
44
|
+
else
|
45
|
+
@lines.last += [[:text_color, sprintf("%#{max_length}s#{INTERSTITIAL}", s)]]
|
46
|
+
end
|
47
|
+
else
|
48
|
+
@lines << "" if i % num_per == 0
|
49
|
+
@lines.last += sprintf "%#{max_length}s#{INTERSTITIAL}", s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Redwood
|
4
|
+
|
5
|
+
class ComposeMode < EditMessageMode
|
6
|
+
def initialize opts={}
|
7
|
+
header = {}
|
8
|
+
header["From"] = (opts[:from] || AccountManager.default_account).full_address
|
9
|
+
header["To"] = opts[:to].map { |p| p.full_address }.join(", ") if opts[:to]
|
10
|
+
header["Cc"] = opts[:cc].map { |p| p.full_address }.join(", ") if opts[:cc]
|
11
|
+
header["Bcc"] = opts[:bcc].map { |p| p.full_address }.join(", ") if opts[:bcc]
|
12
|
+
header["Subject"] = opts[:subj] if opts[:subj]
|
13
|
+
header["References"] = opts[:refs].map { |r| "<#{r}>" }.join(" ") if opts[:refs]
|
14
|
+
header["In-Reply-To"] = opts[:replytos].map { |r| "<#{r}>" }.join(" ") if opts[:replytos]
|
15
|
+
|
16
|
+
super :header => header, :body => (opts[:body] || [])
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_edit_message
|
20
|
+
edited = super
|
21
|
+
BufferManager.kill_buffer self.buffer unless edited
|
22
|
+
edited
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.spawn_nicely opts={}
|
26
|
+
from = opts[:from] || (BufferManager.ask_for_account(:account, "From (default #{AccountManager.default_account.email}): ") or return if $config[:ask_for_from])
|
27
|
+
to = opts[:to] || (BufferManager.ask_for_contacts(:people, "To: ", [opts[:to_default]]) or return if ($config[:ask_for_to] != false))
|
28
|
+
cc = opts[:cc] || (BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc])
|
29
|
+
bcc = opts[:bcc] || (BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc])
|
30
|
+
subj = opts[:subj] || (BufferManager.ask(:subject, "Subject: ") or return if $config[:ask_for_subject])
|
31
|
+
|
32
|
+
mode = ComposeMode.new :from => from, :to => to, :cc => cc, :bcc => bcc, :subj => subj
|
33
|
+
BufferManager.spawn "New Message", mode
|
34
|
+
mode.default_edit_message
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'pp'
|
2
|
+
|
3
|
+
require "sup/service/label_service"
|
4
|
+
|
5
|
+
module Redwood
|
6
|
+
|
7
|
+
class Console
|
8
|
+
def initialize mode
|
9
|
+
@mode = mode
|
10
|
+
@label_service = LabelService.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def query(query)
|
14
|
+
Enumerator.new(Index.instance, :each_message, Index.parse_query(query))
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_labels(query, *labels)
|
18
|
+
count = @label_service.add_labels(query, *labels)
|
19
|
+
print_buffer_dirty_msg count
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove_labels(query, *labels)
|
23
|
+
count = @label_service.remove_labels(query, *labels)
|
24
|
+
print_buffer_dirty_msg count
|
25
|
+
end
|
26
|
+
|
27
|
+
def print_buffer_dirty_msg msg_count
|
28
|
+
puts "Scanned #{msg_count} messages."
|
29
|
+
puts "You might want to refresh open buffers with `@` key."
|
30
|
+
end
|
31
|
+
private :print_buffer_dirty_msg
|
32
|
+
|
33
|
+
def xapian; Index.instance.instance_variable_get :@xapian; end
|
34
|
+
|
35
|
+
def loglevel; Redwood::Logger.level; end
|
36
|
+
def set_loglevel(level); Redwood::Logger.level = level; end
|
37
|
+
|
38
|
+
def special_methods; public_methods - Object.methods end
|
39
|
+
|
40
|
+
def puts x; @mode << "#{x.to_s.rstrip}\n" end
|
41
|
+
def p x; puts x.inspect end
|
42
|
+
|
43
|
+
## files that won't cause problems when reloaded
|
44
|
+
## TODO expand this list / convert to blacklist
|
45
|
+
RELOAD_WHITELIST = %w(sup/index.rb sup/modes/console-mode.rb)
|
46
|
+
|
47
|
+
def reload
|
48
|
+
old_verbose = $VERBOSE
|
49
|
+
$VERBOSE = nil
|
50
|
+
old_features = $".dup
|
51
|
+
begin
|
52
|
+
fs = $".grep(/^sup\//)
|
53
|
+
fs.reject! { |f| not RELOAD_WHITELIST.member? f }
|
54
|
+
fs.each { |f| $".delete f }
|
55
|
+
fs.each do |f|
|
56
|
+
@mode << "reloading #{f}\n"
|
57
|
+
begin
|
58
|
+
require f
|
59
|
+
rescue LoadError => e
|
60
|
+
raise unless e.message =~ /no such file to load/
|
61
|
+
end
|
62
|
+
end
|
63
|
+
rescue Exception
|
64
|
+
$".clear
|
65
|
+
$".concat old_features
|
66
|
+
raise
|
67
|
+
ensure
|
68
|
+
$VERBOSE = old_verbose
|
69
|
+
end
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def clear_hooks
|
74
|
+
HookManager.clear
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class ConsoleMode < LogMode
|
80
|
+
register_keymap do |k|
|
81
|
+
k.add :run, "Restart evaluation", 'e'
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize
|
85
|
+
super "console"
|
86
|
+
@console = Console.new self
|
87
|
+
@binding = @console.instance_eval { binding }
|
88
|
+
end
|
89
|
+
|
90
|
+
def execute cmd
|
91
|
+
begin
|
92
|
+
self << ">> #{cmd}\n"
|
93
|
+
ret = eval cmd, @binding
|
94
|
+
self << "=> #{ret.pretty_inspect}\n"
|
95
|
+
rescue Exception
|
96
|
+
self << "#{$!.class}: #{$!.message}\n"
|
97
|
+
clean_backtrace = []
|
98
|
+
$!.backtrace.each { |l| break if l =~ /console-mode/; clean_backtrace << l }
|
99
|
+
clean_backtrace.each { |l| self << "#{l}\n" }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def prompt
|
104
|
+
BufferManager.ask :console, ">> "
|
105
|
+
end
|
106
|
+
|
107
|
+
def run
|
108
|
+
self << <<EOS
|
109
|
+
Sup v#{VERSION} console session started.
|
110
|
+
Available extra commands: #{(@console.special_methods) * ", "}
|
111
|
+
Ctrl-G stops evaluation; 'e' restarts it.
|
112
|
+
|
113
|
+
EOS
|
114
|
+
while true
|
115
|
+
if(cmd = prompt)
|
116
|
+
execute cmd
|
117
|
+
else
|
118
|
+
self << "Console session ended."
|
119
|
+
break
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
module CanAliasContacts
|
4
|
+
def alias_contact p
|
5
|
+
aalias = BufferManager.ask(:alias, "Alias for #{p.longname}: ", ContactManager.alias_for(p))
|
6
|
+
return if aalias.nil?
|
7
|
+
aalias = nil if aalias.empty? # allow empty aliases
|
8
|
+
|
9
|
+
name = BufferManager.ask(:name, "Name for #{p.longname}: ", p.name)
|
10
|
+
return if name.nil? || name.empty? # don't allow empty names
|
11
|
+
p.name = name
|
12
|
+
|
13
|
+
ContactManager.update_alias p, aalias
|
14
|
+
BufferManager.flash "Contact updated!"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class ContactListMode < LineCursorMode
|
19
|
+
LOAD_MORE_CONTACTS_NUM = 100
|
20
|
+
|
21
|
+
register_keymap do |k|
|
22
|
+
k.add :load_more, "Load #{LOAD_MORE_CONTACTS_NUM} more contacts", 'M'
|
23
|
+
k.add :reload, "Drop contact list and reload", 'D'
|
24
|
+
k.add :alias, "Edit alias/or name for contact", 'a', 'i'
|
25
|
+
k.add :toggle_tagged, "Tag/untag current line", 't'
|
26
|
+
k.add :apply_to_tagged, "Apply next command to all tagged items", '+'
|
27
|
+
k.add :search, "Search for messages from particular people", 'S'
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize mode=:regular
|
31
|
+
@mode = mode
|
32
|
+
@tags = Tagger.new self, "contact"
|
33
|
+
@num = nil
|
34
|
+
@text = []
|
35
|
+
super()
|
36
|
+
end
|
37
|
+
|
38
|
+
include CanAliasContacts
|
39
|
+
def alias
|
40
|
+
p = @contacts[curpos] or return
|
41
|
+
alias_contact p
|
42
|
+
update
|
43
|
+
end
|
44
|
+
|
45
|
+
def lines; @text.length; end
|
46
|
+
def [] i; @text[i]; end
|
47
|
+
|
48
|
+
def toggle_tagged
|
49
|
+
p = @contacts[curpos] or return
|
50
|
+
@tags.toggle_tag_for p
|
51
|
+
update_text_for_line curpos
|
52
|
+
cursor_down
|
53
|
+
end
|
54
|
+
|
55
|
+
def multi_toggle_tagged threads
|
56
|
+
@tags.drop_all_tags
|
57
|
+
update
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply_to_tagged; @tags.apply_to_tagged; end
|
61
|
+
|
62
|
+
def load_more num=LOAD_MORE_CONTACTS_NUM
|
63
|
+
@num += num
|
64
|
+
load
|
65
|
+
update
|
66
|
+
BufferManager.flash "Added #{num.pluralize 'contact'}."
|
67
|
+
end
|
68
|
+
|
69
|
+
def multi_select people
|
70
|
+
case @mode
|
71
|
+
when :regular
|
72
|
+
mode = ComposeMode.new :to => people
|
73
|
+
BufferManager.spawn "new message", mode
|
74
|
+
mode.default_edit_message
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def select
|
79
|
+
p = @contacts[curpos] or return
|
80
|
+
multi_select [p]
|
81
|
+
end
|
82
|
+
|
83
|
+
def multi_search people
|
84
|
+
mode = PersonSearchResultsMode.new people
|
85
|
+
BufferManager.spawn "search for #{people.map { |p| p.name }.join(', ')}", mode
|
86
|
+
mode.load_threads :num => mode.buffer.content_height
|
87
|
+
end
|
88
|
+
|
89
|
+
def search
|
90
|
+
p = @contacts[curpos] or return
|
91
|
+
multi_search [p]
|
92
|
+
end
|
93
|
+
|
94
|
+
def reload
|
95
|
+
@tags.drop_all_tags
|
96
|
+
@num = nil
|
97
|
+
load
|
98
|
+
end
|
99
|
+
|
100
|
+
def load_in_background
|
101
|
+
Redwood::reporting_thread("contact manager load in bg") do
|
102
|
+
load
|
103
|
+
update
|
104
|
+
BufferManager.draw_screen
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def load
|
109
|
+
@num ||= (buffer.content_height * 2)
|
110
|
+
@user_contacts = ContactManager.contacts_with_aliases
|
111
|
+
num = [@num - @user_contacts.length, 0].max
|
112
|
+
BufferManager.say("Loading #{num} contacts from index...") do
|
113
|
+
recentc = Index.load_contacts AccountManager.user_emails, :num => num
|
114
|
+
@contacts = (@user_contacts + recentc).sort_by { |p| p.sort_by_me }.uniq
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
def update
|
121
|
+
regen_text
|
122
|
+
buffer.mark_dirty if buffer
|
123
|
+
end
|
124
|
+
|
125
|
+
def update_text_for_line line
|
126
|
+
@text[line] = text_for_contact @contacts[line]
|
127
|
+
buffer.mark_dirty if buffer
|
128
|
+
end
|
129
|
+
|
130
|
+
def text_for_contact p
|
131
|
+
aalias = ContactManager.alias_for(p) || ""
|
132
|
+
[[:tagged_color, @tags.tagged?(p) ? ">" : " "],
|
133
|
+
[:text_color, sprintf("%-#{@awidth}s %-#{@nwidth}s %s", aalias, p.name, p.email)]]
|
134
|
+
end
|
135
|
+
|
136
|
+
def regen_text
|
137
|
+
@awidth, @nwidth = 0, 0
|
138
|
+
@contacts.each do |p|
|
139
|
+
aalias = ContactManager.alias_for(p)
|
140
|
+
@awidth = aalias.length if aalias && aalias.length > @awidth
|
141
|
+
@nwidth = p.name.length if p.name && p.name.length > @nwidth
|
142
|
+
end
|
143
|
+
|
144
|
+
@text = @contacts.map { |p| text_for_contact p }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class EditMessageAsyncMode < LineCursorMode
|
4
|
+
|
5
|
+
HookManager.register "async-edit", <<EOS
|
6
|
+
Runs when 'H' is pressed in async edit mode. You can run whatever code
|
7
|
+
you want here - though the default case would be launching a text
|
8
|
+
editor. Your hook is assumed to not block, so you should use exec() or
|
9
|
+
fork() to launch the editor.
|
10
|
+
|
11
|
+
Once the hook has returned then sup will be responsive as usual. You will
|
12
|
+
still need to press 'E' to exit this buffer and send the message.
|
13
|
+
|
14
|
+
Variables:
|
15
|
+
file_path: The full path to the file containing the message to be edited.
|
16
|
+
|
17
|
+
Return value: None
|
18
|
+
EOS
|
19
|
+
|
20
|
+
register_keymap do |k|
|
21
|
+
k.add :run_async_hook, "Run the async-edit hook", 'H'
|
22
|
+
k.add :edit_finished, "Finished editing message", 'E'
|
23
|
+
k.add :path_to_clipboard, "Copy file path to the clipboard", :enter
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize parent_edit_mode, file_path, msg_subject
|
27
|
+
@parent_edit_mode = parent_edit_mode
|
28
|
+
@file_path = file_path
|
29
|
+
@orig_mtime = File.mtime @file_path
|
30
|
+
|
31
|
+
@text = ["ASYNC MESSAGE EDIT",
|
32
|
+
"", "Your message with subject:", msg_subject, "is saved in a file:", "", @file_path, "",
|
33
|
+
"You can edit your message in the editor of your choice and continue to",
|
34
|
+
"use sup while you edit your message.", "",
|
35
|
+
"Press <Enter> to have the file path copied to the clipboard.", "",
|
36
|
+
"When you have finished editing, select this buffer and press 'E'.",]
|
37
|
+
run_async_hook()
|
38
|
+
super()
|
39
|
+
end
|
40
|
+
|
41
|
+
def lines; @text.length end
|
42
|
+
|
43
|
+
def [] i
|
44
|
+
@text[i]
|
45
|
+
end
|
46
|
+
|
47
|
+
def killable?
|
48
|
+
if file_being_edited?
|
49
|
+
if !BufferManager.ask_yes_or_no("It appears the file is still being edited. Are you sure?")
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
@parent_edit_mode.edit_message_async_resume true
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
def unsaved?
|
59
|
+
!file_being_edited? && !file_has_been_edited?
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
def edit_finished
|
65
|
+
if file_being_edited?
|
66
|
+
if !BufferManager.ask_yes_or_no("It appears the file is still being edited. Are you sure?")
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
@parent_edit_mode.edit_message_async_resume
|
72
|
+
BufferManager.kill_buffer buffer
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
def path_to_clipboard
|
77
|
+
if system("which xsel > /dev/null 2>&1")
|
78
|
+
# linux/unix path
|
79
|
+
IO.popen('xsel --clipboard --input', 'r+') { |clipboard| clipboard.puts(@file_path) }
|
80
|
+
BufferManager.flash "Copied file path to clipboard."
|
81
|
+
elsif system("which pbcopy > /dev/null 2>&1")
|
82
|
+
# mac path
|
83
|
+
IO.popen('pbcopy', 'r+') { |clipboard| clipboard.puts(@file_path) }
|
84
|
+
BufferManager.flash "Copied file path to clipboard."
|
85
|
+
else
|
86
|
+
BufferManager.flash "No way to copy text to clipboard - try installing xsel."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def run_async_hook
|
91
|
+
HookManager.run("async-edit", {:file_path => @file_path})
|
92
|
+
end
|
93
|
+
|
94
|
+
def file_being_edited?
|
95
|
+
# check for common editor lock files
|
96
|
+
vim_lock_file = File.join(File.dirname(@file_path), '.'+File.basename(@file_path)+'.swp')
|
97
|
+
emacs_lock_file = File.join(File.dirname(@file_path), '.#'+File.basename(@file_path))
|
98
|
+
|
99
|
+
return true if File.exist?(vim_lock_file) || File.exist?(emacs_lock_file)
|
100
|
+
|
101
|
+
false
|
102
|
+
end
|
103
|
+
|
104
|
+
def file_has_been_edited?
|
105
|
+
File.mtime(@file_path) > @orig_mtime
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|