carousel.love/live.lua

436 lines
16 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
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
-- load files in order
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
--
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
-- on incoming messages to a specific file, the app must:
-- determine the definition name from the first word
-- execute the value, returning any errors
-- look up the filename for the definition or define a new filename for it
-- save the message's value to the filename
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
-- We try to save new definitions in the source directory, but this is not
-- possible if the app lives in a .love file. In that case new definitions
-- go in the save dir.
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 = {}
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
-- === on startup, load all files with numeric prefix
2022-11-27 22:06:11 +00:00
2023-11-17 06:10:03 +00:00
function live.load()
fix initialization errors using driver.love Changes inside on.initialize are minefields. Until now, if you made a mistake when modifying on.initialize, you could end up in a situation where the app would fail irrecoverably on the next startup. You'd have to go dig up a text editor to fix it. After this commit, errors in on.initialize wait for commands from driver.love just like any other error. Recovering from errors during initialization is a little different than normal. I don't know how much of initialization completed successfully, so I redo all of it. I think this should be safe; the sorts of things we want to do on startup tend to be idempotent just like the sorts of things we do within an event loop with our existing error handling. Things are still not ideal. Initialization by definition happens only when the app starts up. When you make changes to it, you won't find out about errors until you restart the app[1], which can be much later and a big context switch. But at least you'll be able to fix it in the usual way. Slightly more seamless[2]. One glitch to note: at least on Linux, an app with an initialization error feels "sticky". I can't seem to switch focus away from it using Alt-tab. Hitting F4 on the driver also jarringly brings the client app back in focus when there was an initialization error. But the mouse does work consistently. This feels similar to the issues I find when an app goes unresponsive sometimes. The window manager really wants me to respond to the dialog that it's unresponsive. Still, feels like an improvement. [1] I really need to provide that driver command to restart the app! But there's no room in the menus! I really need a first-class command palette like pensieve.love has! [2] https://lobste.rs/s/idi1wt/open_source_vs_ux
2023-11-17 16:31:41 +00:00
if Live.frozen_definitions == nil then -- a second run due to initialization errors will contain definitions we don't want to freeze
live.freeze_all_existing_definitions()
end
2023-04-16 17:46:40 +00:00
2022-11-27 22:06:11 +00:00
-- version control
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
Live.filenames_to_load = {} -- filenames in order of numeric prefix
Live.filename = {} -- map from definition name to filename (including numeric prefix)
Live.final_prefix = 0
2022-11-27 22:06:11 +00:00
live.load_files_so_far()
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
-- some hysteresis
Live.previous_read = 0
2022-11-27 22:06:11 +00:00
end
function live.load_files_so_far()
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
for _,filename in ipairs(love.filesystem.getDirectoryItems('')) do
local numeric_prefix, root = filename:match('^(%d+)-(.+)')
if numeric_prefix and tonumber(numeric_prefix) > 0 then -- skip 0000
Live.filename[root] = filename
table.insert(Live.filenames_to_load, filename)
Live.final_prefix = math.max(Live.final_prefix, tonumber(numeric_prefix))
2022-11-27 22:06:11 +00:00
end
end
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
table.sort(Live.filenames_to_load)
-- load files from save dir
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
for _,filename in ipairs(Live.filenames_to_load) do
--? print('loading', filename)
2022-11-28 02:07:10 +00:00
local buf = love.filesystem.read(filename)
assert(buf and buf ~= '')
local _, definition_name = filename:match('^(%d+)-(.+)')
local status, err = live.eval(buf, definition_name)
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
if not status then
error(err)
end
2022-11-28 02:07:10 +00:00
end
2022-11-27 22:06:11 +00:00
end
2023-01-03 02:21:55 +00:00
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
APP = 'fw_app'
2022-11-27 22:06:11 +00:00
2023-04-16 17:42:27 +00:00
-- === on each frame, check for messages and alter the app as needed
2022-11-27 22:06:11 +00:00
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
local possibly_mutated = live.run(buf)
if possibly_mutated then
Mode = 'run'
fix initialization errors using driver.love Changes inside on.initialize are minefields. Until now, if you made a mistake when modifying on.initialize, you could end up in a situation where the app would fail irrecoverably on the next startup. You'd have to go dig up a text editor to fix it. After this commit, errors in on.initialize wait for commands from driver.love just like any other error. Recovering from errors during initialization is a little different than normal. I don't know how much of initialization completed successfully, so I redo all of it. I think this should be safe; the sorts of things we want to do on startup tend to be idempotent just like the sorts of things we do within an event loop with our existing error handling. Things are still not ideal. Initialization by definition happens only when the app starts up. When you make changes to it, you won't find out about errors until you restart the app[1], which can be much later and a big context switch. But at least you'll be able to fix it in the usual way. Slightly more seamless[2]. One glitch to note: at least on Linux, an app with an initialization error feels "sticky". I can't seem to switch focus away from it using Alt-tab. Hitting F4 on the driver also jarringly brings the client app back in focus when there was an initialization error. But the mouse does work consistently. This feels similar to the issues I find when an app goes unresponsive sometimes. The window manager really wants me to respond to the dialog that it's unresponsive. Still, feels like an improvement. [1] I really need to provide that driver command to restart the app! But there's no room in the menus! I really need a first-class command palette like pensieve.love has! [2] https://lobste.rs/s/idi1wt/open_source_vs_ux
2023-11-17 16:31:41 +00:00
if Redo_initialization then
Redo_initialization = nil
love.run() -- won't actually replace the event loop;
-- we're just running it for its initialization side-effects
end
end
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_escape(--[[bold]]1, --[[blue]]4))
2022-11-27 22:06:11 +00:00
print(result)
print(reset_terminal())
2023-02-05 05:19:05 +00:00
os.remove(love.filesystem.getAppdataDirectory()..'/_love_akkartik_driver_app')
2022-11-27 22:06:11 +00:00
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_escape(0, --[[green]]2))
2022-11-27 22:06:11 +00:00
print(msg)
print(reset_terminal())
end
2023-01-08 03:11:50 +00:00
function live.send_run_time_error_to_driver(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_escape(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_escape(format, color)
2022-11-27 22:06:11 +00:00
return ('\027[%d;%dm'):format(format, 30+color)
end
function reset_terminal()
return '\027[m'
end
-- returns true if we might have mutated the app, by either creating or deleting a definition
2022-11-27 22:06:11 +00:00
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 == 'RESTART' then
restart()
2022-11-27 22:06:11 +00:00
elseif cmd == 'MANIFEST' then
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
Live.filename[APP] = love.filesystem.getIdentity()
live.send_to_driver(json.encode(Live.filename))
2022-11-27 22:06:11 +00:00
elseif cmd == 'DELETE' then
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
local definition_name = buf:match('^%s*%S+%s+(%S+)')
2023-05-01 05:04:01 +00:00
if Live.frozen_definitions[definition_name] then
live.send_to_driver('ERROR definition '..definition_name..' is part of Freewheeling infrastructure and cannot be deleted.')
return
end
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
if Live.filename[definition_name] then
local index = table.find(Live.filenames_to_load, Live.filename[definition_name])
table.remove(Live.filenames_to_load, index)
live.eval(definition_name..' = nil', 'driver') -- ignore errors which will likely be from keywords like `function = nil`
-- try to remove the file from both source_dir and save_dir
-- this won't work for files inside .love files
nativefs.remove(App.source_dir..Live.filename[definition_name])
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
love.filesystem.remove(Live.filename[definition_name])
Live.filename[definition_name] = nil
end
live.send_to_driver('{}')
return true
2022-11-27 22:06:11 +00:00
elseif cmd == 'GET' then
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
local definition_name = buf:match('^%s*%S+%s+(%S+)')
2023-04-10 05:31:12 +00:00
local val, _ = live.get_binding(definition_name)
if val then
live.send_to_driver(val)
else
live.send_to_driver('ERROR no such value')
end
elseif cmd == 'GET*' then
-- batch version of GET
local result = {}
2023-04-10 05:31:12 +00:00
for definition_name in buf:gmatch('%s+(%S+)') do
print(definition_name)
local val, _ = live.get_binding(definition_name)
2023-01-08 04:48:22 +00:00
if val then
table.insert(result, val)
end
end
local delimiter = '\n==fw: definition boundary==\n'
live.send_to_driver(table.concat(result, delimiter)..delimiter) -- send a final delimiter to simplify the driver's task
elseif cmd == 'DEFAULT_MAP' then
local contents = love.filesystem.read('default_map')
if contents == nil then contents = '{}' end
live.send_to_driver(contents)
2022-11-27 22:06:11 +00:00
-- other commands go here
else
local definition_name = live.get_definition_name_from_buffer(buf)
2023-07-24 07:40:34 +00:00
if definition_name == nil then
-- contents are all Lua comments; we don't currently have a plan for them
live.send_to_driver('ERROR empty definition')
return
end
print('definition name is '..definition_name)
2023-04-10 05:31:12 +00:00
if Live.frozen_definitions[definition_name] then
live.send_to_driver('ERROR definition '..definition_name..' is part of Freewheeling infrastructure and cannot be safely edited live.')
return
end
local status, err = live.eval(buf, definition_name)
2022-11-27 22:06:11 +00:00
if not status then
-- throw an error
live.send_to_driver('ERROR '..cleaned_up_frame(tostring(err)))
return
2022-11-27 22:06:11 +00:00
end
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
-- eval succeeded without errors; persist the definition
local filename = Live.filename[definition_name]
if filename == nil then
Live.final_prefix = Live.final_prefix+1
filename = ('%04d-%s'):format(Live.final_prefix, definition_name)
table.insert(Live.filenames_to_load, filename)
Live.filename[definition_name] = filename
end
-- try to write to source dir
local status, err = nativefs.write(App.source_dir..filename, buf)
if err then
-- fall back on save dir
local status, err2 = love.filesystem.write(filename, buf)
if err2 then
-- throw an error
live.send_to_driver('ERROR '..tostring(err..'\n\n'..err2))
return true
end
end
-- run all tests
Test_errors = {}
2023-01-24 04:12:23 +00:00
App.run_tests(record_error_by_test)
live.send_to_driver(json.encode(Test_errors))
return true
end
end
2022-12-26 08:27:24 +00:00
function live.get_cmd_from_buffer(buf)
-- return the first word
return buf:match('^%s*(%S+)')
end
function live.get_definition_name_from_buffer(buf)
return first_noncomment_word(buf)
end
-- return the first word (separated by whitespace) that's not in a Lua comment
-- or empty string if there's nothing
-- ignore strings; we don't expect them to be the first word in a program
function first_noncomment_word(str)
local pos = 1
while pos <= #str do -- not Unicode-aware; hopefully it doesn't need to be
if str:sub(pos,pos) == '-' then
-- skip any comments
if str:sub(pos+1,pos+1) == '-' then
-- definitely start of a comment
local long_comment_header = str:match('^%[=*%[', pos+2)
if long_comment_header then
-- long comment
local long_comment_trailer = long_comment_header:gsub('%[', ']')
pos = str:find(long_comment_trailer, pos, --[[plain]]true)
if pos == nil then return '' end -- incomplete comment; no first word
pos = pos + #long_comment_trailer
else
-- line comment
pos = str:find('\n', pos)
if pos == nil then return '' end -- incomplete comment; no first word
end
end
end
-- any non-whitespace that's not a comment is the first word
if str:sub(pos,pos):match('%s') then
pos = pos+1
else
return str:match('^%S*', pos)
end
end
return ''
end
function test_first_noncomment_word()
check_eq(first_noncomment_word(''), '', 'empty string')
check_eq(first_noncomment_word('abc'), 'abc', 'single word')
check_eq(first_noncomment_word('abc def'), 'abc', 'stop at space')
check_eq(first_noncomment_word('abc\tdef'), 'abc', 'stop at tab')
check_eq(first_noncomment_word('abc\ndef'), 'abc', 'stop at newline')
check_eq(first_noncomment_word('-- abc\ndef'), 'def', 'ignore line comment')
check_eq(first_noncomment_word('--[[abc]] def'), 'def', 'ignore block comment')
check_eq(first_noncomment_word('--[[abc\n]] def'), 'def', 'ignore multi-line block comment')
check_eq(first_noncomment_word('--[[abc\n--]] def'), 'def', 'ignore comment leader before block comment trailer')
check_eq(first_noncomment_word('--[=[abc]=] def'), 'def', 'ignore long comment')
check_eq(first_noncomment_word('--[=[abc]] def ]=] ghi'), 'ghi', 'ignore long comment containing block comment trailer')
check_eq(first_noncomment_word('--[===[abc\n\ndef ghi\njkl]===]mno\npqr'), 'mno', 'ignore long comment containing block comment trailer')
check_eq(first_noncomment_word('-'), '-', 'incomplete comment token')
check_eq(first_noncomment_word('--abc'), '', 'incomplete line comment')
check_eq(first_noncomment_word('--abc\n'), '', 'just a line comment')
check_eq(first_noncomment_word('--abc\n '), '', 'just a line comment 2')
check_eq(first_noncomment_word('--[ab\n'), '', 'incomplete block comment token is a line comment')
check_eq(first_noncomment_word('--[[ab'), '', 'incomplete block comment')
check_eq(first_noncomment_word('--[[ab\n]'), '', 'incomplete block comment 2')
check_eq(first_noncomment_word('--[=[ab\n]] ]='), '', 'incomplete block comment 3')
check_eq(first_noncomment_word('--[=[ab\n]] ]=]'), '', 'just a block comment')
check_eq(first_noncomment_word('--[=[ab\n]] ]=] \n \n '), '', 'just a block comment 2')
2022-12-26 08:27:24 +00:00
end
2022-11-27 22:06:11 +00:00
function live.get_binding(name)
new file-system format for freewheeling apps 1. No more version history, now we have just the contents of the current version. 2. Editing a definition no longer changes the order in which definitions load. This should make repos easier to browse, and more amenable to modify. You don't need driver.love anymore. And a stable order eliminates some gotchas. For example: using driver.love, define `Foo = 3` in a definition define `Bar = Foo + 1` edit and redefine `Foo = 4` Before this commit, you'd get an error when you restart the app. Definitions used to be loaded in version order, and editing a definition would move it to the end of the load order, potentially after definitions using it. I mostly avoided this by keeping top-level definitions independent. It's fine to refer to any definition inside a function body, we only need to be careful with initializers for global variables which run immediately while loading. After this commit you can still end up in a weird state if you modify a definition that other later definitions use. In the above example, you will now see Foo = 4 and Bar = 4. But when you restart, Foo = 4 and Bar = 5. But that's no more confusing than Emacs's C-x C-e. It's still a good idea to keep top-level definitions order-independent. It's just confusing in a similar way to existing tools if you fail to do so. And your tools won't tend to break as badly. Why did I ever do my weird version history thing? I think it's my deep aversion to risking losing any data entered. (Even though the app currently will seem to lose data in those situations. You'd need to leave your tools to find the data.) Now I rely on driver.love's undo to avoid data loss, but once you shut it down you're stuck with what you have on disk. Or in git. I also wasn't aware for a long time of any primitives for deleting files. This might have colored my choices a lot.
2023-04-16 18:15:03 +00:00
if Live.filename[name] then
return love.filesystem.read(Live.filename[name])
2022-11-27 22:06:11 +00:00
end
end
2023-04-18 04:10:17 +00:00
function table.find(h, x)
for k,v in pairs(h) do
if v == x then
return k
end
end
end
2022-11-27 22:06:11 +00:00
-- 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.
-- filename will show up in call stacks for any error messages
2022-11-27 22:06:11 +00:00
-- return values:
-- all well -> true, ...
-- load failed -> nil, error message
2023-04-15 17:12:32 +00:00
-- run (pcall) failed -> false, error message
function live.eval(buf, filename)
2022-11-27 22:06:11 +00:00
-- 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.
2023-11-10 02:03:29 +00:00
local f = load('return '..buf, filename or 'REPL')
2022-11-27 22:06:11 +00:00
if f then
return pcall(f)
end
local f, err = load(buf, filename or 'REPL')
2022-11-27 22:06:11 +00:00
if f then
return pcall(f)
else
return nil, err
end
end
2023-04-16 17:42:27 +00:00
-- === infrastructure for performing safety checks on any new definition
2023-04-15 17:14:42 +00:00
-- Everything that exists before we start loading the live files is frozen and
-- can't be edited live.
function live.freeze_all_existing_definitions()
Live.frozen_definitions = {on=true} -- special case for version 1
local done = {}
done[Live.frozen_definitions]=true
live.freeze_all_existing_definitions_in(_G, {}, done)
end
function live.freeze_all_existing_definitions_in(tab, scopes, done)
-- track duplicates to avoid cycles like _G._G, _G._G._G, etc.
if done[tab] then return end
done[tab] = true
for name,binding in pairs(tab) do
local full_name = live.full_name(scopes, name)
--? print(full_name)
Live.frozen_definitions[full_name] = true
if type(binding) == 'table' and full_name ~= 'package' then -- var 'package' contains copies of all modules, but not the best name; rely on people to not modify package.loaded.io.open, etc.
table.insert(scopes, name)
live.freeze_all_existing_definitions_in(binding, scopes, done)
table.remove(scopes)
end
end
end
function live.full_name(scopes, name)
local ns = table.concat(scopes, '.')
if #ns == 0 then return name end
return ns..'.'..name
end
-- === on error, pause the app and wait for messages
2022-11-27 22:06:11 +00:00
-- return nil to continue the event loop, non-nil to quit
function live.handle_error(err)
love.graphics.setCanvas() -- undo any canvas we happened to be within, otherwise LÖVE seizes up
Mode = 'error'
local callstack = debug.traceback('', --[[stack frame]]2)
local cleaned_up_error = 'Error: ' .. cleaned_up_frame(tostring(err))..'\n'..cleaned_up_callstack(callstack)
live.send_run_time_error_to_driver(cleaned_up_error)
Error_message = 'Something is wrong. Sorry!\n\n'..cleaned_up_error..'\n\n'..
"(Note: function names above don't include outer tables. So functions like on.draw might show up as just 'draw', etc.)\n\n"..
'Options:\n'..
'- press "ctrl+c" (without the quotes) to copy this message to your clipboard to send to me: ak@akkartik.com\n'..
'- press any other key to retry, see if things start working again\n'..
'- run driver.love to try to fix it yourself. As you do, feel free to ask me questions: ak@akkartik.com\n'
Error_count = Error_count+1
if Error_count > 1 then
Error_message = Error_message..('\n\nThis is error #%d in this session; things will probably not improve in this session. Please copy the message and send it to me: ak@akkartik.com.'):format(Error_count)
2022-11-27 22:06:11 +00:00
end
print(Error_message)
2022-11-27 22:06:11 +00:00
end
fix initialization errors using driver.love Changes inside on.initialize are minefields. Until now, if you made a mistake when modifying on.initialize, you could end up in a situation where the app would fail irrecoverably on the next startup. You'd have to go dig up a text editor to fix it. After this commit, errors in on.initialize wait for commands from driver.love just like any other error. Recovering from errors during initialization is a little different than normal. I don't know how much of initialization completed successfully, so I redo all of it. I think this should be safe; the sorts of things we want to do on startup tend to be idempotent just like the sorts of things we do within an event loop with our existing error handling. Things are still not ideal. Initialization by definition happens only when the app starts up. When you make changes to it, you won't find out about errors until you restart the app[1], which can be much later and a big context switch. But at least you'll be able to fix it in the usual way. Slightly more seamless[2]. One glitch to note: at least on Linux, an app with an initialization error feels "sticky". I can't seem to switch focus away from it using Alt-tab. Hitting F4 on the driver also jarringly brings the client app back in focus when there was an initialization error. But the mouse does work consistently. This feels similar to the issues I find when an app goes unresponsive sometimes. The window manager really wants me to respond to the dialog that it's unresponsive. Still, feels like an improvement. [1] I really need to provide that driver command to restart the app! But there's no room in the menus! I really need a first-class command palette like pensieve.love has! [2] https://lobste.rs/s/idi1wt/open_source_vs_ux
2023-11-17 16:31:41 +00:00
function live.handle_initialization_error(err)
Redo_initialization = true
live.handle_error(err)
end
-- I tend to read code from files myself (say using love.filesystem calls)
-- rather than offload that to load().
-- Functions compiled in this manner have ugly filenames of the form [string "filename"]
-- This function cleans out this cruft from error callstacks.
-- It also strips out the numeric prefixes we introduce in filenames.
function cleaned_up_callstack(callstack)
local frames = {}
for frame in string.gmatch(callstack, '[^\n]+\n*') do
table.insert(frames, cleaned_up_frame(frame))
end
-- the initial "stack traceback:" line was unindented and remains so
return table.concat(frames, '\n\t')
end
function cleaned_up_frame(frame)
local line = frame:gsub('^%s*(.-)\n?$', '%1')
local filename, rest = line:match('([^:]*):(.*)')
return cleaned_up_filename(filename)..':'..rest
end
function cleaned_up_filename(filename)
-- pass through frames that don't match this format
-- this includes the initial line "stack traceback:"
local core_filename = filename:match('^%[string "(.*)"%]$')
if core_filename == nil then return filename end
-- strip out the numeric prefixes we introduce in filenames
local _, core_filename2 = core_filename:match('^(%d+)-(.+)')
return core_filename2 or core_filename
end