]> git.cworth.org Git - sup/blob - bin/sup
Merge branch 'various-api-refactors'
[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 begin
135   Index.lock
136 rescue Index::LockError => e
137   require 'highline'
138
139   h = HighLine.new
140   h.wrap_at = :auto
141   h.say Index.fancy_lock_error_message_for(e)
142
143   case h.ask("Should I ask that process to kill itself? ")
144   when /^\s*y(es)?\s*$/i
145     h.say "Ok, suggesting seppuku..."
146     FileUtils.touch Redwood::SUICIDE_FN
147     sleep SuicideManager::DELAY * 2
148     FileUtils.rm_f Redwood::SUICIDE_FN
149     h.say "Let's try that again."
150     retry
151   else
152     h.say <<EOS
153 Ok, giving up. If the process crashed and left a stale lockfile, you
154 can fix this by manually deleting #{Index.lockfile}.
155 EOS
156     exit
157   end
158 end
159
160 begin
161   Redwood::start
162   Index.load
163
164   if(s = Redwood::SourceManager.source_for DraftManager.source_name)
165     DraftManager.source = s
166   else
167     debug "no draft source, auto-adding..."
168     Redwood::SourceManager.add_source DraftManager.new_source
169   end
170
171   if(s = Redwood::SourceManager.source_for SentManager.source_uri)
172     SentManager.source = s
173   else
174     Redwood::SourceManager.add_source SentManager.default_source
175   end
176
177   HookManager.run "startup"
178
179   debug "starting curses"
180   Redwood::Logger.remove_sink $stderr
181   start_cursing
182
183   bm = BufferManager.init
184   Colormap.new.populate_colormap
185
186   debug "initializing log buffer"
187   lmode = Redwood::LogMode.new "system log"
188   lmode.on_kill { Logger.clear! }
189   Logger.add_sink lmode
190   Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
191
192   debug "initializing inbox buffer"
193   imode = InboxMode.new
194   ibuf = bm.spawn "Inbox", imode
195
196   debug "ready for interaction!"
197
198   bm.draw_screen
199
200   Redwood::SourceManager.usual_sources.each do |s|
201     next unless s.respond_to? :connect
202     reporting_thread("call #connect on #{s}") do
203       begin
204         s.connect
205       rescue SourceError => e
206         error "fatal error loading from #{s}: #{e.message}"
207       end
208     end
209   end unless $opts[:no_initial_poll]
210
211   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] }
212
213   if $opts[:compose]
214     ComposeMode.spawn_nicely :to_default => $opts[:compose]
215   end
216
217   unless $opts[:no_threads]
218     PollManager.start
219     SuicideManager.start
220     Index.start_lock_update_thread
221   end
222
223   if $opts[:search]
224     SearchResultsMode.spawn_from_query $opts[:search]
225   end
226
227   until Redwood::exceptions.nonempty? || SuicideManager.die?
228     c = 
229        begin
230          Ncurses.nonblocking_getch
231        rescue Exception => e
232          if e.is_a?(Interrupt)
233            raise if BufferManager.ask_yes_or_no("Die ungracefully now?")
234            bm.draw_screen
235            nil
236          end
237        end
238     next unless c
239     bm.erase_flash
240
241     action =
242       begin
243         if bm.handle_input c
244           :nothing
245         else
246           bm.resolve_input_with_keymap c, global_keymap
247         end
248       rescue InputSequenceAborted
249         :nothing
250       end
251     case action
252     when :quit_now
253       break if bm.kill_all_buffers_safely
254     when :quit_ask
255       if bm.ask_yes_or_no "Really quit?"
256         break if bm.kill_all_buffers_safely
257       end
258     when :help
259       curmode = bm.focus_buf.mode
260       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
261     when :roll_buffers
262       bm.roll_buffers
263     when :roll_buffers_backwards
264       bm.roll_buffers_backwards
265     when :kill_buffer
266       bm.kill_buffer_safely bm.focus_buf
267     when :list_buffers
268       bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
269     when :list_contacts
270       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
271       b.mode.load_in_background if new
272     when :search
273       query = BufferManager.ask :search, "search all messages: "
274       next unless query && query !~ /^\s*$/
275       SearchResultsMode.spawn_from_query query
276     when :search_unread
277       SearchResultsMode.spawn_from_query "is:unread"
278     when :list_labels
279       labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
280       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
281       unless user_label.nil?
282         if user_label.empty?
283           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
284         else
285           LabelSearchResultsMode.spawn_nicely user_label
286         end
287       end
288     when :compose
289       ComposeMode.spawn_nicely
290     when :poll
291       reporting_thread("user-invoked poll") { PollManager.poll }
292     when :recall_draft
293       case Index.num_results_for :label => :draft
294       when 0
295         bm.flash "No draft messages."
296       when 1
297         m = nil
298         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
299         r = ResumeMode.new(m)
300         BufferManager.spawn "Edit message", r
301         r.edit_message
302       else
303         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
304         b.mode.load_threads :num => b.content_height if new
305       end
306     when :show_inbox
307       BufferManager.raise_to_front ibuf
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