]> git.cworth.org Git - sup/commitdiff
Merge branch 'dont-canonicalize-email-addresses' into next
authorWilliam Morgan <wmorgan-sup@masanjin.net>
Wed, 25 Mar 2009 15:52:16 +0000 (08:52 -0700)
committerWilliam Morgan <wmorgan-sup@masanjin.net>
Wed, 25 Mar 2009 15:52:16 +0000 (08:52 -0700)
12 files changed:
Manifest.txt
README.txt
bin/sup
bin/sup-sync
contrib/completion/_sup.zsh [new file with mode: 0644]
lib/sup.rb
lib/sup/colormap.rb
lib/sup/modes/inbox-mode.rb
lib/sup/modes/thread-index-mode.rb
lib/sup/poll.rb
lib/sup/undo.rb [new file with mode: 0644]
lib/sup/util.rb

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 0d2366e4ebe81840904ee562a8eeed6d72fd09e1..c8a4107785ff137f3bdf9a9f9af2b32fb2d36ab7 100644 (file)
--- a/bin/sup
+++ b/bin/sup
@@ -66,8 +66,10 @@ 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_multi "(n)ext/(p)revious:", 'b' do |kk|
+    kk.add :roll_buffers, "Switch to next buffer", 'n'
+    kk.add :roll_buffers_backwards, "Switch to previous buffer", 'p'
+  end
   k.add :kill_buffer, "Kill the current buffer", 'x'
   k.add :list_buffers, "List all buffers", 'B'
   k.add :list_contacts, "List contacts", 'C'
@@ -86,6 +88,7 @@ def start_cursing
   Ncurses.noecho
   Ncurses.cbreak
   Ncurses.stdscr.keypad 1
+  Ncurses.use_default_colors
   Ncurses.curs_set 0
   Ncurses.start_color
   $cursing = true
index ac5caf6c6b724e2df48db24afb46994e8cfaa683..91710d47495ab263a69e03ec709657d1ee8b66ae 100644 (file)
@@ -143,12 +143,7 @@ begin
       next if target == :changed && entry && entry[:source_id].to_i == source.id && entry[:source_info].to_i == offset
 
       ## get the state currently in the index
-      index_state =
-        if entry
-          entry[:label].split(/\s+/).map { |x| x.intern }
-        else
-          nil
-        end
+      index_state = entry[:label].split(/\s+/).map { |x| x.intern } if entry
 
       ## skip if we're operating on restored messages, and this one
       ## ain't.
@@ -163,7 +158,7 @@ begin
       ## assign message labels based on the operation we're performing
       case op
       when :asis
-        m.labels = index_state if index_state
+        m.labels = ((m.labels - [:unread, :inbox]) + index_state).uniq if index_state
       when :restore
         ## if the entry exists on disk
         if restored_state[m.id]
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 3afac5e012b0e9438434981fa2ebaae068ec9ea4..6d9f12531232bf888f0036a7027c5a8fb0a2e11e 100644 (file)
@@ -123,6 +123,7 @@ module Redwood
     Redwood::PollManager.new
     Redwood::SuicideManager.new Redwood::SUICIDE_FN
     Redwood::CryptoManager.new
+    Redwood::UndoManager.new
   end
 
   def finish
@@ -279,6 +280,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 070d1bac5cf918a653dfdf1729170773150ca8fc..469ed0b91874342a8fde6787008aa625b9d4e576 100644 (file)
@@ -15,37 +15,37 @@ class Colormap
 
   DEFAULT_COLORS = {
     :status => { :fg => "white", :bg => "blue", :attrs => ["bold"] },
-    :index_old => { :fg => "white", :bg => "black" },
-    :index_new => { :fg => "white", :bg => "black", :attrs => ["bold"] },
-    :index_starred => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
-    :index_draft => { :fg => "red", :bg => "black", :attrs => ["bold"] },
-    :labellist_old => { :fg => "white", :bg => "black" },
-    :labellist_new => { :fg => "white", :bg => "black", :attrs => ["bold"] },
-    :twiddle => { :fg => "blue", :bg => "black" },
-    :label => { :fg => "yellow", :bg => "black" },
+    :index_old => { :fg => "white", :bg => "default" },
+    :index_new => { :fg => "white", :bg => "default", :attrs => ["bold"] },
+    :index_starred => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
+    :index_draft => { :fg => "red", :bg => "default", :attrs => ["bold"] },
+    :labellist_old => { :fg => "white", :bg => "default" },
+    :labellist_new => { :fg => "white", :bg => "default", :attrs => ["bold"] },
+    :twiddle => { :fg => "blue", :bg => "default" },
+    :label => { :fg => "yellow", :bg => "default" },
     :message_patina => { :fg => "black", :bg => "green" },
     :alternate_patina => { :fg => "black", :bg => "blue" },
     :missing_message => { :fg => "black", :bg => "red" },
-    :attachment => { :fg => "cyan", :bg => "black" },
-    :cryptosig_valid => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
-    :cryptosig_unknown => { :fg => "cyan", :bg => "black" },
+    :attachment => { :fg => "cyan", :bg => "default" },
+    :cryptosig_valid => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
+    :cryptosig_unknown => { :fg => "cyan", :bg => "default" },
     :cryptosig_invalid => { :fg => "yellow", :bg => "red", :attrs => ["bold"] },
-    :generic_notice_patina => { :fg => "cyan", :bg => "black" },
-    :quote_patina => { :fg => "yellow", :bg => "black" },
-    :sig_patina => { :fg => "yellow", :bg => "black" },
-    :quote => { :fg => "yellow", :bg => "black" },
-    :sig => { :fg => "yellow", :bg => "black" },
-    :to_me => { :fg => "green", :bg => "black" },
-    :starred => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
+    :generic_notice_patina => { :fg => "cyan", :bg => "default" },
+    :quote_patina => { :fg => "yellow", :bg => "default" },
+    :sig_patina => { :fg => "yellow", :bg => "default" },
+    :quote => { :fg => "yellow", :bg => "default" },
+    :sig => { :fg => "yellow", :bg => "default" },
+    :to_me => { :fg => "green", :bg => "default" },
+    :starred => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
     :starred_patina => { :fg => "yellow", :bg => "green", :attrs => ["bold"] },
     :alternate_starred_patina => { :fg => "yellow", :bg => "blue", :attrs => ["bold"] },
-    :snippet => { :fg => "cyan", :bg => "black" },
-    :option => { :fg => "white", :bg => "black" },
-    :tagged => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
-    :draft_notification => { :fg => "red", :bg => "black", :attrs => ["bold"] },
-    :completion_character => { :fg => "white", :bg => "black", :attrs => ["bold"] },
-    :horizontal_selector_selected => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
-    :horizontal_selector_unselected => { :fg => "cyan", :bg => "black" },
+    :snippet => { :fg => "cyan", :bg => "default" },
+    :option => { :fg => "white", :bg => "default" },
+    :tagged => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
+    :draft_notification => { :fg => "red", :bg => "default", :attrs => ["bold"] },
+    :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"] }
   }
   
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 73144db9ea9e03e04ba545b6051d524808020606..610828e62fa1fd705d570b0ae417b388739d1900 100644 (file)
@@ -44,6 +44,7 @@ EOS
     k.add :tag_matching, "Tag matching threads", 'g'
     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={}
