Update the player to use backend-generated JSON frames

openid
Marcin Kulik 11 years ago
parent a1cd6ca2ae
commit ee636bf958

@ -24,6 +24,7 @@ gem 'coffee-rails', '~> 4.0.0'
gem 'uglifier', '>= 2.1.2'
gem 'sinatra', '~> 1.4.3', :require => false
gem 'oj', '~> 2.1.4'
gem 'active_model_serializers', '~> 0.8.1'
group :development do
gem 'quiet_assets', '~> 1.0.1'

@ -10,6 +10,8 @@ GEM
erubis (~> 2.7.0)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
active_model_serializers (0.8.1)
activemodel (>= 3.0)
activemodel (4.0.0)
activesupport (= 4.0.0)
builder (~> 3.1.0)
@ -313,6 +315,7 @@ PLATFORMS
ruby
DEPENDENCIES
active_model_serializers (~> 0.8.1)
airbrake (~> 3.1.7)
cane (~> 2.5.2)
capistrano (~> 2.15)

@ -5,8 +5,6 @@
//= require utf8
//= require extensions
//= require player/brush
//= require player/vt/ansi_interpreter
//= require_tree ./player/vt
//= require player/movie
//= require player/workers/main_worker

@ -7,8 +7,6 @@
//= require_tree ./player/views/renderers
//= require player/views/hud_view
//= require player/abstract_player
//= require player/vt/ansi_interpreter
//= require_tree ./player/vt
//= require player/movie
//= require player/player
//= require player/fallback_player

@ -3,38 +3,47 @@ class AsciiIo.AbstractPlayer
constructor: (@options) ->
@model = @options.model
@createView()
@fetchModel()
@loadFrames()
createView: ->
@view = new AsciiIo.PlayerView
el: @options.el
model: @model
cols: @options.cols
lines: @options.lines
cols: @model.get 'width'
lines: @model.get 'height'
hud: @options.hud
rendererClass: @options.rendererClass
snapshot: @options.snapshot
snapshot: @model.get 'snapshot'
containerWidth: @options.containerWidth
createVT: ->
throw 'not implemented'
createMovie: ->
throw 'not implemented'
movieOptions: ->
stdout: @model.get 'stdout'
stdout_frames: @model.get 'stdout_frames'
duration: @model.get 'duration'
speed: @options.speed
benchmark: @options.benchmark
cols: @options.cols
lines: @options.lines
cols: @model.get 'width'
lines: @model.get 'height'
loadFrames: =>
if @model.get('stdout_frames_url')
@fetchFrames()
else
setTimeout(
=> @model.fetch success: @loadFrames,
2000
)
fetchFrames: ->
url = @model.get('stdout_frames_url')
fetchModel: ->
@model.fetch success: @onModelReady
$.getJSON url, (frames) =>
@model.set 'stdout_frames', frames
@onModelReady()
onModelReady: =>
@createVT()
@createMovie()
@bindEvents()
@view.onModelReady()
@ -48,8 +57,6 @@ class AsciiIo.AbstractPlayer
@view.on 'play-clicked', => @movie.call 'togglePlay'
@view.on 'seek-clicked', (percent) => @movie.call 'seek', percent
@vt.on 'cursor-visibility', (show) => @view.showCursor show
@movie.on 'started', => @view.onStateChanged 'playing'
@movie.on 'paused', => @view.onStateChanged 'paused'
@movie.on 'finished', => @view.onStateChanged 'finished'

@ -11,7 +11,7 @@ class AsciiIo.Brush
@_default ||= @create()
@hash: (brush) ->
"#{brush.fg}_#{brush.bg}_#{brush.blink}_#{brush.bright}_#{brush.italic}_#{brush.underline}_#{brush.reverse}"
"#{brush.fg}_#{brush.bg}_#{brush.blink}_#{brush.bold}_#{brush.underline}_#{brush.inverse}"
@create: (options = {}) ->
key = @hash options
@ -27,10 +27,9 @@ class AsciiIo.Brush
@fg = undefined
@bg = undefined
@blink = false
@bright = false
@italic = false
@bold = false
@underline = false
@reverse = false
@inverse = false
for name, value of options
this[name] = value
@ -42,10 +41,9 @@ class AsciiIo.Brush
fg : @fg
bg : @bg
blink : @blink
bright : @bright
italic : @italic
bold : @bold
underline: @underline
reverse : @reverse
inverse : @inverse
fgColor: ->
if @inverse
@ -77,14 +75,6 @@ class AsciiIo.Brush
color += 8 if color != undefined && color < 8 && @blink
color
applyChanges: (changes) ->
attrs = @attributes()
for attr, val of changes
attrs[attr] = val
AsciiIo.Brush.create attrs
hasDefaultFg: ->
@fgColor() == AsciiIo.Brush.default_fg

@ -1,21 +1,4 @@
class AsciiIo.FallbackPlayer extends AsciiIo.AbstractPlayer
createVT: ->
@vt = new AsciiIo.VT @options.cols, @options.lines
createMovie: ->
@movie = new AsciiIo.Movie @movieOptions()
onModelReady: ->
super
bindEvents: ->
super
@movie.on 'reset', => @vt.reset()
@movie.on 'data', (data) =>
@vt.feed data
state = @vt.state()
@vt.clearChanges()
@movie.trigger 'render', state

