]> git.cworth.org Git - sup/blob - bin/sup
Merge branch 'reply-all-keybindings' 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   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
173   debug "initializing inbox buffer"
174   imode = InboxMode.new
175   ibuf = bm.spawn "Inbox", imode
176
177   debug "ready for interaction!"
178
179   bm.draw_screen
180
181   Redwood::SourceManager.usual_sources.each do |s|
182     next unless s.respond_to? :connect
183     reporting_thread("call #connect on #{s}") do
184       begin
185         s.connect
186       rescue SourceError => e
187         error "fatal error loading from #{s}: #{e.message}"
188       end
189     end
190   end unless $opts[:no_initial_poll]
191
192   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] }
193
194   if $opts[:compose]
195     ComposeMode.spawn_nicely :to_default => $opts[:compose]
196   end
197
198   unless $opts[:no_threads]
199     PollManager.start
200     Index.start_lock_update_thread
201   end
202
203   if $opts[:search]
204     SearchResultsMode.spawn_from_query $opts[:search]
205   end
206
207   until Redwood::exceptions.nonempty? || $die
208     c = begin
209       Ncurses.nonblocking_getch
210     rescue Interrupt => e
211       raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
212       BufferManager.draw_screen
213       nil
214     end
215
216     if c.nil?
217       if BufferManager.sigwinch_happened?
218         debug "redrawing screen on sigwinch"
219         BufferManager.completely_redraw_screen
220       end
221       next
222     end
223
224     if c == 410
225       ## this is ncurses's way of telling us it's detected a refresh.
226       ## since we have our own sigwinch handler, we don't do anything.
227       next
228     end
229
230     bm.erase_flash
231
232     action =
233       begin
234         if bm.handle_input c
235           :nothing
236         else
237           bm.resolve_input_with_keymap c, global_keymap
238         end
239       rescue InputSequenceAborted
240         :nothing
241       end
242     case action
243     when :quit_now
244       break if bm.kill_all_buffers_safely
245     when :quit_ask
246       if bm.ask_yes_or_no "Really quit?"
247         break if bm.kill_all_buffers_safely
248       end
249     when :help
250       curmode = bm.focus_buf.mode
251       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
252     when :roll_buffers
253       bm.roll_buffers
254     when :roll_buffers_backwards
255       bm.roll_buffers_backwards
256     when :kill_buffer
257       bm.kill_buffer_safely bm.focus_buf
258     when :list_buffers
259       bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
260     when :list_contacts
261       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
262       b.mode.load_in_background if new
263     when :search
264       query = BufferManager.ask :search, "search all messages: "
265       next unless query && query !~ /^\s*$/
266       SearchResultsMode.spawn_from_query query
267     when :search_unread
268       SearchResultsMode.spawn_from_query "is:unread"
269     when :list_labels
270       labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
271       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
272       unless user_label.nil?
273         if user_label.empty?
274           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
275         else
276           LabelSearchResultsMode.spawn_nicely user_label
277         end
278       end
279     when :compose
280       ComposeMode.spawn_nicely
281     when :poll
282       reporting_thread("user-invoked poll") { PollManager.poll }
283     when :recall_draft
284       case Index.num_results_for :label => :draft
285       when 0
286         bm.flash "No draft messages."
287       when 1
288         m = nil
289         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
290         r = ResumeMode.new(m)
291         BufferManager.spawn "Edit message", r
292         r.edit_message
293       else
294         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
295         b.mode.load_threads :num => b.content_height if new
296       end
297     when :show_inbox
298       BufferManager.raise_to_front ibuf
299     when :show_console
300       b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new }
301       b.mode.run
302     when :nothing, InputSequenceAborted
303     when :redraw
304       bm.completely_redraw_screen
305     else
306       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
307     end
308
309     bm.draw_screen
310   end
311
312   bm.kill_all_buffers if $die
313 rescue Exception => e
314   Redwood::record_exception e, "main"
315 ensure
316   unless $opts[:no_threads]
317     PollManager.stop if PollManager.instantiated?
318     Index.stop_lock_update_thread
319   end
320
321   HookManager.run "shutdown"
322
323   Redwood::finish
324   stop_cursing
325   Redwood::Logger.remove_all_sinks!
326   Redwood::Logger.add_sink $stderr, false
327   debug "stopped cursing"
328
329   if $die
330     info "I've been ordered to commit seppuku. I obey!"
331   end
332
333   if Redwood::exceptions.empty?
334     debug "no fatal errors. good job, william."
335     Index.save
336   else
337     error "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