]> git.cworth.org Git - sup/commitdiff
yet more fixes to imap, some buffer debugging, and more comment cleanups
authorwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Tue, 2 Jan 2007 19:27:15 +0000 (19:27 +0000)
committerwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Tue, 2 Jan 2007 19:27:15 +0000 (19:27 +0000)
git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@136 5c8cc53c-5e98-4d25-b20a-d8db53a31250

bin/sup
bin/sup-import
lib/sup.rb
lib/sup/buffer.rb
lib/sup/imap.rb
lib/sup/index.rb
lib/sup/mbox/ssh-file.rb
lib/sup/modes/line-cursor-mode.rb
lib/sup/modes/scroll-mode.rb

diff --git a/bin/sup b/bin/sup
index 3066d6d79d986ed3c28d64603f8afca33cb6169e..36fd15263f57c75e06a4db6af0cbbaf3f631c9f5 100644 (file)
--- a/bin/sup
+++ b/bin/sup
@@ -8,19 +8,6 @@ Thread.abort_on_exception = true # make debugging possible
 
 module Redwood
 
-$exception = nil
-def reporting_thread
-  ::Thread.new do
-    begin
-      yield
-    rescue Exception => e
-      $exception ||= e
-      raise
-    end
-  end
-end
-module_function :reporting_thread
-
 global_keymap = Keymap.new do |k|
   k.add :quit, "Quit Redwood", 'q'
   k.add :help, "Show help", 'H', '?'
@@ -115,8 +102,8 @@ begin
   bm.draw_screen
   imode.load_more_threads ibuf.content_height
 
-  # reporting_thread { sleep 3; PollManager.poll }
-  #PollManager.start_thread
+  reporting_thread { sleep 5; PollManager.poll }
+  PollManager.start_thread
 
   until $exception
     bm.draw_screen
@@ -203,23 +190,7 @@ end
 Index.save unless $exception # TODO: think about this
 
 if $exception 
-  case $exception
-  when IndexError
-    $stderr.puts <<EOS
-An error occurred while parsing a message from source:
-   #{$exception.source}.
-Typically, this means that the source has been modified in some
-way which has rendered the messages invalid. For example, if it's
-an mbox file, you may have read or deleted messages using another
-mail client.
-
-You must rebuild the index for this source. Please run:
-  sup-import --rebuild #{$exception.source}
-to correct this error.
-EOS
-#' stupid ruby-mode
-  else
-    $stderr.puts <<EOS
+  $stderr.puts <<EOS
 ----------------------------------------------------------------
 I'm very sorry, but it seems that an error occurred in Sup. 
 Please accept my sincere apologies. If you don't mind, please
@@ -234,7 +205,6 @@ William
 The problem was: #{$exception.message} (error type #{$exception.class.name})
 A backtrace follows:
 EOS
-  end
   raise $exception
 end
 
index d4fbb278ba4771a3371283befa253a9ae06374a3..46d8187b74841d2838fbf06d3d013b3e56cd3443 100644 (file)
@@ -88,6 +88,7 @@ def get_login_info uri, sources
   unless username && password
     username = ask("Username for #{uri.host}: ");
     password = ask("Password for #{uri.host}: ") { |q| q.echo = false }
+    puts # why?
   end
 
   [username, password]
@@ -133,7 +134,7 @@ sources = ARGV.map do |uri|
         username, password = get_login_info uri, index.sources
         Redwood::MBox::SSHLoader.new(uri, username, password, nil, !unusual, !!archive)
       when %r!^imaps?://!
-        username, password = get_login_info uri, sources
+        username, password = get_login_info uri, index.sources
         Redwood::IMAP.new(uri, username, password, nil, !unusual, !!archive)
       else
         Redwood::MBox::Loader.new(uri, nil, !unusual, !!archive)
index c9a1cb4a485d56b302bcfe4fd6ac05be99213c4b..b3ceba73194f5989a0a7228ba4f318148f25acd3 100644 (file)
@@ -15,7 +15,7 @@ end
 module Redwood
   VERSION = "0.0.2"
 
