Moah refactoringz, moah specs

This commit is contained in:
Marcin Kulik 2012-02-07 10:42:57 +01:00
parent ed6d1146da
commit a591423e3b
11 changed files with 1013 additions and 375 deletions

View File

@ -1,14 +1,88 @@
class AsciiIo.AnsiInterpreter
constructor: (terminal) ->
@terminal = terminal
constructor: (screenBuffer) ->
@sb = screenBuffer
@fg = @bg = undefined
@bright = false
@underline = false
@compilePatterns()
noop: ->
# handleC0ControlSet: (data) ->
# console.log 'handling C0'
handlePrintableCharacters: (text) ->
# handleC1ControlSet: (data) ->
# handleControlSequence: (data) ->
_C0_PATTERNS:
# C0 set of 7-bit control characters
"[\x00-\x1f]":
# bell
"\x07": (data) -> @sb.bell()
# backspace
"\x08": (data) -> @sb.backspace()
# Move the cursor to the next tab stop
"\x09": (data) ->
"\x0a": (data) -> @sb.cursorDown 1
"\x0d": (data) -> @sb.cr()
"\x0e": (data) ->
"\x0f": (data) ->
# Reserved (?)
"\x82": (data) ->
# Cancel Character, ignore previous character
"\x94": (data) ->
# Escape sequence
"\x1b": _.extend({}, @_C0_PATTERNS, {
# Control sequence
"\x1b\\[": @_CS_PATTERNS
})
_CS_PATTERNS:
"sth": 1
_PATTERNS: _.extend({}, @_C0_PATTERNS, {
# Printable characters
"([\x20-\x7e])+": @handlePrintableCharacters
# "Delete", always and everywhere ignored
"[\x7f\xff]": @noop
# C1 control set
"[\x80-\x9f]":
# Control sequence
"\x9b": @_CS_PATTERNS
# G1 Displayable, 94 additional displayable characters
"[\xa1-\xfe]": @handlePrintableCharacters
# Always and everywhere a blank space
"\xa0": -> @handlePrintableCharacters(' ')
})
PATTERNS:
"\x07": (data) -> # bell
"\x08": (data) -> @terminal.bs()
"\x07": (data) -> @sb.bell()
"\x08": (data) -> @sb.backspace()
"\x09": (data) -> # Moves the cursor to the next tab stop
"\x0a": (data) -> @terminal.cursorDown 1
"\x0d": (data) -> @terminal.cr()
"\x0a": (data) -> @sb.cursorDown 1
"\x0d": (data) -> @sb.cr()
"\x0e": (data) ->
"\x0f": (data) ->
"\x82": (data) -> # Reserved (?)
@ -16,18 +90,19 @@ class AsciiIo.AnsiInterpreter
# 20 - 7e
"([\x20-\x7e]|\xe2..|[\xc2\xc4\xc5].)+": (data, match) ->
@terminal.print match[0]
@sb.print match[0]
"\x1b\\(B": (data) -> # SCS (Set G0 Character SET)
"\x1b\\[(?:[0-9]+)?(?:;[0-9]+)*([\x40-\x7e])": (data, match) ->
"\x1b\\[([0-9;]*)([\x40-\x7e])": (data, match) ->
if match[1].length == 0
@params = []
re = /(\d+)/g
m = undefined
@params.push parseInt(m[1]) while m = re.exec(match[0])
else
@params = _(match[1].split(';')).map (n) -> if n is '' then undefined else parseInt(n)
@n = @params[0]
@m = @params[1]
@handleCSI match[1]
@handleCSI match[2]
# private standards
"\x1b\\[\\?([\x30-\x3f]+)([hlsr])": (data, match) ->
@ -51,14 +126,14 @@ class AsciiIo.AnsiInterpreter
# steady cursor
else if mode is "25"
if action is "h"
@terminal.showCursor true
@sb.showCursor true
else if action is "l"
@terminal.showCursor false
@sb.showCursor false
else if mode is "47"
if action is "h"
@terminal.switchToAlternateBuffer()
@sb.switchToAlternateBuffer()
else if action is "l"
@terminal.switchToNormalBuffer()
@sb.switchToNormalBuffer()
else if mode is "1000"
# Enables/disables normal mouse tracking
else if mode is "1001"
@ -68,14 +143,14 @@ class AsciiIo.AnsiInterpreter
else if mode is "1049"
if action is "h"
# Save cursor position, switch to alternate screen buffer, and clear screen.
@terminal.saveCursor()
@terminal.switchToAlternateBuffer()
@terminal.clearScreen()
@sb.saveCursor()
@sb.switchToAlternateBuffer()
@sb.clearScreen()
else if action is "l"
# Clear screen, switch to normal screen buffer, and restore cursor position.
@terminal.clearScreen()
@terminal.switchToNormalBuffer()
@terminal.restoreCursor()
@sb.clearScreen()
@sb.switchToNormalBuffer()
@sb.restoreCursor()
else
throw "unknown mode: " + mode + action
@ -90,50 +165,92 @@ class AsciiIo.AnsiInterpreter
"\x1bP([^\\\\])*?\\\\": (data) -> # DCS, Device Control String
"\x1bM": ->
@terminal.ri @n or 1
@sb.ri @n or 1
"\x1b\x37": (data) -> # save cursor pos and char attrs
@terminal.saveCursor()
@sb.saveCursor()
"\x1b\x38": (data) -> # restore cursor pos and char attrs
@terminal.restoreCursor()
@sb.restoreCursor()
handleCSI: (term) ->
switch term
when "@"
@terminal.reserveCharacters @n
@sb.reserveCharacters @n
when "A"
@terminal.cursorUp @n or 1
@sb.cursorUp @n or 1
when "B"
@terminal.cursorDown @n or 1
@sb.cursorDown @n or 1
when "C"
@terminal.cursorForward @n or 1
@sb.cursorForward @n or 1
when "D"
@terminal.cursorBack @n or 1
@sb.cursorBack @n or 1
when "G"
@terminal.setCursorColumn @n
@sb.setCursorColumn @n
when "H"
@terminal.setCursorPos @n or 1, @m or 1
@sb.setCursorPos @n or 1, @m or 1
when "J"
@terminal.eraseData @n or 0
@sb.eraseData @n or 0
when "K"
@terminal.eraseInLine @n or 0
@sb.eraseInLine @n or 0
when "L"
@terminal.insertLines @cursorY, @n or 1
@sb.insertLines @n or 1
when "M"
@terminal.deleteLines @cursorY, @n or 1
@sb.deleteLines @n or 1
when "d" # VPA - Vertical Position Absolute
@terminal.setCursorLine(@n)
@sb.setCursorLine(@n)
when "l" # l, Reset mode
console.log "(TODO) reset: " + @n
when "m"
@terminal.setSGR @params
@handleSGR @params
when "P" # DCH - Delete Character, from current position to end of field
@terminal.deleteCharacter @n or 1
@sb.deleteCharacter @n or 1
when "r" # Set top and bottom margins (scroll region on VT100)
else
throw "no handler for CSI term: " + term
handleSGR: (numbers) ->
numbers = [0] if numbers.length is 0
i = 0
while i < numbers.length
n = numbers[i]
if n is 0
@fg = @bg = undefined
@bright = false
@underline = false
else if n is 1
@bright = true
else if n is 4
@underline = true
else if n is 24
@underline = false
else if n >= 30 and n <= 37
@fg = n - 30
else if n is 38
@fg = numbers[i + 2]
i += 2
else if n is 39
@fg = undefined
else if n >= 40 and n <= 47
@bg = n - 40
else if n is 48
@bg = numbers[i + 2]
i += 2
else if n is 49
@bg = undefined
i++
props = {}
props.fg = @fg if @fg
props.bg = @bg if @bg
props.bright = true if @bright
props.underline = true if @underline
@sb.setBrush AsciiIo.Brush.create(props)
compilePatterns: ->
@COMPILED_PATTERNS = ([new RegExp("^" + re), f] for re, f of @PATTERNS)
@ -152,5 +269,4 @@ class AsciiIo.AnsiInterpreter
break unless match
@terminal.render()
data

