diff --git a/0000-freewheeling-start b/0000-freewheeling-start new file mode 100644 index 0000000..0761829 --- /dev/null +++ b/0000-freewheeling-start @@ -0,0 +1,2 @@ +This file contains no definition, but is used as a marker in the save dir to +indicate all definitions have been copied from the repo to the save dir. diff --git a/0001-fwmanifest b/0001-fwmanifest deleted file mode 100644 index 23a5f80..0000000 --- a/0001-fwmanifest +++ /dev/null @@ -1 +0,0 @@ -{"fw_parent":0,"on":1} diff --git a/README.md b/README.md index f92ae4c..bf46b82 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ This repo is a template you can copy to create your own live apps that juggle text editor widgets. The editors support copy/paste, search, infinite undo, etc. You can't modify editor functionality live (yet?). -[More information about the on-disk representation of freewheeling apps.](representation.md) - This repo is a fork of [lines.love](http://akkartik.name/lines.html), an editor for plain text where you can also seamlessly insert line drawings. Designed above all to be easy to modify and give you early warning if your @@ -21,12 +19,18 @@ extremely well-behaved. I'll assume below that you can invoke it using the Run this app from the terminal, [passing its directory to LÖVE](https://love2d.org/wiki/Getting_Started#Running_Games) +## Hacking + To modify it live without restarting the app each time, download [the driver app](https://git.sr.ht/~akkartik/driver.love). Here's an example session using a fork of this repo: ![making changes without restarting the app](assets/2.gif) +To publish your changes: + * delete all files with a numeric prefix from the repo, and then + * move all files with a numeric prefix from the save dir to the repo + ## Keyboard shortcuts Up to you! But within the included editor widget if you use it: diff --git a/head b/head deleted file mode 100644 index d00491f..0000000 --- a/head +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/live.lua b/live.lua index 29124a1..fd856f3 100644 --- a/live.lua +++ b/live.lua @@ -1,18 +1,17 @@ -- 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 numeric prefix in file 'head', obtain a manifest --- load all files (which must start with a numeric prefix) from the manifest +-- load files in order -- -- 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 new, smallest unused numeric prefix --- execute the value --- if there's an error, go back to the previous value of the same --- definition if one exists +-- 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 -- -- 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 @@ -27,17 +26,18 @@ Live = {} -- these will be modified live on = {} --- === on startup, load the version at head +-- === on startup, load all files with numeric prefix function live.initialize(arg) live.freeze_all_existing_definitions() -- 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.filenames_to_load = {} -- filenames in order of numeric prefix + Live.filename = {} -- map from definition name to filename (including numeric prefix) + Live.final_prefix = 0 live.load_files_so_far() + + -- some hysteresis Live.previous_read = 0 if on.load then on.load() end @@ -45,114 +45,48 @@ 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 - if file:match('^%d') then - table.insert(files, file) - end - 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 == 'fwmanifest' then - assert(not manifest_found) - manifest_found = true - else - assert(not file_found) - file_found = true - end + -- if necessary, copy files from repo to save dir + if io.open(love.filesystem.getSaveDirectory()..'/0000-freewheeling-start') == nil then + print('copying all definitions from repo to save dir') + for _,filename in ipairs(love.filesystem.getDirectoryItems('')) do + for numeric_prefix, root in filename:gmatch('(%d+)-(.+)') do + -- only runs once + local buf = love.filesystem.read(filename) + print('copying', filename) + love.filesystem.write(filename, buf) 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 + -- load files from save dir + for _,filename in ipairs(love.filesystem.getDirectoryItems('')) do + for numeric_prefix, root in filename:gmatch('(%d+)-(.+)') do -- only runs once - local index = tonumber(numeric_prefix) - -- skip - if index ~= nil then - if root ~= 'fwmanifest' then - assert(index == #result+1) - table.insert(result, root) - end + if 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)) end end end - return result + table.sort(Live.filenames_to_load) + live.load_all(files) end -function live.load_everything_in_manifest() - local files_to_load = {} - for k,v in pairs(Live.manifest) do - -- Most keys in the manifest are definitions. If we need to store any - -- metadata we'll do it in keys starting with a specific prefix. - if not starts_with(k, 'fw_') then - local root, index = k, v - local filename = live.versioned_filename(index, root) - table.insert(files_to_load, filename) - end - end - table.sort(files_to_load) - for _,filename in ipairs(files_to_load) do +function live.load_all() + for _,filename in ipairs(Live.filenames_to_load) do +--? print('loading', filename) local buf = love.filesystem.read(filename) assert(buf and buf ~= '') local status, err = live.eval(buf) - if status == nil then - error(('error loading %s from manifest: %s'):format(filename, err)) + if not status then + return err end end end -PARENT = 'fw_parent' + APP = 'fw_app' -function live.versioned_filename(index, root) - return ('%04d-%s'):format(index, root) -end - -function live.versioned_manifest(index) - return ('%04d-fwmanifest'):format(index) -end - -- === on each frame, check for messages and alter the app as needed function live.update(dt) @@ -221,18 +155,19 @@ function live.run(buf) elseif cmd == 'RESTART' then restart() elseif cmd == 'MANIFEST' then - Live.manifest[APP] = love.filesystem.getIdentity() -- doesn't need to be persisted, but no harm if it does.. - live.send_to_driver(json.encode(Live.manifest)) + Live.filename[APP] = love.filesystem.getIdentity() + live.send_to_driver(json.encode(Live.filename)) elseif cmd == 'DELETE' then - local definition_name = buf:match('^%S+%s+(%S+)') - Live.manifest[definition_name] = nil - live.eval(definition_name..' = nil') -- ignore errors which will likely be from keywords like `function = nil` - local next_filename = live.versioned_filename(Live.next_version, definition_name) - love.filesystem.write(next_filename, '') - table.insert(Live.history, definition_name) - live.roll_forward() + local definition_name = buf:match('^%s*%S+%s+(%S+)') + 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') -- ignore errors which will likely be from keywords like `function = nil` + love.filesystem.remove(Live.filename[definition_name]) + Live.filename[definition_name] = nil + end elseif cmd == 'GET' then - local definition_name = buf:match('^%S+%s+(%S+)') + local definition_name = buf:match('^%s*%S+%s+(%S+)') local val, _ = live.get_binding(definition_name) if val then live.send_to_driver(val) @@ -262,18 +197,21 @@ function live.run(buf) live.send_to_driver('ERROR definition '..definition_name..' is part of Freewheeling infrastructure and cannot be safely edited live.') return end - local next_filename = live.versioned_filename(Live.next_version, definition_name) - love.filesystem.write(next_filename, buf) - table.insert(Live.history, definition_name) - Live.manifest[definition_name] = Live.next_version - live.roll_forward() local status, err = live.eval(buf) if not status then - live.roll_back() -- throw an error live.send_to_driver('ERROR '..tostring(err)) return end + -- 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 + love.filesystem.write(filename, buf) -- run all tests Test_errors = {} App.run_tests(record_error_by_test) @@ -281,31 +219,13 @@ function live.run(buf) end end --- update Live.Head and record the new Live.Manifest (which caller has already modified) -function live.roll_forward() - 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 -end - --- update app.Head and reload app.Manifest appropriately -function live.roll_back() - Live.head = Live.manifest[PARENT] - love.filesystem.write('head', tostring(Live.head)) - local previous_manifest_filename = live.versioned_manifest(Live.head) - Live.manifest = json.decode(love.filesystem.read(previous_manifest_filename)) -end - function live.get_cmd_from_buffer(buf) return buf:match('^%s*(%S+)') end function live.get_binding(name) - if Live.manifest[name] then - return love.filesystem.read(live.versioned_filename(Live.manifest[name], name)) + if Live.filename[name] then + return love.filesystem.read(Live.filename[name]) end end diff --git a/representation.md b/representation.md deleted file mode 100644 index edec3ba..0000000 --- a/representation.md +++ /dev/null @@ -1,74 +0,0 @@ -# The on-disk representation of freewheeling apps - -When you start up a freewheeling app, you'll see a directory printed out in -the parent terminal (always launch it from a terminal window): - -``` -new edits will go to /home/... -``` - -When editing such an app using the driver (see [README.md](README.md)), new -definitions will go into this directory. Let's call it `$SAVE_DIR` in the rest -of this doc. - -It is always safe to move such definitions into this repo. (We'll call it `.` -in the rest of this doc.) You'll want to do this if you're sharing them with -others, and it's also helpful if the driver crashes on your app. Moving -definitions will never, ever change app behavior. - -```sh -$ mv -i $SAVE_DIR/[0-9]* . # should never clobber any existing files -$ mv $SAVE_DIR/head . # expected to clobber the existing file -``` - -Try looking inside the `head` file with a text editor. It'll contain a number, -the current version of the _manifest_ for this app. For example: - -``` -478 -``` - -This means the current state of the app is in a file called `0478-fwmanifest`. -If you moved the files you should see such a file in `.`. If you open this -file, you'll see a JSON table from definition names to version ids. For -example: - -``` -{ "a": 273, "b": 478} -``` - -This means the current definition of `a` is in `0273-a` and of `b` in -`0478-b`. - -Poking around these files gets repetitive, so there's a tool to streamline -things: - -``` -lua tools/stitch-live.lua 0478-fwmanifest -``` - -`stitch-live.lua` takes a manifest file as its argument, and prints out all -the definitions that make up the app at that version. - -To compare two versions of the app, use `stitch-live.lua` to copy the -definitions in each into a separate file, and use a file comparison tool (e.g. -`diff`) to compare the two files. - -# Scenarios considered in designing this representation - -* Capture history of changes. - - Though it is perhaps too fine-grained and noisy. - -* Merge changes from non-live forks to live ones. - - New files in repo can't hide changes in save dir, because filenames are - always disjoint between the two. - - This doesn't apply yet to live updates. Two forks of a single live app - will likely have unnecessary merge conflicts. - -* No special tools required to publish changes to others. - - Just move files from save dir to repo. - -# Scenarios I _would_ like to take into consideration in the future - -* Cleaner commits; it's clear what changed. -* merge changes between live forks. diff --git a/tools/stitch-live.lua b/tools/stitch-live.lua deleted file mode 100644 index 7ce916f..0000000 --- a/tools/stitch-live.lua +++ /dev/null @@ -1,37 +0,0 @@ -json = require 'json' - -function main(args) - local infile = io.open(args[1]) - local manifest_s = infile:read('*a') - infile:close() - local manifest = json.decode(manifest_s) - local core_filenames = {} - for k,v in pairs(manifest) do - if not starts_with(k, 'fw_') then - table.insert(core_filenames, k) - end - end - table.sort(core_filenames) - for _,core in ipairs(core_filenames) do - local filename = ('%04d'):format(manifest[core])..'-'..core - local f = io.open(filename) - if f then - print(f:read('*a')) - print('') - end - end -end - -function starts_with(s, prefix) - if #s < #prefix then - return false - end - for i=1,#prefix do - if s:sub(i,i) ~= prefix:sub(i,i) then - return false - end - end - return true -end - -main(arg)