]> git.cworth.org Git - sup/blob - bin/sup
make SUP_LOG_LEVEL self-documenting
[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 end
82
83 ## the following magic enables wide characters when used with a ruby
84 ## ncurses.so that's been compiled against libncursesw. (note the w.) why
85 ## this works, i have no idea. much like pretty much every aspect of
86 ## dealing with curses.  cargo cult programming at its best.
87 ##
88 ## BSD users: if libc.so.6 is not found, try installing compat6x.
89 require 'dl/import'
90 module LibC
91   extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable
92   setlocale_lib = case Config::CONFIG['arch']
93     when /darwin/; "libc.dylib"
94     when /cygwin/; "cygwin1.dll"
95     else; "libc.so.6"
96   end
97
98   debug "dynamically loading setlocale() from #{setlocale_lib}"
99   begin
100     dlload setlocale_lib
101     extern "void setlocale(int, const char *)"
102     debug "setting locale..."
103     LibC.setlocale(6, "")  # LC_ALL == 6
104   rescue RuntimeError => e
105     warn "cannot dlload setlocale(); ncurses wide character support probably broken."
106     warn "dlload error was #{e.class}: #{e.message}"
107     if Config::CONFIG['arch'] =~ /bsd/
108       warn "BSD variant detected. You may have to install a compat6x package to acquire libc."
109     end
110   end
111 end
112
113 def start_cursing
114   Ncurses.initscr
115   Ncurses.noecho
116   Ncurses.cbreak
117   Ncurses.stdscr.keypad 1
118   Ncurses.use_default_colors
119   Ncurses.curs_set 0
120   Ncurses.start_color
121   $cursing = true
122 end
123
124 def stop_cursing
125   return unless $cursing
126   Ncurses.curs_set 1
127   Ncurses.echo
128   Ncurses.endwin
129 end
130 module_function :start_cursing, :stop_cursing
131
132 Index.init
133 begin
134   Index.lock
135 rescue Index::LockError => e
136   require 'highline'
137
138   h = HighLine.new
139   h.wrap_at = :auto
140   h.say Index.fancy_lock_error_message_for(e)
141
142   case h.ask("Should I ask that process to kill itself? ")
143   when /^\s*y(es)?\s*$/i
144     h.say "Ok, suggesting seppuku..."
145     FileUtils.touch Redwood::SUICIDE_FN
146     sleep SuicideManager::DELAY * 2
147     FileUtils.rm_f Redwood::SUICIDE_FN
148     h.say "Let's try that again."
149     retry
150   else
151     h.say <<EOS
152 Ok, giving up. If the process crashed and left a stale lockfile, you
153 can fix this by manually deleting #{Index.lockfile}.
154 EOS
155     exit
156   end
157 end
158
159 begin
160   Redwood::start
161   Index.load
162
163   if(s = Redwood::SourceManager.source_for DraftManager.source_name)
164     DraftManager.source = s
165   else
166     debug "no draft source, auto-adding..."
167     Redwood::SourceManager.add_source DraftManager.new_source
168   end
169
170   if(s = Redwood::SourceManager.source_for SentManager.source_uri)
171     SentManager.source = s
172   else
173     Redwood::SourceManager.add_source SentManager.default_source
174   end
175
176   HookManager.run "startup"
177
178   debug "starting curses"
179   Redwood::Logger.remove_sink $stderr
180   start_cursing
181
182   bm = BufferManager.init
183   Colormap.new.populate_colormap
184
185   debug "initializing log buffer"
186   lmode = Redwood::LogMode.new "system log"
187   lmode.on_kill { Logger.clear! }
188   Logger.add_sink lmode
189   Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
190   if Logger::LEVELS.index(Logger.level) > 0
191     Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}."
192   end
193
194   debug "initializing inbox buffer"
195   imode = InboxMode.new
196   ibuf = bm.spawn "Inbox", imode
197
198   debug "ready for interaction!"
199
200   bm.draw_screen
201
202   Redwood::SourceManager.usual_sources.each do |s|
203     next unless s.respond_to? :connect
204     reporting_thread("call #connect on #{s}") do
205       begin
206         s.connect
207       rescue SourceError => e
208         error "fatal error loading from #{s}: #{e.message}"
209       end
210     end
211   end unless $opts[:no_initial_poll]
212
213   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] }
214
215   if $opts[:compose]
216     ComposeMode.spawn_nicely :to_default => $opts[:compose]
217   end
218
219   unless $opts[:no_threads]
220     PollManager.start
221     SuicideManager.start
222     Index.start_lock_update_thread
223   end
224
225   if $opts[:search]
226     SearchResultsMode.spawn_from_query $opts[:search]
227   end
228
229   until Redwood::exceptions.nonempty? || SuicideManager.die?
230     c = 
231        begin
232          Ncurses.nonblocking_getch
233        rescue Exception => e
234          if e.is_a?(Interrupt)
235            raise if BufferManager.ask_yes_or_no("Die ungracefully now?")
236            bm.draw_screen
237            nil
238          end
239        end
240     next unless c
241     bm.erase_flash
242
243     action =
244       begin
245         if bm.handle_input c
246           :nothing
247         else
248           bm.resolve_input_with_keymap c, global_keymap
249         end
250       rescue InputSequenceAborted
251         :nothing
252       end
253     case action
254     when :quit_now
255       break if bm.kill_all_buffers_safely
256     when :quit_ask
257       if bm.ask_yes_or_no "Really quit?"
258         break if bm.kill_all_buffers_safely
259       end
260     when :help
261       curmode = bm.focus_buf.mode
262       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
263     when :roll_buffers
264       bm.roll_buffers
265     when :roll_buffers_backwards
266       bm.roll_buffers_backwards
267     when :kill_buffer
268       bm.kill_buffer_safely bm.focus_buf
269     when :list_buffers
270       bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
271     when :list_contacts
272       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
273       b.mode.load_in_background if new
274     when :search
275       query = BufferManager.ask :search, "search all messages: "
276       next unless query && query !~ /^\s*$/
277       SearchResultsMode.spawn_from_query query
278     when :search_unread
279       SearchResultsMode.spawn_from_query "is:unread"
280     when :list_labels
281       labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
282       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
283       unless user_label.nil?
284         if user_label.empty?
285           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
286         else
287           LabelSearchResultsMode.spawn_nicely user_label
288         end
289       end
290     when :compose
291       ComposeMode.spawn_nicely
292     when :poll
293       reporting_thread("user-invoked poll") { PollManager.poll }
294     when :recall_draft
295       case Index.num_results_for :label => :draft
296       when 0
297         bm.flash "No draft messages."
298       when 1
299         m = nil
300         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
301         r = ResumeMode.new(m)
302         BufferManager.spawn "Edit message", r
303         r.edit_message
304       else
305         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
306         b.mode.load_threads :num => b.content_height if new
307       end
308     when :nothing, InputSequenceAborted
309     when :redraw
310       bm.completely_redraw_screen
311     else
312       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
313     end
314
315     bm.draw_screen
316   end
317
318   bm.kill_all_buffers if SuicideManager.die?
319 rescue Exception => e
320   Redwood::record_exception e, "main"
321 ensure
322   unless $opts[:no_threads]
323     PollManager.stop if PollManager.instantiated?
324     SuicideManager.stop if PollManager.instantiated?
325     Index.stop_lock_update_thread
326   end
327
328   HookManager.run "shutdown"
329
330   Redwood::finish
331   stop_cursing
332   Redwood::Logger.remove_all_sinks!
333   Redwood::Logger.add_sink $stderr, false
334   debug "stopped cursing"
335
336   if SuicideManager.instantiated? && SuicideManager.die?
337     info "I've been ordered to commit seppuku. I obey!"
338   end
339
340   if Redwood::exceptions.empty?
341     debug "no fatal errors. good job, william."
342     Index.save
343   else
344     error "oh crap, an exception"
345   end
346
347   Index.unlock
348 end
349
350 unless Redwood::exceptions.empty?
351   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
352     Redwood::exceptions.each do |e, name|
353       f.puts "--- #{e.class.name} from thread: #{name}"
354       f.puts e.message, e.backtrace
355     end
356   end
357   $stderr.puts <<EOS
358 ----------------------------------------------------------------
359 I'm very sorry. It seems that an error occurred in Sup. Please
360 accept my sincere apologies. If you don't mind, please send the
361 contents of ~/.sup/exception-log.txt and a brief report of the
362 circumstances to sup-talk at rubyforge dot orgs so that I might
363 address this problem. Thank you!
364
365 Sincerely,
366 William
367 ----------------------------------------------------------------
368 EOS
369   Redwood::exceptions.each do |e, name|
370     puts "--- #{e.class.name} from thread: #{name}"
371     puts e.message, e.backtrace
372   end
373 end
374
375 end