class AsciiIo.AnsiInterpreter constructor: (@callback) -> @cb = @callback @sgrInterpreter = new AsciiIo.SgrInterpreter() @reset() reset: -> @data = '' parse: (data) -> @data += data while @data.length > 0 processed = @handleData @data if processed is 0 # console.log "no kurwa: #{@formattedData(@data)}" break @data = @data.slice processed @data handleData: (data) -> if data.match(/^\x1b[\x00-\x1f]/) @handleControlCharacter(data[1]) return 2 else if match = data.match(/^(\x1b\x5d|\x9d).*?(\x1b\\|\x9c|\x07)/) # OSC seq return match[0].length else if match = data.match(/^(\x1b[PX_^]|[\x90\x98\x9e\x9f]).*?(\x1b\\|\x9c)/) # DCS/SOS/PM/APC seq return match[0].length else if match = data.match(/^(?:\x1b\x5b|\x9b)([\x30-\x3f]*?)[\x20-\x2f]*?[\x40-\x7e]/) # Control sequences @handleControlSequence(match[0], match[1], match) return match[0].length else if match = data.match(/^\x1b[\x20-\x2f]*?[\x30-\x3f]/) @handlePrivateEscSeq(match[0]) return match[0].length else if match = data.match(/^\x1b[\x20-\x2f]*?[\x40-\x5a\x5c\x5e-\x7e]/) # excluding \x5b ([) and \x5d (]) # they're both handled above @handleStandardEscSeq(match[0]) return match[0].length else if data.match(/^\x1b\x7f/) # DELETE return 2 else if data.match(/^[\x00-\x1a\x1c-\x1f]/) # excluding \x1b "ESC" @handleControlCharacter(data[0]) return 1 else if match = data.match(/^([\x20-\x7e]|[\xe2-\xe8]..|[\xc3-\xc5].|[\xa1-\xfe])+/) @handlePrintableCharacters(match[0]) return match[0].length else if data[0] is "\x7f" # DELETE, always and everywhere ignored return 1 else if data.match(/^[\x80-\x9f]/) @handleControlCharacter(data[0]) return 1 else if data[0] is "\xa0" # Same as SPACE (\x20) @handlePrintableCharacters(' ') return 1 else if data[0] is "\xff" # Same as DELETE (\x7f) return 1 else return 0 handleControlCharacter: (char) -> action = switch char when "\x07" 'bell' when "\x08" 'backspace' when "\x09" 'goToNextHorizontalTabStop' when "\x0a" 'lineFeed' when "\x0b" 'verticalTab' when "\x0c" 'formFeed' when "\x0d" 'carriageReturn' when "\x84" 'index' # "ESC D" when "\x85" 'newLine' # "ESC E" when "\x88" 'setHorizontalTabStop' # "ESC H" when "\x8d" 'reverseIndex' # "ESC M" @cb action if action handlePrintableCharacters: (text) -> @cb 'print', text handleStandardEscSeq: (data) -> last = data[data.length - 1] intermediate = data[data.length - 2] action = switch last when "A" if intermediate is '(' 'setUkCharset' when "B" if intermediate is '(' 'setUsCharset' when "D" 'index' when "E" 'newLine' when "H" 'setHorizontalTabStop' when "M" 'reverseIndex' when "c" 'resetTerminal' @cb action if action handlePrivateEscSeq: (data) -> last = data[data.length - 1] intermediate = data[data.length - 2] action = switch last when "0" if intermediate is '(' 'setSpecialCharset' when "7" 'saveTerminalState' when "8" 'restoreTerminalState' @cb action if action handleControlSequence: (data, params, match) -> if params and params.match(/^[\x3c-\x3f]/) @handlePrivateControlSequence(data, params) else @handleStandardControlSequence(data, params) handleStandardControlSequence: (data, params) -> term = data[data.length - 1] numbers = @parseParams(params) n = numbers[0] m = numbers[1] switch term when "@" @cb 'insertCharacters', n when "A" @cb 'priorRow', n when "B" @cb 'nextRow', n when "C" @cb 'nextColumn', n when "D" @cb 'priorColumn', n when "E" @cb 'nextRowFirstColumn', n when "F" @cb 'priorRowFirstColumn', n when "G" @cb 'goToColumn', n when "H" @cb 'goToRowAndColumn', n, m when "I" @cb 'goToNextHorizontalTabStop', n when "J" if n is 2 @cb 'eraseScreen' else if n is 1 @cb 'eraseFromScreenStart' else @cb 'eraseToScreenEnd' when "K" if n is 2 @cb 'eraseRow' else if n is 1 @cb 'eraseFromRowStart' else @cb 'eraseToRowEnd' when "L" @cb 'insertLine', n or 1 when "M" @cb 'deleteLine', n or 1 when "P" # DCH - Delete Character, from current position to end of field @cb 'deleteCharacters', n or 1 when "S" @cb 'scrollUp', n when "T" @cb 'scrollDown', n when "X" @cb 'eraseCharacters', n when "Z" @cb 'goToPriorHorizontalTabStop', n when "b" @cb 'repeatLastCharacter', n when "d" # VPA - Vertical Position Absolute @cb 'goToRow', n when "f" @cb 'goToRowAndColumn', n, m when "g" if !n or n is 0 @cb 'clearHorizontalTabStop' else if n is 3 @cb 'clearAllHorizontalTabStops' when "l" # l, Reset mode console.log "(TODO) reset: " + n when "m" @handleSGR numbers when "n" @cb 'reportRowAndColumn' when "r" # Set top and bottom margins (scroll region on VT100) if n is undefined n = 1 if m is undefined m = @lines @cb 'setScrollRegion', n, m handlePrivateControlSequence: (data, params) -> action = data[data.length - 1] modes = @parseParams(params) for mode in modes if mode is 25 if action is "h" @cb 'showCursor' else if action is "l" @cb 'hideCursor' else if mode is 47 if action is "h" @cb 'switchToAlternateBuffer' else if action is "l" @cb 'switchToNormalBuffer' else if mode is 1049 if action is "h" # Save cursor position, switch to alternate screen buffer, and clear screen. @cb 'switchToAlternateBuffer' @cb 'clearScreen' else if action is "l" # Clear screen, switch to normal screen buffer, and restore cursor position. @cb 'clearScreen' @cb 'switchToNormalBuffer' parseParams: (params) -> if params.length is 0 numbers = [] else numbers = _(params.replace(/[^0-9;]/, '').split(';')).map (n) -> if n is '' then undefined else parseInt(n, 10) numbers handleSGR: (numbers) -> # @buffer.setBrush @sgrInterpreter.buildBrush(@buffer.getBrush(), numbers) formattedData: (data) -> head = data.slice(0, 100) hex = ("0x#{c.charCodeAt(0).toString(16)}" for c in head) Utf8.decode(head) + " (" + hex.join() + ")"