sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit 3d4b1f0e36a9633c686324b0fe0b3bd12f21a58a
parent 8d38421db6b09a1549505ec20dc4401491f1807a
Author: Eric Weikl <eric.weikl@gmx.net>
Date:   Sun,  2 Jun 2013 14:01:18 +0200

Merge branch 'sup-heliotrope/develop' into maildir-sync

Diffstat:
M .travis.yml | 2 +-
A bin/sup-psych-ify-config-files | 16 ++++++++++++++++
M lib/sup.rb | 42 +++++++++++++++++++++++++++---------------
M lib/sup/buffer.rb | 2 +-
M lib/sup/colormap.rb | 1 +
M lib/sup/draft.rb | 7 +++++--
M lib/sup/hook.rb | 8 ++++++++
M lib/sup/horizontal_selector.rb | 11 ++++++++++-
M lib/sup/message_chunks.rb | 4 ++--
M lib/sup/modes/completion_mode.rb | 6 +++---
M lib/sup/modes/edit_message_mode.rb | 11 ++++++++---
M lib/sup/modes/inbox_mode.rb | 2 +-
M lib/sup/modes/line_cursor_mode.rb | 4 ++--
M lib/sup/modes/scroll_mode.rb | 8 ++++----
M lib/sup/poll.rb | 41 +++++++++++++++++++++++++++++------------
M lib/sup/source.rb | 4 ++--
M sup.gemspec | 9 ++++++++-
A test/test_yaml_migration.rb | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A test/unit/test_horizontal_selector.rb | 40 ++++++++++++++++++++++++++++++++++++++++
19 files changed, 248 insertions(+), 50 deletions(-)
diff --git a/.travis.yml b/.travis.yml
@@ -3,7 +3,7 @@ language: ruby
 rvm:
   - 2.0.0
   - 1.9.3
-  - 1.8.7
+  - 1.9.2
 
 before_install:
   - sudo apt-get update -qq
diff --git a/bin/sup-psych-ify-config-files b/bin/sup-psych-ify-config-files
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
+
+require "sup"
+require "fileutils"
+
+Redwood.start
+
+fn = Redwood::SOURCE_FN
+FileUtils.cp fn, "#{fn}.syck_bak"
+
+Redwood::SourceManager.load_sources fn
+Redwood::SourceManager.save_sources fn, true
+
+Redwood.finish
diff --git a/lib/sup.rb b/lib/sup.rb
@@ -1,11 +1,6 @@
 require 'rubygems'
-
-require 'syck'
 require 'yaml'
-if YAML.const_defined? :ENGINE
-  YAML::ENGINE.yamler = 'syck'
-end
-
+YAML::ENGINE.yamler = 'psych'
 require 'zlib'
 require 'thread'
 require 'fileutils'
@@ -28,18 +23,23 @@ end
 class Module
   def yaml_properties *props
     props = props.map { |p| p.to_s }
-    vars = props.map { |p| "@#{p}" }
-    klass = self
-    path = klass.name.gsub(/::/, "/")
 
-    klass.instance_eval do
-      define_method(:to_yaml_properties) { vars }
-      define_method(:to_yaml_type) { "!#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}/#{path}" }
+    path = name.gsub(/::/, "/")
+    yaml_tag "!#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}/#{path}"
+
+    define_method :init_with do |coder|
+      initialize(*coder.map.values_at(*props))
     end
 
-    YAML.add_domain_type("#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}", path) do |type, val|
-      klass.new(*props.map { |p| val[p] })
+    define_method :encode_with do |coder|
+      coder.map = props.inject({}) do |hash, key|
+        hash[key] = instance_variable_get("@#{key}")
+        hash
+      end
     end
+
+    # Legacy
+    Psych.load_tags["!#{Redwood::LEGACY_YAML_DOMAIN},#{Redwood::YAML_DATE}/#{path}"] = self
   end
 end
 
@@ -59,7 +59,8 @@ module Redwood
   LOG_FN     = File.join(BASE_DIR, "log")
   SYNC_OK_FN = File.join(BASE_DIR, "sync-back-ok")
 