View File

@ -1,2 +1,5 @@
class AsciiIo.HudView extends Backbone.View
tagName: 'div'
className: 'hud'
initialize: (options) ->

View File

@ -3,57 +3,74 @@ class AsciiIo.Movie
SPEED: 1.0
constructor: (@data, @timing) ->
@frameNo = 0
@dataIndex = 0
@currentTime = 0
@processedFramesTime = 0
_.extend(this, Backbone.Events)
play: ->
@nextFrame()
togglePlay: ->
pause: ->
# TODO
togglePlay: ->
# TODO
seek: (percent) ->
# TODO
nextFrame: () ->
return if @currentData.length > 100
frame = @timing[@frameNo]
unless frame
@terminal.stopCursorBlink()
console.log "finished in #{((new Date()).getTime() - @startTime) / 1000} seconds"
return
@frameNo += 1
# return if @currentData.length > 100
if frame = @timing[@frameNo++] # @frameNo += 1
[delay, count] = frame
if delay > @MIN_DELAY
realDelay = delay * 1000 * (1.0 / @SPEED)
setTimeout(
=>
@terminal.restartCursorBlink()
@processFrame(count)
@nextFrame()
realDelay
)
else
@processFrame(count)
@nextFrame()
processFrame: (count) ->
@currentData += @data.slice(@dataIndex, @dataIndex + count)
frameData = @data.slice(@dataIndex, @dataIndex + count)
@dataIndex += count
@currentData = @interpreter.feed(@currentData)
if delay <= @MIN_DELAY
@triggerAndSchedule(frameData)
else
realDelay = delay * 1000 * (1.0 / @SPEED)
setTimeout(
=>
@trigger('movie-awake') # @terminal.restartCursorBlink()
@triggerAndSchedule(frameData)
realDelay
)
if @currentData.length > 0
@logStatus(count)
true
else
@trigger('movie-finished')
# @terminal.stopCursorBlink()
# console.log "finished in #{((new Date()).getTime() - @startTime) / 1000} seconds"
logStatus: (count) ->
console.log 'rest: ' + Utf8.decode(@currentData)
false
if @currentData.length > 100
head = @currentData.slice(0, 100)
hex = ("0x#{c.charCodeAt(0).toString(16)}" for c in head)
console.log "failed matching: '" + Utf8.decode(head) + "' (" + hex.join() + ") [pos: " + (@dataIndex - count) + "]"
return
triggerAndSchedule: (data) ->
@trigger('movie-frame', data)
@nextFrame()
# processFrame: (count) ->
# # return
# # @currentData += @data.slice(@dataIndex, @dataIndex + count)
# data = @data.slice(@dataIndex, @dataIndex + count)
# # console.log data
# @dataIndex += count
# @trigger('movie-frame', data)
# @currentData = @interpreter.feed(@currentData)
# if @currentData.length > 0
# @logStatus(count)
# logStatus: (count) ->
# console.log 'rest: ' + Utf8.decode(@currentData)
# if @currentData.length > 100
# head = @currentData.slice(0, 100)
# hex = ("0x#{c.charCodeAt(0).toString(16)}" for c in head)
# console.log "failed matching: '" + Utf8.decode(head) + "' (" + hex.join() + ") [pos: " + (@dataIndex - count) + "]"
# return

