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