@ -11,9 +11,7 @@ class AsciiIo.Movie
@completedFramesTime = 0
@playing = false
@lastFrameAt = undefined
@framesProcessed = 0
@clearPauseState()
@trigger 'reset'
call: (method, args...) ->
@[method].apply this, args
@ -21,8 +19,8 @@ class AsciiIo.Movie
now: ->
(new Date()).getTime()
stdout: ->
@options.stdout
stdout_frames: ->
@options.stdout_frames
play: ->
return if @isPlaying()
@ -70,8 +68,7 @@ class AsciiIo.Movie
@playing = true
@resumedAt = @now()
frame = @stdout()[@frameNo]
[delay, data] = frame
[delay, changes] = @stdout_frames()[@frameNo]
delayMs = delay * 1000
delayLeft = delayMs - @totalFrameWaitTime
@processFrameWithDelay(delayLeft)
@ -87,7 +84,7 @@ class AsciiIo.Movie
!@isPlaying() and !@isFinished() and @frameNo > 0
isFinished: ->
!@isPlaying() and @frameNo >= (@stdout().length - 1)
!@isPlaying() and @frameNo >= @stdout_frames().length
seek: (percent) ->
@stop()
@ -103,16 +100,13 @@ class AsciiIo.Movie
totalCount = 0
delay = data = undefined
@trigger 'reset'
while time < requestedTime
[delay, data] = @stdout()[frameNo]
[delay, changes] = @stdout_frames()[frameNo]
if time + delay >= requestedTime
break
frameData = String.fromCharCode.apply(String, data)
@trigger 'data', [frameData]
@trigger 'render', changes
time += delay
frameNo += 1
@ -170,7 +164,7 @@ class AsciiIo.Movie
@totalFrameWaitTime = 0
nextFrame: ->
frame = @stdout()[@frameNo]
frame = @stdout_frames()[@frameNo]
if not frame or frame.length is 0
@playing = false
@ -178,15 +172,10 @@ class AsciiIo.Movie
return false
[delay, data] = frame
[delay, changes] = frame
if delay <= @MIN_DELAY and @framesProcessed < 100
@framesProcessed += 1
@processFrame()
else
@framesProcessed = 0
realDelay = delay * 1000 * (1.0 / @options.speed)
@processFrameWithDelay(realDelay)
realDelay = delay * 1000 * (1.0 / @options.speed)
@processFrameWithDelay(realDelay)
true
@ -199,11 +188,8 @@ class AsciiIo.Movie
)
processFrame: ->
frame = @stdout()[@frameNo]
[delay, data] = frame
frameData = String.fromCharCode.apply(String, data)
@trigger 'data', [frameData]
[delay, changes] = @stdout_frames()[@frameNo]
@trigger 'render', changes
@frameNo += 1
@completedFramesTime += delay * 1000

@ -7,9 +7,6 @@ class AsciiIo.Player extends AsciiIo.AbstractPlayer
createWorkerProxy: ->
@workerProxy = new AsciiIo.WorkerProxy(window.mainWorkerPath)
createVT: ->
@vt = @workerProxy.getObjectProxy 'vt'
createMovie: ->
@movie = @workerProxy.getObjectProxy 'movie'

@ -16,7 +16,7 @@ class AsciiIo.PlayerView extends Backbone.View
@$el.append @rendererView.$el
@rendererView.afterInsertedToDom()
@rendererView.renderSnapshot @options.snapshot
@renderSnapshot()
setupClipping: ->
if @options.containerWidth
@ -35,6 +35,7 @@ class AsciiIo.PlayerView extends Backbone.View
@$el.append @hudView.$el
onModelReady: ->
@renderSnapshot()
@hideLoadingOverlay()
@hudView.setDuration @model.get('duration') if @hudView
@ -48,6 +49,9 @@ class AsciiIo.PlayerView extends Backbone.View
onSeekClicked: (percent) ->
@trigger 'seek-clicked', percent
renderSnapshot: ->
@rendererView.renderSnapshot @model.get('snapshot')
showOverlay: (html) ->
element = $(html)
element.css('margin-right': "#{@rightClipWidth}px") if @rightClipWidth

@ -7,7 +7,8 @@ class AsciiIo.Renderer.Base extends Backbone.View
@lines = options.lines
@showCursor true
@startCursorBlink()
@clearState()
@clearChanges()
@cursor = { x: undefined, y: undefined, visible: true }
requestAnimationFrame @render
width: ->
@ -19,31 +20,30 @@ class AsciiIo.Renderer.Base extends Backbone.View
elementWidth: ->
@$el.outerWidth()
clearState: ->
@state =
changes: {}
cursorX: undefined
cursorY: undefined
dirty: false
clearChanges: ->
@changes = {}
@dirty = false
onClick: ->
@trigger('terminal-click')
push: (state) ->
_(@state.changes).extend state.changes
@state.cursorX = state.cursorX
@state.cursorY = state.cursorY
@state.dirty = true
push: (changes) ->
if changes.lines
_(@changes).extend changes.lines
@dirty = true
if changes.cursor
_(@cursor).extend changes.cursor
render: =>
requestAnimationFrame @render
if @state.dirty
for n, fragments of @state.changes
c = if parseInt(n) is @state.cursorY then @state.cursorX else undefined
if @dirty
for n, fragments of @changes
c = if parseInt(n) is @cursor.y then @cursor.x else undefined
@renderLine n, fragments || [], c
@clearState()
@clearChanges()
renderLine: (n, data, cursorX) ->
throw '#renderLine not implemented'

@ -21,7 +21,7 @@ class AsciiIo.Renderer.Pre extends AsciiIo.Renderer.Base
@$el.css(width: @width() + 'px', height: @height() + 'px')
render: ->
if @state.dirty
if @dirty
@$el.find('.cursor').removeClass('cursor')
super
@ -33,18 +33,8 @@ class AsciiIo.Renderer.Pre extends AsciiIo.Renderer.Base
for fragment in fragments
[text, brush] = fragment
if cursorX isnt undefined and rendered <= cursorX < rendered + text.length
left = text.slice(0, cursorX - rendered)
cursor =
'<span class="cursor visible">' + text[cursorX - rendered] + '</span>'
right = text.slice(cursorX - rendered + 1)
t = @escape(left) + cursor + @escape(right)
else
t = @escape(text)
html.push @spanFromBrush(brush)
html.push t
html.push @escape(text)
html.push '</span>'
rendered += text.length
@ -72,7 +62,7 @@ class AsciiIo.Renderer.Pre extends AsciiIo.Renderer.Base
unless brush.hasDefaultBg()
span += " bg" + brush.bgColor()
if brush.bright
if brush.bold
span += " bright"
if brush.underline
@ -103,9 +93,3 @@ class AsciiIo.Renderer.Pre extends AsciiIo.Renderer.Base
resetCursorState: ->
cursor = @$el.find(".cursor")
cursor.addClass "visible"
# TODO: check if it's used
clearScreen: ->
# this.lineData.length = 0;
# @cursorY = @cursorX = 0
@$el.find(".line").empty()