@@ -68,6 +69,8 @@ EOS
 
     UpdateManager.register self
 
+    @save_thread_mutex = Mutex.new
+
     @last_load_more_size = nil
     to_load_more do |size|
       next if @last_load_more_size == 0
@@ -83,6 +86,7 @@ EOS
 
   def reload
     drop_all_threads
+    UndoManager.clear
     BufferManager.draw_screen
     load_threads :num => buffer.content_height
   end
@@ -208,6 +212,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
@@ -231,65 +239,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
 
@@ -350,10 +421,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
 
@@ -364,10 +434,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
 
@@ -376,24 +445,41 @@ 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
 
-  def save
-    BufferManager.say("Saving contacts...") { ContactManager.instance.save }
-    dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
-    return if dirty_threads.empty?
+  def save background=true
+    if background
+      Redwood::reporting_thread("saving thread") { actually_save }
+    else
+      actually_save
+    end
+  end
+
+  def actually_save
+    @save_thread_mutex.synchronize do
+      BufferManager.say("Saving contacts...") { ContactManager.instance.save }
+      dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
+      next if dirty_threads.empty?
 
-    BufferManager.say("Saving threads...") do |say_id|
-      dirty_threads.each_with_index do |t, i|
-        BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
-        t.save Index
+      BufferManager.say("Saving threads...") do |say_id|
+        dirty_threads.each_with_index do |t, i|
+          BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
+          t.save Index
+        end
       end
     end
   end
@@ -407,7 +493,7 @@ EOS
       sleep 0.1 # TODO: necessary?
       BufferManager.erase_flash
     end
-    save
+    save false
     super
   end
 
@@ -436,6 +522,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
@@ -444,17 +534,44 @@ 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
 
   def multi_edit_labels threads
-    user_labels = BufferManager.ask_for_labels :add_labels, "Add labels: ", [], @hidden_labels
+    user_labels = BufferManager.ask_for_labels :labels, "Add/remove labels (use -label to remove): ", [], @hidden_labels
     return unless user_labels
-    
-    hl = user_labels.select { |l| @hidden_labels.member? l }
+
+    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 { |t| user_labels.each { |l| t.apply_label l } }
-      user_labels.each { |l| LabelManager << l }
+      undo = threads.map do |t|
+        old_labels = t.labels
+        user_labels.each do |(l, to_remove)|
+          if to_remove
+            t.remove_label l
+          else
+            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
index 87cf9963796286f73a92f87478c207e23300cfa0..d766587f58815f27ec07b485c07ca6ec5204542a 100644 (file)
@@ -97,7 +97,7 @@ EOS
         numi = 0
         add_messages_from source do |m, offset, entry|
           ## always preserve the labels on disk.
-          m.labels = entry[:label].split(/\s+/).map { |x| x.intern } if entry
+          m.labels = ((m.labels - [:unread, :inbox]) + entry[:label].split(/\s+/).map { |x| x.intern }).uniq if entry
           yield "Found message at #{offset} with labels {#{m.labels * ', '}}"
           unless entry
             num += 1
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
index fc625328b2afb63310b4fa39ed196217d4eeae0b..dbcffcc5951356dec2c533393ba100b4071b8eac 100644 (file)
@@ -620,11 +620,12 @@ end
 
 class Iconv
   def self.easy_decode target, charset, text
-    return text if charset =~ /^(x-unknown|unknown[-_]?8bit|ascii[-_]?7[-_]?bit)$/i
+    return text if charset =~ /^(x-unknown|unknown[-_ ]?8bit|ascii[-_ ]?7[-_ ]?bit)$/i
     charset = case charset
-                when /UTF[-_]?8/i: "utf-8"
-                when /(iso[-_])?latin[-_]?1$/i: "ISO-8859-1"
-                when /unicode[-_]1[-_]1[-_]utf[-_]7/i: "utf-7"
+                when /UTF[-_ ]?8/i: "utf-8"
+                when /(iso[-_ ])?latin[-_ ]?1$/i: "ISO-8859-1"
+                when /iso[-_ ]?8859[-_ ]?15/i: 'ISO-8859-15'
+                when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i: "utf-7"
                 else charset
               end