sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit af218d4236c162f69fd5d816204aba4aaeae2ab2
parent 5357ad14812b08b2cff8fb345fb96faae4644cdc
Author: wmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Date:   Sun, 19 Aug 2007 00:45:50 +0000

hook system

git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@528 5c8cc53c-5e98-4d25-b20a-d8db53a31250

Diffstat:
M Manifest.txt | 1 +
M bin/sup | 7 ++++++-
M doc/TODO | 12 +++++++-----
M lib/sup.rb | 8 ++++++++
M lib/sup/buffer.rb | 2 ++
A lib/sup/hook.rb | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M lib/sup/modes/thread-index-mode.rb | 4 ++--
M lib/sup/poll.rb | 26 ++++++++++++++++++++++++--
M lib/sup/util.rb | 16 +---------------
M www/index.html | 4 ++++
10 files changed, 166 insertions(+), 25 deletions(-)
diff --git a/Manifest.txt b/Manifest.txt
@@ -21,6 +21,7 @@ lib/sup/buffer.rb
 lib/sup/colormap.rb
 lib/sup/contact.rb
 lib/sup/draft.rb
+lib/sup/hook.rb
 lib/sup/imap.rb
 lib/sup/index.rb
 lib/sup/keymap.rb
diff --git a/bin/sup b/bin/sup
@@ -16,9 +16,15 @@ Usage:
 
 Options are:
 EOS
+  opt :list_hooks, "List all hooks and descriptions thereof, and quit."
   opt :no_threads, "Turn of threading. Helps with debugging. (Necessarily disables background polling for new messages.)"
 end
 
+if $opts[:list_hooks]
+  Redwood::HookManager.print_hooks
+  exit
+end
+
 Thread.abort_on_exception = true # make debugging possible
 
 module Redwood
@@ -241,7 +247,6 @@ begin
         bm.spawn "New Message", mode
         mode.edit_message
       when :poll
-        #          bm.raise_to_front PollManager.buffer
         reporting_thread { PollManager.poll }
       when :recall_draft
         case Index.num_results_for :label => :draft
diff --git a/doc/TODO b/doc/TODO
@@ -1,15 +1,14 @@
 for next release
 ----------------
+_ imap "add all folders on this server" option in sup-add
 _ mailing list subscribe/unsubscribe
 _ BufferManager#ask_for_labels opens up label-list-mode if empty
 _ tab completion for mid-text cursors
+_ ncurses text entry horizontal scrolling
 _ forward attachments
 _ messages as attachments
-_ individual labeling in thread-view-mode
-_ tab completion for to: and cc: in compose-mode
 _ use trac or something. this file is getting a little silly.
 _ gpg integration
-_ user-defined hooks
 _ saved searches
 _ bugfix: missing sources should be handled better
 _ bugfix: screwing with the headers when editing causes a crash
@@ -20,10 +19,10 @@ _ bugfix: need a better way to force an address to a particular name,
    for things like evite addresses
 _ bugfix: ferret flakiness: just added message but can't find it (? still relevant ?)
 _ for new message flashes, add new message counts until keypress
-_ bugfix: deadlock (on rubyforge)
+_ bugfix: deadlock (on rubyforge) (? still valid ?)
 _ bugfix: ferret corrupt index problem at index.c:901. see http://ferret.davebalmain.com/trac/ticket/279
 _ bugfix: read before thread-index has finished loading then hides the
-   thread?!? wtf. (on jamie)
+   thread?!? wtf. (on jamie) (? still valid ?)
 _ bugfix: width in index-mode needs to be determined per-character
    rather than per-byte
 _ search results: highlight relevant snippets and open to relevant
@@ -33,6 +32,9 @@ _ undo
 _ gmail support
 _ warnings: top-posting, missing attachment, ruby-talk:XXXX detection
 _ Net::SMTP support
+x user-defined hooks
+x tab completion for to: and cc: in compose-mode
+x individual labeling in thread-view-mode
 x translate aliases in queries on to: and from: fields
 x tab completion on labeling
 
diff --git a/lib/sup.rb b/lib/sup.rb
@@ -43,6 +43,7 @@ module Redwood
   SENT_FN    = File.join(BASE_DIR, "sent.mbox")
   LOCK_FN    = File.join(BASE_DIR, "lock")
   SUICIDE_FN = File.join(BASE_DIR, "please-kill-yourself")
+  HOOK_DIR   = File.join(BASE_DIR, "hooks")
 
   YAML_DOMAIN = "masanjin.net"
   YAML_DATE = "2006-10-01"
@@ -205,6 +206,13 @@ else
 end
 
 require "sup/util"
+require "sup/hook"
+
+## we have to initialize this guy first, because other classes must
+## reference it in order to register hooks, and they do that at parse
+## time.
+Redwood::HookManager.new Redwood::HOOK_DIR
+
 require "sup/update"
 require "sup/suicide"
 require "sup/message"
diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb
@@ -2,6 +2,7 @@ require 'etc'
 require 'thread'
 require 'ncurses'
 
+if defined? Ncurses
 module Ncurses
   def rows
     lame, lamer = [], []
@@ -43,6 +44,7 @@ module Ncurses
   KEY_CANCEL = 7 # ctrl-g
   KEY_TAB = 9
 end
+end
 
 module Redwood
 
