sup 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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