]> git.cworth.org Git - sup/blob - bin/sup
Merge branch 'ncurses-fixes' 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 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 =
225       begin
226         if bm.handle_input c
227           :nothing
228         else
229           bm.resolve_input_with_keymap c, global_keymap
230         end
231       rescue InputSequenceAborted
232         :nothing
233       end
234     case action
235     when :quit_now
236       break if bm.kill_all_buffers_safely
237     when :quit_ask
238       if bm.ask_yes_or_no "Really quit?"
239         break if bm.kill_all_buffers_safely
240       end
241     when :help
242       curmode = bm.focus_buf.mode
243       bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
244     when :roll_buffers
245       bm.roll_buffers
246     when :roll_buffers_backwards
247       bm.roll_buffers_backwards
248     when :kill_buffer
249       bm.kill_buffer_safely bm.focus_buf
250     when :list_buffers
251       bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
252     when :list_contacts
253       b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
254       b.mode.load_in_background if new
255     when :search
256       query = BufferManager.ask :search, "search all messages: "
257       next unless query && query !~ /^\s*$/
258       SearchResultsMode.spawn_from_query query
259     when :search_unread
260       SearchResultsMode.spawn_from_query "is:unread"
261     when :list_labels
262       labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
263       user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
264       unless user_label.nil?
265         if user_label.empty?
266           bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
267         else
268           LabelSearchResultsMode.spawn_nicely user_label
269         end
270       end
271     when :compose
272       ComposeMode.spawn_nicely
273     when :poll
274       reporting_thread("user-invoked poll") { PollManager.poll }
275     when :recall_draft
276       case Index.num_results_for :label => :draft
277       when 0
278         bm.flash "No draft messages."
279       when 1
280         m = nil
281         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
282         r = ResumeMode.new(m)
283         BufferManager.spawn "Edit message", r
284         r.edit_message
285       else
286         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
287         b.mode.load_threads :num => b.content_height if new
288       end
289     when :nothing, InputSequenceAborted
290     when :redraw
291       bm.completely_redraw_screen
292     else
293       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
294     end
295
296     bm.draw_screen
297   end
298
299   bm.kill_all_buffers if $die
300 rescue Exception => e
301   Redwood::record_exception e, "main"
302 ensure
303   unless $opts[:no_threads]
304     PollManager.stop if PollManager.instantiated?
305     Index.stop_lock_update_thread
306   end
307
308   HookManager.run "shutdown"
309
310   Redwood::finish
311   stop_cursing
312   Redwood::log "stopped cursing"
313
314   if $die
315     Redwood::log "I've been ordered to commit seppuku. I obey!"
316   end
317
318   if Redwood::exceptions.empty?
319     Redwood::log "no fatal errors. good job, william."
320     Index.save
321   else
322     Redwood::log "oh crap, an exception"
323   end
324
325   Index.unlock
326 end
327
328 unless Redwood::exceptions.empty?
329   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
330     Redwood::exceptions.each do |e, name|
331       f.puts "--- #{e.class.name} from thread: #{name}"
332       f.puts e.message, e.backtrace
333     end
334   end
335   $stderr.puts <<EOS
336 ----------------------------------------------------------------
337 I'm very sorry. It seems that an error occurred in Sup. Please
338 accept my sincere apologies. If you don't mind, please send the
339 contents of ~/.sup/exception-log.txt and a brief report of the
340 circumstances to sup-talk at rubyforge dot orgs so that I might
341 address this problem. Thank you!
342
343 Sincerely,
344 William
345 ----------------------------------------------------------------
346 EOS
347   Redwood::exceptions.each do |e, name|
348     puts "--- #{e.class.name} from thread: #{name}"
349     puts e.message, e.backtrace
350   end
351 end
352
353 end