#!/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 == "\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[m" # Reset color print "\x1b[?1049l" # Back to regular buffer end end if options[:server] do_server else do_client end