diff --git a/app/assets/javascripts/main_worker.js b/app/assets/javascripts/main_worker.js index f183ecc..8922004 100644 --- a/app/assets/javascripts/main_worker.js +++ b/app/assets/javascripts/main_worker.js @@ -5,6 +5,7 @@ //= require utf8 //= require extensions //= require player/brush +//= require player/vt/ansi_interpreter //= require_tree ./player/vt //= require player/movie //= require player/workers/main_worker diff --git a/app/assets/javascripts/player.js b/app/assets/javascripts/player.js index f115a73..2e3d752 100644 --- a/app/assets/javascripts/player.js +++ b/app/assets/javascripts/player.js @@ -9,6 +9,7 @@ //= require player/views/hud_view //= require player/data_unpacker //= require player/abstract_player +//= require player/vt/ansi_interpreter //= require_tree ./player/vt //= require player/movie //= require player/player diff --git a/app/assets/javascripts/player/vt/ansi_interpreter.js.coffee b/app/assets/javascripts/player/vt/ansi_interpreter.js.coffee new file mode 100644 index 0000000..83d18b1 --- /dev/null +++ b/app/assets/javascripts/player/vt/ansi_interpreter.js.coffee @@ -0,0 +1,277 @@ +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] + + switch last + when "0" + if intermediate is '(' + @cb 'setSpecialCharset' + when "7" + @cb 'saveTerminalState' + when "8" + @cb '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 "@" + @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() + ")" diff --git a/app/assets/javascripts/player/vt/vt.js.coffee b/app/assets/javascripts/player/vt/vt.js.coffee index a6964f3..a265399 100644 --- a/app/assets/javascripts/player/vt/vt.js.coffee +++ b/app/assets/javascripts/player/vt/vt.js.coffee @@ -2,282 +2,23 @@ class AsciiIo.VT constructor: (@cols, @lines) -> _.extend(this, Backbone.Events) - @sgrInterpreter = new AsciiIo.SgrInterpreter() + @interpreter = new AsciiIo.AnsiInterpreter @onChange @reset() - reset: -> - @data = '' - @resetTerminal() + onChange: (action, args...) => + @[action](args...) - 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 + feed: (data) -> + rest = @interpreter.parse data + rest.length is 0 - 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) + reset: -> + @interpreter.reset() + @resetTerminal() 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 @@ -397,6 +138,91 @@ class AsciiIo.VT setSpecialCharset: -> @buffer.setCharset('special') +# ---- new + + print: (text) -> + @buffer.print text + + insertCharacters: (n) -> + @buffer.insertCharacters n + + priorRow: (n) -> + @buffer.priorRow n + + nextRow: (n) -> + @buffer.nextRow n + + nextColumn: (n) -> + @buffer.nextColumn n + + priorColumn: (n) -> + @buffer.priorColumn n + + nextRowFirstColumn: (n) -> + @buffer.nextRowFirstColumn n + + priorRowFirstColumn: (n) -> + @buffer.priorRowFirstColumn n + + goToColumn: (n) -> + @buffer.goToColumn n + + goToRowAndColumn: (n, m) -> + @buffer.goToRowAndColumn n, m + + goToNextHorizontalTabStop: (n = 1) -> + @buffer.goToNextHorizontalTabStop n + + eraseScreen: -> + @buffer.eraseScreen() + + eraseFromScreenStart: -> + @buffer.eraseFromScreenStart() + + eraseToScreenEnd: -> + @buffer.eraseToScreenEnd() + + eraseRow: -> + @buffer.eraseRow() + + eraseFromRowStart: -> + @buffer.eraseFromRowStart() + + eraseToRowEnd: -> + @buffer.eraseToRowEnd() + + insertLine: (n) -> + @buffer.insertLine n + + deleteLine: (n) -> + @buffer.deleteLine n + + deleteCharacters: (n) -> + @buffer.deleteCharacters n + + scrollUp: (n) -> + @buffer.scrollUp n + + scrollDown: (n) -> + @buffer.scrollDown n + + eraseCharacters: (n) -> + @buffer.eraseCharacters n + + goToPriorHorizontalTabStop: (n) -> + @buffer.goToPriorHorizontalTabStop n + + repeatLastCharacter: (n) -> + @buffer.repeatLastCharacter n + + goToRow: (n) -> + @buffer.goToRow n + + clearHorizontalTabStop: -> + @buffer.clearHorizontalTabStop() + + clearAllHorizontalTabStops: -> + @buffer.clearAllHorizontalTabStops() # References: # http://en.wikipedia.org/wiki/ANSI_escape_code