]> git.cworth.org Git - sup/blob - bin/sup
Merge branch 'buffer-rolling' 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   debug "dynamically loading setlocale() from #{setlocale_lib}"
99   begin
100     dlload setlocale_lib
101     extern "void setlocale(int, const char *)"
102     debug "setting locale..."
103     LibC.setlocale(6, "")  # LC_ALL == 6
104   rescue RuntimeError => e
105     warn "cannot dlload setlocale(); ncurses wide character support probably broken."
106     warn "dlload error was #{e.class}: #{e.message}"
107     if Config::CONFIG['arch'] =~ /bsd/
108       warn "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.init
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     debug "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   debug "starting curses"
159   Redwood::Logger.remove_sink $stderr
160   start_cursing
161
162   bm = BufferManager.init
163   Colormap.new.populate_colormap
164
165   debug "initializing log buffer"
166   lmode = Redwood::LogMode.new "system log"
167   lmode.on_kill { Logger.clear! }
168   Logger.add_sink lmode
169   Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
170
171   debug "initializing inbox buffer"
172   imode = InboxMode.new
173   ibuf = bm.spawn "Inbox", imode
174
175   debug "ready for interaction!"
176
177   bm.draw_screen
178
179   Redwood::SourceManager.usual_sources.each do |s|
180     next unless s.respond_to? :connect
181     reporting_thread("call #connect on #{s}") do
182       begin
183         s.connect
184       rescue SourceError => e
185         error "fatal error loading from #{s}: #{e.message}"
186       end
187     end
188   end unless $opts[:no_initial_poll]
189
190   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] }
191
192   if $opts[:compose]
193     ComposeMode.spawn_nicely :to_default => $opts[:compose]
194   end
195
196   unless $opts[:no_threads]
197     PollManager.start
198     Index.start_lock_update_thread
199   end
200
201   if $opts[:search]
202     SearchResultsMode.spawn_from_query $opts[:search]
203   end
204
205   until Redwood::exceptions.nonempty? || $die
206     c = begin
207       Ncurses.nonblocking_getch
208     rescue Interrupt => e
209       raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
210       BufferManager.draw_screen
211       nil
212     end
213
214     if c.nil?
215       if BufferManager.sigwinch_happened?
216         debug "redrawing screen on sigwinch"
217         BufferManager.completely_redraw_screen
218       end
219       next
220     end
221
222     if c == 410
223       ## this is ncurses's way of telling us it's detected a refresh.
224       ## since we have our own sigwinch handler, we don't do anything.
225       next
226     end
227
228     bm.erase_flash
229
230     action =
231       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 :nothing, InputSequenceAborted
296     when :redraw
297       bm.completely_redraw_screen
298     else
299       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
300     end
301
302     bm.draw_screen
303   end
304
305   bm.kill_all_buffers if $die
306 rescue Exception => e
307   Redwood::record_exception e, "main"
308 ensure
309   unless $opts[:no_threads]
310     PollManager.stop if PollManager.instantiated?
311     Index.stop_lock_update_thread
312   end
313
314   HookManager.run "shutdown"
315
316   Redwood::finish
317   stop_cursing
318   Redwood::Logger.remove_all_sinks!
319   Redwood::Logger.add_sink $stderr, false
320   debug "stopped cursing"
321
322   if $die
323     info "I've been ordered to commit seppuku. I obey!"
324   end
325
326   if Redwood::exceptions.empty?
327     debug "no fatal errors. good job, william."
328     Index.save
329   else
330     error "oh crap, an exception"
331   end
332
333   Index.unlock
334 end
335
336 unless Redwood::exceptions.empty?
337   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
338     Redwood::exceptions.each do |e, name|
339       f.puts "--- #{e.class.name} from thread: #{name}"
340       f.puts e.message, e.backtrace
341     end
342   end
343   $stderr.puts <<EOS
344 ----------------------------------------------------------------
345 I'm very sorry. It seems that an error occurred in Sup. Please
346 accept my sincere apologies. If you don't mind, please send the
347 contents of ~/.sup/exception-log.txt and a brief report of the
348 circumstances to sup-talk at rubyforge dot orgs so that I might
349 address this problem. Thank you!
350
351 Sincerely,
352 William
353 ----------------------------------------------------------------
354 EOS
355   Redwood::exceptions.each do |e, name|
356     puts "--- #{e.class.name} from thread: #{name}"
357     puts e.message, e.backtrace
358   end
359 end
360
361 end