View File

@ -1,19 +1,41 @@
class AsciiIo.PlayerView extends Backbone.View
initialize: (options) ->
@element = @$el
terminalElement = $('<pre class="terminal">')
hudElement = $('<div class="hud">')
@element.append(terminalElement)
@element.append(hudElement)
# @interpreter ?
@terminal = new AsciiIo.TerminalView({
el: terminalElement[0], cols: options.cols, lines: options.lines
})
@hud = new AsciiIo.HudView({ el: hudElement[0] })
@movie = new AsciiIo.Movie(options.data, options.timing)
@screenBuffer = new AsciiIo.ScreenBuffer(options.cols, options.lines)
@interpreter = new AsciiIo.AnsiInterpreter(@screenBuffer)
@terminal.on 'terminal-click', =>
@createChildViews()
@bindEvents()
createChildViews: ->
@terminalView = new AsciiIo.TerminalView(
cols: this.options.cols
lines: this.options.lines
)
@$el.append(@terminalView.$el)
@hudView = new AsciiIo.HudView()
@$el.append(@hudView.$el)
bindEvents: ->
@terminalView.on 'terminal-click', =>
@movie.togglePlay()
@hudView.on 'hud-play-click', =>
@movie.togglePlay()
@hudView.on 'hud-seek-click', (percent) =>
@movie.seek(percent)
@movie.on 'movie-frame', (frame) =>
@interpreter.feed(frame)
changes = @screenBuffer.changes()
@terminalView.render(changes)
@screenBuffer.clearChanges()
@movie.on 'movie-finished', =>
@terminalView.stopCursorBlink()
play: ->
@movie.play()

View File

@ -0,0 +1,237 @@
class AsciiIo.ScreenBuffer
constructor: (@cols, @lines) ->
@cursorX = 0
@cursorY = 0
@normalBuffer = []
@alternateBuffer = []
@lineData = @normalBuffer
@dirtyLines = {}
setBrush: (brush) ->
@brush = brush
getLine: (n) ->
n = (if typeof n isnt "undefined" then n else @cursorY)
throw "cant getLine " + n if n >= @lines
line = @lineData[n]
if typeof line is "undefined"
line = @lineData[n] = []
@fill n, 0, @cols, " "
line
switchToNormalBuffer: ->
@lineData = @normalBuffer
@updateScreen()
switchToAlternateBuffer: ->
@lineData = @alternateBuffer
@updateScreen()
updateLine: (n) ->
n = (if typeof n isnt "undefined" then n else @cursorY)
@dirtyLines[n] = n
updateScreen: ->
@dirtyLines[n] = n for n in [0...@lines]
setCursorLine: (line) ->
oldLine = @cursorY
@cursorY = line - 1
@updateLine oldLine
@updateLine()
setCursorColumn: (col) ->
@cursorX = col - 1
@updateLine()
setCursorPos: (line, col) ->
@setCursorLine(line)
@setCursorColumn(col)
saveCursor: ->
@savedCol = @cursorX
@savedLine = @cursorY
restoreCursor: ->
oldLine = @cursorY
@cursorY = @savedLine
@cursorX = @savedCol
@updateLine oldLine
@updateLine()
cursorLeft: ->
if @cursorX > 0
@cursorX -= 1
@updateLine()
cursorRight: ->
if @cursorX < @cols
@cursorX += 1
@updateLine()
cursorUp: (n) ->
for i in [0...n]
if @cursorY > 0
@cursorY -= 1
@updateLine @cursorY
@updateLine @cursorY + 1
cursorDown: (n) ->
for i in [0...n]
if @cursorY + 1 < @lines
@cursorY += 1
@updateLine @cursorY - 1
@updateLine @cursorY
else
@lineData.splice 0, 1
@updateScreen()
cursorForward: (n) ->
@cursorRight() for i in [0...n]
cursorBack: (n) ->
@cursorLeft() for i in [0...n]
cr: ->
@cursorX = 0
@updateLine()
backspace: ->
if @cursorX > 0
@cursorLeft()
@updateLine()
bell: ->
# nothing to do
print: (text) ->
text = Utf8.decode(text)
i = 0
while i < text.length
if @cursorX >= @cols
@cursorY += 1
@cursorX = 0
@fill @cursorY, @cursorX, 1, text[i]
@cursorX += 1
i++
@updateLine()
eraseData: (n) ->
if n is 0
@eraseInLine 0
l = @cursorY + 1
while l < @lines
@clearLineData l
@updateLine l
l++
else if n is 1
l = 0
while l < @cursorY
@clearLineData l
@updateLine l
l++
@eraseInLine n
else if n is 2
l = 0
while l < @lines
@clearLineData l
@updateLine l
l++
eraseInLine: (n) ->
if n is 0
@fill @cursorY, @cursorX, @cols - @cursorX, " "
else if n is 1
@fill @cursorY, 0, @cursorX, " "
else if n is 2
@fill @cursorY, 0, @cols, " "
@updateLine()
clearLineData: (n) ->
@fill n, 0, @cols, " "
deleteCharacter: (n) ->
@getLine().splice(@cursorX, n)
@updateLine()
reserveCharacters: (n) ->
line = @getLine()
@lineData[@cursorY] = line.slice(0, @cursorX).concat(" ".times(n).split(""), line.slice(@cursorX, @cols - n))
@updateLine()
ri: (n) ->
i = 0
while i < n
if @cursorY is 0
@insertLines n, 0
else
@cursorUp()
i++
insertLines: (n, l = @cursorY) ->
i = 0
while i < n
@lineData.splice l, 0, []
@clearLineData l
i++
# trim lineData to max size
@lineData.length = @lines
@updateScreen()
deleteLines: (n, l = @cursorY) ->
@lineData.splice l, n
# expand lineData to max size
@lineData.length = @lines
@updateScreen()
fill: (line, col, n, char) ->
prefix = ""
postfix = ""
if @fg isnt undefined or @bg isnt undefined or @bright or @underline
prefix = "<span class=\""
brightOffset = (if @bright then 8 else 0)
if @fg isnt undefined
prefix += " fg" + (@fg + brightOffset)
else if @bright
prefix += " bright"
if @underline
prefix += " underline"
prefix += " bg" + @bg if @bg isnt undefined
prefix += "\">"
postfix = "</span>"
char = prefix + char + postfix
lineArr = @getLine(line)
i = 0
while i < n
lineArr[col + i] = char
i++
changes: ->
{}
clearChanges: ->

