bin/sup (12539B) - 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 imode.load_threads :num => ibuf.content_height, :when_done => lambda { |num| reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }
210
211 if $opts[:compose]
212 to = Person.from_address_list $opts[:compose]
213 mode = ComposeMode.new :to => to, :subj => $opts[:subject]
214 BufferManager.spawn "New Message", mode
215 mode.default_edit_message
216 end
217
218 unless $opts[:no_threads]
219 PollManager.start
220 IdleManager.start
221 Index.start_lock_update_thread
222 end
223
224 if $opts[:search]
225 SearchResultsMode.spawn_from_query $opts[:search]
226 end
227
228 until Redwood::exceptions.nonempty? || $die
229 c = begin
230 Ncurses::CharCode.get false
231 rescue Interrupt
232 raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
233 BufferManager.draw_screen
234 Ncurses::CharCode.empty
235 end
236
237 if c.empty?
238 if BufferManager.sigwinch_happened?
239 debug "redrawing screen on sigwinch"
240 BufferManager.completely_redraw_screen
241 end
242 next
243 end
244
245 IdleManager.ping
246
247 if c.is_keycode? 410
248 ## this is ncurses's way of telling us it's detected a refresh.
249 ## since we have our own sigwinch handler, we don't do anything.
250 next
251 end
252
253 bm.erase_flash
254
255 action =
256 begin
257 if bm.handle_input c
258 :nothing
259 else
260 bm.resolve_input_with_keymap c, global_keymap
261 end
262 rescue InputSequenceAborted
263 :nothing
264 end
265 case action
266 when :quit_now
267 break if bm.kill_all_buffers_safely
268 when :quit_ask
269 if bm.ask_yes_or_no "Really quit?"
270 break if bm.kill_all_buffers_safely
271 end
272 when :help
273 curmode = bm.focus_buf.mode
274 bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
275 when :roll_buffers
276 bm.roll_buffers
277 when :roll_buffers_backwards
278 bm.roll_buffers_backwards
279 when :kill_buffer
280 bm.kill_buffer_safely bm.focus_buf
281 when :list_buffers
282 bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
283 when :list_contacts
284 b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
285 b.mode.load_in_background if new
286 when :search
287 completions = LabelManager.all_labels.map { |l| "label:#{LabelManager.string_for l}" }
288 completions = completions.each { |l| l.fix_encoding! }
289 completions += Index::COMPL_PREFIXES
290 query = BufferManager.ask_many_with_completions :search, "Search all messages (enter for saved searches): ", completions
291 unless query.nil?
292 if query.empty?
293 bm.spawn_unless_exists("Saved searches") { SearchListMode.new }
294 else
295 SearchResultsMode.spawn_from_query query
296 end
297 end
298 when :search_unread
299 SearchResultsMode.spawn_from_query "is:unread"
300 when :list_labels
301 labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
302 labels = labels.each { |l| l.fix_encoding! }
303
304 user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
305 unless user_label.nil?
306 if user_label.empty?
307 bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
308 else
309 LabelSearchResultsMode.spawn_nicely user_label
310 end
311 end
312 when :compose
313 ComposeMode.spawn_nicely
314 when :poll
315 reporting_thread("user-invoked poll") { PollManager.poll }
316 when :poll_unusual
317 if BufferManager.ask_yes_or_no "Really poll unusual sources?"
318 reporting_thread("user-invoked unusual poll") { PollManager.poll_unusual }
319 end
320 when :recall_draft
321 case Index.num_results_for :label => :draft
322 when 0
323 bm.flash "No draft messages."
324 when 1
325 m = nil
326 Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
327 r = ResumeMode.new(m)
328 BufferManager.spawn "Edit message", r
329 r.default_edit_message
330 else
331 b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
332 b.mode.load_threads :num => b.content_height if new
333 end
334 when :show_inbox
335 BufferManager.raise_to_front ibuf
336 when :clear_hooks
337 HookManager.clear
338 when :show_console
339 b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new }
340 b.mode.run
341 when :reload_colors
342 Colormap.reset
343 Colormap.populate_colormap
344 bm.completely_redraw_screen
345 bm.flash "reloaded colors"
346 when :run_keybindings_hook
347 HookManager.clear_one 'keybindings'
348 Keymap.run_hook global_keymap
349 bm.flash "keybindings hook run"
350 when :nothing, InputSequenceAborted
351 when :redraw
352 bm.completely_redraw_screen
353 else
354 bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
355 end
356
357 bm.draw_screen
358 end
359
360 bm.kill_all_buffers if $die
361 rescue Exception => e
362 Redwood::record_exception e, "main"
363 ensure
364 unless $opts[:no_threads]
365 PollManager.stop if PollManager.instantiated?
366 IdleManager.stop if IdleManager.instantiated?
367 Index.stop_lock_update_thread
368 end
369
370 HookManager.run "shutdown" if HookManager.instantiated?
371
372 Index.stop_sync_worker
373 Redwood::finish
374 stop_cursing
375 Redwood::Logger.remove_all_sinks!
376 Redwood::Logger.add_sink $stderr, false
377 debug "stopped cursing"
378
379 if $die
380 info "I've been ordered to commit seppuku. I obey!"
381 end
382
383 if Redwood::exceptions.empty?
384 debug "no fatal errors. good job, william."
385 Index.save
386 else
387 error "oh crap, an exception"
388 end
389
390 Index.unlock
391
392 if (fn = ENV['SUP_PROFILE'])
393 result = RubyProf.stop
394 File.open(fn, 'w') { |io| RubyProf::CallTreePrinter.new(result).print(io) }
395 end
396 end
397
398 unless Redwood::exceptions.empty?
399 File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
400 Redwood::exceptions.each do |e, name|
401 f.puts "--- #{e.class.name} from thread: #{name}"
402 f.puts e.message, e.backtrace
403 end
404 end
405 $stderr.puts <<EOS
406 ----------------------------------------------------------------
407 We are very sorry. It seems that an error occurred in Sup. Please
408 accept our sincere apologies. Please submit the contents of
409 #{BASE_DIR}/exception-log.txt and a brief report of the
410 circumstances to https://github.com/sup-heliotrope/sup/issues so that
411 we might address this problem. Thank you!
412
413 Sincerely,
414 The Sup Developers
415 ----------------------------------------------------------------
416 EOS
417 Redwood::exceptions.each do |e, name|
418 puts "--- #{e.class.name} from thread: #{name}"
419 puts e.message, e.backtrace
420 end
421 end
422
423 end