@ -1,286 +0,0 @@
class AsciiIo.AnsiInterpreter
constructor: ->
@sgrInterpreter = new AsciiIo.SgrInterpreter()
parse: (data) ->
@commands = []
while data.length > 0
processed = @handleData data
if processed is 0
# console.log "no kurwa: #{@formattedData(@data)}"
break
data = data.slice processed
[@commands, data]
cb: ->
@commands.push arguments
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]..|[\xc2-\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 'reserveCharacters', n
when "A"
n = 1 if n is undefined
@cb 'priorRow', n
when "B"
n = 1 if n is undefined
@cb 'nextRow', n
when "C"
n = 1 if n is undefined
@cb 'nextColumn', n
when "D"
n = 1 if n is undefined
@cb 'priorColumn', n
when "E"
@cb 'nextRowFirstColumn', n
when "F"
@cb 'priorRowFirstColumn', n
when "G"
n = 1 if n is undefined
@cb 'goToColumn', n
when "H"
n = 1 if n is undefined
m = 1 if m is undefined
@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 'insertLines', n or 1
when "M"
@cb 'deleteLines', 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 'eraseScreen'
else if action is "l"
# Clear screen, switch to normal screen buffer, and restore cursor position.
@cb 'eraseScreen'
@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) ->
numbers = [0] if numbers.length is 0
changes = @sgrInterpreter.parse numbers
@cb 'updateBrush', changes
formattedData: (data) ->
head = data.slice(0, 100)
hex = ("0x#{c.charCodeAt(0).toString(16)}" for c in head)
Utf8.decode(head) + " (" + hex.join() + ")"

@ -1,365 +0,0 @@
class AsciiIo.ScreenBuffer
constructor: (@cols, @lines, @scrollRegion, @tabStops) ->
@lineData = []
@dirtyLines = {}
@cursorX = 0
@cursorY = 0
@setBrush AsciiIo.Brush.default()
@setCharset 'us'
topMargin: ->
@scrollRegion.getTop()
bottomMargin: ->
@scrollRegion.getBottom()
updateLine: (n = @cursorY) ->
@dirtyLines[n] = true
updateLines: (a, b) ->
n = a
while n <= b
@updateLine n
n++
updateScreen: ->
@updateLine n for n in [0...@lines]
changes: ->
changes = {}
for n, _ of @dirtyLines
data = @lineData[n] || []
fragments = []
currentBrush = AsciiIo.Brush.default()
currentText = ''
for i in [0...@cols]
d = data[i]
if d
[char, brush] = d
else
[char, brush] = [' ', currentBrush]
if brush == currentBrush
currentText += char
else
fragments.push([currentText, currentBrush]) if currentText.length > 0
currentText = char
currentBrush = brush
fragments.push([currentText, currentBrush]) if currentText.length > 0
changes[n] = fragments
changes
clearChanges: ->
@dirtyLines = {}
getLine: (n = @cursorY) ->
throw "cant getLine " + n if n >= @lines
line = @lineData[n]
if typeof line is "undefined"
line = @lineData[n] = []
@fill n, 0, @cols, " "
line
_addEmptyLine: (l) ->
@lineData.splice l, 0, []
@clearLineData l
_removeLine: (l) ->
@lineData.splice l, 1
reserveCharacters: (n) ->
line = @getLine()
@lineData[@cursorY] = line.slice(0, @cursorX).concat(" ".times(n).split(""), line.slice(@cursorX, @cols - n))
@updateLine()
fill: (line, col, n, char, brush=@brush) ->
lineArr = @getLine(line)
i = 0
while i < n
lineArr[col + i] = [char, brush]
i++
deleteCharacters: (n) ->
line = @getLine()
brush = line[line.length-1][1]
line.splice(@cursorX, n)
@fill(@cursorY, @cols - n, n, ' ', brush)
@updateLine()
print: (text) ->
text = Utf8.decode(text)
startLine = @cursorY
i = 0
while i < text.length
if @cursorX >= @cols
@goToNextRowFirstColumn()
@fill @cursorY, @cursorX, 1, @charsetModifier(text[i])
@cursorX += 1
i++
endLine = @cursorY
@updateLines(startLine, endLine)
clearLineData: (n) ->
@fill n, 0, @cols, " "
# ----- Cursor control
correctCursorPos: ->
if @cursorX < 0
@cursorX = 0
if @cursorX >= @cols
@cursorX = @cols - 1
if @cursorY < 0
@cursorY = 0
if @cursorY >= @lines
@cursorY = @lines - 1
priorRow: (n = 1) ->
for i in [0...n]
if @cursorY > 0
@cursorY -= 1
@updateLine @cursorY
@updateLine @cursorY + 1
nextRow: (n = 1) ->
for i in [0...n]
if @cursorY + 1 < @lines
@cursorY += 1
@updateLine @cursorY - 1
@updateLine @cursorY
nextColumn: (n = 1) ->
@_cursorRight() for i in [0...n]
priorColumn: (n = 1) ->
@_cursorLeft() for i in [0...n]
_cursorLeft: ->
if @cursorX > 0
@cursorX -= 1
@updateLine()
_cursorRight: ->
if @cursorX < @cols - 1
@cursorX += 1
@updateLine()
priorRowFirstColumn: (n = 1) ->
@goToFirstColumn()
@priorRow n
nextRowFirstColumn: (n = 1) ->
@goToFirstColumn()
@nextRow n
goToColumn: (col = 1) ->
@cursorX = col - 1
@correctCursorPos()
@updateLine()
goToRow: (line = 1) ->
oldLine = @cursorY
@cursorY = line - 1
@correctCursorPos()
if oldLine != @cursorY
@updateLine oldLine
@updateLine()
goToRowAndColumn: (line = 1, col = 1) ->
@goToRow line
@goToColumn col
goToNextHorizontalTabStop: (n) ->
x = @tabStops.next(@cursorX)
@goToRowAndColumn(@cursorY + 1, x + 1)
@updateLine()
goToPriorHorizontalTabStop: (n) ->
x = @tabStops.prev(@cursorX)
@goToRowAndColumn(@cursorY + 1, x + 1)
@updateLine()
clearHorizontalTabStop: ->
console.log 'clearHorizontalTabStop'
clearAllHorizontalTabStops: ->
console.log 'clearAllHorizontalTabStops'
saveCursor: ->
@savedCol = @cursorX
@savedLine = @cursorY
restoreCursor: ->
oldLine = @cursorY
@cursorY = @savedLine
@cursorX = @savedCol
@updateLine oldLine
@updateLine()
goToFirstColumn: ->
@cursorX = 0
@updateLine()
# ----- Attribute control
setBrush: (brush) ->
@brush = brush
getBrush: ->
@brush
saveBrush: ->
@savedBrush = @brush
restoreBrush: ->
@brush = @savedBrush
repeatLastCharacter: (n = 1) ->
updateBrush: (changes) ->
@brush = @brush.applyChanges changes
# ----- Scroll control
inScrollRegion: ->
@cursorY >= @topMargin() and @cursorY <= @bottomMargin()
scrollUp: (n = 1) ->
@insertLines n, @topMargin()
scrollDown: (n = 1) ->
@deleteLines n, @topMargin()
insertLines: (n, l = @cursorY) ->
return unless @inScrollRegion()
i = 0
while i < n
@_removeLine @bottomMargin()
@_addEmptyLine l
i++
@updateLines(l, @bottomMargin())
deleteLines: (n, l = @cursorY) ->
return unless @inScrollRegion()
i = 0
while i < n
@_removeLine l
@_addEmptyLine @bottomMargin()
i++
@updateLines(l, @bottomMargin())
goToPriorRow: ->
if @cursorY is @topMargin()
@scrollUp()
else
@priorRow()
goToNextRow: ->
if @cursorY is @bottomMargin()
@scrollDown()
else
@nextRow()
goToNextRowFirstColumn: ->
@goToFirstColumn()
@goToNextRow()
setLineWrap: (linewrap) ->
# ----- Erase control
eraseScreen: ->
l = 0
while l < @lines
@clearLineData l
@updateLine l
l++
eraseFromScreenStart: ->
l = 0
while l < @cursorY
@clearLineData l
@updateLine l
l++
@eraseFromRowStart()
eraseToScreenEnd: ->
@eraseToRowEnd()
l = @cursorY + 1
while l < @lines
@clearLineData l
@updateLine l
l++
eraseRow: ->
@fill @cursorY, 0, @cols, " "
@updateLine()
eraseFromRowStart: ->
@fill @cursorY, 0, @cursorX, " "
@updateLine()
eraseToRowEnd: ->
@fill @cursorY, @cursorX, @cols - @cursorX, " "
@updateLine()
eraseCharacters: (n = 1) ->
@fill @cursorY, @cursorX, n, " "
@updateLine()
# ------ Charset control
setCharset: (charset) ->
@charset = charset
switch charset
when 'uk'
@charsetModifier = @ukCharsetModifier
when 'us'
@charsetModifier = @usCharsetModifier
when 'special'
@charsetModifier = @specialCharsetModifier
getCharset: ->
@charset
usCharsetModifier: (char) ->
char
ukCharsetModifier: (char) ->
char
specialCharsetModifier: (char) ->
AsciiIo.SpecialCharset[char.charCodeAt(0)] or char

