From a378358abfe76f49524cc1d360d28223736737a3 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Thu, 9 Feb 2012 22:31:14 +0100 Subject: [PATCH] Improvements to interpreter and screen buffer --- .../javascripts/ansi-interpreter.js.coffee | 9 +- .../javascripts/screen_buffer.js.coffee | 8 +- spec/javascripts/ansi-interpreter_spec.coffee | 245 ++++++++++++++++-- 3 files changed, 238 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/ansi-interpreter.js.coffee b/app/assets/javascripts/ansi-interpreter.js.coffee index 1d62bd5..47fb341 100644 --- a/app/assets/javascripts/ansi-interpreter.js.coffee +++ b/app/assets/javascripts/ansi-interpreter.js.coffee @@ -78,6 +78,7 @@ class AsciiIo.AnsiInterpreter }) PATTERNS: + "\x00": (data) -> "\x07": (data) -> @sb.bell() "\x08": (data) -> @sb.backspace() "\x09": (data) -> # Moves the cursor to the next tab stop @@ -145,10 +146,10 @@ class AsciiIo.AnsiInterpreter # Save cursor position, switch to alternate screen buffer, and clear screen. @sb.saveCursor() @sb.switchToAlternateBuffer() - @sb.clearScreen() + @sb.clear() else if action is "l" # Clear screen, switch to normal screen buffer, and restore cursor position. - @sb.clearScreen() + @sb.clear() @sb.switchToNormalBuffer() @sb.restoreCursor() else @@ -165,7 +166,7 @@ class AsciiIo.AnsiInterpreter "\x1bP([^\\\\])*?\\\\": (data) -> # DCS, Device Control String "\x1bM": -> - @sb.ri @n or 1 + @sb.ri() "\x1b\x37": (data) -> # save cursor pos and char attrs @sb.saveCursor() @@ -269,4 +270,4 @@ class AsciiIo.AnsiInterpreter break unless match - data + data.length is 0 diff --git a/app/assets/javascripts/screen_buffer.js.coffee b/app/assets/javascripts/screen_buffer.js.coffee index 92abaed..cdc5d2a 100644 --- a/app/assets/javascripts/screen_buffer.js.coffee +++ b/app/assets/javascripts/screen_buffer.js.coffee @@ -11,6 +11,9 @@ class AsciiIo.ScreenBuffer setBrush: (brush) -> @brush = brush + clear: -> + @lineData.length = 0 + getLine: (n) -> n = (if typeof n isnt "undefined" then n else @cursorY) @@ -108,9 +111,6 @@ class AsciiIo.ScreenBuffer @cursorLeft() @updateLine() - bell: -> - # nothing to do - print: (text) -> text = Utf8.decode(text) @@ -174,7 +174,7 @@ class AsciiIo.ScreenBuffer @lineData[@cursorY] = line.slice(0, @cursorX).concat(" ".times(n).split(""), line.slice(@cursorX, @cols - n)) @updateLine() - ri: (n) -> + ri: (n=1) -> i = 0 while i < n if @cursorY is 0 diff --git a/spec/javascripts/ansi-interpreter_spec.coffee b/spec/javascripts/ansi-interpreter_spec.coffee index e0858cf..b8243dd 100644 --- a/spec/javascripts/ansi-interpreter_spec.coffee +++ b/spec/javascripts/ansi-interpreter_spec.coffee @@ -3,6 +3,11 @@ describe AsciiIo.AnsiInterpreter, -> cols = 80 lines = 24 + isSwallowed = (d) -> + screenBuffer = {} # will throw 'undefined is not a function' + interpreter = new AsciiIo.AnsiInterpreter(screenBuffer) + expect(interpreter.feed(d || data)).toEqual(true) + beforeEach -> screenBuffer = new AsciiIo.ScreenBuffer(cols, lines) interpreter = new AsciiIo.AnsiInterpreter(screenBuffer) @@ -13,15 +18,6 @@ describe AsciiIo.AnsiInterpreter, -> # A single character with an ASCII code within the ranges: 000 to 037 and # 200 to 237 octal, 00 - 1F and 80 - 9F hex. - describe 'x00', -> - beforeEach -> - screenBuffer = {} # will throw 'undefined is not a function' - interpreter = new AsciiIo.AnsiInterpreter(screenBuffer) - - it 'is ignored', -> - data += '\x00' - interpreter.feed(data) - describe 'x07', -> it 'calls bell', -> data += '\x07' @@ -50,6 +46,12 @@ describe AsciiIo.AnsiInterpreter, -> interpreter.feed(data) expect(screenBuffer.cr).toHaveBeenCalled() + describe 'other', -> + it "is swallowed", -> + for c in ['\x00', '\x09', '\x0e', '\x0f', '\x82', '\x94'] + isSwallowed(c) + + describe 'printable character', -> describe 'from ASCII range (0x20-0x7e)', -> it 'calls print', -> @@ -80,8 +82,11 @@ describe AsciiIo.AnsiInterpreter, -> beforeEach -> data += '\x1b' - describe 'with ESC + control character inside', -> + describe 'with C0 control nested inside another escape sequence', -> + # C0 Control = 00-1F # Interpret the character, then resume processing the sequence. + # Example: CR, LF, XON, and XOFF work as normal within an ESCape + # sequence. # it 'aaa', -> # data += '[1\x1b\x0dm' @@ -91,7 +96,126 @@ describe AsciiIo.AnsiInterpreter, -> # expect(screenBuffer.cr).toHaveBeenCalled() # expect(screenBuffer.setSGR).toHaveBeenCalledWith([1]) - describe 'control sequence', -> + describe 'with intermediate', -> + # Intermediate = 20-2F !"#$%&'()*+,-./ + # Expect zero or more intermediates, a parameter terminates a private + # function, an alphabetic terminates a standard sequence. Example: ESC + # ( A defines standard character set, ESC ( 0 a DEC set. + + describe '(', -> + + beforeEach -> + data += '(' + + describe 'B', -> + + beforeEach -> + data += 'B' + + it 'is swallowed', isSwallowed + + describe 'followed by parameter', -> + # private function + + describe 'followed by an alphabetic', -> + # standard sequence + + describe 'with parameter', -> + # Parameter = 30-3F 0123456789:;<=>? + # End of a private 2-character escape sequence. Example: ESC = sets + # special keypad mode, ESC > clears it. + + describe '7', -> + beforeEach -> + data += '7' + + it 'saves cursor', -> + spyOn screenBuffer, 'saveCursor' + interpreter.feed(data) + expect(screenBuffer.saveCursor).toHaveBeenCalled() + + describe '8', -> + beforeEach -> + data += '8' + + it 'restores cursor', -> + spyOn screenBuffer, 'restoreCursor' + interpreter.feed(data) + expect(screenBuffer.restoreCursor).toHaveBeenCalled() + + describe '=', -> + beforeEach -> + data += '=' + + it 'is swallowed', isSwallowed + + describe '>', -> + beforeEach -> + data += '>' + + it 'is swallowed', isSwallowed + + describe 'with uppercase', -> + # Uppercase = 40-5F @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ + # Translate it into a C1 control character and act on it. Example: ESC + # D does indexes down, ESC M indexes up. (CSI is special) + + describe 'M', -> + # Reverse Index, go up one line, reverse scroll if necessary + + beforeEach -> + data += 'M' + + it 'goes up 1 line', -> + spyOn screenBuffer, 'ri' + interpreter.feed(data) + expect(screenBuffer.ri).toHaveBeenCalled() + + describe 'P', -> + # Device Control String, terminated by ST + + beforeEach -> + data += 'Pfoobar\\' + + it 'is swallowed', isSwallowed + + describe ']', -> + # Operating system command + + beforeEach -> + screenBuffer = {} # will throw 'undefined is not a function' + interpreter = new AsciiIo.AnsiInterpreter(screenBuffer) + + describe '0;...BELL', -> + beforeEach -> + data += ']0;foobar\x07' + + it 'is swallowed', isSwallowed + + describe '1;...BELL', -> + beforeEach -> + data += ']1;foobar\x07' + + it 'is swallowed', isSwallowed + + describe '2;...BELL', -> + beforeEach -> + data += ']2;foobar\x07' + + it 'is swallowed', isSwallowed + + + describe 'with lowercase', -> + # Lowercase = 60-7E `abcdefghijlkmnopqrstuvwxyz{|}~ + # End of a standard 2-character escape sequence. Example: ESC c resets + # the terminal. + + describe 'with delete', -> + # Delete = 7F + # Ignore it, and continue interpreting the ESCape sequence C1 and G1: + # Treat the same as their 7-bit counterparts + + describe 'with control sequence', -> # A string starting with CSI (233 octal, 9B hex) or with ESC[ # (Left-Bracket) and terminated by an alphabetic character. Any number of # parameter characters (digits 0 to 9, semicolon, and question mark) may @@ -102,11 +226,11 @@ describe AsciiIo.AnsiInterpreter, -> data += '[' describe 'buffering', -> - it 'allows parsing in chunks', -> - spyOn interpreter, 'handleSGR' - interpreter.feed(data) - interpreter.feed('m') - expect(interpreter.handleSGR).toHaveBeenCalled() + # it 'allows parsing in chunks', -> + # spyOn interpreter, 'handleSGR' + # interpreter.feed(data) + # interpreter.feed('m') + # expect(interpreter.handleSGR).toHaveBeenCalled() describe '@', -> it 'calls reserveCharacters', -> @@ -290,6 +414,95 @@ describe AsciiIo.AnsiInterpreter, -> interpreter.feed(data) expect(screenBuffer.deleteCharacter).toHaveBeenCalledWith(3) + describe 'c', -> + beforeEach -> + data += '>c' + + it 'is swallowed', isSwallowed + + describe 'from private standards', -> + # first character after CSI is one of: " < = > (074-077 octal, 3C-3F ) + + describe 'DEC/xterm specific', -> + describe '$~', -> + beforeEach -> + data += '$~' + + it 'is swallowed', isSwallowed + + describe '?', -> + beforeEach -> + data += '?' + + describe '1h', -> + beforeEach -> + data += '1h' + + it 'is swallowed', isSwallowed + + describe '25h', -> + beforeEach -> + data += '25h' + + it 'shows cursor', -> + spyOn screenBuffer, 'showCursor' + interpreter.feed(data) + expect(screenBuffer.showCursor).toHaveBeenCalledWith(true) + + describe '25l', -> + beforeEach -> + data += '25l' + + it 'hides cursor', -> + spyOn screenBuffer, 'showCursor' + interpreter.feed(data) + expect(screenBuffer.showCursor).toHaveBeenCalledWith(false) + + describe '47h', -> + beforeEach -> + data += '47h' + + it 'switches to alternate buffer', -> + spyOn screenBuffer, 'switchToAlternateBuffer' + interpreter.feed(data) + expect(screenBuffer.switchToAlternateBuffer).toHaveBeenCalled() + + describe '47l', -> + beforeEach -> + data += '47l' + + it 'switches to normal buffer', -> + spyOn screenBuffer, 'switchToNormalBuffer' + interpreter.feed(data) + expect(screenBuffer.switchToNormalBuffer).toHaveBeenCalled() + + describe '1049h', -> + beforeEach -> + data += '1049h' + + it 'saves cursor position, switches to alternate buffer and clear screen', -> + spyOn screenBuffer, 'saveCursor' + spyOn screenBuffer, 'switchToAlternateBuffer' + spyOn screenBuffer, 'clear' + interpreter.feed(data) + expect(screenBuffer.saveCursor).toHaveBeenCalled() + expect(screenBuffer.switchToAlternateBuffer).toHaveBeenCalled() + expect(screenBuffer.clear).toHaveBeenCalled() + + describe '1049l', -> + beforeEach -> + data += '1049l' + + it 'clears screen, switches to normal buffer and restores cursor position', -> + spyOn screenBuffer, 'clear' + spyOn screenBuffer, 'switchToNormalBuffer' + spyOn screenBuffer, 'restoreCursor' + interpreter.feed(data) + expect(screenBuffer.clear).toHaveBeenCalled() + expect(screenBuffer.switchToNormalBuffer).toHaveBeenCalled() + expect(screenBuffer.restoreCursor).toHaveBeenCalled() + + describe '#handleSGR', -> numbers = undefined