tktextio.rb   [plain text]


#!/usr/bin/env ruby
#
#  TkTextIO class :: handling I/O stream on a TkText widget
#                             by Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp)
#
#  NOTE: TkTextIO supports 'character' (not 'byte') access only. 
#        So, for example, TkTextIO#getc returns a character, TkTextIO#pos 
#        means the character position, TkTextIO#read(size) counts by 
#        characters, and so on.
#        Of course, it is available to make TkTextIO class to suuport 
#        'byte' access. However, it may break multi-byte characters. 
#        and then, displayed string on the text widget may be garbled.
#        I think that it is not good on the supposed situation of using 
#        TkTextIO. 
#
require 'tk'
require 'tk/text'
require 'tk/textmark'
require 'thread'

class TkTextIO < TkText
  # keep safe level
  @@create_queues = proc{ [Queue.new, Mutex.new, Queue.new, Mutex.new] }

  OPT_DEFAULTS = {
    'mode'       => nil,
    'overwrite'  => false, 
    'text'       => nil, 
    'show'       => :pos, 
    'wrap'       => 'char', 
    'sync'       => true, 
    'prompt'     => nil, 
    'prompt_cmd' => nil, 
    'hist_size'  => 1000, 
  }

  def create_self(keys)
    opts = _get_io_params((keys.kind_of?(Hash))? keys: {})

    super(keys)

    @count_var = TkVariable.new

    @write_buffer = ''
    @read_buffer  = ''
    @buf_size = 0
    @buf_max = 1024

    @write_buf_queue, @write_buf_mutex, 
    @read_buf_queue,  @read_buf_mutex  = @@create_queues.call

    @idle_flush  = TkTimer.new(:idle, 1, proc{ @flusher.run rescue nil })
    @timer_flush = TkTimer.new(250, -1, proc{ @flusher.run rescue nil })

    @flusher = Thread.new{ loop { Thread.stop; flush() } }

    @receiver = Thread.new{
      begin
        loop {
          str = @write_buf_queue.deq
          @write_buf_mutex.synchronize { @write_buffer << str }
          @idle_flush.start
        }
      ensure
        @flusher.kill
      end
    }

    @timer_flush.start

    _setup_io(opts)
  end
  private :create_self

  def destroy
    @flusher.kill rescue nil

    @idle_flush.stop rescue nil
    @timer_flush.stop rescue nil

    @receiver.kill rescue nil

    super()
  end

  ####################################

  def _get_io_params(keys)
    opts = {}
    self.class.const_get(:OPT_DEFAULTS).each{|k, v| 
      if keys.has_key?(k)
        opts[k] = keys.delete(k)
      else
        opts[k] = v
      end
    }
    opts
  end

  def _setup_io(opts)
    unless defined? @txtpos
      @txtpos = TkTextMark.new(self, '1.0')
    else
      @txtpos.set('1.0')
    end
    @txtpos.gravity = :left

    @lineno = 0
    @line_offset = 0

    @hist_max = opts['hist_size'].to_i
    @hist_index = 0
    @history = Array.new(@hist_max)
    @history[0] = ''

    self['wrap'] = wrap

    self.show_mode = opts['show']

    self.value = opts['text'] if opts['text']

    @overwrite = (opts['overwrite'])? true: false

    @sync = opts['sync']

    @prompt = opts['prompt']
    @prompt_cmd = opts['prompt_cmd']

    @open  = {:r => true,  :w => true}  # default is 'r+'

    @console_mode = false
    @end_of_stream = false
    @console_buffer = nil

    case opts['mode']
    when nil
      # do nothing

    when :console, 'console'
      @console_mode = true
      # @console_buffer = TkTextIO.new(:mode=>'r')
      @console_buffer = self.class.new(:mode=>'r')
      self.show_mode = :insert

    when 'r', 'rb'
      @open[:r] = true; @open[:w] = nil

    when 'r+', 'rb+', 'r+b'
      @open[:r] = true; @open[:w] = true

    when 'w', 'wb'
      @open[:r] = nil;  @open[:w] = true
      self.value=''

    when 'w+', 'wb+', 'w+b'
      @open[:r] = true; @open[:w] = true
      self.value=''

    when 'a', 'ab'
      @open[:r] = nil;  @open[:w] = true
      @txtpos.set('end - 1 char')
      @txtpos.gravity = :right

    when 'a+', 'ab+', 'a+b'
      @open[:r] = true;  @open[:w] = true
      @txtpos.set('end - 1 char')
      @txtpos.gravity = :right

    else
      fail ArgumentError, "unknown mode `#{opts['mode']}'"
    end

    unless defined? @ins_head
      @ins_head = TkTextMark.new(self, 'insert')
      @ins_head.gravity = :left
    end

    unless defined? @ins_tail
      @ins_tail = TkTextMark.new(self, 'insert')
      @ins_tail.gravity = :right
    end

    unless defined? @tmp_mark
      @tmp_mark = TkTextMark.new(self, 'insert')
      @tmp_mark.gravity = :left
    end

    if @console_mode
      _set_console_line
      _setup_console_bindings
    end
  end
  private :_get_io_params, :_setup_io

  def _set_console_line
    @tmp_mark.set(@ins_tail)

    mark_set('insert', 'end')

    prompt = ''
    prompt << @prompt_cmd.call if @prompt_cmd
    prompt << @prompt if @prompt
    insert(@tmp_mark, prompt)

    @ins_head.set(@ins_tail)
    @ins_tail.set('insert')

    @txtpos.set(@tmp_mark)

    _see_pos
  end

  def _replace_console_line(str)
    self.delete(@ins_head, @ins_tail)
    self.insert(@ins_head, str)
  end

  def _get_console_line
    @tmp_mark.set(@ins_tail)
    s = self.get(@ins_head, @tmp_mark)
    _set_console_line
    s
  end
  private :_set_console_line, :_replace_console_line, :_get_console_line

  def _cb_up
    @history[@hist_index].replace(self.get(@ins_head, @ins_tail))
    @hist_index += 1
    @hist_index -= 1 if @hist_index >= @hist_max || !@history[@hist_index]
    _replace_console_line(@history[@hist_index]) if @history[@hist_index]
    Tk.callback_break
  end
  def _cb_down
    @history[@hist_index].replace(self.get(@ins_head, @ins_tail))
    @hist_index -= 1
    @hist_index = 0 if @hist_index < 0
    _replace_console_line(@history[@hist_index]) if @history[@hist_index]
    Tk.callback_break
  end
  def _cb_left
    if @console_mode && compare('insert', '<=', @ins_head)
      mark_set('insert', @ins_head)
      Tk.callback_break
    end
  end
  def _cb_backspace
    if @console_mode && compare('insert', '<=', @ins_head)
      Tk.callback_break
    end
  end
  def _cb_ctrl_a
    if @console_mode
      mark_set('insert', @ins_head)
      Tk.callback_break
    end
  end
  private :_cb_up, :_cb_down, :_cb_left, :_cb_backspace, :_cb_ctrl_a

  def _setup_console_bindings
    @bindtag = TkBindTag.new

    tags = self.bindtags
    tags[tags.index(self)+1, 0] = @bindtag
    self.bindtags = tags

    @bindtag.bind('Return'){
      insert('end - 1 char', "\n")
      if (str = _get_console_line)
        @read_buf_queue.push(str)

        @history[0].replace(str.chomp)
        @history.pop
        @history.unshift('')
        @hist_index = 0
      end

      Tk.update
      Tk.callback_break
    }
    @bindtag.bind('Alt-Return'){
      Tk.callback_continue
    }

    @bindtag.bind('FocusIn'){
      if @console_mode
        mark_set('insert', @ins_tail)
        Tk.callback_break
      end
    }

    ins_mark = TkTextMark.new(self, 'insert')

    @bindtag.bind('ButtonPress'){
      if @console_mode
        ins_mark.set('insert')
      end
    }

    @bindtag.bind('ButtonRelease-1'){
      if @console_mode && compare('insert', '<=', @ins_head)
        mark_set('insert', ins_mark)
        Tk.callback_break
      end
    }

    @bindtag.bind('ButtonRelease-2', '%x %y'){|x, y|
      if @console_mode
        # paste a text at 'insert' only
        x1, y1, x2, y2 =  bbox(ins_mark)
        unless x == x1 && y == y1
          Tk.event_generate(self, 'ButtonRelease-2', :x=>x1, :y=>y1)
          Tk.callback_break
        end
      end
    }

    @bindtag.bind('Up'){ _cb_up }
    @bindtag.bind('Control-p'){ _cb_up }

    @bindtag.bind('Down'){ _cb_down }
    @bindtag.bind('Control-n'){ _cb_down }

    @bindtag.bind('Left'){ _cb_left }
    @bindtag.bind('Control-b'){ _cb_left }

    @bindtag.bind('BackSpace'){ _cb_backspace }
    @bindtag.bind('Control-h'){ _cb_backspace }

    @bindtag.bind('Home'){ _cb_ctrl_a }
    @bindtag.bind('Control-a'){ _cb_ctrl_a }
  end
  private :_setup_console_bindings

  def _block_read(size = nil, ret = '', block_mode = true)
    return '' if size == 0
    return nil if ! @read_buf_queue && @read_buffer.empty?
    ret = '' unless ret.kind_of?(String)
    ret.replace('') unless ret.empty?

    if block_mode == nil # partial
      if @read_buffer.empty?
        ret << @read_buffer.slice!(0..-1)
        return ret
      end
    end

    if size.kind_of?(Numeric)
      loop{
        @read_buf_mutex.synchronize {
          buf_len = @read_buffer.length
          if buf_len >= size
            ret << @read_buffer.slice!(0, size)
            return ret
          else
            ret << @read_buffer.slice!(0..-1)
            size -= buf_len
            return ret unless @read_buf_queue
          end
        }
        @read_buffer << @read_buf_queue.pop
      }
    else # readline
      rs = (size)? size: $/
      rs = rs.to_s if rs.kind_of?(Regexp)
      loop{
        @read_buf_mutex.synchronize {
          if (str = @read_buffer.slice!(/\A(.*)(#{rs})/m))
            ret << str
            return ret
          else
            ret << @read_buffer.slice!(0..-1)
            return ret unless @read_buf_queue
          end
        }
        @read_buffer << @read_buf_queue.pop
      }
    end
  end

  def _block_write
    ###### currently, not support
  end
  private :_block_read, :_block_write

  ####################################

  def <<(obj)
    _write(obj)
    self
  end

  def binmode
    self
  end

  def clone
    fail NotImplementedError, 'cannot clone TkTextIO'
  end
  def dup
    fail NotImplementedError, 'cannot duplicate TkTextIO'
  end

  def close
    close_read
    close_write
    nil
  end
  def close_read
    @open[:r] = false if @open[:r]
    nil
  end
  def close_write
    @open[:w] = false if @opne[:w]
    nil
  end

  def closed?(dir=nil)
    case dir
    when :r, 'r'
      !@open[:r]
    when :w, 'w'
      !@open[:w]
    else
      !@open[:r] && !@open[:w]
    end
  end

  def _check_readable
    fail IOError, "not opened for reading" if @open[:r].nil?
    fail IOError, "closed stream" if !@open[:r]
  end
  def _check_writable
    fail IOError, "not opened for writing" if @open[:w].nil?
    fail IOError, "closed stream" if !@open[:w]
  end
  private :_check_readable, :_check_writable

  def each_line(rs = $/)
    _check_readable
    while(s = self.gets(rs))
      yield(s)
    end
    self
  end
  alias each each_line

  def each_char
    _check_readable
    while(c = self.getc)
      yield(c)
    end
    self
  end
  alias each_byte each_char

  def eof?
    compare(@txtpos, '==', 'end - 1 char')
  end
  alias eof eof?

  def fcntl(*args)
    fail NotImplementedError, "fcntl is not implemented on #{self.class}"
  end

  def fsync
    0
  end

  def fileno
    nil
  end

  def flush
    Thread.pass
    if @open[:w] || ! @write_buffer.empty?
      @write_buf_mutex.synchronize {
        _sync_write_buf(@write_buffer) 
        @write_buffer[0..-1] = ''
      }
    end
    self
  end

  def getc
    return _block_read(1) if @console_mode

    _check_readable
    return nil if eof?
    c = get(@txtpos)
    @txtpos.set(@txtpos + '1 char')
    _see_pos
    c
  end

  def gets(rs = $/)
    return _block_read(rs) if @console_mode

    _check_readable
    return nil if eof?
    _readline(rs)
  end

  def ioctrl(*args)
    fail NotImplementedError, 'iocntl is not implemented on TkTextIO'
  end

  def isatty
    false
  end
  def tty?
    false
  end

  def lineno
    @lineno + @line_offset
  end

  def lineno=(num)
    @line_offset = num - @lineno
    num
  end

  def overwrite?
    @overwrite
  end

  def overwrite=(ovwt)
    @overwrite = (ovwt)? true: false
  end

  def pid
    nil
  end

  def index_pos
    index(@txtpos)
  end
  alias tell_index index_pos

  def index_pos=(idx)
    @txtpos.set(idx)
    @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
    _see_pos
    idx
  end

  def pos
    s = get('1.0', @txtpos)
    number(tk_call('string', 'length', s))
  end
  alias tell pos

  def pos=(idx)
    seek(idx, IO::SEEK_SET)
    idx
  end

  def pos_gravity
    @txtpos.gravity
  end

  def pos_gravity=(side)
    @txtpos.gravity = side
    side
  end

  def print(arg=$_, *args)
    _check_writable
    args.unshift(arg)
    args.map!{|val| (val == nil)? 'nil': val.to_s }
    str = args.join($,)
    str << $\ if $\
    _write(str)
    nil
  end
  def printf(*args)
    _check_writable
    _write(sprintf(*args))
    nil
  end

  def putc(c)
    _check_writable
    c = c.chr if c.kind_of?(Fixnum)
    _write(c)
    c
  end

  def puts(*args)
    _check_writable
    if args.empty?
      _write("\n")
      return nil
    end
    args.each{|arg|
      if arg == nil
        _write("nil\n")
      elsif arg.kind_of?(Array)
        puts(*arg)
      elsif arg.kind_of?(String)
        _write(arg.chomp)
        _write("\n")
      else
        begin
          arg = arg.to_ary
          puts(*arg)
        rescue
          puts(arg.to_s)
        end
      end
    }
    nil
  end

  def _read(len)
    epos = @txtpos + "#{len} char"
    s = get(@txtpos, epos)
    @txtpos.set(epos)
    @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
    _see_pos
    s
  end
  private :_read

  def read(len=nil, buf=nil)
    return _block_read(len, buf) if @console_mode

    _check_readable
    if len
      return "" if len == 0
      return nil if eof?
      s = _read(len)
    else
      s = get(@txtpos, 'end - 1 char')
      @txtpos.set('end - 1 char')
      _see_pos
    end
    buf.replace(s) if buf.kind_of?(String)
    s
  end

  def readchar
    return _block_read(1) if @console_mode

    _check_readable
    fail EOFError if eof?
    c = get(@txtpos)
    @txtpos.set(@txtpos + '1 char')
    _see_pos
    c
  end

  def _readline(rs = $/)
    if rs == nil
      s = get(@txtpos, 'end - 1 char')
      @txtpos.set('end - 1 char')
    elsif rs == ''
      @count_var.value  # make it global
      idx = tksearch_with_count([:regexp], @count_var, 
                                   "\n(\n)+", @txtpos, 'end - 1 char')
      if idx
        s = get(@txtpos, idx) << "\n"
        @txtpos.set("#{idx} + #{@count_var.value} char")
        @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
      else
        s = get(@txtpos, 'end - 1 char')
        @txtpos.set('end - 1 char')
      end
    else
      @count_var.value  # make it global
      idx = tksearch_with_count(@count_var, rs, @txtpos, 'end - 1 char')
      if idx
        s = get(@txtpos, "#{idx} + #{@count_var.value} char")
        @txtpos.set("#{idx} + #{@count_var.value} char")
        @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
      else
        s = get(@txtpos, 'end - 1 char')
        @txtpos.set('end - 1 char')
      end
    end

    _see_pos
    @lineno += 1
    $_ = s
  end
  private :_readline

  def readline(rs = $/)
    return _block_readline(rs) if @console_mode

    _check_readable
    fail EOFError if eof?
    _readline(rs)
  end

  def readlines(rs = $/)
    if @console_mode
      lines = []
      while (line = _block_readline(rs))
        lines << line
      end
      return lines
    end

    _check_readable
    lines = []
    until(eof?)
      lines << _readline(rs)
    end
    $_ = nil
    lines
  end

  def readpartial(maxlen, buf=nil)
    #return @console_buffer.readpartial(maxlen, buf) if @console_mode
    return _block_read(maxlen, buf, nil) if @console_mode

    _check_readable
    fail EOFError if eof?
    s = _read(maxlen)
    buf.replace(s) if buf.kind_of?(String)
    s
  end

  def reopen(*args)
    fail NotImplementedError, 'reopen is not implemented on TkTextIO'
  end

  def rewind
    @txtpos.set('1.0')
    _see_pos
    @lineno = 0
    @line_offset = 0
    self
  end

  def seek(offset, whence=IO::SEEK_SET)
    case whence
    when IO::SEEK_SET
      offset = "1.0 + #{offset} char" if offset.kind_of?(Numeric)
      @txtpos.set(offset)

    when IO::SEEK_CUR
      offset = "#{offset} char" if offset.kind_of?(Numeric)
      @txtpos.set(@txtpos + offset)

    when IO::SEEK_END
      offset = "#{offset} char" if offset.kind_of?(Numeric)
      @txtpos.set("end - 1 char + #{offset}")

    else
      fail Errno::EINVAL, 'invalid whence argument'
    end

    @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
    _see_pos

    0
  end
  alias sysseek seek

  def _see_pos
    see(@show) if @show
  end
  private :_see_pos

  def show_mode
    (@show == @txtpos)? :pos : @show
  end

  def show_mode=(mode)
    # define show mode  when file position is changed. 
    #  mode == :pos or "pos" or true :: see current file position. 
    #  mode == :insert or "insert"   :: see insert cursor position. 
    #  mode == nil or false          :: do nothing
    #  else see 'mode' position ('mode' should be text index or mark)
    case mode
    when :pos, 'pos', true
      @show = @txtpos
    when :insert, 'insert'
      @show = :insert
    when nil, false
      @show = false
    else
      begin
        index(mode)
      rescue
        fail ArgumentError, 'invalid show-position'
      end
      @show = mode
    end

    _see_pos

    mode
  end

  def stat
    fail NotImplementedError, 'stat is not implemented on TkTextIO'
  end

  def sync
    @sync
  end

  def sync=(mode)
    @sync = mode
  end

  def sysread(len, buf=nil)
    return _block_read(len, buf) if @console_mode

    _check_readable
    fail EOFError if eof?
    s = _read(len)
    buf.replace(s) if buf.kind_of?(String)
    s
  end

  def syswrite(obj)
    _write(obj)
  end

  def to_io
    self
  end

  def trancate(len)
    delete("1.0 + #{len} char", :end)
    0
  end

  def ungetc(c)
    if @console_mode
      @read_buf_mutex.synchronize {
        @read_buffer[0,0] = c.chr
      }
      return nil
    end

    _check_readable
    c = c.chr if c.kind_of?(Fixnum)
    if compare(@txtpos, '>', '1.0')
      @txtpos.set(@txtpos - '1 char')
      delete(@txtpos)
      insert(@txtpos, tk_call('string', 'range', c, 0, 1))
      @txtpos.set(@txtpos - '1 char') if @txtpos.gravity == 'right'
      _see_pos
    else
      fail IOError, 'cannot ungetc at head of stream'
    end
    nil
  end

=begin
  def _write(obj)
    #s = _get_eval_string(obj)
    s = (obj.kind_of?(String))? obj: obj.to_s
    n = number(tk_call('string', 'length', s))
    delete(@txtpos, @txtpos + "#{n} char") if @overwrite
    self.insert(@txtpos, s)
    @txtpos.set(@txtpos + "#{n} char")
    @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
    _see_pos
    Tk.update if @sync
    n
  end
  private :_write
=end
#=begin
  def _sync_write_buf(s)
    if (n = number(tk_call('string', 'length', s))) > 0
      delete(@txtpos, @txtpos + "#{n} char") if @overwrite
      self.insert(@txtpos, s)
      #Tk.update

      @txtpos.set(@txtpos + "#{n} char")
      @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)

      @ins_head.set(@txtpos) if compare(@txtpos, '>', @ins_head)

      _see_pos
    end
    self
  end
  private :_sync_write_buf

  def _write(obj)
    s = (obj.kind_of?(String))? obj: obj.to_s
    n = number(tk_call('string', 'length', s))
    @write_buf_queue.enq(s)
    if @sync
      Thread.pass
      Tk.update
    end
    n
  end
  private :_write
#=end

  def write(obj)
    _check_writable
    _write(obj)
  end
end

####################
#  TEST
####################
if __FILE__ == $0
  ev_loop = Thread.new{Tk.mainloop}

  f = TkFrame.new.pack
  #tio = TkTextIO.new(f, :show=>:nil, 
  #tio = TkTextIO.new(f, :show=>:pos, 
  tio = TkTextIO.new(f, :show=>:insert, 
                     :text=>">>> This is an initial text line. <<<\n\n"){
#    yscrollbar(TkScrollbar.new(f).pack(:side=>:right, :fill=>:y))
    pack(:side=>:left, :fill=>:both, :expand=>true)
  }

  Tk.update

  $stdin  = tio
  $stdout = tio
  $stderr = tio

  STDOUT.print("\n========= TkTextIO#gets for inital text ========\n\n")

  while(s = gets)
    STDOUT.print(s)
  end

  STDOUT.print("\n============ put strings to TkTextIO ===========\n\n")

  puts "On this sample, a text widget works as if it is a I/O stream."
  puts "Please see the code."
  puts
  printf("printf message: %d %X\n", 123456, 255)
  puts
  printf("(output by 'p' method) This TkTextIO object is ...\n")
  p tio
  print(" [ Current wrap mode of this object is 'char'. ]\n")
  puts
  warn("This is a warning message generated by 'warn' method.")
  puts
  puts "current show_mode is #{tio.show_mode}."
  if tio.show_mode == :pos
    puts "So, you can see the current file position on this text widget."
  else
    puts "So, you can see the position '#{tio.show_mode}' on this text widget."
  end
  print("Please scroll up this text widget to see the head of lines.\n")
  print("---------------------------------------------------------\n")

  STDOUT.print("\n=============== TkTextIO#readlines =============\n\n")

  tio.seek(0)
  lines = readlines
  STDOUT.puts(lines.inspect)

  STDOUT.print("\n================== TkTextIO#each ===============\n\n")

  tio.rewind
  tio.each{|line| STDOUT.printf("%2d: %s\n", tio.lineno, line.chomp)}

  STDOUT.print("\n================================================\n\n")

  STDOUT.print("\n========= reverse order (seek by lines) ========\n\n")

  tio.seek(-1, IO::SEEK_END)
  begin
    begin
      tio.seek(:linestart, IO::SEEK_CUR)
    rescue
      # maybe use old version of tk/textmark.rb
      tio.seek('0 char linestart', IO::SEEK_CUR)
    end
    STDOUT.print(gets)
    tio.seek('-1 char linestart -1 char', IO::SEEK_CUR)
  end while(tio.pos > 0)

  STDOUT.print("\n================================================\n\n")

  tio.seek(0, IO::SEEK_END)

  STDOUT.print("tio.sync ==  #{tio.sync}\n")
#  tio.sync = false
#  STDOUT.print("tio.sync ==  #{tio.sync}\n")

  (0..10).each{|i|
    STDOUT.print("#{i}\n")
    s = ''
    (0..1000).each{ s << '*' }
    print(s)
  }
  print("\n")
  print("\n=========================================================\n\n")

  s = ''
  timer = TkTimer.new(:idle, -1, proc{
                        #STDOUT.print("idle call\n")
                        unless s.empty?
                          print(s)
                          s = ''
                        end
                      }).start
  (0..10).each{|i|
    STDOUT.print("#{i}\n")
    (0..1000).each{ s << '*' }
  }
#  timer.stop
  until s.empty?
    sleep 0.1
  end
  timer.stop

=begin
  tio.sync = false
  print("\n")
  #(0..10000).each{ putc('*') }
  (0..10).each{|i|
    STDOUT.print("#{i}\n")
    (0..1000).each{ putc('*') }
  }

  (0..10).each{|i|
    STDOUT.print("#{i}\n")
    s = ''
    (0..1000).each{ s << '*' }
    print(s)
  }
=end

  num = 0
#  io = TkTextIO.new(:mode=>:console, :prompt=>'').pack
#=begin
  io = TkTextIO.new(:mode=>:console, 
                    :prompt_cmd=>proc{
                      s = "[#{num}]"
                      num += 1
                      s
                    }, 
                    :prompt=>'-> ').pack
#=end
  Thread.new{loop{sleep 2; io.puts 'hoge'}}
  Thread.new{loop{p io.gets}}

  ev_loop.join
end