]> git.cworth.org Git - sup/blob - bin/sup
protect getch from ctrl-c's outside of the main event loop
[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   k.add :show_console, "Show the Console buffer", '~'
83 end
84
85 ## the following magic enables wide characters when used with a ruby
86 ## ncurses.so that's been compiled against libncursesw. (note the w.) why
87 ## this works, i have no idea. much like pretty much every aspect of
88 ## dealing with curses.  cargo cult programming at its best.
89 ##
90 ## BSD users: if libc.so.6 is not found, try installing compat6x.
91 require 'dl/import'
92 module LibC
93   extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable
94   setlocale_lib = case Config::CONFIG['arch']
95     when /darwin/; "libc.dylib"
96     when /cygwin/; "cygwin1.dll"
97     else; "libc.so.6"
98   end
99
100   debug "dynamically loading setlocale() from #{setlocale_lib}"
101   begin
102     dlload setlocale_lib
103     extern "void setlocale(int, const char *)"
104     debug "setting locale..."
105     LibC.setlocale(6, "")  # LC_ALL == 6
106   rescue RuntimeError => e
107     warn "cannot dlload setlocale(); ncurses wide character support probably broken."
108     warn "dlload error was #{e.class}: #{e.message}"
109     if Config::CONFIG['arch'] =~ /bsd/
110       warn "BSD variant detected. You may have to install a compat6x package to acquire libc."
111     end
112   end
113 end
114
115 def start_cursing
116   Ncurses.initscr
117   Ncurses.noecho
118   Ncurses.cbreak
119   Ncurses.stdscr.keypad 1
120   Ncurses.use_default_colors
121   Ncurses.curs_set 0
122   Ncurses.start_color
123   $cursing = true
124 end
125
126 def stop_cursing
127   return unless $cursing
128   Ncurses.curs_set 1
129   Ncurses.echo
130   Ncurses.endwin
131 end
132 module_function :start_cursing, :stop_cursing
133
134 Index.init
135 Index.lock_interactively or exit
136
137 begin
138   Redwood::start
139   Index.load
140
141   $die = false
142   trap("TERM") { |x| $die = true }
143   trap("WINCH") { |x| BufferManager.sigwinch_happened! }
144
145   if(s = Redwood::SourceManager.source_for DraftManager.source_name)
146     DraftManager.source = s
147   else
148     debug "no draft source, auto-adding..."
149     Redwood::SourceManager.add_source DraftManager.new_source
150   end
151
152   if(s = Redwood::SourceManager.source_for SentManager.source_uri)
153     SentManager.source = s
154   else
155     Redwood::SourceManager.add_source SentManager.default_source
156   end
157
158   HookManager.run "startup"
159
160   debug "starting curses"
161   Redwood::Logger.remove_sink $stderr
162   start_cursing
163
164   bm = BufferManager.init
165   Colormap.new.populate_colormap
166
167   debug "initializing log buffer"
168   lmode = Redwood::LogMode.new "system log"
169   lmode.on_kill { Logger.clear! }
170   Logger.add_sink lmode
171   Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
172   if Logger::LEVELS.index(Logger.level) > 0
173     Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}."
174   end
175
176   debug "initializing inbox buffer"
177   imode = InboxMode.new
178   ibuf = bm.spawn "Inbox", imode
179
180   debug "ready for interaction!"
181
182   bm.draw_screen
183
184   Redwood::SourceManager.usual_sources.each do |s|
185     next unless s.respond_to? :connect
186     reporting_thread("call #connect on #{s}") do
187       begin
188         s.connect
189       rescue SourceError => e
190         error "fatal error loading from #{s}: #{e.message}"
191       end
192     end
193   end unless $opts[:no_initial_poll]
194
195   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] }
196
197   if $opts[:compose]
198     ComposeMode.spawn_nicely :to_default => $opts[:compose]
199   end
200
201   unless $opts[:no_threads]
202     PollManager.start
203     Index.start_lock_update_thread
204   end
205
206   if $opts[:search]
207     SearchResultsMode.spawn_from_query $opts[:search]
208   end
209
210   until Redwood::exceptions.nonempty? || $die
211     c = begin
212       Ncurses.nonblocking_getch
213     rescue Interrupt
214       raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
215       BufferManager.draw_screen
216       nil
217     end
218
219     if c.nil?
220       if BufferManager.sigwinch_happened?
221         debug "redrawing screen on sigwinch"
222         BufferManager.completely_redraw_screen
223       end
224       next
225     end
226
227     if c == 410
228       ## this is ncurses's way of telling us it's detected a refresh.
229       ## since we have our own sigwinch handler, we don't do anything.
230       next
231     end
232
233     bm.erase_flash
234
235     action = 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 :show_inbox
300       BufferManager.raise_to_front ibuf
301     when :show_console
302       b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new }
303       b.mode.run
304     when :nothing, InputSequenceAborted
305     when :redraw
306       bm.completely_redraw_screen
307     else
308       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
309     end
310
311     bm.draw_screen
312   end
313
314   bm.kill_all_buffers if $die
315 rescue Exception => e
316   Redwood::record_exception e, "main"
317 ensure
318   unless $opts[:no_threads]
319     PollManager.stop if PollManager.instantiated?
320     Index.stop_lock_update_thread
321   end
322
323   HookManager.run "shutdown"
324
325   Redwood::finish
326   stop_cursing
327   Redwood::Logger.remove_all_sinks!
328   Redwood::Logger.add_sink $stderr, false
329   debug "stopped cursing"
330
331   if $die
332     info "I've been ordered to commit seppuku. I obey!"
333   end
334
335   if Redwood::exceptions.empty?
336     debug "no fatal errors. good job, william."
337     Index.save
338   else
339     error "oh crap, an exception"
340   end
341
342   Index.unlock
343 end
344
345 unless Redwood::exceptions.empty?
346   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
347     Redwood::exceptions.each do |e, name|
348       f.puts "--- #{e.class.name} from thread: #{name}"
349       f.puts e.message, e.backtrace
350     end
351   end
352   $stderr.puts <<EOS
353 ----------------------------------------------------------------
354 I'm very sorry. It seems that an error occurred in Sup. Please
355 accept my sincere apologies. If you don't mind, please send the
356 contents of ~/.sup/exception-log.txt and a brief report of the
357 circumstances to sup-talk at rubyforge dot orgs so that I might
358 address this problem. Thank you!
359
360 Sincerely,
361 William
362 ----------------------------------------------------------------
363 EOS
364   Redwood::exceptions.each do |e, name|
365     puts "--- #{e.class.name} from thread: #{name}"
366     puts e.message, e.backtrace
367   end
368 end
369
370 end