# # file:: terminalfilter.rb # author:: Jon A. Lambert # version:: 2.8.0 # date:: 01/19/2006 # # This source code copyright (C) 2005, 2006 by Jon A. Lambert # All rights reserved. # # Released under the terms of the TeensyMUD Public License # See LICENSE file for additional information. # $:.unshift "lib" if !$:.include? "lib" $:.unshift "vendor" if !$:.include? "vendor" require 'network/protocol/filter' require 'network/protocol/asciicodes' require 'network/protocol/vt100codes' # The TerminalFilter class implements a subset of ANSI/VT100 protocol. # class TerminalFilter < Filter include ASCIICodes include VT100Codes logger 'INFO' # Construct filter # # [+pstack+] The ProtocolStack associated with this filter def initialize(pstack) super(pstack) @mode = :ground # Parse mode :ground, :escape @csi = "" end # Run any post-contruction initialization # [+args+] Optional initial options def init(args=nil) true end # The filter_in method filters out VTxx terminal data and inserts format # strings into the input stream. # [+str+] The string to be processed # [+return+] The filtered data def filter_in(str) buf = "" str.each_byte do |b| case mode? when :ground case b when 0x20..0x7e buf << b.chr when ESC log.debug("(#{@pstack.conn.object_id}) ESC found") @collect = "" set_mode :escape # These cause immediate execution no matter what mode when ENQ, BEL, BS, TAB, VT, FF, SO, SI, DC1, DC3, CAN, SUB, DEL case b when BS, DEL log.debug("(#{@pstack.conn.object_id}) BS, DEL found") # slice local buffer or connection buffer buf.slice!(-1) || @pstack.conn.inbuffer.slice!(-1) when CAN, SUB @collect = "" set_mode :ground else buf << execute(b) end else buf << b.chr end when :escape case b when ?[ log.debug("(#{@pstack.conn.object_id}) CSI sequence found") set_mode :csi when ?] log.debug("(#{@pstack.conn.object_id}) OSC/XTERM sequence found") set_mode :xterm when ?P log.debug("(#{@pstack.conn.object_id}) DCS sequence found") set_mode :dcs when ?O log.debug("(#{@pstack.conn.object_id}) SS3 sequence found") set_mode :ss3 when ?X, ?^, ?_ log.debug("(#{@pstack.conn.object_id}) SOS/PM/APC sequence found") set_mode :sospmapc when ?D buf << "[SCROLLDOWN]" set_mode :ground when ?M buf << "[SCROLLUP]" set_mode :ground # VT52 when ?A buf << "[UP 1]" set_mode :ground when ?B buf << "[DOWN 1]" set_mode :ground when ?C buf << "[RIGHT 1]" set_mode :ground when ?D buf << "[LEFT 1]" set_mode :ground # /VT52 # when ?H # Set tab at current position - ignored # when ?E # Next line - like CRLF? # when ?7 # Save cursor and attributes SCURA # when ?8 # Restore cursor and attributes RCURA # when ?c # reset device # These cause immediate execution no matter what mode when ENQ, BEL, BS, TAB, VT, FF, SO, SI, DC1, DC3, CAN, SUB, DEL case b when BS, DEL log.debug("(#{@pstack.conn.object_id}) BS, DEL found") # slice local buffer or connection buffer buf.slice!(-1) || @pstack.conn.inbuffer.slice!(-1) when CAN, SUB @collect = "" set_mode :ground else buf << execute(b) end when 0x20..0x2F # " !"#$%&'()*+,-./" @collect << b.chr set_mode :escint else # These should all be immediately dispatched and sent to ground mode # when "0123456789:;<=>?@ABCDEFGHIJKLMNO" 0x30..0x4F # when "QRSTUVW" 0x51..0x57 # when "YZ" 0x59..0x5A # when "\\" 0x5C # when "`abcdefghijklmnopqrstuvwxyz{|}~" 0x60..0x7e set_mode :ground end when :escint # case b # when ?( # Set default font # when ?) # Set alternate font # both ( and ) may be followed by A,B,0,1,2 !! # end set_mode :ground when :dcs # terminated by ST or ESC \ set_mode :ground when :xterm set_mode :ground when :sospmapc set_mode :ground when :csi case b when ?A buf << "[UP #{@collect.to_i == 0 ? 1 : @collect.to_i}]" set_mode :ground when ?B buf << "[DOWN #{@collect.to_i == 0 ? 1 : @collect.to_i}]" set_mode :ground when ?C buf << "[RIGHT #{@collect.to_i == 0 ? 1 : @collect.to_i}]" set_mode :ground when ?D buf << "[LEFT #{@collect.to_i == 0 ? 1 : @collect.to_i}]" set_mode :ground when ?H, ?f # set cursor position \e[H or \e[<row>;<col>H a = @collect.split(";") a = ["1","1"] if a.empty? buf << "[HOME #{a[0]},#{a[1]}]" set_mode :ground when ?R # report cursor pos a = @collect.split(";") a = ["1","1"] if a.empty? buf << "[CURSOR #{a[0]},#{a[1]}]" set_mode :ground when ?r # Set scrolling region # Enable scrolling entire display \e[r or just a region \e[<srow>;<erow>r a = @collect.split(";") if a.empty? # lines numbered from 1 # This should be 1 to n or the whole screen if no parms buf << "[SCRRESET]" else buf << "[SCRREG #{a[0]},#{a[1]}]" end set_mode :ground when ?J if @collect.to_i == 2 buf << "[CLEAR]" end set_mode :ground # Erase from cursor to end of screen Esc [ 0 J or Esc [ J # Erase from beginning of screen to cursor Esc [ 1 J # Erase entire screen Esc [ 2 J when ?K if @collect.to_i == 2 buf << "[CLEARLINE]" end set_mode :ground # when ?K Erase line # Erase from cursor to end of line Esc [ 0 K or Esc [ K # Erase from beginning of line to cursor Esc [ 1 K # Erase line containing cursor Esc [ 2 K when ?G buf << "[POS #{@collect.to_i == 0 ? 1 : @collect.to_i}]" set_mode :ground # when ?G Set starting column of presentation when ?g, ?c, ?h, ?l, ?s, ?u, ?x, ?y, ?q, ?i, ?p # unhandled set_mode :ground # when ?c DA request/response - Device code - response is \e[<code>0c # when ?g Tab clear at current position # CSI 3 g is clear all tabs # when ?h Set mode # when ?l Reset mode # Enable Line Wrap <ESC>[7h # Disable Line Wrap <ESC>[7l # when ?s save cursor # when ?u unsave cursor # Same as ESC7 and ESC8 but not attributes? # when ?x Report terminal parameters # when ?y Confidence test # when ?q load LEDs # when ?i printing # when ?p Set Key Definition <ESC>[{key};"{string}"p # when ?P # -> set_mode :dcs when ?n # Device status request i = @collect.to_i if i == 6 # Query cursor position - response is \e[<row>;<col>R # request for report cursor pos buf << "[CURREPT]" end set_mode :ground when ?m # SGR color a = @collect.split(";") a.each do |cd| s = SGR2CODE[cd] buf << s if s end set_mode :ground when ?z # MXP mode buf << "[MXP @collect.to_i]" set_mode :ground when ?~ # keys i = @collect.to_i case i when 1, 7 buf << "[HOME 1,1]" when 2 buf << "[INSERT]" when 3 # delete log.debug("(#{@pstack.conn.object_id}) BS, DEL found") buf.slice!(-1) || @pstack.conn.inbuffer.slice!(-1) echo(BS.chr) when 4, 8 buf << "[END]" when 5 buf << "[PAGEUP]" when 6 buf << "[PAGEDOWN]" when 11 buf << "[F1]" when 12 buf << "[F2]" when 13 buf << "[F3]" when 14 buf << "[F4]" when 15 buf << "[F5]" when 17 buf << "[F6]" when 18 buf << "[F7]" when 19 buf << "[F8]" when 20 buf << "[F9]" when 21 buf << "[F10]" end set_mode :ground # These cause immediate execution no matter what mode when ENQ, BEL, BS, TAB, VT, FF, SO, SI, DC1, DC3, CAN, SUB, DEL case b when BS, DEL log.debug("(#{@pstack.conn.object_id}) BS, DEL found") # slice local buffer or connection buffer buf.slice!(-1) || @pstack.conn.inbuffer.slice!(-1) when CAN, SUB @collect = "" set_mode :ground else buf << execute(b) end else @collect << b.chr end when :ss3 case b # Windows XP telnet when ?P buf << "[F1]" when ?Q buf << "[F2]" when ?R buf << "[F3]" when ?S buf << "[F4]" # /Windows XP telnet # ANSI cursor key mode when ?A buf << "[UP 1]" when ?B buf << "[DOWN 1]" when ?C buf << "[RIGHT 1]" when ?D buf << "[LEFT 1]" # /ANSI cursor key mode # These cause immediate execution no matter what mode when ENQ, BEL, BS, TAB, VT, FF, SO, SI, DC1, DC3, CAN, SUB, DEL case b when BS, DEL log.debug("(#{@pstack.conn.object_id}) BS, DEL found") # slice local buffer or connection buffer buf.slice!(-1) || @pstack.conn.inbuffer.slice!(-1) when CAN, SUB @collect = "" set_mode :ground else buf << execute(b) end end set_mode :ground end end # eachwhile b buf end # The filter_out method filters output data # [+str+] The string to be processed # [+return+] The filtered data def filter_out(str) return '' if str.nil? || str.empty? case @pstack.terminal when /^vt/, 'xterm' VTKeys.each do |key,val| while str =~ key do s = val.dup p1 = $1.dup if $1 p2 = $2.dup if $2 if p1 && p2 s.sub!(/\$;\$/, "#{p1};#{p2}") elsif p1 s.sub!(/\$/, p1) end str.sub!(key,s) end end else VTKeys.each do |key,val| while str =~ key do str.sub!(key,"") end end end str end # Handle server-side echo def echo(ch) if @pstack.echo_on if @pstack.hide_on && ch[0] != CR @pstack.conn.sock.send('*',0) else @pstack.conn.sock.send(ch,0) end end end def execute(b) case b when ENQ, SO, SI, DC1, DC3 # not handled # ENQ Transmit ANSWERBACK message # SO Switch to G1 character set # SI Switch to G0 character set # DC1 Causes terminal to resume transmission (XON). # DC3 Causes terminal to stop transmitting all codes except XOFF and XON (XOFF). "" when VT, FF "[UP 1]" when TAB log.debug("(#{@pstack.conn.object_id}) TAB found") "[TAB]" when BEL "[BELL]" else "" end end # Get current parse mode # [+return+] The current parse mode def mode? return @mode end # set current parse mode # [+m+] Mode to set it to def set_mode(m) @mode = m end end