]> git.cworth.org Git - sup/blob - bin/sup
Merge branch 'ncurses-fixes'
[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   k.add :show_inbox, "Show the Inbox buffer", 'I'
82 end
83
84 ## the following magic enables wide characters when used with a ruby
85 ## ncurses.so that's been compiled against libncursesw. (note the w.) why
86 ## this works, i have no idea. much like pretty much every aspect of
87 ## dealing with curses.  cargo cult programming at its best.
88 ##
89 ## BSD users: if libc.so.6 is not found, try installing compat6x.
90 require 'dl/import'
91 module LibC
92   extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable
93   setlocale_lib = case Config::CONFIG['arch']
94     when /darwin/; "libc.dylib"
95     when /cygwin/; "cygwin1.dll"
96     else; "libc.so.6"
97   end
98
99   debug "dynamically loading setlocale() from #{setlocale_lib}"
100   begin
101     dlload setlocale_lib
102     extern "void setlocale(int, const char *)"
103     debug "setting locale..."
104     LibC.setlocale(6, "")  # LC_ALL == 6
105   rescue RuntimeError => e
106     warn "cannot dlload setlocale(); ncurses wide character support probably broken."
107     warn "dlload error was #{e.class}: #{e.message}"
108     if Config::CONFIG['arch'] =~ /bsd/
109       warn "BSD variant detected. You may have to install a compat6x package to acquire libc."
110     end
111   end
112 end
113
114 def start_cursing
115   Ncurses.initscr
116   Ncurses.noecho
117   Ncurses.cbreak
118   Ncurses.stdscr.keypad 1
119   Ncurses.use_default_colors
120   Ncurses.curs_set 0
121   Ncurses.start_color
122   $cursing = true
123 end
124
125 def stop_cursing
126   return unless $cursing
127   Ncurses.curs_set 1
128   Ncurses.echo
129   Ncurses.endwin
130 end
131 module_function :start_cursing, :stop_cursing
132
133 Index.init
134 begin
135   Index.lock
136 rescue Index::LockError => e
137   require 'highline'
138
139   h = HighLine.new
140   h.wrap_at = :auto
141   h.say Index.fancy_lock_error_message_for(e)
142
143   case h.ask("Should I ask that process to kill itself? ")
144   when /^\s*y(es)?\s*$/i
145     h.say "Ok, suggesting seppuku..."
146     FileUtils.touch Redwood::SUICIDE_FN
147     sleep SuicideManager::DELAY * 2
148     FileUtils.rm_f Redwood::SUICIDE_FN
149     h.say "Let's try that again."
150     retry
151   else
152     h.say <<EOS
153 Ok, giving up. If the process crashed and left a stale lockfile, you
154 can fix this by manually deleting #{Index.lockfile}.
155 EOS
156     exit
157   end
158 end
159
160 begin
161   Redwood::start
162   Index.load
163
164   trap("TERM") { |x| SuicideManager.please_die! }
165   trap("WINCH") { |x| BufferManager.sigwinch_happened! }
166
167   if(s = Redwood::SourceManager.source_for DraftManager.source_name)
168     DraftManager.source = s
169   else
170     debug "no draft source, auto-adding..."
171     Redwood::SourceManager.add_source DraftManager.new_source
172   end
173
174   if(s = Redwood::SourceManager.source_for SentManager.source_uri)
175     SentManager.source = s
176   else
177     Redwood::SourceManager.add_source SentManager.default_source
178   end
179
180   HookManager.run "startup"
181
182   debug "starting curses"
183   Redwood::Logger.remove_sink $stderr
184   start_cursing
185
186   bm = BufferManager.init
187   Colormap.new.populate_colormap
188
189   debug "initializing log buffer"
190   lmode = Redwood::LogMode.new "system log"
191   lmode.on_kill { Logger.clear! }
192   Logger.add_sink lmode
193   Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
194
195   debug "initializing inbox buffer"
196   imode = InboxMode.new
197   ibuf = bm.spawn "Inbox", imode
198
199   debug "ready for interaction!"
200
201   bm.draw_screen
202
203   Redwood::SourceManager.usual_sources.each do |s|
204     next unless s.respond_to? :connect
205     reporting_thread("call #connect on #{s}") do
206       begin
207         s.connect
208       rescue SourceError => e
209         error "fatal error loading from #{s}: #{e.message}"
210       end
211     end
212   end unless $opts[:no_initial_poll]
213
214   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] }
215
216   if $opts[:compose]
217     ComposeMode.spawn_nicely :to_default => $opts[:compose]
218   end
219
220   unless $opts[:no_threads]
221     PollManager.start
222     SuicideManager.start
223     Index.start_lock_update_thread
224   end
225
226   if $opts[:search]
227     SearchResultsMode.spawn_from_query $opts[:search]
228   end
229
230   until Redwood::exceptions.nonempty? || SuicideManager.die?
231     c = begin
232       Ncurses.nonblocking_getch
233     rescue Interrupt => e
234       raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
235       BufferManager.draw_screen
236       nil
237     end
238
239     if c.nil?
240       if BufferManager.sigwinch_happened?
241         debug "redrawing screen on sigwinch"
242         BufferManager.completely_redraw_screen
243       end
244       next
245     end
246
247     if c == 410
248       ## this is ncurses's way of telling us it's detected a refresh.
249       ## since we have our own sigwinch handler, we don't do anything.
250       next
251     end
252
253     bm.erase_flash
254
255     action = begin
256       if bm.handle_input c
257         :nothing
258       else
259         bm.resolve_input_with_keymap c, global_keymap
260       end
261     rescue InputSequenceAborted
262       :nothing
263     end
264     case action
265     when :quit_now
266       break if bm.kill_all_buffers_safely
267     when :quit_ask
268       if bm.ask_yes_or_no "Really quit?"
269         break if bm.kill_all_buffers_safely
270       end
271     when :help
272       curmode = bm.focus_buf.mode
273       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
274     when :roll_buffers
275       bm.roll_buffers
276     when :roll_buffers_backwards
277       bm.roll_buffers_backwards
278     when :kill_buffer
279       bm.kill_buffer_safely bm.focus_buf
280     when :list_buffers
281       bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
282     when :list_contacts
283       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
284       b.mode.load_in_background if new
285     when :search
286       query = BufferManager.ask :search, "search all messages: "
287       next unless query && query !~ /^\s*$/
288       SearchResultsMode.spawn_from_query query
289     when :search_unread
290       SearchResultsMode.spawn_from_query "is:unread"
291     when :list_labels
292       labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
293       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
294       unless user_label.nil?
295         if user_label.empty?
296           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
297         else
298           LabelSearchResultsMode.spawn_nicely user_label
299         end
300       end
301     when :compose
302       ComposeMode.spawn_nicely
303     when :poll
304       reporting_thread("user-invoked poll") { PollManager.poll }
305     when :recall_draft
306       case Index.num_results_for :label => :draft
307       when 0
308         bm.flash "No draft messages."
309       when 1
310         m = nil
311         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
312         r = ResumeMode.new(m)
313         BufferManager.spawn "Edit message", r
314         r.edit_message
315       else
316         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
317         b.mode.load_threads :num => b.content_height if new
318       end
319     when :show_inbox
320       BufferManager.raise_to_front ibuf
321     when :nothing, InputSequenceAborted
322     when :redraw
323       bm.completely_redraw_screen
324     else
325       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
326     end
327
328     bm.draw_screen
329   end
330
331   bm.kill_all_buffers if SuicideManager.die?
332 rescue Exception => e
333   Redwood::record_exception e, "main"
334 ensure
335   unless $opts[:no_threads]
336     PollManager.stop if PollManager.instantiated?
337     SuicideManager.stop if PollManager.instantiated?
338     Index.stop_lock_update_thread
339   end
340
341   HookManager.run "shutdown"
342
343   Redwood::finish
344   stop_cursing
345   Redwood::Logger.remove_all_sinks!
346   Redwood::Logger.add_sink $stderr, false
347   debug "stopped cursing"
348
349   if SuicideManager.instantiated? && SuicideManager.die?
350     info "I've been ordered to commit seppuku. I obey!"
351   end
352
353   if Redwood::exceptions.empty?
354     debug "no fatal errors. good job, william."
355     Index.save
356   else
357     error "oh crap, an exception"
358   end
359
360   Index.unlock
361 end
362
363 unless Redwood::exceptions.empty?
364   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
365     Redwood::exceptions.each do |e, name|
366       f.puts "--- #{e.class.name} from thread: #{name}"
367       f.puts e.message, e.backtrace
368     end
369   end
370   $stderr.puts <<EOS
371 ----------------------------------------------------------------
372 I'm very sorry. It seems that an error occurred in Sup. Please
373 accept my sincere apologies. If you don't mind, please send the
374 contents of ~/.sup/exception-log.txt and a brief report of the
375 circumstances to sup-talk at rubyforge dot orgs so that I might
376 address this problem. Thank you!
377
378 Sincerely,
379 William
380 ----------------------------------------------------------------
381 EOS
382   Redwood::exceptions.each do |e, name|
383     puts "--- #{e.class.name} from thread: #{name}"
384     puts e.message, e.backtrace
385   end
386 end
387
388 end