View File

@ -1,18 +1,10 @@
class AsciiIo.TerminalView extends Backbone.View
tagName: 'pre'
className: 'terminal'
initialize: (options) ->
# @element = $(element) # $(".player .term")
@element = @$el
@cols = options.cols
@lines = options.lines
@cursorX = 0
@cursorY = 0
@normalBuffer = []
@alternateBuffer = []
@lineData = @normalBuffer
@dirtyLines = {}
@fg = @bg = undefined
@underline = false
@createChildElements()
@showCursor true
@ -28,35 +20,20 @@ class AsciiIo.TerminalView extends Backbone.View
while i < @lines
row = $("<span class=\"line\">")
@element.append row
@element.append "\n"
@$el.append row
@$el.append "\n"
i++
getLine: (n) ->
n = (if typeof n isnt "undefined" then n else @cursorY)
throw "cant getLine " + n if n >= @lines
line = @lineData[n]
if typeof line is "undefined"
line = @lineData[n] = []
@fill n, 0, @cols, " "
line
clearScreen: ->
# this.lineData.length = 0;
@cursorY = @cursorX = 0
@element.find(".line").empty()
@$el.find(".line").empty()
switchToNormalBuffer: ->
@lineData = @normalBuffer
@updateScreen()
render: ->
for _, n of @dirtyLines
@renderLine n
switchToAlternateBuffer: ->
@lineData = @alternateBuffer
@updateScreen()
# @dirtyLines = {}
renderLine: (n) ->
html = @getLine(n)
@ -64,251 +41,16 @@ class AsciiIo.TerminalView extends Backbone.View
if n is @cursorY
html = html.slice(0, @cursorX).concat([ "<span class=\"cursor\">" + (html[@cursorX] or "") + "</span>" ], html.slice(@cursorX + 1) or [])
@element.find(".line:eq(" + n + ")").html html.join("")
render: ->
for _, n of @dirtyLines
@renderLine n
@dirtyLines = {}
updateLine: (n) ->
n = (if typeof n isnt "undefined" then n else @cursorY)
@dirtyLines[n] = n
updateScreen: ->
@dirtyLines[n] = n for n in [0...@lines]
@$el.find(".line:eq(" + n + ")").html html.join("")
showCursor: (show) ->
if show
@element.addClass "cursor-on"
@$el.addClass "cursor-on"
else
@element.removeClass "cursor-on"
setSGR: (codes) ->
codes = [0] if codes.length is 0
i = 0
while i < codes.length
n = codes[i]
if n is 0
@fg = @bg = undefined
@bright = false
@underline = false
else if n is 1
@bright = true
else if n is 4
@underline = true
else if n is 24
@underline = false
else if n >= 30 and n <= 37
@fg = n - 30
else if n is 38
@fg = codes[i + 2]
i += 2
else if n is 39
@fg = undefined
else if n >= 40 and n <= 47
@bg = n - 40
else if n is 48
@bg = codes[i + 2]
i += 2
else if n is 49
@bg = undefined
i++
setCursorLine: (line) ->
oldLine = @cursorY
@cursorY = line - 1
@updateLine oldLine
@updateLine()
setCursorColumn: (col) ->
@cursorX = col - 1
@updateLine()
setCursorPos: (line, col) ->
@setCursorLine(line)
@setCursorColumn(col)
saveCursor: ->
@savedCol = @cursorX
@savedLine = @cursorY
restoreCursor: ->
oldLine = @cursorY
@cursorY = @savedLine
@cursorX = @savedCol
@updateLine oldLine
@updateLine()
cursorLeft: ->
if @cursorX > 0
@cursorX -= 1
@updateLine()
cursorRight: ->
if @cursorX < @cols
@cursorX += 1
@updateLine()
cursorUp: (n) ->
for i in [0...n]
if @cursorY > 0
@cursorY -= 1
@updateLine @cursorY
@updateLine @cursorY + 1
cursorDown: (n) ->
for i in [0...n]
if @cursorY + 1 < @lines
@cursorY += 1
@updateLine @cursorY - 1
@updateLine @cursorY
else
@lineData.splice 0, 1
@updateScreen()
cursorForward: (n) ->
@cursorRight() for i in [0...n]
cursorBack: (n) ->
@cursorLeft() for i in [0...n]
cr: ->
@cursorX = 0
@updateLine()
bs: ->
if @cursorX > 0
@cursorLeft()
@updateLine()
print: (text) ->
text = Utf8.decode(text)
i = 0
while i < text.length
if @cursorX >= @cols
@cursorY += 1
@cursorX = 0
@fill @cursorY, @cursorX, 1, text[i]
@cursorX += 1
i++
@updateLine()
eraseData: (n) ->
if n is 0
@eraseInLine 0
l = @cursorY + 1
while l < @lines
@clearLineData l
@updateLine l
l++
else if n is 1
l = 0
while l < @cursorY
@clearLineData l
@updateLine l
l++
@eraseInLine n
else if n is 2
l = 0
while l < @lines
@clearLineData l
@updateLine l
l++
eraseInLine: (n) ->
if n is 0
@fill @cursorY, @cursorX, @cols - @cursorX, " "
else if n is 1
@fill @cursorY, 0, @cursorX, " "
else if n is 2
@fill @cursorY, 0, @cols, " "
@updateLine()
clearLineData: (n) ->
@fill n, 0, @cols, " "
deleteCharacter: (n) ->
@getLine().splice(@cursorX, n)
@updateLine()
reserveCharacters: (n) ->
line = @getLine()
@lineData[@cursorY] = line.slice(0, @cursorX).concat(" ".times(n).split(""), line.slice(@cursorX, @cols - n))
@updateLine()
ri: (n) ->
i = 0
while i < n
if @cursorY is 0
@insertLines 0, n
else
@cursorUp()
i++
insertLines: (l, n) ->
i = 0
while i < n
@lineData.splice l, 0, []
@clearLineData l
i++
# trim lineData to max size
@lineData.length = @lines
@updateScreen()
deleteLines: (l, n) ->
@lineData.splice l, n
# expand lineData to max size
@lineData.length = @lines
@updateScreen()
fill: (line, col, n, char) ->
prefix = ""
postfix = ""
if @fg isnt undefined or @bg isnt undefined or @bright or @underline
prefix = "<span class=\""
brightOffset = (if @bright then 8 else 0)
if @fg isnt undefined
prefix += " fg" + (@fg + brightOffset)
else if @bright
prefix += " bright"
if @underline
prefix += " underline"
prefix += " bg" + @bg if @bg isnt undefined
prefix += "\">"
postfix = "</span>"
char = prefix + char + postfix
lineArr = @getLine(line)
i = 0
while i < n
lineArr[col + i] = char
i++
@$el.removeClass "cursor-on"
blinkCursor: ->
cursor = @element.find(".cursor")
cursor = @$el.find(".cursor")
if cursor.hasClass("inverted")
cursor.removeClass "inverted"
else

