new live app

[?]
Nov 14, 2022, 1:12 AM
TNRO6KLZXIZUFWKCXSWAJHN2CMHS56ATGGULOKMJC2YNCFRJZKLAC

Dependencies

Change contents

  • add root
    [1.0]
    [0.1]
  • file addition: main.lua (----------)
    [0.2]
    -- 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'
    json = require 'json'
    -- a namespace of "frameworky" things in addition to love.*
    -- these can't be modified
    app = {}
    -- a namespace of frameworky callbacks
    -- these can be modified live
    on = {}
    -- ========= on startup, load the version at head
    function love.load(args, commandline)
    -- version control
    app.Head = 0
    app.Next_version = 1
    app.History = {} -- array of filename roots corresponding to each numeric prefix
    app.Manifest = {} -- mapping from roots to numeric prefixes as of version app.Head
    app.load_files_so_far()
    -- hysteresis in a few places
    app.Current_time = 0
    app.Previous_read = 0
    app.Last_focus_time = 0
    app.Last_resize_time = 0
    love.window.setTitle('app')
    if on.load then on.load() end
    end
    function app.load_files_so_far()
    print('new edits will go to ' .. love.filesystem.getSaveDirectory())
    local files = {}
    app.append_files_with_numeric_prefix('', files)
    table.sort(files)
    app.check_integrity(files)
    app.append_files_with_numeric_prefix(love.filesystem.getSaveDirectory(), files)
    table.sort(files)
    app.check_integrity(files)
    app.History = app.load_history(files)
    app.Next_version = #app.History + 1
    local head_string = love.filesystem.read('head')
    app.Head = tonumber(head_string)
    if app.Head > 0 then
    app.Manifest = json.decode(love.filesystem.read(app.versioned_manifest(app.Head)))
    end
    app.load_everything_in_manifest()
    end
    function app.append_files_with_numeric_prefix(dir, files)
    for _,file in ipairs(love.filesystem.getDirectoryItems(dir)) do
    table.insert(files, file)
    end
    end
    function app.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 app.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 app.load_everything_in_manifest()
    for k,v in pairs(app.Manifest) do
    if k ~= 'parent' then
    local root, index = k, v
    local filename = app.versioned_filename(index, root)
    local buf = love.filesystem.read(filename)
    assert(buf and buf ~= '')
    app.eval(buf)
    end
    end
    end
    function app.versioned_filename(index, root)
    return ('%04d-%s'):format(index, root)
    end
    function app.versioned_manifest(index)
    return ('%04d-manifest'):format(index)
    end
    -- ========= on each frame, check for messages and alter the app as needed
    function love.update(dt)
    app.Current_time = app.Current_time + dt
    if app.Current_time < app.Last_resize_time + 0.1 then
    return
    end
    if app.Current_time - app.Previous_read > 0.1 then
    local buf = app.receive()
    if buf then
    app.run(buf)
    end
    app.Previous_read = app.Current_time
    end
    if on.update then on.update(dt) end
    end
    -- look for a message from outside, and return nil if there's nothing
    function app.receive()
    local f = io.open(love.filesystem.getUserDirectory()..'/_love_akkartik_driver_app')
    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('<='..app.color(--[[bold]]1, --[[blue]]4))
    print(result)
    print(app.reset_terminal())
    -- we can't unlink files, so just clear them
    local clear = io.open(love.filesystem.getUserDirectory()..'/_love_akkartik_driver_app', 'w')
    clear:close()
    return result
    end
    function app.send(msg)
    local f = io.open(love.filesystem.getUserDirectory()..'/_love_akkartik_app_driver', 'w')
    if f == nil then return end
    f:write(msg)
    f:close()
    print('=>'..app.color(0, --[[green]]2))
    print(msg)
    print(app.reset_terminal())
    end
    -- args:
    -- format: 0 for normal, 1 for bold
    -- color: 0-15
    function app.color(format, color)
    return ('\027[%d;%dm'):format(format, 30+color)
    end
    function app.reset_terminal()
    return '\027[m'
    end
    -- define or undefine top-level bindings
    function app.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
    app.send(json.encode(app.Manifest))
    elseif cmd == 'DELETE' then
    local binding = buf:match('^%S+%s+(%S+)')
    app.Manifest[binding] = nil
    app.eval(binding..' = nil')
    local next_filename = app.versioned_filename(app.Next_version, binding)
    love.filesystem.write(next_filename, '')
    table.insert(app.History, binding)
    app.Manifest.parent = app.Head
    local manifest_filename = app.versioned_manifest(app.Next_version)
    love.filesystem.write(manifest_filename, json.encode(app.Manifest))
    app.Head = app.Next_version
    love.filesystem.write('head', tostring(app.Head))
    app.Next_version = app.Next_version + 1
    elseif cmd == 'GET' then
    local binding = buf:match('^%S+%s+(%S+)')
    app.send(app.get_binding(binding))
    -- other commands go here
    else
    local binding = cmd
    local next_filename = app.versioned_filename(app.Next_version, binding)
    love.filesystem.write(next_filename, buf)
    table.insert(app.History, binding)
    app.Manifest[binding] = app.Next_version
    app.Manifest.parent = app.Head
    local manifest_filename = app.versioned_manifest(app.Next_version)
    love.filesystem.write(manifest_filename, json.encode(app.Manifest))
    app.Head = app.Next_version
    love.filesystem.write('head', tostring(app.Head))
    app.Next_version = app.Next_version + 1
    local status, err = app.eval(buf)
    if not status then
    -- roll back
    app.Head = app.Manifest.parent
    local previous_manifest_filename = app.versioned_manifest(app.Head)
    app.Manifest = json.decode(love.filesystem.read(previous_manifest_filename))
    -- throw an error
    app.send('ERROR '..tostring(err))
    end
    end
    end
    function app.get_binding(name)
    if app.Manifest[name] then
    return love.filesystem.read(app.versioned_filename(app.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 app.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
    -- all other love callbacks can just be delegated to the on.* namespace
    -- just easier to remember
    -- how to make these discoverable?
    function love.draw()
    if on.draw then on.draw() end
    end
    function love.quit()
    if on.quit then on.quit() end
    end
    function love.resize(w,h)
    screen = {w=w, h=h}
    if on.resize then on.resize(w,h) end
    app.Last_resize_time = app.Current_time
    end
    function love.filedropped()
    if on.filedropped then on.filedropped() end
    end
    function love.focus(in_focus)
    if in_focus then
    app.Last_focus_time = app.Current_time
    end
    if on.focus then on.focus(in_focus) end
    end
    function love.keypressed(key, scancode, isrepeat)
    -- ignore events for some time after window in focus (mostly alt-tab)
    if app.Current_time < app.Last_focus_time + 0.01 then
    return
    end
    if on.keypressed then on.keypressed(key, scancode, isrepeat) end
    end
    function love.textinput(t)
    -- ignore events for some time after window in focus (mostly alt-tab)
    if app.Current_time < app.Last_focus_time + 0.01 then
    return
    end
    if on.textinput then on.textinput(t) end
    end
    function love.keyreleased(key, scancode, isrepeat)
    if on.keyreleased then on.keyreleased(key, scancode, isrepeat) end
    end
    function love.mousepressed(x,y, mouse_button)
    if on.mousepressed then on.mousepressed(x,y, mouse_button) end
    end
    function love.mousereleased(x,y, mouse_button)
    if on.mousereleased then on.mousereleased(x,y, mouse_button) end
    end
    -- ========= on error, pause the app and wait for messages
    function love.run()
    love.load(love.arg.parseGameArguments(arg), arg)
    love.timer.step()
    local dt = 0
    return function()
    local status, result = xpcall(app.try_run, app.handle_error)
    return result
    end
    end
    -- one iteration of the event loop
    -- return nil to continue the event loop, non-nil to quit
    -- from https://love2d.org/wiki/love.run
    function app.try_run()
    if love.event then
    love.event.pump()
    for name, a,b,c,d,e,f in love.event.poll() do
    if name == 'quit' then
    if not love.quit() then
    return a or 0
    end
    end
    love.handlers[name](a,b,c,d,e,f)
    end
    end
    -- update
    dt = love.timer.step()
    love.update(dt)
    -- draw before update to give it a chance to mutate state
    love.graphics.origin()
    love.graphics.clear(love.graphics.getBackgroundColor())
    love.draw()
    love.graphics.present()
    love.timer.sleep(0.001)
    -- returning nil continues the loop
    end
    -- return nil to continue the event loop, non-nil to quit
    function app.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
    app.send('ERROR '..stack_trace)
    local buf
    repeat
    buf = app.receive()
    until buf
    if buf == 'QUIT' then
    return true
    end
    app.run(buf)
    end
    -- some abbreviations
    graphics = love.graphics
    color = graphics.setColor
    line = graphics.line
    rect = graphics.rectangle
    circ = graphics.circle
    ellipse = graphics.ellipse
    -- also screen is set in resize above (which seems to also be called during initialization)
  • file addition: json.lua (----------)
    [0.2]
    --
    -- https://github.com/rxi/json.lua
    --
    -- Copyright (c) 2020 rxi
    --
    -- Permission is hereby granted, free of charge, to any person obtaining a copy of
    -- this software and associated documentation files (the "Software"), to deal in
    -- the Software without restriction, including without limitation the rights to
    -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
    -- of the Software, and to permit persons to whom the Software is furnished to do
    -- so, subject to the following conditions:
    --
    -- The above copyright notice and this permission notice shall be included in all
    -- copies or substantial portions of the Software.
    --
    -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -- SOFTWARE.
    --
    local json = { _version = "0.1.2" }
    -------------------------------------------------------------------------------
    -- Encode
    -------------------------------------------------------------------------------
    local encode
    local escape_char_map = {
    [ "\\" ] = "\\",
    [ "\"" ] = "\"",
    [ "\b" ] = "b",
    [ "\f" ] = "f",
    [ "\n" ] = "n",
    [ "\r" ] = "r",
    [ "\t" ] = "t",
    }
    local escape_char_map_inv = { [ "/" ] = "/" }
    for k, v in pairs(escape_char_map) do
    escape_char_map_inv[v] = k
    end
    local function escape_char(c)
    return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
    end
    local function encode_nil(val)
    return "null"
    end
    local function encode_table(val, stack)
    local res = {}
    stack = stack or {}
    -- Circular reference?
    if stack[val] then error("circular reference") end
    stack[val] = true
    if rawget(val, 1) ~= nil or next(val) == nil then
    -- Treat as array -- check keys are valid and it is not sparse
    local n = 0
    for k in pairs(val) do
    if type(k) ~= "number" then
    error("invalid table: mixed or invalid key types")
    end
    n = n + 1
    end
    if n ~= #val then
    error("invalid table: sparse array")
    end
    -- Encode
    for i, v in ipairs(val) do
    table.insert(res, encode(v, stack))
    end
    stack[val] = nil
    return "[" .. table.concat(res, ",") .. "]"
    else
    -- Treat as an object
    for k, v in pairs(val) do
    if type(k) ~= "string" then
    error("invalid table: mixed or invalid key types")
    end
    table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
    end
    stack[val] = nil
    return "{" .. table.concat(res, ",") .. "}"
    end
    end
    local function encode_string(val)
    return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
    end
    local function encode_number(val)
    -- Check for NaN, -inf and inf
    if val ~= val or val <= -math.huge or val >= math.huge then
    error("unexpected number value '" .. tostring(val) .. "'")
    end
    return string.format("%.14g", val)
    end
    local type_func_map = {
    [ "nil" ] = encode_nil,
    [ "table" ] = encode_table,
    [ "string" ] = encode_string,
    [ "number" ] = encode_number,
    [ "boolean" ] = tostring,
    }
    encode = function(val, stack)
    local t = type(val)
    local f = type_func_map[t]
    if f then
    return f(val, stack)
    end
    error("unexpected type '" .. t .. "'")
    end
    function json.encode(val)
    return ( encode(val) )
    end
    -------------------------------------------------------------------------------
    -- Decode
    -------------------------------------------------------------------------------
    local parse
    local function create_set(...)
    local res = {}
    for i = 1, select("#", ...) do
    res[ select(i, ...) ] = true
    end
    return res
    end
    local space_chars = create_set(" ", "\t", "\r", "\n")
    local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
    local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
    local literals = create_set("true", "false", "null")
    local literal_map = {
    [ "true" ] = true,
    [ "false" ] = false,
    [ "null" ] = nil,
    }
    local function next_char(str, idx, set, negate)
    for i = idx, #str do
    if set[str:sub(i, i)] ~= negate then
    return i
    end
    end
    return #str + 1
    end
    local function decode_error(str, idx, msg)
    local line_count = 1
    local col_count = 1
    for i = 1, idx - 1 do
    col_count = col_count + 1
    if str:sub(i, i) == "\n" then
    line_count = line_count + 1
    col_count = 1
    end
    end
    error( string.format("%s at line %d col %d", msg, line_count, col_count) )
    end
    local function codepoint_to_utf8(n)
    -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
    local f = math.floor
    if n <= 0x7f then
    return string.char(n)
    elseif n <= 0x7ff then
    return string.char(f(n / 64) + 192, n % 64 + 128)
    elseif n <= 0xffff then
    return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
    elseif n <= 0x10ffff then
    return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
    f(n % 4096 / 64) + 128, n % 64 + 128)
    end
    error( string.format("invalid unicode codepoint '%x'", n) )
    end
    local function parse_unicode_escape(s)
    local n1 = tonumber( s:sub(1, 4), 16 )
    local n2 = tonumber( s:sub(7, 10), 16 )
    -- Surrogate pair?
    if n2 then
    return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
    else
    return codepoint_to_utf8(n1)
    end
    end
    local function parse_string(str, i)
    local res = ""
    local j = i + 1
    local k = j
    while j <= #str do
    local x = str:byte(j)
    if x < 32 then
    decode_error(str, j, "control character in string")
    elseif x == 92 then -- `\`: Escape
    res = res .. str:sub(k, j - 1)
    j = j + 1
    local c = str:sub(j, j)
    if c == "u" then
    local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
    or str:match("^%x%x%x%x", j + 1)
    or decode_error(str, j - 1, "invalid unicode escape in string")
    res = res .. parse_unicode_escape(hex)
    j = j + #hex
    else
    if not escape_chars[c] then
    decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
    end
    res = res .. escape_char_map_inv[c]
    end
    k = j + 1
    elseif x == 34 then -- `"`: End of string
    res = res .. str:sub(k, j - 1)
    return res, j + 1
    end
    j = j + 1
    end
    decode_error(str, i, "expected closing quote for string")
    end
    local function parse_number(str, i)
    local x = next_char(str, i, delim_chars)
    local s = str:sub(i, x - 1)
    local n = tonumber(s)
    if not n then
    decode_error(str, i, "invalid number '" .. s .. "'")
    end
    return n, x
    end
    local function parse_literal(str, i)
    local x = next_char(str, i, delim_chars)
    local word = str:sub(i, x - 1)
    if not literals[word] then
    decode_error(str, i, "invalid literal '" .. word .. "'")
    end
    return literal_map[word], x
    end
    local function parse_array(str, i)
    local res = {}
    local n = 1
    i = i + 1
    while 1 do
    local x
    i = next_char(str, i, space_chars, true)
    -- Empty / end of array?
    if str:sub(i, i) == "]" then
    i = i + 1
    break
    end
    -- Read token
    x, i = parse(str, i)
    res[n] = x
    n = n + 1
    -- Next token
    i = next_char(str, i, space_chars, true)
    local chr = str:sub(i, i)
    i = i + 1
    if chr == "]" then break end
    if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
    end
    return res, i
    end
    local function parse_object(str, i)
    local res = {}
    i = i + 1
    while 1 do
    local key, val
    i = next_char(str, i, space_chars, true)
    -- Empty / end of object?
    if str:sub(i, i) == "}" then
    i = i + 1
    break
    end
    -- Read key
    if str:sub(i, i) ~= '"' then
    decode_error(str, i, "expected string for key")
    end
    key, i = parse(str, i)
    -- Read ':' delimiter
    i = next_char(str, i, space_chars, true)
    if str:sub(i, i) ~= ":" then
    decode_error(str, i, "expected ':' after key")
    end
    i = next_char(str, i + 1, space_chars, true)
    -- Read value
    val, i = parse(str, i)
    -- Set
    res[key] = val
    -- Next token
    i = next_char(str, i, space_chars, true)
    local chr = str:sub(i, i)
    i = i + 1
    if chr == "}" then break end
    if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
    end
    return res, i
    end
    local char_func_map = {
    [ '"' ] = parse_string,
    [ "0" ] = parse_number,
    [ "1" ] = parse_number,
    [ "2" ] = parse_number,
    [ "3" ] = parse_number,
    [ "4" ] = parse_number,
    [ "5" ] = parse_number,
    [ "6" ] = parse_number,
    [ "7" ] = parse_number,
    [ "8" ] = parse_number,
    [ "9" ] = parse_number,
    [ "-" ] = parse_number,
    [ "t" ] = parse_literal,
    [ "f" ] = parse_literal,
    [ "n" ] = parse_literal,
    [ "[" ] = parse_array,
    [ "{" ] = parse_object,
    }
    parse = function(str, idx)
    local chr = str:sub(idx, idx)
    local f = char_func_map[chr]
    if f then
    return f(str, idx)
    end
    decode_error(str, idx, "unexpected character '" .. chr .. "'")
    end
    function json.decode(str)
    if type(str) ~= "string" then
    error("expected argument of type string, got " .. type(str))
    end
    local res, idx = parse(str, next_char(str, 1, space_chars, true))
    idx = next_char(str, idx, space_chars, true)
    if idx <= #str then
    decode_error(str, idx, "trailing garbage")
    end
    return res
    end
    return json
  • file addition: head (----------)
    [0.2]
    0
  • file addition: conf.lua (----------)
    [0.2]
    function love.conf(t)
    t.window.resizable = true
    end