elsif File.directory?(answer)
spawn_modal "file browser", FileBrowserMode.new(answer)
else
- answer
+ File.expand_path answer
end
end
def ask_getch question, accept=nil
raise "impossible!" if @asking
- @asking = true
accept = accept.split(//).map { |x| x[0] } if accept
Ncurses.refresh
end
+ @asking = true
ret = nil
done = false
until done
@sources[source.id] = source
end
- def source_for uri; @sources.values.find { |s| s.is_source_for? uri }; end
- def usual_sources; @sources.values.find_all { |s| s.usual? }; end
- def sources; @sources.values; end
+ def sources
+ ## favour the inbox by listing non-archived sources first
+ @sources.values.sort_by { |s| s.id }.partition { |s| !s.archived? }.flatten
+ end
+
+ def source_for uri; sources.find { |s| s.is_source_for? uri }; end
+ def usual_sources; sources.find_all { |s| s.usual? }; end
def load_index dir=File.join(@dir, "ferret")
if File.exists? dir
File.chmod 0600, fn
FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
end
- Redwood::save_yaml_obj @sources.values.sort_by { |s| s.id.to_i }, fn, true
+ Redwood::save_yaml_obj sources.sort_by { |s| s.id.to_i }, fn, true
File.chmod 0600, fn
end
@sources_dirty = false
def initialize mode=:regular
@mode = mode
- @tags = Tagger.new self
+ @tags = Tagger.new self, "contact"
@num = nil
@text = []
super()
def attach_file
fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
return unless fn
- @attachments << RMail::Message.make_file_attachment(fn)
- @attachment_names << fn
- update
+ begin
+ @attachments << RMail::Message.make_file_attachment(fn)
+ @attachment_names << fn
+ update
+ rescue SystemCallError => e
+ BufferManager.flash "Can't read #{fn}: #{e.message}"
+ end
end
def delete_attachment
attr_reader :status, :topline, :botline, :leftcol
- COL_JUMP = 4
+ COL_JUMP = 2
register_keymap do |k|
k.add :line_down, "Down one line", :down, 'j', 'J'
end
register_keymap do |k|
- k.add :refine_search, "Refine search", '.'
+ k.add :refine_search, "Refine search", '|'
end
def refine_search
- query = BufferManager.ask :search, "query: ", (@qobj.to_s + " ")
+ query = BufferManager.ask :search, "refine query: ", (@qobj.to_s + " ")
return unless query && query !~ /^\s*$/
- SearchResultsMode.spawn_from_query query, @qopts
+ SearchResultsMode.spawn_from_query query
end
## a proper is_relevant? method requires some way of asking ferret
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", '*'
@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
threads.each { |t| select t }
end
- ## this is called by thread-view-modes when the user wants to view
- ## the next thread without going to index-mode. we update the cursor
- ## as a convenience.
+ ## 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 l < @threads.length - 1
- set_cursor_pos l + 1 # move out of mutex?
- @threads[l + 1]
+ if target_l >= 0 && target_l < @threads.length
+ @threads[target_l]
end
- end or return
+ end
- select t, b
+ 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
end
def handle_deleted_update sender, m
- @ts_mutex.synchronize do
- return unless @ts.contains? m
- @ts.remove_thread_containing_id m.id
- end
+ 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
## 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.size + n, opts) do |i|
+ @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
+ break if @interrupt_search
end
@ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
end
end
+ def cancel_search
+ @interrupt_search = true
+ end
+
+ def load_all_threads
+ load_threads :num => -1
+ end
+
def load_threads opts={}
- n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
+ 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
kk.add :unread_and_next, "Mark this thread as unread, kill buffer, and view next", 'N'
kk.add :do_nothing_and_next, "Kill buffer, and view next", 'n'
end
+
+ k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read/do (n)othing:", ']' do |kk|
+ kk.add :archive_and_prev, "Archive this thread, kill buffer, and view previous", 'a'
+ kk.add :delete_and_prev, "Delete this thread, kill buffer, and view previous", 'd'
+ kk.add :spam_and_prev, "Mark this thread as spam, kill buffer, and view previous", 's'
+ kk.add :unread_and_prev, "Mark this thread as unread, kill buffer, and view previous", 'N'
+ kk.add :do_nothing_and_prev, "Kill buffer, and view previous", 'n'
+ end
end
## there are a couple important instance variables we hold to format
end
end
+ IDEAL_TOP_CONTEXT = 3 # try and give 3 rows of top context
+ IDEAL_LEFT_CONTEXT = 4 # try and give 4 columns of left context
def jump_to_message m, loose_alignment=false
l = @layout[m]
left = l.depth * INDENT_SPACES
## jump to the top line
if loose_alignment
- jump_to_line [l.top - 3, 0].max # give 3 lines of top context
+ jump_to_line [l.top - IDEAL_TOP_CONTEXT, 0].max # give 3 lines of top context
else
jump_to_line l.top
end
## jump to the left column
- if loose_alignment
- ## try and give 4 columns of left context, but not if it means that
- ## the right of the message is truncated.
- jump_to_col [[left - 4, rightcol - l.width - 1].min, 0].max
- else
- jump_to_col left
- end
+ ideal_left = left +
+ if loose_alignment
+ -IDEAL_LEFT_CONTEXT + (l.width - buffer.content_width + IDEAL_LEFT_CONTEXT + 1).clamp(0, IDEAL_LEFT_CONTEXT)
+ else
+ 0
+ end
+
+ jump_to_col [ideal_left, 0].max
## either way, move the cursor to the first line
set_cursor_pos l.top
def unread_and_next; unread_and_then :next end
def do_nothing_and_next; do_nothing_and_then :next end
+ def archive_and_prev; archive_and_then :prev end
+ def spam_and_prev; spam_and_then :prev end
+ def delete_and_prev; delete_and_then :prev end
+ def unread_and_prev; unread_and_then :prev end
+ def do_nothing_and_prev; do_nothing_and_then :prev end
+
def archive_and_then op
dispatch op do
@thread.remove_label :inbox
return if @dying
@dying = true
+ l = lambda do
+ yield if block_given?
+ BufferManager.kill_buffer_safely buffer
+ end
+
case op
when :next
- @index_mode.launch_next_thread_after(@thread) do
- @thread.save Index if block_given? && yield
- BufferManager.kill_buffer_safely buffer
- end
+ @index_mode.launch_next_thread_after @thread, &l
+ when :prev
+ @index_mode.launch_prev_thread_before @thread, &l
when :kill
- @thread.save Index if yield
- BufferManager.kill_buffer_safely buffer
+ l.call
else
raise ArgumentError, "unknown thread dispatch operation #{op.inspect}"
end
module Redwood
class Tagger
- def initialize mode
+ def initialize mode, noun="thread", plural_noun=nil
@mode = mode
@tagged = {}
+ @noun = noun
+ @plural_noun = plural_noun || (@noun + "s")
end
def tagged? o; @tagged[o]; end
return
end
- noun = num_tagged == 1 ? "thread" : "threads"
+ noun = num_tagged == 1 ? @noun : @plural_noun
unless action
c = BufferManager.ask_getch "apply to #{num_tagged} tagged #{noun}:"
## load in (at most) num number of threads from the index
def load_n_threads num, opts={}
@index.each_id_by_date opts do |mid, builder|
- break if size >= num
+ break if size >= num unless num == -1
next if contains_id? mid
m = builder.call