From: Dennis Nedry on
I've been debugging my full screen console ruby editor.

Suddenly, for no apparent reason, I'm starting to get

(a) random quits while typing. espically when pressing "t".

(b) Select:Interrupt while typing.

This didn't used to happen. The line it is dying at is...

if select(@in_io],nil,nil,1)

very strange, and frustrating! This is on linux...

ruby version...

1.8.6 (2007-09-24 patchlevel 111) [i686-linux]

I'm willing to post the whole thing, as long as I don't get too much
abuse for it not being very well coded. It's a work in progress.

From: Dennis Nedry on
On Tue, May 25, 2010 at 2:23 PM, Yukihiro Matsumoto <matz(a)ruby-lang.org> wrote:
> I feel sympathy, but above code snippet help us nothing.  If you need
> help from the list, you'd better disclose everything, to reproduce the
> problem, no matter how bad your code is right now.

But Matz, my Ruby is real bad. It looks like my Pascal did. I never
did manage to get my head around this new fangled stuff. I'm 41, and
i learned to program when i was about 10, on a pdp 11. Anyway, I'm
getting off topic. Did I thank you for this wonderful language.
Anyway, here goes... these are the two main files...

fsed.rb (Poor Dossy)

#
# Full Screen EDitor (FSED) for QUARKware QBBS.
#
# Copyright (C) 2002, Dossy <dossy(a)panoptic.com>
# All rights reserved.
#
# $Id: fsed.rb,v 1.1 2002/09/12 12:27:16 dossy Exp $
#

module Editors
module FSED
VERSION = "0.75"
ESC = 27.chr
RETURN = 10.chr

class Buffer
def initialize(max_lines,in_file)
@buffer = getfile(in_file)
@max_lines = max_lines
end

def []=(x, y, value)
if @buffer[y - 1] == nil
@buffer[y - 1] = []
end
@buffer[y - 1][x - 1] = value
end

def [](x, y)
if @buffer[y - 1].nil?
nil
else
@buffer[y - 1][x - 1]
end
end

def clear
@buffer = []
end

def length
@buffer.length
end

def delete_at(x, y)
unless @buffer[y - 1].nil?
@buffer[y - 1].slice!(x - 1) # used to use delete_at but that
didn't always work.
end
end

def insert_at_line(y,x,in_str)
inthing = nil
if !in_str.nil? then
in_str.each_with_index {|c,i| @buffer[y-1].insert(x+ i,c)}
end
end

def insert_line_at(y,ln)
@buffer[y] = [] if @buffer[y] == nil
@buffer.insert(y,ln)
end

def delete_line_at(y)
@buffer.delete_at(y - 1)
end

def buffer_length

return @buffer.length
end


def length_y(y)
if !@buffer[y - 1].nil?
return @buffer[y - 1].length
else
return 0
end
end

def del_range(y,x1,x2)
total = x2 - x1
str = @buffer[y-1].slice!(-(total),total)
return str
end

def insert_char(x, y, value)
@buffer[y - 1] = [] if @buffer[y - 1] == nil
if (x-1) > @buffer[y-1].length then
@buffer[y-1] << value
else
@buffer[y - 1].insert(x-1,value)
end
end

def find_first_space(y)
return @buffer[y-1].index(" ")
end

def find_nearest_space(y,space)
result = nil
@buffer[y - 1] = [] if @buffer[y - 1].nil?
highest = 0
@buffer[y-1].each_with_index {|c,i|
highest = i
result = i if (c == " ") and (i <= space)
}
result= highest if highest <= space
return result
end

def paragraph_up(start_y,width)

for i in start_y..(a)buffer.length #- 1
break if @buffer[i,0].nil?
pos = length_y(i-1)
space = width - pos #how much space on the line above?
unwrap_space = find_nearest_space(i,space)
break if unwrap_space.nil? or unwrap_space == 0
unwrap_str = del_range(i,0,unwrap_space+1)
insert_at_line(i - 1,pos,unwrap_str)
end
end

def detect_and_wrap(y,x,width)
l_space = 0; wrap = nil
@buffer[y - 1] = [] if @buffer[y - 1].nil?
l = @buffer[y-1].length - 1
test = @buffer[y-1]

