]> git.notmuchmail.org Git - sup/commitdiff
Merge branch 'master' into next
authorWilliam Morgan <wmorgan-sup@masanjin.net>
Mon, 13 Apr 2009 14:54:02 +0000 (07:54 -0700)
committerWilliam Morgan <wmorgan-sup@masanjin.net>
Mon, 13 Apr 2009 14:54:02 +0000 (07:54 -0700)
20 files changed:
Manifest.txt
README.txt
bin/sup
contrib/completion/_sup.zsh [new file with mode: 0644]
doc/NewUserGuide.txt
lib/sup.rb
lib/sup/buffer.rb
lib/sup/colormap.rb
lib/sup/label.rb
lib/sup/logger.rb
lib/sup/mode.rb
lib/sup/modes/buffer-list-mode.rb
lib/sup/modes/contact-list-mode.rb
lib/sup/modes/edit-message-mode.rb
lib/sup/modes/inbox-mode.rb
lib/sup/modes/label-list-mode.rb
lib/sup/modes/resume-mode.rb
lib/sup/modes/thread-index-mode.rb
lib/sup/poll.rb
lib/sup/undo.rb [new file with mode: 0644]

index 6ecfe670d07649cf288cfadcfe78f3ce71cd4731..be633d776e2b1e5165edeac4d7631cf714471207 100644 (file)
@@ -71,5 +71,6 @@ lib/sup/suicide.rb
 lib/sup/tagger.rb
 lib/sup/textfield.rb
 lib/sup/thread.rb
+lib/sup/undo.rb
 lib/sup/update.rb
 lib/sup/util.rb
index 9a4490acabe3da4e986d470e46bea0b0da3c49e4..0437de9f0c91b87b300c99077a88d7df82162100 100644 (file)
@@ -80,7 +80,7 @@ Current limitations which will be fixed:
 - Unix-centrism in MIME attachment handling and in sendmail
   invocation.
 
-- Several obvious missing features, like undo, filters / saved
+- Several obvious missing features, like filters / saved
   searches, message annotations, etc.
 
 == SYNOPSYS:
diff --git a/bin/sup b/bin/sup
index d8eee0e3215fff5cc52470a4695aef3c561e60d4..e16c91bf510b05281082c86b7bacadd240f30c97 100644 (file)
--- a/bin/sup
+++ b/bin/sup
@@ -67,9 +67,9 @@ global_keymap = Keymap.new do |k|
   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 :roll_buffers_backwards, "Switch to previous buffer", 'B'
   k.add :kill_buffer, "Kill the current buffer", 'x'
-  k.add :list_buffers, "List all buffers", 'B'
+  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'
@@ -81,6 +81,36 @@ global_keymap = Keymap.new do |k|
   k.add :recall_draft, "Edit most recent draft message", 'R'
 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::Importable
+  setlocale_lib = case Config::CONFIG['arch']
+    when /darwin/; "libc.dylib"
+    when /cygwin/; "cygwin1.dll"
+    else; "libc.so.6"
+  end
+
+  Redwood::log "dyamically loading setlocale() from #{setlocale_lib}"
+  begin
+    dlload setlocale_lib
+    extern "void setlocale(int, const char *)"
+    Redwood::log "setting locale..."
+    LibC.setlocale(6, "")  # LC_ALL == 6
+  rescue RuntimeError => e
+    Redwood::log "cannot dlload setlocale(); ncurses wide character support probably broken."
+    Redwood::log "dlload error was #{e.class}: #{e.message}"
+    if Config::CONFIG['arch'] =~ /bsd/
+      Redwood::log "BSD variant detected. You may have to install a compat6x package to acquire libc."
+    end
+  end
+end
+
 def start_cursing
   Ncurses.initscr
   Ncurses.noecho
@@ -230,7 +260,7 @@ begin
     when :kill_buffer
       bm.kill_buffer_safely bm.focus_buf
     when :list_buffers
-      bm.spawn_unless_exists("Buffer List") { BufferListMode.new }
+      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
@@ -241,7 +271,7 @@ begin
     when :search_unread
       SearchResultsMode.spawn_from_query "is:unread"
     when :list_labels