-  BASE_DIR   = File.join(ENV["HOME"], ".sup")
+  BASE_DIR   = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
   CONFIG_FN  = File.join(BASE_DIR, "config.yaml")
   SOURCE_FN  = File.join(BASE_DIR, "sources.yaml")
   LABEL_FN   = File.join(BASE_DIR, "labels.txt")
@@ -27,6 +27,20 @@ module Redwood
   YAML_DOMAIN = "masanjin.net"
   YAML_DATE = "2006-10-01"
 
+## record exceptions thrown in threads nicely
+  $exception = nil
+  def reporting_thread
+    ::Thread.new do
+      begin
+        yield
+      rescue Exception => e
+        $exception ||= e
+        raise
+      end
+    end
+  end
+  module_function :reporting_thread
+
 ## one-stop shop for yamliciousness
   def register_yaml klass, props
     vars = props.map { |p| "@#{p}" }
index d672bfebcc2e01a85d158fe1516acfb523326e46..4000004524d6f07da76b5de5eaee87d81d5254a3 100644 (file)
@@ -13,6 +13,9 @@ module Ncurses
     lamer.first
   end
 
+  def mutex; @mutex ||= Mutex.new; end
+  def sync &b; mutex.synchronize &b; end
+
   ## aaahhh, user input. who would have though that such a simple
   ## idea would be SO FUCKING COMPLICATED?! because apparently
   ## Ncurses.getch (and Curses.getch), even in cbreak mode, BLOCKS
@@ -37,23 +40,13 @@ module Ncurses
     end
   end
 
-  module_function :rows, :cols, :nonblocking_getch
+  module_function :rows, :cols, :nonblocking_getch, :mutex, :sync
 
   KEY_CANCEL = "\a"[0] # ctrl-g
 end
 
 module Redwood
 
-## could be a bottleneck, but doesn't seem to significantly slow
-## things down.
-
-class SafeNcurses
-  def self.method_missing meth, *a, &b
-    @mutex ||= Mutex.new
-    @mutex.synchronize { Ncurses.send meth, *a, &b }
-  end
-end
-
 class Buffer
   attr_reader :mode, :x, :y, :width, :height, :title
   bool_reader :dirty
@@ -71,9 +64,10 @@ class Buffer
   def content_width; @width; end
 
   def resize rows, cols 
-    return if rows == @width && cols == @height
+    return if cols == @width && rows == @height
     @width = cols
     @height = rows
+    @dirty = true
     mode.resize rows, cols
   end
 
@@ -82,6 +76,7 @@ class Buffer
     draw_status
     commit
   end
+
   def mark_dirty; @dirty = true; end
 
   def commit
@@ -143,7 +138,7 @@ class BufferManager
     @minibuf_stack = []
     @textfields = {}
     @flash = nil
-    @freeze = false
+    @shelled = false
 
     self.class.i_am_the_instance self
   end
@@ -151,8 +146,7 @@ class BufferManager
   def buffers; @name_map.to_a; end
 
   def focus_on buf
-    raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless
-      @buffers.member? buf
+    raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
     return if buf == @focus_buf 
     @focus_buf.blur if @focus_buf
     @focus_buf = buf
@@ -160,8 +154,7 @@ class BufferManager
   end
 
   def raise_to_front buf
-    raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless
-      @buffers.member? buf
+    raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
     @buffers.delete buf
     @buffers.push buf
     focus_on buf
@@ -189,40 +182,49 @@ class BufferManager
   end
 
   def completely_redraw_screen
-    return if @freeze
-    SafeNcurses.clear
-    @dirty = true
-    draw_screen
+    return if @shelled
+
+    Ncurses.sync do
+      @dirty = true
+      Ncurses.clear
+      draw_screen :sync => false
+    end
   end
 
   def handle_resize
-    return if @freeze
-    rows, cols = SafeNcurses.rows, SafeNcurses.cols
+    return if @shelled
+    rows, cols = Ncurses.rows, Ncurses.cols
     @buffers.each { |b| b.resize rows - minibuf_lines, cols }
     completely_redraw_screen
