lib/sup/tagger.rb
lib/sup/textfield.rb
lib/sup/thread.rb
+lib/sup/undo.rb
lib/sup/update.rb
lib/sup/util.rb
- 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:
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'
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
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
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?
--- /dev/null
+#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
+
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
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
inbox, and you can browse the mailing list traffic at any point by
searching for that label.
-
Appendix C: Reading blogs with Sup
----------------------------------
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
Redwood::PollManager.new
Redwood::SuicideManager.new Redwood::SUICIDE_FN
Redwood::CryptoManager.new
+ Redwood::UndoManager.new
end
def finish
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"
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={}
@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
@mode.draw
draw_status status
commit
+ @atime = Time.now
end
## s nil means a blank line!
## 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
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
: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
## 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 ]
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
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
end
def killable?; true; end
+ def unsaved?; false end
def draw; end
def focus; end
def blur; end
def focus
reload # buffers may have been killed or created since last view
+ set_cursor_pos 0
end
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
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
!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
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
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
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
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 }
super :header => header, :body => body, :have_signature => true
end
+ def unsaved?; !@safe end
+
def killable?
return true if @safe
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={}
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
add_or_unhide m
end
+ def undo
+ UndoManager.undo
+ end
+
def update
@mutex.synchronize do
## let's see you do THIS in python
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
## 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
## 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
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 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
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
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
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
(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
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
--- /dev/null
+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