carousel.love/live.lua

315 lines
9.8 KiB
Lua
Raw Normal View History

2022-11-27 22:06:11 +00:00
-- A general architecture for free-wheeling, live programs:
-- on startup:
-- scan both the app directory and the save directory for files with numeric prefixes
2022-12-26 08:27:24 +00:00
-- from the numeric prefix in file 'head', obtain a manifest
-- load all files (which must start with a numeric prefix) from the manifest
2022-11-27 22:06:11 +00:00
--
-- then start drawing frames on screen and reacting to events
--
-- events from keyboard and mouse are handled as the app desires
--
-- on incoming messages to a specific file, however, the app must:
2022-12-26 08:27:24 +00:00
-- save the message's value to a new, smallest unused numeric prefix
2022-11-27 22:06:11 +00:00
-- execute the value
2022-12-26 08:27:24 +00:00
-- if there's an error, go back to the previous value of the same
-- definition if one exists
2022-11-27 22:06:11 +00:00
--
2022-12-26 08:27:24 +00:00
-- if a game encounters a run-time error, send it to the driver and await
-- further instructions. The app will go unresponsive in the meantime, that
-- is expected. To shut it down cleanly, type C-q in the driver.
2022-11-27 22:06:11 +00:00
-- namespace for these functions
live = {}
-- state for these functions
Live = {}
-- a namespace of frameworky callbacks
-- these will be modified live
on = {}
-- ========= on startup, load the version at head
function live.initialize(arg)
-- version control
Live.head = 0
Live.next_version = 1
Live.history = {} -- array of filename roots corresponding to each numeric prefix
Live.manifest = {} -- mapping from roots to numeric prefixes as of version Live.head
2022-11-27 22:06:11 +00:00
live.load_files_so_far()
Live.previous_read = 0
2022-11-27 22:06:11 +00:00
if on.load then on.load() end
end
function live.load_files_so_far()
print('new edits will go to ' .. love.filesystem.getSaveDirectory())
local files = {}
live.append_files_with_numeric_prefix('', files)
table.sort(files)
live.check_integrity(files)
live.append_files_with_numeric_prefix(love.filesystem.getSaveDirectory(), files)
table.sort(files)
live.check_integrity(files)
Live.history = live.load_history(files)
Live.next_version = #Live.history + 1
2022-11-27 22:06:11 +00:00
local head_string = love.filesystem.read('head')
Live.head = tonumber(head_string)
if Live.head > 0 then
Live.manifest = json.decode(love.filesystem.read(live.versioned_manifest(Live.head)))
2022-11-27 22:06:11 +00:00
end
live.load_everything_in_manifest()
end
function live.append_files_with_numeric_prefix(dir, files)
for _,file in ipairs(love.filesystem.getDirectoryItems(dir)) do
2022-12-01 03:26:46 +00:00
if file:match('^%d') then
table.insert(files, file)
end
2022-11-27 22:06:11 +00:00
end
end
function live.check_integrity(files)
local manifest_found, file_found = false, false
local expected_index = 1
for _,file in ipairs(files) do
for numeric_prefix, root in file:gmatch('(%d+)-(.+)') do
-- only runs once
local index = tonumber(numeric_prefix)
-- skip files without numeric prefixes
if index ~= nil then
if index < expected_index then
print(index, expected_index)
end
assert(index >= expected_index)
if index > expected_index then
assert(index == expected_index+1)
assert(manifest_found and file_found)
expected_index = index
manifest_found, file_found = false, false
end
assert(index == expected_index)
if root == 'manifest' then
assert(not manifest_found)
manifest_found = true
else
assert(not file_found)
file_found = true
end
end
end
end
end
function live.load_history(files)
local result = {}
for _,file in ipairs(files) do
for numeric_prefix, root in file:gmatch('(%d+)-(.+)') do
-- only runs once
local index = tonumber(numeric_prefix)
-- skip
if index ~= nil then
if root ~= 'manifest' then
assert(index == #result+1)
table.insert(result, root)
end
end
end
end
return result
end
function live.load_everything_in_manifest()
2022-11-28 02:07:10 +00:00
local files_to_load = {}
for k,v in pairs(Live.manifest) do
2022-11-27 22:06:11 +00:00
if k ~= 'parent' then
local root, index = k, v
local filename = live.versioned_filename(index, root)
2022-11-28 02:07:10 +00:00
table.insert(files_to_load, filename)
2022-11-27 22:06:11 +00:00
end
end
2022-11-28 02:07:10 +00:00
table.sort(files_to_load)
for _,filename in ipairs(files_to_load) do
local buf = love.filesystem.read(filename)
assert(buf and buf ~= '')
live.eval(buf)
end
2022-11-27 22:06:11 +00:00
end
function live.versioned_filename(index, root)
return ('%04d-%s'):format(index, root)
end
function live.versioned_manifest(index)
return ('%04d-manifest'):format(index)
end
-- ========= on each frame, check for messages and alter the app as needed
function live.update(dt)
if Current_time - Live.previous_read > 0.1 then
2022-12-26 08:27:24 +00:00
local buf = live.receive_from_driver()
2022-11-27 22:06:11 +00:00
if buf then
live.run(buf)
2022-12-24 04:30:35 +00:00
if on.code_change then on.code_change() end
2022-11-27 22:06:11 +00:00
end
Live.previous_read = Current_time
2022-11-27 22:06:11 +00:00
end
end
-- look for a message from outside, and return nil if there's nothing
2022-12-26 08:27:24 +00:00
function live.receive_from_driver()
local f = io.open(love.filesystem.getAppdataDirectory()..'/_love_akkartik_driver_app')
2022-11-27 22:06:11 +00:00
if f == nil then return nil end
local result = f:read('*a')
f:close()
if result == '' then return nil end -- empty file == no message
print('<='..color(--[[bold]]1, --[[blue]]4))
print(result)
print(reset_terminal())
-- we can't unlink files, so just clear them
local clear = io.open(love.filesystem.getAppdataDirectory()..'/_love_akkartik_driver_app', 'w')
2022-11-27 22:06:11 +00:00
clear:close()
return result
end
2022-12-26 08:27:24 +00:00
function live.send_to_driver(msg)
local f = io.open(love.filesystem.getAppdataDirectory()..'/_love_akkartik_app_driver', 'w')
2022-11-27 22:06:11 +00:00
if f == nil then return end
f:write(msg)
f:close()
print('=>'..color(0, --[[green]]2))
print(msg)
print(reset_terminal())
end
function live.send_run_time_error(msg)
local f = io.open(love.filesystem.getAppdataDirectory()..'/_love_akkartik_app_driver_run_time_error', 'w')
if f == nil then return end
f:write(msg)
f:close()
print('=>'..color(0, --[[red]]1))
print(msg)
print(reset_terminal())
end
2022-11-27 22:06:11 +00:00
-- args:
-- format: 0 for normal, 1 for bold
-- color: 0-15
function color(format, color)
return ('\027[%d;%dm'):format(format, 30+color)
end
function reset_terminal()
return '\027[m'
end
-- define or undefine top-level bindings
function live.run(buf)
2022-12-26 08:27:24 +00:00
local cmd = live.get_cmd_from_buffer(buf)
2022-11-27 22:06:11 +00:00
assert(cmd)
print('command is '..cmd)
if cmd == 'QUIT' then
love.event.quit(1)
elseif cmd == 'MANIFEST' then
2022-12-26 08:27:24 +00:00
live.send_to_driver(json.encode(Live.manifest))
2022-11-27 22:06:11 +00:00
elseif cmd == 'DELETE' then
local binding = buf:match('^%S+%s+(%S+)')
Live.manifest[binding] = nil
2022-11-27 22:06:11 +00:00
live.eval(binding..' = nil')
local next_filename = live.versioned_filename(Live.next_version, binding)
2022-11-27 22:06:11 +00:00
love.filesystem.write(next_filename, '')
table.insert(Live.history, binding)
Live.manifest.parent = Live.head
local manifest_filename = live.versioned_manifest(Live.next_version)
love.filesystem.write(manifest_filename, json.encode(Live.manifest))
Live.head = Live.next_version
love.filesystem.write('head', tostring(Live.head))
Live.next_version = Live.next_version + 1
2022-11-27 22:06:11 +00:00
elseif cmd == 'GET' then
local binding = buf:match('^%S+%s+(%S+)')
2022-12-26 08:27:24 +00:00
live.send_to_driver(live.get_binding(binding))
2022-11-27 22:06:11 +00:00
-- other commands go here
else
local binding = cmd
local next_filename = live.versioned_filename(Live.next_version, binding)
2022-11-27 22:06:11 +00:00
love.filesystem.write(next_filename, buf)
table.insert(Live.history, binding)
Live.manifest[binding] = Live.next_version
Live.manifest.parent = Live.head
local manifest_filename = live.versioned_manifest(Live.next_version)
love.filesystem.write(manifest_filename, json.encode(Live.manifest))
Live.head = Live.next_version
love.filesystem.write('head', tostring(Live.head))
Live.next_version = Live.next_version + 1
2022-11-27 22:06:11 +00:00
local status, err = live.eval(buf)
if not status then
-- roll back
Live.head = Live.manifest.parent
local previous_manifest_filename = live.versioned_manifest(Live.head)
Live.manifest = json.decode(love.filesystem.read(previous_manifest_filename))
2022-11-27 22:06:11 +00:00
-- throw an error
2022-12-26 08:27:24 +00:00
live.send_to_driver('ERROR '..tostring(err))
2022-11-27 22:06:11 +00:00
end
2022-12-26 08:27:24 +00:00
live.send_to_driver('ok')
2022-11-27 22:06:11 +00:00
end
end
2022-12-26 08:27:24 +00:00
function live.get_cmd_from_buffer(buf)
return buf:match('^%s*(%S+)')
end
2022-11-27 22:06:11 +00:00
function live.get_binding(name)
if Live.manifest[name] then
return love.filesystem.read(live.versioned_filename(Live.manifest[name], name))
2022-11-27 22:06:11 +00:00
end
end
-- Wrapper for Lua's weird evaluation model.
-- Lua is persnickety about expressions vs statements, so we need to do some
-- extra work to get the result of an evaluation.
-- return values:
-- all well -> true, ...
-- load failed -> nil, error message
-- run failed -> false, error message
function live.eval(buf)
-- We assume a program is either correct with 'return' prefixed xor not.
-- Is this correct? Who knows! But the Lua REPL does this as well.
local f = load('return '..buf, 'REPL')
if f then
return pcall(f)
end
local f, err = load(buf, 'REPL')
if f then
return pcall(f)
else
return nil, err
end
end
-- ========= on error, pause the app and wait for messages
-- return nil to continue the event loop, non-nil to quit
function live.handle_error(err)
local msg = tostring(err)
-- draw a pause indicator on screen
love.graphics.setColor(1,0,0)
love.graphics.rectangle('fill', 10,10, 3,10)
love.graphics.rectangle('fill', 16,10, 3,10)
love.graphics.present()
-- print stack trace here just in case we ran the app through a terminal
local stack_trace = debug.traceback('Error: ' .. tostring(msg), --[[stack frame]]2):gsub('\n[^\n]+$', '')
print(stack_trace)
print('Look in the driver for options to investigate further.')
print("(You probably can't close the app window at this point. If you don't have the driver set up, you might need to force-quit.)")
-- send stack trace to driver and wait for a response
2022-12-26 08:27:24 +00:00
live.send_run_time_error_to_driver(stack_trace)
2022-11-27 22:06:11 +00:00
local buf
repeat
2022-12-26 08:27:24 +00:00
buf = live.receive_from_driver()
2022-11-27 22:06:11 +00:00
until buf
if buf == 'QUIT' then
return true
end
live.run(buf)
end