if !@buffer[y-1][width].nil? then #is there now a character past max_width
test.slice!(-1) if test.last == " "
l_space = test.rindex(' ')
wrap = @buffer[y-1][l_space..l]
del_range(y,l_space+1,l+1)
return [l_space,wrap]
end
end

def line(line)
if !@buffer[line - 1].nil? then #protect against backspace on
an empty line -- produces a nil
@buffer[line - 1].collect { |char|
if char.nil?
" "
else
char
end
}.to_s.chomp
end
end

def buff_out
@buffer.collect { |line|
if line.nil?
"\n"
else
[ line.collect { |char|
if char.nil?
" "
else
char
end
}, "\n" ].to_s
end
}
end


def dump
@buffer.collect { |line|
if line.nil?
"\n"
else
[ line.collect { |char|
if char.nil?
" "
else
char
end
}, "\n" ].to_s
end
}.to_s.chomp
end


# def display
# i = 0
# @buffer.collect { |line| i = i + 1
# if line.nil?
# "#{i}: \n"
# else
# "#{i}: " << [ line.collect { |char|
# if char.nil?
# " "
# else
# char
# end
# }, "\n" ].to_s
# end
# }.to_s.chomp
#end

def to_s(top_start,vp_height)

out = String.new
top_stop = @buffer.length - 1
top_stop = vp_height + top_start - 1 if @buffer.length - 1 >=
vp_height + top_start

for i in top_start..top_stop
one_line = @buffer[i].to_s
out = out+ one_line + "\n"
end
out
end

def room_on_line(y,str,width)

room = false

if !@buffer[y].nil?

if !str.nil? then
room = true if (@buffer[y].length - 1) + str.length < width
end
end
return room
end

def getfile(filename)
file_array = []
if !filename.nil? then
#this used to use chars.to_a but it didn't always work right
if File.exists?(filename)
IO.foreach(filename) { |line|
line.gsub!("\n","")
line.gsub!("\r","")
build = []
for i in 0..line.length-1
build << line[i].chr
end
file_array << build}
end
end
puts
file_array
end

end

class EditorState
attr_reader :current_cursor_position, :previous_cursor_position
attr_reader :screen_width, :screen_height
attr_reader :viewport_width, :viewport_height
attr_reader :header_height
attr_reader :buffer

def initialize(width, height,in_file)
@dirty = true
@current_cursor_position = [1, 1]
@previous_cursor_position = [1, 1]
@screen_width = width
@screen_height = height
@buffer = Buffer.new(500,in_file)
@buffer_top = 0

@header_height = 2
@viewport_width = @screen_width
@viewport_height = @screen_height - @header_height
@wrapped = false
@insert = true

open_error_log
end

require "windows.rb"

def open_error_log
$lf = File.new("debug.txt", File::CREAT|File::TRUNC|File::RDWR, 0644)
end

def current_x
@current_cursor_position[0]
end

def current_y
@current_cursor_position[1]
end


def place_cursor(x, y)
@previous_cursor_position = @current_cursor_position
@current_cursor_position = [x, y]
@dirty = true
end

def move_cursor_up(x)
result = NO_REDRAW
new_y = current_y - x
if new_y < 1 then
new_y = 1
@buffer_top -=x if @buffer_top > 0
result = REDRAW
end
place_cursor(current_x, new_y)
return result
end

def page_up
redraw = move_cursor_up(@viewport_height)
return redraw
end

def move_cursor_down(x)
result = NO_REDRAW
new_y = current_y + x
@wrapped = false
if new_y >=@viewport_height then
new_y = (current_y)
@buffer_top += x
result = REDRAW
end
place_cursor(current_x, new_y)
return result
end

def page_down
down = @buffer.buffer_length - (current_y+ @buffer_top)
if down > @viewport_height then
redraw = move_cursor_down(@viewport_height)
return redraw
else
return NO_REDRAW
end
end


def move_cursor_left(x)
new_x = current_x - x
new_x = 1 if new_x < 1
place_cursor(new_x, current_y)
end

def home_cursor
place_cursor(1,current_y)
end

def move_cursor_right(x)
new_x = current_x + x
new_x = @viewport_width if new_x > @viewport_width
place_cursor(new_x, current_y)
end

def end_cursor
end_line = @buffer.length_y(current_y)+1
end_line = @viewport_width if end_line > @viewport_width
place_cursor(end_line,current_y)
end

def clear_screen
"#{ESC}[2J#{ESC}[H#{ESC}[00m"
end

