lib/sup/keymap.rb
lib/sup/label.rb
lib/sup/logger.rb
+lib/sup/interactive-lock.rb
lib/sup/maildir.rb
lib/sup/mbox.rb
lib/sup/mbox/loader.rb
lib/sup/rfc2047.rb
lib/sup/sent.rb
lib/sup/source.rb
-lib/sup/suicide.rb
lib/sup/tagger.rb
lib/sup/textfield.rb
lib/sup/thread.rb
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
+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
unless $opts[:no_threads]
PollManager.start
- SuicideManager.start
Index.start_lock_update_thread
end
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
+ until Redwood::exceptions.nonempty? || $die
+ c = begin
+ Ncurses.nonblocking_getch
+ rescue Interrupt => e
+ raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
+ BufferManager.draw_screen
+ nil
+ end
+
+ if c.nil?
+ if BufferManager.sigwinch_happened?
+ Redwood::log "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 =
bm.draw_screen
end
- bm.kill_all_buffers if SuicideManager.die?
+ bm.kill_all_buffers if $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
stop_cursing
Redwood::log "stopped cursing"
- if SuicideManager.instantiated? && SuicideManager.die?
+ if $die
Redwood::log "I've been ordered to commit seppuku. I obey!"
end
Redwood::start
index = Redwood::Index.new
-index.lock_or_die
+index.lock_interactively or exit
begin
Redwood::SourceManager.load_sources
end
seen = {}
-index.lock_or_die
+index.lock_interactively or exit
begin
index.load
Redwood::start
index = Redwood::Index.new
-index.lock_or_die
+index.lock_interactively or exit
deleted_fp, spam_fp = nil
unless opts[:dry_run]
remove_labels = (opts[:remove] || "").split(",").map { |l| l.intern }.uniq
Trollop::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
+Trollop::die "no sources specified" if ARGV.empty?
Redwood::start
+index = Redwood::Index.new
+index.lock_interactively or exit
begin
- index = Redwood::Index.new
index.load
source_ids =
Redwood::DraftManager.new Redwood::DRAFT_DIR
Redwood::UpdateManager.new
Redwood::PollManager.new
- Redwood::SuicideManager.new Redwood::SUICIDE_FN
Redwood::CryptoManager.new
Redwood::UndoManager.new
Redwood::SourceManager.new
## now everything else (which can feel free to call Redwood::log at load time)
require "sup/update"
-require "sup/suicide"
require "sup/message-chunks"
require "sup/message"
require "sup/source"
require "sup/person"
require "sup/account"
require "sup/thread"
+require "sup/interactive-lock"
require "sup/index"
require "sup/textfield"
require "sup/colormap"
## magically, this stuff seems to work now. i could swear it didn't
## before. hm.
def nonblocking_getch
- if IO.select([$stdin], nil, nil, 1)
- Ncurses.getch
- else
- nil
+ ## INSANTIY
+ ## it is NECESSARY to wrap Ncurses.getch in a select() otherwise all
+ ## background threads will be BLOCKED. (except in very modern versions
+ ## of libncurses-ruby. the current one on ubuntu seems to work well.)
+ if IO.select([$stdin], nil, nil, 0.5)
+ c = Ncurses.getch
end
end
@flash = nil
@shelled = @asking = false
@in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
+ @sigwinch_happened = false
+ @sigwinch_mutex = Mutex.new
self.class.i_am_the_instance self
end
+ def sigwinch_happened!; @sigwinch_mutex.synchronize { @sigwinch_happened = true } end
+ def sigwinch_happened?; @sigwinch_mutex.synchronize { @sigwinch_happened } end
+
def buffers; @name_map.to_a; end
def focus_on buf
def completely_redraw_screen
return if @shelled
+ ## this magic makes Ncurses get the new size of the screen
+ Ncurses.endwin
+ Ncurses.refresh
+ @sigwinch_mutex.synchronize { @sigwinch_happened = false }
+ Redwood::log "new screen size is #{Ncurses.rows} x #{Ncurses.cols}"
+
status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
Ncurses.sync do
class FerretIndex < BaseIndex
+ HookManager.register "custom-search", <<EOS
+Executes before a string search is applied to the index,
+returning a new search string.
+Variables:
+ subs: The string being searched. Be careful about shadowing:
+ this variable is actually a method, so use a temporary variable
+ or explicitly call self.subs; the substitutions in index.rb
+ don't actually work.
+EOS
+
def initialize dir=BASE_DIR
super
def parse_query s
query = {}
- subs = s.gsub(/\b(to|from):(\S+)\b/) do
+ subs = HookManager.run("custom-search", :subs => s) || s
+
+ subs = subs.gsub(/\b(to|from):(\S+)\b/) do
field, name = $1, $2
if(p = ContactManager.contact_for(name))
[field, p.email]
attr_writer :__locals
+ ## an annoying gotcha here is that if you try something
+ ## like var = var.foo(), var will magically get allocated
+ ## to Nil and method_missing will never get called. You
+ ## can work around this by calling self.var or simply
+ ## not assigning it to itself.
def method_missing m, *a
case @__locals[m]
when Proc
module Redwood
class BaseIndex
+ include InteractiveLock
+
class LockError < StandardError
def initialize h
@h = h
@lock_update_thread = nil
end
- def possibly_pluralize number_of, kind
- "#{number_of} #{kind}" +
- if number_of == 1 then "" else "s" end
- end
-
- def fancy_lock_error_message_for e
- secs = (Time.now - e.mtime).to_i
- mins = secs / 60
- time =
- if mins == 0
- possibly_pluralize secs , "second"
- else
- possibly_pluralize mins, "minute"
- end
-
- <<EOS
-Error: the sup index is locked by another process! User '#{e.user}' on
-host '#{e.host}' is running #{e.pname} with pid #{e.pid}. The process was alive
-as of #{time} ago.
-EOS
- end
-
- def lock_or_die
- begin
- lock
- rescue LockError => e
- $stderr.puts fancy_lock_error_message_for(e)
- $stderr.puts <<EOS
-
-You can wait for the process to finish, or, if it crashed and left a
-stale lock file behind, you can manually delete #{@lock.path}.
-EOS
- exit
- end
- end
-
def unlock
if @lock && @lock.locked?
Redwood::log "unlocking #{lockfile}..."
--- /dev/null
+require 'fileutils'
+
+module Redwood
+
+## wrap a nice interactive layer on top of anything that has a #lock method
+## which throws a LockError which responds to #user, #host, #mtim, #pname, and
+## #pid.
+
+module InteractiveLock
+ def pluralize number_of, kind; "#{number_of} #{kind}" + (number_of == 1 ? "" : "s") end
+
+ def time_ago_in_words time
+ secs = (Time.now - time).to_i
+ mins = secs / 60
+ time = if mins == 0
+ pluralize secs, "second"
+ else
+ pluralize mins, "minute"
+ end
+ end
+
+ DELAY = 5 # seconds
+
+ def lock_interactively stream=$stderr
+ begin
+ Index.lock
+ rescue Index::LockError => e
+ stream.puts <<EOS
+Error: the index is locked by another process! User '#{e.user}' on
+host '#{e.host}' is running #{e.pname} with pid #{e.pid}.
+The process was alive as of at least #{time_ago_in_words e.mtime} ago.
+
+EOS
+ stream.print "Should I ask that process to kill itself (y/n)? "
+ stream.flush
+
+ success = if $stdin.gets =~ /^\s*y(es)?\s*$/i
+ stream.puts "Ok, trying to kill process..."
+
+ begin
+ Process.kill "TERM", e.pid.to_i
+ sleep DELAY
+ rescue Errno::ESRCH # no such process
+ stream.puts "Hm, I couldn't kill it."
+ end
+
+ stream.puts "Let's try that again."
+ begin
+ Index.lock
+ rescue Index::LockError => e
+ stream.puts "I couldn't lock the index. The lockfile might just be stale."
+ stream.print "Should I just remove it and continue? (y/n) "
+ stream.flush
+
+ if $stdin.gets =~ /^\s*y(es)?\s*$/i
+ FileUtils.rm e.path
+
+ stream.puts "Let's try that one more time."
+ begin
+ Index.lock
+ true
+ rescue Index::LockError => e
+ end
+ end
+ end
+ end
+
+ stream.puts "Sorry, couldn't unlock the index." unless success
+ success
+ end
+ end
+end
+
+end
class EnclosedMessage
attr_reader :lines
- def initialize from, body
- @from = from
- @lines = body.split "\n"
- end
+ def initialize from, to, cc, date, subj
+ @from = from ? "unknown sender" : from.full_adress
+ @to = to ? "" : to.map { |p| p.full_address }.join(", ")
+ @cc = cc ? "" : cc.map { |p| p.full_address }.join(", ")
+ if date
+ @date = date.rfc822
+ else
+ @date = ""
+ end
- def from
- @from ? @from.longname : "unknown sender"
+ @subj = subj
+
+ @lines = "\nFrom: #{from}\n"
+ @lines += "To: #{to}\n"
+ if !cc.empty?
+ @lines += "Cc: #{cc}\n"
+ end
+ @lines += "Date: #{date}\n"
+ @lines += "Subject: #{subj}\n\n"
end
def inlineable?; false end
def viewable?; false end
def patina_color; :generic_notice_patina_color end
- def patina_text; "Begin enclosed message from #{from} (#{@lines.length} lines)" end
+ def patina_text; "Begin enclosed message sent on #{@date}" end
def color; :quote_color end
end
chunks
elsif m.header.content_type == "message/rfc822"
payload = RMail::Parser.read(m.body)
- from = payload.header.from.first
- from_person = from ? Person.from_address(from.format) : nil
- [Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
- message_to_chunks(payload, encrypted)
+ from = payload.header.from.first ? payload.header.from.first.format : ""
+ to = payload.header.to.map { |p| p.format }.join(", ")
+ cc = payload.header.cc.map { |p| p.format }.join(", ")
+ subj = payload.header.subject
+ subj = subj ? Message.normalize_subj(payload.header.subject.gsub(/\s+/, " ").gsub(/\s+$/, "")) : subj
+ if Rfc2047.is_encoded? subj
+ subj = Rfc2047.decode_to $encoding, subj
+ end
+ msgdate = payload.header.date
+ from_person = from ? Person.from_address(from) : nil
+ to_people = to ? Person.from_address_list(to) : nil
+ cc_people = cc ? Person.from_address_list(cc) : nil
+ [Chunk::EnclosedMessage.new(from_person, to_people, cc_people, msgdate, subj)] + message_to_chunks(payload, encrypted)
else
filename =
## first, paw through the headers looking for a filename
+++ /dev/null
-module Redwood
-
-class SuicideManager
- include Singleton
-
- DELAY = 5
-
- def initialize fn
- @fn = fn
- @die = false
- @thread = nil
- self.class.i_am_the_instance self
- FileUtils.rm_f @fn
- end
-
- bool_reader :die
-
- def start
- @thread = Redwood::reporting_thread("suicide watch") do
- while true
- sleep DELAY
- if File.exists? @fn
- FileUtils.rm_f @fn
- @die = true
- end
- end
- end
- end
-
- def stop
- @thread.kill if @thread
- @thread = nil
- end
-end
-
-end
def lockinfo_on_disk
h = load_lock_id IO.read(path)
h['mtime'] = File.mtime path
+ h['path'] = path
h
end