]> git.cworth.org Git - sup/blob - bin/sup
Allow thread index view to sort oldest first
[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 Thread.current.priority = 1 # keep ui responsive
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", ';'
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   k.add :show_inbox, "Show the Inbox buffer", 'I'
83   k.add :show_console, "Show the Console buffer", '~'
84 end
85
86 ## the following magic enables wide characters when used with a ruby
87 ## ncurses.so that's been compiled against libncursesw. (note the w.) why
88 ## this works, i have no idea. much like pretty much every aspect of
89 ## dealing with curses.  cargo cult programming at its best.
90 ##
91 ## BSD users: if libc.so.6 is not found, try installing compat6x.
92 require 'dl/import'
93 module LibC
94   extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable
95   setlocale_lib = case Config::CONFIG['arch']
96     when /darwin/; "libc.dylib"
97     when /cygwin/; "cygwin1.dll"
98     else; "libc.so.6"
99   end
100
101   debug "dynamically loading setlocale() from #{setlocale_lib}"
102   begin
103     dlload setlocale_lib
104     extern "void setlocale(int, const char *)"
105     debug "setting locale..."
106     LibC.setlocale(6, "")  # LC_ALL == 6
107   rescue RuntimeError => e
108     warn "cannot dlload setlocale(); ncurses wide character support probably broken."
109     warn "dlload error was #{e.class}: #{e.message}"
110     if Config::CONFIG['arch'] =~ /bsd/
111       warn "BSD variant detected. You may have to install a compat6x package to acquire libc."
112     end
113   end
114 end
115
116 def start_cursing
117   Ncurses.initscr
118   Ncurses.noecho
119   Ncurses.cbreak
120   Ncurses.stdscr.keypad 1
121   Ncurses.use_default_colors
122   Ncurses.curs_set 0
123   Ncurses.start_color
124   $cursing = true
125 end
126
127 def stop_cursing
128   return unless $cursing
129   Ncurses.curs_set 1
130   Ncurses.echo
131   Ncurses.endwin
132 end
133 module_function :start_cursing, :stop_cursing
134
135 Index.init
136 Index.lock_interactively or exit
137
138 begin
139   Redwood::start
140   Index.load
141
142   $die = false
143   trap("TERM") { |x| $die = true }
144   trap("WINCH") { |x| BufferManager.sigwinch_happened! }
145
146   if(s = Redwood::SourceManager.source_for DraftManager.source_name)
147     DraftManager.source = s
148   else
149     debug "no draft source, auto-adding..."
150     Redwood::SourceManager.add_source DraftManager.new_source
151   end
152
153   if(s = Redwood::SourceManager.source_for SentManager.source_uri)
154     SentManager.source = s
155   else
156     Redwood::SourceManager.add_source SentManager.default_source
157   end
158
159   HookManager.run "startup"
160
161   debug "starting curses"
162   Redwood::Logger.remove_sink $stderr
163   start_cursing
164
165   bm = BufferManager.init
166   Colormap.new.populate_colormap
167
168   debug "initializing log buffer"
169   lmode = Redwood::LogMode.new "system log"
170   lmode.on_kill { Logger.clear! }
171   Logger.add_sink lmode
172   Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
173   if Logger::LEVELS.index(Logger.level) > 0
174     Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}."
175   end
176
177   debug "initializing inbox buffer"
178   imode = InboxMode.new
179   ibuf = bm.spawn "Inbox", imode
180
181   debug "ready for interaction!"
182
183   bm.draw_screen
184
185   Redwood::SourceManager.usual_sources.each do |s|
186     next unless s.respond_to? :connect
187     reporting_thread("call #connect on #{s}") do
188       begin
189         s.connect
190       rescue SourceError => e
191         error "fatal error loading from #{s}: #{e.message}"
192       end
193     end
194   end unless $opts[:no_initial_poll]
195
196   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] }
197
198   if $opts[:compose]
199     ComposeMode.spawn_nicely :to_default => $opts[:compose]
200   end
201
202   unless $opts[:no_threads]
203     PollManager.start
204     Index.start_lock_update_thread
205   end
206
207   if $opts[:search]
208     SearchResultsMode.spawn_from_query $opts[:search], true
209   end
210
211   until Redwood::exceptions.nonempty? || $die
212     c = begin
213       Ncurses.nonblocking_getch
214     rescue Interrupt
215       raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
216       BufferManager.draw_screen
217       nil
218     end
219
220     if c.nil?
221       if BufferManager.sigwinch_happened?
222         debug "redrawing screen on sigwinch"
223         BufferManager.completely_redraw_screen
224       end
225       next
226     end
227
228     if c == 410
229       ## this is ncurses's way of telling us it's detected a refresh.
230       ## since we have our own sigwinch handler, we don't do anything.
231       next
232     end
233
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", :system => true) { 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, true
271     when :search_unread
272       SearchResultsMode.spawn_from_query "is:unread", InboxMode.newest_first
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 :show_inbox
302       BufferManager.raise_to_front ibuf
303     when :show_console
304       b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new }
305       b.mode.run
306     when :nothing, InputSequenceAborted
307     when :redraw
308       bm.completely_redraw_screen
309     else
310       bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
311     end
312
313     bm.draw_screen
314   end
315
316   bm.kill_all_buffers if $die
317 rescue Exception => e
318   Redwood::record_exception e, "main"
319 ensure
320   unless $opts[:no_threads]
321     PollManager.stop if PollManager.instantiated?
322     Index.stop_lock_update_thread
323   end
324
325   HookManager.run "shutdown"
326
327   Redwood::finish
328   stop_cursing
329   Redwood::Logger.remove_all_sinks!
330   Redwood::Logger.add_sink $stderr, false
331   debug "stopped cursing"
332
333   if $die
334     info "I've been ordered to commit seppuku. I obey!"
335   end
336
337   if Redwood::exceptions.empty?
338     debug "no fatal errors. good job, william."
339     Index.save
340   else
341     error "oh crap, an exception"
342   end
343
344   Index.unlock
345 end
346
347 unless Redwood::exceptions.empty?
348   File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
349     Redwood::exceptions.each do |e, name|
350       f.puts "--- #{e.class.name} from thread: #{name}"
351       f.puts e.message, e.backtrace
352     end
353   end
354   $stderr.puts <<EOS
355 ----------------------------------------------------------------
356 I'm very sorry. It seems that an error occurred in Sup. Please
357 accept my sincere apologies. If you don't mind, please send the
358 contents of ~/.sup/exception-log.txt and a brief report of the
359 circumstances to sup-talk at rubyforge dot orgs so that I might
360 address this problem. Thank you!
361
362 Sincerely,
363 William
364 ----------------------------------------------------------------
365 EOS
366   Redwood::exceptions.each do |e, name|
367     puts "--- #{e.class.name} from thread: #{name}"
368     puts e.message, e.backtrace
369   end
370 end
371
372 end