-      labels = LabelManager.listable_labels.map { |l| LabelManager.string_for l }
+      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?
diff --git a/contrib/completion/_sup.zsh b/contrib/completion/_sup.zsh
new file mode 100644 (file)
index 0000000..76870ca
--- /dev/null
@@ -0,0 +1,114 @@
+#compdef sup sup-add sup-config sup-dump sup-sync sup-sync-back sup-tweak-labels sup-recover-sources
+# vim: set et sw=2 sts=2 ts=2 ft=zsh :
+
+# TODO: sources completion: maildir://some/dir, mbox://some/file, ...
+#       for sup-add, sup-sync, sup-sync-back, sup-tweak-labels
+
+(( ${+functions[_sup_cmd]} )) ||
+_sup_cmd()
+{
+  _arguments -s : \
+    "(--list-hooks -l)"{--list-hooks,-l}"[list all hooks and descriptions, and quit]" \
+    "(--no-threads -n)"{--no-threads,-n}"[turn off threading]" \
+    "(--no-initial-poll -o)"{--no-initial-poll,-o}"[Don't poll for new messages when starting]" \
+    "(--search -s)"{--search,-s}"[search for this query upon startup]:Query: " \
+    "(--compose -c)"{--compose,-c}"[compose message to this recipient upon startup]:Email: " \
+    "--version[show version information]" \
+    "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_add_cmd]} )) ||
+_sup_add_cmd()
+{
+  _arguments -s : \
+    "(--archive -a)"{--archive,-a}"[automatically archive all new messages from this source]" \
+    "(--unusual -u)"{--unusual,-u}"[do not automatically poll for new messages from this source]" \
+    "(--labels -l)"{--labels,-l}"[set of labels to apply to all messages from this source]:Labels: " \
+    "(--force-new -f)"{--force-new,-f}"[create a new account for this source, even if one already exists]" \
+    "--version[show version information]" \
+    "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_config_cmd]} )) ||
+_sup_config_cmd()
+{
+  _arguments -s : \
+    "--version[show version information]" \
+    "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_dump_cmd]} )) ||
+_sup_dump_cmd()
+{
+  _arguments -s : \
+    "--version[show version information]" \
+    "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_recover_sources_cmd]} )) ||
+_sup_recover_sources_cmd()
+{
+  _arguments -s : \
+    "--archive[automatically archive all new messages from this source]" \
+    "--scan-num[number of messages to scan per source]:" \
+    "--unusual[do not automatically poll for new messages from this source]" \
+    "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_sync_cmd]} )) ||
+_sup_sync_cmd()
+{
+  # XXX Add only when --restore is given: (--restored -r)
+  #     Add only when --changed or--all are given: (--start-at -s)
+  _arguments -s : \
+    "--new[operate on new messages only]" \
+    "(--changed -c)"{--changed,-c}"[scan over the entire source for messages that have been deleted, altered, or moved from another source]" \
+    "(--restored -r)"{--restored,-r}"[operate only on those messages included in a dump file as specified by --restore which have changed state]" \
+    "(--all -a)"{--all,-a}"[operate on all messages in the source, regardless of newness or changedness]" \
+    "(--start-at -s)"{--start-at,-s}"[start at a particular offset]:Offset: " \
+    "--asis[if the message is already in the index, preserve its state, otherwise, use default source state]" \
+    "--restore[restore message state from a dump file created with sup-dump]:File:_file" \
+    "--discard[discard any message state in the index and use the default source state]" \
+    "(--archive -x)"{--archive,-x}"[mark messages as archived when using the default source state]" \
+    "(--read -e)"{--read,-e}"[mark messages as read when using the default source state]" \
+    "--extra-labels[apply these labels when using the default source state]:Labels: " \
+    "(--verbose -v)"{--verbose,-v}"[print message ids as they're processed]" \
+    "(--optimize -o)"{--optimize,-o}"[as the final operation, optimize the index]" \
+    "--all-sources[scan over all sources]" \
+    "(--dry-run -n)"{--dry-run,-n}"[don't actually modify the index]" \
+    "--version[show version information]" \
+    "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_sync_back_cmd]} )) ||
+_sup_sync_back_cmd()
+{
+  _arguments -s : \
+    "(--drop-deleted -d)"{--drop-deleted,-d}"[drop deleted messages]" \
+    "--move-deleted[move deleted messages to a local mbox file]:File:_file" \
+    "(--drop-spam -s)"{--drop-spam,-s}"[drop spam messages]" \
+    "--move-spam[move spam messages to a local mbox file]:File:_file" \
+    "--with-dotlockfile[specific dotlockfile location (mbox files only)]:File:_file" \
+    "--dont-use-dotlockfile[don't use dotlockfile to lock mbox files]" \
+    "(--verbose -v)"{--verbose,-v}"[print message ids as they're processed]" \
+    "(--dry-run -n)"{--dry-run,-n}"[don't actually modify the index]" \
+    "--version[show version information]" \
+    "(--help -h)"{--help,-h}"[show help]"
+}
+
+(( ${+functions[_sup_tweak_labels_cmd]} )) ||
+_sup_tweak_labels_cmd()
+{
+  _arguments -s : \
+    "(--add -a)"{--add,-a}"[which labels to add to every message from the specified sources]:Labels: " \
+    "(--remove -r)"{--remove,-r}"[which labels to remove from every message from the specified sources]:Labels: " \
+    "--all-sources[scan over all sources]" \
+    "(--verbose -v)"{--verbose,-v}"[print message ids as they're processed]" \
+    "(--dry-run -n)"{--dry-run,-n}"[don't actually modify the index]" \
+    "--version[show version information]" \
+    "(--help -h)"{--help,-h}"[show help]"
+}
+
+_call_function ret _${words[1]//-/_}_cmd
+return ret
+
index e4d49eef3bcb87aae0d8b83f37bb89c7896689f3..5f9c3baad61c3a9a41265cd0709c221a508ae936 100644 (file)
@@ -98,9 +98,9 @@ press 'n' and 'p' to jump forward and backward between open messages,
 aligning the display as necessary.
 
 Now press 'x' to kill the thread view buffer. You should see the inbox
-again. If you don't, you can cycle through the buffers by pressing
-'b', or you can press 'B' to see a list of all buffers and simply
-select the inbox.
+again. If you don't, you can cycle through the buffers by pressing 'b'
+and 'B' (forwards and backwards, respectively), or you can press ';' to
+see a list of all buffers and simply select the inbox.
 
 There are many operations you can perform on threads beyond viewing
 them. To archive a thread, press 'a'. The thread will disappear from
@@ -125,8 +125,8 @@ in the labels as a sequence of space-separated words. To cancel the
 input, press Ctrl-G.
 
 Many of these operations can be applied to a group of threads. Press
-'t' to tag a thread. Tag a couple, then press ';' to apply the next
-command to the set of threads. ';t', of course, will untag all tagged
+'t' to tag a thread. Tag a couple, then press '+' to apply the next
+command to the set of threads. '+t', of course, will untag all tagged
 messages.
 
 Ok, let's try using labels and search. Press 'L' to do a quick label
@@ -245,7 +245,6 @@ Here's what I recommend:
    inbox, and you can browse the mailing list traffic at any point by
    searching for that label.
 
-
 Appendix C: Reading blogs with Sup
 ----------------------------------
 
index 3afac5e012b0e9438434981fa2ebaae068ec9ea4..96510b2e2d7419629a0a3fd3e9d96eafca29c36e 100644 (file)
@@ -6,19 +6,6 @@ require 'fileutils'
 require 'gettext'
 require 'curses'
 
-## 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'
-module LibC
-  extend DL::Importable
-  dlload Config::CONFIG['arch'] =~ /darwin/ ? "libc.dylib" : "libc.so.6"
-  extern "void setlocale(int, const char *)"
-end
-LibC.setlocale(6, "")  # LC_ALL == 6
-
 class Object
   ## this is for debugging purposes because i keep calling #id on the
   ## wrong object and i want it to throw an exception
@@ -123,6 +110,7 @@ module Redwood
     Redwood::PollManager.new
     Redwood::SuicideManager.new Redwood::SUICIDE_FN
     Redwood::CryptoManager.new
+    Redwood::UndoManager.new
   end
 
   def finish
@@ -279,6 +267,7 @@ require "sup/tagger"
 require "sup/draft"
 require "sup/poll"
 require "sup/crypto"
+require "sup/undo"
 require "sup/horizontal-selector"
 require "sup/modes/line-cursor-mode"
 require "sup/modes/help-mode"
index c9e34b3595c62b82b64350163e0d547e127f95eb..cb61e12ffd19df6a2ccc0a2df7c54ec80066938e 100644 (file)
@@ -51,8 +51,8 @@ module Redwood
 class InputSequenceAborted < StandardError; end
 
 class Buffer
-  attr_reader :mode, :x, :y, :width, :height, :title
-  bool_reader :dirty
+  attr_reader :mode, :x, :y, :width, :height, :title, :atime
+  bool_reader :dirty, :system
   bool_accessor :force_to_top
 
   def initialize window, mode, width, height, opts={}
@@ -63,6 +63,8 @@ class Buffer
     @title = opts[:title] || ""
     @force_to_top = opts[:force_to_top] || false
     @x, @y, @width, @height = 0, 0, width, height
+    @atime = Time.at 0
+    @system = opts[:system] || false
   end
 
   def content_height; @height - 1; end
@@ -97,6 +99,7 @@ class Buffer
     @mode.draw
     draw_status status
     commit
+    @atime = Time.now
   end
 
   ## s nil means a blank line!
@@ -338,7 +341,7 @@ EOS
     ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
     ## (opts[:left] || 0))
     w = Ncurses.stdscr
-    b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
+    b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => opts[:force_to_top], :system => opts[:system]
     mode.buffer = b
     @name_map[realtitle] = b
 
@@ -478,7 +481,9 @@ EOS
     default = default_labels.join(" ")
     default += " " unless default.empty?
 
-    applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
+    # here I would prefer to give more control and allow all_labels instead of
+    # user_defined_labels only
+    applyable_labels = (LabelManager.user_defined_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
 
     answer = ask_many_with_completions domain, question, applyable_labels, default
 
index 469ed0b91874342a8fde6787008aa625b9d4e576..fe60f61362a5d65cd3abe22d4fcfbf8bc21d26c2 100644 (file)
@@ -46,7 +46,10 @@ class Colormap
     :completion_character => { :fg => "white", :bg => "default", :attrs => ["bold"] },
     :horizontal_selector_selected => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
     :horizontal_selector_unselected => { :fg => "cyan", :bg => "default" },
-    :search_highlight => { :fg => "black", :bg => "yellow", :attrs => ["bold"] }
+    :search_highlight => { :fg => "black", :bg => "yellow", :attrs => ["bold"] },
+    :system_buf => { :fg => "blue", :bg => "default" },
+    :regular_buf => { :fg => "white", :bg => "default" },
+    :modified_buffer => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
   }
   
   def initialize
index 70a26ead5409e2b63a0edef48cb2a9bb4ef4cc6a..e63db9ed6d5790714b40c9d268fab432f45d0325 100644 (file)
@@ -7,9 +7,6 @@ class LabelManager
   ## add/remove these via normal label mechanisms.
   RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted, :inbox, :attachment ]
 
-  ## labels which it nonetheless makes sense to search for by
-  LISTABLE_RESERVED_LABELS = [ :starred, :spam, :draft, :sent, :killed, :deleted, :inbox, :attachment ]
-
   ## labels that will typically be hidden from the user
   HIDDEN_RESERVED_LABELS = [ :starred, :unread, :attachment ]
 
@@ -28,18 +25,18 @@ class LabelManager
     self.class.i_am_the_instance self
   end
 
-  ## all listable (just user-defined at the moment) labels, ordered
+  ## all labels user-defined and system, ordered
   ## nicely and converted to pretty strings. use #label_for to recover
   ## the original label.
-  def listable_labels
+  def all_labels
     ## uniq's only necessary here because of certain upgrade issues
-    (LISTABLE_RESERVED_LABELS + @labels.keys).uniq
+    (RESERVED_LABELS + @labels.keys).uniq
   end
 
-  ## all apply-able (user-defined and system listable) labels, ordered
+  ## all user-defined labels, ordered
   ## nicely and converted to pretty strings. use #label_for to recover
   ## the original label.
-  def applyable_labels
+  def user_defined_labels
     @labels.keys
   end
 
index ebdeebe74dc67369342680c2d5a07f56ecadeb34..4ac6551bff21e50ab45704f295d6d930edf1ce6d 100644 (file)
@@ -18,7 +18,7 @@ class Logger
   def make_buf
     return if @mode.buffer || !BufferManager.instantiated? || !@respawn || @spawning
     @spawning = true
-    @mode.buffer = BufferManager.instance.spawn "<log>", @mode, :hidden => true
+    @mode.buffer = BufferManager.instance.spawn "log", @mode, :hidden => true, :system => true
     @spawning = false
   end
 
index 9900dc303d80395a3026ced166fc82b78a15ce54..bea46d92d425835142f2ae3b94eca035b0a64148 100644 (file)
@@ -24,6 +24,7 @@ class Mode
   end
 
   def killable?; true; end
+  def unsaved?; false end
   def draw; end
   def focus; end
   def blur; end
index 91b2fac7fa4d50e974368486ca17a1ddc44f462e..1554caedfe77e2bfeb8a99efbfd65f63f479c074 100644 (file)
@@ -16,6 +16,7 @@ class BufferListMode < LineCursorMode
 
   def focus
     reload # buffers may have been killed or created since last view
+    set_cursor_pos 0
   end
 
 protected
@@ -26,10 +27,13 @@ protected
   end
 
   def regen_text
-    @bufs = BufferManager.buffers.sort_by { |name, buf| name }
+    @bufs = BufferManager.buffers.reject { |name, buf| buf.mode == self }.sort_by { |name, buf| buf.atime }.reverse
     width = @bufs.max_of { |name, buf| buf.mode.name.length }
     @text = @bufs.map do |name, buf|
-      sprintf "%#{width}s  %s", buf.mode.name, name
+      base_color = buf.system? ? :system_buf_color : :regular_buf_color
+      [[base_color, sprintf("%#{width}s ", buf.mode.name)],
+       [:modified_buffer_color, (buf.mode.unsaved? ? '*' : ' ')],
+       [base_color, " " + name]]
     end
   end
 
index 7c16babd61aacf220998bfb93aca300c0629d112..edbef5e3a27f3f9cba65c8cf1cfaecec05021e3e 100644 (file)
@@ -23,7 +23,7 @@ class ContactListMode < LineCursorMode
     k.add :reload, "Drop contact list and reload", 'D'
     k.add :alias, "Edit alias/or name for contact", 'a', 'i'
     k.add :toggle_tagged, "Tag/untag current line", 't'
-    k.add :apply_to_tagged, "Apply next command to all tagged items", ';'
+    k.add :apply_to_tagged, "Apply next command to all tagged items", '+'
     k.add :search, "Search for messages from particular people", 'S'
   end
 
index 31aa8972766c34d17ae8f9cda5379b5363af559f..353e76ac137644d9a1389705d997630501a2a6ab 100644 (file)
@@ -145,6 +145,8 @@ EOS
     !edited? || BufferManager.ask_yes_or_no("Discard message?")
   end
 
+  def unsaved?; edited? end
+
   def attach_file
     fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
     return unless fn
index 559892d5655f07b80a1186e114950156220a13c5..21eb9ac43bff5cbd01fb0e10807b61381aabf1bd 100644 (file)
@@ -26,12 +26,28 @@ class InboxMode < ThreadIndexMode
 
   def archive
     return unless cursor_thread
+    thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
+
+    undo = lambda {
+      thread.apply_label :inbox
+      add_or_unhide thread.first
+    }
+    UndoManager.register("archiving thread #{thread.first.id}", undo)
+
     cursor_thread.remove_label :inbox
     hide_thread cursor_thread
     regen_text
   end
 
   def multi_archive threads
+    undo = threads.map {|t|
+             lambda{
+               t.apply_label :inbox
+               add_or_unhide t.first
+             }}
+    UndoManager.register("archiving #{threads.size} #{threads.size.pluralize 'thread'}",
+                         undo << lambda {regen_text} )
+
     threads.each do |t|
       t.remove_label :inbox
       hide_thread t
@@ -41,6 +57,15 @@ class InboxMode < ThreadIndexMode
 
   def read_and_archive
     return unless cursor_thread
+    thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
+
+    undo = lambda {
+      thread.apply_label :inbox
+      thread.apply_label :unread
+      add_or_unhide thread.first
+    }
+    UndoManager.register("reading and archiving thread ", undo)
+
     cursor_thread.remove_label :unread
     cursor_thread.remove_label :inbox
     hide_thread cursor_thread
@@ -48,6 +73,16 @@ class InboxMode < ThreadIndexMode
   end
 
   def multi_read_and_archive threads
+    undo = threads.map {|t|
+      lambda {
+        t.apply_label :inbox
+        t.apply_label :unread
+        add_or_unhide t.first
+      }
+    }
+    UndoManager.register("reading and archiving #{threads.size} #{threads.size.pluralize 'thread'}",
+                         undo << lambda {regen_text})
+
     threads.each do |t|
       t.remove_label :unread
       t.remove_label :inbox
index 132b654066a8f107e2e52d7d2622c722e1e65ea6..a35d110df8d0607589b9c412682818674c181c9c 100644 (file)
@@ -48,12 +48,12 @@ protected
 
   def regen_text
     @text = []
-    labels = LabelManager.listable_labels
+    labels = LabelManager.all_labels
 
     counts = labels.map do |label|
       string = LabelManager.string_for label
       total = Index.num_results_for :label => label
-      unread = Index.num_results_for :labels => [label, :unread]
+      unread = (label == :unread)? total : Index.num_results_for(:labels => [label, :unread])
       [label, string, total, unread]
     end.sort_by { |l, s, t, u| s.downcase }
 
index b1c69fc1dc2f35fe1eea2350b21873e6c08470e9..e527542bd8266a6c0a38666955a2434f98d13516 100644 (file)
@@ -11,6 +11,8 @@ class ResumeMode < EditMessageMode
     super :header => header, :body => body, :have_signature => true
   end
 
+  def unsaved?; !@safe end
+
   def killable?
     return true if @safe
 
index cd9a0277689a9debba64003332345adc96d530fb..7b87bf66ba9112a3fb20a465a540f6c0eef085ba 100644 (file)
@@ -42,8 +42,9 @@ EOS
     k.add :toggle_tagged, "Tag/untag selected thread", 't'
     k.add :toggle_tagged_all, "Tag/untag all threads", 'T'
     k.add :tag_matching, "Tag matching threads", 'g'
-    k.add :apply_to_tagged, "Apply next command to all tagged threads", ';'
+    k.add :apply_to_tagged, "Apply next command to all tagged threads", '+'
     k.add :join_threads, "Force tagged threads to be joined into the same thread", '#'
+    k.add :undo, "Undo the previous action", 'u'
   end
 
   def initialize hidden_labels=[], load_thread_opts={}
@@ -79,12 +80,14 @@ EOS
     end
   end
 
+  def unsaved?; dirty? end
   def lines; @text.length; end
   def [] i; @text[i]; end
   def contains_thread? t; @threads.include?(t) end
 
   def reload
     drop_all_threads
+    UndoManager.clear
     BufferManager.draw_screen
     load_threads :num => buffer.content_height
   end
@@ -210,6 +213,10 @@ EOS
     add_or_unhide m
   end
 
+  def undo
+    UndoManager.undo
+  end
+
   def update
     @mutex.synchronize do
       ## let's see you do THIS in python
@@ -233,65 +240,128 @@ EOS
   end
 
   def actually_toggle_starred t
+    thread = t # cargo cult programming
+    pos = curpos
     if t.has_label? :starred # if ANY message has a star
+      undo = lambda {
+        thread.first.add_label :starred
+        update_text_for_line pos
+        UpdateManager.relay self, :starred, thread.first
+      }
       t.remove_label :starred # remove from all
       UpdateManager.relay self, :unstarred, t.first
     else
+      undo = lambda {
+        thread.remove_label :starred
+        update_text_for_line pos
+        UpdateManager.relay self, :unstarred, thread.first
+      }
       t.first.add_label :starred # add only to first
       UpdateManager.relay self, :starred, t.first
     end
+
+    return undo
   end  
 
   def toggle_starred 
     t = cursor_thread or return
-    actually_toggle_starred t
+    undo = actually_toggle_starred t
+    UndoManager.register("starring/unstarring thread #{t.first.id}",undo)
     update_text_for_line curpos
     cursor_down
   end
 
   def multi_toggle_starred threads
-    threads.each { |t| actually_toggle_starred t }
+    undo = threads.map { |t| actually_toggle_starred t }
+    UndoManager.register("starring/unstarring #{threads.size} #{threads.size.pluralize 'thread'}",
+                         undo)
     regen_text
   end
 
   def actually_toggle_archived t
+    thread = t
+    pos = curpos
     if t.has_label? :inbox
       t.remove_label :inbox
+      undo = lambda {
+        thread.apply_label :inbox
+        update_text_for_line pos
+        UpdateManager.relay self,:unarchived, thread.first
+      }
       UpdateManager.relay self, :archived, t.first
     else
       t.apply_label :inbox
+      undo = lambda {
+        thread.remove_label :inbox
+        update_text_for_line pos
+        UpdateManager.relay self, :unarchived, thread.first
+      }
       UpdateManager.relay self, :unarchived, t.first
     end
+
+    return undo
   end
 
   def actually_toggle_spammed t
+    thread = t
     if t.has_label? :spam
+      undo = lambda {
+        thread.apply_label :spam
+        self.hide_thread thread
+        UpdateManager.relay self,:spammed, thread.first
+      }
       t.remove_label :spam
+      add_or_unhide t.first
       UpdateManager.relay self, :unspammed, t.first
     else
+      undo = lambda {
+        thread.remove_label :spam
+        add_or_unhide thread.first
+        UpdateManager.relay self,:unspammed, thread.first
+      }
       t.apply_label :spam
+      hide_thread t
       UpdateManager.relay self, :spammed, t.first
     end
+
+    return undo
   end
 
   def actually_toggle_deleted t
     if t.has_label? :deleted
+      undo = lambda {
+        t.apply_label :deleted
+        hide_thread t
+        UpdateManager.relay self, :deleted, t.first
+      }
       t.remove_label :deleted
+      add_or_unhide t.first
       UpdateManager.relay self, :undeleted, t.first
     else
+      undo = lambda {
+        t.remove_label :deleted
+        add_or_unhide t.first
+        UpdateManager.relay self, :undeleted, t.first
+      }
       t.apply_label :deleted
+  hide_thread t
       UpdateManager.relay self, :deleted, t.first
     end
+
+    return undo
   end
 
   def toggle_archived 
     t = cursor_thread or return
-    actually_toggle_archived t
+    undo = [actually_toggle_archived(t), lambda {self.update_text_for_line curpos}]
+    UndoManager.register("deleting/undeleting thread #{t.first.id}",undo)
     update_text_for_line curpos
   end
 
   def multi_toggle_archived threads
-    threads.each { |t| actually_toggle_archived t }
+    undo = threads.map { |t| actually_toggle_archived t}
+    UndoManager.register("deleting/undeleting #{threads.size} #{threads.size.pluralize 'thread'}",
+                         undo << lambda {self.regen_text})
     regen_text
   end
 
@@ -352,10 +422,9 @@ EOS
   ## see deleted or spam emails, and when you undelete or unspam them
   ## you also want them to disappear immediately.
   def multi_toggle_spam threads
-    threads.each do |t|
-      actually_toggle_spammed t
-      hide_thread t 
-    end
+    undo = threads.map{ |t| actually_toggle_spammed t}
+    UndoManager.register("marking/unmarking #{threads.size} #{threads.size.pluralize 'thread'} as spam",
+                         undo <<  lambda {self.regen_text})
     regen_text
   end
 
@@ -366,10 +435,9 @@ EOS
 
   ## see comment for multi_toggle_spam
   def multi_toggle_deleted threads
-    threads.each do |t|
-      actually_toggle_deleted t
-      hide_thread t 
-    end
+    undo = threads.map{ |t| actually_toggle_deleted t}
+    UndoManager.register("deleting/undeleting #{threads.size} #{threads.size.pluralize 'thread'}",
+                         undo << lambda {regen_text})
     regen_text
   end
 
@@ -378,11 +446,18 @@ EOS
     multi_kill [t]
   end
 
+  ## m-m-m-m-MULTI-KILL
   def multi_kill threads
-    threads.each do |t|
+    undo = threads.map do |t|
       t.apply_label :killed
       hide_thread t
+      thread = t
+      lambda { thread.remove_label :killed
+        add_or_unhide thread.first
+      }
     end
+    UndoManager.register("killing #{threads.size} #{threads.size.pluralize 'thread'}",
+                         undo << lambda {regen_text})
     regen_text
     BufferManager.flash "#{threads.size.pluralize 'Thread'} killed."
   end
@@ -453,6 +528,10 @@ EOS
   def edit_labels
     thread = cursor_thread or return
     speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
+
+    old_labels = thread.labels
+    pos = curpos
+
     keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
 
     user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl, @hidden_labels
@@ -461,6 +540,15 @@ EOS
     thread.labels = keepl + user_labels
     user_labels.each { |l| LabelManager << l }
     update_text_for_line curpos
+
+    undo = lambda{
+      thread.labels = old_labels
+      update_text_for_line pos
+      UpdateManager.relay self, :labeled, thread.first
+    }
+
+    UndoManager.register("labeling thread #{thread.first.id}", undo)
+
     UpdateManager.relay self, :labeled, thread.first
   end
 
@@ -471,7 +559,8 @@ EOS
     user_labels.map! { |l| (l.to_s =~ /^-/)? [l.to_s.gsub(/^-?/, '').to_sym, true] : [l, false] }
     hl = user_labels.select { |(l,_)| @hidden_labels.member? l }
     if hl.empty?
-      threads.each do |t|
+      undo = threads.map do |t|
+        old_labels = t.labels
         user_labels.each do |(l, to_remove)|
           if to_remove
             t.remove_label l
@@ -479,8 +568,16 @@ EOS
             t.apply_label l
           end
         end
+        ## UpdateManager or some other regresh mechanism?
+        UpdateManager.relay self, :labeled, t.first
+        lambda do
+          t.labels = old_labels
+          UpdateManager.relay self, :labeled, t.first
+        end
       end
       user_labels.each { |(l,_)| LabelManager << l }
+      UndoManager.register("labeling #{threads.size} #{threads.size.pluralize 'thread'}",
+                          undo << lambda { regen_text})
     else
       BufferManager.flash "'#{hl}' is a reserved label!"
     end
@@ -753,7 +850,6 @@ protected
       (t.labels - @hidden_labels).map { |label| [:label_color, "+#{label} "] } +
       [[:snippet_color, snippet]
     ]
-
   end
 
   def dirty?; @mutex.synchronize { (@hidden_threads.keys + @threads).any? { |t| t.dirty? } } end
index d766587f58815f27ec07b485c07ca6ec5204542a..442b6db22192a050b8c41c7bfcdc4383a4b574bc 100644 (file)
@@ -40,7 +40,7 @@ EOS
   end
 
   def buffer
-    b, new = BufferManager.spawn_unless_exists("<poll for new messages>", :hidden => true) { PollMode.new }
+    b, new = BufferManager.spawn_unless_exists("poll for new messages", :hidden => true, :system => true) { PollMode.new }
     b
   end
 
diff --git a/lib/sup/undo.rb b/lib/sup/undo.rb
new file mode 100644 (file)
index 0000000..250433d
--- /dev/null
@@ -0,0 +1,42 @@
+module Redwood
+
+## Implements a single undo list for the Sup instance
+##
+## The basic idea is to keep a list of lambdas to undo
+## things. When an action is called (such as 'archive'),
+## a lambda is registered with UndoManager that will
+## undo the archival action
+
+class UndoManager
+  include Singleton
+
+  def initialize
+    @@actionlist = []
+    self.class.i_am_the_instance self
+  end
+
+  def register desc, actions
+    actions = [actions] unless actions.is_a?Array
+    raise StandardError, "when would I need to undo 'nothing?'" unless actions.length > 0
+    Redwood::log "registering #{actions.length} actions: #{desc}"
+    @@actionlist.push({:desc => desc, :actions => actions})
+  end
+
+  def undo
+    unless @@actionlist.length == 0 then
+      actionset = @@actionlist.pop
+      Redwood::log "undoing #{actionset[:desc]}..."
+      actionset[:actions].each{|action|
+        action.call
+      }
+      BufferManager.flash "undid #{actionset[:desc]}"
+    else
+      BufferManager.flash "nothing more to undo"
+    end
+  end
+
+  def clear
+    @@actionlist = []
+  end
+end
+end