]> git.cworth.org Git - sup/blob - bin/sup
added a shutdown hook
[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 'fastthread'
9 require "sup"
10
11 BIN_VERSION = "git"
12
13 unless Redwood::VERSION == BIN_VERSION
14   $stderr.puts <<EOS
15
16 Error: version mismatch!
17 The sup executable is at version #{BIN_VERSION.inspect}.
18 The sup libraries are at version #{Redwood::VERSION.inspect}.
19
20 Is your development environment conflicting with rubygems?
21 EOS
22   exit(-1)
23 end
24
25 $opts = Trollop::options do
26   version "sup v#{Redwood::VERSION}"
27   banner <<EOS
28 Sup is a curses-based email client.
29
30 Usage:
31   sup [options]
32
33 Options are:
34 EOS
35   opt :list_hooks, "List all hooks and descriptions, and quit."
36   opt :no_threads, "Turn off threading. Helps with debugging. (Necessarily disables background polling for new messages.)"
37   opt :no_initial_poll, "Don't poll for new messages when starting."
38   opt :search, "Search for this query upon startup", :type => String
39   opt :compose, "Compose message to this recipient upon startup", :type => String
40 end
41
42 Redwood::HookManager.register "startup", <<EOS
43 Executes at startup
44 No variables.
45 No return value.
46 EOS
47
48 Redwood::HookManager.register "shutdown", <<EOS 
49 Executes when sup is shutting down. May be run when sup is crashing,
50 so don\'t do anything too important. Run before the label, contacts,
51 and people are saved.
52 No variables.
53 No return value.
54 EOS
55
56 if $opts[:list_hooks]
57   Redwood::HookManager.print_hooks
58   exit
59 end
60
61 Thread.abort_on_exception = true # make debugging possible
62
63 module Redwood
64
65 global_keymap = Keymap.new do |k|
66   k.add :quit_ask, "Quit Sup, but ask first", 'q'
67   k.add :quit_now, "Quit Sup immediately", 'Q'
68   k.add :help, "Show help", 'H', '?'
69   k.add :roll_buffers, "Switch to next buffer", 'b'
70 #  k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
71   k.add :kill_buffer, "Kill the current buffer", 'x'
72   k.add :list_buffers, "List all buffers", 'B'
73   k.add :list_contacts, "List contacts", 'C'
74   k.add :redraw, "Redraw screen", :ctrl_l
75   k.add :search, "Search all messages", '\\', 'F'
76   k.add :search_unread, "Show all unread messages", 'U'
77   k.add :list_labels, "List labels", 'L'
78   k.add :poll, "Poll for new messages", 'P'
79   k.add :compose, "Compose new message", 'm', 'c'
80   k.add :nothing, "Do nothing", :ctrl_g
81   k.add :recall_draft, "Edit most recent draft message", 'R'
82 end
83
84 def start_cursing
85   Ncurses.initscr
86   Ncurses.noecho
87   Ncurses.cbreak
88   Ncurses.stdscr.keypad 1
89   Ncurses.curs_set 0
90   Ncurses.start_color
91   $cursing = true
92 end
93
94 def stop_cursing
95   return unless $cursing
96   Ncurses.curs_set 1
97   Ncurses.echo
98   Ncurses.endwin
99 end
100 module_function :start_cursing, :stop_cursing
101
102 Index.new
103 begin
104   Index.lock
105 rescue Index::LockError => e
106   require 'highline'
107
108   h = HighLine.new
109   h.wrap_at = :auto
110   h.say Index.fancy_lock_error_message_for(e)
111
112   case h.ask("Should I ask that process to kill itself? ")
113   when /^\s*y(es)?\s*$/i
114     h.say "Ok, suggesting seppuku..."
115     FileUtils.touch Redwood::SUICIDE_FN
116     sleep SuicideManager::DELAY * 2
117     FileUtils.rm_f Redwood::SUICIDE_FN
118     h.say "Let's try that again."
119     retry
120   else
121     h.say <<EOS
122 Ok, giving up. If the process crashed and left a stale lockfile, you
123 can fix this by manually deleting #{Index.lockfile}.
124 EOS
125     exit
126   end
127 end
128
129 begin
130   Redwood::start
131   Index.load
132
133   if(s = Index.source_for DraftManager.source_name)
134     DraftManager.source = s
135   else
136     Redwood::log "no draft source, auto-adding..."
137     Index.add_source DraftManager.new_source
138   end
139
140   if(s = Index.source_for SentManager.source_name)
141     SentManager.source = s
142   else
143     Redwood::log "no sent mail source, auto-adding..."
144     Index.add_source SentManager.new_source
145   end
146
147   HookManager.run "startup"
148
149   log "starting curses"
150   start_cursing
151
152   bm = BufferManager.new
153   Colormap.new.populate_colormap
154
155   log "initializing mail index buffer"
156   imode = InboxMode.new
157   ibuf = bm.spawn "Inbox", imode
158
159   log "ready for interaction!"
160   Logger.make_buf
161
162   bm.draw_screen
163
164   Index.usual_sources.each do |s|
165     next unless s.respond_to? :connect
166     reporting_thread("call #connect on #{s}") do
167       begin
168         s.connect
169       rescue SourceError => e
170         Redwood::log "fatal error loading from #{s}: #{e.message}"
171       end
172     end
173   end unless $opts[:no_initial_poll]
174   
175   imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }
176
177   if $opts[:compose]
178     ComposeMode.spawn_nicely :to_default => $opts[:compose]
179   end
180
181   unless $opts[:no_threads]
182     PollManager.start
183     SuicideManager.start
184     Index.start_lock_update_thread
185   end
186
187   if $opts[:search]
188     SearchResultsMode.spawn_from_query $opts[:search]
189   end
190
191   until Redwood::exceptions.nonempty? || SuicideManager.die?
192     c = Ncurses.nonblocking_getch
193     next unless c
194     bm.erase_flash
195
196     action =
197       begin
198         if bm.handle_input c
199           :nothing
200         else
201           bm.resolve_input_with_keymap c, global_keymap
202         end
203       rescue InputSequenceAborted
204         :nothing
205       end
206
207     case action
208     when :quit_now
209       break if bm.kill_all_buffers_safely
210     when :quit_ask
211       if bm.ask_yes_or_no "Really quit?"
212         break if bm.kill_all_buffers_safely
213       end
214     when :help
215       curmode = bm.focus_buf.mode
216       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
217     when :roll_buffers
218       bm.roll_buffers
219     when :roll_buffers_backwards
220       bm.roll_buffers_backwards
221     when :kill_buffer
222       bm.kill_buffer_safely bm.focus_buf
223     when :list_buffers
224       bm.spawn_unless_exists("Buffer List") { BufferListMode.new }
225     when :list_contacts
226       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
227       b.mode.load_in_background if new
228     when :search
229       query = BufferManager.ask :search, "search all messages: "
230       next unless query && query !~ /^\s*$/
231       SearchResultsMode.spawn_from_query query
232     when :search_unread
233       SearchResultsMode.spawn_from_query "is:unread"
234     when :list_labels
235       labels = LabelManager.listable_labels.map { |l| LabelManager.string_for l }
236       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
237       unless user_label.nil?
238         if user_label.empty?
239           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
240         else
241           LabelSearchResultsMode.spawn_nicely user_label
242         end
243       end
244     when :compose
245       ComposeMode.spawn_nicely
246     when :poll
247       reporting_thread("user-invoked poll") { PollManager.poll }
248     when :recall_draft
249       case Index.num_results_for :label => :draft
250       when 0
251         bm.flash "No draft messages."
252       when 1
253         m = nil
254         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
255         r = ResumeMode.new(m)
256         BufferManager.spawn "Edit message", r
257         r.edit_message
258       else
259         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
260         b.mode.load_threads :num => b.content_height if new
261       end
262     when :nothing, InputSequenceAborted
263     when :redraw
264       bm.completely_redraw_screen
265     else
266       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
267     end
268
269     bm.draw_screen
270   end
271
272   bm.kill_all_buffers if SuicideManager.die?
273 rescue Exception => e
274   Redwood::record_exception e, "main"
275 ensure
276   unless $opts[:no_threads]
277     PollManager.stop if PollManager.instantiated?
278     SuicideManager.stop if PollManager.instantiated?
279     Index.stop_lock_update_thread
280   end
281
282   HookManager.run "shutdown"
283
284   Redwood::finish
285   stop_cursing
286   Redwood::log "stopped cursing"
287
288   if SuicideManager.instantiated? && SuicideManager.die?
289     Redwood::log "I've been ordered to commit seppuku. I obey!"
290   end
291
292   if Redwood::exceptions.empty?
293     Redwood::log "no fatal errors. good job, william."
294     Index.save
295   else
296     Redwood::log "oh crap, an exception"
297   end
298
299   Index.unlock
300 end
301
302 unless Redwood::exceptions.empty?
303   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
304     Redwood::exceptions.each do |e, name|
305       f.puts "--- #{e.class.name} from thread: #{name}"
306       f.puts e.message, e.backtrace
307     end
308   end
309   $stderr.puts <<EOS
310 ----------------------------------------------------------------
311 I'm very sorry. It seems that an error occurred in Sup. Please
312 accept my sincere apologies. If you don't mind, please send the
313 contents of ~/.sup/exception-log.txt and a brief report of the
314 circumstances to sup-talk at rubyforge dot orgs so that I might
315 address this problem. Thank you!
316
317 Sincerely,
318 William
319 ----------------------------------------------------------------
320 EOS
321   Redwood::exceptions.each do |e, name|
322     puts "--- #{e.class.name} from thread: #{name}"
323     puts e.message, e.backtrace
324   end
325 end
326
327 end