def toggle_ins
@insert = !@insert
end

def parse_c(line)
COLORTABLE.each_pair {|color, result| line.gsub!(color,result) }
return line
end

def header
out_str = "INS"
out_str = "OVR" if !@insert
out = String.new
out << parse_c("%WQuark%YEDIT #{VERSION}%W".fit(79)) +"\n"
out << parse_c("%YCTRL + e%YX%Wit %Y|%W %YG%W Help %Y|%W %YS%Wave
%Y|%W %YN%Wewline %Y|%W %YY%W Delete %Y|%W #{out_str} %Y|%W Line:
#{current_y + @buffer_top}".fit(79)) << "\n"
out << bg("black") << fg("WHITE")
return out
end

def redraw(force)
@dirty = true if force
if @dirty
@dirty = false
if force
[clear_screen,
header,
buffer.to_s(@buffer_top,@viewport_height),
update_cursor_position].to_s
else
""
end
else
""
end
end

def clear
@buffer.clear
end

# this is complicated... too complicated...



def input_char_at_cursor(c)
if @insert then #we are
in insert mode
@buffer.insert_char(current_x,(current_y) + @buffer_top,c)
l_space,wrap = @buffer.detect_and_wrap(current_y,current_x,@screen_width - 1)
if !wrap.nil? then
if !(current_x < @buffer.length_y(current_y) - 1) then
#no wrap... insert a character
if (current_x + wrap.length) < @screen_width then
move_cursor_right(1)
return [c,NO_REDRAW]
else
#wrap at insert at end of line
@buffer.insert_line_at(current_y,wrap.to_s.strip!)
home_cursor
move_cursor_right(wrap.length - 1)
move_cursor_down(1)
end
$lf.print "I'm here...wrap line\n"
return [nil,REDRAW] #we don't want a character
printed because we are in overflow
end

$lf.print "@buffer.length + wrap.length:
#{@buffer.length_y(current_y) +(wrap.length + 2)}\n"
$lf.print "@screen_width: #{@screen_width}\n"
$lf.print "room on line: #{@buffer.room_on_line(current_y +
1,wrap,@screen_width)}\n"
$lf.print "@buffer.length: #{@buffer.length}\n"
if (@buffer.length_y(current_y) + (wrap.length + 2)) >=
@screen_width then #wrap for insert not at end of line
$lf.print "in insert not at end of line...\n"
if @buffer.room_on_line(current_y + 1,wrap,@screen_width) then

@buffer.insert_at_line(current_y+1,0,wrap) #subsequent words go
to next line

$lf.print "on next existing line...\n"
else

@buffer.insert_line_at(current_y,wrap) #out of room so make a new line

$lf.print "on a new line...\n"
end
move_cursor_right(1)
$lf.print "done with insert before line\n"
return [c,REDRAW]
end
end
move_cursor_right(1) #redraw because we are inserting...
if (@buffer.length_y(current_y)+1) == current_x then
return [c,NO_REDRAW] #insert mode at eol so no redraw
else
return [c,REDRAW] #insert mode not at eol, so redraw
end

else
if current_x < @screen_width then
@buffer[current_x,(current_y) + @buffer_top] = c #we are in
over-write mode....
move_cursor_right(1)
$lf.print "Overwrite Mode\n"
return [c,NO_REDRAW]
else
$lf.print "I'm here...sixth return\n"
return [nil,NO_REDRAW]
end
end
end


def newline

if @buffer.length_y(current_y) == 0 then
@buffer.insert_line_at(current_y,nil)
move_cursor_down(1)
else
str = @buffer.del_range(current_y,current_x-1,@buffer.length_y(current_y))
@buffer.insert_line_at(current_y,str)
move_cursor_left(current_x)
move_cursor_down(1)
end
@dirty = true
end


def deleteline
@buffer.delete_line_at(current_y)
end

def backspace
if current_x > 1 then #normal delete, not at BOL
move_cursor_left(1)
@buffer.delete_at(current_x, current_y)
else
if current_y > 1 then # delete at BOL
if @buffer.length_y(current_y-1) == 0 then #blank line above
@buffer.delete_line_at(current_y-1)
move_cursor_up(1)
else
@buffer.paragraph_up(current_y,@screen_width) #move up until you
hit a blank line...
move_cursor_up(1)
home_cursor
move_cursor_right(@buffer.length_y(current_y))
end
return REDRAW
end
end
return REDRAW
end

