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