bin/sup (12403B) - raw
1 #!/usr/bin/env ruby
2 # encoding: utf-8
3
4 $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
5
6 require 'ncursesw'
7
8 require 'sup/util/ncurses'
9 require 'sup/util/locale_fiddler'
10 require 'sup/util/axe'
11
12 no_gpgme = false
13 begin
14 require 'gpgme'
15 rescue LoadError
16 no_gpgme = true
17 end
18
19 require 'fileutils'
20 require 'optimist'
21 require "sup"
22
23 if ENV['SUP_PROFILE']
24 require 'ruby-prof'
25 RubyProf.start
26 end
27
28 if no_gpgme
29 info "No 'gpgme' gem detected. Install it for email encryption, decryption and signatures."
30 end
31
32 $opts = Optimist::options do
33 version "sup v#{Redwood::VERSION}"
34 banner <<EOS
35 Sup is a curses-based email client.
36
37 Usage:
38 sup [options]
39
40 Options are:
41 EOS
42 opt :list_hooks, "List all hooks and descriptions, and quit. Use --hooks-matching to filter."
43 opt :hooks_matching, "If given, list all hooks and descriptions matching the given pattern. Needs the --list-hooks option", short: "m", default: ""
44 opt :no_threads, "Turn off threading. Helps with debugging. (Necessarily disables background polling for new messages.)"
45 opt :no_initial_poll, "Don't poll for new messages when starting."
46 opt :search, "Search for this query upon startup", :type => String
47 opt :compose, "Compose message to this recipient upon startup", :type => String
48 opt :subject, "When composing, use this subject", :type => String, :short => "j"
49 end
50
51 Optimist::die :subject, "requires --compose" if $opts[:subject] && !$opts[:compose]
52
53 Redwood::HookManager.register "startup", <<EOS
54 Executes at startup
55 No variables.
56 No return value.
57 EOS
58
59 Redwood::HookManager.register "shutdown", <<EOS
60 Executes when sup is shutting down. May be run when sup is crashing,
61 so don\'t do anything too important. Run before the label, contacts,
62 and people are saved.
63 No variables.
64 No return value.
65 EOS
66
67 if $opts[:list_hooks]
68 Redwood.start
69 Redwood::HookManager.print_hooks $opts[:hooks_matching]
70 exit
71 end
72
73 Thread.abort_on_exception = true # make debugging possible
74 Thread.current.priority = 1 # keep ui responsive
75
76 module Redwood
77
78 global_keymap = Keymap.new do |k|
79 k.add :quit_ask, "Quit Sup, but ask first", 'q'
80 k.add :quit_now, "Quit Sup immediately", 'Q'
81 k.add :help, "Show help", '?'
82 k.add :roll_buffers, "Switch to next buffer", 'b'
83 k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
84 k.add :kill_buffer, "Kill the current buffer", 'x'
85 k.add :list_buffers, "List all buffers", ';'
86 k.add :list_contacts, "List contacts", 'C'
87 k.add :redraw, "Redraw screen", :ctrl_l
88 k.add :search, "Search all messages", '\\', 'F'
89 k.add :search_unread, "Show all unread messages", 'U'
90 k.add :list_labels, "List labels", 'L'
91 k.add :poll, "Poll for new messages", 'P'
92 k.add :poll_unusual, "Poll for new messages from unusual sources", '{'
93 k.add :compose, "Compose new message", 'm', 'c'
94 k.add :nothing, "Do nothing", :ctrl_g
95 k.add :recall_draft, "Edit most recent draft message", 'R'
96 k.add :show_inbox, "Show the Inbox buffer", 'I'
97 k.add :clear_hooks, "Clear all hooks", 'H'
98 k.add :show_console, "Show the Console buffer", '~'
99
100 ## Submap for less often used keybindings
101 k.add_multi "reload (c)olors, rerun (k)eybindings hook", 'O' do |kk|
102 kk.add :reload_colors, "Reload colors", 'c'
103 kk.add :run_keybindings_hook, "Rerun keybindings hook", 'k'
104 end
105 end
106
107 require 'rbconfig'
108
109 unless RbConfig::CONFIG['arch'] =~ /openbsd/
110 debug "dynamically loading setlocale()"
111 begin
112 class LibC; extend LocaleFiddler; end
113 debug "setting locale..."
114 LibC.setlocale(6, "")
115 rescue RuntimeError => e
116 warn "cannot dlload setlocale(); ncurses wide character support probably broken."
117 warn "dlload error was #{e.class}: #{e.message}"
118 end
119 end
120
121 def start_cursing
122 Ncurses.initscr
123 Ncurses.noecho
124 Ncurses.cbreak
125 Ncurses.stdscr.keypad 1
126 Ncurses.use_default_colors
127 Ncurses.curs_set 0
128 Ncurses.start_color
129 Ncurses.prepare_form_driver
130 $cursing = true
131 end
132
133 def stop_cursing
134 return unless $cursing
135 Ncurses.curs_set 1
136 Ncurses.echo
137 Ncurses.endwin
138 end
139 module_function :start_cursing, :stop_cursing
140
141 Index.init
142 Index.lock_interactively or exit
143
144 begin
145 Redwood::start
146 Index.load
147 Redwood::check_syncback_settings
148 Index.start_sync_worker unless $opts[:no_threads]
149
150 $die = false
151 trap("TERM") { |x| $die = true }
152 trap("WINCH") do |x|
153 ::Thread.new do
154 BufferManager.sigwinch_happened!
155 end
156 end
157
158 if(s = Redwood::SourceManager.source_for DraftManager.source_name)
159 DraftManager.source = s
160 else
161 debug "no draft source, auto-adding..."
162 Redwood::SourceManager.add_source DraftManager.new_source
163 end
164
165 if(s = Redwood::SourceManager.source_for SentManager.source_uri)
166 SentManager.source = s
167 else
168 Redwood::SourceManager.add_source SentManager.default_source
169 end
170
171 HookManager.run "startup"
172 Redwood::Keymap.run_hook global_keymap
173
174 debug "starting curses"
175 Redwood::Logger.remove_sink $stderr
176 start_cursing
177
178 bm = BufferManager.init
179 Colormap.new.populate_colormap
180
181 debug "initializing log buffer"
182 lmode = Redwood::LogMode.new "system log"
183 lmode.on_kill { Logger.clear! }
184 Logger.add_sink lmode
185 Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
186 if Logger::LEVELS.index(Logger.level) > 0
187 Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}."
188 end
189
190 debug "initializing inbox buffer"
191 imode = InboxMode.new
192 ibuf = bm.spawn "Inbox", imode
193
194 debug "ready for interaction!"
195
196 bm.draw_screen
197
198 Redwood::SourceManager.usual_sources.each do |s|
199 next unless s.respond_to? :connect
200 reporting_thread("call #connect on #{s}") do
201 begin
202 s.connect
203 rescue SourceError => e
204 error "fatal error loading from #{s}: #{e.message}"
205 end
206 end
207 end unless $opts[:no_initial_poll]
208
209 reporting_thread("poll after loading inbox") do
210 sleep 1
211 PollManager.poll
212 end unless $opts[:no_threads] || $opts[:no_initial_poll]
213
214 if $opts[:compose]
215 to = Person.from_address_list $opts[:compose]
216 mode = ComposeMode.new :to => to, :subj => $opts[:subject]
217 BufferManager.spawn "New Message", mode
218 mode.default_edit_message
219 end
220
221 unless $opts[:no_threads]
222 PollManager.start
223 IdleManager.start
224 Index.start_lock_update_thread
225 end
226
227 if $opts[:search]
228 SearchResultsMode.spawn_from_query $opts[:search]
229 end
230
231 until Redwood::exceptions.nonempty? || $die
232 c = begin
233 Ncurses::CharCode.get false
234 rescue Interrupt
235 raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
236 BufferManager.draw_screen
237 Ncurses::CharCode.empty
238 end
239
240 if c.empty?
241 if BufferManager.sigwinch_happened?
242 debug "redrawing screen on sigwinch"
243 BufferManager.completely_redraw_screen
244 end
245 next
246 end
247
248 IdleManager.ping
249
250 if c.is_keycode? 410
251 ## this is ncurses's way of telling us it's detected a refresh.
252 ## since we have our own sigwinch handler, we don't do anything.
253 next
254 end
255
256 bm.erase_flash
257
258 action =
259 begin
260 if bm.handle_input c
261 :nothing
262 else
263 bm.resolve_input_with_keymap c, global_keymap
264 end
265 rescue InputSequenceAborted
266 :nothing
267 end
268 case action
269 when :quit_now
270 break if bm.kill_all_buffers_safely
271 when :quit_ask
272 if bm.ask_yes_or_no "Really quit?"
273 break if bm.kill_all_buffers_safely
274 end
275 when :help
276 curmode = bm.focus_buf.mode
277 bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
278 when :roll_buffers
279 bm.roll_buffers
280 when :roll_buffers_backwards
281 bm.roll_buffers_backwards
282 when :kill_buffer
283 bm.kill_buffer_safely bm.focus_buf
284 when :list_buffers
285 bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
286 when :list_contacts
287 b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
288 b.mode.load_in_background if new
289 when :search
290 completions = LabelManager.all_labels.map { |l| "label:#{LabelManager.string_for l}" }
291 completions = completions.each { |l| l.fix_encoding! }
292 completions += Index::COMPL_PREFIXES
293 query = BufferManager.ask_many_with_completions :search, "Search all messages (enter for saved searches): ", completions
294 unless query.nil?
295 if query.empty?
296 bm.spawn_unless_exists("Saved searches") { SearchListMode.new }
297 else
298 SearchResultsMode.spawn_from_query query
299 end
300 end
301 when :search_unread
302 SearchResultsMode.spawn_from_query "is:unread"
303 when :list_labels
304 labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
305 labels = labels.each { |l| l.fix_encoding! }
306
307 user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
308 unless user_label.nil?
309 if user_label.empty?
310 bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
311 else
312 LabelSearchResultsMode.spawn_nicely user_label
313 end
314 end
315 when :compose
316 ComposeMode.spawn_nicely
317 when :poll
318 reporting_thread("user-invoked poll") { PollManager.poll }
319 when :poll_unusual
320 if BufferManager.ask_yes_or_no "Really poll unusual sources?"
321 reporting_thread("user-invoked unusual poll") { PollManager.poll_unusual }
322 end
323 when :recall_draft
324 case Index.num_results_for :label => :draft
325 when 0
326 bm.flash "No draft messages."
327 when 1
328 m = nil
329 Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
330 r = ResumeMode.new(m)
331 BufferManager.spawn "Edit message", r
332 r.default_edit_message
333 else
334 BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
335 end
336 when :show_inbox
337 BufferManager.raise_to_front ibuf
338 when :clear_hooks
339 HookManager.clear
340 when :show_console
341 b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new }
342 b.mode.run
343 when :reload_colors
344 Colormap.reset
345 Colormap.populate_colormap
346 bm.completely_redraw_screen
347 bm.flash "reloaded colors"
348 when :run_keybindings_hook
349 HookManager.clear_one 'keybindings'
350 Keymap.run_hook global_keymap
351 bm.flash "keybindings hook run"
352 when :nothing, InputSequenceAborted
353 when :redraw
354 bm.completely_redraw_screen
355 else
356 bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
357 end
358
359 bm.draw_screen
360 end
361
362 bm.kill_all_buffers if $die
363 rescue Exception => e
364 Redwood::record_exception e, "main"
365 ensure
366 unless $opts[:no_threads]
367 PollManager.stop if PollManager.instantiated?
368 IdleManager.stop if IdleManager.instantiated?
369 Index.stop_lock_update_thread
370 end
371
372 HookManager.run "shutdown" if HookManager.instantiated?
373
374 Index.stop_sync_worker
375 Redwood::finish
376 stop_cursing
377 Redwood::Logger.remove_all_sinks!
378 Redwood::Logger.add_sink $stderr, false
379 debug "stopped cursing"
380
381 if $die
382 info "I've been ordered to commit seppuku. I obey!"
383 end
384
385 if Redwood::exceptions.empty?
386 debug "no fatal errors. good job, william."
387 Index.save
388 else
389 error "oh crap, an exception"
390 end
391
392 Index.unlock
393
394 if (fn = ENV['SUP_PROFILE'])
395 result = RubyProf.stop
396 File.open(fn, 'w') { |io| RubyProf::CallTreePrinter.new(result).print(io) }
397 end
398 end
399
400 unless Redwood::exceptions.empty?
401 File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
402 Redwood::exceptions.each do |e, name|
403 f.puts "--- #{e.class.name} from thread: #{name}"
404 f.puts e.message, e.backtrace
405 end
406 end
407 $stderr.puts <<EOS
408 ----------------------------------------------------------------
409 We are very sorry. It seems that an error occurred in Sup. Please
410 accept our sincere apologies. Please submit the contents of
411 #{BASE_DIR}/exception-log.txt and a brief report of the
412 circumstances to https://github.com/sup-heliotrope/sup/issues so that
413 we might address this problem. Thank you!
414
415 Sincerely,
416 The Sup Developers
417 ----------------------------------------------------------------
418 EOS
419 Redwood::exceptions.each do |e, name|
420 puts "--- #{e.class.name} from thread: #{name}"
421 puts e.message, e.backtrace
422 end
423 end
424
425 end