1#!/usr/bin/env ruby
2#
3#  TkTextIO class :: handling I/O stream on a TkText widget
4#                             by Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp)
5#
6#  NOTE: TkTextIO supports 'character' (not 'byte') access only.
7#        So, for example, TkTextIO#getc returns a character, TkTextIO#pos
8#        means the character position, TkTextIO#read(size) counts by
9#        characters, and so on.
10#        Of course, it is available to make TkTextIO class to suuport
11#        'byte' access. However, it may break multi-byte characters.
12#        and then, displayed string on the text widget may be garbled.
13#        I think that it is not good on the supposed situation of using
14#        TkTextIO.
15#
16require 'tk'
17require 'tk/text'
18require 'tk/textmark'
19require 'thread'
20
21class TkTextIO < TkText
22  # keep safe level
23  @@create_queues = proc{ [Queue.new, Mutex.new, Queue.new, Mutex.new] }
24
25  OPT_DEFAULTS = {
26    'mode'       => nil,
27    'overwrite'  => false,
28    'text'       => nil,
29    'show'       => :pos,
30    'wrap'       => 'char',
31    'sync'       => true,
32    'prompt'     => nil,
33    'prompt_cmd' => nil,
34    'hist_size'  => 1000,
35  }
36
37  def create_self(keys)
38    opts = _get_io_params((keys.kind_of?(Hash))? keys: {})
39
40    super(keys)
41
42    @count_var = TkVariable.new
43
44    @write_buffer = ''
45    @read_buffer  = ''
46    @buf_size = 0
47    @buf_max = 1024
48
49    @write_buf_queue, @write_buf_mutex,
50    @read_buf_queue,  @read_buf_mutex  = @@create_queues.call
51
52    @idle_flush  = TkTimer.new(:idle, 1, proc{ @flusher.run rescue nil })
53    @timer_flush = TkTimer.new(250, -1, proc{ @flusher.run rescue nil })
54
55    @flusher = Thread.new{ loop { Thread.stop; flush() } }
56
57    @receiver = Thread.new{
58      begin
59        loop {
60          str = @write_buf_queue.deq
61          @write_buf_mutex.synchronize { @write_buffer << str }
62          @idle_flush.start
63        }
64      ensure
65        @flusher.kill
66      end
67    }
68
69    @timer_flush.start
70
71    _setup_io(opts)
72  end
73  private :create_self
74
75  def destroy
76    @flusher.kill rescue nil
77
78    @idle_flush.stop rescue nil
79    @timer_flush.stop rescue nil
80
81    @receiver.kill rescue nil
82
83    super()
84  end
85
86  ####################################
87
88  def _get_io_params(keys)
89    opts = {}
90    self.class.const_get(:OPT_DEFAULTS).each{|k, v|
91      if keys.has_key?(k)
92        opts[k] = keys.delete(k)
93      else
94        opts[k] = v
95      end
96    }
97    opts
98  end
99
100  def _setup_io(opts)
101    unless defined? @txtpos
102      @txtpos = TkTextMark.new(self, '1.0')
103    else
104      @txtpos.set('1.0')
105    end
106    @txtpos.gravity = :left
107
108    @lineno = 0
109    @line_offset = 0
110
111    @hist_max = opts['hist_size'].to_i
112    @hist_index = 0
113    @history = Array.new(@hist_max)
114    @history[0] = ''
115
116    self['wrap'] = wrap
117
118    self.show_mode = opts['show']
119
120    self.value = opts['text'] if opts['text']
121
122    @overwrite = (opts['overwrite'])? true: false
123
124    @sync = opts['sync']
125
126    @prompt = opts['prompt']
127    @prompt_cmd = opts['prompt_cmd']
128
129    @open  = {:r => true,  :w => true}  # default is 'r+'
130
131    @console_mode = false
132    @end_of_stream = false
133    @console_buffer = nil
134
135    case opts['mode']
136    when nil
137      # do nothing
138
139    when :console, 'console'
140      @console_mode = true
141      # @console_buffer = TkTextIO.new(:mode=>'r')
142      @console_buffer = self.class.new(:mode=>'r')
143      self.show_mode = :insert
144
145    when 'r', 'rb'
146      @open[:r] = true; @open[:w] = nil
147
148    when 'r+', 'rb+', 'r+b'
149      @open[:r] = true; @open[:w] = true
150
151    when 'w', 'wb'
152      @open[:r] = nil;  @open[:w] = true
153      self.value=''
154
155    when 'w+', 'wb+', 'w+b'
156      @open[:r] = true; @open[:w] = true
157      self.value=''
158
159    when 'a', 'ab'
160      @open[:r] = nil;  @open[:w] = true
161      @txtpos.set('end - 1 char')
162      @txtpos.gravity = :right
163
164    when 'a+', 'ab+', 'a+b'
165      @open[:r] = true;  @open[:w] = true
166      @txtpos.set('end - 1 char')
167      @txtpos.gravity = :right
168
169    else
170      fail ArgumentError, "unknown mode `#{opts['mode']}'"
171    end
172
173    unless defined? @ins_head
174      @ins_head = TkTextMark.new(self, 'insert')
175      @ins_head.gravity = :left
176    end
177
178    unless defined? @ins_tail
179      @ins_tail = TkTextMark.new(self, 'insert')
180      @ins_tail.gravity = :right
181    end
182
183    unless defined? @tmp_mark
184      @tmp_mark = TkTextMark.new(self, 'insert')
185      @tmp_mark.gravity = :left
186    end
187
188    if @console_mode
189      _set_console_line
190      _setup_console_bindings
191    end
192  end
193  private :_get_io_params, :_setup_io
194
195  def _set_console_line
196    @tmp_mark.set(@ins_tail)
197
198    mark_set('insert', 'end')
199
200    prompt = ''
201    prompt << @prompt_cmd.call if @prompt_cmd
202    prompt << @prompt if @prompt
203    insert(@tmp_mark, prompt)
204
205    @ins_head.set(@ins_tail)
206    @ins_tail.set('insert')
207
208    @txtpos.set(@tmp_mark)
209
210    _see_pos
211  end
212
213  def _replace_console_line(str)
214    self.delete(@ins_head, @ins_tail)
215    self.insert(@ins_head, str)
216  end
217
218  def _get_console_line
219    @tmp_mark.set(@ins_tail)
220    s = self.get(@ins_head, @tmp_mark)
221    _set_console_line
222    s
223  end
224  private :_set_console_line, :_replace_console_line, :_get_console_line
225
226  def _cb_up
227    @history[@hist_index].replace(self.get(@ins_head, @ins_tail))
228    @hist_index += 1
229    @hist_index -= 1 if @hist_index >= @hist_max || !@history[@hist_index]
230    _replace_console_line(@history[@hist_index]) if @history[@hist_index]
231    Tk.callback_break
232  end
233  def _cb_down
234    @history[@hist_index].replace(self.get(@ins_head, @ins_tail))
235    @hist_index -= 1
236    @hist_index = 0 if @hist_index < 0
237    _replace_console_line(@history[@hist_index]) if @history[@hist_index]
238    Tk.callback_break
239  end
240  def _cb_left
241    if @console_mode && compare('insert', '<=', @ins_head)
242      mark_set('insert', @ins_head)
243      Tk.callback_break
244    end
245  end
246  def _cb_backspace
247    if @console_mode && compare('insert', '<=', @ins_head)
248      Tk.callback_break
249    end
250  end
251  def _cb_ctrl_a
252    if @console_mode
253      mark_set('insert', @ins_head)
254      Tk.callback_break
255    end
256  end
257  def _cb_ctrl_u
258    if @console_mode
259      mark_set('insert', @ins_head)
260      delete('insert', 'insert lineend')
261      Tk.callback_break
262    end
263  end
264  private :_cb_up, :_cb_down, :_cb_left, :_cb_backspace,
265          :_cb_ctrl_a, :_cb_ctrl_u
266
267  def _setup_console_bindings
268    @bindtag = TkBindTag.new
269
270    tags = self.bindtags
271    tags[tags.index(self)+1, 0] = @bindtag
272    self.bindtags = tags
273
274    @bindtag.bind('Return'){
275      insert('end - 1 char', "\n")
276      if (str = _get_console_line)
277        @read_buf_queue.push(str)
278
279        @history[0].replace(str.chomp)
280        @history.pop
281        @history.unshift('')
282        @hist_index = 0
283      end
284
285      Tk.update
286      Tk.callback_break
287    }
288    @bindtag.bind('Alt-Return'){
289      Tk.callback_continue
290    }
291
292    @bindtag.bind('FocusIn'){
293      if @console_mode
294        mark_set('insert', @ins_tail)
295        Tk.callback_break
296      end
297    }
298
299    ins_mark = TkTextMark.new(self, 'insert')
300
301    @bindtag.bind('ButtonPress'){
302      if @console_mode
303        ins_mark.set('insert')
304      end
305    }
306
307    @bindtag.bind('ButtonRelease-1'){
308      if @console_mode && compare('insert', '<=', @ins_head)
309        mark_set('insert', ins_mark)
310        Tk.callback_break
311      end
312    }
313
314    @bindtag.bind('ButtonRelease-2', '%x %y'){|x, y|
315      if @console_mode
316        # paste a text at 'insert' only
317        x1, y1, x2, y2 =  bbox(ins_mark)
318        unless x == x1 && y == y1
319          Tk.event_generate(self, 'ButtonRelease-2', :x=>x1, :y=>y1)
320          Tk.callback_break
321        end
322      end
323    }
324
325    @bindtag.bind('Up'){ _cb_up }
326    @bindtag.bind('Control-p'){ _cb_up }
327
328    @bindtag.bind('Down'){ _cb_down }
329    @bindtag.bind('Control-n'){ _cb_down }
330
331    @bindtag.bind('Left'){ _cb_left }
332    @bindtag.bind('Control-b'){ _cb_left }
333
334    @bindtag.bind('BackSpace'){ _cb_backspace }
335    @bindtag.bind('Control-h'){ _cb_backspace }
336
337    @bindtag.bind('Home'){ _cb_ctrl_a }
338    @bindtag.bind('Control-a'){ _cb_ctrl_a }
339
340    @bindtag.bind('Control-u'){ _cb_ctrl_u }
341  end
342  private :_setup_console_bindings
343
344  def _block_read(size = nil, ret = '', block_mode = true)
345    return '' if size == 0
346    return nil if ! @read_buf_queue && @read_buffer.empty?
347    ret = '' unless ret.kind_of?(String)
348    ret.replace('') unless ret.empty?
349
350    if block_mode == nil # partial
351      if @read_buffer.empty?
352        ret << @read_buffer.slice!(0..-1)
353        return ret
354      end
355    end
356
357    if size.kind_of?(Numeric)
358      loop{
359        @read_buf_mutex.synchronize {
360          buf_len = @read_buffer.length
361          if buf_len >= size
362            ret << @read_buffer.slice!(0, size)
363            return ret
364          else
365            ret << @read_buffer.slice!(0..-1)
366            size -= buf_len
367            return ret unless @read_buf_queue
368          end
369        }
370        @read_buffer << @read_buf_queue.pop
371      }
372    else # readline
373      rs = (size)? size: $/
374      rs = rs.to_s if rs.kind_of?(Regexp)
375      loop{
376        @read_buf_mutex.synchronize {
377          if (str = @read_buffer.slice!(/\A(.*)(#{rs})/m))
378            ret << str
379            return ret
380          else
381            ret << @read_buffer.slice!(0..-1)
382            return ret unless @read_buf_queue
383          end
384        }
385        @read_buffer << @read_buf_queue.pop
386      }
387    end
388  end
389
390  def _block_write
391    ###### currently, not support
392  end
393  private :_block_read, :_block_write
394
395  ####################################
396
397  def <<(obj)
398    _write(obj)
399    self
400  end
401
402  def binmode
403    self
404  end
405
406  def clone
407    fail NotImplementedError, 'cannot clone TkTextIO'
408  end
409  def dup
410    fail NotImplementedError, 'cannot duplicate TkTextIO'
411  end
412
413  def close
414    close_read
415    close_write
416    nil
417  end
418  def close_read
419    @open[:r] = false if @open[:r]
420    nil
421  end
422  def close_write
423    @open[:w] = false if @opne[:w]
424    nil
425  end
426
427  def closed?(dir=nil)
428    case dir
429    when :r, 'r'
430      !@open[:r]
431    when :w, 'w'
432      !@open[:w]
433    else
434      !@open[:r] && !@open[:w]
435    end
436  end
437
438  def _check_readable
439    fail IOError, "not opened for reading" if @open[:r].nil?
440    fail IOError, "closed stream" if !@open[:r]
441  end
442  def _check_writable
443    fail IOError, "not opened for writing" if @open[:w].nil?
444    fail IOError, "closed stream" if !@open[:w]
445  end
446  private :_check_readable, :_check_writable
447
448  def each_line(rs = $/)
449    _check_readable
450    while(s = self.gets(rs))
451      yield(s)
452    end
453    self
454  end
455  alias each each_line
456
457  def each_char
458    _check_readable
459    while(c = self.getc)
460      yield(c)
461    end
462    self
463  end
464  alias each_byte each_char
465
466  def eof?
467    compare(@txtpos, '==', 'end - 1 char')
468  end
469  alias eof eof?
470
471  def fcntl(*args)
472    fail NotImplementedError, "fcntl is not implemented on #{self.class}"
473  end
474
475  def fsync
476    0
477  end
478
479  def fileno
480    nil
481  end
482
483  def flush
484    Thread.pass
485    if @open[:w] || ! @write_buffer.empty?
486      @write_buf_mutex.synchronize {
487        _sync_write_buf(@write_buffer)
488        @write_buffer[0..-1] = ''
489      }
490    end
491    self
492  end
493
494  def getc
495    return _block_read(1) if @console_mode
496
497    _check_readable
498    return nil if eof?
499    c = get(@txtpos)
500    @txtpos.set(@txtpos + '1 char')
501    _see_pos
502    c
503  end
504
505  def gets(rs = $/)
506    return _block_read(rs) if @console_mode
507
508    _check_readable
509    return nil if eof?
510    _readline(rs)
511  end
512
513  def ioctrl(*args)
514    fail NotImplementedError, 'iocntl is not implemented on TkTextIO'
515  end
516
517  def isatty
518    false
519  end
520  def tty?
521    false
522  end
523
524  def lineno
525    @lineno + @line_offset
526  end
527
528  def lineno=(num)
529    @line_offset = num - @lineno
530    num
531  end
532
533  def overwrite?
534    @overwrite
535  end
536
537  def overwrite=(ovwt)
538    @overwrite = (ovwt)? true: false
539  end
540
541  def pid
542    nil
543  end
544
545  def index_pos
546    index(@txtpos)
547  end
548  alias tell_index index_pos
549
550  def index_pos=(idx)
551    @txtpos.set(idx)
552    @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
553    _see_pos
554    idx
555  end
556
557  def pos
558    s = get('1.0', @txtpos)
559    number(tk_call('string', 'length', s))
560  end
561  alias tell pos
562
563  def pos=(idx)
564    seek(idx, IO::SEEK_SET)
565    idx
566  end
567
568  def pos_gravity
569    @txtpos.gravity
570  end
571
572  def pos_gravity=(side)
573    @txtpos.gravity = side
574    side
575  end
576
577  def print(arg=$_, *args)
578    _check_writable
579    args.unshift(arg)
580    args.map!{|val| (val == nil)? 'nil': val.to_s }
581    str = args.join($,)
582    str << $\ if $\
583    _write(str)
584    nil
585  end
586  def printf(*args)
587    _check_writable
588    _write(sprintf(*args))
589    nil
590  end
591
592  def putc(c)
593    _check_writable
594    c = c.chr if c.kind_of?(Fixnum)
595    _write(c)
596    c
597  end
598
599  def puts(*args)
600    _check_writable
601    if args.empty?
602      _write("\n")
603      return nil
604    end
605    args.each{|arg|
606      if arg == nil
607        _write("nil\n")
608      elsif arg.kind_of?(Array)
609        puts(*arg)
610      elsif arg.kind_of?(String)
611        _write(arg.chomp)
612        _write("\n")
613      else
614        begin
615          arg = arg.to_ary
616          puts(*arg)
617        rescue
618          puts(arg.to_s)
619        end
620      end
621    }
622    nil
623  end
624
625  def _read(len)
626    epos = @txtpos + "#{len} char"
627    s = get(@txtpos, epos)
628    @txtpos.set(epos)
629    @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
630    _see_pos
631    s
632  end
633  private :_read
634
635  def read(len=nil, buf=nil)
636    return _block_read(len, buf) if @console_mode
637
638    _check_readable
639    if len
640      return "" if len == 0
641      return nil if eof?
642      s = _read(len)
643    else
644      s = get(@txtpos, 'end - 1 char')
645      @txtpos.set('end - 1 char')
646      _see_pos
647    end
648    buf.replace(s) if buf.kind_of?(String)
649    s
650  end
651
652  def readchar
653    return _block_read(1) if @console_mode
654
655    _check_readable
656    fail EOFError if eof?
657    c = get(@txtpos)
658    @txtpos.set(@txtpos + '1 char')
659    _see_pos
660    c
661  end
662
663  def _readline(rs = $/)
664    if rs == nil
665      s = get(@txtpos, 'end - 1 char')
666      @txtpos.set('end - 1 char')
667    elsif rs == ''
668      @count_var.value  # make it global
669      idx = tksearch_with_count([:regexp], @count_var,
670                                   "\n(\n)+", @txtpos, 'end - 1 char')
671      if idx
672        s = get(@txtpos, idx) << "\n"
673        @txtpos.set("#{idx} + #{@count_var.value} char")
674        @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
675      else
676        s = get(@txtpos, 'end - 1 char')
677        @txtpos.set('end - 1 char')
678      end
679    else
680      @count_var.value  # make it global
681      idx = tksearch_with_count(@count_var, rs, @txtpos, 'end - 1 char')
682      if idx
683        s = get(@txtpos, "#{idx} + #{@count_var.value} char")
684        @txtpos.set("#{idx} + #{@count_var.value} char")
685        @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
686      else
687        s = get(@txtpos, 'end - 1 char')
688        @txtpos.set('end - 1 char')
689      end
690    end
691
692    _see_pos
693    @lineno += 1
694    $_ = s
695  end
696  private :_readline
697
698  def readline(rs = $/)
699    return _block_readline(rs) if @console_mode
700
701    _check_readable
702    fail EOFError if eof?
703    _readline(rs)
704  end
705
706  def readlines(rs = $/)
707    if @console_mode
708      lines = []
709      while (line = _block_readline(rs))
710        lines << line
711      end
712      return lines
713    end
714
715    _check_readable
716    lines = []
717    until(eof?)
718      lines << _readline(rs)
719    end
720    $_ = nil
721    lines
722  end
723
724  def readpartial(maxlen, buf=nil)
725    #return @console_buffer.readpartial(maxlen, buf) if @console_mode
726    return _block_read(maxlen, buf, nil) if @console_mode
727
728    _check_readable
729    fail EOFError if eof?
730    s = _read(maxlen)
731    buf.replace(s) if buf.kind_of?(String)
732    s
733  end
734
735  def reopen(*args)
736    fail NotImplementedError, 'reopen is not implemented on TkTextIO'
737  end
738
739  def rewind
740    @txtpos.set('1.0')
741    _see_pos
742    @lineno = 0
743    @line_offset = 0
744    self
745  end
746
747  def seek(offset, whence=IO::SEEK_SET)
748    case whence
749    when IO::SEEK_SET
750      offset = "1.0 + #{offset} char" if offset.kind_of?(Numeric)
751      @txtpos.set(offset)
752
753    when IO::SEEK_CUR
754      offset = "#{offset} char" if offset.kind_of?(Numeric)
755      @txtpos.set(@txtpos + offset)
756
757    when IO::SEEK_END
758      offset = "#{offset} char" if offset.kind_of?(Numeric)
759      @txtpos.set("end - 1 char + #{offset}")
760
761    else
762      fail Errno::EINVAL, 'invalid whence argument'
763    end
764
765    @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
766    _see_pos
767
768    0
769  end
770  alias sysseek seek
771
772  def _see_pos
773    see(@show) if @show
774  end
775  private :_see_pos
776
777  def show_mode
778    (@show == @txtpos)? :pos : @show
779  end
780
781  def show_mode=(mode)
782    # define show mode  when file position is changed.
783    #  mode == :pos or "pos" or true :: see current file position.
784    #  mode == :insert or "insert"   :: see insert cursor position.
785    #  mode == nil or false          :: do nothing
786    #  else see 'mode' position ('mode' should be text index or mark)
787    case mode
788    when :pos, 'pos', true
789      @show = @txtpos
790    when :insert, 'insert'
791      @show = :insert
792    when nil, false
793      @show = false
794    else
795      begin
796        index(mode)
797      rescue
798        fail ArgumentError, 'invalid show-position'
799      end
800      @show = mode
801    end
802
803    _see_pos
804
805    mode
806  end
807
808  def stat
809    fail NotImplementedError, 'stat is not implemented on TkTextIO'
810  end
811
812  def sync
813    @sync
814  end
815
816  def sync=(mode)
817    @sync = mode
818  end
819
820  def sysread(len, buf=nil)
821    return _block_read(len, buf) if @console_mode
822
823    _check_readable
824    fail EOFError if eof?
825    s = _read(len)
826    buf.replace(s) if buf.kind_of?(String)
827    s
828  end
829
830  def syswrite(obj)
831    _write(obj)
832  end
833
834  def to_io
835    self
836  end
837
838  def trancate(len)
839    delete("1.0 + #{len} char", :end)
840    0
841  end
842
843  def ungetc(c)
844    if @console_mode
845      @read_buf_mutex.synchronize {
846        @read_buffer[0,0] = c.chr
847      }
848      return nil
849    end
850
851    _check_readable
852    c = c.chr if c.kind_of?(Fixnum)
853    if compare(@txtpos, '>', '1.0')
854      @txtpos.set(@txtpos - '1 char')
855      delete(@txtpos)
856      insert(@txtpos, tk_call('string', 'range', c, 0, 1))
857      @txtpos.set(@txtpos - '1 char') if @txtpos.gravity == 'right'
858      _see_pos
859    else
860      fail IOError, 'cannot ungetc at head of stream'
861    end
862    nil
863  end
864
865=begin
866  def _write(obj)
867    #s = _get_eval_string(obj)
868    s = (obj.kind_of?(String))? obj: obj.to_s
869    n = number(tk_call('string', 'length', s))
870    delete(@txtpos, @txtpos + "#{n} char") if @overwrite
871    self.insert(@txtpos, s)
872    @txtpos.set(@txtpos + "#{n} char")
873    @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
874    _see_pos
875    Tk.update if @sync
876    n
877  end
878  private :_write
879=end
880#=begin
881  def _sync_write_buf(s)
882    if (n = number(tk_call('string', 'length', s))) > 0
883      delete(@txtpos, @txtpos + "#{n} char") if @overwrite
884      self.insert(@txtpos, s)
885      #Tk.update
886
887      @txtpos.set(@txtpos + "#{n} char")
888      @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end)
889
890      @ins_head.set(@txtpos) if compare(@txtpos, '>', @ins_head)
891
892      _see_pos
893    end
894    self
895  end
896  private :_sync_write_buf
897
898  def _write(obj)
899    s = (obj.kind_of?(String))? obj: obj.to_s
900    n = number(tk_call('string', 'length', s))
901    @write_buf_queue.enq(s)
902    if @sync
903      Thread.pass
904      Tk.update
905    end
906    n
907  end
908  private :_write
909#=end
910
911  def write(obj)
912    _check_writable
913    _write(obj)
914  end
915end
916
917####################
918#  TEST
919####################
920if __FILE__ == $0
921  ev_loop = Thread.new{Tk.mainloop}
922
923  f = TkFrame.new.pack
924  #tio = TkTextIO.new(f, :show=>:nil,
925  #tio = TkTextIO.new(f, :show=>:pos,
926  tio = TkTextIO.new(f, :show=>:insert,
927                     :text=>">>> This is an initial text line. <<<\n\n"){
928#    yscrollbar(TkScrollbar.new(f).pack(:side=>:right, :fill=>:y))
929    pack(:side=>:left, :fill=>:both, :expand=>true)
930  }
931
932  Tk.update
933
934  $stdin  = tio
935  $stdout = tio
936  $stderr = tio
937
938  STDOUT.print("\n========= TkTextIO#gets for inital text ========\n\n")
939
940  while(s = gets)
941    STDOUT.print(s)
942  end
943
944  STDOUT.print("\n============ put strings to TkTextIO ===========\n\n")
945
946  puts "On this sample, a text widget works as if it is a I/O stream."
947  puts "Please see the code."
948  puts
949  printf("printf message: %d %X\n", 123456, 255)
950  puts
951  printf("(output by 'p' method) This TkTextIO object is ...\n")
952  p tio
953  print(" [ Current wrap mode of this object is 'char'. ]\n")
954  puts
955  warn("This is a warning message generated by 'warn' method.")
956  puts
957  puts "current show_mode is #{tio.show_mode}."
958  if tio.show_mode == :pos
959    puts "So, you can see the current file position on this text widget."
960  else
961    puts "So, you can see the position '#{tio.show_mode}' on this text widget."
962  end
963  print("Please scroll up this text widget to see the head of lines.\n")
964  print("---------------------------------------------------------\n")
965
966  STDOUT.print("\n=============== TkTextIO#readlines =============\n\n")
967
968  tio.seek(0)
969  lines = readlines
970  STDOUT.puts(lines.inspect)
971
972  STDOUT.print("\n================== TkTextIO#each ===============\n\n")
973
974  tio.rewind
975  tio.each{|line| STDOUT.printf("%2d: %s\n", tio.lineno, line.chomp)}
976
977  STDOUT.print("\n================================================\n\n")
978
979  STDOUT.print("\n========= reverse order (seek by lines) ========\n\n")
980
981  tio.seek(-1, IO::SEEK_END)
982  begin
983    begin
984      tio.seek(:linestart, IO::SEEK_CUR)
985    rescue
986      # maybe use old version of tk/textmark.rb
987      tio.seek('0 char linestart', IO::SEEK_CUR)
988    end
989    STDOUT.print(gets)
990    tio.seek('-1 char linestart -1 char', IO::SEEK_CUR)
991  end while(tio.pos > 0)
992
993  STDOUT.print("\n================================================\n\n")
994
995  tio.seek(0, IO::SEEK_END)
996
997  STDOUT.print("tio.sync ==  #{tio.sync}\n")
998#  tio.sync = false
999#  STDOUT.print("tio.sync ==  #{tio.sync}\n")
1000
1001  (0..10).each{|i|
1002    STDOUT.print("#{i}\n")
1003    s = ''
1004    (0..1000).each{ s << '*' }
1005    print(s)
1006  }
1007  print("\n")
1008  print("\n=========================================================\n\n")
1009
1010  s = ''
1011  timer = TkTimer.new(:idle, -1, proc{
1012                        #STDOUT.print("idle call\n")
1013                        unless s.empty?
1014                          print(s)
1015                          s = ''
1016                        end
1017                      }).start
1018  (0..10).each{|i|
1019    STDOUT.print("#{i}\n")
1020    (0..1000).each{ s << '*' }
1021  }
1022#  timer.stop
1023  until s.empty?
1024    sleep 0.1
1025  end
1026  timer.stop
1027
1028=begin
1029  tio.sync = false
1030  print("\n")
1031  #(0..10000).each{ putc('*') }
1032  (0..10).each{|i|
1033    STDOUT.print("#{i}\n")
1034    (0..1000).each{ putc('*') }
1035  }
1036
1037  (0..10).each{|i|
1038    STDOUT.print("#{i}\n")
1039    s = ''
1040    (0..1000).each{ s << '*' }
1041    print(s)
1042  }
1043=end
1044
1045  num = 0
1046#  io = TkTextIO.new(:mode=>:console, :prompt=>'').pack
1047#=begin
1048  io = TkTextIO.new(:mode=>:console,
1049                    :prompt_cmd=>proc{
1050                      s = "[#{num}]"
1051                      num += 1
1052                      s
1053                    },
1054                    :prompt=>'-> ').pack
1055#=end
1056  Thread.new{loop{sleep 2; io.puts 'hoge'}}
1057  Thread.new{loop{p io.gets}}
1058
1059  ev_loop.join
1060end
1061