#!/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', } def do_server server = TCPServer.new(SRV_HOST, PORT) sockets = [server] 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] 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