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