]> git.cworth.org Git - sup/blob - bin/sup
improve dlload of setlocale() and include cygwin
[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 'fastthread'
9 require "sup"
10
11 BIN_VERSION = "git"
12
13 unless Redwood::VERSION == BIN_VERSION
14   $stderr.puts <<EOS
15
16 Error: version mismatch!
17 The sup executable is at version #{BIN_VERSION.inspect}.
18 The sup libraries are at version #{Redwood::VERSION.inspect}.
19
20 Is your development environment conflicting with rubygems?
21 EOS
22   exit(-1)
23 end
24
25 $opts = Trollop::options do
26   version "sup v#{Redwood::VERSION}"
27   banner <<EOS
28 Sup is a curses-based email client.
29
30 Usage:
31   sup [options]
32
33 Options are:
34 EOS
35   opt :list_hooks, "List all hooks and descriptions, and quit."
36   opt :no_threads, "Turn off threading. Helps with debugging. (Necessarily disables background polling for new messages.)"
37   opt :no_initial_poll, "Don't poll for new messages when starting."
38   opt :search, "Search for this query upon startup", :type => String
39   opt :compose, "Compose message to this recipient upon startup", :type => String
40 end
41
42 Redwood::HookManager.register "startup", <<EOS
43 Executes at startup
44 No variables.
45 No return value.
46 EOS
47
48 Redwood::HookManager.register "shutdown", <<EOS 
49 Executes when sup is shutting down. May be run when sup is crashing,
50 so don\'t do anything too important. Run before the label, contacts,
51 and people are saved.
52 No variables.
53 No return value.
54 EOS
55
56 if $opts[:list_hooks]
57   Redwood::HookManager.print_hooks
58   exit
59 end
60
61 Thread.abort_on_exception = true # make debugging possible
62
63 module Redwood
64
65 global_keymap = Keymap.new do |k|
66   k.add :quit_ask, "Quit Sup, but ask first", 'q'
67   k.add :quit_now, "Quit Sup immediately", 'Q'
68   k.add :help, "Show help", '?'
69   k.add :roll_buffers, "Switch to next buffer", 'b'
70 #  k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
71   k.add :kill_buffer, "Kill the current buffer", 'x'
72   k.add :list_buffers, "List all buffers", 'B'
73   k.add :list_contacts, "List contacts", 'C'
74   k.add :redraw, "Redraw screen", :ctrl_l
75   k.add :search, "Search all messages", '\\', 'F'
76   k.add :search_unread, "Show all unread messages", 'U'
77   k.add :list_labels, "List labels", 'L'
78   k.add :poll, "Poll for new messages", 'P'
79   k.add :compose, "Compose new message", 'm', 'c'
80   k.add :nothing, "Do nothing", :ctrl_g
81   k.add :recall_draft, "Edit most recent draft message", 'R'
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::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 "dyamically 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   end
109 end
110
111 def start_cursing
112   Ncurses.initscr
113   Ncurses.noecho
114   Ncurses.cbreak
115   Ncurses.stdscr.keypad 1
116   Ncurses.use_default_colors
117   Ncurses.curs_set 0
118   Ncurses.start_color
119   $cursing = true
120 end
121
122 def stop_cursing
123   return unless $cursing
124   Ncurses.curs_set 1
125   Ncurses.echo
126   Ncurses.endwin
127 end
128 module_function :start_cursing, :stop_cursing
129
130 Index.new
131 begin
132   Index.lock
133 rescue Index::LockError => e
134   require 'highline'
135
136   h = HighLine.new
137   h.wrap_at = :auto
138   h.say Index.fancy_lock_error_message_for(e)
139
140   case h.ask("Should I ask that process to kill itself? ")
141   when /^\s*y(es)?\s*$/i
142     h.say "Ok, suggesting seppuku..."
143     FileUtils.touch Redwood::SUICIDE_FN
144     sleep SuicideManager::DELAY * 2
145     FileUtils.rm_f Redwood::SUICIDE_FN
146     h.say "Let's try that again."
147     retry
148   else
149     h.say <<EOS
150 Ok, giving up. If the process crashed and left a stale lockfile, you
151 can fix this by manually deleting #{Index.lockfile}.
152 EOS
153     exit
154   end
155 end
156
157 begin
158   Redwood::start
159   Index.load
160
161   if(s = Index.source_for DraftManager.source_name)
162     DraftManager.source = s
163   else
164     Redwood::log "no draft source, auto-adding..."
165     Index.add_source DraftManager.new_source
166   end
167
168   if(s = Index.source_for SentManager.source_name)
169     SentManager.source = s
170   else
171     Redwood::log "no sent mail source, auto-adding..."
172     Index.add_source SentManager.new_source
173   end
174
175   HookManager.run "startup"
176
177   log "starting curses"
178   start_cursing
179
180   bm = BufferManager.new
181   Colormap.new.populate_colormap
182
183   log "initializing mail index buffer"
184   imode = InboxMode.new
185   ibuf = bm.spawn "Inbox", imode
186
187   log "ready for interaction!"
188   Logger.make_buf
189
190   bm.draw_screen
191
192   Index.usual_sources.each do |s|
193     next unless s.respond_to? :connect
194     reporting_thread("call #connect on #{s}") do
195       begin
196         s.connect
197       rescue SourceError => e
198         Redwood::log "fatal error loading from #{s}: #{e.message}"
199       end
200     end
201   end unless $opts[:no_initial_poll]
202   
203   imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }
204
205   if $opts[:compose]
206     ComposeMode.spawn_nicely :to_default => $opts[:compose]
207   end
208
209   unless $opts[:no_threads]
210     PollManager.start
211     SuicideManager.start
212     Index.start_lock_update_thread
213   end
214
215   if $opts[:search]
216     SearchResultsMode.spawn_from_query $opts[:search]
217   end
218
219   until Redwood::exceptions.nonempty? || SuicideManager.die?
220     c = 
221        begin
222          Ncurses.nonblocking_getch
223        rescue Exception => e
224          if e.is_a?(Interrupt)
225            raise if BufferManager.ask_yes_or_no("Die ungracefully now?")
226            bm.draw_screen
227            nil
228          end
229        end
230     next unless c
231     bm.erase_flash
232
233     action =
234       begin
235         if bm.handle_input c
236           :nothing
237         else
238           bm.resolve_input_with_keymap c, global_keymap
239         end
240       rescue InputSequenceAborted
241         :nothing
242       end
243     case action
244     when :quit_now
245       break if bm.kill_all_buffers_safely
246     when :quit_ask
247       if bm.ask_yes_or_no "Really quit?"
248         break if bm.kill_all_buffers_safely
249       end
250     when :help
251       curmode = bm.focus_buf.mode
252       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
253     when :roll_buffers
254       bm.roll_buffers
255     when :roll_buffers_backwards
256       bm.roll_buffers_backwards
257     when :kill_buffer
258       bm.kill_buffer_safely bm.focus_buf
259     when :list_buffers
260       bm.spawn_unless_exists("Buffer List") { BufferListMode.new }
261     when :list_contacts
262       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
263       b.mode.load_in_background if new
264     when :search
265       query = BufferManager.ask :search, "search all messages: "
266       next unless query && query !~ /^\s*$/
267       SearchResultsMode.spawn_from_query query
268     when :search_unread
269       SearchResultsMode.spawn_from_query "is:unread"
270     when :list_labels
271       labels = LabelManager.listable_labels.map { |l| LabelManager.string_for l }
272       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
273       unless user_label.nil?
274         if user_label.empty?
275           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
276         else
277           LabelSearchResultsMode.spawn_nicely user_label
278         end
279       end
280     when :compose
281       ComposeMode.spawn_nicely
282     when :poll
283       reporting_thread("user-invoked poll") { PollManager.poll }
284     when :recall_draft
285       case Index.num_results_for :label => :draft
286       when 0
287         bm.flash "No draft messages."
288       when 1
289         m = nil
290         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
291         r = ResumeMode.new(m)
292         BufferManager.spawn "Edit message", r
293         r.edit_message
294       else
295         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
296         b.mode.load_threads :num => b.content_height if new
297       end
298     when :nothing, InputSequenceAborted
299     when :redraw
300       bm.completely_redraw_screen
301     else
302       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
303     end
304
305     bm.draw_screen
306   end
307
308   bm.kill_all_buffers if SuicideManager.die?
309 rescue Exception => e
310   Redwood::record_exception e, "main"
311 ensure
312   unless $opts[:no_threads]
313     PollManager.stop if PollManager.instantiated?
314     SuicideManager.stop if PollManager.instantiated?
315     Index.stop_lock_update_thread
316   end
317
318   HookManager.run "shutdown"
319
320   Redwood::finish
321   stop_cursing
322   Redwood::log "stopped cursing"
323
324   if SuicideManager.instantiated? && SuicideManager.die?
325     Redwood::log "I've been ordered to commit seppuku. I obey!"
326   end
327
328   if Redwood::exceptions.empty?
329     Redwood::log "no fatal errors. good job, william."
330     Index.save
331   else
332     Redwood::log "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