@ -1,22 +0,0 @@
class AsciiIo.ScrollRegion
constructor: (@top, @bottom) ->
setTop: (@top) ->
setBottom: (@bottom) ->
getTop: ->
@top
getBottom: ->
@bottom
save: ->
@savedTop = @top
@savedBottom = @bottom
restore: ->
@top = @savedTop
@bottom = @savedBottom

@ -1,57 +0,0 @@
class AsciiIo.SgrInterpreter
parse: (numbers) ->
changes = {}
i = 0
while i < numbers.length
n = numbers[i]
if n is 0
changes.fg = undefined
changes.bg = undefined
changes.blink = false
changes.bright = false
changes.italic = false
changes.underline = false
changes.reverse = false
else if n is 1
changes.bright = true
else if n is 3
changes.italic = true
else if n is 4
changes.underline = true
else if n is 5
changes.blink = true
else if n is 7
changes.reverse = true
else if n is 23
changes.italic = false
else if n is 24
changes.underline = false
else if n is 25
changes.blink = false
else if n is 27
changes.reverse = false
else if n >= 30 and n <= 37
changes.fg = n - 30
else if n is 38
changes.fg = numbers[i + 2]
i += 2
else if n is 39
changes.fg = undefined
else if n >= 40 and n <= 47
changes.bg = n - 40
else if n is 48
changes.bg = numbers[i + 2]
i += 2
else if n is 49
changes.bg = undefined
else if n >= 90 and n <= 97
changes.fg = n - 90
else if n >= 100 and n <= 107
changes.bg = n - 100
i++
changes

