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 "dynamically 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?
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
answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
if answer
- answer.split_on_commas.map { |x| ContactManager.contact_for(x) || PersonManager.person_for(x) }
+ answer.split_on_commas.map { |x| ContactManager.contact_for(x) || Person.from_address(x) }
end
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
## do whatever crypto transformation is necessary
if @crypto_selector && @crypto_selector.val != :none
- from_email = PersonManager.person_for(@header["From"]).email
- to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| PersonManager.person_for(p).email }
+ from_email = Person.from_address(@header["From"]).email
+ to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| Person.from_address(p).email }
m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
end
end
def sig_lines
- p = PersonManager.person_for(@header["From"])
+ p = Person.from_address(@header["From"])
from_email = p && p.email
## first run the hook
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", '#'
end
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
end
end
+ def unsaved?; dirty? end
def lines; @text.length; end
def [] i; @text[i]; end
def contains_thread? t; @threads.include?(t) end
BufferManager.flash "#{threads.size.pluralize 'Thread'} killed."
end
- def save
- BufferManager.say("Saving contacts...") { ContactManager.instance.save }
- dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
- return if dirty_threads.empty?
+ def save background=true
+ if background
+ Redwood::reporting_thread("saving thread") { actually_save }
+ else
+ actually_save
+ end
+ end
+
+ def actually_save
+ @save_thread_mutex.synchronize do
+ BufferManager.say("Saving contacts...") { ContactManager.instance.save }
+ dirty_threads = @mutex.synchronize { (@threads + @hidden_threads.keys).select { |t| t.dirty? } }
+ next if dirty_threads.empty?
- BufferManager.say("Saving threads...") do |say_id|
- dirty_threads.each_with_index do |t, i|
- BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
- t.save Index
+ BufferManager.say("Saving threads...") do |say_id|
+ dirty_threads.each_with_index do |t, i|
+ BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
+ t.save Index
+ end
end
end
end
sleep 0.1 # TODO: necessary?
BufferManager.erase_flash
end
- save
+ save false
super
end
end
def multi_edit_labels threads
- user_labels = BufferManager.ask_for_labels :add_labels, "Add labels: ", [], @hidden_labels
+ user_labels = BufferManager.ask_for_labels :labels, "Add/remove labels (use -label to remove): ", [], @hidden_labels
return unless user_labels
-
- hl = user_labels.select { |l| @hidden_labels.member? l }
+
+ user_labels.map! { |l| (l.to_s =~ /^-/)? [l.to_s.gsub(/^-?/, '').to_sym, true] : [l, false] }
+ hl = user_labels.select { |(l,_)| @hidden_labels.member? l }
if hl.empty?
- threads.each { |t| user_labels.each { |l| t.apply_label l } }
- user_labels.each { |l| LabelManager << l }
+ threads.each do |t|
+ user_labels.each do |(l, to_remove)|
+ if to_remove
+ t.remove_label l
+ else
+ t.apply_label l
+ end
+ end
+ end
+ user_labels.each { |(l,_)| LabelManager << l }
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
numi = 0
add_messages_from source do |m, offset, entry|
## always preserve the labels on disk.
- m.labels = entry[:label].split(/\s+/).map { |x| x.intern } if entry
+ m.labels = ((m.labels - [:unread, :inbox]) + entry[:label].split(/\s+/).map { |x| x.intern }).uniq if entry
yield "Found message at #{offset} with labels {#{m.labels * ', '}}"
unless entry
num += 1
- from_and_subj << [m.from.longname, m.subj]
+ from_and_subj << [m.from && m.from.longname, m.subj]
if m.has_label?(:inbox) && ([:spam, :deleted, :killed] & m.labels).empty?
- from_and_subj_inbox << [m.from.longname, m.subj]
+ from_and_subj_inbox << [m.from && m.from.longname, m.subj]
numi += 1
end
end