mewny/mewny.rb
2026-02-07 17:07:50 -06:00

265 lines
6.4 KiB
Ruby
Executable file

#!/usr/bin/env ruby
require 'socket'
require 'io/console'
require 'optparse'
PUBLIC_HOST = '0.0.0.0'
PRIVATE_HOST = '127.0.0.1'
DEFAULT_CLIENT_HOST = '127.0.0.1'
options = {
:port => 7570,
:server => false,
:host => DEFAULT_CLIENT_HOST,
:server_host => PRIVATE_HOST,
:rows => 24,
:cols => 40,
}
OptionParser.new do |opt|
opt.on('-pPORT', '--port PORT', Integer,
'Port to serve/listen on (defaults to 7570)') do |o|
options[:port] = o
end
opt.on('-S', '--server',
'Make this instance function as a server.'
) do
options[:server] = true
end
opt.on('-s', '--client',
'Make this instance function as a client'
) do
options[:server] = false
end
opt.on('-HHOST', '--host HOST', String,
'(client only) Host to connect to (defaults to `localhost`)') do |o|
options[:host] = o
end
opt.on('-rROWS', '--rows ROWS', Integer,
'(server only) The number of rows to have in the shared buffer'
) do |o|
options[:rows] = o
end
opt.on('-cCOLS', '--cols COLS', Integer,
'(server only) The number of columns to have in the shared buffer'
) do |o|
options[:cols] = o
end
opt.on('-X', '--public',
# X -- eXposed
'(server only) Serve publicly, rather than privately'
) do
options[:server_host] = PUBLIC_HOST
end
opt.on('-x', '--private',
# x -- not visible
'(server only) Serve privately, rather than publicly (default)'
) do
options[:server_host] = PRIVATE_HOST
end
opt.on('-h', '--help', 'Get help with this command') do
puts opt
exit
end
end.parse!(into: options)
HOST = options[:host]
SRV_HOST = options[:server_host]
PORT = options[:port]
ROWS = options[:rows]
COLS = options[:cols]
VERSION = {
:SRV_TXT => '0.01',
:CLN_TXT => '0.01',
:CLN_ARO => '0.02',
}
def do_server
server = TCPServer.new(SRV_HOST, PORT)
sockets = [server]
puts "PORT: #{PORT}"
puts "GRID SIZE: #{ROWS}r#{COLS}c"
puts 'Listening...'
pre = "#{ROWS}r#{COLS}c >>> "
buffer = pre.ljust(ROWS*COLS)[0...ROWS*COLS]
windex = pre.length # Wipe your buffer clean with your write index
windex %= buffer.length
motions = []
def new_buf_wi_out(buf, wi, newtext, motions: [])
buf = buf.dup
out = ''
nti = 0 # New Text Index
while (nti < newtext.length) || (motions.length > 0)
if motions.length > 0 && nti >= motions[0][:nti]
case motions[0][:dir]
when 'u'
wi -= COLS
when 'd'
wi += COLS
when 'l'
wi -= 1
when 'r'
wi += 1
end
wi %= ROWS*COLS
motions.shift # remove first motion
end
out << "#{VERSION[:SRV_TXT]}|"
out << ((wi/COLS)+1).to_s + '|'
out << ((wi%COLS)+1).to_s + '|'
next if nti >= newtext.length
loop do
out << newtext[nti]
buf[wi] = newtext[nti]
wi += 1
nti += 1
break if motions.length > 0 && nti >= motions[0][:nti]
break if nti >= newtext.length
break if wi%COLS == 0
end
wi %= ROWS*COLS
out << "\n"
end
[buf, wi, out]
end
loop do
ready, = IO.select(sockets)
newtext = ''
ready.each do |sck|
if sck == server
client = server.accept
sockets << client
puts "Connection: #{client.peeraddr}"
client.puts new_buf_wi_out(buffer,0,buffer)[2]
if buffer.length > 1
client.puts new_buf_wi_out(buffer,windex-1,buffer[windex-1])[2]
end
next
end
begin
msg = sck.gets
unless msg
puts "Disconnect: #{sck.peeraddr}"
sck.close
sockets.delete(sck)
next
end
ver, msg = msg.chomp.split('|',2)
case ver
when VERSION[:CLN_TXT]
next unless msg != nil
newtext << clean_chars(msg)
when VERSION[:CLN_ARO]
next unless msg != nil
dir = 'l'
case msg
when 'A'
dir = 'u'
when 'B'
dir = 'd'
when 'C'
dir = 'r'
when 'D'
dir = 'l'
else
next
end
motions << {
:nti => newtext.length - 1,
:dir => dir
}
end
rescue Errno::ECONNRESET, Errno::EPIPE
puts "Disconnect (err): #{sck.peeraddr}"
sck.close
sockets.delete(sck)
end
buffer, windex, out = new_buf_wi_out(buffer,windex,newtext,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
end
def clean_chars(chars)
out = ''
chars.each_char do |char|
if char.ord < 0x20 # Non printing
out << '^'+(char.ord + 0x40).chr
elsif char.ord == 0x7F # DEL (Non print)
out << '^?'
else
out << char
end
end
out
end
def do_client
socket = TCPSocket.new(HOST, PORT)
puts "PORT: #{PORT}"
puts "HOST: #{HOST}"
begin
print "\x1b[?1049h\x1b[2J\x1b[3J" # Open and clear alternate buffer
#reader = Thread.new do
Thread.new do
loop do
msg = socket.gets
exit 0 unless msg
ver, msg = msg.split('|',2)
next unless ver == VERSION[:SRV_TXT]
next unless msg != nil
# In message version 0.01 and lower,
# all messages are in an assumed format...
row,col,chars = msg.split('|',3)
print "\x1b[#{row};#{col}H#{chars.chomp}"
end
end
while (char = STDIN.noecho(&:getch))
exit unless char != "\x04" # ^D
if char == "\x1b" # Detect arrows
char2 = STDIN.noecho(&:getch)
if /[A-D]/.match? (char3 = STDIN.noecho(&:getch).upcase)
socket.puts "#{VERSION[:CLN_ARO]}|#{char3}"
next
else
char << char2+char3
end
elsif (char == "\x08" || # backspace
char == "\x7F" ) # delete
socket.puts "#{VERSION[:CLN_ARO]}|D"
next
end
socket.puts "#{VERSION[:CLN_TXT]}|#{clean_chars(char)}"
end
ensure
print "\x1b[?1049l" # Back to regular buffer
end
end
if options[:server]
do_server
else
do_client
end