]> git.cworth.org Git - sup/blob - bin/sup
make ctrl-c prompt user if sup should die ungracefully
[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", '?'
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     Redwood::reporting_thread("be friendly") do
186       id = BufferManager.say "Welcome to Sup! Press '?' at any point for help."
187       sleep 10
188       BufferManager.clear id
189     end
190   end
191
192   if $opts[:search]
193     SearchResultsMode.spawn_from_query $opts[:search]
194   end
195
196   until Redwood::exceptions.nonempty? || SuicideManager.die?
197     c = 
198        begin
199          Ncurses.nonblocking_getch
200        rescue Exception => e
201          if e.is_a?(Interrupt)
202            raise if BufferManager.ask_yes_or_no("Die ungracefully now?")
203            bm.draw_screen
204            nil
205          end
206        end
207     next unless c
208     bm.erase_flash
209
210     action =
211       begin
212         if bm.handle_input c
213           :nothing
214         else
215           bm.resolve_input_with_keymap c, global_keymap
216         end
217       rescue InputSequenceAborted
218         :nothing
219       end
220     case action
221     when :quit_now
222       break if bm.kill_all_buffers_safely
223     when :quit_ask
224       if bm.ask_yes_or_no "Really quit?"
225         break if bm.kill_all_buffers_safely
226       end
227     when :help
228       curmode = bm.focus_buf.mode
229       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
230     when :roll_buffers
231       bm.roll_buffers
232     when :roll_buffers_backwards
233       bm.roll_buffers_backwards
234     when :kill_buffer
235       bm.kill_buffer_safely bm.focus_buf
236     when :list_buffers
237       bm.spawn_unless_exists("Buffer List") { BufferListMode.new }
238     when :list_contacts
239       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
240       b.mode.load_in_background if new
241     when :search
242       query = BufferManager.ask :search, "search all messages: "
243       next unless query && query !~ /^\s*$/
244       SearchResultsMode.spawn_from_query query
245     when :search_unread
246       SearchResultsMode.spawn_from_query "is:unread"
247     when :list_labels
248       labels = LabelManager.listable_labels.map { |l| LabelManager.string_for l }
249       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
250       unless user_label.nil?
251         if user_label.empty?
252           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
253         else
254           LabelSearchResultsMode.spawn_nicely user_label
255         end
256       end
257     when :compose
258       ComposeMode.spawn_nicely
259     when :poll
260       reporting_thread("user-invoked poll") { PollManager.poll }
261     when :recall_draft
262       case Index.num_results_for :label => :draft
263       when 0
264         bm.flash "No draft messages."
265       when 1
266         m = nil
267         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
268         r = ResumeMode.new(m)
269         BufferManager.spawn "Edit message", r
270         r.edit_message
271       else
272         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
273         b.mode.load_threads :num => b.content_height if new
274       end
275     when :nothing, InputSequenceAborted
276     when :redraw
277       bm.completely_redraw_screen
278     else
279       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
280     end
281
282     bm.draw_screen
283   end
284
285   bm.kill_all_buffers if SuicideManager.die?
286 rescue Exception => e
287   Redwood::record_exception e, "main"
288 ensure
289   unless $opts[:no_threads]
290     PollManager.stop if PollManager.instantiated?
291     SuicideManager.stop if PollManager.instantiated?
292     Index.stop_lock_update_thread
293   end
294
295   HookManager.run "shutdown"
296
297   Redwood::finish
298   stop_cursing
299   Redwood::log "stopped cursing"
300
301   if SuicideManager.instantiated? && SuicideManager.die?
302     Redwood::log "I've been ordered to commit seppuku. I obey!"
303   end
304
305   if Redwood::exceptions.empty?
306     Redwood::log "no fatal errors. good job, william."
307     Index.save
308   else
309     Redwood::log "oh crap, an exception"
310   end
311
312   Index.unlock
313 end
314
315 unless Redwood::exceptions.empty?
316   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
317     Redwood::exceptions.each do |e, name|
318       f.puts "--- #{e.class.name} from thread: #{name}"
319       f.puts e.message, e.backtrace
320     end
321   end
322   $stderr.puts <<EOS
323 ----------------------------------------------------------------
324 I'm very sorry. It seems that an error occurred in Sup. Please
325 accept my sincere apologies. If you don't mind, please send the
326 contents of ~/.sup/exception-log.txt and a brief report of the
327 circumstances to sup-talk at rubyforge dot orgs so that I might
328 address this problem. Thank you!
329
330 Sincerely,
331 William
332 ----------------------------------------------------------------
333 EOS
334   Redwood::exceptions.each do |e, name|
335     puts "--- #{e.class.name} from thread: #{name}"
336     puts e.message, e.backtrace
337   end
338 end
339
340 end