diff --git a/lib/sup/hook.rb b/lib/sup/hook.rb
@@ -0,0 +1,111 @@
+module Redwood
+
+class HookManager
+
+  ## there's probably a better way to do this, but to evaluate a hook
+  ## with a bunch of pre-set "local variables" i define a function
+  ## per variable and then instance_evaluate the code.
+  ##
+  ## 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.
+  class HookContext
+    def initialize name, hash
+      @__name = name
+      hash.each do |k, v|
+        self.class.instance_eval { define_method(k) { v } }
+      end
+    end
+
+    def say s
+      @__say_id = BufferManager.say s, @__say_id
+    end
+
+    def log s
+      Redwood::log "hook[#@__name]: #{s}"
+    end
+
+    def __binding 
+      binding
+    end
+
+    def __cleanup
+      BufferManager.clear @__say_id if @__say_id
+    end
+  end
+
+  include Singleton
+
+  def initialize dir
+    @dir = dir
+    @hooks = {}
+    @descs = {}
+    Dir.mkdir dir unless File.exists? dir
+
+    self.class.i_am_the_instance self
+  end
+
+  def run name, locals={}
+    hook = hook_for(name) or return
+    context = HookContext.new name, locals
+
+    begin
+      result = eval @hooks[name], context.__binding, fn_for(name)
+      if result.is_a? String
+        log "got return value: #{result.inspect}"
+        BufferManager.flash result 
+      end
+    rescue Exception => e
+      log "error running hook: #{e.message}"
+      BufferManager.flash "Error running hook: #{e.message}"
+    end
+    context.__cleanup
+  end
+
+  def register name, desc
+    @descs[name] = desc
+  end
+
+  def print_hooks f=$stdout
+puts <<EOS
+Have #{@descs.size} registered hooks:
+
+EOS
+
+    @descs.sort.each do |name, desc|
+      f.puts <<EOS
+#{name}
+#{"-" * name.length}
+File: #{fn_for name}
+#{desc}
+EOS
+    end
+  end
+
+private
+
+  def hook_for name
+    unless @hooks.member? name
+      @hooks[name] =
+        begin
+          returning IO.readlines(fn_for(name)).join do
+            log "read '#{name}' from #{fn_for(name)}"
+          end
+        rescue SystemCallError => e
+          nil
+        end
+    end
+
+    @hooks[name]
+  end
+
+  def fn_for name
+    File.join @dir, "#{name}.rb"
+  end
+
+  def log m
+    Redwood::log("hook: " + m)
+  end
+end
+
+end
diff --git a/lib/sup/modes/thread-index-mode.rb b/lib/sup/modes/thread-index-mode.rb
@@ -382,9 +382,9 @@ class ThreadIndexMode < LineCursorMode
     myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
       opts[:when_done].call(num) if opts[:when_done]
       if num > 0
-        BufferManager.flash "Found #{num} threads"
+        BufferManager.flash "Found #{num} threads."
       else
-        BufferManager.flash "No matches"
+        BufferManager.flash "No matches."
       end
     end)})
 
diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb
@@ -5,6 +5,20 @@ module Redwood
 class PollManager
   include Singleton
 
+  HookManager.register "before-poll", <<EOS
+Executes immediately before a poll for new messages commences.
+No variables.
+EOS
+
+  HookManager.register "after-poll", <<EOS
+Executes immediately before a poll for new messages commences.
+Variables:
+            num: the total number of new messages
+      num_inbox: the number of new messages appearing in the inbox (i.e. not
+                 auto-archived).
+  from_and_subj: an array of (from email address, subject) pairs
+EOS
+
   DELAY = 300
 
   def initialize
@@ -20,13 +34,18 @@ class PollManager
   end
 
   def poll
+    HookManager.run "before-poll"
+
     BufferManager.flash "Polling for new messages..."
-    num, numi = buffer.mode.poll
+    num, numi, from_and_subj = buffer.mode.poll
     if num > 0
       BufferManager.flash "Loaded #{num} new messages, #{numi} to inbox." 
     else
       BufferManager.flash "No new messages." 
     end
+
+    HookManager.run "after-poll", :num => num, :num_inbox => numi, :from_and_subj => from_and_subj
+
     [num, numi]
   end
 
@@ -46,6 +65,8 @@ class PollManager
 
   def do_poll
     total_num = total_numi = 0
+    from_and_subj = []
+
     @mutex.synchronize do
       Index.usual_sources.each do |source|
 #        yield "source #{source} is done? #{source.done?} (cur_offset #{source.cur_offset} >= #{source.end_offset})"
@@ -66,6 +87,7 @@ class PollManager
           unless entry
             num += 1
             numi += 1 if m.labels.include? :inbox
+            from_and_subj << [m.from.longname, m.subj]
           end
           m
         end
@@ -78,7 +100,7 @@ class PollManager
       @last_poll = Time.now
       @polling = false
     end
-    [total_num, total_numi]
+    [total_num, total_numi, from_and_subj]
   end
 
   ## this is the main mechanism for adding new messages to the
diff --git a/lib/sup/util.rb b/lib/sup/util.rb
@@ -123,21 +123,7 @@ class Object
     ret
   end
 
-  ## takes a value which it yields and then returns, so that code
-  ## like:
-  ##
-  ## x = expensive_operation
-  ## log "got #{x}"
-  ## x
-  ##
-  ## now becomes:
-  ##
-  ## with(expensive_operation) { |x| log "got #{x}" }
-  ##
-  ## i'm sure there's pithy comment i could make here about the
-  ## superiority of lisp, but fuck lisp.
-  ##
-  ## addendum: apparently this is a "k combinator". whoda thunk it?
+  ## "k combinator"
   def returning x; yield x; x; end
 
   ## clone of java-style whole-method synchronization
diff --git a/www/index.html b/www/index.html
@@ -24,6 +24,10 @@
 		&ldquo;Every other client we've tried is intolerable.&rdquo;
 		</blockquote>
 
+		<blockquote>
+		&ldquo;Sup is almost to the point where I could jump ship from mutt.&rdquo;
+		</blockquote>
+
 		<p>
    Sup is a console-based email client for people with a lot of email.
    It supports tagging, very fast full-text search, automatic