#!/usr/bin/env ruby require 'rubygems' require 'ncurses' require 'curses' require 'fileutils' require 'trollop' require "sup" BIN_VERSION = "0.9.1" unless Redwood::VERSION == BIN_VERSION $stderr.puts <<EOS Error: version mismatch! The sup executable is at version #{BIN_VERSION.inspect}. The sup libraries are at version #{Redwood::VERSION.inspect}. Is your development environment conflicting with rubygems? EOS exit(-1) end $opts = Trollop::options do version "sup v#{Redwood::VERSION}" banner <<EOS Sup is a curses-based email client. Usage: sup [options] Options are: EOS opt :list_hooks, "List all hooks and descriptions, and quit." opt :no_threads, "Turn off threading. Helps with debugging. (Necessarily disables background polling for new messages.)" opt :no_initial_poll, "Don't poll for new messages when starting." opt :search, "Search for this query upon startup", :type => String opt :compose, "Compose message to this recipient upon startup", :type => String opt :subject, "When composing, use this subject", :type => String, :short => "j" end Trollop::die :subject, "requires --compose" if $opts[:subject] && !$opts[:compose] Redwood::HookManager.register "startup", <<EOS Executes at startup No variables. No return value. EOS Redwood::HookManager.register "shutdown", <<EOS Executes when sup is shutting down. May be run when sup is crashing, so don\'t do anything too important. Run before the label, contacts, and people are saved. No variables. No return value. EOS if $opts[:list_hooks] Redwood::HookManager.print_hooks exit end Thread.abort_on_exception = true # make debugging possible Thread.current.priority = 1 # keep ui responsive module Redwood global_keymap = Keymap.new do |k| k.add :quit_ask, "Quit Sup, but ask first", 'q' k.add :quit_now, "Quit Sup immediately", 'Q' k.add :help, "Show help", '?' k.add :roll_buffers, "Switch to next buffer", 'b' k.add :roll_buffers_backwards, "Switch to previous buffer", 'B' k.add :kill_buffer, "Kill the current buffer", 'x' k.add :list_buffers, "List all buffers", ';' k.add :list_contacts, "List contacts", 'C' k.add :redraw, "Redraw screen", :ctrl_l k.add :search, "Search all messages", '\\', 'F' k.add :search_unread, "Show all unread messages", 'U' k.add :list_labels, "List labels", 'L' k.add :poll, "Poll for new messages", 'P' k.add :compose, "Compose new message", 'm', 'c' k.add :nothing, "Do nothing", :ctrl_g k.add :recall_draft, "Edit most recent draft message", 'R' k.add :show_inbox, "Show the Inbox buffer", 'I' k.add :show_console, "Show the Console buffer", '~' end ## the following magic enables wide characters when used with a ruby ## ncurses.so that's been compiled against libncursesw. (note the w.) why ## this works, i have no idea. much like pretty much every aspect of ## dealing with curses. cargo cult programming at its best. ## ## BSD users: if libc.so.6 is not found, try installing compat6x. require 'dl/import' module LibC extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable setlocale_lib = case Config::CONFIG['arch'] when /darwin/; "libc.dylib" when /cygwin/; "cygwin1.dll" else; "libc.so.6" end debug "dynamically loading setlocale() from #{setlocale_lib}" begin dlload setlocale_lib extern "void setlocale(int, const char *)" debug "setting locale..." LibC.setlocale(6, "") # LC_ALL == 6 rescue RuntimeError => e warn "cannot dlload setlocale(); ncurses wide character support probably broken." warn "dlload error was #{e.class}: #{e.message}" if Config::CONFIG['arch'] =~ /bsd/ warn "BSD variant detected. You may have to install a compat6x package to acquire libc." end end end def start_cursing Ncurses.initscr Ncurses.noecho Ncurses.cbreak Ncurses.stdscr.keypad 1 Ncurses.use_default_colors Ncurses.curs_set 0 Ncurses.start_color $cursing = true end def stop_cursing return unless $cursing Ncurses.curs_set 1 Ncurses.echo Ncurses.endwin end module_function :start_cursing, :stop_cursing Index.init Index.lock_interactively or exit begin Redwood::start Index.load $die = false trap("TERM") { |x| $die = true } trap("WINCH") { |x| BufferManager.sigwinch_happened! } if(s = Redwood::SourceManager.source_for DraftManager.source_name) DraftManager.source = s else debug "no draft source, auto-adding..." Redwood::SourceManager.add_source DraftManager.new_source end if(s = Redwood::SourceManager.source_for SentManager.source_uri) SentManager.source = s else Redwood::SourceManager.add_source SentManager.default_source end HookManager.run "startup" debug "starting curses" Redwood::Logger.remove_sink $stderr start_cursing bm = BufferManager.init Colormap.new.populate_colormap debug "initializing log buffer" lmode = Redwood::LogMode.new "system log" lmode.on_kill { Logger.clear! } Logger.add_sink lmode Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}." if Logger::LEVELS.index(Logger.level) > 0 Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}." end debug "initializing inbox buffer" imode = InboxMode.new ibuf = bm.spawn "Inbox", imode debug "ready for interaction!" bm.draw_screen Redwood::SourceManager.usual_sources.each do |s| next unless s.respond_to? :connect reporting_thread("call #connect on #{s}") do begin s.connect rescue SourceError => e error "fatal error loading from #{s}: #{e.message}" end end end unless $opts[:no_initial_poll] 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] } if $opts[:compose] to = Person.from_address_list $opts[:compose] mode = ComposeMode.new :to => to, :subj => $opts[:subject] BufferManager.spawn "New Message", mode mode.edit_message end unless $opts[:no_threads] PollManager.start Index.start_lock_update_thread end if $opts[:search] SearchResultsMode.spawn_from_query $opts[:search] end until Redwood::exceptions.nonempty? || $die c = begin Ncurses.nonblocking_getch rescue Interrupt raise if BufferManager.ask_yes_or_no "Die ungracefully now?" BufferManager.draw_screen nil end if c.nil? if BufferManager.sigwinch_happened? debug "redrawing screen on sigwinch" BufferManager.completely_redraw_screen end next end if c == 410 ## this is ncurses's way of telling us it's detected a refresh. ## since we have our own sigwinch handler, we don't do anything. next end bm.erase_flash action = begin if bm.handle_input c :nothing else bm.resolve_input_with_keymap c, global_keymap end rescue InputSequenceAborted :nothing end case action when :quit_now break if bm.kill_all_buffers_safely when :quit_ask if bm.ask_yes_or_no "Really quit?" break if bm.kill_all_buffers_safely end when :help curmode = bm.focus_buf.mode bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap } when :roll_buffers bm.roll_buffers when :roll_buffers_backwards bm.roll_buffers_backwards when :kill_buffer bm.kill_buffer_safely bm.focus_buf when :list_buffers bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new } when :list_contacts b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new } b.mode.load_in_background if new when :search query = BufferManager.ask :search, "search all messages: " next unless query && query !~ /^\s*$/ SearchResultsMode.spawn_from_query query when :search_unread SearchResultsMode.spawn_from_query "is:unread" when :list_labels labels = LabelManager.all_labels.map { |l| LabelManager.string_for l } user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels unless user_label.nil? if user_label.empty? bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty? else LabelSearchResultsMode.spawn_nicely user_label end end when :compose ComposeMode.spawn_nicely when :poll reporting_thread("user-invoked poll") { PollManager.poll } when :recall_draft case Index.num_results_for :label => :draft when 0 bm.flash "No draft messages." when 1 m = nil Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call } r = ResumeMode.new(m) BufferManager.spawn "Edit message", r r.edit_message else b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] } b.mode.load_threads :num => b.content_height if new end when :show_inbox BufferManager.raise_to_front ibuf when :show_console b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new } b.mode.run when :nothing, InputSequenceAborted when :redraw bm.completely_redraw_screen else bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}." end bm.draw_screen end bm.kill_all_buffers if $die rescue Exception => e Redwood::record_exception e, "main" ensure unless $opts[:no_threads] PollManager.stop if PollManager.instantiated? Index.stop_lock_update_thread end HookManager.run "shutdown" Redwood::finish stop_cursing Redwood::Logger.remove_all_sinks! Redwood::Logger.add_sink $stderr, false debug "stopped cursing" if $die info "I've been ordered to commit seppuku. I obey!" end if Redwood::exceptions.empty? debug "no fatal errors. good job, william." Index.save else error "oh crap, an exception" end Index.unlock end unless Redwood::exceptions.empty? File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f| Redwood::exceptions.each do |e, name| f.puts "--- #{e.class.name} from thread: #{name}" f.puts e.message, e.backtrace end end $stderr.puts <<EOS ---------------------------------------------------------------- I'm very sorry. It seems that an error occurred in Sup. Please accept my sincere apologies. If you don't mind, please send the contents of #{BASE_DIR}/exception-log.txt and a brief report of the circumstances to sup-talk at rubyforge dot orgs so that I might address this problem. Thank you! Sincerely, William ---------------------------------------------------------------- EOS Redwood::exceptions.each do |e, name| puts "--- #{e.class.name} from thread: #{name}" puts e.message, e.backtrace end end end