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