@ -1,95 +0,0 @@
AsciiIo.SpecialCharset =
33: "!"
34: '"'
35: "#"
36: "$"
37: "%"
38: "&"
39: "'"
40: "("
41: ")"
42: "*"
43: "+"
44: ","
45: "-"
46: "."
47: "/"
48: "0"
49: "1"
50: "2"
51: "3"
52: "4"
53: "5"
54: "6"
55: "7"
56: "8"
57: "9"
58: ":"
59: ";"
60: "<"
61: "="
62: ">"
63: "?"
64: "@"
65: ""
66: ""
67: ""
68: ""
69: ""
70: ""
71: ""
72: "H"
73: "I"
74: "J"
75: "K"
76: "L"
77: "M"
78: "N"
79: "O"
80: "P"
81: "Q"
82: "R"
83: "S"
84: "T"
85: "U"
86: "V"
87: "W"
88: "X"
89: "Y"
90: "Z"
91: "["
92: "\\"
93: "]"
94: "^"
95: "_"
96: ""
97: ""
98: ""
99: ""
100: ""
101: ""
102: "°"
103: "±"
104: ""
105: ""
106: ""
107: ""
108: ""
109: ""
110: ""
111: ""
112: ""
113: ""
114: ""
115: ""
116: ""
117: ""
118: ""
119: ""
120: ""
121: ""
122: ""
123: "π"
124: ""
125: "£"
126: "·"

@ -1,28 +0,0 @@
class AsciiIo.TabStops
constructor: (@cols) ->
@stops = (x for x in [0...@cols] when x % 8 is 0)
add: (col) ->
unless _(@stops).include(col)
pos = _(@stops).sortedIndex(col)
@stops.splice(pos, 0, col)
next: (cursorX) ->
for x in @stops
if x > cursorX
return x
@cols
prev: (cursorX) ->
ret = 0
for x in @stops
if x > cursorX
break
ret = x
ret

@ -1,234 +0,0 @@
class AsciiIo.VT
constructor: (@cols, @lines) ->
_.extend(this, Backbone.Events)
@interpreter = new AsciiIo.AnsiInterpreter
@reset()
feed: (data) ->
@data += data
[commands, @data] = @interpreter.parse @data
for command in commands
[name, args...] = command
@[name].apply this, args
@data.length is 0
reset: ->
@data = ''
@resetTerminal()
bell: ->
@trigger 'bell'
state: ->
changes: @buffer.changes()
cursorX: @buffer.cursorX
cursorY: @buffer.cursorY
clearChanges: ->
@buffer.clearChanges()
# ==== Screen buffer operations
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')
print: (text) ->
@buffer.print text
reserveCharacters: (n) ->
@buffer.reserveCharacters 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()
insertLines: (n) ->
@buffer.insertLines n
deleteLines: (n) ->
@buffer.deleteLines 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()
updateBrush: (attrs) ->
@buffer.updateBrush attrs
# 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

