#!/usr/bin/env ruby # encoding: utf-8 $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) require 'rubygems' require 'ncursesw' no_gpgme = false begin require 'gpgme' rescue LoadError no_gpgme = true end require 'fileutils' require 'trollop' require "sup" if ENV['SUP_PROFILE'] require 'ruby-prof' RubyProf.start end if no_gpgme info "No 'gpgme' gem detected. Install it for email encryption, decryption and signatures." 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.start 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 :clear_hooks, "Clear all hooks", 'H' k.add :show_console, "Show the Console buffer", '~' ## Submap for less often used keybindings k.add_multi "reload (c)olors, rerun (k)eybindings hook", 'O' do |kk| kk.add :reload_colors, "Reload colors", 'c' kk.add :run_keybindings_hook, "Rerun keybindings hook", 'k' end 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. require 'dl/import' require 'rbconfig' module LibC extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable setlocale_lib = case RbConfig::CONFIG['arch'] when /darwin/; "libc.dylib" when /cygwin/; "cygwin1.dll" when /freebsd/; "libc.so.7" 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}" 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 Index.start_sync_worker unless $opts[:no_threads] $die = false trap("TERM") { |x| $die = true } trap("WINCH") do |x| ::Thread.new do BufferManager.sigwinch_happened! end end 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" Redwood::Keymap.run_hook global_keymap 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 IdleManager.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 IdleManager.ping 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 completions = LabelManager.all_labels.map { |l| "label:#{LabelManager.string_for l}" } completions = completions.each { |l| l.fix_encoding } completions += Index::COMPL_PREFIXES query = BufferManager.ask_many_with_completions :search, "Search all messages (enter for saved searches): ", completions unless query.nil? if query.empty? bm.spawn_unless_exists("Saved searches") { SearchListMode.new } else SearchResultsMode.spawn_from_query query end end when :search_unread SearchResultsMode.spawn_from_query "is:unread" when :list_labels labels = LabelManager.all_labels.map { |l| LabelManager.string_for l } labels = labels.each { |l| l.fix_encoding } 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 :clear_hooks HookManager.clear when :show_console b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new } b.mode.run when :reload_colors Colormap.reset Colormap.populate_colormap bm.completely_redraw_screen bm.flash "reloaded colors" when :run_keybindings_hook HookManager.clear_one 'keybindings' Keymap.run_hook global_keymap bm.flash "keybindings hook 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? IdleManager.stop if IdleManager.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 if (fn = ENV['SUP_PROFILE']) result = RubyProf.stop File.open(fn, 'w') { |io| RubyProf::CallTreePrinter.new(result).print(io) } end 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 ---------------------------------------------------------------- We are very sorry. It seems that an error occurred in Sup. Please accept our sincere apologies. Please submit the contents of #{BASE_DIR}/exception-log.txt and a brief report of the circumstances to https://github.com/sup-heliotrope/sup/issues so that we might address this problem. Thank you! Sincerely, The Sup Developers ---------------------------------------------------------------- EOS Redwood::exceptions.each do |e, name| puts "--- #{e.class.name} from thread: #{name}" puts e.message, e.backtrace end end end