324 lines
8.3 KiB
Ruby
Executable file
324 lines
8.3 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('--expose-this-instance-for-the-world-to-see-because-I-know-what-I\'m-doing [please]',
|
|
# X -- eXposed
|
|
'(server only) Serve publicly, rather than privately. Please ONLY add this option if you entierly know what you\'re doing and have permission from/are the server owner. Say please to expose.'
|
|
) do |o|
|
|
if o == "please"
|
|
options[:server_host] = PUBLIC_HOST
|
|
end
|
|
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',
|
|
:SRV_CO8 => '0.30', # 8-color
|
|
:CLN_CO8 => '0.30', # 8-color
|
|
}
|
|
|
|
def do_server
|
|
server = TCPServer.new(SRV_HOST, PORT)
|
|
sockets = [server]
|
|
sockats = {
|
|
server => {
|
|
:color => {
|
|
:co8 => 7
|
|
}
|
|
}
|
|
}
|
|
|
|
puts "PORT: #{PORT}"
|
|
puts "GRID SIZE: #{ROWS}r#{COLS}c"
|
|
if SRV_HOST == PUBLIC_HOST then puts "EXPOSED" end
|
|
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 = []
|
|
|
|
def new_buf_cob_wi_out(buf, cob, wi, newtext, newcolors, motions: [])
|
|
buf = buf.dup
|
|
cob = cob.dup
|
|
out = ''
|
|
nti = 0 # New Text Index
|
|
wco = {} #Writing Color
|
|
cur = motions.length > 0 #Update cursor at end
|
|
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 = wi%(ROWS*COLS)
|
|
motions.shift # remove first motion
|
|
end
|
|
next if nti >= newtext.length
|
|
if newcolors[nti] != wco
|
|
wco = newcolors[nti]
|
|
if wco.has_key? :co8
|
|
out << "#{VERSION[:SRV_CO8]}|#{wco[:co8]}\n"
|
|
end
|
|
end
|
|
out << "#{VERSION[:SRV_TXT]}|"
|
|
out << ((wi/COLS)+1).to_s + '|'
|
|
out << ((wi%COLS)+1).to_s + '|'
|
|
loop do
|
|
out << newtext[nti]
|
|
buf[wi] = newtext[nti]
|
|
cob[wi] = wco
|
|
wi += 1
|
|
nti += 1
|
|
break if newcolors[nti] != wco
|
|
break if motions.length > 0 && nti >= motions[0][:nti]
|
|
break if nti >= newtext.length
|
|
break if wi%COLS == 0
|
|
end
|
|
wi = wi%(ROWS*COLS)
|
|
out << "\n"
|
|
end
|
|
if cur
|
|
out << "#{VERSION[:SRV_TXT]}|"
|
|
out << ((wi/COLS)+1).to_s + '|'
|
|
out << ((wi%COLS)+1).to_s + "|\n"
|
|
end
|
|
[buf, cob, wi, out]
|
|
end
|
|
|
|
loop do
|
|
ready, = IO.select(sockets)
|
|
newtext = ''
|
|
newcolors = []
|
|
|
|
ready.each do |sck|
|
|
if sck == server
|
|
client = server.accept
|
|
sockats[client] = {
|
|
:color => {
|
|
:co8 => 7
|
|
}
|
|
}
|
|
sockets << client
|
|
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
|
|
next
|
|
end
|
|
begin
|
|
msg = sck.gets
|
|
unless msg
|
|
puts "Disconnect: #{sck.peeraddr}"
|
|
sck.close
|
|
sockets.delete(sck)
|
|
sockats.delete(sck)
|
|
next
|
|
end
|
|
ver, msg = msg.chomp.split('|',2)
|
|
next unless msg != nil
|
|
case ver
|
|
when VERSION[:CLN_TXT]
|
|
newtext << clean_chars(msg)
|
|
msg.length.times do
|
|
newcolors << sockats[sck][:color].dup
|
|
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
|
|
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
|
|
puts "Disconnect (err): #{sck.peeraddr}"
|
|
sck.close
|
|
sockets.delete(sck)
|
|
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
|
|
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
|
|
print "\x1b[m" # Reset color
|
|
|
|
#reader = Thread.new do
|
|
Thread.new do
|
|
loop do
|
|
msg = socket.gets
|
|
exit 0 unless msg
|
|
ver, msg = msg.split('|',2)
|
|
next unless msg != nil
|
|
case ver
|
|
when VERSION[:SRV_TXT]
|
|
row,col,chars = msg.split('|',3)
|
|
print "\x1b[#{row};#{col}H#{chars.chomp}"
|
|
when VERSION[:SRV_CO8]
|
|
next if ENV["TERM"].downcase == "dumb"
|
|
print "\x1b[3#{msg.to_i.clamp(0,7)}m"
|
|
end
|
|
end
|
|
end
|
|
|
|
socket.puts "#{VERSION[:CLN_CO8]}|#{rand(1..7)}"
|
|
while (char = STDIN.noecho(&:getch))
|
|
exit if char == "\x04" # ^D
|
|
if char == "\x0F" # (^O) Color change
|
|
if /[0-7]/.match? (char2 = STDIN.noecho(&:getch).upcase)
|
|
socket.puts "#{VERSION[:CLN_CO8]}|#{char2}"
|
|
next
|
|
else
|
|
char << char2
|
|
end
|
|
elsif 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"
|
|
socket.puts "#{VERSION[:CLN_TXT]}| "
|
|
socket.puts "#{VERSION[:CLN_ARO]}|D"
|
|
next
|
|
end
|
|
socket.puts "#{VERSION[:CLN_TXT]}|#{clean_chars(char)}"
|
|
end
|
|
ensure
|
|
print "\x1b[m" # Reset color
|
|
print "\x1b[?1049l" # Back to regular buffer
|
|
end
|
|
end
|
|
|
|
if options[:server]
|
|
do_server
|
|
else
|
|
do_client
|
|
end
|