#!/usr/bin/env ruby require 'rubygems' no_ncursesw = false begin require 'ncursesw' rescue LoadError require 'ncurses' no_ncursesw = true end require 'fileutils' require 'trollop' require "sup"; Redwood::check_library_version_against "0.10" if no_ncursesw debug "No 'ncursesw' gem detected. Install it for wide character support." 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" opt :index, "Use this index type ('auto' for autodetect)", :default => "auto" 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 :poll_unusual, "Poll for new messages from unusual sources", '{' 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 $opts[:index] Index.lock_interactively or exit if Index.is_a_deprecated_ferret_index? FERRET_DEPRECATION_WARNING_FN = File.join BASE_DIR, "you-have-been-warned-about-ferret-deprecation" unless File.exist? FERRET_DEPRECATION_WARNING_FN $stderr.puts <<EOS Warning! Your 30-day trial period for using Sup is almost over! To purchase the full version of Sup, please see http://sup.rubyforge.org/. Just kidding. BUT! You are using an old Ferret index. The Ferret backend is deprecated and support will be removed in the next version of Sup. You should convert to Xapian before that happens. The conversion process may take several hours. It is safe and interruptable. You can start it at any point by typing: sup-convert-ferret-index Press enter to continue and be on your way. You won't see this message again, just a brief reminder at shutdown. EOS $stdin.gets FileUtils.touch FERRET_DEPRECATION_WARNING_FN end end begin Redwood::start Index.load Index.start_sync_worker unless $opts[:no_threads] $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 :poll_unusual if BufferManager.ask_yes_or_no "Really poll unusual sources?" reporting_thread("user-invoked unusual poll") { PollManager.poll_unusual } end 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" Index.stop_sync_worker 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. Please submit the contents of #{BASE_DIR}/exception-log.txt and a brief report of the circumstances to http://masanjin.net/sup-bugs/ 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 if Index.is_a_deprecated_ferret_index? puts "Reminder: to update your Ferret index to Xapian, run sup-convert-ferret-index." end end