@ -1,4 +1,3 @@
vt = undefined
movie = undefined
addEventListener 'message', (e) =>
@ -10,21 +9,13 @@ addEventListener 'message', (e) =>
when 'call'
switch d.objectName
when 'vt'
vt[d.method](d.args...)
when 'movie'
movie[d.method](d.args...)
@initialize = (options) ->
vt = new AsciiIo.VT options.cols, options.lines
vt.on 'all', (event, args...) ->
postMessage evt: event, src: 'vt', args: args
movie = new AsciiIo.Movie(
stdout: options.stdout
stdout_frames: options.stdout_frames
duration: options.duration
speed: options.speed
benchmark: options.benchmark
@ -35,12 +26,4 @@ addEventListener 'message', (e) =>
movie.on 'all', (event, args...) ->
postMessage evt: event, src: 'movie', args: args
movie.on 'reset', => vt.reset()
movie.on 'data', (data) =>
vt.feed data
state = vt.state()
vt.clearChanges()
movie.trigger 'render', state
console.log 'inited!'

@ -37,8 +37,7 @@ class AsciicastsController < ApplicationController
end
format.json do
response.headers['Cache-Control'] = 'no-cache' # prevent Rack buffering
self.response_body = AsciicastStreamer.new(@asciicast)
respond_with @asciicast
end
format.js do

@ -91,7 +91,7 @@ class AsciicastDecorator < ApplicationDecorator
%(<script type="text/javascript" src="#{src}" id="#{id}" async></script>)
end
def duration
def formatted_duration
duration = model.duration.to_i
minutes = duration / 60
seconds = duration % 60

@ -14,17 +14,14 @@ module AsciicastsHelper
end
render :partial => 'asciicasts/player', :locals => {
asciicast: serialized_asciicast(asciicast),
player_class: player_class,
cols: asciicast.terminal_columns,
lines: asciicast.terminal_lines,
speed: (options[:speed] || params[:speed] || 1).to_f,
benchmark: !!params[:bm],
asciicast_id: asciicast.id,
container_width: params[:container_width],
renderer_class: renderer_class,
auto_play: options.key?(:auto_play) ? !!options[:auto_play] : false,
hud: options.key?(:hud) ? !!options[:hud] : true,
snapshot: asciicast.snapshot.to_json
hud: options.key?(:hud) ? !!options[:hud] : true
}
end
@ -33,4 +30,10 @@ module AsciicastsHelper
:data => { :confirm => 'Really delete this asciicast?' }
end
private
def serialized_asciicast(asciicast)
AsciicastSerializer.new(asciicast).to_json
end
end

@ -5,7 +5,7 @@ class Asciicast < ActiveRecord::Base
mount_uploader :stdin_timing, StdinTimingUploader
mount_uploader :stdout_data, StdoutDataUploader
mount_uploader :stdout_timing, StdoutTimingUploader
mount_uploader :stdout_frames, BaseUploader
mount_uploader :stdout_frames, StdoutFramesUploader
serialize :snapshot, JSON

@ -1,15 +1,7 @@
class AsciicastSerializer
def initialize(asciicast)
@asciicast = asciicast
end
def as_json(*)
asciicast.as_json
end
private
attr_reader :asciicast
class AsciicastSerializer < ActiveModel::Serializer
self.root = false
attributes :id, :duration, :stdout_frames_url, :snapshot
attribute :terminal_columns, key: :width
attribute :terminal_lines, key: :height
end

@ -1,53 +0,0 @@
class AsciicastStreamer
delegate :each, :to => :json_streamer
def initialize(asciicast)
build_json_streamer(asciicast)
end
private
attr_reader :json_streamer
def build_json_streamer(asciicast)
attributes = attributes_for_streaming(asciicast)
@json_streamer = JsonStreamer.new(attributes)
end
def attributes_for_streaming(asciicast)
attributes = AsciicastSerializer.new(asciicast).as_json
saved_time = prepare_stdout(attributes, asciicast)
prepare_duration(attributes, asciicast, saved_time)
attributes
end
def prepare_stdout(attributes, asciicast)
saved_time = 0
attributes['stdout'] = lambda do |&blk|
blk.call('[')
asciicast.stdout.each do |delay, frame_bytes|
if asciicast.time_compression && delay > Asciicast::MAX_DELAY
saved_time += (delay - Asciicast::MAX_DELAY)
delay = Asciicast::MAX_DELAY
end
blk.call(%([#{delay},#{frame_bytes.bytes.to_a}],))
end
blk.call('[]]')
end
saved_time
end
def prepare_duration(attributes, asciicast, saved_time)
attributes['duration'] = lambda do |&blk|
blk.call(asciicast.duration - saved_time)
end
end
end

@ -1,28 +0,0 @@
class JsonStreamer
def initialize(object)
@object = object
end
def each(&blk)
yield '{'
@object.each_with_index do |key_value, i|
key, value = key_value
yield %("#{key}":)
if value.respond_to?(:call)
value.call do |v|
yield v.to_s
end
else
yield value.to_json
end
yield ',' unless i + 1 == @object.size
end
yield '}'
end
end

@ -0,0 +1,7 @@
class StdoutFramesUploader < BaseUploader
def filename
'stdout.json' if original_filename.present?
end
end

@ -1,19 +0,0 @@
<div class="player"></div>
<script>
$(function() {
var playerClass = <%= player_class.html_safe %>;
window.player = new playerClass({
el: $('.player'),
cols: <%= cols %>,
lines: <%= lines %>,
speed: <%= speed %>,
benchmark: <%= benchmark %>,
model: new AsciiIo.Asciicast({ id: <%= asciicast_id %> }),
containerWidth: <%= container_width || 'null' %>,
rendererClass: <%= renderer_class.html_safe %>,
autoPlay: <%= auto_play %>,
hud: <%= hud %>,
snapshot: <%= snapshot.html_safe %>
});
});
</script>

@ -0,0 +1,16 @@
.player
javascript:
$(function() {
var playerClass = #{player_class.html_safe};
window.player = new playerClass({
el: $('.player'),
speed: #{speed},
benchmark: #{benchmark},
model: new AsciiIo.Asciicast(#{asciicast.html_safe}),
containerWidth: #{container_width || 'null'},
rendererClass: #{renderer_class.html_safe},
autoPlay: #{auto_play},
hud: #{hud}
});
});

@ -10,7 +10,7 @@
.info
h3 = link_to asciicast.title, asciicast
p.date
span> = asciicast.duration
span> = asciicast.formatted_duration
' |
span> = asciicast.author_link
' |

@ -89,13 +89,7 @@ describe AsciicastsController do
end
context 'for json request' do
let(:streamer) { double('streamer') }
before do
allow(AsciicastStreamer).to receive(:new).with(asciicast).
and_return(streamer)
allow(controller).to receive(:response_body=).and_call_original
get :show, :id => asciicast.id, :format => :json
end
@ -104,10 +98,6 @@ describe AsciicastsController do
it 'should not be counted as a visit' do
expect(ViewCounter).to_not have_received(:new)
end
it 'should assign the streamer to the response_body' do
expect(controller).to have_received(:response_body=).with(streamer)
end
end
context 'for js request' do

@ -274,8 +274,8 @@ describe AsciicastDecorator do
end
end
describe '#duration' do
subject { decorator.duration }
describe '#formatted_duration' do
subject { decorator.formatted_duration }
context "when it's below 1 minute" do
before do

@ -1,593 +0,0 @@
describe 'AsciiIo.AnsiInterpreter', ->
data = interpreter = calls = checkNumber = undefined
parse = (data, expectedRest) ->
[commands, rest] = interpreter.parse data
calls = commands
checkNumber = 0
if arguments.length is 2
expect(rest).toEqual expectedRest
expectCall = (args...) ->
n = checkNumber
checkNumber += 1
# console.log calls[n]
expect(calls[n]).toEqual args
expectNoCall = ->
# console.log calls[0]
expect(calls.length).toEqual 0
isSwallowed = (d = data) ->
parse d, ''
expectNoCall()
beforeEach ->
calls = []
interpreter = new AsciiIo.AnsiInterpreter
describe '#parse', ->
it 'returns not parsed data back', ->
parse '\x1b', '\x1b'
expectNoCall()
it 'returns empty string if all data was parsed', ->
parse 'abc def', ''
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 'x07', ->
it 'calls bell', ->
parse '\x07'
expectCall 'bell'
describe 'x08', ->
it 'calls backspace', ->
parse '\x08'
expectCall 'backspace'
describe 'x09', ->
it 'calls goToNextHorizontalTabStop', ->
parse '\x09'
expectCall 'goToNextHorizontalTabStop'
describe 'x0a', ->
it 'calls lineFeed', ->
parse '\x0a'
expectCall 'lineFeed'
describe 'x0b', ->
it 'calls verticalTab', ->
parse '\x0b'
expectCall 'verticalTab'
describe 'x0c', ->
it 'calls formFeed', ->
parse '\x0c'
expectCall 'formFeed'
describe 'x0d', ->
it 'calls carriageReturn', ->
parse '\x0d'
expectCall 'carriageReturn'
describe 'x84', ->
it 'calls index', ->
parse '\x84'
expectCall 'index'
describe 'x85', ->
it 'calls newLine', ->
parse '\x85'
expectCall 'newLine'
describe 'x88', ->
it 'calls setHorizontalTabStop', ->
parse '\x88'
expectCall 'setHorizontalTabStop'
describe 'x8d', ->
it 'calls reverseIndex', ->
parse '\x8d'
expectCall 'reverseIndex'
describe 'other', ->
it "is swallowed", ->
for c in ['\x00', '\x0e', '\x0f', '\x82', '\x94']
isSwallowed c
describe 'printable character', ->
describe 'from ASCII range (0x20-0x7e)', ->
it 'calls print', ->
parse '\x20foobar\x7e', ''
expectCall 'print', '\x20foobar\x7e'
describe 'from Unicode', ->
it 'calls print', ->
parse '\xe2ab\xe8ZZ', ''
expectCall 'print', '\xe2ab\xe8ZZ'
parse '\xc2\x09\xc4b\xc5c', ''
expectCall 'print', '\xc2\x09\xc4b\xc5c'
parse '\xa1A\xc9\xfe', ''
expectCall 'print', '\xa1A\xc9\xfe'
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 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 'stops interpreting current seq and handles nested C0 control char', ->
# data += '[1\x1b\x0dm'
# parse data, ''
# expectCall 'cr'
# expectCall 'updateBrush', bright: true
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 '0', ->
beforeEach ->
data += '0'
it 'calls setSpecialCharset', ->
parse data
expectCall 'setSpecialCharset'
describe 'A', ->
beforeEach ->
data += 'A'
it 'calls setUkCharset', ->
parse data
expectCall 'setUkCharset'
describe 'B', ->
beforeEach ->
data += 'B'
it 'calls setUsCharset', ->
parse data
expectCall 'setUsCharset'
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 terminal state', ->
parse data
expectCall 'saveTerminalState'
describe '8', ->
beforeEach ->
data += '8'
it 'restores terminal state', ->
parse data
expectCall 'restoreTerminalState'
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', ->
parse data
expectCall 'reverseIndex'
describe 'P', ->
# Device Control String, terminated by ST
beforeEach ->
data += 'Pfoobar\\'
# it 'is swallowed', isSwallowed
describe ']', ->
# Operating system command
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
# appear within the Control Sequence. The terminating character may be
# preceded by an intermediate character (such as space).
beforeEach ->
data += '['
describe '@', ->
it 'calls reserveCharacters', ->
data += '3@'
parse data
expectCall 'reserveCharacters', 3
describe 'A', ->
it 'calls priorRow(1) if no number given', ->
data += 'A'
parse data
expectCall 'priorRow', 1
it 'calls priorRow(n) if number given', ->
data += '3A'
parse data
expectCall 'priorRow', 3
describe 'B', ->
it 'calls nextRow(1) if no number given', ->
data += 'B'
parse data
expectCall 'nextRow', 1
it 'calls nextRow(n) if number given', ->
data += '3B'
parse data
expectCall 'nextRow', 3
describe 'C', ->
it 'calls nextColumn(1) if no number given', ->
data += 'C'
parse data
expectCall 'nextColumn', 1
it 'calls nextColumn(n) if number given', ->
data += '3C'
parse data
expectCall 'nextColumn', 3
describe 'D', ->
it 'calls priorColumn(1) if no number given', ->
data += 'D'
parse data
expectCall 'priorColumn', 1
it 'calls priorColumn(n) if number given', ->
data += '3D'
parse data
expectCall 'priorColumn', 3
describe 'E', ->
it 'calls nextRowFirstColumn(n)', ->
data += '3E'
parse data
expectCall 'nextRowFirstColumn', 3
describe 'F', ->
it 'calls priorRowFirstColumn(n)', ->
data += '3F'
parse data
expectCall 'priorRowFirstColumn', 3
describe 'G', ->
it 'calls goToColumn(n)', ->
data += '3G'
parse data
expectCall 'goToColumn', 3
describe 'H', ->
it 'calls goToRowAndColumn(n, m) when n and m given', ->
data += '3;4H'
parse data
expectCall 'goToRowAndColumn', 3, 4
it 'calls goToRowAndColumn(1, m) when no n given', ->
data += ';3H'
parse data
expectCall 'goToRowAndColumn', 1, 3
it 'calls goToRowAndColumn(n, 1) when no m given', ->
data += '3;H'
parse data
expectCall 'goToRowAndColumn', 3, 1
it 'calls goToRowAndColumn(n, 1) when no m given (no semicolon)', ->
data += '3H'
parse data
expectCall 'goToRowAndColumn', 3, 1
it 'calls goToRowAndColumn(1, 1) when no n nor m given', ->
data += 'H'
parse data
expectCall 'goToRowAndColumn', 1, 1
describe 'I', ->
it 'calls goToNextHorizontalTabStop', ->
data += '2I'
parse data
expectCall 'goToNextHorizontalTabStop', 2
describe 'J', ->
it 'calls eraseToScreenEnd when no n given', ->
data += 'J'
parse data
expectCall 'eraseToScreenEnd'
it 'calls eraseToScreenEnd when 0 given', ->
data += '0J' # TODO check if it's 0 or 3
parse data
expectCall 'eraseToScreenEnd'
it 'calls eraseFromScreenStart when 1 given', ->
data += '1J'
parse data
expectCall 'eraseFromScreenStart'
it 'calls eraseScreen when 2 given', ->
data += '2J'
parse data
expectCall 'eraseScreen'
describe 'K', ->
it 'calls eraseToRowEnd when no n given', ->
data += 'K'
parse data
expectCall 'eraseToRowEnd'
it 'calls eraseToRowEnd when 0 given', ->
data += '0K' # TODO: check if its 0 or 3
parse data
expectCall 'eraseToRowEnd'
it 'calls eraseFromRowStart when 1 given', ->
data += '1K'
parse data
expectCall 'eraseFromRowStart'
it 'calls eraseRow when 2 given', ->
data += '2K'
parse data
expectCall 'eraseRow'
describe 'L', ->
it 'calls insertLines(1) when no n given', ->
data += 'L'
parse data
expectCall 'insertLines', 1
it 'calls insertLines(n) when n given', ->
data += '3L'
parse data
expectCall 'insertLines', 3
describe 'M', ->
it 'calls deleteLines(1) when no n given', ->
data += 'M'
parse data
expectCall 'deleteLines', 1
it 'calls deleteLines(n) when n given', ->
data += '3M'
parse data
expectCall 'deleteLines', 3
describe 'P', ->
it 'calls deleteCharacters(1) when no n given', ->
data += 'P'
parse data
expectCall 'deleteCharacters', 1
it 'calls deleteCharacters(n) when n given', ->
data += '3P'
parse data
expectCall 'deleteCharacters', 3
describe 'S', ->
it 'calls scrollUp(n)', ->
data += '4S'
parse data
expectCall 'scrollUp', 4
describe 'T', ->
it 'calls scrollDown(n)', ->
data += '4T'
parse data
expectCall 'scrollDown', 4
describe 'X', ->
it 'calls eraseCharacters(n)', ->
data += '4X'
parse data
expectCall 'eraseCharacters', 4
describe 'Z', ->
it 'calls goToPriorHorizontalTabStop(n)', ->
data += '5Z'
parse data
expectCall 'goToPriorHorizontalTabStop', 5
describe 'b', ->
# TODO
describe 'c', ->
beforeEach ->
data += '>c'
it 'is swallowed', isSwallowed
describe 'd', ->
it 'calls goToRow(n)', ->
data += '3d'
parse data
expectCall 'goToRow', 3
describe 'f', ->
# TODO
describe 'g', ->
# TODO
describe 'l', ->
# TODO
describe 'm', ->
it 'calls handleSGR([n, m, ...]) when n and m given', ->
data += '1;4;33m'
spyOn interpreter, 'handleSGR'
parse data
expect(interpreter.handleSGR).toHaveBeenCalledWith([1, 4, 33])
it 'calls handleSGR([]) when no n nor m given', ->
data += 'm'
spyOn interpreter, 'handleSGR'
parse data
expect(interpreter.handleSGR).toHaveBeenCalledWith([])
describe 'n', ->
# TODO
describe 'r', ->
# TODO
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', ->
parse data
expectCall 'showCursor'
describe '25l', ->
beforeEach ->
data += '25l'
it 'hides cursor', ->
parse data
expectCall 'hideCursor'
describe '47h', ->
beforeEach ->
data += '47h'
it 'switches to alternate buffer', ->
parse data
expectCall 'switchToAlternateBuffer'
describe '47l', ->
beforeEach ->
data += '47l'
it 'switches to normal buffer', ->
parse data
expectCall 'switchToNormalBuffer'
describe '1049h', ->
beforeEach ->
data += '1049h'
it 'saves cursor position, switches to alternate buffer and clear screen', ->
parse data
expectCall 'switchToAlternateBuffer'
expectCall 'eraseScreen'
describe '1049l', ->
beforeEach ->
data += '1049l'
it 'clears screen, switches to normal buffer and restores cursor position', ->
parse data
expectCall 'eraseScreen'
expectCall 'switchToNormalBuffer'

@ -1,70 +0,0 @@
describe 'AsciiIo.SgrInterpreter', ->
interpreter = new AsciiIo.SgrInterpreter
expectChange = (numbers, hash) ->
attrs = interpreter.parse numbers
expect(attrs).toEqual hash
describe '#parse', ->
it 'resets brush for 0', ->
expectChange [0], AsciiIo.Brush.default().attributes()
it 'sets bright attr for 1', ->
expectChange [1], bright: true
it 'sets italic attr for 3', ->
expectChange [3], italic: true
it 'sets underline attr for 4', ->
expectChange [4], underline: true
it 'sets blink attr for 5', ->
expectChange [5], blink: true
it 'sets reverse attr for 7', ->
expectChange [7], reverse: true
it 'unsets italic for 23', ->
expectChange [23], italic: false
it 'unsets underline attr for 24', ->
expectChange [24], underline: false
it 'unsets blink attr for 25', ->
expectChange [25], blink: false
it 'unsets reverse attr for 27', ->
expectChange [27], reverse: false
it 'sets foreground for 30-37', ->
expectChange [30], fg: 0
expectChange [32], fg: 2
expectChange [37], fg: 7
it 'sets foreground for 38;5;x', ->
expectChange [38, 5, 100], fg: 100
it 'resets foreground for 39', ->
expectChange [39], fg: undefined
it 'sets background for 40-47', ->
expectChange [40], bg: 0
expectChange [44], bg: 4
expectChange [47], bg: 7
it 'sets background for 48;5;x', ->
expectChange [48, 5, 200], bg: 200
it 'resets background for 49', ->
expectChange [49], bg: undefined
it 'sets foreground for 90-97', ->
expectChange [90], fg: 0
expectChange [93], fg: 3
expectChange [97], fg: 7
it 'sets background for 100-107', ->
expectChange [100], bg: 0
expectChange [103], bg: 3
expectChange [107], bg: 7

@ -0,0 +1,15 @@
require 'spec_helper'
describe AsciicastSerializer do
let(:serializer) { described_class.new(asciicast) }
let(:asciicast) { create(:asciicast) }
describe '#to_json' do
subject { JSON.parse(serializer.to_json) }
it { should eq({ "id" => 1, "duration" => 11.146430015563965,
"stdout_frames_url" => nil, "snapshot" => nil,
"width" => 96, "height" => 26 }) }
end
end

@ -1,28 +0,0 @@
require 'spec_helper'
describe JsonStreamer do
let(:streamer) { JsonStreamer.new(object) }
let(:object) { {
:foo => 'bar',
'baz' => 1337,
'qux' => true,
'arr' => [1, 2],
'blk' => lambda { |&blk| blk['12.']; blk[3] }
} }
describe '#each' do
it 'calls supplied block with each key value pair' do
expect { |b| streamer.each(&b) }.
to yield_successive_args(
'{',
'"foo":', '"bar"', ',',
'"baz":', '1337' , ',',
'"qux":', 'true' , ',',
'"arr":', '[1,2]', ',',
'"blk":', '12.' , '3',
'}')
end
end
end
Loading…
Cancel
Save