Add savefile and modeline :3

This commit is contained in:
Mewrry the Kitty 2026-02-12 13:46:36 -06:00
parent e5e0998e2c
commit 7ec21fd308

359
mewny.rb
View file

@ -7,6 +7,19 @@ require 'optparse'
PUBLIC_HOST = '0.0.0.0' PUBLIC_HOST = '0.0.0.0'
PRIVATE_HOST = '127.0.0.1' PRIVATE_HOST = '127.0.0.1'
DEFAULT_CLIENT_HOST = '127.0.0.1' DEFAULT_CLIENT_HOST = '127.0.0.1'
VERSION = {
:SRV_TXT => '0.01',
:CLN_TXT => '0.01',
:CLN_ARO => '0.02',
:SRV_CO8 => '0.30', # 8-color
:CLN_CO8 => '0.30', # 8-color
:SRV_GSZ => '0.40',
:FIL_TXT => '0.01',
:FIL_CUP => '0.02',
:FIL_CO8 => '0.30',
}
options = { options = {
:port => 7570, :port => 7570,
:server => false, :server => false,
@ -14,6 +27,9 @@ options = {
:server_host => PRIVATE_HOST, :server_host => PRIVATE_HOST,
:rows => 24, :rows => 24,
:cols => 40, :cols => 40,
:file => nil,
:file_lockout => false,
:file_interval => 600,
} }
OptionParser.new do |opt| OptionParser.new do |opt|
opt.on('-pPORT', '--port PORT', Integer, opt.on('-pPORT', '--port PORT', Integer,
@ -34,6 +50,11 @@ OptionParser.new do |opt|
'(client only) Host to connect to (defaults to `localhost`)') do |o| '(client only) Host to connect to (defaults to `localhost`)') do |o|
options[:host] = o options[:host] = o
end end
opt.on('-tSECONDS', '--save-interval SECONDS', Float,
'(server only) How long to wait between file writes in seconds. (defaults to 600 which is 10 minutes)'
) do |o|
options[:file_interval] = o
end
opt.on('-rROWS', '--rows ROWS', Integer, opt.on('-rROWS', '--rows ROWS', Integer,
'(server only) The number of rows to have in the shared buffer' '(server only) The number of rows to have in the shared buffer'
) do |o| ) do |o|
@ -58,11 +79,49 @@ OptionParser.new do |opt|
) do ) do
options[:server_host] = PRIVATE_HOST options[:server_host] = PRIVATE_HOST
end end
opt.on('-f FILE', '--save-file FILE',
'(server only) File to read initial state from and save state to.'
) do |f|
break if options[:file_lockout]
options[:file_lockout] = true
file_stuff = {
:name => f,
:text => '',
:color => [],
:cursor => 0,
:writeme => false
}
unless File.exist? f
file_stuff[:writeme] = true
options[:file] = file_stuff
options[:file_lockout] = false
next
end
File.readlines(f, chomp: true).each do |msg|
ver, msg = msg.chomp.split('|',2)
next unless msg != nil
case ver
when VERSION[:FIL_TXT]
file_stuff[:text] = msg
when VERSION[:FIL_CO8]
msg.each_char.with_index do |c, i|
next unless c =~ /[0-7]/
file_stuff[:color] << {
:co8 => 7
} until file_stuff[:color].length > i
file_stuff[:color][i][:co8] = c.to_i.clamp(0,7)
end
when VERSION[:FIL_CUP]
file_stuff[:cursor] = msg.to_i
end
end
options[:file] = file_stuff
end
opt.on('-h', '--help', 'Get help with this command') do opt.on('-h', '--help', 'Get help with this command') do
puts opt puts opt
exit exit
end end
end.parse!(into: options) end.parse!
HOST = options[:host] HOST = options[:host]
SRV_HOST = options[:server_host] SRV_HOST = options[:server_host]
@ -70,15 +129,22 @@ PORT = options[:port]
ROWS = options[:rows] ROWS = options[:rows]
COLS = options[:cols] COLS = options[:cols]
VERSION = { FILE_INTERVAL = options[:file_interval]
:SRV_TXT => '0.01',
:CLN_TXT => '0.01', def do_server(file_opts)
:CLN_ARO => '0.02', def write_current(wi, buffer, cobuf, file_opts)
:SRV_CO8 => '0.30', # 8-color return if file_opts.nil?
:CLN_CO8 => '0.30', # 8-color File.open(file_opts[:name], 'w') do |f|
} f.puts("#{VERSION[:FIL_TXT]}|#{clean_chars(buffer)}\n")
f.write("#{VERSION[:FIL_CO8]}|")
cobuf.each do |color|
f.write(color[:co8].to_s)
end
f.puts ''
f.puts("#{VERSION[:FIL_CUP]}|#{wi}")
end
end
def do_server
server = TCPServer.new(SRV_HOST, PORT) server = TCPServer.new(SRV_HOST, PORT)
sockets = [server] sockets = [server]
sockats = { sockats = {
@ -88,19 +154,33 @@ def do_server
} }
} }
} }
if file_opts.nil? || file_opts[:writeme]
pre = "#{ROWS}r#{COLS}c >>> "
buffer = pre.ljust(ROWS*COLS)[0...ROWS*COLS]
cobuf = Array.new(ROWS*COLS, {
:co8 => 7
})
windex = pre.length # Wipe your buffer clean with your write index
else
buffer = file_opts[:text].ljust(ROWS*COLS)[0...ROWS*COLS]
cobuf = file_opts[:color]
if cobuf.length < ROWS*COLS
cobuf += Array.new(ROWS*COLS - cobuf.length ) { {:co8 => 7} }
else
cobuf = cobuf.first(ROWS*COLS)
end
windex = file_opts[:cursor]
end
windex = windex%buffer.length
if not file_opts.nil?
write_current(windex, buffer, cobuf, file_opts)
end
puts "PORT: #{PORT}" puts "PORT: #{PORT}"
puts "GRID SIZE: #{ROWS}r#{COLS}c" puts "GRID SIZE: #{ROWS}r#{COLS}c"
if SRV_HOST == PUBLIC_HOST then puts "EXPOSED" end if SRV_HOST == PUBLIC_HOST then puts "EXPOSED" end
puts 'Listening...' puts 'Listening...'
pre = "#{ROWS}r#{COLS}c >>> "
buffer = pre.ljust(ROWS*COLS)[0...ROWS*COLS]
cobuf = Array.new(ROWS*COLS, {
:co8 => 7
})
windex = pre.length # Wipe your buffer clean with your write index
windex = windex%buffer.length
motions = [] motions = []
def new_buf_cob_wi_out(buf, cob, wi, newtext, newcolors, motions: []) def new_buf_cob_wi_out(buf, cob, wi, newtext, newcolors, motions: [])
@ -157,88 +237,128 @@ def do_server
[buf, cob, wi, out] [buf, cob, wi, out]
end end
loop do begin
ready, = IO.select(sockets) old = {
newtext = '' :windex => windex.dup,
newcolors = [] :buffer => buffer.dup,
:cobuf => cobuf.dup,
ready.each do |sck| :lastsave => Time.now,
if sck == server }
client = server.accept Thread.new do
sockats[client] = { loop do
:color => { time_since_next_save = Time.now - old[:lastsave]
:co8 => 7 time_till_next_save = FILE_INTERVAL - time_since_next_save
# To ensure thread doesn't bog down cpu too much.....
sleep [
[
[1.0, # Wait a second
time_till_next_save/2 # or half the remaining time
].max, # Whichever's longer
time_till_next_save].min, # Unless the remaining time is shorter than a second
1.0].max # But don't wait longer than no time at all
unless (old[:windex] == windex &&
old[:buffer] == buffer &&
old[:cobuf] == cobuf)
next if file_opts.nil?
next if time_since_next_save < FILE_INTERVAL
old = {
:lastsave => Time.now,
:windex => windex.dup,
:buffer => buffer.dup,
:cobuf => cobuf.dup
} }
} write_current(windex, buffer, cobuf, file_opts)
sockets << client puts "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] File autosaved."
puts "Connection: #{client.peeraddr}"
client.puts new_buf_cob_wi_out(buffer,cobuf,0,buffer,cobuf)[3]
if buffer.length > 1
client.puts new_buf_cob_wi_out(
buffer,cobuf,windex-1,
buffer[windex-1],[cobuf[windex-1]])[3]
end end
next
end end
begin end
msg = sck.gets loop do
unless msg ready, = IO.select(sockets)
puts "Disconnect: #{sck.peeraddr}" newtext = ''
sck.close newcolors = []
sockets.delete(sck)
sockats.delete(sck) ready.each do |sck|
if sck == server
client = server.accept
sockats[client] = {
:color => {
:co8 => 7
}
}
sockets << client
puts "Connection: #{client.peeraddr}"
client.puts "#{VERSION[:SRV_GSZ]}|#{ROWS}|#{COLS}"
client.puts new_buf_cob_wi_out(buffer,cobuf,0,buffer,cobuf)[3]
if buffer.length > 1
client.puts new_buf_cob_wi_out(
buffer,cobuf,windex-1,
buffer[windex-1],[cobuf[windex-1]])[3]
end
next next
end end
ver, msg = msg.chomp.split('|',2) begin
next unless msg != nil msg = sck.gets
case ver unless msg
when VERSION[:CLN_TXT] puts "Disconnect: #{sck.peeraddr}"
newtext << clean_chars(msg) sck.close
msg.length.times do sockets.delete(sck)
newcolors << sockats[sck][:color].dup sockats.delete(sck)
end
when VERSION[:CLN_ARO]
dir = 'l'
case msg
when 'A'
dir = 'u'
when 'B'
dir = 'd'
when 'C'
dir = 'r'
when 'D'
dir = 'l'
else
next next
end end
motions << { ver, msg = msg.chomp.split('|',2)
:nti => newtext.length - 1, next unless msg != nil
:dir => dir, case ver
} when VERSION[:CLN_TXT]
when VERSION[:CLN_CO8] newtext << clean_chars(msg)
sockats[sck][:color][:co8] = msg.to_i.clamp(0,7) msg.length.times do
end newcolors << sockats[sck][:color].dup
rescue Errno::ECONNRESET, Errno::EPIPE end
puts "Disconnect (err): #{sck.peeraddr}" when VERSION[:CLN_ARO]
sck.close dir = 'l'
sockets.delete(sck) case msg
end when 'A'
buffer, cobuf, windex, out = new_buf_cob_wi_out(buffer,cobuf,windex,newtext,newcolors,motions:motions) dir = 'u'
motions = [] when 'B'
clients = sockets - [server] dir = 'd'
clients.each do |c| when 'C'
begin dir = 'r'
c.puts out when 'D'
dir = 'l'
else
next
end
motions << {
:nti => newtext.length - 1,
:dir => dir,
}
when VERSION[:CLN_CO8]
sockats[sck][:color][:co8] = msg.to_i.clamp(0,7)
end
rescue Errno::ECONNRESET, Errno::EPIPE rescue Errno::ECONNRESET, Errno::EPIPE
puts "Disconnect (err): #{c.peeraddr}" puts "Disconnect (err): #{sck.peeraddr}"
c.close sck.close
sockets.delete(c) sockets.delete(sck)
end end
buffer, cobuf, windex, out = new_buf_cob_wi_out(buffer,cobuf,windex,newtext,newcolors,motions:motions)
motions = []
clients = sockets - [server]
clients.each do |c|
begin
c.puts out
rescue Errno::ECONNRESET, Errno::EPIPE
puts "Disconnect (err): #{c.peeraddr}"
c.close
sockets.delete(c)
end
end
#if out != ''
# puts "#{windex}: (#{(windex/COLS)+1},#{(windex%COLS)+1})"
#end
end end
#if out != ''
# puts "#{windex}: (#{(windex/COLS)+1},#{(windex%COLS)+1})"
#end
end end
ensure
write_current(windex, buffer, cobuf, file_opts)
puts "File saved on exit."
end end
end end
@ -256,12 +376,61 @@ def clean_chars(chars)
out out
end end
def do_client def do_client
socket = TCPSocket.new(HOST, PORT) socket = TCPSocket.new(HOST, PORT)
grid_rows = 24
grid_cols = 40
puts "PORT: #{PORT}" puts "PORT: #{PORT}"
puts "HOST: #{HOST}" puts "HOST: #{HOST}"
def print_mode(mode,gridrows)
return if ENV["TERM"].downcase == "dumb"
#print ("\x1b[s"+ # Save cursor
# "\x1b[m"+ # Reset color
# "\x1b[#{gridrows+1};1H"+# Cursor pos
# "\x1b[2K"+ # Clear line
# mode+ # Print mode
# "\x1b[u") # Restore cursor
print "\e[s\e[m\e[#{gridrows+1};1H\e[2K#{mode}\e[u"
end
def parse_esckey(gridrows)
begin
char2 = STDIN.read_nonblock(1)
return [:RAW, "\e#{char2}"] unless char2 == '['
rescue
return [:KEY_ESCAPE, '']
end
begin
char3 = STDIN.read_nonblock(1)
rescue
return [:RAW, "\e#{char2}"]
end
case char3
when 'A'
return [:ARROW, 'A']
when 'B'
return [:ARROW, 'B']
when 'C'
return [:ARROW, 'C']
when 'D'
return [:ARROW, 'D']
else
loop do
begin
char3 << STDIN.read_nonblock(1)
break unless char3[-1] =~ /[0-9:;\<=\>\?]/
rescue
break
end
end
print_mode "#{clean_chars("\e"+char2+char3)} -- I'm unfamiliar with that key.",gridrows
return [:RAW, "\e#{char2}#{char3}"]
end
end
begin begin
print "\x1b[?1049h\x1b[2J\x1b[3J" # Open and clear alternate buffer print "\x1b[?1049h\x1b[2J\x1b[3J" # Open and clear alternate buffer
print "\x1b[m" # Reset color print "\x1b[m" # Reset color
@ -280,12 +449,16 @@ def do_client
when VERSION[:SRV_CO8] when VERSION[:SRV_CO8]
next if ENV["TERM"].downcase == "dumb" next if ENV["TERM"].downcase == "dumb"
print "\x1b[3#{msg.to_i.clamp(0,7)}m" print "\x1b[3#{msg.to_i.clamp(0,7)}m"
when VERSION[:SRV_GSZ]
grid_rows, grid_cols = msg.split('|',2).map { |p| p.to_i }
print_mode ":3",grid_rows
end end
end end
end end
socket.puts "#{VERSION[:CLN_CO8]}|#{rand(1..7)}" socket.puts "#{VERSION[:CLN_CO8]}|#{rand(1..7)}"
while (char = STDIN.noecho(&:getch)) while (char = STDIN.noecho(&:getch))
print_mode ":3",grid_rows
exit if char == "\x04" # ^D exit if char == "\x04" # ^D
if char == "\x0F" # (^O) Color change if char == "\x0F" # (^O) Color change
if /[0-7]/.match? (char2 = STDIN.noecho(&:getch).upcase) if /[0-7]/.match? (char2 = STDIN.noecho(&:getch).upcase)
@ -295,12 +468,12 @@ def do_client
char << char2 char << char2
end end
elsif char == "\x1b" # Detect arrows elsif char == "\x1b" # Detect arrows
char2 = STDIN.noecho(&:getch) keytype, keytext = parse_esckey(grid_rows)
if /[A-D]/.match? (char3 = STDIN.noecho(&:getch).upcase) if keytype == :ARROW
socket.puts "#{VERSION[:CLN_ARO]}|#{char3}" socket.puts "#{VERSION[:CLN_ARO]}|#{keytext}"
next next
else elsif keytype != :KEY_ESCAPE
char << char2+char3 char = keytext
end end
elsif (char == "\x08" || # backspace elsif (char == "\x08" || # backspace
char == "\x7F" ) # delete char == "\x7F" ) # delete
@ -318,7 +491,7 @@ def do_client
end end
if options[:server] if options[:server]
do_server do_server(options[:file])
else else
do_client do_client
end end