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.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +12 -0
  4. data/CONTRIBUTORS +84 -0
  5. data/Gemfile +3 -0
  6. data/HACKING +42 -0
  7. data/History.txt +361 -0
  8. data/LICENSE +280 -0
  9. data/README.md +70 -0
  10. data/Rakefile +12 -0
  11. data/ReleaseNotes +231 -0
  12. data/bin/sup +434 -0
  13. data/bin/sup-add +118 -0
  14. data/bin/sup-config +243 -0
  15. data/bin/sup-dump +43 -0
  16. data/bin/sup-import-dump +101 -0
  17. data/bin/sup-psych-ify-config-files +21 -0
  18. data/bin/sup-recover-sources +87 -0
  19. data/bin/sup-sync +210 -0
  20. data/bin/sup-sync-back-maildir +127 -0
  21. data/bin/sup-tweak-labels +140 -0
  22. data/contrib/colorpicker.rb +100 -0
  23. data/contrib/completion/_sup.zsh +114 -0
  24. data/devel/console.sh +3 -0
  25. data/devel/count-loc.sh +3 -0
  26. data/devel/load-index.rb +9 -0
  27. data/devel/profile.rb +12 -0
  28. data/devel/start-console.rb +5 -0
  29. data/doc/FAQ.txt +119 -0
  30. data/doc/Hooks.txt +79 -0
  31. data/doc/Philosophy.txt +69 -0
  32. data/lib/sup.rb +467 -0
  33. data/lib/sup/account.rb +90 -0
  34. data/lib/sup/buffer.rb +768 -0
  35. data/lib/sup/colormap.rb +239 -0
  36. data/lib/sup/contact.rb +67 -0
  37. data/lib/sup/crypto.rb +461 -0
  38. data/lib/sup/draft.rb +119 -0
  39. data/lib/sup/hook.rb +159 -0
  40. data/lib/sup/horizontal_selector.rb +59 -0
  41. data/lib/sup/idle.rb +42 -0
  42. data/lib/sup/index.rb +882 -0
  43. data/lib/sup/interactive_lock.rb +89 -0
  44. data/lib/sup/keymap.rb +140 -0
  45. data/lib/sup/label.rb +87 -0
  46. data/lib/sup/logger.rb +77 -0
  47. data/lib/sup/logger/singleton.rb +10 -0
  48. data/lib/sup/maildir.rb +257 -0
  49. data/lib/sup/mbox.rb +187 -0
  50. data/lib/sup/message.rb +803 -0
  51. data/lib/sup/message_chunks.rb +328 -0
  52. data/lib/sup/mode.rb +140 -0
  53. data/lib/sup/modes/buffer_list_mode.rb +50 -0
  54. data/lib/sup/modes/completion_mode.rb +55 -0
  55. data/lib/sup/modes/compose_mode.rb +38 -0
  56. data/lib/sup/modes/console_mode.rb +125 -0
  57. data/lib/sup/modes/contact_list_mode.rb +148 -0
  58. data/lib/sup/modes/edit_message_async_mode.rb +110 -0
  59. data/lib/sup/modes/edit_message_mode.rb +728 -0
  60. data/lib/sup/modes/file_browser_mode.rb +109 -0
  61. data/lib/sup/modes/forward_mode.rb +82 -0
  62. data/lib/sup/modes/help_mode.rb +19 -0
  63. data/lib/sup/modes/inbox_mode.rb +85 -0
  64. data/lib/sup/modes/label_list_mode.rb +138 -0
  65. data/lib/sup/modes/label_search_results_mode.rb +38 -0
  66. data/lib/sup/modes/line_cursor_mode.rb +203 -0
  67. data/lib/sup/modes/log_mode.rb +57 -0
  68. data/lib/sup/modes/person_search_results_mode.rb +12 -0
  69. data/lib/sup/modes/poll_mode.rb +19 -0
  70. data/lib/sup/modes/reply_mode.rb +228 -0
  71. data/lib/sup/modes/resume_mode.rb +52 -0
  72. data/lib/sup/modes/scroll_mode.rb +252 -0
  73. data/lib/sup/modes/search_list_mode.rb +204 -0
  74. data/lib/sup/modes/search_results_mode.rb +59 -0
  75. data/lib/sup/modes/text_mode.rb +76 -0
  76. data/lib/sup/modes/thread_index_mode.rb +1033 -0
  77. data/lib/sup/modes/thread_view_mode.rb +941 -0
  78. data/lib/sup/person.rb +134 -0
  79. data/lib/sup/poll.rb +272 -0
  80. data/lib/sup/rfc2047.rb +56 -0
  81. data/lib/sup/search.rb +110 -0
  82. data/lib/sup/sent.rb +58 -0
  83. data/lib/sup/service/label_service.rb +45 -0
  84. data/lib/sup/source.rb +244 -0
  85. data/lib/sup/tagger.rb +50 -0
  86. data/lib/sup/textfield.rb +253 -0
  87. data/lib/sup/thread.rb +452 -0
  88. data/lib/sup/time.rb +93 -0
  89. data/lib/sup/undo.rb +38 -0
  90. data/lib/sup/update.rb +30 -0
  91. data/lib/sup/util.rb +747 -0
  92. data/lib/sup/util/ncurses.rb +274 -0
  93. data/lib/sup/util/path.rb +9 -0
  94. data/lib/sup/util/query.rb +17 -0
  95. data/lib/sup/util/uri.rb +15 -0
  96. data/lib/sup/version.rb +3 -0
  97. data/sup.gemspec +53 -0
  98. data/test/dummy_source.rb +61 -0
  99. data/test/gnupg_test_home/gpg.conf +1 -0
  100. data/test/gnupg_test_home/pubring.gpg +0 -0
  101. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  102. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  103. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  104. data/test/gnupg_test_home/secring.gpg +0 -0
  105. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
  106. data/test/gnupg_test_home/trustdb.gpg +0 -0
  107. data/test/integration/test_label_service.rb +18 -0
  108. data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
  109. data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
  110. data/test/messages/missing-line.eml +9 -0
  111. data/test/test_crypto.rb +109 -0
  112. data/test/test_header_parsing.rb +168 -0
  113. data/test/test_helper.rb +7 -0
  114. data/test/test_message.rb +532 -0
  115. data/test/test_messages_dir.rb +147 -0
  116. data/test/test_yaml_migration.rb +85 -0
  117. data/test/test_yaml_regressions.rb +17 -0
  118. data/test/unit/service/test_label_service.rb +19 -0
  119. data/test/unit/test_horizontal_selector.rb +40 -0
  120. data/test/unit/util/test_query.rb +46 -0
  121. data/test/unit/util/test_string.rb +57 -0
  122. data/test/unit/util/test_uri.rb +19 -0
  123. 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