require 'trollop'
require "sup"
+$exceptions = []
$opts = Trollop::options do
version "sup v#{Redwood::VERSION}"
banner <<EOS
Index.usual_sources.each do |s|
next unless s.respond_to? :connect
- reporting_thread do
+ reporting_thread("call #connect on #{s}") do
begin
s.connect
rescue SourceError => e
end
end
- imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread { sleep 1; PollManager.poll } unless $opts[:no_threads] }
+ imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] }
unless $opts[:no_threads]
PollManager.start
SearchResultsMode.spawn_from_query $opts[:search]
end
- until $exception || SuicideManager.die?
+ until $exceptions.nonempty? || SuicideManager.die?
c = Ncurses.nonblocking_getch
next unless c
bm.erase_flash
when :list_buffers
bm.spawn_unless_exists("Buffer List") { BufferListMode.new }
when :list_contacts
- b = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
- b.mode.load_in_background if b
+ 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*$/
when :compose
ComposeMode.spawn_nicely
when :poll
- reporting_thread { PollManager.poll }
+ reporting_thread("user-invoked poll") { PollManager.poll }
when :recall_draft
case Index.num_results_for :label => :draft
when 0
BufferManager.spawn "Edit message", r
r.edit_message
else
- b = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
- b.mode.load_threads :num => b.content_height if b
+ b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
+ b.mode.load_threads :num => b.content_height if new
end
when :nothing
when :redraw
bm.draw_screen
end
rescue Exception => e
- $exception ||= e
+ $exceptions << [e, "main"]
ensure
unless $opts[:no_threads]
PollManager.stop if PollManager.instantiated?
Redwood::log "I've been ordered to commit sepuku. I obey!"
end
- case $exception
- when nil
+ if $exceptions.empty?
Redwood::log "no fatal errors. good job, william."
Index.save
else
Index.unlock
end
-if $exception
+unless $exceptions.empty?
File.open("sup-exception-log.txt", "w") do |f|
- f.puts "--- #{e.class.name} at #{Time.now}"
- f.puts e.message, e.backtrace
+ $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, but it seems that an error occurred in Sup.
-Please accept my sincere apologies. If you don't mind, please
-send the backtrace below and a brief report of the circumstances
-to sup-talk at rubyforge dot orgs so that I might address this
-problem. Thank you!
+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
----------------------------------------------------------------
-
-The problem was: '#{$exception.message}' (error type #{$exception.class.name})
-A backtrace follows:
EOS
- raise $exception
+ $exceptions.each do |e, name|
+ puts "--- #{e.class.name} from thread: #{name}"
+ puts e.message, e.backtrace
+ end
end
end
_ mark thread as unread should have a version within thread-view-mode
which then also closes the buffer
_ bugfix: time zone parsing broken?
-_ mailing list auto-subscribe/unsubscribe
_ forwards optionally include attachments
_ attach messages
_ flesh out gpg integration: sign & encrypt outgoing
portion of thread
_ have "notes" (treated as emails to oneself, never sent) as
first-class objects.
+x multi-thread dump upon crash
+x hook manager caches values of any proc "variables"
+x bugfix: remove delay on startup if a usual imap source exists
+x bugfix: broken source handling still needs to be improved
+x speed up querying
+x bugfix: sources sometimes aren't added by sup-add
+x more widgets: terminal title, statusbar
+x mailing list auto-subscribe/unsubscribe
future
------
+_ ldbd support
+_ don't use a people.txt; store email addresses directly in the index. too many
+ problems with email addresses that occur with multiple names.
+_ infix match instead of prefix match for tab completion (maybe!)
_ fix killed threads contributing to unread message count problem (prob. need
to maintain all killed message ids and our own unread message count for
inbox).
end
## record exceptions thrown in threads nicely
- $exception = nil
- def reporting_thread
+ def reporting_thread name
if $opts[:no_threads]
yield
else
begin
yield
rescue Exception => e
- $exception ||= e
+ $exceptions ||= []
+ $exceptions << [e, name]
raise
end
end
end
## not really a good place for this, so I'll just dump it here.
+ ##
+ ## a source error is either a FatalSourceError or an OutOfSyncSourceError.
+ ## the superclass SourceError is just a generic.
def report_broken_sources opts={}
return unless BufferManager.instantiated?
- broken_sources = Index.usual_sources.select { |s| s.error.is_a? FatalSourceError }
- File.open("goat", "w") { |f| f.puts Kernel.caller }
+ broken_sources = Index.sources.select { |s| s.error.is_a? FatalSourceError }
unless broken_sources.empty?
BufferManager.spawn_unless_exists("Broken source notification for #{broken_sources.join(',')}", opts) do
TextMode.new(<<EOM)
end
end
- desynced_sources = Index.usual_sources.select { |s| s.error.is_a? OutOfSyncSourceError }
+ desynced_sources = Index.sources.select { |s| s.error.is_a? OutOfSyncSourceError }
unless desynced_sources.empty?
BufferManager.spawn_unless_exists("Out-of-sync source notification for #{broken_sources.join(',')}", opts) do
TextMode.new(<<EOM)
mode.resize rows, cols
end
- def redraw
- draw if @dirty
- draw_status
+ def redraw status
+ if @dirty
+ draw status
+ else
+ draw_status status
+ end
+
commit
end
@w.noutrefresh
end
- def draw
+ def draw status
@mode.draw
- draw_status
+ draw_status status
commit
end
@w.clear
end
- def draw_status
- write @height - 1, 0, " [#{mode.name}] #{title} #{mode.status}",
- :color => :status_color
+ def draw_status status
+ write @height - 1, 0, status, :color => :status_color
end
def focus
## are canceled by any keypress except this one.
CONTINUE_IN_BUFFER_SEARCH_KEY = "n"
+ HookManager.register "status-bar-text", <<EOS
+Sets the status bar. The default status bar contains the mode name, the buffer
+title, and the mode status. Note that this will be called at least once per
+keystroke, so excessive computation is discouraged.
+
+Variables:
+ num_inbox: number of messages in inbox
+ num_inbox_unread: total number of messages marked as unread
+ num_total: total number of messages in the index
+ num_spam: total number of messages marked as spam
+ title: title of the current buffer
+ mode: current mode name (string)
+ status: current mode status (string)
+Return value: a string to be used as the status bar.
+EOS
+
+ HookManager.register "terminal-title-text", <<EOS
+Sets the title of the current terminal, if applicable. Note that this will be
+called at least once per keystroke, so excessive computation is discouraged.
+
+Variables: the same as status-bar-text hook.
+Return value: a string to be used as the terminal title.
+EOS
+
def initialize
@name_map = {}
@buffers = []
def completely_redraw_screen
return if @shelled
+ status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
+
Ncurses.sync do
@dirty = true
Ncurses.clear
- draw_screen :sync => false
+ draw_screen :sync => false, :status => status, :title => title
end
end
def draw_screen opts={}
return if @shelled
+ status, title =
+ if opts.member? :status
+ [opts[:status], opts[:title]]
+ else
+ get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
+ end
+
+ print "\033]2;#{title}\07" if title
+
Ncurses.mutex.lock unless opts[:sync] == false
## disabling this for the time being, to help with debugging
false && @buffers.inject(@dirty) do |dirty, buf|
buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
#dirty ? buf.draw : buf.redraw
- buf.draw
+ buf.draw status
dirty
end
if true
buf = @buffers.last
buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
- @dirty ? buf.draw : buf.redraw
+ @dirty ? buf.draw(status) : buf.redraw(status)
end
draw_minibuf :sync => false unless opts[:skip_minibuf]
Ncurses.mutex.unlock unless opts[:sync] == false
end
- ## gets the mode from the block, which is only called if the buffer
- ## doesn't already exist. this is useful in the case that generating
- ## the mode is expensive, as it often is.
+ ## if the named buffer already exists, pops it to the front without
+ ## calling the block. otherwise, gets the mode from the block and
+ ## creates a new buffer. returns two things: the buffer, and a boolean
+ ## indicating whether it's a new buffer or not.
def spawn_unless_exists title, opts={}
- if @name_map.member? title
- raise_to_front @name_map[title] unless opts[:hidden]
- nil
- else
- mode = yield
- spawn title, mode, opts
- @name_map[title]
- end
+ new =
+ if @name_map.member? title
+ raise_to_front @name_map[title] unless opts[:hidden]
+ false
+ else
+ mode = yield
+ spawn title, mode, opts
+ true
+ end
+ [@name_map[title], new]
end
def spawn title, mode, opts={}
end
private
+ def default_status_bar buf
+ " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
+ end
+
+ def default_terminal_title buf
+ "Sup #{Redwood::VERSION} :: #{buf.title}"
+ end
+
+ def get_status_and_title buf
+ opts = {
+ :num_inbox => lambda { Index.num_results_for :label => :inbox },
+ :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
+ :num_total => lambda { Index.size },
+ :num_spam => lambda { Index.num_results_for :label => :spam },
+ :title => buf.title,
+ :mode => buf.mode.name,
+ :status => buf.mode.status
+ }
+
+ statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
+ term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
+
+ [statusbar_text, term_title_text]
+ end
def users
unless @users
##
## i don't bother providing setters, since i'm pretty sure the
## charade will fall apart pretty quickly with respect to scoping.
- ## this is basically fail-fast.
+ ## "fail-fast", we'll call it.
class HookContext
def initialize name
@__say_id = nil
def method_missing m, *a
case @__locals[m]
when Proc
- @__locals[m].call(*a)
+ @__locals[m] = @__locals[m].call(*a) # only call the proc once
when nil
super
else
rescue Exception => e
log "error running hook: #{e.message}"
log e.backtrace.join("\n")
- BufferManager.flash "Error running hook: #{e.message}"
@hooks[name] = nil # disable it
+ BufferManager.flash "Error running hook: #{e.message}"
end
context.__cleanup
result
end
end
+ def enabled? name; !hook_for(name).nil? end
+
private
def hook_for name
require 'chronic'
$have_chronic = true
rescue LoadError => e
- Redwood::log "'chronic' library not found. run 'gem install chronic' to install."
+ Redwood::log "optional 'chronic' library not found (run 'gem install chronic' to install)"
$have_chronic = false
end
end
def start_lock_update_thread
- @lock_update_thread = Redwood::reporting_thread do
+ @lock_update_thread = Redwood::reporting_thread("lock update") do
while true
sleep 30
@lock.touch_yourself
def add_source source
raise "duplicate source!" if @sources.include? source
@sources_dirty = true
- source.id ||= @sources.size
- ##TODO: why was this necessary?
+ max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id }
+ source.id ||= (max || 0) + 1
##source.id += 1 while @sources.member? source.id
@sources[source.id] = source
end
end
until pending.empty? || (opts[:limit] && messages.size >= opts[:limit])
- id = pending.pop
- next if searched.member? id
- searched[id] = true
q = Ferret::Search::BooleanQuery.new true
- q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
- q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
+
+ pending.each do |id|
+ searched[id] = true
+ q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
+ q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
+ end
+ pending = []
q = build_query :qobj => q
#Redwood::log "got #{mid} as a child of #{id}"
messages[mid] ||= lambda { build_message docid }
refs = @index[docid][:refs].split(" ")
- pending += refs
+ pending += refs.select { |id| !searched[id] }
end
end
end
+
if killed
Redwood::log "thread for #{m.id} is killed, ignoring"
false
## i would like, for example, to be able to add in a ruby-talk
## specific module that would detect and link to /ruby-talk:\d+/
## sequences in the text of an email. (how sweet would that be?)
+##
+## this class cathces all source exceptions. if the underlying source throws
+## an error, it is caught and handled.
+
class Message
SNIPPET_LEN = 80
RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i
Redwood::log "problem getting messages from #{@source}: #{e.message}"
## we need force_to_top here otherwise this window will cover
## up the error message one
+ @source.error ||= e
Redwood::report_broken_sources :force_to_top => true
[Chunk::Text.new(error_message(e.message))]
end
EOS
end
+ ## wrap any source methods that might throw sourceerrors
def with_source_errors_handled
begin
yield
rescue SourceError => e
Redwood::log "problem getting messages from #{@source}: #{e.message}"
+ @source.error ||= e
+ Redwood::report_broken_sources :force_to_top => true
error_message e.message
end
end
end
def load_in_background
- Redwood::reporting_thread do
+ Redwood::reporting_thread("contact manager load in bg") do
load
update
BufferManager.draw_screen
NON_EDITABLE_HEADERS = %w(Message-Id Date)
HookManager.register "signature", <<EOS
-Generates a signature for a message.
+Generates a message signature.
Variables:
header: an object that supports string-to-string hashtable-style access
to the raw headers for the message. E.g., header["From"],
when :inbox
BufferManager.raise_to_front InboxMode.instance.buffer
else
- b = BufferManager.spawn_unless_exists("All threads with label '#{label}'") { LabelSearchResultsMode.new [label] }
- b.mode.load_threads :num => b.content_height if b
+ b, new = BufferManager.spawn_unless_exists("All threads with label '#{label}'") { LabelSearchResultsMode.new [label] }
+ b.mode.load_threads :num => b.content_height if new
end
end
end
@twiddles = opts.member?(:twiddles) ? opts[:twiddles] : true
@search_query = nil
@search_line = nil
+ @status = ""
super()
end
def lines; @text.length; end
def [] i; @text[i]; end
- #def contains_thread? t; !@lines[t].nil?; end
def contains_thread? t; @threads.include?(t) end
def reload
def select t=nil
t ||= cursor_thread or return
- ## TODO: don't regen text completely
- Redwood::reporting_thread do
+ Redwood::reporting_thread("load messages for thread-view-mode") do
num = t.size
message = "Loading #{num.pluralize 'message body'}..."
BufferManager.say(message) do |sid|
def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
return if @load_thread # todo: wrap in mutex
- @load_thread = Redwood::reporting_thread do
+ @load_thread = Redwood::reporting_thread("load threads for thread-index-mode") do
num = load_n_threads n, opts
opts[:when_done].call(num) if opts[:when_done]
@load_thread = nil
def unsubscribe_from_list
m = @message_lines[curpos] or return
if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
- spawn_compose_mode :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
+ ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
else
BufferManager.flash "Can't find List-Unsubscribe header for this message."
end
def forward
m = @message_lines[curpos] or return
- spawn_forward_mode m
+ ForwardMode.spawn_nicely m
end
include CanAliasContacts
def compose
p = @person_lines[curpos]
if p
- spawn_compose_mode :to => [p]
+ ComposeMode.spawn_nicely :to => [p]
else
- spawn_compose_mode
+ ComposeMode.spawn_nicely
end
end
end
def buffer
- @buffer ||= BufferManager.spawn_unless_exists("<poll for new messages>", :hidden => true) { PollMode.new }
+ b, new = BufferManager.spawn_unless_exists("<poll for new messages>", :hidden => true) { PollMode.new }
+ b
end
def poll
end
def start
- @thread = Redwood::reporting_thread do
+ @thread = Redwood::reporting_thread("periodic poll") do
while true
sleep DELAY / 2
poll if @last_poll.nil? || (Time.now - @last_poll) >= DELAY
module Redwood
-class SourceError < StandardError; end
+class SourceError < StandardError
+ def initialize *a
+ raise "don't instantiate me!" if SourceError.is_a?(self.class)
+ super
+ end
+end
class OutOfSyncSourceError < SourceError; end
class FatalSourceError < SourceError; end
bool_reader :die
def start
- @thread = Redwood::reporting_thread do
+ @thread = Redwood::reporting_thread("suicide watch") do
while true
sleep DELAY
if File.exists? @fn
def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
def last= e; self[-1] = e end
+ def nonempty?; !empty? end
end
class Time
end
end
-## wraps an object. if it throws an exception, keeps a copy, and
-## rethrows it for any further method calls.
+## wraps an object. if it throws an exception, keeps a copy.
class Recoverable
def initialize o
@o = o
- @e = nil
+ @error = nil
@mutex = Mutex.new
end
- def clear_error!; @e = nil; end
- def has_errors?; !@e.nil?; end
- def error; @e; end
+ attr_accessor :error
+
+ def clear_error!; @error = nil; end
+ def has_errors?; !@error.nil?; end
def method_missing m, *a, &b; __pass m, *a, &b end
begin
@o.send(m, *a, &b)
rescue Exception => e
- @e ||= e
- raise e
+ @error ||= e
+ raise
end
end
end