View File

@ -19,7 +19,17 @@ module AsciicastsHelper
var time = #{j var_time};
var cols = #{asciicast.terminal_columns};
var lines = #{asciicast.terminal_lines};
$(function() { new AsciiIo.Player(cols, lines, data, time); });
$(function() {
window.player = new AsciiIo.PlayerView({
el: $('.player'),
cols: cols,
lines: lines,
data: data,
timing: time
});
window.player.play();
});
</script>
EOS
end

View File

@ -1,3 +1,389 @@
describe AsciiIo.AnsiInterpreter, ->
it 'rocks', ->
expect(1).toBeTruthy()
interpreter = screenBuffer = data = undefined
cols = 80
lines = 24
beforeEach ->
screenBuffer = new AsciiIo.ScreenBuffer(cols, lines)
interpreter = new AsciiIo.AnsiInterpreter(screenBuffer)
data = ''
describe '#feed', ->
describe 'C0 set control character', ->
# 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'
spyOn screenBuffer, 'bell'
interpreter.feed(data)
expect(screenBuffer.bell).toHaveBeenCalled()
describe 'x08', ->
it 'calls backspace', ->
data += '\x08'
spyOn screenBuffer, 'backspace'
interpreter.feed(data)
expect(screenBuffer.backspace).toHaveBeenCalled()
describe 'x0a', ->
it 'calls cursorDown(1)', ->
data += '\x0a'
spyOn screenBuffer, 'cursorDown'
interpreter.feed(data)
expect(screenBuffer.cursorDown).toHaveBeenCalledWith(1)
describe 'x0d', ->
it 'calls cr', ->
data += '\x0d'
spyOn screenBuffer, 'cr'
interpreter.feed(data)
expect(screenBuffer.cr).toHaveBeenCalled()
describe 'printable character', ->
describe 'from ASCII range (0x20-0x7e)', ->
it 'calls print', ->
data += '\x20foobar\x7e'
spyOn screenBuffer, 'print'
interpreter.feed(data)
expect(screenBuffer.print).toHaveBeenCalledWith(data)
describe 'from Unicode', ->
it 'calls print', ->
data += '\xe2ab\xe2ZZ'
spyOn screenBuffer, 'print'
interpreter.feed(data)
expect(screenBuffer.print).toHaveBeenCalledWith(data)
describe 'from Unicode (really ???)', ->
it 'calls print', ->
data += '\xc2a\xc4b\xc5c'
spyOn screenBuffer, 'print'
interpreter.feed(data)
expect(screenBuffer.print).toHaveBeenCalledWith(data)
describe 'escape sequence', ->
# 2 or 3 character string starting with ESCape. (Four or more character
# strings are allowed but not defined.)
beforeEach ->
data += '\x1b'
describe 'with ESC + control character inside', ->
# Interpret the character, then resume processing the sequence.
# it 'aaa', ->
# data += '[1\x1b\x0dm'
# spyOn screenBuffer, 'cr'
# spyOn screenBuffer, 'setSGR'
# interpreter.feed(data)
# expect(screenBuffer.cr).toHaveBeenCalled()
# expect(screenBuffer.setSGR).toHaveBeenCalledWith([1])
describe '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
# appear within the Control Sequence. The terminating character may be
# preceded by an intermediate character (such as space).
beforeEach ->
data += '['
describe 'buffering', ->
it 'allows parsing in chunks', ->
spyOn interpreter, 'handleSGR'
interpreter.feed(data)
interpreter.feed('m')
expect(interpreter.handleSGR).toHaveBeenCalled()
describe '@', ->
it 'calls reserveCharacters', ->
data += '3@'
spyOn screenBuffer, 'reserveCharacters'
interpreter.feed(data)
expect(screenBuffer.reserveCharacters).toHaveBeenCalledWith(3)
describe 'A', ->
it 'calls cursorUp(1) if no number given', ->
data += 'A'
spyOn screenBuffer, 'cursorUp'
interpreter.feed(data)
expect(screenBuffer.cursorUp).toHaveBeenCalledWith(1)
it 'calls cursorUp(n) if number given', ->
data += '3A'
spyOn screenBuffer, 'cursorUp'
interpreter.feed(data)
expect(screenBuffer.cursorUp).toHaveBeenCalledWith(3)
describe 'B', ->
it 'calls cursorDown(1) if no number given', ->
data += 'B'
spyOn screenBuffer, 'cursorDown'
interpreter.feed(data)
expect(screenBuffer.cursorDown).toHaveBeenCalledWith(1)
it 'calls cursorDown(n) if number given', ->
data += '3B'
spyOn screenBuffer, 'cursorDown'
interpreter.feed(data)
expect(screenBuffer.cursorDown).toHaveBeenCalledWith(3)
describe 'C', ->
it 'calls cursorForward(1) if no number given', ->
data += 'C'
spyOn screenBuffer, 'cursorForward'
interpreter.feed(data)
expect(screenBuffer.cursorForward).toHaveBeenCalledWith(1)
it 'calls cursorForward(n) if number given', ->
data += '3C'
spyOn screenBuffer, 'cursorForward'
interpreter.feed(data)
expect(screenBuffer.cursorForward).toHaveBeenCalledWith(3)
describe 'D', ->
it 'calls cursorBack(1) if no number given', ->
data += 'D'
spyOn screenBuffer, 'cursorBack'
interpreter.feed(data)
expect(screenBuffer.cursorBack).toHaveBeenCalledWith(1)
it 'calls cursorBack(n) if number given', ->
data += '3D'
spyOn screenBuffer, 'cursorBack'
interpreter.feed(data)
expect(screenBuffer.cursorBack).toHaveBeenCalledWith(3)
describe 'G', ->
it 'calls setCursorColumn(n)', ->
data += '3G'
spyOn screenBuffer, 'setCursorColumn'
interpreter.feed(data)
expect(screenBuffer.setCursorColumn).toHaveBeenCalledWith(3)
describe 'H', ->
it 'calls setCursorPos(n, m) when n and m given', ->
data += '3;4H'
spyOn screenBuffer, 'setCursorPos'
interpreter.feed(data)
expect(screenBuffer.setCursorPos).toHaveBeenCalledWith(3, 4)
it 'calls setCursorPos(1, m) when no n given', ->
data += ';3H'
spyOn screenBuffer, 'setCursorPos'
interpreter.feed(data)
expect(screenBuffer.setCursorPos).toHaveBeenCalledWith(1, 3)
it 'calls setCursorPos(n, 1) when no m given', ->
data += '3;H'
spyOn screenBuffer, 'setCursorPos'
interpreter.feed(data)
expect(screenBuffer.setCursorPos).toHaveBeenCalledWith(3, 1)
it 'calls setCursorPos(n, 1) when no m given (no semicolon)', ->
data += '3H'
spyOn screenBuffer, 'setCursorPos'
interpreter.feed(data)
expect(screenBuffer.setCursorPos).toHaveBeenCalledWith(3, 1)
it 'calls setCursorPos(1, 1) when no n nor m given', ->
data += 'H'
spyOn screenBuffer, 'setCursorPos'
interpreter.feed(data)
expect(screenBuffer.setCursorPos).toHaveBeenCalledWith(1, 1)
describe 'J', ->
it 'calls eraseData(0) when no n given', ->
data += 'J'
spyOn screenBuffer, 'eraseData'
interpreter.feed(data)
expect(screenBuffer.eraseData).toHaveBeenCalledWith(0)
it 'calls eraseData(n) when n given', ->
data += '3J'
spyOn screenBuffer, 'eraseData'
interpreter.feed(data)
expect(screenBuffer.eraseData).toHaveBeenCalledWith(3)
describe 'K', ->
it 'calls eraseInLine(0) when no n given', ->
data += 'K'
spyOn screenBuffer, 'eraseInLine'
interpreter.feed(data)
expect(screenBuffer.eraseInLine).toHaveBeenCalledWith(0)
it 'calls eraseInLine(n) when n given', ->
data += '3K'
spyOn screenBuffer, 'eraseInLine'
interpreter.feed(data)
expect(screenBuffer.eraseInLine).toHaveBeenCalledWith(3)
describe 'L', ->
it 'calls insertLines(1) when no n given', ->
data += 'L'
spyOn screenBuffer, 'insertLines'
interpreter.feed(data)
expect(screenBuffer.insertLines).toHaveBeenCalledWith(1)
it 'calls insertLines(n) when n given', ->
data += '3L'
spyOn screenBuffer, 'insertLines'
interpreter.feed(data)
expect(screenBuffer.insertLines).toHaveBeenCalledWith(3)
describe 'M', ->
it 'calls deleteLines(1) when no n given', ->
data += 'M'
spyOn screenBuffer, 'deleteLines'
interpreter.feed(data)
expect(screenBuffer.deleteLines).toHaveBeenCalledWith(1)
it 'calls deleteLines(n) when n given', ->
data += '3M'
spyOn screenBuffer, 'deleteLines'
interpreter.feed(data)
expect(screenBuffer.deleteLines).toHaveBeenCalledWith(3)
describe 'd', ->
it 'calls setCursorLine(n)', ->
data += '3d'
spyOn screenBuffer, 'setCursorLine'
interpreter.feed(data)
expect(screenBuffer.setCursorLine).toHaveBeenCalledWith(3)
describe 'm', ->
it 'calls handleSGR([n, m, ...]) when n and m given', ->
data += '1;4;33m'
spyOn interpreter, 'handleSGR'
interpreter.feed(data)
expect(interpreter.handleSGR).toHaveBeenCalledWith([1, 4, 33])
it 'calls handleSGR([]) when no n nor m given', ->
data += 'm'
spyOn interpreter, 'handleSGR'
interpreter.feed(data)
expect(interpreter.handleSGR).toHaveBeenCalledWith([])
describe 'P', ->
it 'calls deleteCharacter(1) when no n given', ->
data += 'P'
spyOn screenBuffer, 'deleteCharacter'
interpreter.feed(data)
expect(screenBuffer.deleteCharacter).toHaveBeenCalledWith(1)
it 'calls deleteCharacter(n) when n given', ->
data += '3P'
spyOn screenBuffer, 'deleteCharacter'
interpreter.feed(data)
expect(screenBuffer.deleteCharacter).toHaveBeenCalledWith(3)
describe '#handleSGR', ->
numbers = undefined
it 'resets brush for 0', ->
spyOn screenBuffer, 'setBrush'
numbers = [31]
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ fg: 1 })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
numbers = [0]
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({})
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
it 'sets bright attr for 1', ->
numbers = [1]
spyOn screenBuffer, 'setBrush'
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ bright: true })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
it 'sets underline attr for 4', ->
numbers = [4]
spyOn screenBuffer, 'setBrush'
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ underline: true })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
it 'unsets underline attr for 24', ->
spyOn screenBuffer, 'setBrush'
numbers = [31, 4]
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ fg: 1, underline: true })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
numbers = [24]
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ fg: 1 })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
it 'sets foreground for 30-37', ->
numbers = [32]
spyOn screenBuffer, 'setBrush'
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ fg: 2 })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
it 'sets foreground for 38;5;x', ->
numbers = [38, 5, 100]
spyOn screenBuffer, 'setBrush'
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ fg: 100 })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
it 'unsets foreground for 39', ->
spyOn screenBuffer, 'setBrush'
numbers = [32]
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ fg: 2 })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
numbers = [39]
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({})
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
it 'sets background for 40-47', ->
numbers = [42]
spyOn screenBuffer, 'setBrush'
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ bg: 2 })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
it 'sets background for 48;5;x', ->
numbers = [48, 5, 200]
spyOn screenBuffer, 'setBrush'
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ bg: 200 })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
it 'unsets background for 49', ->
spyOn screenBuffer, 'setBrush'
numbers = [42]
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({ bg: 2 })
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)
numbers = [49]
interpreter.handleSGR(numbers)
expectedBrush = AsciiIo.Brush.create({})
expect(screenBuffer.setBrush).toHaveBeenCalledWith(expectedBrush)

