commit 8818a4e2e8a4bf95df1d5d7ed5689bb5a34abb48
parent f09b4815984924d8d5a9b4c7d694994c94a3488f
Author: Whyme Lyu <callme5long@gmail.com>
Date: Mon, 20 May 2013 07:14:21 -0700
Merge pull request #57 from 5long/develop
More Cleanups
Diffstat:
33 files changed, 997 insertions(+), 1263 deletions(-)
diff --git a/doc/NewUserGuide.txt b/doc/NewUserGuide.txt
@@ -1,258 +0,0 @@
-Welcome to Sup! Here's how to get started.
-
-First, try running `sup`. Since this is your first time, you'll be
-confronted with a mostly blank screen, and a notice at the bottom that
-you have no new messages. That's because Sup doesn't hasn't loaded
-anything into its index yet, and has no idea where to look for them
-anyways.
-
-If you want to play around a little at this point, you can press 'b'
-to cycle between buffers, ';' to get a list of the open buffers, and
-'x' to kill a buffer. There's probably not too much interesting there,
-but there's a log buffer with some cryptic messages. You can also
-press '?' at any point to get a list of keyboard commands, but in the
-absence of any email, these will be mostly useless. When you get
-bored, press 'q' to quit.
-
-To use Sup for email, we need to load messages into the index. The
-index is where Sup stores all message state (e.g. read or unread, any
-message labels), and all information necessary for searching and for
-threading messages. Sup only knows about messages in its index.
-
-We can add messages to the index by telling Sup about the "source"
-where the messages reside. Sources are things like mbox folders, and
-maildir directories. Sup doesn't duplicate the actual message content
-in the index; it only stores whatever information is necessary for
-searching, threading and labelling. So when you search for messages or
-view your inbox, Sup talks only to the index (stored locally on
-disk). When you view a thread, Sup requests the full content of all
-the messages from the source.
-
-The easiest way to set up all your sources is to run `sup-config`.
-This will interactively walk you through some basic configuration,
-prompt you for all the sources you need, and optionally import
-messages from them. Sup-config uses two other tools, sup-add and
-sup-sync, to load messages into the index. In the future you may make
-use of these tools directly (see below).
-
-Once you've run sup-config, you're ready to run `sup`. You should see
-the most recent unarchived messages appear in your inbox.
-Congratulations, you've got Sup working!
-
-If you're coming from the world of traditional MUAs, there are a
-couple differences you should be aware of at this point. First, Sup
-has no folders. Instead, you organize and find messages by a
-combination of search and labels (known as "tags" everywhere else in
-the world). Search and labels are an integral part of Sup because in
-Sup, rather than viewing the contents of a folder, you view the
-results of a search. I mentioned above that your inbox is, by
-definition, the set of all messages that aren't archived. This means
-that your inbox is nothing more than the result of the search for all
-messages with the label "inbox". (It's actually slightly more
-complicated---we also omit messages marked as killed, deleted or
-spam.)
-
-You could replicate the folder paradigm easily under this scheme, by
-giving each message exactly one label and only viewing the results of
-simple searches for those labels. But you'd quickly find out that life
-can be easier than that if you just trust the search engine, and use
-labels judiciously for things that are too hard to find with search.
-The idea is that a labeling system that allows arbitrary, user-defined
-labels, supplemented by a quick and easy-to-access search mechanism
-provides all the functionality that folders does, plus much more, at a
-far lower cost to the user.
-
-Now let's take a look at your inbox. You'll see that Sup groups
-messages together into threads: each line in the inbox is a thread,
-and the number in parentheses is the number of messages in that
-thread. (If there's no number, there's just one message in the
-thread.) In Sup, most operations are on threads, not individual
-messages. The idea is that you rarely want to operate on a message
-independent of its context. You typically want to view, archive, kill,
-or label all the messages in a thread at one time.
-
-Use the up and down arrows to highlight a thread. ('j' and 'k' do the
-same thing, and 'J' and 'K' will scroll the whole window. Even the
-left and right arrow keys work.) By default, Sup only loads as many
-threads as it takes to fill the window; if you'd like to load more,
-press 'M'. You can hit tab to cycle between only threads with new
-messages.
-
-Highlight a thread and press enter to view it. You'll notice that all
-messages in the thread are displayed together, laid out graphically by
-their relationship to each other (replies are nested under parents).
-By default, only the new messages in a thread are expanded, and the
-others are hidden. You can toggle an individual message's state by
-highlighting a green line and pressing enter. You can use 'E' to
-expand or collapse all messages or 'N' to expand only the new
-messages. You'll also notice that Sup hides quoted text and
-signatures. If you highlight a particular hidden chunk, you can press
-enter to expand it, or you can press 'o' to toggle every hidden chunk
-in a particular message.
-
-Other useful keyboard commands when viewing a thread are: 'n' and 'p'
-to jump to the next and previous open messages, 'h' to toggle the
-detailed headers for the current message, and enter to expand or
-collapse the current message (when it's on a text region). Enter and
-'n' in combination are useful for scanning through a thread---press
-enter to close the current message and jump to the next open one, and
-'n' to keep it open and jump. If the buffer is misaligned with a
-message, you can press 'z' to highlight it.
-
-This is a lot to remember, but you can always hit '?' to see the full
-list of keyboard commands at any point. There's a lot of useful stuff
-in there---once you learn some, try out some of the others!
-
-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 ';' 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
-your inbox, but will still appear in search results. If someone
-replies an archived thread, it will reappear in your inbox. To kill a
-thread, press '&'. Killed threads will never come back to your inbox,
-even if people reply, but will still be searchable. (This is useful
-for those interminable threads that you really have no immediate
-interest in, but which seem to pop up on every mailing list.)
-
-If a thread is spam, press 'S'. It will disappear and won't come back.
-It won't even appear in search results, unless you explicitly search
-for spam.
-
-You can star a thread by pressing '*'. Starred threads are displayed
-with a little yellow asterisk next to them, but otherwise have no
-special semantics. But you can also search for them easily---we'll see
-how in a moment.
-
-To edit the labels for (all the messages in) a thread, press 'l'. Type
-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
-messages.
-
-Ok, let's try using labels and search. Press 'L' to do a quick label
-search. You'll be prompted for a label; simply hit enter to bring up
-scrollable list of all the labels you've ever used, along with some
-special labels (Draft, Starred, Sent, Spam, etc.). Highlight a label
-and press enter to view all the messages with that label.
-
-What you just did was actually a specific search. For a general search,
-press '\' (backslash---forward slash is used for in-buffer search,
-following console conventions). Now type in your query (again, Ctrl-G to
-cancel at any point.) You can just type in arbitrary text, which will be
-matched on a per-word basis against the bodies of all email in the
-index, or you can make use of the full Xapian query syntax
-(http://xapian.org/docs/queryparser.html):
-
-- Phrasal queries using double-quotes, e.g.: "three contiguous words"
-- Queries against a particular field using <field name>:<query>,
- e.g.: label:ruby-talk, or from:matz@ruby-lang.org. (Fields include:
- body, from, to, and subject.)
-- Force non-occurrence by -, e.g. -body:"hot soup".
-- If you have the chronic gem installed, date queries like
- "before:today", "on:today", "after:yesterday", "after:(2 days ago)"
- (parentheses required for multi-word descriptions).
-
-You can combine those all together. For example:
-
- label:ruby-talk subject:\[ANN\] -rails on:today
-
-Play around with the search, and see the Xapian documentation for
-details on more sophisticated queries (date ranges, "within n words",
-etc.)
-
-At this point, you're well on your way to figuring out all the cool
-things Sup can do. By repeated application of the '?' key, see if you
-can figure out how to:
-
- - List some recent contacts
- - Easily search for all mail from a recent contact
- - Easily search for all mail from several recent contacts
- - Add someone to your address book
- - Postpone a message (i.e., save a draft)
- - Quickly re-edit a just-saved draft message
- - View the raw header of a message
- - Star an individual message, not just a thread
-
-There's one last thing to be aware of when using Sup: how it interacts
-with other email programs. As I described above, Sup stores data about
-messages in the index, but doesn't duplicate the message contents
-themselves. The messages remain on the source. If the index and the
-source every fall out of sync, e.g. due to another email client
-modifying the source, then Sup will be unable to operate on that
-source. For example, for mbox files, Sup stores a byte offset into the
-file for each message. If a message deleted from that file by another
-client, or even marked as read (yeah, mbox sucks), all succeeding
-offsets will be wrong.
-
-That's the bad news. The good news is that Sup is pretty good at being
-able to detect this type of situation, and fixing it is just a matter
-of running `sup-sync --changed` on the source. Sup will even tell you
-how to invoke sup-sync when it detects a problem. This is a
-complication you will almost certainly run in to if you use both Sup
-and another MUA on the same source, so it's good to be aware of it.
-
-Have fun, and email sup-talk@rubyforge.org if you have any problems!
-
-Appendix A: sup-add and sup-sync
----------------------------------
-
-Instead of using sup-config to add a new source, you can manually run
-`sup-add` with a URI pointing to it. The URI should be of the form:
-
-- mbox://path/to/a/filename, for an mbox file on disk.
-- maildir://path/to/a/filename, for a maildir directory on disk.
-
-Before you add the source, you need make three decisions. The first is
-whether you want Sup to regularly poll this source for new messages.
-By default it will, but if this is a source that will never have new
-messages, you can specify `--unusual`. Sup polls only "usual" sources
-when checking for new mail (unless you manually invoke sup-sync).
-
-The second is whether you want messages from the source to be
-automatically archived. An archived message will not show up in your
-inbox, but will be found when you search. (Your inbox in Sup is, by
-definition, the set of all all non-archived messages). Specify
-`--archive` to automatically archive all messages from the source. This
-is useful for sources that contain, for example, high-traffic mailing
-lists that you don't want polluting your inbox.
-
-The final decision is whether you want any labels automatically
-applied to messages from this source. You can use `--labels` to do this.
-
-Now that you've added the source, let's import all the current
-messages from it, by running sup-sync with the source URI. You can
-specify `--archive` to automatically archive all messages in this
-import; typically you'll want to specify this for every source you
-import except your actual inbox. You can also specify `--read` to mark
-all imported messages as read; the default is to preserve the
-read/unread status from the source.
-
-Sup-sync will now load all the messages from the source into the
-index. Depending on the size of the source, this may take a while.
-Don't panic! It's a one-time process.
-
-Appendix B: Automatically labeling incoming email
--------------------------------------------------
-
-One option is to filter incoming email into different sources with
-something like procmail, and have each of these sources auto-apply
-labels by using `sup-add --labels`.
-
-But the better option is to learn Ruby and write a before-add hook.
-This will allow you to apply labels based on whatever crazy logic you
-can come up with. See http://sup.rubyforge.org/wiki/wiki.pl?Hooks for
-examples.
-
-Appendix C: Reading blogs with Sup
-----------------------------------
-
-Really, blog posts should be read like emails are read---you should be
-able to mark them as unread, flag them, label them, etc. Use rss2email
-to transform RSS feeds into emails, direct them all into a source, and
-add that source to Sup. Voila!
diff --git a/lib/sup.rb b/lib/sup.rb
@@ -326,11 +326,11 @@ end
require "sup/buffer"
require "sup/keymap"
require "sup/mode"
-require "sup/modes/scroll-mode"
-require "sup/modes/text-mode"
-require "sup/modes/log-mode"
+require "sup/modes/scroll_mode"
+require "sup/modes/text_mode"
+require "sup/modes/log_mode"
require "sup/update"
-require "sup/message-chunks"
+require "sup/message_chunks"
require "sup/message"
require "sup/source"
require "sup/mbox"
@@ -338,7 +338,7 @@ require "sup/maildir"
require "sup/person"
require "sup/account"
require "sup/thread"
-require "sup/interactive-lock"
+require "sup/interactive_lock"
require "sup/index"
require "sup/textfield"
require "sup/colormap"
@@ -349,31 +349,31 @@ 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"
-require "sup/modes/edit-message-mode"
-require "sup/modes/edit-message-async-mode"
-require "sup/modes/compose-mode"
-require "sup/modes/resume-mode"
-require "sup/modes/forward-mode"
-require "sup/modes/reply-mode"
-require "sup/modes/label-list-mode"
-require "sup/modes/contact-list-mode"
-require "sup/modes/thread-view-mode"
-require "sup/modes/thread-index-mode"
-require "sup/modes/label-search-results-mode"
-require "sup/modes/search-results-mode"
-require "sup/modes/person-search-results-mode"
-require "sup/modes/inbox-mode"
-require "sup/modes/buffer-list-mode"
-require "sup/modes/poll-mode"
-require "sup/modes/file-browser-mode"
-require "sup/modes/completion-mode"
-require "sup/modes/console-mode"
+require "sup/horizontal_selector"
+require "sup/modes/line_cursor_mode"
+require "sup/modes/help_mode"
+require "sup/modes/edit_message_mode"
+require "sup/modes/edit_message_async_mode"
+require "sup/modes/compose_mode"
+require "sup/modes/resume_mode"
+require "sup/modes/forward_mode"
+require "sup/modes/reply_mode"
+require "sup/modes/label_list_mode"
+require "sup/modes/contact_list_mode"
+require "sup/modes/thread_view_mode"
+require "sup/modes/thread_index_mode"
+require "sup/modes/label_search_results_mode"
+require "sup/modes/search_results_mode"
+require "sup/modes/person_search_results_mode"
+require "sup/modes/inbox_mode"
+require "sup/modes/buffer_list_mode"
+require "sup/modes/poll_mode"
+require "sup/modes/file_browser_mode"
+require "sup/modes/completion_mode"
+require "sup/modes/console_mode"
require "sup/sent"
require "sup/search"
-require "sup/modes/search-list-mode"
+require "sup/modes/search_list_mode"
require "sup/idle"
$:.each do |base|
diff --git a/lib/sup/horizontal-selector.rb b/lib/sup/horizontal_selector.rb
diff --git a/lib/sup/index.rb b/lib/sup/index.rb
@@ -4,14 +4,7 @@ require 'xapian'
require 'set'
require 'fileutils'
require 'monitor'
-
-begin
- require 'chronic'
- $have_chronic = true
-rescue LoadError => e
- debug "No 'chronic' gem detected. Install it for date/time query restrictions."
- $have_chronic = false
-end
+require 'chronic'
if ([Xapian.major_version, Xapian.minor_version, Xapian.revision] <=> [1,2,1]) < 0
fail "Xapian version 1.2.1 or higher required"
@@ -388,27 +381,25 @@ EOS
end
end
- if $have_chronic
- lastdate = 2<<32 - 1
- firstdate = 0
- subs = subs.gsub(/\b(before|on|in|during|after):(\((.+?)\)\B|(\S+)\b)/) do
- field, datestr = $1, ($3 || $4)
- realdate = Chronic.parse datestr, :guess => false, :context => :past
- if realdate
- case field
- when "after"
- debug "chronic: translated #{field}:#{datestr} to #{realdate.end}"
- "date:#{realdate.end.to_i}..#{lastdate}"
- when "before"
- debug "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
- "date:#{firstdate}..#{realdate.end.to_i}"
- else
- debug "chronic: translated #{field}:#{datestr} to #{realdate}"
- "date:#{realdate.begin.to_i}..#{realdate.end.to_i}"
- end
+ lastdate = 2<<32 - 1
+ firstdate = 0
+ subs = subs.gsub(/\b(before|on|in|during|after):(\((.+?)\)\B|(\S+)\b)/) do
+ field, datestr = $1, ($3 || $4)
+ realdate = Chronic.parse datestr, :guess => false, :context => :past
+ if realdate
+ case field
+ when "after"
+ debug "chronic: translated #{field}:#{datestr} to #{realdate.end}"
+ "date:#{realdate.end.to_i}..#{lastdate}"
+ when "before"
+ debug "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
+ "date:#{firstdate}..#{realdate.end.to_i}"
else
- raise ParseError, "can't understand date #{datestr.inspect}"
+ debug "chronic: translated #{field}:#{datestr} to #{realdate}"
+ "date:#{realdate.begin.to_i}..#{realdate.end.to_i}"
end
+ else
+ raise ParseError, "can't understand date #{datestr.inspect}"
end
end
diff --git a/lib/sup/interactive-lock.rb b/lib/sup/interactive_lock.rb
diff --git a/lib/sup/message-chunks.rb b/lib/sup/message_chunks.rb
diff --git a/lib/sup/modes/buffer-list-mode.rb b/lib/sup/modes/buffer_list_mode.rb
diff --git a/lib/sup/modes/completion-mode.rb b/lib/sup/modes/completion_mode.rb
diff --git a/lib/sup/modes/compose-mode.rb b/lib/sup/modes/compose_mode.rb
diff --git a/lib/sup/modes/console-mode.rb b/lib/sup/modes/console_mode.rb
diff --git a/lib/sup/modes/contact-list-mode.rb b/lib/sup/modes/contact_list_mode.rb
diff --git a/lib/sup/modes/edit-message-async-mode.rb b/lib/sup/modes/edit_message_async_mode.rb
diff --git a/lib/sup/modes/edit-message-mode.rb b/lib/sup/modes/edit_message_mode.rb
diff --git a/lib/sup/modes/file-browser-mode.rb b/lib/sup/modes/file_browser_mode.rb
diff --git a/lib/sup/modes/forward-mode.rb b/lib/sup/modes/forward_mode.rb
diff --git a/lib/sup/modes/help-mode.rb b/lib/sup/modes/help_mode.rb
diff --git a/lib/sup/modes/inbox-mode.rb b/lib/sup/modes/inbox_mode.rb
diff --git a/lib/sup/modes/label-list-mode.rb b/lib/sup/modes/label_list_mode.rb
diff --git a/lib/sup/modes/label-search-results-mode.rb b/lib/sup/modes/label_search_results_mode.rb
diff --git a/lib/sup/modes/line-cursor-mode.rb b/lib/sup/modes/line_cursor_mode.rb
diff --git a/lib/sup/modes/log-mode.rb b/lib/sup/modes/log_mode.rb
diff --git a/lib/sup/modes/person-search-results-mode.rb b/lib/sup/modes/person_search_results_mode.rb
diff --git a/lib/sup/modes/poll-mode.rb b/lib/sup/modes/poll_mode.rb
diff --git a/lib/sup/modes/reply-mode.rb b/lib/sup/modes/reply_mode.rb
diff --git a/lib/sup/modes/resume-mode.rb b/lib/sup/modes/resume_mode.rb
diff --git a/lib/sup/modes/scroll-mode.rb b/lib/sup/modes/scroll_mode.rb
diff --git a/lib/sup/modes/search-list-mode.rb b/lib/sup/modes/search_list_mode.rb
diff --git a/lib/sup/modes/search-results-mode.rb b/lib/sup/modes/search_results_mode.rb
diff --git a/lib/sup/modes/text-mode.rb b/lib/sup/modes/text_mode.rb
diff --git a/lib/sup/modes/thread-index-mode.rb b/lib/sup/modes/thread-index-mode.rb
@@ -1,950 +0,0 @@
-require 'set'
-
-module Redwood
-
-## subclasses should implement:
-## - is_relevant?
-
-class ThreadIndexMode < LineCursorMode
- DATE_WIDTH = Time::TO_NICE_S_MAX_LEN
- MIN_FROM_WIDTH = 15
- LOAD_MORE_THREAD_NUM = 20
-
- HookManager.register "index-mode-size-widget", <<EOS
-Generates the per-thread size widget for each thread.
-Variables:
- thread: The message thread to be formatted.
-EOS
-
- HookManager.register "index-mode-date-widget", <<EOS
-Generates the per-thread date widget for each thread.
-Variables:
- thread: The message thread to be formatted.
-EOS
-
- HookManager.register "mark-as-spam", <<EOS
-This hook is run when a thread is marked as spam
-Variables:
- thread: The message thread being marked as spam.
-EOS
-
- register_keymap do |k|
- k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
- k.add_multi "Load all threads (! to confirm) :", '!' do |kk|
- kk.add :load_all_threads, "Load all threads (may list a _lot_ of threads)", '!'
- end
- k.add :cancel_search, "Cancel current search", :ctrl_g
- k.add :reload, "Refresh view", '@'
- k.add :toggle_archived, "Toggle archived status", 'a'
- k.add :toggle_starred, "Star or unstar all messages in thread", '*'
- k.add :toggle_new, "Toggle new/read status of all messages in thread", 'N'
- k.add :edit_labels, "Edit or add labels for a thread", 'l'
- k.add :edit_message, "Edit message (drafts only)", 'e'
- k.add :toggle_spam, "Mark/unmark thread as spam", 'S'
- k.add :toggle_deleted, "Delete/undelete thread", 'd'
- k.add :kill, "Kill thread (never to be seen in inbox again)", '&'
- k.add :flush_index, "Flush all changes now", '$'
- k.add :jump_to_next_new, "Jump to next new thread", :tab
- k.add :reply, "Reply to latest message in a thread", 'r'
- k.add :reply_all, "Reply to all participants of the latest message in a thread", 'G'
- 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 :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:
- @threads = []
- @hidden_threads = {}
- @size_widget_width = nil
- @size_widgets = []
- @date_widget_width = nil
- @date_widgets = []
- @tags = Tagger.new self
-
- ## these guys, and @text and @lines, are not covered
- @load_thread = nil
- @load_thread_opts = load_thread_opts
- @hidden_labels = hidden_labels + LabelManager::HIDDEN_RESERVED_LABELS
- @date_width = DATE_WIDTH
-
- @interrupt_search = false
-
- 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 => size,
- :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
- def select t=nil, when_done=nil
- t ||= cursor_thread or return
-
- Redwood::reporting_thread("load messages for thread-view-mode") do
- num = t.size
- message = "Loading #{num.pluralize 'message body'}..."
- BufferManager.say(message) do |sid|
- t.each_with_index do |(m, *o), i|
- next unless m
- BufferManager.say "#{message} (#{i}/#{num})", sid if t.size > 1
- m.load_from_source!
- end
- end
- mode = ThreadViewMode.new t, @hidden_labels, self
- BufferManager.spawn t.subj, mode
- BufferManager.draw_screen
- mode.jump_to_first_open if $config[:jump_to_open_message]
- BufferManager.draw_screen # lame TODO: make this unnecessary
- ## the first draw_screen is needed before topline and botline
- ## are set, and the second to show the cursor having moved
-
- t.remove_label :unread
- Index.save_thread t
-
- update_text_for_line curpos
- UpdateManager.relay self, :read, t.first
- when_done.call if when_done
- end
- end
-
- def multi_select threads
- threads.each { |t| select t }
- end
-
- ## these two methods are called by thread-view-modes when the user
- ## wants to view the previous/next thread without going back to
- ## index-mode. we update the cursor as a convenience.
- def launch_next_thread_after thread, &b
- launch_another_thread thread, 1, &b
- end
-
- def launch_prev_thread_before thread, &b
- launch_another_thread thread, -1, &b
- end
-
- def launch_another_thread thread, direction, &b
- l = @lines[thread] or return
- target_l = l + direction
- t = @mutex.synchronize do
- if target_l >= 0 && target_l < @threads.length
- @threads[target_l]
- end
- end
-
- if t # there's a next thread
- set_cursor_pos target_l # move out of mutex?
- select t, b
- elsif b # no next thread. call the block anyways
- b.call
- end
- end
-
- def handle_single_message_labeled_update sender, m
- ## no need to do anything different here; we don't differentiate
- ## messages from their containing threads
- handle_labeled_update sender, m
- end
-
- def handle_labeled_update sender, m
- if(t = thread_containing(m))
- l = @lines[t] or return
- update_text_for_line l
- elsif is_relevant?(m)
- add_or_unhide m
- end
- end
-
- def handle_simple_update sender, m
- t = thread_containing(m) or return
- l = @lines[t] or return
- update_text_for_line l
- end
-
- %w(read unread archived starred unstarred).each do |state|
- define_method "handle_#{state}_update" do |*a|
- handle_simple_update(*a)
- end
- end
-
- ## overwrite me!
- def is_relevant? m; false; end
-
- def handle_added_update sender, m
- add_or_unhide m
- BufferManager.draw_screen
- end
-
- def handle_single_message_deleted_update sender, m
- @ts_mutex.synchronize do
- return unless @ts.contains? m
- @ts.remove_id m.id
- end
- update
- end
-
- def handle_deleted_update sender, m
- t = @ts_mutex.synchronize { @ts.thread_for m }
- return unless t
- hide_thread t
- update
- end
-
- def handle_spammed_update sender, m
- t = @ts_mutex.synchronize { @ts.thread_for m }
- return unless t
- hide_thread t
- update
- end
-
- def handle_undeleted_update sender, m
- add_or_unhide m
- end
-
- def undo
- UndoManager.undo
- end
-
- def update
- old_cursor_thread = cursor_thread
- @mutex.synchronize do
- ## let's see you do THIS in python
- @threads = @ts.threads.select { |t| !@hidden_threads.member?(t) }.select(&:has_message?).sort_by(&:sort_key)
- @size_widgets = @threads.map { |t| size_widget_for_thread t }
- @size_widget_width = @size_widgets.max_of { |w| w.display_length }
- @date_widgets = @threads.map { |t| date_widget_for_thread t }
- @date_widget_width = @date_widgets.max_of { |w| w.display_length }
- end
- set_cursor_pos @threads.index(old_cursor_thread)||curpos
-
- regen_text
- end
-
- def edit_message
- return unless(t = cursor_thread)
- message, *crap = t.find { |m, *o| m.has_label? :draft }
- if message
- mode = ResumeMode.new message
- BufferManager.spawn "Edit message", mode
- 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
- undo = actually_toggle_starred t
- UndoManager.register "toggling thread starred status", undo, lambda { Index.save_thread t }
- update_text_for_line curpos
- cursor_down
- Index.save_thread t
- end
-
- def multi_toggle_starred threads
- UndoManager.register "toggling #{threads.size.pluralize 'thread'} starred status",
- threads.map { |t| actually_toggle_starred t },
- lambda { threads.each { |t| Index.save_thread t } }
- regen_text
- threads.each { |t| Index.save_thread t }
- 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
- undo = actually_toggle_archived t
- UndoManager.register "deleting/undeleting thread #{t.first.id}", undo, lambda { update_text_for_line curpos },
- lambda { Index.save_thread t }
- update_text_for_line curpos
- Index.save_thread t
- end
-
- def multi_toggle_archived threads
- undos = threads.map { |t| actually_toggle_archived t }
- UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", undos, lambda { regen_text },
- lambda { threads.each { |t| Index.save_thread t } }
- regen_text
- threads.each { |t| Index.save_thread t }
- end
-
- def toggle_new
- t = cursor_thread or return
- t.toggle_label :unread
- update_text_for_line curpos
- cursor_down
- Index.save_thread t
- end
-
- def multi_toggle_new threads
- threads.each { |t| t.toggle_label :unread }
- regen_text
- threads.each { |t| Index.save_thread t }
- end
-
- def multi_toggle_tagged threads
- @mutex.synchronize { @tags.drop_all_tags }
- regen_text
- end
-
- def join_threads
- ## this command has no non-tagged form. as a convenience, allow this
- ## command to be applied to tagged threads without hitting ';'.
- @tags.apply_to_tagged :join_threads
- end
-
- def multi_join_threads threads
- @ts.join_threads threads or return
- threads.each { |t| Index.save_thread t }
- @tags.drop_all_tags # otherwise we have tag pointers to invalid threads!
- update
- end
-
- def jump_to_next_new
- n = @mutex.synchronize do
- ((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread } ||
- (0 ... curpos).find { |i| @threads[i].has_label? :unread }
- end
- if n
- ## jump there if necessary
- jump_to_line n unless n >= topline && n < botline
- set_cursor_pos n
- else
- BufferManager.flash "No new messages."
- end
- end
-
- def toggle_spam
- t = cursor_thread or return
- multi_toggle_spam [t]
- end
-
- ## both spam and deleted have the curious characteristic that you
- ## always want to hide the thread after either applying or removing
- ## that label. in all thread-index-views except for
- ## 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
- undos = threads.map { |t| actually_toggle_spammed t }
- threads.each { |t| HookManager.run("mark-as-spam", :thread => t) }
- UndoManager.register "marking/unmarking #{threads.size.pluralize 'thread'} as spam",
- undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
- regen_text
- threads.each { |t| Index.save_thread t }
- 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
- undos = threads.map { |t| actually_toggle_deleted t }
- UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}",
- undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
- regen_text
- threads.each { |t| Index.save_thread t }
- end
-
- def kill
- t = cursor_thread or return
- multi_kill [t]
- end
-
- def flush_index
- @flush_id = BufferManager.say "Flushing index..."
- Index.save_index
- BufferManager.clear @flush_id
- end
-
- ## m-m-m-m-MULTI-KILL
- def multi_kill threads
- UndoManager.register "killing/unkilling #{threads.size.pluralize 'threads'}" do
- threads.each do |t|
- if t.toggle_label :killed
- add_or_unhide t.first
- else
- hide_thread t
- end
- end.each do |t|
- UpdateManager.relay self, :labeled, t.first
- Index.save_thread t
- end
- regen_text
- end
-
- threads.each do |t|
- if t.toggle_label :killed
- hide_thread t
- else
- add_or_unhide t.first
- end
- end.each do |t|
- # send 'labeled'... this might be more specific
- UpdateManager.relay self, :labeled, t.first
- Index.save_thread t
- end
-
- killed, unkilled = threads.partition { |t| t.has_label? :killed }.map(&:size)
- BufferManager.flash "#{killed.pluralize 'thread'} killed, #{unkilled} unkilled"
- regen_text
- end
-
- def cleanup
- UpdateManager.unregister self
-
- if @load_thread
- @load_thread.kill
- BufferManager.clear @mbid if @mbid
- sleep 0.1 # TODO: necessary?
- BufferManager.erase_flash
- end
- dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
- fail "dirty threads remain" unless dirty_threads.empty?
- super
- end
-
- def toggle_tagged
- t = cursor_thread or return
- @mutex.synchronize { @tags.toggle_tag_for t }
- update_text_for_line curpos
- cursor_down
- end
-
- def toggle_tagged_all
- @mutex.synchronize { @threads.each { |t| @tags.toggle_tag_for t } }
- regen_text
- end
-
- def tag_matching
- query = BufferManager.ask :search, "tag threads matching (regex): "
- return if query.nil? || query.empty?
- 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.sort_by {|x| x.to_s}, @hidden_labels
- return unless user_labels
-
- thread.labels = Set.new(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
- Index.save_thread thread
- end
-
- UpdateManager.relay self, :labeled, thread.first
- Index.save_thread thread
- end
-
- def multi_edit_labels threads
- user_labels = BufferManager.ask_for_labels :labels, "Add/remove labels (use -label to remove): ", [], @hidden_labels
- return unless user_labels
-
- 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
- UpdateManager.relay self, :labeled, t.first
- 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
- Index.save_thread t
- end
- regen_text
- end
-
- threads.each { |t| Index.save_thread t }
- end
-
- def reply type_arg=nil
- t = cursor_thread or return
- m = t.latest_message
- return if m.nil? # probably won't happen
- m.load_from_source!
- mode = ReplyMode.new m, type_arg
- BufferManager.spawn "Reply to #{m.subj}", mode
- end
-
- def reply_all; reply :all; end
-
- def forward
- t = cursor_thread or return
- m = t.latest_message
- return if m.nil? # probably won't happen
- m.load_from_source!
- ForwardMode.spawn_nicely :message => m
- end
-
- def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
- return if @load_thread # todo: wrap in mutex
- @load_thread = Redwood::reporting_thread("load threads for thread-index-mode") do
- num = load_n_threads n, opts
- opts[:when_done].call(num) if opts[:when_done]
- @load_thread = nil
- end
- end
-
- ## TODO: figure out @ts_mutex in this method
- def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
- @interrupt_search = false
- @mbid = BufferManager.say "Searching for threads..."
-
- ts_to_load = n
- ts_to_load = ts_to_load + @ts.size unless n == -1 # -1 means all threads
-
- orig_size = @ts.size
- last_update = Time.now
- @ts.load_n_threads(ts_to_load, opts) do |i|
- if (Time.now - last_update) >= 0.25
- BufferManager.say "Loaded #{i.pluralize 'thread'}...", @mbid
- update
- BufferManager.draw_screen
- last_update = Time.now
- end
- ::Thread.pass
- break if @interrupt_search
- end
- @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
-
- update
- BufferManager.clear @mbid
- @mbid = nil
- BufferManager.draw_screen
- @ts.size - orig_size
- end
- ignore_concurrent_calls :load_n_threads
-
- def status
- if (l = lines) == 0
- "line 0 of 0"
- else
- "line #{curpos + 1} of #{l}"
- end
- end
-
- def cancel_search
- @interrupt_search = true
- end
-
- def load_all_threads
- load_threads :num => -1
- end
-
- def load_threads opts={}
- if opts[:num].nil?
- n = ThreadIndexMode::LOAD_MORE_THREAD_NUM
- else
- n = opts[:num]
- end
-
- myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
- opts[:when_done].call(num) if opts[:when_done]
-
- if num > 0
- BufferManager.flash "Found #{num.pluralize 'thread'}."
- else
- BufferManager.flash "No matches."
- end
- end)})
-
- if opts[:background] || opts[:background].nil?
- load_n_threads_background n, myopts
- else
- load_n_threads n, myopts
- end
- end
- ignore_concurrent_calls :load_threads
-
- def resize rows, cols
- regen_text
- super
- end
-
-protected
-
- def add_or_unhide m
- @ts_mutex.synchronize do
- if (is_relevant?(m) || @ts.is_relevant?(m)) && !@ts.contains?(m)
- @ts.load_thread_for_message m, @load_thread_opts
- end
-
- @hidden_threads.delete @ts.thread_for(m)
- end
-
- update
- end
-
- def thread_containing m; @ts_mutex.synchronize { @ts.thread_for m } end
-
- ## used to tag threads by query. this can be made a lot more sophisticated,
- ## but for right now we'll do the obvious this.
- def thread_matches? t, query
- t.subj =~ query || t.snippet =~ query || t.participants.any? { |x| x.longname =~ query }
- end
-
- def size_widget_for_thread t
- HookManager.run("index-mode-size-widget", :thread => t) || default_size_widget_for(t)
- end
-
- def date_widget_for_thread t
- HookManager.run("index-mode-date-widget", :thread => t) || default_date_widget_for(t)
- end
-
- def cursor_thread; @mutex.synchronize { @threads[curpos] }; end
-
- def drop_all_threads
- @tags.drop_all_tags
- initialize_threads
- update
- end
-
- def hide_thread t
- @mutex.synchronize do
- i = @threads.index(t) or return
- raise "already hidden" if @hidden_threads[t]
- @hidden_threads[t] = true
- @threads.delete_at i
- @size_widgets.delete_at i
- @date_widgets.delete_at i
- @tags.drop_tag_for t
- end
- end
-
- def update_text_for_line l
- return unless l # not sure why this happens, but it does, occasionally
-
- need_update = false
-
- @mutex.synchronize do
- @size_widgets[l] = size_widget_for_thread @threads[l]
- @date_widgets[l] = date_widget_for_thread @threads[l]
-
- ## if a widget size has increased, we need to redraw everyone
- need_update =
- (@size_widgets[l].size > @size_widget_width) or
- (@date_widgets[l].size > @date_widget_width)
- end
-
- if need_update
- update
- else
- @text[l] = text_for_thread_at l
- buffer.mark_dirty if buffer
- end
- end
-
- def regen_text
- threads = @mutex.synchronize { @threads }
- @text = threads.map_with_index { |t, i| text_for_thread_at i }
- @lines = threads.map_with_index { |t, i| [t, i] }.to_h
- buffer.mark_dirty if buffer
- end
-
- def authors; map { |m, *o| m.from if m }.compact.uniq; end
-
- ## preserve author order from the thread
- def author_names_and_newness_for_thread t, limit=nil
- new = {}
- seen = {}
- authors = t.map do |m, *o|
- next unless m && m.from
- new[m.from] ||= m.has_label?(:unread)
- next if seen[m.from]
- seen[m.from] = true
- m.from
- end.compact
-
- result = []
- authors.each do |a|
- break if limit && result.size >= limit
- name = if AccountManager.is_account?(a)
- "me"
- elsif t.authors.size == 1
- a.mediumname
- else
- a.shortname
- end
-
- result << [name, new[a]]
- end
-
- if result.size == 1 && (author_and_newness = result.assoc("me"))
- unless (recipients = t.participants - t.authors).empty?
- result = recipients.collect do |r|
- break if limit && result.size >= limit
- name = (recipients.size == 1) ? r.mediumname : r.shortname
- ["(#{name})", author_and_newness[1]]
- end
- end
- end
-
- result
- end
-
- AUTHOR_LIMIT = 5
- def text_for_thread_at line
- t, size_widget, date_widget = @mutex.synchronize do
- [@threads[line], @size_widgets[line], @date_widgets[line]]
- end
-
- starred = t.has_label? :starred
-
- ## format the from column
- cur_width = 0
- ann = author_names_and_newness_for_thread t, AUTHOR_LIMIT
- 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.display_length > from_width
- name[0 ... (from_width - cur_width - 1)] + "."
- 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.display_length
-
- if last && from_width > cur_width
- abbrev += " " * (from_width - cur_width)
- end
-
- from << [(newness ? :index_new_color : (starred ? :index_starred_color : :index_old_color)), abbrev]
- end
-
- dp = t.direct_participants.any? { |p| AccountManager.is_account? p }
- p = dp || t.participants.any? { |p| AccountManager.is_account? p }
-
- subj_color =
- if t.has_label?(:draft)
- :index_draft_color
- elsif t.has_label?(:unread)
- :index_new_color
- elsif starred
- :index_starred_color
- elsif Colormap.sym_is_defined(:index_subject_color)
- :index_subject_color
- else
- :index_old_color
- end
-
- size_padding = @size_widget_width - size_widget.display_length
- size_widget_text = sprintf "%#{size_padding}s%s", "", size_widget
-
- date_padding = @date_widget_width - date_widget.display_length
- date_widget_text = sprintf "%#{date_padding}s%s", "", date_widget
-
- [
- [:tagged_color, @tags.tagged?(t) ? ">" : " "],
- [:date_color, date_widget_text],
- [:starred_color, (starred ? "*" : " ")],
- ] +
- from +
- [
- [:size_widget_color, size_widget_text],
- [:to_me_color, t.labels.member?(:attachment) ? "@" : " "],
- [:to_me_color, dp ? ">" : (p ? '+' : " ")],
- ] +
- (t.labels - @hidden_labels).sort_by {|x| x.to_s}.map {
- |label| [Colormap.sym_is_defined("label_#{label}_color".to_sym) || :label_color, "#{label} "]
- } +
- [
- [subj_color, t.subj + (t.subj.empty? ? "" : " ")],
- [:snippet_color, t.snippet],
- ]
- end
-
- def dirty?; @mutex.synchronize { (@hidden_threads.keys + @threads).any? { |t| t.dirty? } } end
-
-private
-
- def default_size_widget_for t
- case t.size
- when 1
- ""
- else
- "(#{t.size})"
- end
- end
-
- def default_date_widget_for t
- t.date.getlocal.to_nice_s
- end
-
- def from_width
- [(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
- end
-
- def initialize_threads
- @ts = ThreadSet.new Index.instance, $config[:thread_by_subject]
- @ts_mutex = Mutex.new
- @hidden_threads = {}
- end
-end
-
-end
diff --git a/lib/sup/modes/thread_index_mode.rb b/lib/sup/modes/thread_index_mode.rb
@@ -0,0 +1,950 @@
+require 'set'
+
+module Redwood
+
+## subclasses should implement:
+## - is_relevant?
+
+class ThreadIndexMode < LineCursorMode
+ DATE_WIDTH = Time::TO_NICE_S_MAX_LEN
+ MIN_FROM_WIDTH = 15
+ LOAD_MORE_THREAD_NUM = 20
+
+ HookManager.register "index-mode-size-widget", <<EOS
+Generates the per-thread size widget for each thread.
+Variables:
+ thread: The message thread to be formatted.
+EOS
+
+ HookManager.register "index-mode-date-widget", <<EOS
+Generates the per-thread date widget for each thread.
+Variables:
+ thread: The message thread to be formatted.
+EOS
+
+ HookManager.register "mark-as-spam", <<EOS
+This hook is run when a thread is marked as spam
+Variables:
+ thread: The message thread being marked as spam.
+EOS
+
+ register_keymap do |k|
+ k.add :load_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
+ k.add_multi "Load all threads (! to confirm) :", '!' do |kk|
+ kk.add :load_all_threads, "Load all threads (may list a _lot_ of threads)", '!'
+ end
+ k.add :cancel_search, "Cancel current search", :ctrl_g
+ k.add :reload, "Refresh view", '@'
+ k.add :toggle_archived, "Toggle archived status", 'a'
+ k.add :toggle_starred, "Star or unstar all messages in thread", '*'
+ k.add :toggle_new, "Toggle new/read status of all messages in thread", 'N'
+ k.add :edit_labels, "Edit or add labels for a thread", 'l'
+ k.add :edit_message, "Edit message (drafts only)", 'e'
+ k.add :toggle_spam, "Mark/unmark thread as spam", 'S'
+ k.add :toggle_deleted, "Delete/undelete thread", 'd'
+ k.add :kill, "Kill thread (never to be seen in inbox again)", '&'
+ k.add :flush_index, "Flush all changes now", '$'
+ k.add :jump_to_next_new, "Jump to next new thread", :tab
+ k.add :reply, "Reply to latest message in a thread", 'r'
+ k.add :reply_all, "Reply to all participants of the latest message in a thread", 'G'
+ 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 :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:
+ @threads = []
+ @hidden_threads = {}
+ @size_widget_width = nil
+ @size_widgets = []
+ @date_widget_width = nil
+ @date_widgets = []
+ @tags = Tagger.new self
+
+ ## these guys, and @text and @lines, are not covered
+ @load_thread = nil
+ @load_thread_opts = load_thread_opts
+ @hidden_labels = hidden_labels + LabelManager::HIDDEN_RESERVED_LABELS
+ @date_width = DATE_WIDTH
+
+ @interrupt_search = false
+
+ 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 => size,
+ :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
+ def select t=nil, when_done=nil
+ t ||= cursor_thread or return
+
+ Redwood::reporting_thread("load messages for thread-view-mode") do
+ num = t.size
+ message = "Loading #{num.pluralize 'message body'}..."
+ BufferManager.say(message) do |sid|
+ t.each_with_index do |(m, *_), i|
+ next unless m
+ BufferManager.say "#{message} (#{i}/#{num})", sid if t.size > 1
+ m.load_from_source!
+ end
+ end
+ mode = ThreadViewMode.new t, @hidden_labels, self
+ BufferManager.spawn t.subj, mode
+ BufferManager.draw_screen
+ mode.jump_to_first_open if $config[:jump_to_open_message]
+ BufferManager.draw_screen # lame TODO: make this unnecessary
+ ## the first draw_screen is needed before topline and botline
+ ## are set, and the second to show the cursor having moved
+
+ t.remove_label :unread
+ Index.save_thread t
+
+ update_text_for_line curpos
+ UpdateManager.relay self, :read, t.first
+ when_done.call if when_done
+ end
+ end
+
+ def multi_select threads
+ threads.each { |t| select t }
+ end
+
+ ## these two methods are called by thread-view-modes when the user
+ ## wants to view the previous/next thread without going back to
+ ## index-mode. we update the cursor as a convenience.
+ def launch_next_thread_after thread, &b
+ launch_another_thread thread, 1, &b
+ end
+
+ def launch_prev_thread_before thread, &b
+ launch_another_thread thread, -1, &b
+ end
+
+ def launch_another_thread thread, direction, &b
+ l = @lines[thread] or return
+ target_l = l + direction
+ t = @mutex.synchronize do
+ if target_l >= 0 && target_l < @threads.length
+ @threads[target_l]
+ end
+ end
+
+ if t # there's a next thread
+ set_cursor_pos target_l # move out of mutex?
+ select t, b
+ elsif b # no next thread. call the block anyways
+ b.call
+ end
+ end
+
+ def handle_single_message_labeled_update sender, m
+ ## no need to do anything different here; we don't differentiate
+ ## messages from their containing threads
+ handle_labeled_update sender, m
+ end
+
+ def handle_labeled_update sender, m
+ if(t = thread_containing(m))
+ l = @lines[t] or return
+ update_text_for_line l
+ elsif is_relevant?(m)
+ add_or_unhide m
+ end
+ end
+
+ def handle_simple_update sender, m
+ t = thread_containing(m) or return
+ l = @lines[t] or return
+ update_text_for_line l
+ end
+
+ %w(read unread archived starred unstarred).each do |state|
+ define_method "handle_#{state}_update" do |*a|
+ handle_simple_update(*a)
+ end
+ end
+
+ ## overwrite me!
+ def is_relevant? m; false; end
+
+ def handle_added_update sender, m
+ add_or_unhide m
+ BufferManager.draw_screen
+ end
+
+ def handle_single_message_deleted_update sender, m
+ @ts_mutex.synchronize do
+ return unless @ts.contains? m
+ @ts.remove_id m.id
+ end
+ update
+ end
+
+ def handle_deleted_update sender, m
+ t = @ts_mutex.synchronize { @ts.thread_for m }
+ return unless t
+ hide_thread t
+ update
+ end
+
+ def handle_spammed_update sender, m
+ t = @ts_mutex.synchronize { @ts.thread_for m }
+ return unless t
+ hide_thread t
+ update
+ end
+
+ def handle_undeleted_update sender, m
+ add_or_unhide m
+ end
+
+ def undo
+ UndoManager.undo
+ end
+
+ def update
+ old_cursor_thread = cursor_thread
+ @mutex.synchronize do
+ ## let's see you do THIS in python
+ @threads = @ts.threads.select { |t| !@hidden_threads.member?(t) }.select(&:has_message?).sort_by(&:sort_key)
+ @size_widgets = @threads.map { |t| size_widget_for_thread t }
+ @size_widget_width = @size_widgets.max_of { |w| w.display_length }
+ @date_widgets = @threads.map { |t| date_widget_for_thread t }
+ @date_widget_width = @date_widgets.max_of { |w| w.display_length }
+ end
+ set_cursor_pos @threads.index(old_cursor_thread)||curpos
+
+ regen_text
+ end
+
+ def edit_message
+ return unless(t = cursor_thread)
+ message, *_ = t.find { |m, *o| m.has_label? :draft }
+ if message
+ mode = ResumeMode.new message
+ BufferManager.spawn "Edit message", mode
+ else
+ BufferManager.flash "Not a draft message!"
+ end
+ end
+
+ ## returns an undo lambda
+ def actually_toggle_starred t
+ 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
+ undo = actually_toggle_starred t
+ UndoManager.register "toggling thread starred status", undo, lambda { Index.save_thread t }
+ update_text_for_line curpos
+ cursor_down
+ Index.save_thread t
+ end
+
+ def multi_toggle_starred threads
+ UndoManager.register "toggling #{threads.size.pluralize 'thread'} starred status",
+ threads.map { |t| actually_toggle_starred t },
+ lambda { threads.each { |t| Index.save_thread t } }
+ regen_text
+ threads.each { |t| Index.save_thread t }
+ 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
+ undo = actually_toggle_archived t
+ UndoManager.register "deleting/undeleting thread #{t.first.id}", undo, lambda { update_text_for_line curpos },
+ lambda { Index.save_thread t }
+ update_text_for_line curpos
+ Index.save_thread t
+ end
+
+ def multi_toggle_archived threads
+ undos = threads.map { |t| actually_toggle_archived t }
+ UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", undos, lambda { regen_text },
+ lambda { threads.each { |t| Index.save_thread t } }
+ regen_text
+ threads.each { |t| Index.save_thread t }
+ end
+
+ def toggle_new
+ t = cursor_thread or return
+ t.toggle_label :unread
+ update_text_for_line curpos
+ cursor_down
+ Index.save_thread t
+ end
+
+ def multi_toggle_new threads
+ threads.each { |t| t.toggle_label :unread }
+ regen_text
+ threads.each { |t| Index.save_thread t }
+ end
+
+ def multi_toggle_tagged threads
+ @mutex.synchronize { @tags.drop_all_tags }
+ regen_text
+ end
+
+ def join_threads
+ ## this command has no non-tagged form. as a convenience, allow this
+ ## command to be applied to tagged threads without hitting ';'.
+ @tags.apply_to_tagged :join_threads
+ end
+
+ def multi_join_threads threads
+ @ts.join_threads threads or return
+ threads.each { |t| Index.save_thread t }
+ @tags.drop_all_tags # otherwise we have tag pointers to invalid threads!
+ update
+ end
+
+ def jump_to_next_new
+ n = @mutex.synchronize do
+ ((curpos + 1) ... lines).find { |i| @threads[i].has_label? :unread } ||
+ (0 ... curpos).find { |i| @threads[i].has_label? :unread }
+ end
+ if n
+ ## jump there if necessary
+ jump_to_line n unless n >= topline && n < botline
+ set_cursor_pos n
+ else
+ BufferManager.flash "No new messages."
+ end
+ end
+
+ def toggle_spam
+ t = cursor_thread or return
+ multi_toggle_spam [t]
+ end
+
+ ## both spam and deleted have the curious characteristic that you
+ ## always want to hide the thread after either applying or removing
+ ## that label. in all thread-index-views except for
+ ## 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
+ undos = threads.map { |t| actually_toggle_spammed t }
+ threads.each { |t| HookManager.run("mark-as-spam", :thread => t) }
+ UndoManager.register "marking/unmarking #{threads.size.pluralize 'thread'} as spam",
+ undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
+ regen_text
+ threads.each { |t| Index.save_thread t }
+ 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
+ undos = threads.map { |t| actually_toggle_deleted t }
+ UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}",
+ undos, lambda { regen_text }, lambda { threads.each { |t| Index.save_thread t } }
+ regen_text
+ threads.each { |t| Index.save_thread t }
+ end
+
+ def kill
+ t = cursor_thread or return
+ multi_kill [t]
+ end
+
+ def flush_index
+ @flush_id = BufferManager.say "Flushing index..."
+ Index.save_index
+ BufferManager.clear @flush_id
+ end
+
+ ## m-m-m-m-MULTI-KILL
+ def multi_kill threads
+ UndoManager.register "killing/unkilling #{threads.size.pluralize 'threads'}" do
+ threads.each do |t|
+ if t.toggle_label :killed
+ add_or_unhide t.first
+ else
+ hide_thread t
+ end
+ end.each do |t|
+ UpdateManager.relay self, :labeled, t.first
+ Index.save_thread t
+ end
+ regen_text
+ end
+
+ threads.each do |t|
+ if t.toggle_label :killed
+ hide_thread t
+ else
+ add_or_unhide t.first
+ end
+ end.each do |t|
+ # send 'labeled'... this might be more specific
+ UpdateManager.relay self, :labeled, t.first
+ Index.save_thread t
+ end
+
+ killed, unkilled = threads.partition { |t| t.has_label? :killed }.map(&:size)
+ BufferManager.flash "#{killed.pluralize 'thread'} killed, #{unkilled} unkilled"
+ regen_text
+ end
+
+ def cleanup
+ UpdateManager.unregister self
+
+ if @load_thread
+ @load_thread.kill
+ BufferManager.clear @mbid if @mbid
+ sleep 0.1 # TODO: necessary?
+ BufferManager.erase_flash
+ end
+ dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
+ fail "dirty threads remain" unless dirty_threads.empty?
+ super
+ end
+
+ def toggle_tagged
+ t = cursor_thread or return
+ @mutex.synchronize { @tags.toggle_tag_for t }
+ update_text_for_line curpos
+ cursor_down
+ end
+
+ def toggle_tagged_all
+ @mutex.synchronize { @threads.each { |t| @tags.toggle_tag_for t } }
+ regen_text
+ end
+
+ def tag_matching
+ query = BufferManager.ask :search, "tag threads matching (regex): "
+ return if query.nil? || query.empty?
+ 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.sort_by {|x| x.to_s}, @hidden_labels
+ return unless user_labels
+
+ thread.labels = Set.new(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
+ Index.save_thread thread
+ end
+
+ UpdateManager.relay self, :labeled, thread.first
+ Index.save_thread thread
+ end
+
+ def multi_edit_labels threads
+ user_labels = BufferManager.ask_for_labels :labels, "Add/remove labels (use -label to remove): ", [], @hidden_labels
+ return unless user_labels
+
+ 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
+ UpdateManager.relay self, :labeled, t.first
+ 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
+ Index.save_thread t
+ end
+ regen_text
+ end
+
+ threads.each { |t| Index.save_thread t }
+ end
+
+ def reply type_arg=nil
+ t = cursor_thread or return
+ m = t.latest_message
+ return if m.nil? # probably won't happen
+ m.load_from_source!
+ mode = ReplyMode.new m, type_arg
+ BufferManager.spawn "Reply to #{m.subj}", mode
+ end
+
+ def reply_all; reply :all; end
+
+ def forward
+ t = cursor_thread or return
+ m = t.latest_message
+ return if m.nil? # probably won't happen
+ m.load_from_source!
+ ForwardMode.spawn_nicely :message => m
+ end
+
+ def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
+ return if @load_thread # todo: wrap in mutex
+ @load_thread = Redwood::reporting_thread("load threads for thread-index-mode") do
+ num = load_n_threads n, opts
+ opts[:when_done].call(num) if opts[:when_done]
+ @load_thread = nil
+ end
+ end
+
+ ## TODO: figure out @ts_mutex in this method
+ def load_n_threads n=LOAD_MORE_THREAD_NUM, opts={}
+ @interrupt_search = false
+ @mbid = BufferManager.say "Searching for threads..."
+
+ ts_to_load = n
+ ts_to_load = ts_to_load + @ts.size unless n == -1 # -1 means all threads
+
+ orig_size = @ts.size
+ last_update = Time.now
+ @ts.load_n_threads(ts_to_load, opts) do |i|
+ if (Time.now - last_update) >= 0.25
+ BufferManager.say "Loaded #{i.pluralize 'thread'}...", @mbid
+ update
+ BufferManager.draw_screen
+ last_update = Time.now
+ end
+ ::Thread.pass
+ break if @interrupt_search
+ end
+ @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
+
+ update
+ BufferManager.clear @mbid
+ @mbid = nil
+ BufferManager.draw_screen
+ @ts.size - orig_size
+ end
+ ignore_concurrent_calls :load_n_threads
+
+ def status
+ if (l = lines) == 0
+ "line 0 of 0"
+ else
+ "line #{curpos + 1} of #{l}"
+ end
+ end
+
+ def cancel_search
+ @interrupt_search = true
+ end
+
+ def load_all_threads
+ load_threads :num => -1
+ end
+
+ def load_threads opts={}
+ if opts[:num].nil?
+ n = ThreadIndexMode::LOAD_MORE_THREAD_NUM
+ else
+ n = opts[:num]
+ end
+
+ myopts = @load_thread_opts.merge({ :when_done => (lambda do |num|
+ opts[:when_done].call(num) if opts[:when_done]
+
+ if num > 0
+ BufferManager.flash "Found #{num.pluralize 'thread'}."
+ else
+ BufferManager.flash "No matches."
+ end
+ end)})
+
+ if opts[:background] || opts[:background].nil?
+ load_n_threads_background n, myopts
+ else
+ load_n_threads n, myopts
+ end
+ end
+ ignore_concurrent_calls :load_threads
+
+ def resize rows, cols
+ regen_text
+ super
+ end
+
+protected
+
+ def add_or_unhide m
+ @ts_mutex.synchronize do
+ if (is_relevant?(m) || @ts.is_relevant?(m)) && !@ts.contains?(m)
+ @ts.load_thread_for_message m, @load_thread_opts
+ end
+
+ @hidden_threads.delete @ts.thread_for(m)
+ end
+
+ update
+ end
+
+ def thread_containing m; @ts_mutex.synchronize { @ts.thread_for m } end
+
+ ## used to tag threads by query. this can be made a lot more sophisticated,
+ ## but for right now we'll do the obvious this.
+ def thread_matches? t, query
+ t.subj =~ query || t.snippet =~ query || t.participants.any? { |x| x.longname =~ query }
+ end
+
+ def size_widget_for_thread t
+ HookManager.run("index-mode-size-widget", :thread => t) || default_size_widget_for(t)
+ end
+
+ def date_widget_for_thread t
+ HookManager.run("index-mode-date-widget", :thread => t) || default_date_widget_for(t)
+ end
+
+ def cursor_thread; @mutex.synchronize { @threads[curpos] }; end
+
+ def drop_all_threads
+ @tags.drop_all_tags
+ initialize_threads
+ update
+ end
+
+ def hide_thread t
+ @mutex.synchronize do
+ i = @threads.index(t) or return
+ raise "already hidden" if @hidden_threads[t]
+ @hidden_threads[t] = true
+ @threads.delete_at i
+ @size_widgets.delete_at i
+ @date_widgets.delete_at i
+ @tags.drop_tag_for t
+ end
+ end
+
+ def update_text_for_line l
+ return unless l # not sure why this happens, but it does, occasionally
+
+ need_update = false
+
+ @mutex.synchronize do
+ @size_widgets[l] = size_widget_for_thread @threads[l]
+ @date_widgets[l] = date_widget_for_thread @threads[l]
+
+ ## if a widget size has increased, we need to redraw everyone
+ need_update =
+ (@size_widgets[l].size > @size_widget_width) or
+ (@date_widgets[l].size > @date_widget_width)
+ end
+
+ if need_update
+ update
+ else
+ @text[l] = text_for_thread_at l
+ buffer.mark_dirty if buffer
+ end
+ end
+
+ def regen_text
+ threads = @mutex.synchronize { @threads }
+ @text = threads.map_with_index { |t, i| text_for_thread_at i }
+ @lines = threads.map_with_index { |t, i| [t, i] }.to_h
+ buffer.mark_dirty if buffer
+ end
+
+ def authors; map { |m, *o| m.from if m }.compact.uniq; end
+
+ ## preserve author order from the thread
+ def author_names_and_newness_for_thread t, limit=nil
+ new = {}
+ seen = {}
+ authors = t.map do |m, *o|
+ next unless m && m.from
+ new[m.from] ||= m.has_label?(:unread)
+ next if seen[m.from]
+ seen[m.from] = true
+ m.from
+ end.compact
+
+ result = []
+ authors.each do |a|
+ break if limit && result.size >= limit
+ name = if AccountManager.is_account?(a)
+ "me"
+ elsif t.authors.size == 1
+ a.mediumname
+ else
+ a.shortname
+ end
+
+ result << [name, new[a]]
+ end
+
+ if result.size == 1 && (author_and_newness = result.assoc("me"))
+ unless (recipients = t.participants - t.authors).empty?
+ result = recipients.collect do |r|
+ break if limit && result.size >= limit
+ name = (recipients.size == 1) ? r.mediumname : r.shortname
+ ["(#{name})", author_and_newness[1]]
+ end
+ end
+ end
+
+ result
+ end
+
+ AUTHOR_LIMIT = 5
+ def text_for_thread_at line
+ t, size_widget, date_widget = @mutex.synchronize do
+ [@threads[line], @size_widgets[line], @date_widgets[line]]
+ end
+
+ starred = t.has_label? :starred
+
+ ## format the from column
+ cur_width = 0
+ ann = author_names_and_newness_for_thread t, AUTHOR_LIMIT
+ 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.display_length > from_width
+ name[0 ... (from_width - cur_width - 1)] + "."
+ 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.display_length
+
+ if last && from_width > cur_width
+ abbrev += " " * (from_width - cur_width)
+ end
+
+ from << [(newness ? :index_new_color : (starred ? :index_starred_color : :index_old_color)), abbrev]
+ end
+
+ is_me = AccountManager.method(:is_account?)
+ directly_participated = t.direct_participants.any?(&is_me)
+ participated = directly_participated || t.participants.any?(&is_me)
+
+ subj_color =
+ if t.has_label?(:draft)
+ :index_draft_color
+ elsif t.has_label?(:unread)
+ :index_new_color
+ elsif starred
+ :index_starred_color
+ elsif Colormap.sym_is_defined(:index_subject_color)
+ :index_subject_color
+ else
+ :index_old_color
+ end
+
+ size_padding = @size_widget_width - size_widget.display_length
+ size_widget_text = sprintf "%#{size_padding}s%s", "", size_widget
+
+ date_padding = @date_widget_width - date_widget.display_length
+ date_widget_text = sprintf "%#{date_padding}s%s", "", date_widget
+
+ [
+ [:tagged_color, @tags.tagged?(t) ? ">" : " "],
+ [:date_color, date_widget_text],
+ [:starred_color, (starred ? "*" : " ")],
+ ] +
+ from +
+ [
+ [:size_widget_color, size_widget_text],
+ [:to_me_color, t.labels.member?(:attachment) ? "@" : " "],
+ [:to_me_color, directly_participated ? ">" : (participated ? '+' : " ")],
+ ] +
+ (t.labels - @hidden_labels).sort_by {|x| x.to_s}.map {
+ |label| [Colormap.sym_is_defined("label_#{label}_color".to_sym) || :label_color, "#{label} "]
+ } +
+ [
+ [subj_color, t.subj + (t.subj.empty? ? "" : " ")],
+ [:snippet_color, t.snippet],
+ ]
+ end
+
+ def dirty?; @mutex.synchronize { (@hidden_threads.keys + @threads).any? { |t| t.dirty? } } end
+
+private
+
+ def default_size_widget_for t
+ case t.size
+ when 1
+ ""
+ else
+ "(#{t.size})"
+ end
+ end
+
+ def default_date_widget_for t
+ t.date.getlocal.to_nice_s
+ end
+
+ def from_width
+ [(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
+ end
+
+ def initialize_threads
+ @ts = ThreadSet.new Index.instance, $config[:thread_by_subject]
+ @ts_mutex = Mutex.new
+ @hidden_threads = {}
+ end
+end
+
+end
diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread_view_mode.rb
diff --git a/sup.gemspec b/sup.gemspec
@@ -44,6 +44,7 @@ DESC
s.add_dependency "lockfile"
s.add_dependency "mime-types", "~> 1"
s.add_dependency "gettext"
+ s.add_dependency "chronic", "~> 0.9", ">= 0.9.1"
s.add_development_dependency "bundler", "~> 1.3"
s.add_development_dependency "rake"