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