-  YAML_DOMAIN = "masanjin.net"
+  YAML_DOMAIN = "supmua.org"
+  LEGACY_YAML_DOMAIN = "masanjin.net"
   YAML_DATE = "2006-10-01"
 
   ## record exceptions thrown in threads nicely
@@ -347,6 +348,7 @@ require "sup/logger/singleton"
 ## determine encoding and character set
 $encoding = Locale.current.charset
 $encoding = "UTF-8" if $encoding == "utf8"
+$encoding = "UTF-8" if $encoding == "UTF8"
 if $encoding
   debug "using character set encoding #{$encoding.inspect}"
 else
@@ -354,6 +356,16 @@ else
   $encoding = "UTF-8"
 end
 
+# test encoding
+teststr = "test"
+teststr.encode('UTF-8')
+begin
+  teststr.encode($encoding)
+rescue Encoding::ConverterNotFoundError
+  warn "locale encoding is invalid, defaulting to utf-8"
+  $encoding = "UTF-8"
+end
+
 require "sup/buffer"
 require "sup/keymap"
 require "sup/mode"
diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb
@@ -709,7 +709,7 @@ EOS
     end
 
     Ncurses.mutex.lock unless opts[:sync] == false
-    Ncurses.attrset Colormap.color_for(:none)
+    Ncurses.attrset Colormap.color_for(:text_color)
     adj = @asking ? 2 : 1
     m.each_with_index do |s, i|
       Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
