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
|
|
|
|
-- from the largest numeric prefix found, obtain a manifest
|
|
|
|
-- load all files (which must start with a numeric prefix) from the manifest)
|
|
|
|
--
|
|
|
|
-- 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:
|
|
|
|
-- save the message's value to a specific, unused numeric prefix
|
|
|
|
-- execute the value
|
|
|
|
--
|
|
|
|
-- if a game encounters an error:
|
|
|
|
-- find the previous version of the definition in the 'head' numeric prefix
|
|
|
|
-- decrement 'head'
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
live.load_files_so_far()
|
2022-11-27 22:27:04 +00:00
|
|
|
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
|
|
|
|
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)))
|
|
|
|
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 = {}
|
2022-11-27 22:06:11 +00:00
|
|
|
for k,v in pairs(Live.Manifest) do
|
|
|
|
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
|
|
|
|
local buf = live.receive()
|
|
|
|
if buf then
|
|
|
|
live.run(buf)
|
|
|
|
end
|
|
|
|
Live.Previous_read = Current_time
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- look for a message from outside, and return nil if there's nothing
|
|
|
|
function live.receive()
|
2022-12-01 03:25:34 +00:00
|
|
|
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
|
2022-12-01 03:25:34 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
function live.send(msg)
|
2022-12-01 03:25:34 +00:00
|
|
|
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
|
|
|
|
|
2022-11-27 22:27:04 +00:00
|
|
|
function live.send_run_time_error(msg)
|
2022-12-01 03:25:34 +00:00
|
|
|
local f = io.open(love.filesystem.getAppdataDirectory()..'/_love_akkartik_app_driver_run_time_error', 'w')
|
2022-11-27 22:27:04 +00:00
|
|
|
if f == nil then return end
|
|
|
|
f:write(msg)
|
|
|
|
f:close()
|
2022-11-28 02:19:58 +00:00
|
|
|
print('=>'..color(0, --[[red]]1))
|
2022-11-27 22:27:04 +00:00
|
|
|
print(msg)
|
2022-11-28 02:19:58 +00:00
|
|
|
print(reset_terminal())
|
2022-11-27 22:27:04 +00:00
|
|
|
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)
|
|
|
|
local cmd = buf:match('^%S+')
|
|
|
|
assert(cmd)
|
|
|
|
print('command is '..cmd)
|
|
|
|
if cmd == 'QUIT' then
|
|
|
|
love.event.quit(1)
|
|
|
|
elseif cmd == 'MANIFEST' then
|
|
|
|
live.send(json.encode(Live.Manifest))
|
|
|
|
elseif cmd == 'DELETE' then
|
|
|
|
local binding = buf:match('^%S+%s+(%S+)')
|
|
|
|
Live.Manifest[binding] = nil
|
|
|
|
live.eval(binding..' = nil')
|
|
|
|
local next_filename = live.versioned_filename(Live.Next_version, binding)
|
|
|
|
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
|
|
|
|
elseif cmd == 'GET' then
|
|
|
|
local binding = buf:match('^%S+%s+(%S+)')
|
|
|
|
live.send(live.get_binding(binding))
|
|
|
|
-- other commands go here
|
|
|
|
else
|
|
|
|
local binding = cmd
|
|
|
|
local next_filename = live.versioned_filename(Live.Next_version, binding)
|
|
|
|
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
|
|
|
|
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))
|
|
|
|
-- throw an error
|
|
|
|
live.send('ERROR '..tostring(err))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function live.get_binding(name)
|
|
|
|
if Live.Manifest[name] then
|
|
|
|
return love.filesystem.read(live.versioned_filename(Live.Manifest[name], name))
|
|
|
|
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-11-27 22:27:04 +00:00
|
|
|
live.send_run_time_error(stack_trace)
|
2022-11-27 22:06:11 +00:00
|
|
|
local buf
|
|
|
|
repeat
|
|
|
|
buf = live.receive()
|
|
|
|
until buf
|
|
|
|
if buf == 'QUIT' then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
live.run(buf)
|
|
|
|
end
|