opus/apps/recorder.lua

530 lines
14 KiB
Lua

-- +---------------------+------------+---------------------+
-- | | | |
-- | | RecGif | |
-- | | | |
-- +---------------------+------------+---------------------+
local version = "Version 1.1.6"
-- Records your terminal and saves the result as an animating GIF.
-- http://www.computercraft.info/forums2/index.php?/topic/24840-recgif/
-- ----------------------------------------------------------
-- Original code by Bomb Bloke
-- Modified to integrate with opus os
local calls, recTerm, oldTerm, arg, showInput, skipLast, lastDelay, curInput, callCount, callListCount = {{["delay"] = 0}}, {}, term.current(), {...}, false, false, 2, "", 1, 2
local curBlink, oldBlink, curCalls, tTerm, buffer, colourNum, xPos, yPos, oldXPos, oldYPos, tCol, bCol, xSize, ySize = false, false, calls[1], {}, {}, {}, 1, 1, 1, 1, colours.white, colours.black, term.getSize()
local greys, buttons = {["0"] = true, ["7"] = true, ["8"] = true, ["f"] = true}, {"l", "r", "m"}
local charW, charH, chars, resp
local filename
local function showSyntax()
print('Gif Recorder by Bomb Bloke\n')
print('Syntax: recGif [-i] [-s] [-ld:<delay>] filename')
print(' -i : show input')
print(' -s : skip last')
print(' -ld : last delay')
end
for i = #arg, 1, -1 do
local curArg = arg[i]:lower()
if curArg == "-i" then
showInput, ySize = true, ySize + 1
table.remove(arg, i)
elseif curArg == "-s" then
skipLast = true
table.remove(arg, i)
elseif curArg:sub(1, 4) == "-ld:" then
curArg = tonumber(curArg:sub(5))
if curArg then lastDelay = curArg end
table.remove(arg, i)
elseif curArg == '-?' then
showSyntax()
return
elseif i ~= #arg then
showSyntax()
printError('\nInvalid argument')
return
end
end
print('Press control-p to stop recording')
local filename = arg[#arg]
if not filename then
print('Enter file name:')
filename = read()
end
if #filename == 0 then
showSyntax()
print()
error('Invalid file name')
end
print('Initializing...')
fs.mount('.recGif', 'ramfs', 'directory')
fs.mount('.recGif/GIF', 'urlfs', 'http://pastebin.com/raw/5uk9uRjC')
fs.mount('.recGif/package', 'urlfs', 'http://pastebin.com/raw/cUYTGbpb')
-- don't pollute global env
local function loadAPI(filename, env)
local apiEnv = Util.shallowCopy(env)
apiEnv.shell = nil
apiEnv.multishell = nil
setmetatable(apiEnv, { __index = _G })
local fn = loadfile(filename, apiEnv)
fn()
return apiEnv
end
package = loadAPI('.recGif/package', getfenv(1))
GIF = loadAPI('.recGif/GIF', getfenv(1))
oldTerm = Util.shallowCopy(multishell.term)
local oldDir = shell.dir()
shell.setDir('.recGif')
shell.run("package get Y0eLUPtr")
shell.setDir(oldDir)
local function snooze()
local myEvent = tostring({})
os.queueEvent(myEvent)
os.pullEvent(myEvent)
end
local function safeString(text)
local newText = {}
for i = 1, #text do
local val = text:byte(i)
newText[i] = (val > 31 and val < 127) and val or 63
end
return string.char(unpack(newText))
end
local function safeCol(text, subst)
local newText = {}
for i = 1, #text do
local val = text:sub(i, i)
newText[i] = greys[val] and val or subst
end
return table.concat(newText)
end
-- Build a terminal that records stuff:
recTerm = multishell.term
for key, func in pairs(oldTerm) do
recTerm[key] = function(...)
local result = {pcall(func, ...)}
if result[1] then
curCalls[callCount] = {key, ...}
callCount = callCount + 1
return unpack(result, 2)
else error(result[2], 2) end
end
end
local tabId = multishell.getCurrent()
multishell.addHotkey(25, function()
os.queueEvent('recorder_stop')
end)
local tabs = multishell.getTabs()
for _,tab in pairs(tabs) do
if tab.isOverview then
multishell.hideTab(tabId)
multishell.setFocus(tab.tabId)
os.queueEvent('term_resize')
break
end
end
do
local curTime = os.clock() - 1
while true do
local event = { os.pullEventRaw() }
if event[1] == 'recorder_stop' or event[1] == 'terminate' then
break
end
local newTime = os.clock()
if newTime ~= curTime then
local delay = curCalls.delay + (newTime - curTime)
curTime = newTime
if callCount > 1 then
curCalls.delay = curCalls.delay + delay
curCalls, callCount = {["delay"] = 0}, 1
calls[callListCount] = curCalls
callListCount = callListCount + 1
elseif callListCount > 2 then
calls[callListCount - 2].delay = calls[callListCount - 2].delay + delay
end
end
if showInput and (event[1] == "key" or event[1] == "mouse_click") then
curCalls[callCount] = {unpack(event)}
callCount = callCount + 1
end
end
end
multishell.removeHotkey(25)
for k,fn in pairs(oldTerm) do
multishell.term[k] = fn
end
multishell.unhideTab(tabId)
multishell.setFocus(tabId)
if #calls[#calls] == 0 then calls[#calls] = nil end
if skipLast and #calls > 1 then calls[#calls] = nil end
calls[#calls].delay = lastDelay
-- Recording done, bug user as to whether to encode it:
print(string.format("Encoding %d frames...", #calls))
--Util.writeTable('tmp/raw.txt', calls)
-- Perform a quick re-parse of the recorded data (adding frames for when the cursor blinks):
do
local callListCount, tempCalls, blink, oldBlink, curBlink, blinkDelay = 1, {}, false, false, true, 0
for i = 1, #calls - 1 do
curCalls = calls[i]
tempCalls[callListCount] = curCalls
for j = 1, #curCalls do if curCalls[j][1] == "setCursorBlink" then blink = curCalls[j][2] end end
if blink then
if blinkDelay == 0 then
curCalls[#curCalls + 1] = {"toggleCur", curBlink}
blinkDelay, curBlink = 0.4, not curBlink
end
while tempCalls[callListCount].delay > blinkDelay do
local remainder = tempCalls[callListCount].delay - blinkDelay
tempCalls[callListCount].delay = blinkDelay
callListCount = callListCount + 1
tempCalls[callListCount] = {{"toggleCur", curBlink}, ["delay"] = remainder}
blinkDelay, curBlink = 0.4, not curBlink
end
blinkDelay = blinkDelay - tempCalls[callListCount].delay
else
if oldBlink then curCalls[#curCalls + 1] = {"toggleCur", false} end
blinkDelay = (curCalls.delay - blinkDelay) % 0.4
end
callListCount, oldBlink = callListCount + 1, blink
end
tempCalls[callListCount] = calls[#calls]
tempCalls[callListCount][#tempCalls[callListCount] + 1] = {"toggleCur", false}
calls, curCalls = tempCalls, nil
end
snooze()
-- Load font data:
do
local ascii, counter = GIF.toPaintutils(GIF.flattenGIF(GIF.loadGIF(".recGif/ascii.gif"))), 0
local newFont, ybump, xbump = #ascii ~= #ascii[1], 0, 0
charW, charH, chars = newFont and #ascii[1] / 16 or #ascii[1] * 3 / 64, #ascii / 16, {}
for yy = 0, newFont and 15 or 7 do
for xx = 0, 15 do
local newChar, length = {}, 0
-- Place in 2d grid of bools:
for y = 1, charH do
local newRow = {}
for x = 1, charW do
local set = ascii[y + ybump][x + xbump] == 1
if set and x > length then length = x end
newRow[x] = set
end
newChar[y] = newRow
end
-- Center:
if not newFont then for y = 1, charH do for x = 1, math.floor((charW - length) / 2) do table.insert(newChar[y], 1, false) end end end
chars[counter] = newChar
counter, xbump = counter + 1, xbump + (newFont and charW or charH)
end
xbump, ybump = 0, ybump + charH
end
end
snooze()
-- Terminal data translation:
do
local hex, counter = "0123456789abcdef", 1
for i = 1, 16 do
colourNum[counter] = hex:sub(i, i)
counter = counter * 2
end
end
for y = 1, ySize do
buffer[y] = {}
for x = 1, xSize do buffer[y][x] = {" ", colourNum[tCol], colourNum[bCol]} end
end
if showInput then for x = 1, xSize do buffer[ySize][x][3] = colourNum[colours.lightGrey] end end
tTerm.blit = function(text, fgCol, bgCol)
if xPos > xSize or xPos + #text - 1 < 1 or yPos < 1 or yPos > ySize then return end
if not _HOST then text = safeString(text) end
if not term.isColour() then
fgCol = safeCol(fgCol, "0")
bgCol = safeCol(bgCol, "f")
end
if xPos < 1 then
text = text:sub(2 - xPos)
fgCol = fgCol:sub(2 - xPos)
bgCol = bgCol:sub(2 - xPos)
xPos = 1
end
if xPos + #text - 1 > xSize then
text = text:sub(1, xSize - xPos + 1)
fgCol = fgCol:sub(1, xSize - xPos + 1)
bgCol = bgCol:sub(1, xSize - xPos + 1)
end
for x = 1, #text do
buffer[yPos][xPos + x - 1][1] = text:sub(x, x)
buffer[yPos][xPos + x - 1][2] = fgCol:sub(x, x)
buffer[yPos][xPos + x - 1][3] = bgCol:sub(x, x)
end
xPos = xPos + #text
end
tTerm.write = function(text)
text = tostring(text)
tTerm.blit(text, string.rep(colourNum[tCol], #text), string.rep(colourNum[bCol], #text))
end
tTerm.clearLine = function()
local oldXPos = xPos
xPos = 1
tTerm.write(string.rep(" ", xSize))
xPos = oldXPos
end
tTerm.clear = function()
local oldXPos, oldYPos = xPos, yPos
for y = 1, ySize do
xPos, yPos = 1, y
tTerm.write(string.rep(" ", xSize))
end
xPos, yPos = oldXPos, oldYPos
end
tTerm.setCursorPos = function(x, y)
xPos, yPos = math.floor(x), math.floor(y)
end
tTerm.setTextColour = function(col)
tCol = col
end
tTerm.setTextColor = function(col)
tCol = col
end
tTerm.setBackgroundColour = function(col)
bCol = col
end
tTerm.setBackgroundColor = function(col)
bCol = col
end
tTerm.scroll = function(lines)
if math.abs(lines) < ySize then
local oldXPos, oldYPos = xPos, yPos
for y = 1, ySize do
if y + lines > 0 and y + lines <= ySize then
for x = 1, xSize do
xPos, yPos = x, y
tTerm.blit(buffer[y + lines][x][1], buffer[y + lines][x][2], buffer[y + lines][x][3])
end
else
yPos = y
tTerm.clearLine()
end
end
xPos, yPos = oldXPos, oldYPos
else tTerm.clear() end
end
tTerm.toggleCur = function(newBlink)
curBlink = newBlink
end
tTerm.newInput = function(input)
local oldTC, oldBC, oldX, oldY = tCol, bCol, xPos, yPos
tCol, bCol, xPos, yPos, ySize, input = colours.grey, colours.lightGrey, 1, ySize + 1, ySize + 1, input .. " "
while #curInput + #input + 1 > xSize do curInput = curInput:sub(curInput:find(" ") + 1) end
curInput = curInput .. input .. " "
tTerm.clearLine()
tTerm.write(curInput)
tCol, bCol, xPos, yPos, ySize = oldTC, oldBC, oldX, oldY, ySize - 1
end
tTerm.key = function(key)
tTerm.newInput((not keys.getName(key)) and "unknownKey" or keys.getName(key))
end
tTerm.mouse_click = function(button, x, y)
tTerm.newInput(buttons[button] .. "C@" .. tostring(x) .. "x" .. tostring(y))
end
local image = {["width"] = xSize * charW, ["height"] = ySize * charH}
for i = 1, #calls do
local xMin, yMin, xMax, yMax, oldBuffer, curCalls, changed = xSize + 1, ySize + 1, 0, 0, {}, calls[i], false
calls[i] = nil
for y = 1, ySize do
oldBuffer[y] = {}
for x = 1, xSize do oldBuffer[y][x] = {buffer[y][x][1], buffer[y][x][2], buffer[y][x][3], buffer[y][x][4]} end
end
snooze()
if showInput then ySize = ySize - 1 end
for j = 1, #curCalls do if tTerm[curCalls[j][1]] then tTerm[curCalls[j][1]](unpack(curCalls[j], 2)) end end
if showInput then ySize = ySize + 1 end
if i > 1 then
for yy = 1, ySize do for xx = 1, xSize do if buffer[yy][xx][1] ~= oldBuffer[yy][xx][1] or (buffer[yy][xx][2] ~= oldBuffer[yy][xx][2] and buffer[yy][xx][1] ~= " ") or buffer[yy][xx][3] ~= oldBuffer[yy][xx][3] then
changed = true
if xx < xMin then xMin = xx end
if xx > xMax then xMax = xx end
if yy < yMin then yMin = yy end
if yy > yMax then yMax = yy end
end end end
else xMin, yMin, xMax, yMax, changed = 1, 1, xSize, ySize, true end
if oldBlink and (xPos ~= oldXPos or yPos ~= oldYPos or not curBlink) and oldXPos > 0 and oldYPos > 0 and oldXPos <= xSize and oldYPos <= ySize then
changed = true
if oldXPos < xMin then xMin = oldXPos end
if oldXPos > xMax then xMax = oldXPos end
if oldYPos < yMin then yMin = oldYPos end
if oldYPos > yMax then yMax = oldYPos end
buffer[oldYPos][oldXPos][4] = false
end
if curBlink and (xPos ~= oldXPos or yPos ~= oldYPos or not oldBlink) and xPos > 0 and yPos > 0 and xPos <= xSize and yPos <= ySize then
changed = true
if xPos < xMin then xMin = xPos end
if xPos > xMax then xMax = xPos end
if yPos < yMin then yMin = yPos end
if yPos > yMax then yMax = yPos end
buffer[yPos][xPos][4] = true
end
oldBlink, oldXPos, oldYPos = curBlink, xPos, yPos
local thisFrame = {["xstart"] = (xMin - 1) * charW, ["ystart"] = (yMin - 1) * charH, ["xend"] = (xMax - xMin + 1) * charW, ["yend"] = (yMax - yMin + 1) * charH, ["delay"] = curCalls.delay, ["disposal"] = 1}
for y = 1, (yMax - yMin + 1) * charH do
local row = {}
for x = 1, (xMax - xMin + 1) * charW do row[x] = " " end
thisFrame[y] = row
end
snooze()
for yy = yMin, yMax do
local yBump = (yy - yMin) * charH
for xx = xMin, xMax do if buffer[yy][xx][1] ~= oldBuffer[yy][xx][1] or (buffer[yy][xx][2] ~= oldBuffer[yy][xx][2] and buffer[yy][xx][1] ~= " ") or buffer[yy][xx][3] ~= oldBuffer[yy][xx][3] or buffer[yy][xx][4] ~= oldBuffer[yy][xx][4] or i == 1 then
local thisChar, thisT, thisB, xBump = chars[buffer[yy][xx][1]:byte()], buffer[yy][xx][2], buffer[yy][xx][3], (xx - xMin) * charW
for y = 1, charH do for x = 1, charW do thisFrame[y + yBump][x + xBump] = thisChar[y][x] and thisT or thisB end end
if buffer[yy][xx][4] then
thisT, thisChar = colourNum[tCol], chars[95]
for y = 1, charH do for x = 1, charW do if thisChar[y][x] then thisFrame[y + yBump][x + xBump] = thisT end end end
end
end end
for y = yBump + 1, yBump + charH do
local skip, chars, row = 0, {}, {}
for x = 1, #thisFrame[y] do
if thisFrame[y][x] == " " then
if #chars > 0 then
row[#row + 1] = table.concat(chars)
chars = {}
end
skip = skip + 1
else
if skip > 0 then
row[#row + 1] = skip
skip = 0
end
chars[#chars + 1] = thisFrame[y][x]
end
end
if #chars > 0 then row[#row + 1] = table.concat(chars) end
thisFrame[y] = row
end
snooze()
end
if changed then image[#image + 1] = thisFrame else image[#image].delay = image[#image].delay + curCalls.delay end
end
buffer = nil
GIF.saveGIF(image, filename)
fs.unmount('.recGif')
print("Encode complete")