]> git.cworth.org Git - sup/blob - bin/sup
keybindings: ; -> buffer-list-mode, b, B, +
[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", ';'
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.use_default_colors
90   Ncurses.curs_set 0
91   Ncurses.start_color
92   $cursing = true
93 end
94
95 def stop_cursing
96   return unless $cursing
97   Ncurses.curs_set 1
98   Ncurses.echo
99   Ncurses.endwin
100 end
101 module_function :start_cursing, :stop_cursing
102
103 Index.new
104 begin
105   Index.lock
106 rescue Index::LockError => e
107   require 'highline'
108
109   h = HighLine.new
110   h.wrap_at = :auto
111   h.say Index.fancy_lock_error_message_for(e)
112
113   case h.ask("Should I ask that process to kill itself? ")
114   when /^\s*y(es)?\s*$/i
115     h.say "Ok, suggesting seppuku..."
116     FileUtils.touch Redwood::SUICIDE_FN
117     sleep SuicideManager::DELAY * 2
118     FileUtils.rm_f Redwood::SUICIDE_FN
119     h.say "Let's try that again."
120     retry
121   else
122     h.say <<EOS
123 Ok, giving up. If the process crashed and left a stale lockfile, you
124 can fix this by manually deleting #{Index.lockfile}.
125 EOS
126     exit
127   end
128 end
129
130 begin
131   Redwood::start
132   Index.load
133
134   if(s = Index.source_for DraftManager.source_name)
135     DraftManager.source = s
136   else
137     Redwood::log "no draft source, auto-adding..."
138     Index.add_source DraftManager.new_source
139   end
140
141   if(s = Index.source_for SentManager.source_name)
142     SentManager.source = s
143   else
144     Redwood::log "no sent mail source, auto-adding..."
145     Index.add_source SentManager.new_source
146   end
147
148   HookManager.run "startup"
149
150   log "starting curses"
151   start_cursing
152
153   bm = BufferManager.new
154   Colormap.new.populate_colormap
155
156   log "initializing mail index buffer"
157   imode = InboxMode.new
158   ibuf = bm.spawn "Inbox", imode
159
160   log "ready for interaction!"
161   Logger.make_buf
162
163   bm.draw_screen
164
165   Index.usual_sources.each do |s|
166     next unless s.respond_to? :connect
167     reporting_thread("call #connect on #{s}") do
168       begin
169         s.connect
170       rescue SourceError => e
171         Redwood::log "fatal error loading from #{s}: #{e.message}"
172       end
173     end
174   end unless $opts[:no_initial_poll]
175   
176   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] }
177
178   if $opts[:compose]
179     ComposeMode.spawn_nicely :to_default => $opts[:compose]
180   end
181
182   unless $opts[:no_threads]
183     PollManager.start
184     SuicideManager.start
185     Index.start_lock_update_thread
186   end
187
188   if $opts[:search]
189     SearchResultsMode.spawn_from_query $opts[:search]
190   end
191
192   until Redwood::exceptions.nonempty? || SuicideManager.die?
193     c = 
194        begin
195          Ncurses.nonblocking_getch
196        rescue Exception => e
197          if e.is_a?(Interrupt)
198            raise if BufferManager.ask_yes_or_no("Die ungracefully now?")
199            bm.draw_screen
200            nil
201          end
202        end
203     next unless c
204     bm.erase_flash
205
206     action =
207       begin
208         if bm.handle_input c
209           :nothing
210         else
211           bm.resolve_input_with_keymap c, global_keymap
212         end
213       rescue InputSequenceAborted
214         :nothing
215       end
216     case action
217     when :quit_now
218       break if bm.kill_all_buffers_safely
219     when :quit_ask
220       if bm.ask_yes_or_no "Really quit?"
221         break if bm.kill_all_buffers_safely
222       end
223     when :help
224       curmode = bm.focus_buf.mode
225       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
226     when :roll_buffers
227       bm.roll_buffers
228     when :roll_buffers_backwards
229       bm.roll_buffers_backwards
230     when :kill_buffer
231       bm.kill_buffer_safely bm.focus_buf
232     when :list_buffers
233       bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
234     when :list_contacts
235       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
236       b.mode.load_in_background if new
237     when :search
238       query = BufferManager.ask :search, "search all messages: "
239       next unless query && query !~ /^\s*$/
240       SearchResultsMode.spawn_from_query query
241     when :search_unread
242       SearchResultsMode.spawn_from_query "is:unread"
243     when :list_labels
244       labels = LabelManager.listable_labels.map { |l| LabelManager.string_for l }
245       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
246       unless user_label.nil?
247         if user_label.empty?
248           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
249         else
250           LabelSearchResultsMode.spawn_nicely user_label
251         end
252       end
253     when :compose
254       ComposeMode.spawn_nicely
255     when :poll
256       reporting_thread("user-invoked poll") { PollManager.poll }
257     when :recall_draft
258       case Index.num_results_for :label => :draft
259       when 0
260         bm.flash "No draft messages."
261       when 1
262         m = nil
263         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
264         r = ResumeMode.new(m)
265         BufferManager.spawn "Edit message", r
266         r.edit_message
267       else
268         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
269         b.mode.load_threads :num => b.content_height if new
270       end
271     when :nothing, InputSequenceAborted
272     when :redraw
273       bm.completely_redraw_screen
274     else
275       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
276     end
277
278     bm.draw_screen
279   end
280
281   bm.kill_all_buffers if SuicideManager.die?
282 rescue Exception => e
283   Redwood::record_exception e, "main"
284 ensure
285   unless $opts[:no_threads]
286     PollManager.stop if PollManager.instantiated?
287     SuicideManager.stop if PollManager.instantiated?
288     Index.stop_lock_update_thread
289   end
290
291   HookManager.run "shutdown"
292
293   Redwood::finish
294   stop_cursing
295   Redwood::log "stopped cursing"
296
297   if SuicideManager.instantiated? && SuicideManager.die?
298     Redwood::log "I've been ordered to commit seppuku. I obey!"
299   end
300
301   if Redwood::exceptions.empty?
302     Redwood::log "no fatal errors. good job, william."
303     Index.save
304   else
305     Redwood::log "oh crap, an exception"
306   end
307
308   Index.unlock
309 end
310
311 unless Redwood::exceptions.empty?
312   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
313     Redwood::exceptions.each do |e, name|
314       f.puts "--- #{e.class.name} from thread: #{name}"
315       f.puts e.message, e.backtrace
316     end
317   end
318   $stderr.puts <<EOS
319 ----------------------------------------------------------------
320 I'm very sorry. It seems that an error occurred in Sup. Please
321 accept my sincere apologies. If you don't mind, please send the
322 contents of ~/.sup/exception-log.txt and a brief report of the
323 circumstances to sup-talk at rubyforge dot orgs so that I might
324 address this problem. Thank you!
325
326 Sincerely,
327 William
328 ----------------------------------------------------------------
329 EOS
330   Redwood::exceptions.each do |e, name|
331     puts "--- #{e.class.name} from thread: #{name}"
332     puts e.message, e.backtrace
333   end
334 end
335
336 end