View File

@ -1,21 +1,76 @@
describe AsciiIo.Movie, ->
movie = data = timing = null
beforeEach ->
movie = null
data = ''
timing = []
describe '#play', ->
it 'calls nextFrame', ->
movie = new AsciiIo.Movie('', [])
movie = new AsciiIo.Movie(data, timing)
spyOn movie, 'nextFrame'
movie.play()
expect(movie.nextFrame).toHaveBeenCalled()
describe '#pause', ->
describe '#toggle', ->
describe '#togglePlay', ->
describe '#seek', ->
describe '#nextFrame', ->
describe 'when playing', ->
it 'triggers movie-frame event', ->
beforeEach ->
data = 'X'
it 'triggers movie-frame event immediately if delay is < MIN_DELAY', ->
timing = [[0.8 * AsciiIo.Movie::MIN_DELAY, 1]]
movie = new AsciiIo.Movie(data, timing)
obj = { callback: -> true }
movie.on('movie-frame', (arg) -> obj.callback(arg))
spyOn(obj, 'callback')
movie.nextFrame()
expect(obj.callback).toHaveBeenCalledWith('X')
it 'triggers movie-frame event after <delay> if delay is > MIN_DELAY', ->
timing = [[20 * AsciiIo.Movie::MIN_DELAY, 1]]
movie = new AsciiIo.Movie(data, timing)
obj = { callback: -> true }
movie.on('movie-frame', (arg) -> obj.callback(arg))
spyOn(obj, 'callback')
nextFrameCallTime = (new Date).getTime()
ret = movie.nextFrame()
expect(ret).toBe(true)
waitsFor(
->
called =
obj.callback.callCount == 1 and obj.callback.argsForCall[0][0] == data
actualDelay = ((new Date).getTime() - nextFrameCallTime) / 1000 # seconds
diff = actualDelay - (timing[0][0] * (1.0 / AsciiIo.Movie::SPEED))
isProperDelay = diff < Math.abs(0.02)
called and isProperDelay
'movie-frame event not triggered in <delay> time'
1000
)
describe 'when finished', ->
beforeEach ->
timing = []
movie = new AsciiIo.Movie(data, timing)
it 'triggers movie-finished event', ->
obj = { callback: -> true }
movie.on('movie-finished', -> obj.callback())
spyOn(obj, 'callback')
ret = movie.nextFrame()
expect(ret).toBe(false)
expect(obj.callback).toHaveBeenCalled()