-    flash "resized to #{rows}x#{cols}"
+    flash "Resized to #{rows}x#{cols}"
   end
 
-  def draw_screen skip_minibuf=false
-    return if @freeze
+  def draw_screen opts={}
+    return if @shelled
+
+    Ncurses.mutex.lock unless opts[:sync] == false
 
     ## disabling this for the time being, to help with debugging
     ## (currently we only have one buffer visible at a time).
     ## TODO: reenable this if we allow multiple buffers
     false && @buffers.inject(@dirty) do |dirty, buf|
-      buf.resize SafeNcurses.rows - minibuf_lines, SafeNcurses.cols
+      buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
       @dirty ? buf.draw : buf.redraw
     end
+
     ## quick hack
     if true
       buf = @buffers.last
-      buf.resize SafeNcurses.rows - minibuf_lines, SafeNcurses.cols
+      buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
+      File.open("asdf.txt", "a") { |f| f.puts "dirty #@dirty, (re)drawing #{buf.mode.name}" }
       @dirty ? buf.draw : buf.redraw
     end
 
-    draw_minibuf unless skip_minibuf
+    draw_minibuf :sync => false unless opts[:skip_minibuf]
     @dirty = false
-    SafeNcurses.doupdate
+    Ncurses.doupdate
+    Ncurses.refresh if opts[:refresh]
+    Ncurses.mutex.unlock unless opts[:sync] == false
   end
 
   ## gets the mode from the block, which is only called if the buffer
@@ -247,9 +249,8 @@ class BufferManager
       num += 1
     end
 
-    Redwood::log "spawning buffer \"#{realtitle}\""
-    width = opts[:width] || SafeNcurses.cols
-    height = opts[:height] || SafeNcurses.rows - 1
+    width = opts[:width] || Ncurses.cols
+    height = opts[:height] || Ncurses.rows - 1
 
     ## since we are currently only doing multiple full-screen modes,
     ## use stdscr for each window. once we become more sophisticated,
@@ -257,7 +258,7 @@ class BufferManager
     ##
     ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
     ## (opts[:left] || 0))
-    w = SafeNcurses.stdscr
+    w = Ncurses.stdscr
     b = Buffer.new w, mode, width, height, :title => realtitle
     mode.buffer = b
     @name_map[realtitle] = b
@@ -277,7 +278,6 @@ class BufferManager
 
   def kill_buffer buf
     raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
-    Redwood::log "killing buffer \"#{buf.title}\""
 
     buf.mode.cleanup
     @buffers.delete buf
@@ -292,8 +292,7 @@ class BufferManager
   end
 
   def ask domain, question, default=nil
-    @textfields[domain] ||= TextField.new SafeNcurses.stdscr, SafeNcurses.rows - 1, 0,
-                            SafeNcurses.cols
+    @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
     tf = @textfields[domain]
 
     ## this goddamn ncurses form shit is a fucking 1970's
@@ -301,19 +300,19 @@ class BufferManager
     ## that needs to happen in order to display a form and have the
     ## entire screen not disappear and have the cursor in the right
     ## place is TOO FUCKING COMPLICATED.
-    tf.activate question, default
-    @dirty = true
-    draw_screen true
+    Ncurses.sync do
+      tf.activate question, default
+      @dirty = true
+      draw_screen :skip_minibuf => true, :sync => false
+    end
 
     ret = nil
-    @freeze = true
     tf.position_cursor
-    SafeNcurses.refresh
-    while tf.handle_input(SafeNcurses.nonblocking_getch); end
-    @freeze = false
+    Ncurses.sync { Ncurses.refresh }
+    while tf.handle_input(Ncurses.nonblocking_getch); end
 
     ret = tf.value
-    tf.deactivate
+    Ncurses.sync { tf.deactivate }
     @dirty = true
 
     ret