diff --git a/lib/sup/colormap.rb b/lib/sup/colormap.rb
@@ -26,6 +26,7 @@ class Colormap
   @@instance = nil
 
   DEFAULT_COLORS = {
+    :text => { :fg => "white", :bg => "black" },
     :status => { :fg => "white", :bg => "blue", :attrs => ["bold"] },
     :index_old => { :fg => "white", :bg => "default" },
     :index_new => { :fg => "white", :bg => "default", :attrs => ["bold"] },
diff --git a/lib/sup/draft.rb b/lib/sup/draft.rb
@@ -32,14 +32,17 @@ class DraftLoader < Source
   attr_accessor :dir
   yaml_properties
 
-  def initialize
-    dir = Redwood::DRAFT_DIR
+  def initialize dir=Redwood::DRAFT_DIR
     Dir.mkdir dir unless File.exists? dir
     super DraftManager.source_name, true, false
     @dir = dir
     @cur_offset = 0
   end
 
+  def properly_initialized?
+    !!(@dir && @cur_offset)
+  end
+
   def id; DraftManager.source_id; end
   def to_s; DraftManager.source_name; end
   def uri; DraftManager.source_name; end
diff --git a/lib/sup/hook.rb b/lib/sup/hook.rb
@@ -19,6 +19,14 @@ class HookManager
       end
     end
 
+    def flash s
+      if BufferManager.instantiated?
+        BufferManager.flash s
+      else
+        log s
+      end
+    end
+
     def log s
       info "hook[#@__name]: #{s}"
     end
diff --git a/lib/sup/horizontal_selector.rb b/lib/sup/horizontal_selector.rb
@@ -1,6 +1,8 @@
 module Redwood
 
 class HorizontalSelector
+  class UnknownValue < StandardError; end
+
   attr_accessor :label, :changed_by_user
 
   def initialize label, vals, labels, base_color=:horizontal_selector_unselected_color, selected_color=:horizontal_selector_selected_color
@@ -13,7 +15,14 @@ class HorizontalSelector
     @changed_by_user = false
   end
 
-  def set_to val; @selection = @vals.index(val) end
+  def set_to val
+    raise UnknownValue, val.inspect unless can_set_to? val
+    @selection = @vals.index(val)
+  end
+
+  def can_set_to? val
+    @vals.include? val
+  end
 
   def val; @vals[@selection] end
 
diff --git a/lib/sup/message_chunks.rb b/lib/sup/message_chunks.rb
@@ -130,7 +130,7 @@ EOS
       end
     end
 
-    def color; :none end
+    def color; :text_color end
     def patina_color; :attachment_color end
     def patina_text
       if expandable?
@@ -191,7 +191,7 @@ EOS
     def quotable?; true end
     def expandable?; false end
     def viewable?; false end
-    def color; :none end
+    def color; :text_color end
   end
 
   class Quote
diff --git a/lib/sup/modes/completion_mode.rb b/lib/sup/modes/completion_mode.rb
@@ -38,11 +38,11 @@ private
           suffix = s[(@prefix_len + 1) .. -1]
           char = s[@prefix_len].chr
 
-          @lines.last += [[:none, sprintf("%#{max_length - suffix.length - 1}s", prefix)],
+          @lines.last += [[:text_color, sprintf("%#{max_length - suffix.length - 1}s", prefix)],
                           [:completion_character_color, char],
-                          [:none, suffix + INTERSTITIAL]]
+                          [:text_color, suffix + INTERSTITIAL]]
         else
-          @lines.last += [[:none, sprintf("%#{max_length}s#{INTERSTITIAL}", s)]]
+          @lines.last += [[:text_color, sprintf("%#{max_length}s#{INTERSTITIAL}", s)]]
         end
       else
         @lines << "" if i % num_per == 0
diff --git a/lib/sup/modes/edit_message_mode.rb b/lib/sup/modes/edit_message_mode.rb
@@ -131,13 +131,18 @@ EOS
         HorizontalSelector.new "Account:", AccountManager.user_emails + [nil], user_emails_copy + ["Customized"]
 
       if @header["From"] =~ /<?(\S+@(\S+?))>?$/
-        @account_selector.set_to $1
-        @account_user = ""
+        # TODO: this is ugly. might implement an AccountSelector and handle
+        # special cases more transparently.
+        account_from = @account_selector.can_set_to?($1) ? $1 : nil
+        @account_selector.set_to account_from
       else
         @account_selector.set_to nil
-        @account_user = @header["From"]
       end
 
+      # A single source of truth might better than duplicating this in both
+      # @account_user and @account_selector.
+      @account_user = @header["From"]
+
       add_selector @account_selector
     end
 
diff --git a/lib/sup/modes/inbox_mode.rb b/lib/sup/modes/inbox_mode.rb
@@ -1,4 +1,4 @@
-require 'sup'
+require "sup/modes/thread_index_mode"
 
 module Redwood
 
diff --git a/lib/sup/modes/line_cursor_mode.rb b/lib/sup/modes/line_cursor_mode.rb
@@ -47,9 +47,9 @@ protected
 
   def draw_line ln, opts={}
     if ln == @curpos
-      super ln, :highlight => true, :debug => opts[:debug]
+      super ln, :highlight => true, :debug => opts[:debug], :color => :text_color
     else
-      super
+      super ln, :color => :text_color
     end
   end
 
diff --git a/lib/sup/modes/scroll_mode.rb b/lib/sup/modes/scroll_mode.rb
@@ -43,12 +43,12 @@ class ScrollMode < Mode
 
   def draw
     ensure_mode_validity
-    (@topline ... @botline).each { |ln| draw_line ln }
+    (@topline ... @botline).each { |ln| draw_line ln, :color => :text_color }
     ((@botline - @topline) ... buffer.content_height).each do |ln|
       if @twiddles
         buffer.write ln, 0, "~", :color => :twiddle_color
       else
-        buffer.write ln, 0, ""
+        buffer.write ln, 0, "", :color => :text_color
       end
     end
     @status = "lines #{@topline + 1}:#{@botline}/#{lines}"
@@ -208,7 +208,7 @@ protected
       # return
   end
 
-  def matching_text_array s, regex, oldcolor=:none
+  def matching_text_array s, regex, oldcolor=:text_color
     s.split(regex).map do |text|
       next if text.empty?
       if text =~ regex
@@ -244,7 +244,7 @@ protected
   end
 
   def draw_line_from_string ln, s, opts
-    buffer.write ln - @topline, 0, s[@leftcol .. -1], :highlight => opts[:highlight]
+    buffer.write ln - @topline, 0, s[@leftcol .. -1], :highlight => opts[:highlight], :color => opts[:color]
   end
 end
 
diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb
@@ -22,7 +22,11 @@ Variables:
                    num: the total number of new messages added in this poll
              num_inbox: the number of new messages added in this poll which
                         appear in the inbox (i.e. were not auto-archived).
+             num_total: the total number of messages
+       num_inbox_total: the total number of new messages in the inbox.
 num_inbox_total_unread: the total number of unread messages in the inbox
+           num_updated: the total number of updated messages
+           num_deleted: the total number of deleted messages
          from_and_subj: an array of (from email address, subject) pairs
    from_and_subj_inbox: an array of (from email address, subject) pairs for
                         only those messages appearing in the inbox
@@ -43,10 +47,13 @@ EOS
 
   def poll_with_sources
     @mode ||= PollMode.new
-    HookManager.run "before-poll"
 
-    BufferManager.flash "Polling for new messages..."
-    flash_msg = ""
+    if HookManager.enabled? "before-poll"
+      HookManager.run("before-poll")
+    else
+      BufferManager.flash "Polling for new messages..."
+    end
+
     num, numi, numu, numd, from_and_subj, from_and_subj_inbox, loaded_labels = @mode.poll
     clear_running_totals if @should_clear_running_totals
     @running_totals[:num] += num
@@ -55,18 +62,28 @@ EOS
     @running_totals[:numd] += numd
     @running_totals[:loaded_labels] += loaded_labels || []
 
-    flash_msg += "Loaded #{@running_totals[:num].pluralize 'new message'}, #{@running_totals[:numi]} to inbox. " if @running_totals[:num] > 0
-    flash_msg += "Updated #{@running_totals[:numu].pluralize 'message'}. " if @running_totals[:numu] > 0
-    flash_msg += "Deleted #{@running_totals[:numd].pluralize 'message'}. " if @running_totals[:numd] > 0
-    flash_msg += "Labels: #{@running_totals[:loaded_labels].map{|l| l.to_s}.join(', ')}." if @running_totals[:loaded_labels].size > 0
-    if flash_msg == ""
-      BufferManager.flash "No new messages."
+
+    if HookManager.enabled? "after-poll"
+      hook_args = { :num => num, :num_inbox => numi,
+                    :num_total => @running_totals[:num], :num_inbox_total => @running_totals[:numi],
+                    :num_updated => @running_totals[:numu],
+                    :num_deleted => @running_totals[:numd],
+                    :from_and_subj => from_and_subj, :from_and_subj_inbox => from_and_subj_inbox,
+                    :num_inbox_total_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] } }
+
+      HookManager.run("after-poll", hook_args)
     else
