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