From: William Morgan Date: Sun, 6 Sep 2009 13:47:37 +0000 (-0400) Subject: Merge branch 'master' into next X-Git-Url: https://git.cworth.org/git?a=commitdiff_plain;h=d4c1bd840f55a07fd5aeb4d08a1f43c3f7c1850e;hp=4efc8adaf8c63ff054c71149de6c12fe8194ff03;p=sup Merge branch 'master' into next --- diff --git a/bin/sup b/bin/sup index bbb6c17..5049879 100755 --- a/bin/sup +++ b/bin/sup @@ -58,6 +58,7 @@ if $opts[:list_hooks] end Thread.abort_on_exception = true # make debugging possible +Thread.current.priority = 1 # keep ui responsive module Redwood @@ -79,6 +80,7 @@ global_keymap = Keymap.new do |k| k.add :nothing, "Do nothing", :ctrl_g k.add :recall_draft, "Edit most recent draft message", 'R' k.add :show_inbox, "Show the Inbox buffer", 'I' + k.add :show_console, "Show the Console buffer", '~' end ## the following magic enables wide characters when used with a ruby @@ -168,6 +170,9 @@ begin lmode.on_kill { Logger.clear! } Logger.add_sink lmode Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}." + if Logger::LEVELS.index(Logger.level) > 0 + Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}." + end debug "initializing inbox buffer" imode = InboxMode.new @@ -228,15 +233,16 @@ begin bm.erase_flash - action = begin - if bm.handle_input c + action = + begin + if bm.handle_input c + :nothing + else + bm.resolve_input_with_keymap c, global_keymap + end + rescue InputSequenceAborted :nothing - else - bm.resolve_input_with_keymap c, global_keymap end - rescue InputSequenceAborted - :nothing - end case action when :quit_now break if bm.kill_all_buffers_safely @@ -294,6 +300,9 @@ begin end when :show_inbox BufferManager.raise_to_front ibuf + when :show_console + b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new } + b.mode.run when :nothing, InputSequenceAborted when :redraw bm.completely_redraw_screen diff --git a/lib/sup.rb b/lib/sup.rb index aa8079c..7f9396c 100644 --- a/lib/sup.rb +++ b/lib/sup.rb @@ -307,6 +307,7 @@ 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" $:.each do |base| diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb index d85090a..67ccd86 100644 --- a/lib/sup/buffer.rb +++ b/lib/sup/buffer.rb @@ -25,6 +25,8 @@ module Ncurses def mutex; @mutex ||= Mutex.new; end def sync &b; mutex.synchronize(&b); end + ## magically, this stuff seems to work now. i could swear it didn't + ## before. hm. def nonblocking_getch ## INSANTIY ## it is NECESSARY to wrap Ncurses.getch in a select() otherwise all @@ -70,7 +72,7 @@ class Buffer def content_height; @height - 1; end def content_width; @width; end - def resize rows, cols + def resize rows, cols return if cols == @width && rows == @height @width = cols @height = rows diff --git a/lib/sup/ferret_index.rb b/lib/sup/ferret_index.rb index df1139d..2de8727 100644 --- a/lib/sup/ferret_index.rb +++ b/lib/sup/ferret_index.rb @@ -4,6 +4,16 @@ module Redwood class FerretIndex < BaseIndex + HookManager.register "custom-search", < s) || s + + subs = subs.gsub(/\b(to|from):(\S+)\b/) do field, name = $1, $2 if(p = ContactManager.contact_for(name)) [field, p.email] diff --git a/lib/sup/hook.rb b/lib/sup/hook.rb index 0c41162..0212c6b 100644 --- a/lib/sup/hook.rb +++ b/lib/sup/hook.rb @@ -1,33 +1,11 @@ module Redwood class HookManager - ## there's probably a better way to do this, but to evaluate a hook - ## with a bunch of pre-set "local variables" i define a function - ## per variable and then instance_evaluate the code. - ## - ## how does rails do it, when you pass :locals into a partial? - ## - ## i don't bother providing setters, since i'm pretty sure the - ## charade will fall apart pretty quickly with respect to scoping. - ## "fail-fast", we'll call it. class HookContext def initialize name @__say_id = nil @__name = name - @__locals = {} - end - - attr_writer :__locals - - def method_missing m, *a - case @__locals[m] - when Proc - @__locals[m] = @__locals[m].call(*a) # only call the proc once - when nil - super - else - @__locals[m] - end + @__cache = {} end def say s @@ -60,12 +38,24 @@ class HookManager HookManager.tags[tag] = value end - def __binding - binding - end - - def __cleanup + def __run __hook, __filename, __locals + __binding = binding + __lprocs, __lvars = __locals.partition { |k, v| v.is_a?(Proc) } + eval __lvars.map { |k, v| "#{k} = __locals[#{k.inspect}];" }.join, __binding + ## we also support closures for delays evaluation. unfortunately + ## we have to do this via method calls, so you don't get all the + ## semantics of a regular variable. not ideal. + __lprocs.each do |k, v| + self.class.instance_eval do + define_method k do + @__cache[k] ||= v.call + end + end + end + ret = eval __hook, __binding, __filename BufferManager.clear @__say_id if @__say_id + @__cache = {} + ret end end @@ -86,18 +76,16 @@ class HookManager def run name, locals={} hook = hook_for(name) or return context = @contexts[hook] ||= HookContext.new(name) - context.__locals = locals result = nil begin - result = context.instance_eval @hooks[name], fn_for(name) + result = context.__run hook, fn_for(name), locals rescue Exception => e log "error running hook: #{e.message}" log e.backtrace.join("\n") @hooks[name] = nil # disable it BufferManager.flash "Error running hook: #{e.message}" if BufferManager.instantiated? end - context.__cleanup result end @@ -123,19 +111,20 @@ EOS def enabled? name; !hook_for(name).nil? end + def clear; @hooks.clear; end + private def hook_for name unless @hooks.member? name - @hooks[name] = - begin - returning IO.read(fn_for(name)) do - log "read '#{name}' from #{fn_for(name)}" - end - rescue SystemCallError => e - #log "disabled hook for '#{name}': #{e.message}" - nil + @hooks[name] = begin + returning IO.read(fn_for(name)) do + debug "read '#{name}' from #{fn_for(name)}" end + rescue SystemCallError => e + #log "disabled hook for '#{name}': #{e.message}" + nil + end end @hooks[name] diff --git a/lib/sup/message-chunks.rb b/lib/sup/message-chunks.rb index 40e098f..ce7d1ee 100644 --- a/lib/sup/message-chunks.rb +++ b/lib/sup/message-chunks.rb @@ -208,13 +208,25 @@ EOS class EnclosedMessage attr_reader :lines - def initialize from, body - @from = from - @lines = body.split "\n" - end + def initialize from, to, cc, date, subj + @from = from ? "unknown sender" : from.full_adress + @to = to ? "" : to.map { |p| p.full_address }.join(", ") + @cc = cc ? "" : cc.map { |p| p.full_address }.join(", ") + if date + @date = date.rfc822 + else + @date = "" + end - def from - @from ? @from.longname : "unknown sender" + @subj = subj + + @lines = "\nFrom: #{from}\n" + @lines += "To: #{to}\n" + if !cc.empty? + @lines += "Cc: #{cc}\n" + end + @lines += "Date: #{date}\n" + @lines += "Subject: #{subj}\n\n" end def inlineable?; false end @@ -224,7 +236,7 @@ EOS def viewable?; false end def patina_color; :generic_notice_patina_color end - def patina_text; "Begin enclosed message from #{from} (#{@lines.length} lines)" end + def patina_text; "Begin enclosed message sent on #{@date}" end def color; :quote_color end end diff --git a/lib/sup/message.rb b/lib/sup/message.rb index f640011..c69b9e3 100644 --- a/lib/sup/message.rb +++ b/lib/sup/message.rb @@ -436,10 +436,19 @@ private elsif m.header.content_type == "message/rfc822" if m.body payload = RMail::Parser.read(m.body) - from = payload.header.from.first - from_person = from ? Person.from_address(from.format) : nil - [Chunk::EnclosedMessage.new(from_person, payload.to_s)] + - message_to_chunks(payload, encrypted) + from = payload.header.from.first ? payload.header.from.first.format : "" + to = payload.header.to.map { |p| p.format }.join(", ") + cc = payload.header.cc.map { |p| p.format }.join(", ") + subj = payload.header.subject + subj = subj ? Message.normalize_subj(payload.header.subject.gsub(/\s+/, " ").gsub(/\s+$/, "")) : subj + if Rfc2047.is_encoded? subj + subj = Rfc2047.decode_to $encoding, subj + end + msgdate = payload.header.date + from_person = from ? Person.from_address(from) : nil + to_people = to ? Person.from_address_list(to) : nil + cc_people = cc ? Person.from_address_list(cc) : nil + [Chunk::EnclosedMessage.new(from_person, to_people, cc_people, msgdate, subj)] + message_to_chunks(payload, encrypted) else [Chunk::EnclosedMessage.new(nil, "")] end diff --git a/lib/sup/mode.rb b/lib/sup/mode.rb index 209ca45..03deacb 100644 --- a/lib/sup/mode.rb +++ b/lib/sup/mode.rb @@ -1,3 +1,4 @@ +require 'open3' module Redwood class Mode diff --git a/lib/sup/modes/console-mode.rb b/lib/sup/modes/console-mode.rb new file mode 100644 index 0000000..af3d66d --- /dev/null +++ b/lib/sup/modes/console-mode.rb @@ -0,0 +1,103 @@ +require 'pp' + +module Redwood + +class Console + def initialize mode + @mode = mode + end + + def query(query) + Enumerable::Enumerator.new(Index, :each_message, Index.parse_query(query)) + end + + def add_labels(query, *labels) + query(query).each { |m| m.labels += labels; m.save Index } + end + + def remove_labels(query, *labels) + query(query).each { |m| m.labels -= labels; m.save Index } + end + + def xapian; Index.instance.instance_variable_get :@xapian; end + def ferret; Index.instance.instance_variable_get :@index; end + + ## files that won't cause problems when reloaded + ## TODO expand this list / convert to blacklist + RELOAD_WHITELIST = %w(sup/xapian_index.rb sup/modes/console-mode.rb) + + def reload + old_verbose = $VERBOSE + $VERBOSE = nil + old_features = $".dup + begin + fs = $".grep(/^sup\//) + fs.reject! { |f| not RELOAD_WHITELIST.member? f } + fs.each { |f| $".delete f } + fs.each do |f| + @mode << "reloading #{f}\n" + begin + require f + rescue LoadError => e + raise unless e.message =~ /no such file to load/ + end + end + rescue Exception + $".clear + $".concat old_features + raise + ensure + $VERBOSE = old_verbose + end + true + end + + def clear_hooks + HookManager.clear + nil + end +end + +class ConsoleMode < LogMode + register_keymap do |k| + k.add :run, "Restart evaluation", 'e' + end + + def initialize + super "console" + @console = Console.new self + @binding = @console.instance_eval { binding } + self << <> #{cmd}\n" + ret = eval cmd, @binding + self << "=> #{ret.pretty_inspect}\n" + rescue Exception + self << "#{$!.class}: #{$!.message}\n" + clean_backtrace = [] + $!.backtrace.each { |l| break if l =~ /console-mode/; clean_backtrace << l } + clean_backtrace.each { |l| self << "#{l}\n" } + end + end + + def prompt + BufferManager.ask :console, "eval: " + end + + def run + while true + cmd = prompt or return + execute cmd + end + end +end + +end diff --git a/lib/sup/modes/line-cursor-mode.rb b/lib/sup/modes/line-cursor-mode.rb index 246f2b5..c7c6b9a 100644 --- a/lib/sup/modes/line-cursor-mode.rb +++ b/lib/sup/modes/line-cursor-mode.rb @@ -15,11 +15,22 @@ class LineCursorMode < ScrollMode def initialize opts={} @cursor_top = @curpos = opts.delete(:skip_top_rows) || 0 @load_more_callbacks = [] - @load_more_callbacks_m = Mutex.new - @load_more_callbacks_active = false + @load_more_q = Queue.new + @load_more_thread = ::Thread.new do + while true + e = @load_more_q.pop + @load_more_callbacks.each { |c| c.call e } + end + end + super opts end + def cleanup + @load_more_thread.kill + super + end + def draw super set_status @@ -77,7 +88,7 @@ protected end def cursor_down - call_load_more_callbacks buffer.content_height if @curpos == lines - 1 + call_load_more_callbacks buffer.content_height if @curpos >= lines - [buffer.content_height/2,1].max return false unless @curpos < lines - 1 if @curpos >= botline - 1 @@ -163,21 +174,8 @@ private end def call_load_more_callbacks size - go = - @load_more_callbacks_m.synchronize do - if @load_more_callbacks_active - false - else - @load_more_callbacks_active = true - end - end - - return unless go - - @load_more_callbacks.each { |c| c.call size } - @load_more_callbacks_active = false - end - + @load_more_q.push size + end end end diff --git a/lib/sup/modes/reply-mode.rb b/lib/sup/modes/reply-mode.rb index 700dfc1..3d39a8a 100644 --- a/lib/sup/modes/reply-mode.rb +++ b/lib/sup/modes/reply-mode.rb @@ -40,7 +40,7 @@ Return value: The reply mode you desire, or nil to use the default behavior. EOS - def initialize message + def initialize message, type_arg=nil @m = message ## it's important to put this early because it forces a read of @@ -138,7 +138,9 @@ EOS hook_reply = HookManager.run "reply-to", :modes => types @type_selector.set_to( - if types.include? hook_reply + if types.include? type_arg + type_arg + elsif types.include? hook_reply hook_reply elsif @m.is_list_message? :list diff --git a/lib/sup/modes/thread-index-mode.rb b/lib/sup/modes/thread-index-mode.rb index fb6b2ce..82f258b 100644 --- a/lib/sup/modes/thread-index-mode.rb +++ b/lib/sup/modes/thread-index-mode.rb @@ -40,6 +40,7 @@ EOS k.add :save, "Save 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' @@ -76,8 +77,7 @@ EOS @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), + load_threads :num => size, :when_done => lambda { |num| @last_load_more_size = num } end end @@ -111,7 +111,7 @@ EOS mode = ThreadViewMode.new t, @hidden_labels, self BufferManager.spawn t.subj, mode BufferManager.draw_screen - mode.jump_to_first_open true + mode.jump_to_first_open 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 @@ -584,15 +584,17 @@ EOS end end - def reply + 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 + 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 @@ -627,6 +629,7 @@ EOS 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 } } diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb index dfe30ff..8ab1d85 100644 --- a/lib/sup/modes/thread-view-mode.rb +++ b/lib/sup/modes/thread-view-mode.rb @@ -1,4 +1,3 @@ -require 'open3' module Redwood class ThreadViewMode < LineCursorMode @@ -53,6 +52,7 @@ EOS k.add :toggle_new, "Toggle unread/read status of message", 'N' # k.add :collapse_non_new_messages, "Collapse all but unread messages", 'N' k.add :reply, "Reply to a message", 'r' + k.add :reply_all, "Reply to all participants of this message", 'G' k.add :forward, "Forward a message or attachment", 'f' k.add :bounce, "Bounce message to other recipient(s)", '!' k.add :alias, "Edit alias/nickname for a person", 'i' @@ -161,12 +161,14 @@ EOS update end - def reply + def reply type_arg=nil m = @message_lines[curpos] or return - mode = ReplyMode.new m + mode = ReplyMode.new m, type_arg BufferManager.spawn "Reply to #{m.subj}", mode end + def reply_all; reply :all; end + def subscribe_to_list m = @message_lines[curpos] or return if m.list_subscribe && m.list_subscribe =~ // @@ -358,16 +360,16 @@ EOS end end - def jump_to_first_open loose_alignment=false + def jump_to_first_open m = @message_lines[0] or return if @layout[m].state != :closed - jump_to_message m, loose_alignment + jump_to_message m#, true else - jump_to_next_open loose_alignment + jump_to_next_open #true end end - def jump_to_next_open loose_alignment=false + def jump_to_next_open force_alignment=nil return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] } return unless m @@ -375,15 +377,15 @@ EOS break if @layout[nextm].state != :closed m = nextm end - jump_to_message nextm, loose_alignment if nextm + jump_to_message nextm, force_alignment if nextm end def align_current_message m = @message_lines[curpos] or return - jump_to_message m + jump_to_message m, true end - def jump_to_prev_open loose_alignment=false + def jump_to_prev_open m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] } # bah, .to_a return unless m ## jump to the top of the current message if we're in the body; @@ -395,38 +397,33 @@ EOS break if @layout[prevm].state != :closed m = prevm end - jump_to_message prevm, loose_alignment if prevm + jump_to_message prevm if prevm else - jump_to_message m, loose_alignment + jump_to_message m 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 + def jump_to_message m, force_alignment=false l = @layout[m] - left = l.depth * INDENT_SPACES - right = left + l.width - ## jump to the top line - if loose_alignment - jump_to_line [l.top - IDEAL_TOP_CONTEXT, 0].max # give 3 lines of top context - else - jump_to_line l.top - end + ## boundaries of the message + message_left = l.depth * INDENT_SPACES + message_right = message_left + l.width - ## jump to the left column - 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 + ## calculate leftmost colum + left = if force_alignment # force mode: align exactly + message_left + else # regular: minimize cursor movement + ## leftmost and rightmost are boundaries of all valid left-column + ## alignments. + leftmost = [message_left, message_right - buffer.content_width + 1].min + rightmost = message_left + leftcol.clamp(leftmost, rightmost) + end - ## either way, move the cursor to the first line - set_cursor_pos l.top + jump_to_line l.top # move vertically + jump_to_col left # move horizontally + set_cursor_pos l.top # set cursor pos end def expand_all_messages