sup 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|