def update_cursor_position
"#{ESC}[#{current_y + @header_height};#{current_x}H"
end

def w_update_cursor(x,y)
"#{ESC}[#{y + @header_height};#{x}H"
end

def w_clear
return @c.reset
end

def fg(forground)

out = String.new

case forground
when "red"
out = ""
when "RED"
out = ""
when "green"
out << ""
when "GREEN"
out = ""
when "blue"
out = ""
when "BLUE"
out = ""
when "cyan"
out = ""
when "CYAN"
out = ""
when "magenta"
out = ""
when "MAGENTA"
out = ""
when "yellow"
out = ""
when "YELLOW"
out = ""
when "black"
out = ""
when "BLACK"
out = ""
when "hide"
out = "[?25l"
when "show"
out = "[?25h"
when "reset"
out = ""

end
return out
end

def bg(background)

out = String.new

case background
when "red"
out = ""
when "green"
out = ""
when "blue"
out = ""
when "cyan"
out = ""
when "magenta"
out = ""
when "yellow"
out = ""
when "black"
out = ""
when "white"
out = ""
end
return out
end


def center(string,width,color)
result = String.new
outdash = ((width / 2 ) - (string.length / 2))
outdash.times {result << " "}
result << color if !color.nil?
result << string
(width - (outdash + string.length)).times {result << " "}
return result
end

def make_window(startx,starty,width,height,forground,background,border,title)

f_color = fg(forground)
b_color = bg(background)
bdr_color = bg(border)
window = String.new

window << w_update_cursor(startx,starty)
window << bdr_color
window << center(title,width,nil)
for i in 1..height do
window << w_update_cursor(startx,starty+i)
window << bdr_color << " " << b_color
(width - 2).times {window << " "}
window << bdr_color << " "
window << bg("white") << " "
end
window << w_update_cursor(startx,starty+height) << bdr_color
width.times {window << " "}
window << w_update_cursor(startx+1,starty+height+1) << bg("white")
width.times {window << " "}
window << w_update_cursor(startx,starty)

return window
end

def help_window

idt = 16
str = 5
width=58
out = make_window(str,2,60,12,"BLACK","cyan","blue","Help Window")
out << w_update_cursor(idt,str) << fg("yellow") << bg("cyan")
out << "CTRL-A" << fg("white") << " Abort Message"
out << w_update_cursor(idt,str+1) << fg("yellow")
out << "CTRL-L" << fg("white") << " Refresh Screen"
out << w_update_cursor(idt,str+2) << fg("yellow")
out << "CTRL-N" << fg("white") << " New Line"
out << w_update_cursor(idt,str+3) << fg("yellow")
out << "CTRL-X" << fg("white") << " Save (Post) Message"
out << w_update_cursor(idt,str+4) << fg("yellow")
out << "CTRL-Y" << fg("white") << " Delete Line"
out << w_update_cursor(idt,str+6) << fg("yellow")
out << "INSERT" << fg("white") << " Toggle Insert/Overwrite"
out << w_update_cursor(idt,str+8)
out << "ESC to exit this window." << fg("white")
end

def splash_window

idt = 15
str = 8
width = 38
out = make_window(idt-1,str-1,40,8,"WHITE","magenta","cyan","About")
out << w_update_cursor(idt,str+1) <<fg("yellow") << bg("magenta")
out << center("QUARKedit #{VERSION}",width,fg("white"))
out << w_update_cursor(idt,str+3)
out << center("By Dossy and Mark Firestone",width,fg("white"))
out <<w_update_cursor(idt+36,str+7)
#out << fg("hide")
end

def yes_no_window(question)
idt = 11
str = 10
w_width = question.length + 14
width = w_width - 2
out = make_window(idt-2,str-1,w_width,4,"WHITE","red","yellow","Confirm")
out << w_update_cursor(idt+1,str+1)
out << fg("WHITE") << bg("red")
out << question << "(Y,n): "
end

def screen_clear # I don't know why. Needs two redraws or the
background color is wrong....
out = redraw(true)
out << redraw(true)
return out
end

end #of class





class Editor

#Window Constants
MESSAGE = 1
ABORT = 2
SAVE = 3
SPELL = 4

