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