]> git.notmuchmail.org Git - sup/blob - bin/sup
Merge branch 'logging-tweaks'
[sup] / bin / sup
1 #!/usr/bin/env ruby
2
3 require 'rubygems'
4 require 'ncurses'
5 require 'curses'
6 require 'fileutils'
7 require 'trollop'
8 require "sup"
9
10 BIN_VERSION = "git"
11
12 unless Redwood::VERSION == BIN_VERSION
13   $stderr.puts <<EOS
14
15 Error: version mismatch!
16 The sup executable is at version #{BIN_VERSION.inspect}.
17 The sup libraries are at version #{Redwood::VERSION.inspect}.
18
19 Is your development environment conflicting with rubygems?
20 EOS
21   exit(-1)
22 end
23
24 $opts = Trollop::options do
25   version "sup v#{Redwood::VERSION}"
26   banner <<EOS
27 Sup is a curses-based email client.
28
29 Usage:
30   sup [options]
31
32 Options are:
33 EOS
34   opt :list_hooks, "List all hooks and descriptions, and quit."
35   opt :no_threads, "Turn off threading. Helps with debugging. (Necessarily disables background polling for new messages.)"
36   opt :no_initial_poll, "Don't poll for new messages when starting."
37   opt :search, "Search for this query upon startup", :type => String
38   opt :compose, "Compose message to this recipient upon startup", :type => String
39 end
40
41 Redwood::HookManager.register "startup", <<EOS
42 Executes at startup
43 No variables.
44 No return value.
45 EOS
46
47 Redwood::HookManager.register "shutdown", <<EOS 
48 Executes when sup is shutting down. May be run when sup is crashing,
49 so don\'t do anything too important. Run before the label, contacts,
50 and people are saved.
51 No variables.
52 No return value.
53 EOS
54
55 if $opts[:list_hooks]
56   Redwood::HookManager.print_hooks
57   exit
58 end
59
60 Thread.abort_on_exception = true # make debugging possible
61
62 module Redwood
63
64 global_keymap = Keymap.new do |k|
65   k.add :quit_ask, "Quit Sup, but ask first", 'q'
66   k.add :quit_now, "Quit Sup immediately", 'Q'
67   k.add :help, "Show help", '?'
68   k.add :roll_buffers, "Switch to next buffer", 'b'
69   k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
70   k.add :kill_buffer, "Kill the current buffer", 'x'
71   k.add :list_buffers, "List all buffers", ';'
72   k.add :list_contacts, "List contacts", 'C'
73   k.add :redraw, "Redraw screen", :ctrl_l
74   k.add :search, "Search all messages", '\\', 'F'
75   k.add :search_unread, "Show all unread messages", 'U'
76   k.add :list_labels, "List labels", 'L'
77   k.add :poll, "Poll for new messages", 'P'
78   k.add :compose, "Compose new message", 'm', 'c'
79   k.add :nothing, "Do nothing", :ctrl_g
80   k.add :recall_draft, "Edit most recent draft message", 'R'
81   k.add :show_inbox, "Show the Inbox buffer", 'I'
82 end
83
84 ## the following magic enables wide characters when used with a ruby
85 ## ncurses.so that's been compiled against libncursesw. (note the w.) why
86 ## this works, i have no idea. much like pretty much every aspect of
87 ## dealing with curses.  cargo cult programming at its best.
88 ##
89 ## BSD users: if libc.so.6 is not found, try installing compat6x.
90 require 'dl/import'
91 module LibC
92   extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable
93   setlocale_lib = case Config::CONFIG['arch']
94     when /darwin/; "libc.dylib"
95     when /cygwin/; "cygwin1.dll"
96     else; "libc.so.6"
97   end
98
99   debug "dynamically loading setlocale() from #{setlocale_lib}"
100   begin
101     dlload setlocale_lib
102     extern "void setlocale(int, const char *)"
103     debug "setting locale..."
104     LibC.setlocale(6, "")  # LC_ALL == 6
105   rescue RuntimeError => e
106     warn "cannot dlload setlocale(); ncurses wide character support probably broken."
107     warn "dlload error was #{e.class}: #{e.message}"
108     if Config::CONFIG['arch'] =~ /bsd/
109       warn "BSD variant detected. You may have to install a compat6x package to acquire libc."
110     end
111   end
112 end
113
114 def start_cursing
115   Ncurses.initscr
116   Ncurses.noecho
117   Ncurses.cbreak
118   Ncurses.stdscr.keypad 1
119   Ncurses.use_default_colors
120   Ncurses.curs_set 0
121   Ncurses.start_color
122   $cursing = true
123 end
124
125 def stop_cursing
126   return unless $cursing
127   Ncurses.curs_set 1
128   Ncurses.echo
129   Ncurses.endwin
130 end
131 module_function :start_cursing, :stop_cursing
132
133 Index.init
134 Index.lock_interactively or exit
135
136 begin
137   Redwood::start
138   Index.load
139
140   $die = false
141   trap("TERM") { |x| $die = true }
142   trap("WINCH") { |x| BufferManager.sigwinch_happened! }
143
144   if(s = Redwood::SourceManager.source_for DraftManager.source_name)
145     DraftManager.source = s
146   else
147     debug "no draft source, auto-adding..."
148     Redwood::SourceManager.add_source DraftManager.new_source
149   end
150
151   if(s = Redwood::SourceManager.source_for SentManager.source_uri)
152     SentManager.source = s
153   else
154     Redwood::SourceManager.add_source SentManager.default_source
155   end
156
157   HookManager.run "startup"
158
159   debug "starting curses"
160   Redwood::Logger.remove_sink $stderr
161   start_cursing
162
163   bm = BufferManager.init
164   Colormap.new.populate_colormap
165
166   debug "initializing log buffer"
167   lmode = Redwood::LogMode.new "system log"
168   lmode.on_kill { Logger.clear! }
169   Logger.add_sink lmode
170   Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
171   if Logger::LEVELS.index(Logger.level) > 0
172     Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}."
173   end
174
175   debug "initializing inbox buffer"
176   imode = InboxMode.new
177   ibuf = bm.spawn "Inbox", imode
178
179   debug "ready for interaction!"
180
181   bm.draw_screen
182
183   Redwood::SourceManager.usual_sources.each do |s|
184     next unless s.respond_to? :connect
185     reporting_thread("call #connect on #{s}") do
186       begin
187         s.connect
188       rescue SourceError => e
189         error "fatal error loading from #{s}: #{e.message}"
190       end
191     end
192   end unless $opts[:no_initial_poll]
193
194   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] }
195
196   if $opts[:compose]
197     ComposeMode.spawn_nicely :to_default => $opts[:compose]
198   end
199
200   unless $opts[:no_threads]
201     PollManager.start
202     Index.start_lock_update_thread
203   end
204
205   if $opts[:search]
206     SearchResultsMode.spawn_from_query $opts[:search]
207   end
208
209   until Redwood::exceptions.nonempty? || $die
210     c = begin
211       Ncurses.nonblocking_getch
212     rescue Interrupt => e
213       raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
214       BufferManager.draw_screen
215       nil
216     end
217
218     if c.nil?
219       if BufferManager.sigwinch_happened?
220         debug "redrawing screen on sigwinch"
221         BufferManager.completely_redraw_screen
222       end
223       next
224     end
225
226     if c == 410
227       ## this is ncurses's way of telling us it's detected a refresh.
228       ## since we have our own sigwinch handler, we don't do anything.
229       next
230     end
231
232     bm.erase_flash
233
234     action = begin
235       if bm.handle_input c
236         :nothing
237       else
238         bm.resolve_input_with_keymap c, global_keymap
239       end
240     rescue InputSequenceAborted
241       :nothing
242     end
243     case action
244     when :quit_now
245       break if bm.kill_all_buffers_safely
246     when :quit_ask
247       if bm.ask_yes_or_no "Really quit?"
248         break if bm.kill_all_buffers_safely
249       end
250     when :help
251       curmode = bm.focus_buf.mode
252       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
253     when :roll_buffers
254       bm.roll_buffers
255     when :roll_buffers_backwards
256       bm.roll_buffers_backwards
257     when :kill_buffer
258       bm.kill_buffer_safely bm.focus_buf
259     when :list_buffers
260       bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
261     when :list_contacts
262       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
263       b.mode.load_in_background if new
264     when :search
265       query = BufferManager.ask :search, "search all messages: "
266       next unless query && query !~ /^\s*$/
267       SearchResultsMode.spawn_from_query query
268     when :search_unread
269       SearchResultsMode.spawn_from_query "is:unread"
270     when :list_labels
271       labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
272       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
273       unless user_label.nil?
274         if user_label.empty?
275           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
276         else
277           LabelSearchResultsMode.spawn_nicely user_label
278         end
279       end
280     when :compose
281       ComposeMode.spawn_nicely
282     when :poll
283       reporting_thread("user-invoked poll") { PollManager.poll }
284     when :recall_draft
285       case Index.num_results_for :label => :draft
286       when 0
287         bm.flash "No draft messages."
288       when 1
289         m = nil
290         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
291         r = ResumeMode.new(m)
292         BufferManager.spawn "Edit message", r
293         r.edit_message
294       else
295         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
296         b.mode.load_threads :num => b.content_height if new
297       end
298     when :show_inbox
299       BufferManager.raise_to_front ibuf
300     when :nothing, InputSequenceAborted
301     when :redraw
302       bm.completely_redraw_screen
303     else
304       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
305     end
306
307     bm.draw_screen
308   end
309
310   bm.kill_all_buffers if $die
311 rescue Exception => e
312   Redwood::record_exception e, "main"
313 ensure
314   unless $opts[:no_threads]
315     PollManager.stop if PollManager.instantiated?
316     Index.stop_lock_update_thread
317   end
318
319   HookManager.run "shutdown"
320
321   Redwood::finish
322   stop_cursing
323   Redwood::Logger.remove_all_sinks!
324   Redwood::Logger.add_sink $stderr, false
325   debug "stopped cursing"
326
327   if $die
328     info "I've been ordered to commit seppuku. I obey!"
329   end
330
331   if Redwood::exceptions.empty?
332     debug "no fatal errors. good job, william."
333     Index.save
334   else
335     error "oh crap, an exception"
336   end
337
338   Index.unlock
339 end
340
341 unless Redwood::exceptions.empty?
342   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
343     Redwood::exceptions.each do |e, name|
344       f.puts "--- #{e.class.name} from thread: #{name}"
345       f.puts e.message, e.backtrace
346     end
347   end
348   $stderr.puts <<EOS
349 ----------------------------------------------------------------
350 I'm very sorry. It seems that an error occurred in Sup. Please
351 accept my sincere apologies. If you don't mind, please send the
352 contents of ~/.sup/exception-log.txt and a brief report of the
353 circumstances to sup-talk at rubyforge dot orgs so that I might
354 address this problem. Thank you!
355
356 Sincerely,
357 William
358 ----------------------------------------------------------------
359 EOS
360   Redwood::exceptions.each do |e, name|
361     puts "--- #{e.class.name} from thread: #{name}"
362     puts e.message, e.backtrace
363   end
364 end
365
366 end