class AsciiIo.VT constructor: (@cols, @lines) -> _.extend(this, Backbone.Events) @sgrInterpreter = new AsciiIo.SgrInterpreter() @reset() reset: -> @data = '' @resetTerminal() 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) -> switch char when "\x07" @bell() when "\x08" @backspace() when "\x09" @buffer.goToNextHorizontalTabStop() # @tab() 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" handlePrintableCharacters: (text) -> @buffer.print text handleStandardEscSeq: (data) -> last = data[data.length - 1] intermediate = data[data.length - 2] 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() handlePrivateEscSeq: (data) -> last = data[data.length - 1] intermediate = data[data.length - 2] switch last when "0" if intermediate is '(' @setSpecialCharset() when "7" @saveTerminalState() when "8" @restoreTerminalState() 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 "@" @buffer.insertCharacters n when "A" @buffer.priorRow n when "B" @buffer.nextRow n when "C" @buffer.nextColumn n when "D" @buffer.priorColumn n when "E" @buffer.nextRowFirstColumn n when "F" @buffer.priorRowFirstColumn n when "G" @buffer.goToColumn n when "H" @buffer.goToRowAndColumn n, m when "I" @buffer.goToNextHorizontalTabStop n when "J" if n is 2 @buffer.eraseScreen() else if n is 1 @buffer.eraseFromScreenStart() else @buffer.eraseToScreenEnd() when "K" if n is 2 @buffer.eraseRow() else if n is 1 @buffer.eraseFromRowStart() else @buffer.eraseToRowEnd() when "L" @buffer.insertLine n or 1 when "M" @buffer.deleteLine n or 1 when "P" # DCH - Delete Character, from current position to end of field @buffer.deleteCharacters n or 1 when "S" @buffer.scrollUp n when "T" @buffer.scrollDown n when "X" @buffer.eraseCharacters n when "Z" @buffer.goToPriorHorizontalTabStop n when "b" @buffer.repeatLastCharacter n when "d" # VPA - Vertical Position Absolute @buffer.goToRow n when "f" @buffer.goToRowAndColumn n, m when "g" if !n or n is 0 @buffer.clearHorizontalTabStop() else if n is 3 @buffer.clearAllHorizontalTabStops() when "l" # l, Reset mode console.log "(TODO) reset: " + n when "m" @handleSGR numbers when "n" @reportRowAndColumn() when "r" # Set top and bottom margins (scroll region on VT100) if n is undefined n = 1 if m is undefined m = @lines @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" @showCursor() else if action is "l" @hideCursor() else if mode is 47 if action is "h" @switchToAlternateBuffer() else if action is "l" @switchToNormalBuffer() else if mode is 1049 if action is "h" # Save cursor position, switch to alternate screen buffer, and clear screen. @switchToAlternateBuffer() @clearScreen() else if action is "l" # Clear screen, switch to normal screen buffer, and restore cursor position. @clearScreen() @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) bell: -> @trigger 'bell' feed: (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.length is 0 formattedData: (data) -> head = data.slice(0, 100) hex = ("0x#{c.charCodeAt(0).toString(16)}" for c in head) Utf8.decode(head) + " (" + hex.join() + ")" state: -> changes: @buffer.changes() cursorX: @buffer.cursorX cursorY: @buffer.cursorY clearChanges: -> @buffer.clearChanges() # ==== Screen buffer operations clearScreen: -> @buffer.clear() switchToNormalBuffer: -> @buffer = @normalBuffer @updateScreen() switchToAlternateBuffer: -> @alternateBuffer.setBrush(@normalBuffer.getBrush()) @alternateBuffer.setCharset(@normalBuffer.getCharset()) @buffer = @alternateBuffer @updateScreen() updateScreen: -> @buffer.updateScreen() carriageReturn: -> @buffer.goToFirstColumn() backspace: -> @buffer.priorColumn() # === ANSI handlers # ------ Cursor control showCursor: -> @trigger 'cursor-visibility', true hideCursor: -> @trigger 'cursor-visibility', false # ----- Scroll control reverseIndex: -> @buffer.goToPriorRow() lineFeed: -> @buffer.goToNextRow() verticalTab: -> @buffer.goToNextRow() formFeed: -> @buffer.goToNextRow() index: -> @buffer.goToNextRow() newLine: -> @buffer.goToNextRowFirstColumn() # === Commands # ----- Scroll control setScrollRegion: (top, bottom) -> if top < 1 top = 1 if bottom > @lines bottom = @lines if bottom > top @scrollRegion.setTop(top - 1) @scrollRegion.setBottom(bottom - 1) setHorizontalTabStop: -> @tabStops.add(@cursorX) # ----- Terminal control resetTerminal: -> @scrollRegion = new AsciiIo.ScrollRegion(0, @lines - 1) @tabStops = new AsciiIo.TabStops(@cols) @normalBuffer = new AsciiIo.ScreenBuffer(@cols, @lines, @scrollRegion, @tabStops) @alternateBuffer = new AsciiIo.ScreenBuffer(@cols, @lines, @scrollRegion, @tabStops) @buffer = @normalBuffer @fg = @bg = undefined @bright = false @underline = false @italic = false @updateScreen() saveTerminalState: -> @buffer.saveCursor() @scrollRegion.save() @buffer.saveBrush() restoreTerminalState: -> @buffer.restoreBrush() @scrollRegion.restore() @buffer.restoreCursor() reportRowAndColumn: -> setUkCharset: -> @buffer.setCharset('uk') setUsCharset: -> @buffer.setCharset('us') setSpecialCharset: -> @buffer.setCharset('special') # References: # http://en.wikipedia.org/wiki/ANSI_escape_code # http://ttssh2.sourceforge.jp/manual/en/about/ctrlseq.html # http://real-world-systems.com/docs/ANSIcode.html # http://www.shaels.net/index.php/propterm/documents # http://manpages.ubuntu.com/manpages/lucid/man7/urxvt.7.html # http://vt100.net/docs/vt102-ug/chapter5.html