#!/usr/bin/env ruby require 'rubygems' require 'ncurses' require 'curses' require 'fileutils' require 'trollop' require 'fastthread' require "sup" BIN_VERSION = "0.7" 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 end 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 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", 'B' 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' end def start_cursing Ncurses.initscr Ncurses.noecho Ncurses.cbreak Ncurses.stdscr.keypad 1 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.new begin Index.lock rescue Index::LockError => e require 'highline' h = HighLine.new h.wrap_at = :auto h.say Index.fancy_lock_error_message_for(e) case h.ask("Should I ask that process to kill itself? ") when /^\s*y(es)?\s*$/i h.say "Ok, suggesting seppuku..." FileUtils.touch Redwood::SUICIDE_FN sleep SuicideManager::DELAY * 2 FileUtils.rm_f Redwood::SUICIDE_FN h.say "Let's try that again." retry else h.say <<EOS Ok, giving up. If the process crashed and left a stale lockfile, you can fix this by manually deleting #{Index.lockfile}. EOS exit end end begin Redwood::start Index.load if(s = Index.source_for DraftManager.source_name) DraftManager.source = s else Redwood::log "no draft source, auto-adding..." Index.add_source DraftManager.new_source end if(s = Index.source_for SentManager.source_name) SentManager.source = s else Redwood::log "no sent mail source, auto-adding..." Index.add_source SentManager.new_source end HookManager.run "startup" log "starting curses" start_cursing bm = BufferManager.new Colormap.new.populate_colormap log "initializing mail index buffer" imode = InboxMode.new ibuf = bm.spawn "Inbox", imode log "ready for interaction!" Logger.make_buf bm.draw_screen Index.usual_sources.each do |s| next unless s.respond_to? :connect reporting_thread("call #connect on #{s}") do begin s.connect rescue SourceError => e Redwood::log "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 { reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] } if $opts[:compose] ComposeMode.spawn_nicely :to_default => $opts[:compose] end unless $opts[:no_threads] PollManager.start SuicideManager.start Index.start_lock_update_thread end if $opts[:search] SearchResultsMode.spawn_from_query $opts[:search] end until Redwood::exceptions.nonempty? || SuicideManager.die? c = begin Ncurses.nonblocking_getch rescue Exception => e if e.is_a?(Interrupt) raise if BufferManager.ask_yes_or_no("Die ungracefully now?") bm.draw_screen nil end end next unless c 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") { 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.listable_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 :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 SuicideManager.die? rescue Exception => e Redwood::record_exception e, "main" ensure unless $opts[:no_threads] PollManager.stop if PollManager.instantiated? SuicideManager.stop if PollManager.instantiated? Index.stop_lock_update_thread end HookManager.run "shutdown" Redwood::finish stop_cursing Redwood::log "stopped cursing" if SuicideManager.instantiated? && SuicideManager.die? Redwood::log "I've been ordered to commit seppuku. I obey!" end if Redwood::exceptions.empty? Redwood::log "no fatal errors. good job, william." Index.save else Redwood::log "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 ~/.sup/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