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