def initialize(width, height, in_io, out_io,in_file,bbs_mode)
@state = EditorState.new(width, height,in_file)
@in_io = in_io
@out_io = out_io
@w_mode = false
@w_type = MESSAGE
@supress = false
@bbs_mode = bbs_mode
end

def run
@out_io.sync = true
@in_io.sync = false
@out_io.print @state.screen_clear
@out_io.print @state.redraw(true)
@out_io.print @state.splash_window
sleep(1)
@out_io.print @state.redraw(true)
@out_io.print @state.redraw(true)
buf = nil

while true

if select([@in_io], nil, nil, 1)
$lf.print "I made it"


c = @in_io.sysread(1)
$lf.print "after sysread"
#c = @in_io.getc
# $lf.print"c: #{c}\n"
# $lf.print"c-chr: #{c[0]}\n"
# if @supress then # if we are suppressing the mysterious
extra linefeed... we do that here.
# @supress = false
# c = 0.chr if c.bytes.to_a[0] = 10
# end

if @w_mode then #We are in window mode, not edit mode...
# $lf.print "in wmode\n"
#$lf.print "c: #{c.upcase}"

case c
when "\e" #effectively, esc this is cancel for everything
@w_mode = false
@out_io.print @state.screen_clear
else

case @w_type
when ABORT,SAVE
if c.upcase == "Y" or c == RETURN then
print "Y"
@state.clear if @w_type == ABORT
@state.clear_screen
sleep(2)
break
else
@out_io.print @state.screen_clear
@w_mode = false
end
end
end


else
case c
when "\cX" # exit
@out_io.print @state.yes_no_window("Post message... Are you sure?")
@w_type = SAVE
@w_mode = true
when "\cG","\eOP"
@out_io.print @state.help_window
@w_type = MESSAGE
@w_mode = true
when "\cA"
@out_io.print @state.yes_no_window("Abort message... Are you sure?")
@w_type = ABORT
@w_mode = true
when "\cN" #insert line
@state.newline
@out_io.print @state.redraw(true)
when "\cY" #delete line
@state.deleteline
@out_io.print @state.redraw(true)
when "\cL" # refresh
@out_io.print @state.redraw(true)
when "\r","\n"
@state.newline
@supress = true if @bbs_mode #telnet seems to like to echo
linefeeds. lets supress this ...
@out_io.print @state.redraw(true)
when "\010", "\177"
redraw = @state.backspace
@out_io.print "\e[#{@state.current_y + @state.header_height};1H\e[K"
@out_io.print @state.buffer.line(@state.current_y)
@out_io.print @state.update_cursor_position
@out_io.print @state.redraw(true) if redraw
when "\e" # escape
buf = c
else
if buf.nil?
chr = c.unpack("c")[0]
if (chr >= 32 && chr <= 127)
out_c,redraw = @state.input_char_at_cursor(c)
@out_io.putc(out_c) if !out_c.nil?
@out_io.print @state.redraw(true) if redraw
end
else
buf << c
# $lf.print "buf: #{buf}\n"
case buf
when "\e[H","\e[1"
@state.home_cursor
@out_io.print @state.update_cursor_position
when "\e[F","\e[4"
@state.end_cursor
@out_io.print @state.update_cursor_position
when "\e[6"
redraw = @state.page_down
@out_io.print @state.redraw(true) if redraw
when "\e[5"
redraw = @state.page_up
@out_io.print @state.redraw(true) if redraw
when "\e[2"
@state.toggle_ins
@out_io.print @state.redraw(true)
when "\e[A"
redraw = @state.move_cursor_up(1)
if redraw
@out_io.print @state.redraw(true)
else
@out_io.print @state.update_cursor_position
end
buf = nil
when "\e[B"
redraw = @state.move_cursor_down(1)
if redraw
@out_io.print @state.redraw(true)
else
@out_io.print @state.update_cursor_position
end
buf = nil
when "\e[D"
@state.move_cursor_left(1)
@out_io.print @state.update_cursor_position
buf = nil
when "\e[C"
@state.move_cursor_right(1)
@out_io.print @state.update_cursor_position
buf = nil
else
if buf.size >= 3
buf = nil
end
end
end
end
end
end
end
@state.buffer

end

end

end

end

file edit.rb:

require 'fsed'
require "tools.rb"
#require "raspell"




#Consts

SPELL_CHECK = true #if you don't want raspell, comment out the
require above.