View File

@ -25,19 +25,17 @@ describe AsciiIo.PlayerView, ->
})
expect(AsciiIo.TerminalView).toHaveBeenCalledWith({
el: element.find('.terminal')[0], cols: cols, lines: lines
cols: cols, lines: lines
})
it 'creates HudView instance passing proper DOM element', ->
spyOn(AsciiIo, 'HudView')
spyOn(AsciiIo, 'HudView').andReturn({ on: -> 'foo' })
player = new AsciiIo.PlayerView({
el: element, cols: cols, lines: lines, data: data, timing: timing
})
expect(AsciiIo.HudView).toHaveBeenCalledWith({
el: element.find('.hud')[0]
})
expect(AsciiIo.HudView).toHaveBeenCalled()
it 'creates Movie instance', ->
spyOn(AsciiIo, 'Movie').andCallThrough()
@ -49,22 +47,71 @@ describe AsciiIo.PlayerView, ->
expect(AsciiIo.Movie).toHaveBeenCalledWith(data, timing)
describe 'events', ->
it 'toggles movie playback when terminal-click is fired on terminal', ->
player = null
beforeEach ->
player = new AsciiIo.PlayerView({
el: element, cols: cols, lines: lines, data: data, timing: timing
})
it 'toggles movie playback when terminal-click is fired on terminal', ->
spyOn player.movie, 'togglePlay'
player.terminal.trigger 'terminal-click'
player.terminalView.trigger 'terminal-click'
expect(player.movie.togglePlay).toHaveBeenCalled()
it 'toggles movie playback when hud-play-click is fired on hud', ->
spyOn player.movie, 'togglePlay'
player.hudView.trigger 'hud-play-click'
expect(player.movie.togglePlay).toHaveBeenCalled()
it 'seeks movie playback when hud-seek-click is fired on hud', ->
spyOn player.movie, 'seek'
player.hudView.trigger 'hud-seek-click', 55
expect(player.movie.seek).toHaveBeenCalledWith(55)
it 'toggles fullscreen view when hud-fullscreen-click is fired on hud', ->
# pending functionality
it 'stops cursor blinking when movie-finished is fired on movie', ->
spyOn player.terminalView, 'stopCursorBlink'
# it ' when movie-frame is fired on movie', ->
player.movie.trigger 'movie-finished'
expect(player.terminalView.stopCursorBlink).toHaveBeenCalled()
it 'feeds interpreter when movie-frame is fired on movie', ->
frame = { some: 'Frame' }
spyOn player.interpreter, 'feed'
player.movie.trigger 'movie-frame', frame
expect(player.interpreter.feed).toHaveBeenCalledWith(frame)
it 'renders and clears buffer changes when movie-frame is fired on movie', ->
frame = { some: 'Frame' }
changes = { someChanges: 'here' }
spyOn player.terminalView, 'render'
spyOn(player.screenBuffer, 'changes').andReturn(changes)
spyOn(player.screenBuffer, 'clearChanges')
player.movie.trigger 'movie-frame', frame
expect(player.terminalView.render).toHaveBeenCalledWith(changes)
expect(player.screenBuffer.clearChanges).toHaveBeenCalled()
describe '#play', ->
it 'starts movie playback', ->
player = new AsciiIo.PlayerView({
el: element, cols: cols, lines: lines, data: data, timing: timing
})
spyOn player.movie, 'play'
player.play()
expect(player.movie.play).toHaveBeenCalled()

View File

@ -0,0 +1,3 @@
describe AsciiIo.ScreenBuffer, ->