lib/sup/modes/thread-index-mode.rb in sup-0.7 vs lib/sup/modes/thread-index-mode.rb in sup-0.8

- old
+ new

@@ -40,12 +40,13 @@ k.add :reply, "Reply to latest message in a thread", 'r' k.add :forward, "Forward latest message in a thread", 'f' 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={} super() @mutex = Mutex.new # covers the following variables: @@ -66,25 +67,29 @@ initialize_threads # defines @ts and @ts_mutex update # defines @text and @lines 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 load_threads :num => 1, :background => false load_threads :num => (size - 1), :when_done => lambda { |num| @last_load_more_size = num } 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 ## open up a thread view window @@ -206,16 +211,20 @@ def handle_undeleted_update sender, m add_or_unhide m end + def undo + UndoManager.undo + end + def update @mutex.synchronize do ## let's see you do THIS in python @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| [t.date, t.first.id] }.reverse @size_widgets = @threads.map { |t| size_widget_for_thread t } - @size_widget_width = @size_widgets.max_of { |w| w.length } + @size_widget_width = @size_widgets.max_of { |w| w.display_length } end regen_text end @@ -228,70 +237,126 @@ else BufferManager.flash "Not a draft message!" end end + ## returns an undo lambda def actually_toggle_starred t + pos = curpos if t.has_label? :starred # if ANY message has a star t.remove_label :starred # remove from all UpdateManager.relay self, :unstarred, t.first + lambda do + t.first.add_label :starred + UpdateManager.relay self, :starred, t.first + regen_text + end else t.first.add_label :starred # add only to first UpdateManager.relay self, :starred, t.first + lambda do + t.remove_label :starred + UpdateManager.relay self, :unstarred, t.first + regen_text + end end end def toggle_starred t = cursor_thread or return - actually_toggle_starred t + undo = actually_toggle_starred t + UndoManager.register "toggling thread starred status", undo update_text_for_line curpos cursor_down end def multi_toggle_starred threads - threads.each { |t| actually_toggle_starred t } + UndoManager.register "toggling #{threads.size.pluralize 'thread'} starred status", + threads.map { |t| actually_toggle_starred t } regen_text end + ## returns an undo lambda def actually_toggle_archived t + thread = t + pos = curpos if t.has_label? :inbox t.remove_label :inbox UpdateManager.relay self, :archived, t.first + lambda do + thread.apply_label :inbox + update_text_for_line pos + UpdateManager.relay self,:unarchived, thread.first + end else t.apply_label :inbox UpdateManager.relay self, :unarchived, t.first + lambda do + thread.remove_label :inbox + update_text_for_line pos + UpdateManager.relay self, :unarchived, thread.first + end end end + ## returns an undo lambda def actually_toggle_spammed t + thread = t if t.has_label? :spam t.remove_label :spam + add_or_unhide t.first UpdateManager.relay self, :unspammed, t.first + lambda do + thread.apply_label :spam + self.hide_thread thread + UpdateManager.relay self,:spammed, thread.first + end else t.apply_label :spam + hide_thread t UpdateManager.relay self, :spammed, t.first + lambda do + thread.remove_label :spam + add_or_unhide thread.first + UpdateManager.relay self,:unspammed, thread.first + end end end + ## returns an undo lambda def actually_toggle_deleted t if t.has_label? :deleted t.remove_label :deleted + add_or_unhide t.first UpdateManager.relay self, :undeleted, t.first + lambda do + t.apply_label :deleted + hide_thread t + UpdateManager.relay self, :deleted, t.first + end else t.apply_label :deleted + hide_thread t UpdateManager.relay self, :deleted, t.first + lambda do + t.remove_label :deleted + add_or_unhide t.first + UpdateManager.relay self, :undeleted, t.first + end end end def toggle_archived t = cursor_thread or return - actually_toggle_archived t + undo = actually_toggle_archived t + UndoManager.register "deleting/undeleting thread #{t.first.id}", undo, lambda { update_text_for_line curpos } update_text_for_line curpos end def multi_toggle_archived threads - threads.each { |t| actually_toggle_archived t } + undos = threads.map { |t| actually_toggle_archived t } + UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", undos, lambda { regen_text } regen_text end def toggle_new t = cursor_thread or return @@ -348,54 +413,72 @@ ## label-search-results-mode, when you mark a message as spam or ## deleted, you want it to disappear immediately; in LSRM, you only ## 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 + undos = threads.map { |t| actually_toggle_spammed t } + UndoManager.register "marking/unmarking #{threads.size.pluralize 'thread'} as spam", + undos, lambda { regen_text } regen_text end def toggle_deleted t = cursor_thread or return multi_toggle_deleted [t] end ## see comment for multi_toggle_spam def multi_toggle_deleted threads - threads.each do |t| - actually_toggle_deleted t - hide_thread t - end + undos = threads.map { |t| actually_toggle_deleted t } + UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", + undos, lambda { regen_text } regen_text end def kill t = cursor_thread or return multi_kill [t] end + ## m-m-m-m-MULTI-KILL def multi_kill threads + UndoManager.register "killing #{threads.size.pluralize 'thread'}" do + threads.each do |t| + t.remove_label :killed + add_or_unhide t.first + end + regen_text + end + threads.each do |t| t.apply_label :killed hide_thread t end + regen_text - BufferManager.flash "#{threads.size.pluralize 'Thread'} killed." + 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 - 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 + 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 + end end end end def cleanup @@ -405,11 +488,11 @@ @load_thread.kill BufferManager.clear @mbid if @mbid sleep 0.1 # TODO: necessary? BufferManager.erase_flash end - save + save false super end def toggle_tagged t = cursor_thread or return @@ -422,45 +505,82 @@ @mutex.synchronize { @threads.each { |t| @tags.toggle_tag_for t } } regen_text end def tag_matching - query = BufferManager.ask :search, "tag threads matching: " + query = BufferManager.ask :search, "tag threads matching (regex): " return if query.nil? || query.empty? - query = /#{query}/i + query = begin + /#{query}/i + rescue RegexpError => e + BufferManager.flash "error interpreting '#{query}': #{e.message}" + return + end @mutex.synchronize { @threads.each { |t| @tags.tag t if thread_matches?(t, query) } } regen_text end def apply_to_tagged; @tags.apply_to_tagged; 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 return unless user_labels thread.labels = keepl + user_labels user_labels.each { |l| LabelManager << l } update_text_for_line curpos + + UndoManager.register "labeling thread" do + thread.labels = old_labels + update_text_for_line pos + UpdateManager.relay self, :labeled, thread.first + end + 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 } - if hl.empty? - threads.each { |t| user_labels.each { |l| t.apply_label l } } - user_labels.each { |l| LabelManager << l } - else + + 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 } + unless hl.empty? BufferManager.flash "'#{hl}' is a reserved label!" + return end + + old_labels = threads.map { |t| t.labels.dup } + + threads.each do |t| + user_labels.each do |(l, to_remove)| + if to_remove + t.remove_label l + else + t.apply_label l + LabelManager << l + end + end + end + regen_text + + UndoManager.register "labeling #{threads.size.pluralize 'thread'}" do + threads.zip(old_labels).map do |t, old_labels| + t.labels = old_labels + UpdateManager.relay self, :labeled, t.first + end + regen_text + end end def reply t = cursor_thread or return m = t.latest_message @@ -660,34 +780,34 @@ def text_for_thread_at line t, size_widget = @mutex.synchronize { [@threads[line], @size_widgets[line]] } date = t.date.to_nice_s - starred = t.has_label?(:starred) + starred = t.has_label? :starred ## format the from column cur_width = 0 ann = author_names_and_newness_for_thread t from = [] ann.each_with_index do |(name, newness), i| break if cur_width >= from_width last = i == ann.length - 1 abbrev = - if cur_width + name.length > from_width + if cur_width + name.display_length > from_width name[0 ... (from_width - cur_width - 1)] + "." - elsif cur_width + name.length == from_width + elsif cur_width + name.display_length == from_width name[0 ... (from_width - cur_width)] else if last name[0 ... (from_width - cur_width)] else name[0 ... (from_width - cur_width - 1)] + "," end end - cur_width += abbrev.length + cur_width += abbrev.display_length if last && from_width > cur_width abbrev += " " * (from_width - cur_width) end @@ -725,10 +845,9 @@ [subj_color, t.subj + (t.subj.empty? ? "" : " ")], ] + (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 private