@@ -324,15 +323,17 @@ class BufferManager
     accept = accept.split(//).map { |x| x[0] } if accept
 
     flash question
-    SafeNcurses.curs_set 1
-    SafeNcurses.move SafeNcurses.rows - 1, question.length + 1
-    SafeNcurses.refresh
+    Ncurses.sync do
+      Ncurses.curs_set 1
+      Ncurses.move Ncurses.rows - 1, question.length + 1
+      Ncurses.refresh
+    end
 
     ret = nil
     done = false
-    @freeze = true
+    @shelled = true
     until done
-      key = SafeNcurses.nonblocking_getch
+      key = Ncurses.nonblocking_getch
       if key == Ncurses::KEY_CANCEL
         done = true
       elsif (accept && accept.member?(key)) || !accept
@@ -340,11 +341,15 @@ class BufferManager
         done = true
       end
     end
-    @freeze = false
-    SafeNcurses.curs_set 0
-    erase_flash
-    draw_screen
-    SafeNcurses.curs_set 0
+
+    @shelled = false
+
+    Ncurses.sync do
+      SafeNcurses.curs_set 0
+      erase_flash
+      draw_screen :sync => false
+      SafeNcurses.curs_set 0
+    end
 
     ret
   end
@@ -363,23 +368,30 @@ class BufferManager
 
   def minibuf_lines; [(@flash ? 1 : 0) + @minibuf_stack.compact.size, 1].max; end
   
-  def draw_minibuf
-    SafeNcurses.attrset Colormap.color_for(:none)
+  def draw_minibuf opts={}
     m = @minibuf_stack.compact
     m << @flash if @flash
     m << "" if m.empty?
+
+    Ncurses.mutex.lock unless opts[:sync] == false
+    Ncurses.attrset Colormap.color_for(:none)
     m.each_with_index do |s, i|
-      SafeNcurses.mvaddstr SafeNcurses.rows - i - 1, 0, s + (" " * [SafeNcurses.cols - s.length, 0].max)
+      Ncurses.mvaddstr Ncurses.rows - i - 1, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
     end
+    Ncurses.refresh if opts[:refresh]
+    Ncurses.mutex.unlock unless opts[:sync] == false
   end
 
   def say s, id=nil
+    new_id = id.nil?
     id ||= @minibuf_stack.length
     @minibuf_stack[id] = s
-    unless @freeze
-      draw_screen
-      SafeNcurses.refresh
+    if new_id
+      draw_screen :refresh => true
+    else
+      draw_minibuf :refresh => true
     end
+
     if block_given?
       begin
         yield
@@ -394,10 +406,7 @@ class BufferManager
 
   def flash s
     @flash = s
-    unless @freeze
-      draw_screen
-      SafeNcurses.refresh
-    end
+    draw_screen :refresh => true
   end
 
   ## a little tricky because we can't just delete_at id because ids
@@ -406,24 +415,23 @@ class BufferManager
     @minibuf_stack[id] = nil
     if id == @minibuf_stack.length - 1
       id.downto(0) do |i|
-        break unless @minibuf_stack[i].nil?
+        break if @minibuf_stack[i]
         @minibuf_stack.delete_at i
       end
     end
 
-    unless @freeze
-      draw_screen
-      SafeNcurses.refresh
-    end
+    draw_screen :refresh => true
   end
 
   def shell_out command
-    @freeze = true
-    SafeNcurses.endwin
-    system command
-    SafeNcurses.refresh
-    SafeNcurses.curs_set 0
-    @freeze = false
+    @shelled = true
+    Ncurses.sync do
+      Ncurses.endwin
+      system command
+      Ncurses.refresh
+      Ncurses.curs_set 0
+    end
+    @shelled = false
   end
 end
 end
index 06ade3184803cf4f510b8fe81fb50e9dd875627a..296a5d3b57783c85a9767497f7e1dc001e9f1d34 100644 (file)
@@ -52,6 +52,16 @@ class IMAP < Source
     @mutex = Mutex.new
   end
 
+  def say s
+    @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
+    Redwood::log s
+  end
+  def shutup
+    BufferManager.clear @say_id if BufferManager.instantiated?
+    @say_id = nil
+  end
+  private :say, :shutup
+
   def connect
     return false if broken?
     return true if @imap
@@ -69,28 +79,22 @@ class IMAP < Source
     ##
     ## FUCK!!!!!!!!!
 
-    Redwood::log "connecting to #{@parsed_uri.host} port #{ssl? ? 993 : 143}, ssl=#{ssl?} ..."
-    sid = BufferManager.say "Connecting to IMAP server #{host}..." if BufferManager.instantiated?
+    say "Connecting to IMAP server #{host}:#{port}..."
 
     Redwood::reporting_thread do
       begin
         #raise Net::IMAP::ByeResponseError, "simulated imap failure"
-        # @imap = Net::IMAP.new host, ssl? ? 993 : 143, ssl?
-        sleep 3
-        BufferManager.say "Logging in...", sid if BufferManager.instantiated?
-        # @imap.authenticate 'LOGIN', @username, @password
-        sleep 3
-        BufferManager.say "Sizing mailbox...", sid if BufferManager.instantiated?
-        # @imap.examine mailbox
-        # last_id = @imap.responses["EXISTS"][-1]
-        sleep 1
+        @imap = Net::IMAP.new host, ssl? ? 993 : 143, ssl?
+        say "Logging in..."
+        @imap.authenticate 'LOGIN', @username, @password
+        say "Sizing mailbox..."
+        @imap.examine mailbox
+        last_id = @imap.responses["EXISTS"][-1]
         
-        BufferManager.say "Reading headers (because IMAP sucks)...", sid if BufferManager.instantiated?
-        # values = @imap.fetch(1 .. last_id, ['RFC822.SIZE', 'INTERNALDATE'])
-        sleep 3
+        say "Reading headers (because IMAP sucks)..."
+        values = @imap.fetch(1 .. last_id, ['RFC822.SIZE', 'INTERNALDATE'])
         
-        raise Net::IMAP::ByeResponseError, "simulated imap failure"
-        Redwood::log "successfully connected to #{@parsed_uri}"
+        say "Successfully connected to #{@parsed_uri}"
         
         values.each do |v|
           id = make_id v
@@ -101,24 +105,24 @@ class IMAP < Source
         self.broken_msg = e.message.chomp # fucking chomp! fuck!!!
         @imap = nil
         Redwood::log "error connecting to IMAP server: #{self.broken_msg}"
-      ensure 
-        BufferManager.clear sid if BufferManager.instantiated?
+      ensure
+        shutup
       end
     end.join
 
-    @mutex.unlock
     !!@imap
   end
   private :connect
 
   def make_id imap_stuff
     msize, mdate = imap_stuff.attr['RFC822.SIZE'], Time.parse(imap_stuff.attr["INTERNALDATE"])
-    sprintf("%d.%07d", mdate.to_i, msize).to_i
+    sprintf("%d%07d", mdate.to_i, msize).to_i
   end
   private :make_id
 
   def host; @parsed_uri.host; end
-  def mailbox; @parsed_uri.path[1..-1] end ##XXXX TODO handle nil
+  def port; @parsed_uri.port || (ssl? ? 993 : 143); end
+  def mailbox; @parsed_uri.path[1..-1] || 'INBOX'; end
   def ssl?; @parsed_uri.scheme == 'imaps' end
 
   def load_header id
@@ -146,17 +150,16 @@ class IMAP < Source
 
   def get_imap_field id, field
     f = nil
-    @mutex.synchronize do
-      imap_id = @imap_ids[id] or raise SourceError, "Unknown message id #{id}. It is likely that messages have been deleted from this IMAP mailbox."
-      begin
-        f = @imap.fetch imap_id, [field, 'RFC822.SIZE', 'INTERNALDATE']
-        got_id = make_id f
-        raise SourceError, "IMAP message mismatch: requested #{id}, got #{got_id}. It is likely the IMAP mailbox has been modified." unless got_id == id
-      rescue Net::IMAP::Error => e
-        raise SourceError, e.message
-      end
-      raise SourceError, "null IMAP field '#{field}' for message with id #{id} imap id #{imap_id}" if f.nil?
+    imap_id = @imap_ids[id] or raise SourceError, "Unknown message id #{id}. It is likely that messages have been deleted from this IMAP mailbox."
+    begin
+      f = @imap.fetch imap_id, [field, 'RFC822.SIZE', 'INTERNALDATE']
+      got_id = make_id f[0]
+      raise SourceError, "IMAP message mismatch: requested #{id}, got #{got_id}. It is likely the IMAP mailbox has been modified." unless got_id == id
+    rescue Net::IMAP::Error => e
+      raise SourceError, e.message
     end
+    raise SourceError, "null IMAP field '#{field}' for message with id #{id} imap id #{imap_id}" if f.nil?
+
     f[0].attr[field]
   end
   private :get_imap_field
@@ -176,6 +179,7 @@ class IMAP < Source
     @mutex.synchronize { connect or raise SourceError, broken_msg }
     @ids.first
   end
+
   def end_offset
     @mutex.synchronize { connect or raise SourceError, broken_msg }
     @ids.last
index c4078941a1e067266ddcbb06d89344f86bc6cfd6..2e5ba49872481cd46ddb5322f16b870fe7bf172c 100644 (file)
@@ -7,15 +7,6 @@ require 'ferret'
 
 module Redwood
 
-class IndexError < StandardError
-  attr_reader :source
-
-  def initialize source, s
-    super s
-    @source = source
-  end
-end
-
 class Index
   include Singleton
 
index 9e91d709101d59492248dba45184ab6a87d883e6..50a217265f0a7ba9d99d0768fe058af4f3010f86 100644 (file)
@@ -78,7 +78,7 @@ end
 ## the file-like interface to a remote file
 class SSHFile
   MAX_BUF_SIZE = 1024 * 1024 # bytes
-  MAX_TRANSFER_SIZE = 1024 * 64
+  MAX_TRANSFER_SIZE = 1024 * 128
   REASONABLE_TRANSFER_SIZE = 1024 * 32
   SIZE_CHECK_INTERVAL = 60 * 1 # seconds
 
@@ -99,12 +99,11 @@ class SSHFile
     @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
     Redwood::log s
   end
-  private :say
-
   def shutup
     BufferManager.clear @say_id if BufferManager.instantiated?
     @say_id = nil
   end
+  private :say, :shutup
 
   def connect
     return if @session
@@ -114,14 +113,10 @@ class SSHFile
 
     begin
       #raise SSHFileError, "simulated SSH file error"
-      #@session = Net::SSH.start @host, @ssh_opts
-      sleep 3
+      @session = Net::SSH.start @host, @ssh_opts
       say "Starting SSH shell..."
-      # @shell = @session.shell.sync
-      sleep 3
+      @shell = @session.shell.sync
       say "Checking for #@fn..."
-      sleep 1
-      raise Errno::ENOENT, @fn
       raise Errno::ENOENT, @fn unless @shell.test("-e #@fn").status == 0
     ensure
       shutup
@@ -161,7 +156,7 @@ private
     begin
       retries = 0
       connect
-      MBox::debug "sending command: #{cmd.inspect}"
+      MBox::debug "sending command: #{cmd.inspect}"
       begin
         result = @shell.send_command cmd
         raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{result.stderr[0 .. 100]}" unless result.status == 0
index f8a46e951d881c595359a42f91591794f5ae85b4..373a5fc1fec74d131621dedc576f974797666c61 100644 (file)
@@ -79,7 +79,6 @@ protected
     if @curpos == topline
       page_up
       set_cursor_pos [botline - 2, topline].max
-#      raise "cursor position now #@curpos, topline #{topline} botline #{botline}"
     else
       @curpos -= 1
       unless buffer.dirty?
index 3085607c5e31475efaa380b586eac4165e3f10a6..d5978b67e3eaa75581d01c61f91a9e4aa3c4825f 100644 (file)
@@ -91,6 +91,12 @@ protected
                    :highlight => opts[:highlight]
     when Array
       xpos = 0
+
+      ## speed test
+      # str = s.map { |color, text| text }.join
+      # buffer.write ln - @topline, 0, str, :color => :none, :highlight => opts[:highlight]
+      # return
+
       s.each do |color, text|
         raise "nil text for color '#{color}'" if text.nil? # good for debugging
         if xpos + text.length < @leftcol