-      BufferManager.flash flash_msg
+      if @running_totals[:num] > 0
+        flash_msg = "Loaded #{@running_totals[:num].pluralize 'new message'}, #{@running_totals[:numi]} to inbox. " if @running_totals[:num] > 0
+        flash_msg += "Updated #{@running_totals[:numu].pluralize 'message'}. " if @running_totals[:numu] > 0
+        flash_msg += "Deleted #{@running_totals[:numd].pluralize 'message'}. " if @running_totals[:numd] > 0
+        flash_msg += "Labels: #{@running_totals[:loaded_labels].map{|l| l.to_s}.join(', ')}." if @running_totals[:loaded_labels].size > 0
+        BufferManager.flash flash_msg
+      else
+        BufferManager.flash "No new messages."
+      end
     end
 
-    HookManager.run "after-poll", :num => num, :num_inbox => numi, :from_and_subj => from_and_subj, :from_and_subj_inbox => from_and_subj_inbox, :num_inbox_total_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] }
-
   end
 
   def poll
diff --git a/lib/sup/source.rb b/lib/sup/source.rb
@@ -217,9 +217,9 @@ class SourceManager
     end
   end
 
-  def save_sources fn=Redwood::SOURCE_FN
+  def save_sources fn=Redwood::SOURCE_FN, force=false
     @source_mutex.synchronize do
-      if @sources_dirty
+      if @sources_dirty || force
         Redwood::save_yaml_obj sources, fn, false, true
       end
       @sources_dirty = false
diff --git a/sup.gemspec b/sup.gemspec
@@ -5,7 +5,8 @@ require 'sup/version'
 
 # Files
 SUP_EXECUTABLES = %w(sup sup-add sup-config sup-dump sup-import-dump
-  sup-recover-sources sup-sync sup-sync-back sup-tweak-labels)
+  sup-recover-sources sup-sync sup-sync-back sup-tweak-labels
+  sup-psych-ify-config-files)
 SUP_EXTRA_FILES = %w(CONTRIBUTORS README.md LICENSE History.txt ReleaseNotes)
 SUP_FILES =
   SUP_EXTRA_FILES +
