]> git.cworth.org Git - sup/blob - bin/sup
Merge branch 'ncurses-fixes' 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 "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.new
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   trap("TERM") { |x| raise "so speaking as i think, i die, i die!" }
164
165   if(s = Redwood::SourceManager.source_for DraftManager.source_name)
166     DraftManager.source = s
167   else
168     Redwood::log "no draft source, auto-adding..."
169     Redwood::SourceManager.add_source DraftManager.new_source
170   end
171
172   if(s = Redwood::SourceManager.source_for SentManager.source_uri)
173     SentManager.source = s
174   else
175     Redwood::SourceManager.add_source SentManager.default_source
176   end
177
178   HookManager.run "startup"
179
180   log "starting curses"
181   start_cursing
182
183   bm = BufferManager.new
184   Colormap.new.populate_colormap
185
186   log "initializing mail index buffer"
187   imode = InboxMode.new
188   ibuf = bm.spawn "Inbox", imode
189
190   log "ready for interaction!"
191   Logger.make_buf
192
193   bm.draw_screen
194
195   Redwood::SourceManager.usual_sources.each do |s|
196     next unless s.respond_to? :connect
197     reporting_thread("call #connect on #{s}") do
198       begin
199         s.connect
200       rescue SourceError => e
201         Redwood::log "fatal error loading from #{s}: #{e.message}"
202       end
203     end
204   end unless $opts[:no_initial_poll]
205   
206   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] }
207
208   if $opts[:compose]
209     ComposeMode.spawn_nicely :to_default => $opts[:compose]
210   end
211
212   unless $opts[:no_threads]
213     PollManager.start
214     SuicideManager.start
215     Index.start_lock_update_thread
216   end
217
218   if $opts[:search]
219     SearchResultsMode.spawn_from_query $opts[:search]
220   end
221
222   until Redwood::exceptions.nonempty? || SuicideManager.die?
223     c = begin
224       Ncurses.nonblocking_getch
225     rescue Interrupt => e
226       raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
227       BufferManager.draw_screen
228       nil
229     end
230
231     next unless c
232     bm.erase_flash
233
234     action = begin
235       if bm.handle_input c
236         :nothing
237       else
238         bm.resolve_input_with_keymap c, global_keymap
239       end
240     rescue InputSequenceAborted
241       :nothing
242     end
243     case action
244     when :quit_now
245       break if bm.kill_all_buffers_safely
246     when :quit_ask
247       if bm.ask_yes_or_no "Really quit?"
248         break if bm.kill_all_buffers_safely
249       end
250     when :help
251       curmode = bm.focus_buf.mode
252       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
253     when :roll_buffers
254       bm.roll_buffers
255     when :roll_buffers_backwards
256       bm.roll_buffers_backwards
257     when :kill_buffer
258       bm.kill_buffer_safely bm.focus_buf
259     when :list_buffers
260       bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
261     when :list_contacts
262       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
263       b.mode.load_in_background if new
264     when :search
265       query = BufferManager.ask :search, "search all messages: "
266       next unless query && query !~ /^\s*$/
267       SearchResultsMode.spawn_from_query query
268     when :search_unread
269       SearchResultsMode.spawn_from_query "is:unread"
270     when :list_labels
271       labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
272       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
273       unless user_label.nil?
274         if user_label.empty?
275           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
276         else
277           LabelSearchResultsMode.spawn_nicely user_label
278         end
279       end
280     when :compose
281       ComposeMode.spawn_nicely
282     when :poll
283       reporting_thread("user-invoked poll") { PollManager.poll }
284     when :recall_draft
285       case Index.num_results_for :label => :draft
286       when 0
287         bm.flash "No draft messages."
288       when 1
289         m = nil
290         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
291         r = ResumeMode.new(m)
292         BufferManager.spawn "Edit message", r
293         r.edit_message
294       else
295         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
296         b.mode.load_threads :num => b.content_height if new
297       end
298     when :nothing, InputSequenceAborted
299     when :redraw
300       bm.completely_redraw_screen
301     else
302       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
303     end
304
305     bm.draw_screen
306   end
307
308   bm.kill_all_buffers if SuicideManager.die?
309 rescue Exception => e
310   Redwood::record_exception e, "main"
311 ensure
312   unless $opts[:no_threads]
313     PollManager.stop if PollManager.instantiated?
314     SuicideManager.stop if PollManager.instantiated?
315     Index.stop_lock_update_thread
316   end
317
318   HookManager.run "shutdown"
319
320   Redwood::finish
321   stop_cursing
322   Redwood::log "stopped cursing"
323
324   if SuicideManager.instantiated? && SuicideManager.die?
325     Redwood::log "I've been ordered to commit seppuku. I obey!"
326   end
327
328   if Redwood::exceptions.empty?
329     Redwood::log "no fatal errors. good job, william."
330     Index.save
331   else
332     Redwood::log "oh crap, an exception"
333   end
334
335   Index.unlock
336 end
337
338 unless Redwood::exceptions.empty?
339   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
340     Redwood::exceptions.each do |e, name|
341       f.puts "--- #{e.class.name} from thread: #{name}"
342       f.puts e.message, e.backtrace
343     end
344   end
345   $stderr.puts <<EOS
346 ----------------------------------------------------------------
347 I'm very sorry. It seems that an error occurred in Sup. Please
348 accept my sincere apologies. If you don't mind, please send the
349 contents of ~/.sup/exception-log.txt and a brief report of the
350 circumstances to sup-talk at rubyforge dot orgs so that I might
351 address this problem. Thank you!
352
353 Sincerely,
354 William
355 ----------------------------------------------------------------
356 EOS
357   Redwood::exceptions.each do |e, name|
358     puts "--- #{e.class.name} from thread: #{name}"
359     puts e.message, e.backtrace
360   end
361 end
362
363 end