COLORTABLE = {
'%R' => "\e[;1;31;44m", '%G' => "\e[;1;32;44m",
'%Y' => "\e[;1;33;44m", '%B' => "\e[;1;34;44m",
'%M' => "\e[;1;35;44m", '%C' => "\e[;1;36;44m",
'%W' => "\e[;1;37;44m", '%r' => "\e[;31;44m",
'%g' => "\e[;32;44m", '%y' => "\e[;33;44m",
'%b' => "\e[;34;44m", '%m' => "\e[;35;44m",
'%c' => "\e[;36;44m", '%w' => "\e[;31;44m"
}

REDRAW = true
NO_REDRAW = false

def tcgetattr(io)
_TCGETA = 0x5405
attr = [0, 0, 0, 0].pack("SSSS")
io.ioctl(_TCGETA, attr)
attr
end

def tcsetattr(io, attr)
_TCSETA = 0x5406
io.ioctl(_TCSETA, attr)
end

def writefile (filename,array)

lf = File.new(filename, File::WRONLY|File::TRUNC|File::CREAT, 0644)
array.each {|x|
print "."
lf.puts x}
lf.close
puts
end

def pull_apart_args(args)

bbs_mode = false
filename = nil
if !args.nil then
filename = args.last
args.each {|arg| bbs_mode = true if arg == "-L"
#put more switches here
}
end
return [bbs_mode,filename]
end


begin
unless RUBY_PLATFORM =~ /mswin32/
# turn off stdin buffering and echo

# c_iflag bits
INLCR = "0000100".to_i
IGNCR = "0000200".to_i
ICRNL = "0000400".to_i

# c_oflag bits
OPOST = "0000001".to_i

# c_lflag bits
ISIG = "0000001".to_i
ICANON = "0000002".to_i
ECHO = "0000010".to_i

old_attr = tcgetattr($stdin)
input, output, control, local = old_attr.unpack("SSSS")
# input &= ~(INLCR | IGNCR | ICRNL)
# output &= ~OPOST
#local &= ~(ECHO | ICANON | ISIG)
local &= ~(ECHO)
new_attr = [input, output, control, local].pack("SSSS")
tcsetattr($stdin, new_attr)
end

bbs_mode,in_file = pull_apart_args(ARGV)
puts "bbsmode: #{bbs_mode}"
sleep(2)
editor = Editors::FSED::Editor.new(80, 23, STDIN.to_io,
$>.to_io,in_file,bbs_mode)

buffer = editor.run
# if bbs_mode then
$lf.print "writing file...\n"
writefile(in_file,buffer.buff_out)
$lf.print "file written...\n"
#end
ensure
unless RUBY_PLATFORM =~ /mswin32/
tcsetattr($stdin, old_attr)
end
end







--
I know a mouse
And he hasn't got a house
I don't know why
I call him Gerald
He's getting rather old
But he's a good mouse

- Syd Barrett

From: Joel VanderWerf on
Dennis Nedry wrote:
> 1.8.6 (2007-09-24 patchlevel 111) [i686-linux]

Can you try upgrading to the latest 1.8.6 or 1.8.7? IIRC there were a
lot of changes to threading somewhere in the 1.8.6 patchlevels. Or it
could be something else...

From: Dennis Nedry on
On Tue, May 25, 2010 at 5:23 PM, Joel VanderWerf
<joelvanderwerf(a)gmail.com> wrote:
> Dennis Nedry wrote:
>>
>> 1.8.6 (2007-09-24 patchlevel 111) [i686-linux]
>
> Can you try upgrading to the latest 1.8.6 or 1.8.7? IIRC there were a lot of
> changes to threading somewhere in the 1.8.6 patchlevels. Or it could be
> something else...

I thought about that. I'm kind of nervous about doing that, because
the ruby BBS I wrote didn't like 1.8.7. It used to get segfaults.

I think you might be on to something. I don't think I'm doing
something wrong, it just ain't pretty.


--
I know a mouse
And he hasn't got a house
I don't know why
I call him Gerald
He's getting rather old
But he's a good mouse

- Syd Barrett

From: Dennis Nedry on
No takers I guess. Code must be worse than I thought... (;

--
I know a mouse
And he hasn't got a house
I don't know why
I call him Gerald
He's getting rather old
But he's a good mouse

- Syd Barrett