@@ -33,9 +34,15 @@ module Redwood
       * Automatically tracking recent contacts
 DESC
     s.license = 'GPL-2'
+    # TODO: might want to add index migrating script here, too
+    s.post_install_message = <<-EOF
+SUP: Please run `sup-psych-ify-config-files` to migrate from 0.13 to 0.14
+    EOF
     s.files = SUP_FILES
     s.executables = SUP_EXECUTABLES
 
+    s.required_ruby_version = '>= 1.9.2'
+
     s.add_dependency "xapian-full-alaveteli", "~> 1.2"
     s.add_dependency "ncursesw-sup", "~> 1.3", ">= 1.3.1"
     s.add_dependency "rmail", ">= 0.17"
diff --git a/test/test_yaml_migration.rb b/test/test_yaml_migration.rb
@@ -0,0 +1,80 @@
+require "test_helper"
+
+require "sup"
+require "psych"
+
+describe "Sup's YAML util" do
+  describe "Module#yaml_properties" do
+    def build_class_with_name name, &b
+      Class.new do
+        meta_cls = class << self; self; end
+        meta_cls.send(:define_method, :name) { name }
+        class_exec(&b) unless b.nil?
+      end
+    end
+
+    after do
+      Psych.load_tags = {}
+      Psych.dump_tags = {}
+    end
+
+    it "defines YAML tag for class" do
+      cls = build_class_with_name 'Cls' do
+        yaml_properties
+      end
+
+      expected_yaml_tag = "!supmua.org,2006-10-01/Cls"
+
+      Psych.load_tags[expected_yaml_tag].must_equal cls
+      Psych.dump_tags[cls].must_equal expected_yaml_tag
+
+    end
+
+    it "Loads legacy YAML format as well" do
+      cls = build_class_with_name 'Cls' do
+        yaml_properties :id
+        attr_accessor :id
+        def initialize id
+          @id = id
+        end
+      end
+
+      Psych.load_tags["!masanjin.net,2006-10-01/Cls"].must_equal cls
+
+      yaml = <<EOF
+--- !masanjin.net,2006-10-01/Cls
+id: ID
+EOF
+      loaded = YAML.load(yaml)
+
+      loaded.id.must_equal 'ID'
+      loaded.must_be_kind_of cls
+    end
+
+    it "Dumps & loads w/ state re-initialized" do
+      cls = build_class_with_name 'Cls' do
+        yaml_properties :id
+        attr_accessor :id
+        attr_reader :flag
+
+        def initialize id
+          @id = id
+          @flag = true
+        end
+      end
+
+      instance = cls.new 'ID'
+
+      dumped = YAML.dump(instance)
+      loaded = YAML.load(dumped)
+
+      dumped.must_equal <<-EOF
+--- !supmua.org,2006-10-01/Cls
+id: ID
+      EOF
+
+      loaded.id.must_equal 'ID'
+      assert loaded.flag
+    end
+  end
+end
diff --git a/test/unit/test_horizontal_selector.rb b/test/unit/test_horizontal_selector.rb
@@ -0,0 +1,40 @@
+require "test_helper" 
+
+require "sup/horizontal_selector"
+
+describe Redwood::HorizontalSelector do
+  let(:values) { %w[foo@example.com bar@example.com] }
+  let(:strange_value) { "strange@example.com" }
+
+  before do
+    @selector = Redwood::HorizontalSelector.new(
+      'Acc:', values, [])
+  end
+
+  it "init w/ the first value selected" do
+    first_value = values.first
+    @selector.val.must_equal first_value
+  end
+
+  it "stores value for selection" do
+    second_value = values[1]
+    @selector.set_to second_value
+    @selector.val.must_equal second_value
+  end
+
+  describe "for unknown value" do
+    it "cannot select unknown value" do
+      @selector.wont_be :can_set_to?, strange_value
+    end
+
+    it "refuses selecting unknown value" do
+      old_value = @selector.val
+
+      assert_raises Redwood::HorizontalSelector::UnknownValue do
+        @selector.set_to strange_value
+      end
+
+      @selector.val.must_equal old_value
+    end
+  end
+end