From 0eef58cbb1fe20da04610b80287a9cafff31b4e7 Mon Sep 17 00:00:00 2001 From: "Daniel A. Nagy" Date: Sat, 13 Sep 2025 09:16:43 +0200 Subject: [PATCH 001/103] Introduction to Compy The intended audience are beta testers. --- doc/intro.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 doc/intro.md diff --git a/doc/intro.md b/doc/intro.md new file mode 100644 index 00000000..140987fb --- /dev/null +++ b/doc/intro.md @@ -0,0 +1,53 @@ +# Introduction to Compy + +Compy is a 7'' portable educational computer with an interactive development +environment for the [love2d](https://love2d.org) framework. It runs on top of +Android operating system and uses the Lua programming language for the command +line, scripting, configuration and programming. The primary interface is the +command console, with the standard input displayed at the bottom of the screen, +with real-time syntax highlingting (when appropriate). The standard output is +displayed on a terminal occupying the rest of the screen, which also doubles as +a graphical canvas. Thus, commands with both text-based or graphical output can +be entered. Below are some invariant principles of operations: + +* Pointing devices (such as mice, touch pad or touch screen) might be used, but + are not necessary for the successful operation of Compy. +* Apart from syntax highlighting, anything entered in the console + input will only take effect upon pressing the **Enter** key. +* If the input is syntactically invalid at the time of pressing **Enter**, the + user can continue editing, correcting the mistake. +* Users cannot render compy unusable from the command line. Compy can always + be restored to its initial state by inserting a clean _SD card_ or formatting + the currently inserted one. Example projects are written onto the SD card by + entering `example_projects()` on the command line. + +## Persistence + +All data persisted by the user is saved on the included _SD card_. It is +organized into named _projects_, with no file system hieararchy. Technically, +each project is a directory under `Documents/compy/projects/` on the SD card. +Inside these directories, there is no further directory structure. + +Projects can be selected or, in the absence of one, created by entering +`project("...")` in the console, with the project's name between the double +quotes. Running the project means executing a file called "`main.lua`" in its +directory. It can be done by entering `run()` in the console for the currently +selected project or `run("...")` for a different project, with the name of the +project between the double quotes. + +Text files, including Lua sources can be edited using the built-in text editor +that can be started by entering `edit("...")` in the console, with the name of +the file between the quotes. In its absence, `main.lua` will be edited. + +No changes to the edited file will occur unless the user presses **Enter** _and_ +the text in the edit area at the bottom of the screen is syntactically correct. +Pressing **Enter** takes immediate effect on the SD card, there is no need to +"save" the edited file separately. + +If there is no highlighted section in the edited file, the text inputed in the +console will be appended to the end of the file. If there is a bright white +highlight, the entered text is inserted before it. If there is a bright yellow +highlight, the entered text replaces it. + +For more information, please see the documentation of the editor and the +console. From 529ccd32c134e6cac0d10eb46d785e7894e7d8d9 Mon Sep 17 00:00:00 2001 From: "Daniel A. Nagy" Date: Sat, 13 Sep 2025 09:18:05 +0200 Subject: [PATCH 002/103] link to editor documentation --- doc/intro.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/intro.md b/doc/intro.md index 140987fb..26c404be 100644 --- a/doc/intro.md +++ b/doc/intro.md @@ -49,5 +49,5 @@ console will be appended to the end of the file. If there is a bright white highlight, the entered text is inserted before it. If there is a bright yellow highlight, the entered text replaces it. -For more information, please see the documentation of the editor and the -console. +For more information, please see the documentation of the [editor](EDITOR.md) +and the console. From 06d433156158d220a442ed4adfa4f9ad831bc4ef Mon Sep 17 00:00:00 2001 From: "Daniel A. Nagy" Date: Sat, 13 Sep 2025 09:19:42 +0200 Subject: [PATCH 003/103] Link to command line documentation --- doc/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/intro.md b/doc/intro.md index 26c404be..5dc5d240 100644 --- a/doc/intro.md +++ b/doc/intro.md @@ -50,4 +50,4 @@ highlight, the entered text is inserted before it. If there is a bright yellow highlight, the entered text replaces it. For more information, please see the documentation of the [editor](EDITOR.md) -and the console. +and the [console](../README.md). From e683c9ac9bade89aa8e77f62831c33ba220e8adb Mon Sep 17 00:00:00 2001 From: "Daniel A. Nagy" Date: Mon, 17 Nov 2025 12:38:53 +0100 Subject: [PATCH 004/103] Remove runfile() Made redundant by properly working `require` --- src/controller/consoleController.lua | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 8cb2f51c..a560c8c1 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -310,18 +310,6 @@ function ConsoleController.prepare_env(cc) end) end - --- @param name string - --- @return any - prepared.runfile = function(name) - local code = check_open_pr(cc._readfile, cc, name) - local chunk, err = load(code, '', 't') - if chunk then - chunk() - else - print(err) - end - end - --- @param name string prepared.edit = function(name) return check_open_pr(cc.edit, cc, name) From e25e94ee441b399d073fb86eaad3a482d2114eb0 Mon Sep 17 00:00:00 2001 From: "Daniel A. Nagy" Date: Mon, 17 Nov 2025 12:46:32 +0100 Subject: [PATCH 005/103] Update README.md to reflect removal of `runfile()` --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 08e56e21..e9573df6 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,6 @@ contents. Write to _file_ the text supplied as the _content_ parameter. This can be either a string, or an array of strings. -- `runfile(file)` - - Run _file_ if it's a lua script. - ## Editor If a project is open, the files inside can be edited or new ones From 19f6655e3381e62be5709047ec9a8d751708af3d Mon Sep 17 00:00:00 2001 From: aldum Date: Mon, 29 Sep 2025 13:01:21 +0200 Subject: [PATCH 006/103] fix: only pass timestamp to userinput in debug mode --- src/controller/controller.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index b90cb14e..ef6edc17 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -376,7 +376,11 @@ Controller = { end local user_input = get_user_input() if user_input then - user_input.V:draw(user_input.C:get_input(), C.time) + if love.DEBUG then + user_input.V:draw(user_input.C:get_input(), C.time) + else + user_input.V:draw(user_input.C:get_input()) + end end end View.prev_draw = draw From 784ada061ebad7b1b5f322b17403388e84e8af21 Mon Sep 17 00:00:00 2001 From: aldum Date: Mon, 29 Sep 2025 21:54:07 +0200 Subject: [PATCH 007/103] chore(ci): bump android action --- .github/workflows/package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 18bfbe0d..43a18306 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -242,7 +242,7 @@ jobs: VER: ${{ github.ref_name }} run: echo VER_CODE="$(echo $VERSION | sed -e 's/^v//' -e 's/\.//g')" >> $GITHUB_ENV - name: Package for android - uses: compy-toys/love-actions-android@v0.2.5 + uses: compy-toys/love-actions-android@v0.2.6 with: love-ref: "compy" no-soft-keyboard: "enabled" From 238aeea041f062add7bde089e230881cd5f2024d Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 30 Sep 2025 16:14:15 +0200 Subject: [PATCH 008/103] fix(tixy): reset time on each load --- src/examples/tixy/main.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/examples/tixy/main.lua b/src/examples/tixy/main.lua index b89da9b8..a49c676f 100644 --- a/src/examples/tixy/main.lua +++ b/src/examples/tixy/main.lua @@ -44,7 +44,6 @@ function advance() load_example(e) if ex_idx < #examples then ex_idx = ex_idx + 1 - time = 0 end end @@ -53,7 +52,6 @@ function retreat() local e = examples[ex_idx] load_example(e) ex_idx = ex_idx - 1 - time = 0 end end @@ -96,6 +94,7 @@ function setupTixy() local f = loadstring(code) if f then setfenv(f, _G) + time = 0 tixy = f() end end From b40ba54ddd1cfaddb64c69dfa686d10fd393cc60 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 30 Sep 2025 16:23:44 +0200 Subject: [PATCH 009/103] fix: project require not returning value --- src/controller/consoleController.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index a560c8c1..dc546a4a 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -194,7 +194,7 @@ local function project_require(cc, name) local pr_env = cc:get_project_env() if chunk then setfenv(chunk, pr_env) - chunk() + return chunk() else --- hack around love.js not having the bit lib if name == 'bit' and _G.web then From df966f7d18344f040a6912834993f6b24d6cfe6f Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 3 Oct 2025 09:17:04 +0200 Subject: [PATCH 010/103] chore(ci): fix versioninfo in initial build --- .github/workflows/package.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 43a18306..3abe56da 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -58,6 +58,11 @@ jobs: submodules: "recursive" fetch-depth: 0 fetch-tags: true + - name: install just + uses: extractions/setup-just@v2 + - name: Add versioninfo + run: | + just version - name: Build bare love package uses: love-actions/love-actions-core@v1 with: From 8e5c920a7bf1764bd1ffd911471e5b86ad8009a5 Mon Sep 17 00:00:00 2001 From: aldum Date: Mon, 6 Oct 2025 15:23:14 +0200 Subject: [PATCH 011/103] feat(debug): nontable warning --- src/util/debug.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/util/debug.lua b/src/util/debug.lua index 352af995..8e83f1cc 100644 --- a/src/util/debug.lua +++ b/src/util/debug.lua @@ -122,6 +122,10 @@ local function terse_hash(t, level, prev_seen, jsonify) return res end +local function nontable(t) + return '(not a table) ' .. tostring(t) +end + --- @param a table? --- @param skip integer? local function terse_array(a, skip) @@ -142,7 +146,7 @@ local function terse_array(a, skip) return res else - return '' + return nontable(a) end end @@ -154,7 +158,9 @@ end --- @param style dumpstyle? --- @return string local function terse_ast(ast, skip_lineinfo, style) - if type(ast) ~= 'table' then return '' end + if type(ast) ~= 'table' then + return nontable(ast) + end local style = style or 'json5' --- @param t table? From 169499eec633c042ab29688d57d3a0fc6c0f436f Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 12:01:00 +0200 Subject: [PATCH 012/103] refactor: rename G to gfx --- src/controller/consoleController.lua | 10 +- src/controller/controller.lua | 10 +- src/examples/clock/README.md | 16 +-- src/examples/clock/main.lua | 14 +-- src/examples/life/README.md | 8 +- src/examples/life/main.lua | 22 ++-- src/examples/paint/README.md | 22 ++-- src/examples/paint/main.lua | 164 ++++++++++++------------- src/examples/sine/main.lua | 20 +-- src/examples/tixy/README.md | 6 +- src/examples/tixy/main.lua | 24 ++-- src/examples/turtle/README.md | 38 +++--- src/examples/turtle/drawing.lua | 74 +++++------ src/examples/turtle/main.lua | 5 +- src/harmony/init.lua | 12 +- src/main.lua | 14 +-- src/model/canvasModel.lua | 10 +- src/util/view.lua | 46 +++---- src/view/canvas/bgView.lua | 6 +- src/view/canvas/canvasView.lua | 40 +++--- src/view/canvas/terminalView.lua | 40 +++--- src/view/consoleView.lua | 10 +- src/view/editor/bufferView.lua | 46 +++---- src/view/editor/search/resultsView.lua | 32 ++--- src/view/input/statusline.lua | 82 ++++++------- src/view/input/userInputView.lua | 26 ++-- src/view/titleView.lua | 22 ++-- src/view/view.lua | 10 +- tests/editor/buffer_spec.lua | 18 +-- tests/interpreter/analyzer_inputs.lua | 12 +- 30 files changed, 430 insertions(+), 429 deletions(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index dc546a4a..8b4afb7c 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -88,11 +88,11 @@ end --- @return boolean success --- @return string? errmsg local function run_user_code(f, cc, project_path) - local G = love.graphics + local gfx = love.graphics local output = cc.model.output local env = cc:get_base_env() - G.setCanvas(cc:get_canvas()) + gfx.setCanvas(cc:get_canvas()) local ok, call_err if project_path then env = cc:get_project_env() @@ -102,7 +102,7 @@ local function run_user_code(f, cc, project_path) cc.main_ctrl.set_user_handlers(env['love']) end output:restore_main() - G.setCanvas() + gfx.setCanvas() if not ok then local msg = LANG.get_call_error(call_err) return false, msg @@ -209,7 +209,7 @@ end function ConsoleController.prepare_env(cc) local prepared = cc.main_env - prepared.G = love.graphics + prepared.gfx = love.graphics local P = cc.model.projects @@ -343,7 +343,7 @@ function ConsoleController.prepare_project_env(cc) local cfg = cc.model.cfg ---@type table local project_env = cc:get_pre_env_c() - project_env.G = love.graphics + project_env.gfx = love.graphics project_env.require = function(name) return project_require(cc, name) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index ef6edc17..de71ac6e 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -420,11 +420,11 @@ Controller = { View.prev_draw = love.draw View.main_draw = love.draw View.end_draw = function() - local w, h = G.getDimensions() - G.setColor(Color[Color.white]) - G.setFont(C.cfg.view.font) - G.clear() - G.printf(messages.exit_anykey, 0, h / 3, w, "center") + local w, h = gfx.getDimensions() + gfx.setColor(Color[Color.white]) + gfx.setFont(C.cfg.view.font) + gfx.clear() + gfx.printf(messages.exit_anykey, 0, h / 3, w, "center") end end, diff --git a/src/examples/clock/README.md b/src/examples/clock/README.md index 0ab14f54..33397073 100644 --- a/src/examples/clock/README.md +++ b/src/examples/clock/README.md @@ -6,19 +6,19 @@ In this example, we explore how to properly create a program with it's own drawi We will be taking over the screen drawing for this simple game. Similar to `update()`, we can override the `love.draw()` function and have the LOVE2D framework handle displaying the content we wish. -Drawing generally follows a simple procedure: set up some values, such as what foreground and background color to use, then build up our desired image using basic elements. These are called graphics "primitives", and we can access them from the `love.graphics` table (aliased here as `G`). +Drawing generally follows a simple procedure: set up some values, such as what foreground and background color to use, then build up our desired image using basic elements. These are called graphics "primitives", and we can access them from the `love.graphics` table (aliased here as `gfx`). So, our example clock: ```lua function love.draw() - G.setColor(Color[color + Color.bright]) - G.setBackgroundColor(Color[bg_color]) - G.setFont(font) + gfx.setColor(Color[color + Color.bright]) + gfx.setBackgroundColor(Color[bg_color]) + gfx.setFont(font) local text = getTimestamp() local off_x = font:getWidth(text) / 2 local off_y = font:getHeight() / 2 - G.print(text, midx - off_x, midy - off_y) + gfx.print(text, midx - off_x, midy - off_y) end ``` Let's see this step-by-step. @@ -28,13 +28,13 @@ The way the `print` helper, and most graphics helpers work, is by starting drawi So, we need to determine the half-width and half-height of our text object to correctly draw it at the center. To do this, we can use the `getWidth()` and `getHeight()` helpers. We determined the midpoint of the screen earlier: ```lua -width, height = G.getDimensions() +width, height = gfx.getDimensions() midx = width / 2 midy = height / 2 ``` Armed with this, we can draw the time dead center: ```lua -G.print(text, midx - off_x, midy - off_y) +gfx.print(text, midx - off_x, midy - off_y) ``` #### Getting the timestamp @@ -60,7 +60,7 @@ function setTime() t = s + M * m + H * h end ``` -Reading the current time is achieved by using `os.date()`, which unlike `os.time()`, allows us to specify the format of the resulting string. We want to achieve the end result of "hour:minute"second", which we could get with the format string "%H:%M:%S", but we also need these intermediate values separately, to keep time. Instead, if the special format string "*t" is passed to the function, it will return a table with the parts of the timestamp instead of a string. +Reading the current time is achieved by using `os.date()`, which unlike `os.time()`, allows us to specify the format of the resulting stringfx. We want to achieve the end result of "hour:minute"second", which we could get with the format string "%H:%M:%S", but we also need these intermediate values separately, to keep time. Instead, if the special format string "*t" is passed to the function, it will return a table with the parts of the timestamp instead of a stringfx. #### Timekeeping diff --git a/src/examples/clock/main.lua b/src/examples/clock/main.lua index 2ec69534..b940aa84 100644 --- a/src/examples/clock/main.lua +++ b/src/examples/clock/main.lua @@ -1,6 +1,6 @@ -local G = love.graphics +local gfx = love.graphics -width, height = G.getDimensions() +width, height = gfx.getDimensions() midx = width / 2 midy = height / 2 @@ -22,7 +22,7 @@ setTime() math.randomseed(os.time()) color = math.random(7) bg_color = math.random(7) -font = G.newFont(144) +font = gfx.newFont(144) local function pad(i) return string.format("%02d", i) @@ -36,13 +36,13 @@ function getTimestamp() end function love.draw() - G.setColor(Color[color + Color.bright]) - G.setBackgroundColor(Color[bg_color]) - G.setFont(font) + gfx.setColor(Color[color + Color.bright]) + gfx.setBackgroundColor(Color[bg_color]) + gfx.setFont(font) local text = getTimestamp() local off_x = font:getWidth(text) / 2 local off_y = font:getHeight() / 2 - G.print(text, midx - off_x, midy - off_y) + gfx.print(text, midx - off_x, midy - off_y) end function love.update(dt) diff --git a/src/examples/life/README.md b/src/examples/life/README.md index 17e43eec..5e2479ed 100644 --- a/src/examples/life/README.md +++ b/src/examples/life/README.md @@ -10,7 +10,7 @@ We have used the screen size before, but only in a very limited capacity, to det ```lua cell_size = 10 -screen_w, screen_h = G.getDimensions() +screen_w, screen_h = gfx.getDimensions() grid_w = screen_w / cell_size grid_h = screen_h / cell_size ``` @@ -250,10 +250,10 @@ function drawHelp() local right_edge = screen_w - margin local reset_msg = "Reset: [r] key or long press" local speed_msg = "Set speed: [+]/[-] key or drag up/down" - G.print(reset_msg, margin, (bottom - fh) - fh) - G.print(speed_msg, margin, bottom - fh) + gfx.print(reset_msg, margin, (bottom - fh) - fh) + gfx.print(speed_msg, margin, bottom - fh) local speed_label = string.format("Speed: %02d", speed) local label_w = font:getWidth(speed_label) - G.print(speed_label, right_edge - label_w, bottom - fh) + gfx.print(speed_label, right_edge - label_w, bottom - fh) end ``` diff --git a/src/examples/life/main.lua b/src/examples/life/main.lua index 74b42f7a..c3412a62 100644 --- a/src/examples/life/main.lua +++ b/src/examples/life/main.lua @@ -1,12 +1,12 @@ --- original from https://github.com/Aethelios/Conway-s-Game-of-Life-in-Lua-and-Love2D -G = love.graphics -G.setFont(font) +gfx = love.graphics +gfx.setFont(font) fh = font:getHeight() cell_size = 10 margin = 5 -screen_w, screen_h = G.getDimensions() +screen_w, screen_h = gfx.getDimensions() grid_w = screen_w / cell_size grid_h = screen_h / cell_size grid = {} @@ -146,22 +146,22 @@ function drawHelp() local right_edge = screen_w - margin local reset_msg = "Reset: [r] key or long press" local speed_msg = "Set speed: [+]/[-] key or drag up/down" - G.print(reset_msg, margin, (bottom - fh) - fh) - G.print(speed_msg, margin, bottom - fh) + gfx.print(reset_msg, margin, (bottom - fh) - fh) + gfx.print(speed_msg, margin, bottom - fh) local speed_label = string.format("Speed: %02d", speed) local label_w = font:getWidth(speed_label) - G.print(speed_label, right_edge - label_w, bottom - fh) + gfx.print(speed_label, right_edge - label_w, bottom - fh) end function drawCell(x, y) - G.setColor(.9, .9, .9) - G.rectangle('fill', + gfx.setColor(.9, .9, .9) + gfx.rectangle('fill', (x - 1) * cell_size, (y - 1) * cell_size, cell_size, cell_size) - G.setColor(.3, .3, .3) + gfx.setColor(.3, .3, .3) - G.rectangle('line', + gfx.rectangle('line', (x - 1) * cell_size, (y - 1) * cell_size, cell_size, cell_size) @@ -176,7 +176,7 @@ function love.draw() end end - G.setColor(1, 1, 1, 0.5) + gfx.setColor(1, 1, 1, 0.5) drawHelp() end diff --git a/src/examples/paint/README.md b/src/examples/paint/README.md index acd44128..bfde8aa5 100644 --- a/src/examples/paint/README.md +++ b/src/examples/paint/README.md @@ -42,7 +42,7 @@ Building on the 16-color theme, we will divide the screen into 10 columns (8 col Then halve these columns to get the row height. Display the selected background on a double block, with the foreground color in the middle. ```lua -width, height = G.getDimensions() +width, height = gfx.getDimensions() --- color palette block_w = width / 10 block_h = block_w / 2 @@ -142,13 +142,13 @@ In our case, we are only interested in the whole number to navigate our grid, an local y = height - block_h for c = 0, 7 do local x = block_w * (c + 2) - G.setColor(Color[c]) - G.rectangle("fill", x, y, width, block_h) - G.setColor(Color[c + 8]) - G.rectangle("fill", x, y - block_h, width, block_h) - G.setColor(Color[Color.white]) - G.rectangle("line", x, y, width, block_h) - G.rectangle("line", x, y - block_h, width, block_h) + gfx.setColor(Color[c]) + gfx.rectangle("fill", x, y, width, block_h) + gfx.setColor(Color[c + 8]) + gfx.rectangle("fill", x, y - block_h, width, block_h) + gfx.setColor(Color[Color.white]) + gfx.rectangle("line", x, y, width, block_h) + gfx.rectangle("line", x, y - block_h, width, block_h) end ``` @@ -163,7 +163,7 @@ Let's see how this works. First, we set up the canvas: ```lua can_w = width - box_w can_h = height - pal_h - 1 -canvas = G.newCanvas(can_w, can_h) +canvas = gfx.newCanvas(can_w, can_h) ``` The default size of a canvas would be equivalent to the screen, but we have some UI elements here, so a bit smaller makes more sense. However, this does mean we need to calculate the offsets properly when detecting clicks and displaying it. @@ -177,12 +177,12 @@ function useCanvas(x, y, btn) local aw = getWeight() canvas:renderTo(function() -- ... - G.circle("fill", x - box_w, y, aw) + gfx.circle("fill", x - box_w, y, aw) end) end ``` -Note the _x_ coordinate, which is offset by `box_w` (the width of the side panel). When drawing, we go the opposite direction: `G.draw(canvas, box_w)`. +Note the _x_ coordinate, which is offset by `box_w` (the width of the side panel). When drawing, we go the opposite direction: `gfx.draw(canvas, box_w)`. ### Click detection diff --git a/src/examples/paint/main.lua b/src/examples/paint/main.lua index dc2f19b2..01e4ef7d 100644 --- a/src/examples/paint/main.lua +++ b/src/examples/paint/main.lua @@ -1,4 +1,4 @@ -width, height = G.getDimensions() +width, height = gfx.getDimensions() --- color palette block_w = width / 10 block_h = block_w / 2 @@ -29,7 +29,7 @@ weights = { 1, 2, 4, 5, 6, 9, 11, 13 } --- canvas can_w = width - box_w can_h = height - pal_h - 1 -canvas = G.newCanvas(can_w, can_h) +canvas = gfx.newCanvas(can_w, can_h) --- selected color = 0 -- black @@ -55,42 +55,42 @@ function inWeightRange(x, y) end function drawBackground() - G.setColor(Color[Color.black]) - G.rectangle("fill", 0, 0, width, height) + gfx.setColor(Color[Color.black]) + gfx.rectangle("fill", 0, 0, width, height) end function drawPaletteOutline(y) - G.setColor(Color[bg_color]) - G.rectangle("fill", 0, y - block_h, block_w * 2, block_h * 2) - G.setColor(Color[Color.white]) - G.rectangle("line", 0, y - block_h, sel_w, pal_h) - G.rectangle("line", sel_w, y - block_h, width, pal_h) + gfx.setColor(Color[bg_color]) + gfx.rectangle("fill", 0, y - block_h, block_w * 2, block_h * 2) + gfx.setColor(Color[Color.white]) + gfx.rectangle("line", 0, y - block_h, sel_w, pal_h) + gfx.rectangle("line", sel_w, y - block_h, width, pal_h) end function drawSelectedColor(y) - G.setColor(Color[color]) - G.rectangle("fill", block_w / 2, y - (block_h / 2), + gfx.setColor(Color[color]) + gfx.rectangle("fill", block_w / 2, y - (block_h / 2), block_w, block_h) -- outline local line_color = Color.white + Color.bright if color == line_color then line_color = Color.black end - G.setColor(Color[line_color]) - G.rectangle("line", block_w / 2, y - (block_h / 2), + gfx.setColor(Color[line_color]) + gfx.rectangle("line", block_w / 2, y - (block_h / 2), block_w, block_h) end function drawColorBoxes(y) for c = 0, 7 do local x = block_w * (c + 2) - G.setColor(Color[c]) - G.rectangle("fill", x, y, width, block_h) - G.setColor(Color[c + 8]) - G.rectangle("fill", x, y - block_h, width, block_h) - G.setColor(Color[Color.white]) - G.rectangle("line", x, y, width, block_h) - G.rectangle("line", x, y - block_h, width, block_h) + gfx.setColor(Color[c]) + gfx.rectangle("fill", x, y, width, block_h) + gfx.setColor(Color[c + 8]) + gfx.rectangle("fill", x, y - block_h, width, block_h) + gfx.setColor(Color[Color.white]) + gfx.rectangle("line", x, y, width, block_h) + gfx.rectangle("line", x, y - block_h, width, block_h) end end @@ -102,31 +102,31 @@ function drawColorPalette() end function drawBrush(cx, cy) - G.push() - G.translate(cx, cy) + gfx.push() + gfx.translate(cx, cy) local s = icon_d / 100 * .8 - G.scale(s, s) - G.rotate(math.pi / 4) -- 45 degree rotation + gfx.scale(s, s) + gfx.rotate(math.pi / 4) -- 45 degree rotation -- Draw the brush handle (wooden brown color) - G.setColor(0.6, 0.4, 0.2) - G.rectangle("fill", -8, -80, 16, 60) + gfx.setColor(0.6, 0.4, 0.2) + gfx.rectangle("fill", -8, -80, 16, 60) -- Handle highlight - G.setColor(0.8, 0.6, 0.4) - G.rectangle("fill", -6, -75, 3, 50) + gfx.setColor(0.8, 0.6, 0.4) + gfx.rectangle("fill", -6, -75, 3, 50) -- Metal ferrule - G.setColor(0.7, 0.7, 0.8) - G.rectangle("fill", -10, -25, 20, 12) + gfx.setColor(0.7, 0.7, 0.8) + gfx.rectangle("fill", -10, -25, 20, 12) -- Ferrule shine - G.setColor(0.9, 0.9, 1.0) - G.rectangle("fill", -8, -24, 3, 10) + gfx.setColor(0.9, 0.9, 1.0) + gfx.rectangle("fill", -8, -24, 3, 10) -- Bristles with smooth flame-shaped tip - G.setColor(0.2, 0.2, 0.2) - G.rectangle("fill", -12, -13, 24, 25) + gfx.setColor(0.2, 0.2, 0.2) + gfx.rectangle("fill", -12, -13, 24, 25) -- Create flame tip using bezier curve local curve = love.math.newBezierCurve( @@ -140,38 +140,38 @@ function drawBrush(cx, cy) ) local points = curve:render() - G.polygon("fill", points) + gfx.polygon("fill", points) - G.pop() + gfx.pop() end function drawEraser(cx, cy) - G.push() - G.translate(cx, cy) + gfx.push() + gfx.translate(cx, cy) local s = icon_d / 100 - G.scale(s, s) - G.rotate(math.pi / 4) -- 45 degree rotation + gfx.scale(s, s) + gfx.rotate(math.pi / 4) -- 45 degree rotation -- Main eraser body (light blue) - G.setColor(Color[Color.white]) - G.rectangle("fill", -12, -40, 24, 60) + gfx.setColor(Color[Color.white]) + gfx.rectangle("fill", -12, -40, 24, 60) -- Blue stripes running lengthwise (darker blue) - G.setColor(Color[Color.blue]) - G.rectangle("fill", -12, -40, 6, 60) - G.rectangle("fill", 6, -40, 6, 60) + gfx.setColor(Color[Color.blue]) + gfx.rectangle("fill", -12, -40, 6, 60) + gfx.rectangle("fill", 6, -40, 6, 60) -- Worn eraser tip (slightly darker) - G.setColor(Color[Color.white + Color.bright]) - G.rectangle("fill", -12, 15, 24, 8) + gfx.setColor(Color[Color.white + Color.bright]) + gfx.rectangle("fill", -12, 15, 24, 8) -- Eraser crumbs - G.setColor(Color[Color.white]) - G.circle("fill", 18, 25, 2) - G.circle("fill", 22, 30, 1.5) - G.circle("fill", 15, 32, 1) + gfx.setColor(Color[Color.white]) + gfx.circle("fill", 18, 25, 2) + gfx.circle("fill", 22, 30, 1.5) + gfx.circle("fill", 15, 32, 1) - G.pop() + gfx.pop() end -- this is a color @@ -187,14 +187,14 @@ function drawTools() local x = tool_midx - tb_half local y = (i - 1) * (m_2 + tb) if i == tool then - G.setColor(Color[Color.black]) + gfx.setColor(Color[Color.black]) else - G.setColor(Color[Color.white + Color.bright]) + gfx.setColor(Color[Color.white + Color.bright]) end - G.rectangle("fill", x, y + m_2, tb, tb) + gfx.rectangle("fill", x, y + m_2, tb, tb) - G.setColor(Color[Color.black]) - G.rectangle("line", x, y + m_2, tb, tb) + gfx.setColor(Color[Color.black]) + gfx.rectangle("line", x, y + m_2, tb, tb) local draw = tools[i] draw(tool_midx - m_2, y + tb_half + m_4) @@ -202,20 +202,20 @@ function drawTools() end function drawWeightSelector() - G.setColor(Color[Color.white + Color.bright]) - G.rectangle("line", 0, box_h - weight_h, box_w - 1, weight_h) + gfx.setColor(Color[Color.white + Color.bright]) + gfx.rectangle("line", 0, box_h - weight_h, box_w - 1, weight_h) local h = (weight_h - (2 * margin)) / 8 local w = marg_l for i = 0, 7 do local y = wb_y + margin + (i * h) local lw = i + 1 local mid = y + (h / 2) - G.setColor(Color[Color.white + Color.bright]) - G.rectangle("fill", margin, y, w, h) + gfx.setColor(Color[Color.white + Color.bright]) + gfx.rectangle("fill", margin, y, w, h) if lw == weight then - -- G.setColor(Color[Color.white]) - -- G.rectangle("fill", margin, y, w, h) - G.setColor(goose) + -- gfx.setColor(Color[Color.white]) + -- gfx.rectangle("fill", margin, y, w, h) + gfx.setColor(goose) local rx1 = 3 * margin local rx2 = 5 * margin local ry1 = mid - margin @@ -224,7 +224,7 @@ function drawWeightSelector() local x2 = 7 * margin local y1 = mid - m_2 local y2 = mid + m_2 - G.polygon("fill", + gfx.polygon("fill", -- body rx2, ry1, rx1, ry1, @@ -235,9 +235,9 @@ function drawWeightSelector() x2, mid, x1, y1 ) - G.setColor(Color[Color.black]) - G.setLineWidth(2) - G.polygon("line", + gfx.setColor(Color[Color.black]) + gfx.setLineWidth(2) + gfx.polygon("line", -- body rx2, ry1, rx1, ry1, @@ -248,22 +248,22 @@ function drawWeightSelector() x2, mid, x1, y1 ) - G.setLineWidth(1) + gfx.setLineWidth(1) else end - G.setColor(Color[Color.black]) + gfx.setColor(Color[Color.black]) local aw = weights[lw] - G.rectangle("fill", box_w / 3, mid - (aw / 2), + gfx.rectangle("fill", box_w / 3, mid - (aw / 2), box_w / 2, aw) end end function drawToolbox() --- outline - G.setColor(Color[Color.white]) - G.rectangle("fill", 0, 0, box_w - 1, height - pal_h) - G.setColor(Color[Color.white + Color.bright]) - G.rectangle("line", 0, 0, box_w - 1, box_h) + gfx.setColor(Color[Color.white]) + gfx.rectangle("fill", 0, 0, box_w - 1, height - pal_h) + gfx.setColor(Color[Color.white + Color.bright]) + gfx.rectangle("line", 0, 0, box_w - 1, box_h) drawTools() drawWeightSelector() end @@ -282,8 +282,8 @@ function drawTarget() local x, y = love.mouse.getPosition() if inCanvasRange(x, y) then local aw = getWeight() - G.setColor(Color[Color.white]) - G.circle("line", x, y, aw) + gfx.setColor(Color[Color.white]) + gfx.circle("line", x, y, aw) end end @@ -291,7 +291,7 @@ function love.draw() drawBackground() drawToolbox() drawColorPalette() - G.draw(canvas, box_w) + gfx.draw(canvas, box_w) drawTarget() end @@ -327,14 +327,14 @@ function useCanvas(x, y, btn) canvas:renderTo(function() if btn == 1 then if tool == 1 then - G.setColor(Color[color]) + gfx.setColor(Color[color]) elseif tool == 2 then - G.setColor(Color[bg_color]) + gfx.setColor(Color[bg_color]) end elseif btn == 2 then - G.setColor(Color[bg_color]) + gfx.setColor(Color[bg_color]) end - G.circle("fill", x - box_w, y, aw) + gfx.circle("fill", x - box_w, y, aw) end) end diff --git a/src/examples/sine/main.lua b/src/examples/sine/main.lua index 8348f3a1..bcfcf7f7 100644 --- a/src/examples/sine/main.lua +++ b/src/examples/sine/main.lua @@ -1,20 +1,20 @@ -local G = love.graphics +local gfx = love.graphics local x0 = 0 -local xe = G.getWidth() +local xe = gfx.getWidth() local y0 = 0 -local ye = G.getHeight() +local ye = gfx.getHeight() local xh = xe / 2 local yh = ye / 2 -G.setColor(1, 1, 1, 0.5) -G.setLineWidth(1) -G.line(xh, y0, xh, ye) -G.line(x0, yh, xe, yh) +gfx.setColor(1, 1, 1, 0.5) +gfx.setLineWidth(1) +gfx.line(xh, y0, xh, ye) +gfx.line(x0, yh, xe, yh) -G.setColor(1, 0, 0) -G.setPointSize(2) +gfx.setColor(1, 0, 0) +gfx.setPointSize(2) local amp = 100 local times = 2 @@ -27,4 +27,4 @@ for x = 0, xe do table.insert(points, y) end -G.points(points) +gfx.points(points) diff --git a/src/examples/tixy/README.md b/src/examples/tixy/README.md index d5ac52bd..77dca387 100644 --- a/src/examples/tixy/README.md +++ b/src/examples/tixy/README.md @@ -132,14 +132,14 @@ end ```lua function drawCircle(color, radius, x, y) - G.setColor(color) - G.circle( + gfx.setColor(color) + gfx.circle( "fill", x * (size + spacing) + offset, y * (size + spacing) + offset, radius ) - G.circle( + gfx.circle( "line", x * (size + spacing) + offset, y * (size + spacing) + offset, diff --git a/src/examples/tixy/main.lua b/src/examples/tixy/main.lua index a49c676f..aef89ecf 100644 --- a/src/examples/tixy/main.lua +++ b/src/examples/tixy/main.lua @@ -1,6 +1,6 @@ -local G = love.graphics +local gfx = love.graphics math.randomseed(os.time()) -cw, ch = G.getDimensions() +cw, ch = gfx.getDimensions() midx = cw / 2 require("math") @@ -100,19 +100,19 @@ function setupTixy() end function drawBackground() - G.setColor(colors.bg) - G.rectangle("fill", 0, 0, cw, ch) + gfx.setColor(colors.bg) + gfx.rectangle("fill", 0, 0, cw, ch) end function drawCircle(color, radius, x, y) - G.setColor(color) - G.circle( + gfx.setColor(color) + gfx.circle( "fill", x * (size + spacing) + offset, y * (size + spacing) + offset, radius ) - G.circle( + gfx.circle( "line", x * (size + spacing) + offset, y * (size + spacing) + offset, @@ -147,14 +147,14 @@ function drawOutput() end function drawText() - G.setColor(colors.text) + gfx.setColor(colors.text) local sof = (size / 2) + offset local hof = sof / 2 - G.printf(legend, midx + hof, sof, midx - sof) + gfx.printf(legend, midx + hof, sof, midx - sof) if showHelp then - G.setColor(colors.help) - G.setFont(font) - G.printf(help, midx + hof, ch - (5 * sof), midx - sof) + gfx.setColor(colors.help) + gfx.setFont(font) + gfx.printf(help, midx + hof, ch - (5 * sof), midx - sof) end end diff --git a/src/examples/turtle/README.md b/src/examples/turtle/README.md index bc114c43..8704ece6 100644 --- a/src/examples/turtle/README.md +++ b/src/examples/turtle/README.md @@ -29,11 +29,11 @@ For the most simple example of this, let's represent the turtle with only an ell local x_r = 15 local y_r = 20 function turtleA(x, y) - G.ellipse("fill", x, y, x_r, y_r, 100) + gfx.ellipse("fill", x, y, x_r, y_r, 100) end function turtleB(x, y) - G.translate(x, y) - G.ellipse("fill", 0, 0, x_r, y_r, 100) + gfx.translate(x, y) + gfx.ellipse("fill", 0, 0, x_r, y_r, 100) end ``` We can draw it at (x, y) either by drawing the shape to (x, y), or first translating the whole drawing to (x, y), and drawing at (0, 0). This might not seem that big of a deal in this simple case, but when the number of transformations and shapes go up, things cat get hard to track very quickly. @@ -45,7 +45,7 @@ The way we translate this for LOVE is an "x radius" and a "y radius".
In o Next, we are adding the turtle's head, which is in some sort of relation to it's body, but also the location where the whole drawing is. ```lua -G.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100) +gfx.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100) ``` Using the second method, we are able to provide the head position in "turtle coordinates". So far, there's nothing about this we couldn't have done the other route, but let's proceed to the legs, which we want to draw at an angle. LOVE doesn't provide us any way to do this with only the ellipse function, we do need to `rotate` first. @@ -53,15 +53,15 @@ So far, there's nothing about this we couldn't have done the other route, but le See this condensed example: ```lua function frontLeftLeg(x, y, x_r, y_r, leg_xr, leg_yr) - G.setColor(Color[Color.green + Color.bright]) + gfx.setColor(Color[Color.green + Color.bright]) --- move to the turtle's position - G.translate(x, y) + gfx.translate(x, y) --- move to where the leg attaches to the body - G.translate(-x_r, -y_r / 2 - leg_xr) + gfx.translate(-x_r, -y_r / 2 - leg_xr) --- rotate - G.rotate(-math.pi / 4) + gfx.rotate(-math.pi / 4) --- draw the leg - G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) + gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) end ``` @@ -77,17 +77,17 @@ You will notice that the actual code does not look like that. For one, in the le Another, more interesting difference is the `push()` - `pop()` pairs around each leg. ```lua --- left front leg -G.push("all") -G.translate(-x_r, -y_r / 2 - leg_xr) -G.rotate(-math.pi / 4) -G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) -G.pop() +gfx.push("all") +gfx.translate(-x_r, -y_r / 2 - leg_xr) +gfx.rotate(-math.pi / 4) +gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) +gfx.pop() --- right front leg -G.push("all") -G.translate(x_r, -y_r / 2 - leg_xr) -G.rotate(math.pi / 4) -G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) -G.pop() +gfx.push("all") +gfx.translate(x_r, -y_r / 2 - leg_xr) +gfx.rotate(math.pi / 4) +gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) +gfx.pop() ``` Say we are done drawing the left leg, and now we want to proceed to drawing the other one. We could do the opposite transformations to go back to "zero", or transform from our current state to the desired one, but that leads to more complicated math and less readable code. Instead, we work in stages. When done with the first leg, we can "reset" to our previous state (the "turtle coordinates"), and set up our next one again relative to the center. diff --git a/src/examples/turtle/drawing.lua b/src/examples/turtle/drawing.lua index 2327153f..5c5a2077 100644 --- a/src/examples/turtle/drawing.lua +++ b/src/examples/turtle/drawing.lua @@ -1,6 +1,6 @@ -local G = love.graphics +gfx = love.graphics -font = G.newFont() +font = gfx.newFont() bg_color = Color.black body_color = Color.green limb_color = body_color + Color.bright @@ -14,45 +14,45 @@ function drawBackground(color) if color_valid then c = color end - G.setColor(Color[c]) - G.rectangle("fill", 0, 0, width, height) + gfx.setColor(Color[c]) + gfx.rectangle("fill", 0, 0, width, height) end function drawFrontLegs(x_r, y_r, leg_xr, leg_yr) - G.setColor(Color[limb_color]) - G.push("all") - G.translate(-x_r, -y_r / 2 - leg_xr) - G.rotate(-math.pi / 4) - G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) - G.pop() - G.push("all") - G.translate(x_r, -y_r / 2 - leg_xr) - G.rotate(math.pi / 4) - G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) - G.pop() + gfx.setColor(Color[limb_color]) + gfx.push("all") + gfx.translate(-x_r, -y_r / 2 - leg_xr) + gfx.rotate(-math.pi / 4) + gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) + gfx.pop() + gfx.push("all") + gfx.translate(x_r, -y_r / 2 - leg_xr) + gfx.rotate(math.pi / 4) + gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) + gfx.pop() end function drawHindLegs(x_r, y_r, leg_r, leg_yr) - G.setColor(Color[limb_color]) - G.push("all") - G.translate(-x_r, y_r / 2 + leg_r) - G.rotate(math.pi / 4) - G.ellipse("fill", 0, 0, leg_r, leg_yr, 100) - G.pop() - G.push("all") - G.translate(x_r, y_r / 2 + leg_r) - G.rotate(-math.pi / 4) - G.ellipse("fill", 0, 0, leg_r, leg_yr, 100) - G.pop() + gfx.setColor(Color[limb_color]) + gfx.push("all") + gfx.translate(-x_r, y_r / 2 + leg_r) + gfx.rotate(math.pi / 4) + gfx.ellipse("fill", 0, 0, leg_r, leg_yr, 100) + gfx.pop() + gfx.push("all") + gfx.translate(x_r, y_r / 2 + leg_r) + gfx.rotate(-math.pi / 4) + gfx.ellipse("fill", 0, 0, leg_r, leg_yr, 100) + gfx.pop() end function drawBody(x_r, y_r, head_r) --- body - G.setColor(Color[body_color]) - G.ellipse("fill", 0, 0, x_r, y_r, 100) + gfx.setColor(Color[body_color]) + gfx.ellipse("fill", 0, 0, x_r, y_r, 100) --- head local neck = 5 - G.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100) + gfx.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100) --- end end @@ -62,24 +62,24 @@ function drawTurtle(x, y) local leg_yr = 10 local x_r = 15 local y_r = 20 - G.push("all") - G.translate(x, y) + gfx.push("all") + gfx.translate(x, y) drawFrontLegs(x_r, y_r, leg_xr, leg_yr) drawHindLegs(x_r, y_r, leg_xr, leg_yr) drawBody(x_r, y_r, head_r) - G.pop() + gfx.pop() end function drawHelp() - G.setColor(Color[Color.white]) - G.print("Press [I] to open console", 20, 20) + gfx.setColor(Color[Color.white]) + gfx.print("Press [I] to open console", 20, 20) local help = "Enter 'forward', 'back', 'left', or 'right'" .. "to move the turtle!" - G.print(help, 20, 50) + gfx.print(help, 20, 50) end function drawDebuginfo() - G.setColor(Color[debug_color]) + gfx.setColor(Color[debug_color]) local dt = string.format("Turtle position: (%d, %d)", tx, ty) - G.print(dt, width - 200, 20) + gfx.print(dt, width - 200, 20) end diff --git a/src/examples/turtle/main.lua b/src/examples/turtle/main.lua index 1833a4c9..7fe896e5 100644 --- a/src/examples/turtle/main.lua +++ b/src/examples/turtle/main.lua @@ -1,7 +1,8 @@ require("action") require("drawing") -width, height = love.graphics.getDimensions() +gfx = love.graphics +width, height = gfx.getDimensions() midx = width / 2 midy = height / 2 incr = 10 @@ -19,7 +20,7 @@ function eval(input) end function love.draw() - G.setFont(font) + gfx.setFont(font) drawBackground() drawHelp() drawTurtle(tx, ty) diff --git a/src/harmony/init.lua b/src/harmony/init.lua index 111aff9f..2edc0d3a 100644 --- a/src/harmony/init.lua +++ b/src/harmony/init.lua @@ -83,10 +83,10 @@ local function new(_lock) if love.update then love.update(dt) end if love.graphics and love.graphics.isActive() then - local G = love.graphics - G.origin() - G.clear( - G.getBackgroundColor() + local gfx = love.graphics + gfx.origin() + gfx.clear( + gfx.getBackgroundColor() ) if love.draw then love.draw() end @@ -150,7 +150,7 @@ local function utils() if not love.harmony then return end if love.harmony.utils then return end - G = love.graphics + gfx = love.graphics --- @param name love.Event local love_event = function(name, ...) @@ -203,7 +203,7 @@ local function utils() FS.mkdirp(dir) --- @param img_data love.ImageData - G.captureScreenshot(function(img_data) + gfx.captureScreenshot(function(img_data) if img_data then local from = FS.join_path( love.filesystem.getSaveDirectory(), fn diff --git a/src/main.lua b/src/main.lua index 5ccc3c05..46bc7ebe 100644 --- a/src/main.lua +++ b/src/main.lua @@ -15,7 +15,7 @@ local FS = require("util.filesystem") require("lib.error_explorer") -G = love.graphics +local gfx = love.graphics local messages = { how_to_exit = 'Press Ctrl-Esc to exit', @@ -45,11 +45,11 @@ local config_view = function(flags) local font_dir = "assets/fonts/" local mf = "ubuntu_mono_bold_nerd.ttf" - local font_main = G.newFont( + local font_main = gfx.newFont( font_dir .. mf, font_size) - local font_icon = G.newFont( + local font_icon = gfx.newFont( font_dir .. "SFMonoNerdFontMono-Regular.otf", font_size) - local font_cjk = G.newFont( + local font_cjk = gfx.newFont( font_dir .. "SarasaGothicJ-Bold.ttf", font_size * (2 / 3)) font_main:setFallbacks(font_icon, font_cjk) @@ -64,9 +64,9 @@ local config_view = function(flags) local lines = 16 local input_max = 14 - local font_labels = G.newFont(font_dir .. mf, 12) - local w = love.fixWidth or G.getWidth() - local h = love.fixHeight or G.getHeight() + local font_labels = gfx.newFont(font_dir .. mf, 12) + local w = love.fixWidth or gfx.getWidth() + local h = love.fixHeight or gfx.getHeight() local eh = h - 2 * fh local debugheight = math.floor(eh / (love.test_grid_y * fh)) local debugwidth = math.floor(w / love.test_grid_x) / fw diff --git a/src/model/canvasModel.lua b/src/model/canvasModel.lua index 767c99c8..e9449f01 100644 --- a/src/model/canvasModel.lua +++ b/src/model/canvasModel.lua @@ -4,7 +4,7 @@ require("util.view") local class = require('util.class') local Terminal = require("lib.terminal") -local G = love.graphics +local gfx = love.graphics --- @class CanvasModel --- @field terminal table @@ -31,7 +31,7 @@ function CanvasModel.new(cfg) -- h = ViewUtils.get_drawable_height(cfg.view) h = cfg.view.h end - local canvas = G.newCanvas(w, h) + local canvas = gfx.newCanvas(w, h) local custom_height = cfg.view.fh * cfg.view.lh local term = Terminal(w, h, cfg.view.font, nil, custom_height) @@ -85,14 +85,14 @@ end function CanvasModel:clear_canvas() return self.canvas:renderTo(function() - G.clear(0, 0, 0, 0) + gfx.clear(0, 0, 0, 0) end) end function CanvasModel:draw_to() - G.setCanvas(self.canvas) + gfx.setCanvas(self.canvas) end function CanvasModel:restore_main() - G.setCanvas() + gfx.setCanvas() end diff --git a/src/util/view.lua b/src/util/view.lua index 385ab219..2dfec3d6 100644 --- a/src/util/view.lua +++ b/src/util/view.lua @@ -21,8 +21,8 @@ end --- @param cfg ViewConfig local write_line = function(l, str, y, breaks, cfg) local dy = y - (-l + 1 + breaks) * cfg.fh - G.setFont(cfg.font) - G.print(str, 0, dy) + gfx.setFont(cfg.font) + gfx.print(str, 0, dy) end --- Write a token to output @@ -34,17 +34,17 @@ end --- @param selected boolean local write_token = function(dy, dx, token, color, bgcolor, selected) - G.push('all') + gfx.push('all') if selected then - G.setColor(color) + gfx.setColor(color) local back = string.rep('█', string.ulen(token)) - G.print(back, dx, dy) - G.setColor(bgcolor) + gfx.print(back, dx, dy) + gfx.setColor(bgcolor) else - G.setColor(color) + gfx.setColor(color) end - G.print(token, dx, dy) - G.pop() + gfx.print(token, dx, dy) + gfx.pop() end --- Hide elements for debugging @@ -73,61 +73,61 @@ BlendMode = Alpha AlphaMode local blendModes = { { -- 1 name = 'Alpha AlphaM', - blend = function() G.setBlendMode('alpha', "alphamultiply") end + blend = function() gfx.setBlendMode('alpha', "alphamultiply") end }, { -- 2 name = 'Alpha PreM', - blend = function() G.setBlendMode('alpha', "premultiplied") end + blend = function() gfx.setBlendMode('alpha', "premultiplied") end }, -- add { name = 'Add AlphaM', - blend = function() G.setBlendMode('add', "alphamultiply") end + blend = function() gfx.setBlendMode('add', "alphamultiply") end }, { name = 'Add PreM', - blend = function() G.setBlendMode('add', "premultiplied") end + blend = function() gfx.setBlendMode('add', "premultiplied") end }, -- subtract { name = 'Subtract AlphaM', - blend = function() G.setBlendMode('subtract', "alphamultiply") end + blend = function() gfx.setBlendMode('subtract', "alphamultiply") end }, { name = 'Subtract PreM', - blend = function() G.setBlendMode('subtract', "premultiplied") end + blend = function() gfx.setBlendMode('subtract', "premultiplied") end }, -- replace { name = 'Replace AlphaM', - blend = function() G.setBlendMode('replace', "alphamultiply") end + blend = function() gfx.setBlendMode('replace', "alphamultiply") end }, { name = 'Replace PreM', - blend = function() G.setBlendMode('replace', "premultiplied") end + blend = function() gfx.setBlendMode('replace', "premultiplied") end }, -- pre only { name = 'Multiply PreM', - blend = function() G.setBlendMode('multiply', "premultiplied") end + blend = function() gfx.setBlendMode('multiply', "premultiplied") end }, { name = 'Darken PreM', - blend = function() G.setBlendMode('darken', "premultiplied") end + blend = function() gfx.setBlendMode('darken', "premultiplied") end }, { name = 'Lighten PreM', - blend = function() G.setBlendMode('lighten', "premultiplied") end + blend = function() gfx.setBlendMode('lighten', "premultiplied") end }, -- screen { name = 'Screen AlphaM', - blend = function() G.setBlendMode('screen', "alphamultiply") end + blend = function() gfx.setBlendMode('screen', "alphamultiply") end }, { name = 'Screen PreM', - blend = function() G.setBlendMode('screen', "premultiplied") end + blend = function() gfx.setBlendMode('screen', "premultiplied") end }, } @@ -188,7 +188,7 @@ local function draw_hl_text(text, highlight, cfg, options) end end end - G.setColor(color) + gfx.setColor(color) local dy = (t_l - 1) * fh local dx = (c - 1) * fw write_token(dy, dx, char, color, bg, false) diff --git a/src/view/canvas/bgView.lua b/src/view/canvas/bgView.lua index 512137d8..ec660a14 100644 --- a/src/view/canvas/bgView.lua +++ b/src/view/canvas/bgView.lua @@ -10,14 +10,14 @@ function BGView:draw(drawable_height) local w = cfg.w local fh = cfg.fh - G.push('all') + gfx.push('all') -- background in case input is not visible - G.rectangle("fill", + gfx.rectangle("fill", 0, drawable_height - 2, w, fh * 2 + 2 ) - G.pop() + gfx.pop() end diff --git a/src/view/canvas/canvasView.lua b/src/view/canvas/canvasView.lua index 9cf794da..d12f5197 100644 --- a/src/view/canvas/canvasView.lua +++ b/src/view/canvas/canvasView.lua @@ -4,7 +4,7 @@ require("view.canvas.terminalView") local class = require("util.class") require("util.view") -local G = love.graphics +local gfx = love.graphics --- @class CanvasView : ViewBase --- @field bg BGView @@ -26,60 +26,60 @@ function CanvasView:draw( local cfg = self.cfg local test = cfg.drawtest - G.reset() - G.push('all') - G.setBlendMode('alpha', 'alphamultiply') -- default + gfx.reset() + gfx.push('all') + gfx.setBlendMode('alpha', 'alphamultiply') -- default if ViewUtils.conditional_draw('show_snapshot') then if snapshot then - G.draw(snapshot) + gfx.draw(snapshot) end self.bg:draw(drawable_height) end if not test then if ViewUtils.conditional_draw('show_terminal') then - -- G.setBlendMode('multiply', "premultiplied") + -- gfx.setBlendMode('multiply', "premultiplied") TerminalView.draw(terminal, term_canvas, snapshot) end if ViewUtils.conditional_draw('show_canvas') then - G.draw(canvas) + gfx.draw(canvas) end - G.setBlendMode('alpha', 'alphamultiply') -- default + gfx.setBlendMode('alpha', 'alphamultiply') -- default else - G.setBlendMode('alpha', 'alphamultiply') -- default + gfx.setBlendMode('alpha', 'alphamultiply') -- default for i = 0, love.test_grid_y - 1 do for j = 0, love.test_grid_x - 1 do local off_x = cfg.debugwidth * cfg.fw local off_y = cfg.debugheight * cfg.fh local dx = j * off_x local dy = i * off_y - G.reset() - G.translate(dx, dy) + gfx.reset() + gfx.translate(dx, dy) local index = (i * love.test_grid_x) + j + 1 local b = ViewUtils.blendModes[index] if b then - -- G.setBlendMode('alpha') -- default + -- gfx.setBlendMode('alpha') -- default if ViewUtils.conditional_draw('show_terminal') then b.blend() TerminalView.draw(terminal, term_canvas, snapshot) end - G.setBlendMode('alpha') -- default + gfx.setBlendMode('alpha') -- default if ViewUtils.conditional_draw('show_canvas') then - G.draw(canvas) + gfx.draw(canvas) end - G.setBlendMode('alpha') -- default - G.setColor(1, 1, 1, 1) - G.setFont(cfg.labelfont) + gfx.setBlendMode('alpha') -- default + gfx.setColor(1, 1, 1, 1) + gfx.setFont(cfg.labelfont) - -- G.print(index .. ' ' .. b.name) - G.print(b.name) + -- gfx.print(index .. ' ' .. b.name) + gfx.print(b.name) end end end end - G.pop() + gfx.pop() end diff --git a/src/view/canvas/terminalView.lua b/src/view/canvas/terminalView.lua index 50c56bf6..12a44759 100644 --- a/src/view/canvas/terminalView.lua +++ b/src/view/canvas/terminalView.lua @@ -1,4 +1,4 @@ -local G = love.graphics +local gfx = love.graphics --- @class TerminalView TerminalView = {} @@ -10,11 +10,11 @@ local function terminal_draw(terminal, canvas, overlay) terminal.char_width, terminal.char_height -- if terminal.dirty or overlay then - G.push('all') + gfx.push('all') - G.setCanvas(canvas) - G.setFont(terminal.font) - G.clear(terminal.clear_color_alpha) + gfx.setCanvas(canvas) + gfx.setFont(terminal.font) + gfx.clear(terminal.clear_color_alpha) local font_height = terminal.font:getHeight() for y, row in ipairs(terminal.buffer) do @@ -41,33 +41,33 @@ local function terminal_draw(terminal, canvas, overlay) -- Character background if not overlay then - G.setColor(unpack(bg)) - G.rectangle("fill", + gfx.setColor(unpack(bg)) + gfx.rectangle("fill", left, top + (font_height - char_height), char_width, char_height) end - local bm, am = G.getBlendMode() - G.setBlendMode('alpha', "alphamultiply") + local bm, am = gfx.getBlendMode() + gfx.setBlendMode('alpha', "alphamultiply") -- Character - G.setColor(unpack(fg)) - G.print(char, left, top) + gfx.setColor(unpack(fg)) + gfx.print(char, left, top) - G.setBlendMode(bm, am) + gfx.setBlendMode(bm, am) state.dirty = false -- end end end terminal.dirty = false - G.pop() + gfx.pop() -- end if terminal.show_cursor then - G.setFont(terminal.font) + gfx.setFont(terminal.font) if love.timer.getTime() % 1 > 0.5 then - G.print("_", + gfx.print("_", (terminal.cursor_x - 1) * char_width, (terminal.cursor_y - 1) * char_height) end @@ -76,15 +76,15 @@ end --- @param terminal table function TerminalView.draw(terminal, canvas, snapshot) - G.setCanvas() - G.push('all') + gfx.setCanvas() + gfx.push('all') if snapshot then terminal_draw(terminal, canvas, true) else terminal_draw(terminal, canvas) end - G.draw(canvas) - G.setBlendMode('alpha') -- default - G.pop() + gfx.draw(canvas) + gfx.setBlendMode('alpha') -- default + gfx.pop() end diff --git a/src/view/consoleView.lua b/src/view/consoleView.lua index a46fa024..2f8be205 100644 --- a/src/view/consoleView.lua +++ b/src/view/consoleView.lua @@ -8,7 +8,7 @@ require("util.color") require("util.view") require("util.debug") -local G = love.graphics +local gfx = love.graphics --- @param cfg Config --- @param ctrl ConsoleController @@ -73,15 +73,15 @@ function ConsoleView:draw_placeholder() local band = self.cfg.view.fh local w = self.cfg.view.w local h = self.cfg.view.h - G.push('all') - G.setColor(Color[Color.yellow]) + gfx.push('all') + gfx.setColor(Color[Color.yellow]) for o = -h, w, 2 * band do - G.polygon("fill" + gfx.polygon("fill" , o + 0, h , o + h, 0 , o + h + band, 0 , o + band, h ) end - G.pop() + gfx.pop() end diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index b4a8aded..e6bac7c3 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -255,7 +255,7 @@ end --- @param special boolean function BufferView:draw(special) - local G = love.graphics + local gfx = love.graphics local cf_colors = self.cfg.colors local colors = cf_colors.editor local font = self.cfg.font @@ -265,17 +265,17 @@ function BufferView:draw(special) --- @type VisibleContent|VisibleStructuredContent local content_text = vc:get_visible() local last_line_n = #content_text - local width, height = G.getDimensions() + local width, height = gfx.getDimensions() local draw_background = function() - G.push('all') - G.setColor(colors.bg) - G.rectangle("fill", 0, 0, width, height) - G.setColor(Color.with_alpha(colors.fg, .0625)) + gfx.push('all') + gfx.setColor(colors.bg) + gfx.rectangle("fill", 0, 0, width, height) + gfx.setColor(Color.with_alpha(colors.fg, .0625)) local bh = math.min(last_line_n, self.cfg.lines) * fh - G.rectangle("fill", 0, 0, width, bh) - G.pop() + gfx.rectangle("fill", 0, 0, width, bh) + gfx.pop() end local draw_highlight = function() @@ -284,16 +284,16 @@ function BufferView:draw(special) local highlight_line = function(ln) if not ln then return end if special then - G.setColor(colors.highlight_special) + gfx.setColor(colors.highlight_special) else if ls then - G.setColor(colors.highlight_loaded) + gfx.setColor(colors.highlight_loaded) else - G.setColor(colors.highlight) + gfx.setColor(colors.highlight) end end local l_y = (ln - 1) * fh - G.rectangle('fill', 0, l_y, width, fh) + gfx.rectangle('fill', 0, l_y, width, fh) end local off = self.offset @@ -313,7 +313,7 @@ function BufferView:draw(special) end local draw_text = function() - G.setFont(font) + gfx.setFont(font) if self.content_type == 'lua' then local vbl = vc:get_visible_blocks() for _, block in ipairs(vbl) do @@ -337,9 +337,9 @@ function BufferView:draw(special) if love.DEBUG then --- phantom text - G.setColor(Color.with_alpha(colors.fg, 0.3)) + gfx.setColor(Color.with_alpha(colors.fg, 0.3)) local text = string.unlines(content_text) - G.print(text) + gfx.print(text) end elseif self.content_type == 'md' then local text = vc:get_visible() @@ -357,10 +357,10 @@ function BufferView:draw(special) ltf = ltf, ctf = ctf, limit = limit, }) elseif self.content_type == 'plain' then - G.setColor(colors.fg) + gfx.setColor(colors.fg) local text = string.unlines(content_text) - G.print(text) + gfx.print(text) end end @@ -370,8 +370,8 @@ function BufferView:draw(special) local lnc = colors.fg local x = self.cfg.w - font:getWidth(' ') - 3 local lnvc = Color.with_alpha(lnc, 0.2) - G.setColor(lnvc) - G.rectangle("fill", x, 0, 2, self.cfg.h) + gfx.setColor(lnvc) + gfx.rectangle("fill", x, 0, 2, self.cfg.h) local seen = {} for ln = 1, self.LINES do local l_y = (ln - 1) * fh @@ -382,12 +382,12 @@ function BufferView:draw(special) local l_x = self.cfg.w - font:getWidth(l) local l_xv = l_x - font:getWidth(l) - 3.5 if showap then - G.setColor(lnvc) - G.print(string.format('%3d', vln), l_xv, l_y) + gfx.setColor(lnvc) + gfx.print(string.format('%3d', vln), l_xv, l_y) end if not seen[ln_w] then - G.setColor(lnc) - G.print(l, l_x, l_y) + gfx.setColor(lnc) + gfx.print(l, l_x, l_y) seen[ln_w] = true end end diff --git a/src/view/editor/search/resultsView.lua b/src/view/editor/search/resultsView.lua index 880e8ee1..c24015f5 100644 --- a/src/view/editor/search/resultsView.lua +++ b/src/view/editor/search/resultsView.lua @@ -16,14 +16,14 @@ ResultsView = class.create(new) function ResultsView:draw(results) local colors = self.cfg.colors.editor local fh = self.cfg.fh * 1.032 -- magic constant - local width, height = G.getDimensions() + local width, height = gfx.getDimensions() local has_results = (results.results and #(results.results) > 0) local draw_background = function() - G.push('all') - G.setColor(colors.results.bg) - G.rectangle("fill", 0, 0, width, height) - G.pop() + gfx.push('all') + gfx.setColor(colors.results.bg) + gfx.rectangle("fill", 0, 0, width, height) + gfx.pop() end local draw_results = function() @@ -40,33 +40,33 @@ function ResultsView:draw(results) return "" end end - G.push('all') - G.setFont(self.cfg.font) + gfx.push('all') + gfx.setFont(self.cfg.font) if not has_results then - G.setColor(Color.with_alpha(colors.results.fg, 0.5)) - G.print("No results", 25, 0) + gfx.setColor(Color.with_alpha(colors.results.fg, 0.5)) + gfx.print("No results", 25, 0) else for i, v in ipairs(results.results) do local ln = i local lh = (ln - 1) * fh local t = v.r.type local label = getLabel(t) - G.setColor(Color.with_alpha(colors.results.fg, 0.5)) - G.print(label, 2, lh + 2) - G.setColor(colors.results.fg) - G.print(v.r.name, 25, lh) + gfx.setColor(Color.with_alpha(colors.results.fg, 0.5)) + gfx.print(label, 2, lh + 2) + gfx.setColor(colors.results.fg) + gfx.print(v.r.name, 25, lh) end end - G.pop() + gfx.pop() end local draw_selection = function() local highlight_line = function(ln) if not ln then return end - G.setColor(colors.highlight) + gfx.setColor(colors.highlight) local l_y = (ln - 1) * fh - G.rectangle('fill', 0, l_y, width, fh) + gfx.rectangle('fill', 0, l_y, width, fh) end local v = results.selection highlight_line(v) diff --git a/src/view/input/statusline.lua b/src/view/input/statusline.lua index ad2f5fa9..f3c0d037 100644 --- a/src/view/input/statusline.lua +++ b/src/view/input/statusline.lua @@ -10,7 +10,7 @@ end) --- @param nLines integer --- @param time number? function Statusline:draw(status, nLines, time) - local G = love.graphics + local gfx = love.graphics local cf = self.cfg local colors = (function() if love.state.app_state == 'inspect' then @@ -34,10 +34,10 @@ function Statusline:draw(status, nLines, time) local midX = (start_box.x + w) / 2 local function drawBackground() - G.setColor(colors.bg) - G.setFont(font) + gfx.setColor(colors.bg) + gfx.setFont(font) local corr = 2 -- correct for fractional slit left under the terminal - G.rectangle("fill", start_box.x, start_box.y - corr, w, fh + corr) + gfx.rectangle("fill", start_box.x, start_box.y - corr, w, fh + corr) end --- @param m More? @@ -63,21 +63,21 @@ function Statusline:draw(status, nLines, time) y = start_box.y - 2, } - G.setColor(colors.fg) + gfx.setColor(colors.fg) local label = status.label if label then - G.print(label, start_text.x, start_text.y) + gfx.print(label, start_text.x, start_text.y) end if love.DEBUG then - G.setColor(cf.colors.debug) + gfx.setColor(cf.colors.debug) if love.state.testing then - G.print('testing', midX - (8 * cf.fw), start_text.y) + gfx.print('testing', midX - (8 * cf.fw), start_text.y) end - G.print(love.state.app_state, midX - (13 * cf.fw), start_text.y) + gfx.print(love.state.app_state, midX - (13 * cf.fw), start_text.y) if time then - G.print(tostring(time), midX, start_text.y) + gfx.print(tostring(time), midX, start_text.y) end - G.setColor(colors.fg) + gfx.setColor(colors.fg) end local c = status.cursor @@ -98,50 +98,50 @@ function Statusline:draw(status, nLines, time) local more_b = morelabel(custom.buffer_more) .. ' ' local more_i = morelabel(status.input_more) .. ' ' - G.setColor(colors.fg) - local w_il = G.getFont():getWidth(" 999:9999") - local w_br = G.getFont():getWidth("B999 L999-999(99)") - local w_mb = G.getFont():getWidth(" ↕↕ ") - local w_mi = G.getFont():getWidth(" ↕↕ ") + gfx.setColor(colors.fg) + local w_il = gfx.getFont():getWidth(" 999:9999") + local w_br = gfx.getFont():getWidth("B999 L999-999(99)") + local w_mb = gfx.getFont():getWidth(" ↕↕ ") + local w_mi = gfx.getFont():getWidth(" ↕↕ ") local s_mb = endTextX - w_br - w_il - w_mi - w_mb - local cw_p = G.getFont():getWidth(t_blp) - local cw_il = G.getFont():getWidth(t_ic) + local cw_p = gfx.getFont():getWidth(t_blp) + local cw_il = gfx.getFont():getWidth(t_ic) local sxl = endTextX - (cw_p + w_il + w_mi) local s_mi = endTextX - w_il - G.setFont(self.cfg.font) - G.setColor(colors.fg) - if colors.fg2 then G.setColor(colors.fg2) end + gfx.setFont(self.cfg.font) + gfx.setColor(colors.fg) + if colors.fg2 then gfx.setColor(colors.fg2) end --- cursor pos - G.print(t_ic, endTextX - cw_il, start_text.y) + gfx.print(t_ic, endTextX - cw_il, start_text.y) --- input more - G.print(more_i, s_mi, start_text.y - 3) + gfx.print(more_i, s_mi, start_text.y - 3) - G.setColor(colors.fg) + gfx.setColor(colors.fg) if custom.mode == 'reorder' and custom.content_type == 'plain' then - G.setColor(colors.special) + gfx.setColor(colors.special) end --- block line range / line - G.print(t_blp, sxl, start_text.y) - G.setColor(colors.fg) + gfx.print(t_blp, sxl, start_text.y) + gfx.setColor(colors.fg) --- block number if custom.content_type == 'lua' then - local bpw = G.getFont():getWidth(t_bbp) + local bpw = gfx.getFont():getWidth(t_bbp) local sxb = sxl - bpw if sel == lim then - G.setColor(colors.indicator) + gfx.setColor(colors.indicator) end if custom.mode == 'reorder' then - G.setColor(colors.special) + gfx.setColor(colors.special) end - G.print(t_bbp, sxb, start_text.y) + gfx.print(t_bbp, sxb, start_text.y) end --- buffer more - G.setColor(colors.fg) - G.print(more_b, s_mb, start_text.y) + gfx.setColor(colors.fg) + gfx.print(more_b, s_mb, start_text.y) else --- normal statusline local pos_c = ':' .. c.c @@ -154,22 +154,22 @@ function Statusline:draw(status, nLines, time) l_lim = status.n_lines end if ln == l_lim then - G.setColor(colors.indicator) + gfx.setColor(colors.indicator) end local pos_l = 'L' .. ln - local lw = G.getFont():getWidth(pos_l) - local cw = G.getFont():getWidth(pos_c) + local lw = gfx.getFont():getWidth(pos_l) + local cw = gfx.getFont():getWidth(pos_c) local sx = endTextX - (lw + cw) - G.print(pos_l, sx, start_text.y) - G.setColor(colors.fg) - G.print(pos_c, sx + lw, start_text.y) + gfx.print(pos_l, sx, start_text.y) + gfx.setColor(colors.fg) + gfx.print(pos_c, sx + lw, start_text.y) end end end - G.push('all') + gfx.push('all') drawBackground() drawStatus() - G.pop() + gfx.pop() end diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index ad63190d..625ec029 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -25,7 +25,7 @@ UserInputView = class.create(new) --- @param input InputDTO --- @param time number? function UserInputView:draw_input(input, time) - local G = love.graphics + local gfx = love.graphics local cfg = self.cfg local status = self.controller:get_status() @@ -46,7 +46,7 @@ function UserInputView:draw_input(input, time) local drawableWidth = cfg.drawableWidth local w = cfg.drawableChars -- drawtest hack - if drawableWidth < G.getWidth() / 3 then + if drawableWidth < gfx.getWidth() / 3 then w = w * 2 end @@ -103,15 +103,15 @@ function UserInputView:draw_input(input, time) local ch = start_y + (vcl - 1) * fh local x_offset = math.fmod(acc, w) - G.push('all') - G.setColor(cf_colors.input.cursor) - G.print('|', (x_offset - .5) * fw, ch) - G.pop() + gfx.push('all') + gfx.setColor(cf_colors.input.cursor) + gfx.print('|', (x_offset - .5) * fw, ch) + gfx.pop() end local drawBackground = function() - G.setColor(colors.bg) - G.rectangle("fill", + gfx.setColor(colors.bg) + gfx.rectangle("fill", 0, start_y, drawableWidth, @@ -120,7 +120,7 @@ function UserInputView:draw_input(input, time) local highlight = input.highlight local visible = vc:get_visible() - G.setFont(self.cfg.font) + gfx.setFont(self.cfg.font) drawBackground() self.statusline:draw(status, apparentLines, time) @@ -255,7 +255,7 @@ function UserInputView:draw_input(input, time) end else for l, str in ipairs(visible) do - G.setColor(colors.fg) + gfx.setColor(colors.fg) ViewUtils.write_line(l, str, start_y, 0, self.cfg) end end @@ -279,8 +279,8 @@ function UserInputView:draw(input, time) local apparentHeight = #err_text local start_y = h - inHeight local drawBackground = function() - G.setColor(colors.input.error_bg) - G.rectangle("fill", + gfx.setColor(colors.input.error_bg) + gfx.rectangle("fill", 0, start_y, drawableWidth, @@ -288,7 +288,7 @@ function UserInputView:draw(input, time) end drawBackground() - G.setColor(colors.input.error) + gfx.setColor(colors.input.error) for l, str in ipairs(err_text) do local breaks = 0 -- starting height is already calculated ViewUtils.write_line(l, str, start_y, breaks, self.cfg) diff --git a/src/view/titleView.lua b/src/view/titleView.lua index 5a4c8f42..27e2dd13 100644 --- a/src/view/titleView.lua +++ b/src/view/titleView.lua @@ -1,20 +1,20 @@ TitleView = { draw = function(title, x, y, w, custom_font) title = title or "LÖVEputer" - local prev_font = G.getFont() + local prev_font = gfx.getFont() local font = custom_font or prev_font local fh = font:getHeight() x = x or 0 - y = y or G.getHeight() - 2 * fh - w = w or G.getWidth() - G.setColor(Color[0]) - G.rectangle("fill", x, y, w, fh) + y = y or gfx.getHeight() - 2 * fh + w = w or gfx.getWidth() + gfx.setColor(Color[0]) + gfx.rectangle("fill", x, y, w, fh) local i = 1 local c = { 13, 12, 14, 10 } for lx = w - fh, w - 4 * fh, -fh do - G.setColor(Color[c[i]]) + gfx.setColor(Color[c[i]]) i = i + 1 - G.polygon("fill", + gfx.polygon("fill", lx, y, lx - fh, @@ -24,14 +24,14 @@ TitleView = { lx - fh, y + fh) end - G.setColor(Color[15]) + gfx.setColor(Color[15]) if custom_font then - G.setFont(font) + gfx.setFont(font) end - G.print(title, x + fh, y) + gfx.print(title, x + fh, y) if custom_font then - G.setFont(prev_font) + gfx.setFont(prev_font) end end } diff --git a/src/view/view.lua b/src/view/view.lua index 4cc29370..b56302ad 100644 --- a/src/view/view.lua +++ b/src/view/view.lua @@ -8,22 +8,22 @@ View = { --- @param C ConsoleController --- @param CV ConsoleView draw = function(C, CV) - G.push('all') + gfx.push('all') local terminal = C:get_terminal() local canvas = C:get_canvas() local input = C.input:get_input() CV:draw(terminal, canvas, input, canvas_snapshot) - G.pop() + gfx.pop() end, snap_canvas = function() - -- G.captureScreenshot(os.time() .. ".png") + -- gfx.captureScreenshot(os.time() .. ".png") if canvas_snapshot then View.clear_snapshot() collectgarbage() end - G.captureScreenshot(function(img) - canvas_snapshot = G.newImage(img) + gfx.captureScreenshot(function(img) + canvas_snapshot = gfx.newImage(img) end) end, diff --git a/tests/editor/buffer_spec.lua b/tests/editor/buffer_spec.lua index 68e40c57..a61ece0d 100644 --- a/tests/editor/buffer_spec.lua +++ b/tests/editor/buffer_spec.lua @@ -107,7 +107,7 @@ print(sierpinski(4))]]) describe('lua', function() local turtle = { '--- @diagnostic disable', - 'width, height = G.getDimensions()', + 'width, height = gfx.getDimensions()', 'midx = width / 2', 'midy = height / 2', 'incr = 5', @@ -119,15 +119,15 @@ print(sierpinski(4))]]) 'bg_color = Color.black', '', 'local function drawHelp()', - ' G.setColor(Color[Color.white])', - ' G.print("Press [I] to open console", 20, 20)', - ' G.print("Enter \'forward\', \'back\', \'left\', or \'right\' to move the turtle!", 20, 40)', + ' gfx.setColor(Color[Color.white])', + ' gfx.print("Press [I] to open console", 20, 20)', + ' gfx.print("Enter \'forward\', \'back\', \'left\', or \'right\' to move the turtle!", 20, 40)', 'end', '', 'local function drawDebuginfo()', - ' G.setColor(Color[debugColor])', + ' gfx.setColor(Color[debugColor])', ' local label = string.format("Turtle position: (%d, %d)", tx, ty)', - ' G.print(label, width - 200, 20)', + ' gfx.print(label, width - 200, 20)', 'end', '', 'function love.draw()', @@ -212,13 +212,13 @@ print(sierpinski(4))]]) assert.same('', embuf:get_text_content()[1]) embuf:move_selection('down', 2) assert.same(3, embuf:get_selection()) - assert.same({ 'width, height = G.getDimensions()' }, embuf:get_selected_text()) + assert.same({ 'width, height = gfx.getDimensions()' }, embuf:get_selected_text()) local res = { '', '--- @diagnostic disable', '', - 'width, height = G.getDimensions()', + 'width, height = gfx.getDimensions()', 'midx = width / 2', } assert.same(3, embuf:get_selection()) @@ -229,7 +229,7 @@ print(sierpinski(4))]]) embuf:insert_newline() assert.same(res, table.take(embuf:get_text_content(), 5)) embuf:move_selection('down') - assert.same({ 'width, height = G.getDimensions()' }, embuf:get_selected_text()) + assert.same({ 'width, height = gfx.getDimensions()' }, embuf:get_selected_text()) embuf:insert_newline() assert.same(res, table.take(embuf:get_text_content(), 5)) end) diff --git a/tests/interpreter/analyzer_inputs.lua b/tests/interpreter/analyzer_inputs.lua index 4987c615..98b5505d 100644 --- a/tests/interpreter/analyzer_inputs.lua +++ b/tests/interpreter/analyzer_inputs.lua @@ -210,7 +210,7 @@ end local fullclock = [[ --- @diagnostic disable: duplicate-set-field,lowercase-global -width, height = G.getDimensions() +width, height = gfx.getDimensions() midx = width / 2 midy = height / 2 @@ -232,7 +232,7 @@ s = 0 math.randomseed(os.time()) color = math.random(7) bg_color = math.random(7) -font = G.newFont(72) +font = gfx.newFont(72) local function pad(i) return string.format("%02d", i) @@ -249,15 +249,15 @@ function getTimestamp() end function love.draw() - G.setColor(Color[color + Color.bright]) - G.setBackgroundColor(Color[bg_color]) - G.setFont(font) + gfx.setColor(Color[color + Color.bright]) + gfx.setBackgroundColor(Color[bg_color]) + gfx.setFont(font) local text = getTimestamp() local l = string.len(text) local off_x = l * font:getWidth(' ') local off_y = font:getHeight() / 2 - G.print(text, midx - off_x, midy - off_y, 0, 1, 1) + gfx.print(text, midx - off_x, midy - off_y, 0, 1, 1) end function love.update(dt) From 6cb9da99ea46fdbf3baa73b9a73e5573ff89d726 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 12:04:41 +0200 Subject: [PATCH 013/103] style: cleanup --- src/controller/consoleController.lua | 1 + src/examples/clock/README.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 8b4afb7c..13944078 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -198,6 +198,7 @@ local function project_require(cc, name) else --- hack around love.js not having the bit lib if name == 'bit' and _G.web then + ---@diagnostic disable-next-line: inject-field pr_env.bit = o_require('util.luabit') end end diff --git a/src/examples/clock/README.md b/src/examples/clock/README.md index 33397073..667f5b74 100644 --- a/src/examples/clock/README.md +++ b/src/examples/clock/README.md @@ -83,11 +83,11 @@ function getTimestamp() return string.format("%s:%s:%s", hours, minutes, seconds) end ``` -Quick aside on format strings: for a digital clock, we usually want to always have the numbers displayed as two digits, with colons in between them. To achieve this, we `pad` all our results, then stitch them together with `string.format()`. +Quick aside on format strings: for a digital clock, we usually want to have the numbers displayed as two digits, with colons in between them. To achieve this, we `pad` all our results, then stitch them together with `string.format()`. In the above code, we are doing two different kinds of division.First, an integer division (`/`), going from seconds to minutes and hours, in this case we are only interested in the whole numbers, for example: 143 seconds is two full minutes and then some, but what the clock will be displaying is '02', so the remaining 23 seconds is not interesing for the minutes part. However, for 143 minutes, the display _should_ say 23, disregarding the two full hours, we are only interested in the remainder part. We can get this value by using `math.fmod()`, in this example, `math.fmod(143, 60)`. #### User documentation This program displays the current time in a randomly selected color over a randomly selected background. These colors can be changed by pressing [Space] and [Shift-Space], respectively. -Should the clock deviate from the correct time (for example, because the program run was paused), it can be reset with the [R] key. \ No newline at end of file +Should the clock deviate from the correct time (for example, because the program run was paused), it can be reset with the [R] key. From 244fe9cac580bfc165feddb39304be41ef1e5676 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 12:12:37 +0200 Subject: [PATCH 014/103] refactor(examples): take advantage of fixed require --- src/examples/tixy/examples.lua | 4 +++- src/examples/tixy/main.lua | 2 +- src/examples/turtle/action.lua | 4 +++- src/examples/turtle/main.lua | 3 +-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/examples/tixy/examples.lua b/src/examples/tixy/examples.lua index 963ffdf2..618eb7f5 100644 --- a/src/examples/tixy/examples.lua +++ b/src/examples/tixy/examples.lua @@ -1,4 +1,4 @@ -examples = {} +local examples = {} function example(c, l) table.insert(examples, { @@ -98,3 +98,5 @@ example( "return (x-5)^2 + (y-5)^2 - 99*sin(t)", "create your own!" ) + +return examples diff --git a/src/examples/tixy/main.lua b/src/examples/tixy/main.lua index aef89ecf..da687455 100644 --- a/src/examples/tixy/main.lua +++ b/src/examples/tixy/main.lua @@ -4,7 +4,7 @@ cw, ch = gfx.getDimensions() midx = cw / 2 require("math") -require("examples") +examples = require("examples") size = 28 spacing = 3 diff --git a/src/examples/turtle/action.lua b/src/examples/turtle/action.lua index e02c700d..ac80980c 100644 --- a/src/examples/turtle/action.lua +++ b/src/examples/turtle/action.lua @@ -18,7 +18,7 @@ function pause(msg) pause(msg or "user paused the game") end -actions = { +local actions = { forward = moveForward, fd = moveForward, back = moveBack, @@ -29,3 +29,5 @@ actions = { r = moveRight, pause = pause } + +return actions diff --git a/src/examples/turtle/main.lua b/src/examples/turtle/main.lua index 7fe896e5..7e570807 100644 --- a/src/examples/turtle/main.lua +++ b/src/examples/turtle/main.lua @@ -1,7 +1,6 @@ -require("action") +actions = require("action") require("drawing") -gfx = love.graphics width, height = gfx.getDimensions() midx = width / 2 midy = height / 2 From 17cc9838e451557a9d55b4ad57d0d0531c6913d3 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 12:33:34 +0200 Subject: [PATCH 015/103] test(analyze): accomodate new type --- tests/interpreter/analyzer_inputs.lua | 60 ++++++++++++++------------- tests/interpreter/analyzer_spec.lua | 2 +- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/tests/interpreter/analyzer_inputs.lua b/tests/interpreter/analyzer_inputs.lua index 98b5505d..cfdab8e2 100644 --- a/tests/interpreter/analyzer_inputs.lua +++ b/tests/interpreter/analyzer_inputs.lua @@ -1,7 +1,7 @@ --- @param s str ---- @param defs Assignment[] +--- @param semi SemanticInfo --- @return table {string[], string[]} -local prep = function(s, defs) +local prep = function(s, semi) local orig = (function() if type(s) == 'string' then return string.lines(s) @@ -14,7 +14,7 @@ local prep = function(s, defs) end end)() - return { orig, defs } + return { orig, semi } end local table1 = prep({ @@ -30,7 +30,7 @@ local table1 = prep({ ' z = 2,', ' 3,', '}', -}, { +}, SemanticInfo({ { name = 't', line = 1, type = 'local' }, { name = 't.ty', line = 2, type = 'field' }, { name = 't2', line = 4, type = 'global' }, @@ -38,13 +38,13 @@ local table1 = prep({ { name = 't2.w2', line = 6, type = 'field' }, { name = 'a', line = 8, type = 'global' }, { name = 'a.z', line = 10, type = 'field' }, -}) +}, {})) local table2 = prep({ 'tmp = {}', 'tmp[1] = 2', -}, { +}, SemanticInfo({ { name = 'tmp', line = 1, type = 'global' } -}) +}, {})) local simple = { --- sets @@ -53,21 +53,22 @@ local simple = { 'y = 3', 'x = 3', 'w, ww = 10, 11', - }, { + }, SemanticInfo({ { line = 1, name = 'x', type = 'global', }, { line = 2, name = 'y', type = 'global', }, { line = 3, name = 'x', type = 'global', }, { line = 4, name = 'w', type = 'global', }, { line = 4, name = 'ww', type = 'global', }, - }), + }, {})), + prep({ 'local l = 1', 'local x, y = 2, 3', - }, { + }, SemanticInfo({ { line = 1, name = 'l', type = 'local', }, { line = 2, name = 'x', type = 'local', }, { line = 2, name = 'y', type = 'local', }, - }), + }, {})), --- tables table1, -- table2, @@ -75,35 +76,35 @@ local simple = { prep({ 'function drawBackground()', 'end', - }, { + }, SemanticInfo({ { line = 1, name = 'drawBackground', type = 'function', }, - }), + }, {})), prep({ 'function love.draw()', ' draw()', 'end', - }, { + }, SemanticInfo({ { line = 1, name = 'love.draw', type = 'function', }, - }), + }, {})), prep({ 'function love.handlers.keypressed()', 'end', - }, { + }, SemanticInfo({ { line = 1, name = 'love.handlers.keypressed', type = 'function', }, - }), + }, {})), prep({ 'local function drawBody()', 'end', - }, { + }, SemanticInfo({ { name = 'drawBody', line = 1, type = 'function' } - }), + }, {})), --- methods prep({ 'function M:draw()', 'end', - }, { + }, SemanticInfo({ { name = 'M:draw', line = 1, type = 'method' } - }), + }, {})), } local sierpinski = [[function sierpinski(depth) @@ -295,14 +296,14 @@ end ]] local full = { - prep(sierpinski, { + prep(sierpinski, SemanticInfo({ { line = 1, name = 'sierpinski', type = 'function', }, { line = 2, name = 'lines', type = 'global', }, { line = 4, name = 'sp', type = 'global', }, { line = 5, name = 'tmp', type = 'global', }, { line = 10, name = 'lines', type = 'global', }, - }), - prep(clock, { + }, {})), + prep(clock, SemanticInfo({ { line = 1, name = 'love.draw', type = 'function', }, { line = 5, name = 'love.update', type = 'function', }, { line = 6, name = 't', type = 'global', }, @@ -312,8 +313,8 @@ local full = { { line = 16, name = 'love.keyreleased', type = 'function', }, { line = 19, name = 'bg_color', type = 'global', }, { line = 21, name = 'color', type = 'global', }, - }), - prep(meta, { + }, {})), + prep(meta, SemanticInfo({ { line = 3, name = 'M:extract_comments', type = 'method', }, { line = 4, name = 'lfi', type = 'local', }, { line = 5, name = 'lla', type = 'local', }, @@ -346,8 +347,8 @@ local full = { { line = 32, name = 'li.multiline', type = 'field', }, { line = 33, name = 'li.position', type = 'field', }, { line = 34, name = 'li.prepend_newline', type = 'field', }, - }), - prep(fullclock, { + }, {})), + prep(fullclock, SemanticInfo({ { line = 2, name = 'width', type = 'global', }, { line = 2, name = 'height', type = 'global', }, { line = 3, name = 'midx', type = 'global', }, @@ -386,7 +387,8 @@ local full = { { line = 69, name = 'bg_color', type = 'global', }, { line = 71, name = 'color', type = 'global', }, { line = 75, name = 'love.keyreleased', type = 'function', }, - }), + }, {})), +} } return { diff --git a/tests/interpreter/analyzer_spec.lua b/tests/interpreter/analyzer_spec.lua index 11d16417..37d62aec 100644 --- a/tests/interpreter/analyzer_spec.lua +++ b/tests/interpreter/analyzer_spec.lua @@ -96,7 +96,7 @@ describe('analyzer #analyzer', function() ---@diagnostic disable-next-line: param-type-mismatch local semDB = analyzer.analyze(r) it('matches ' .. i, function() - assert.same(output, semDB.assignments) + assert.same(output, semDB) end) else Log.warn('syntax error in input #' .. i) From 107b8191873efa1663b9e19a62891f8cc65c6ea6 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 12:36:06 +0200 Subject: [PATCH 016/103] refactor(analyzer): prepare SemanticInfo for requires --- src/model/lang/lua/analyze.lua | 52 +++++++++++++--------------- src/model/lang/lua/semantic_info.lua | 27 +++++++++++++++ 2 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 src/model/lang/lua/semantic_info.lua diff --git a/src/model/lang/lua/analyze.lua b/src/model/lang/lua/analyze.lua index f4c47ed7..4bf5ec27 100644 --- a/src/model/lang/lua/analyze.lua +++ b/src/model/lang/lua/analyze.lua @@ -1,19 +1,7 @@ require('util.tree') +require('model.lang.lua.semantic_info') ---- @alias AssignmentType ---- | 'function' ---- | 'method' ---- | 'local' ---- | 'global' ---- | 'field' - ---- @class Assignment ---- @field name string ---- @field line integer ---- @field type AssignmentType - ---- @class SemanticInfo ---- @field assignments Assignment[] +--- utils local keywords_list = { "and", @@ -43,6 +31,16 @@ for _, kw in pairs(keywords_list) do keywords[kw] = true end +--- @param n AST +--- @return number? +local function get_line_number(n) + local li = n.lineinfo + local li_f = type(li) == 'table' and li.first + return type(li_f) == 'table' and li_f.line or nil +end + +--- assignments + --- @param ast AST --- @return string? local function get_idx_stack(ast) @@ -82,15 +80,6 @@ end local function definition_extractor(node) local deftags = { 'Local', 'Localrec', 'Set' } - local function get_line_number(n) - local li = n.lineinfo - local li_f = type(li) == 'table' and li.first - return type(li_f) == 'table' and li_f.line - end - local function get_lhs_name(n) - return n[1] - end - if type(node) == 'table' and node.tag then local tag = node.tag if table.is_member(deftags, tag) then @@ -170,7 +159,7 @@ local function definition_extractor(node) at = 'global' end for i, w in ipairs(lhs) do - local n = get_lhs_name(w) + local n = w[1] if is_local and not rhs[i] then dec_only = true end @@ -203,8 +192,8 @@ local function defmatch(name) end --- @param ast AST ---- @return SemanticInfo -local function analyze(ast) +--- @return Assignment[] +local function get_assignments(ast) local sets = table.flatten( Tree.preorder(ast, definition_extractor) ) @@ -231,7 +220,16 @@ local function analyze(ast) table.insert(candidates, v) end end - return { assignments = assignments } + return assignments +end + + +--- @param ast AST +--- @return string[] +local function analyze(ast) + local assignments = get_assignments(ast) + local reqs = {} + return SemanticInfo(assignments, reqs) end return { diff --git a/src/model/lang/lua/semantic_info.lua b/src/model/lang/lua/semantic_info.lua new file mode 100644 index 00000000..908c8d15 --- /dev/null +++ b/src/model/lang/lua/semantic_info.lua @@ -0,0 +1,27 @@ +--- @class SemanticInfoBase +--- @field name string +--- @field line integer + +--- @alias AssignmentType +--- | 'function' +--- | 'method' +--- | 'local' +--- | 'global' +--- | 'field' + +--- @class Assignment : SemanticInfoBase +--- @field type AssignmentType + +--- @class Require : SemanticInfoBase + +local class = require('util.class') + +--- @class SemanticInfo +--- @field assignments Assignment[] +--- @field requires Require[] +SemanticInfo = class.create(function(asn, reqs) + return { + assignments = asn or {}, + requires = reqs or {}, + } +end) From 876a2ffbfde82aeea1bdbe8c6948aa972d44b83c Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 12:44:38 +0200 Subject: [PATCH 017/103] feat(analyzer): support requires --- src/model/lang/lua/analyze.lua | 38 ++++++++++++++++++++++++++- tests/interpreter/analyzer_inputs.lua | 24 +++++++++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/model/lang/lua/analyze.lua b/src/model/lang/lua/analyze.lua index 4bf5ec27..410e91d5 100644 --- a/src/model/lang/lua/analyze.lua +++ b/src/model/lang/lua/analyze.lua @@ -223,12 +223,48 @@ local function get_assignments(ast) return assignments end +--- requires + +--- @param node AST +--- @return Require[] +local function req_extractor(node) + local calltags = { 'Call' } + if type(node) == 'table' and node.tag then + local tag = node.tag + if table.is_member(calltags, tag) then + local lhs = node[1] + local rhs = node[2] + + if tag == 'Call' + and lhs.tag == 'Id' and lhs[1] == 'require' + then + local val = rhs[1] + local li = get_line_number(rhs) + return { { line = li, name = val } } + end + end + end + return {} +end + +--- @param ast AST +--- @return Require[] +local function get_requires(ast) + local candidates = table.flatten( + Tree.preorder(ast, req_extractor) + ) + local reqs = {} + for _, v in ipairs(candidates or {}) do + table.insert(reqs, v) + end + return reqs +end --- @param ast AST --- @return string[] local function analyze(ast) local assignments = get_assignments(ast) - local reqs = {} + local reqs = get_requires(ast) return SemanticInfo(assignments, reqs) end diff --git a/tests/interpreter/analyzer_inputs.lua b/tests/interpreter/analyzer_inputs.lua index cfdab8e2..bf55daab 100644 --- a/tests/interpreter/analyzer_inputs.lua +++ b/tests/interpreter/analyzer_inputs.lua @@ -389,9 +389,29 @@ local full = { { line = 75, name = 'love.keyreleased', type = 'function', }, }, {})), } + +local req = { + prep({ + 'require ("math")', + 'x = sin(pi)', + }, SemanticInfo({ + { name = 'x', line = 2, type = 'global' } + }, { + { name = 'math', line = 1 } + })), + prep({ + 'require("action")', + 'print "req test"', + 'require("drawing")', + }, SemanticInfo({ + }, { + { name = 'action', line = 1 }, + { name = 'drawing', line = 3 }, + })), } return { - { 'simple', simple }, - { 'full', full }, + { 'simple', simple }, + { 'full', full }, + { 'require', req }, } From 4a82ff2463eae3c24d69131d422ff12a7ea9f034 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 13:37:55 +0200 Subject: [PATCH 018/103] feat: log stacktrace on execution error --- src/controller/controller.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index de71ac6e..946b6a63 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -8,6 +8,9 @@ local messages = { user_break = "BREAK into program", exit_anykey = "Press any key to exit.", exec_error = function(err) + Log.error((debug.traceback( + "Error: " .. tostring(err), 1):gsub("\n[^\n]+$", "") + )) return 'Execution error at ' .. err end } From 74bc17f444a7b97396b74f40aaf05e7fbf72ad61 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 13:41:37 +0200 Subject: [PATCH 019/103] fix: disable selection in user_input --- src/controller/consoleController.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 13944078..6e463b8f 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -341,24 +341,24 @@ function ConsoleController.prepare_project_env(cc) require("controller.userInputController") require("model.input.userInputModel") require("view.input.userInputView") - local cfg = cc.model.cfg + local cfg = cc.model.cfg ---@type table - local project_env = cc:get_pre_env_c() + local project_env = cc:get_pre_env_c() project_env.gfx = love.graphics - project_env.require = function(name) + project_env.require = function(name) return project_require(cc, name) end --- @param msg string? - project_env.pause = function(msg) + project_env.pause = function(msg) cc:suspend_run(msg) end - project_env.stop = function() + project_env.stop = function() cc:stop_project_run() end - project_env.continue = function() + project_env.continue = function() if love.state.app_state == 'inspect' then -- resume love.state.app_state = 'running' @@ -388,7 +388,7 @@ function ConsoleController.prepare_project_env(cc) if not input_ref then return end ui_model = UserInputModel(cfg, eval, true, prompt) ui_model:set_text(init) - local inp_con = UserInputController(ui_model, input_ref) + local inp_con = UserInputController(ui_model, input_ref, true) local view = UserInputView(cfg.view, inp_con) love.state.user_input = { M = ui_model, C = inp_con, V = view From 759089f44a317642d518085bc4f3a21446e875ba Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 16:09:36 +0200 Subject: [PATCH 020/103] feat: pass Console reference to Editor --- src/controller/consoleController.lua | 5 +++-- src/controller/editorController.lua | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 6e463b8f..2210eb7c 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -35,13 +35,11 @@ function ConsoleController.new(M, main_ctrl) local config = M.cfg pre_env.font = config.view.font local IC = UserInputController(M.input) - local EC = EditorController(M.editor) local self = setmetatable({ time = 0, model = M, main_ctrl = main_ctrl, input = IC, - editor = EC, -- console runner env main_env = env, -- copy of the application's env before the prep @@ -58,6 +56,9 @@ function ConsoleController.new(M, main_ctrl) cfg = config }, ConsoleController) + --- the editor has to know about us + local EC = EditorController(M.editor, self) + self.editor = EC -- initialize the stub env tables ConsoleController.prepare_env(self) ConsoleController.prepare_project_env(self) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index b79d89c1..b48abb8f 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -6,7 +6,8 @@ require("view.input.customStatus") local class = require('util.class') --- @param M EditorModel -local function new(M) +--- @oaram CC ConsoleController +local function new(M, CC) return { input = UserInputController(M.input, nil, true), model = M, @@ -14,6 +15,7 @@ local function new(M) M.search, UserInputController(M.search.input, nil, true) ), + console = CC, view = nil, mode = 'edit', } @@ -28,6 +30,7 @@ end --- @field model EditorModel --- @field input UserInputController --- @field search SearchController +--- @field console ConsoleController --- @field view EditorView? --- @field state EditorState? --- @field mode EditorMode From 38e92deef9a6b408fb6fbee21040ddc5fb49d81a Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 16:09:59 +0200 Subject: [PATCH 021/103] feat(table): add find_by_v --- src/util/table.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/util/table.lua b/src/util/table.lua index d3d91ecc..ed9f9c0d 100644 --- a/src/util/table.lua +++ b/src/util/table.lua @@ -281,6 +281,17 @@ function table.find_by(self, pred) end end +--- Find first element that the predicate holds for +--- @param self table[] +--- @param pred function +--- @return any? +function table.find_by_v(self, pred) + if not self or not pred then return end + for _, v in pairs(self) do + if pred(v) then return v end + end +end + --- Filter elements that satisfy the predicate --- enumerates sequentially --- @param self table[] From 10bb61054c58c6bf94aab842bcdd5e9c736579e3 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 7 Oct 2025 16:10:59 +0200 Subject: [PATCH 022/103] feat(editor): add initial 'step into required' function --- src/controller/editorController.lua | 19 +++++++++++++++++++ src/model/editor/bufferModel.lua | 2 +- src/model/editor/bufferSemanticInfo.lua | 14 +++++++++++--- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index b48abb8f..697cafe6 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -91,6 +91,20 @@ function EditorController:open(name, content, save) self:set_state() end +function EditorController:follow_require() + local buf = self:get_active_buffer() + if not buf.semantic then return end + local bn = buf:get_selection() + local reqs = buf.semantic.requires + local reqsel = table.find_by_v(reqs, function(r) + return r.block == bn + end) + if reqsel then + local name = reqsel.name + self.console:edit(name .. '.lua') + end +end + --- @param m EditorMode --- @return boolean local function is_normal(m) @@ -612,6 +626,11 @@ function EditorController:_normal_mode_keys(k) and k == "pagedown" then self:_scroll('down', false, 1) end + + -- step into + if k == "f4" then + self:follow_require() + end end local function clear() if Key.ctrl() and k == "w" then diff --git a/src/model/editor/bufferModel.lua b/src/model/editor/bufferModel.lua index cac3a5c9..109ff87a 100644 --- a/src/model/editor/bufferModel.lua +++ b/src/model/editor/bufferModel.lua @@ -215,7 +215,7 @@ function BufferModel:set_selection(sel) end --- Get index of selected line/block ---- @return integer +--- @return integer blocknum function BufferModel:get_selection() return self.selection end diff --git a/src/model/editor/bufferSemanticInfo.lua b/src/model/editor/bufferSemanticInfo.lua index 76f7e500..3895c2b0 100644 --- a/src/model/editor/bufferSemanticInfo.lua +++ b/src/model/editor/bufferSemanticInfo.lua @@ -5,6 +5,9 @@ require('util.table') --- @class Definition: Assignment --- @field block blocknum +--- @class RequireCall: Require +--- @field block blocknum + --- @class BufferSemanticInfo --- @field definitions Definition[] @@ -12,14 +15,19 @@ require('util.table') --- @param rev table --- @return BufferSemanticInfo local function convert(si, rev) - local as = si.assignments - local defs = table.map(as, function(a) + local blockmap = function(a) local r = table.clone(a) r.block = rev[a.line] return r - end) + end + local as = si.assignments + local defs = table.map(as, blockmap) + local rs = si.requires + local reqs = table.map(rs, blockmap) + return { definitions = defs, + requires = reqs, } end From 2d436bdc006dac8db9db152328a720927ab06f7c Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 8 Oct 2025 12:34:23 +0200 Subject: [PATCH 023/103] style: sort out AST annotations --- src/lib/djot/djot.lua | 8 ++++---- src/lib/djot/djot/ast.lua | 22 +++++++++++----------- src/lib/djot/djot/filter.lua | 2 +- src/model/interpreter/eval/evaluator.lua | 2 +- src/model/lang/lua/analyze.lua | 16 ++++++++-------- src/model/lang/lua/parser.lua | 3 ++- src/model/lang/md/parser.lua | 2 +- 7 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/lib/djot/djot.lua b/src/lib/djot/djot.lua index f9a84eb3..3dfbc267 100644 --- a/src/lib/djot/djot.lua +++ b/src/lib/djot/djot.lua @@ -60,7 +60,7 @@ end --- @param sourcepos (boolean?) if true, source positions are included in the AST --- @param warn (function?) function that processes a warning, accepting a warning --- object with `pos` and `message` fields. ---- @return (AST) +--- @return (djotAST) local function parse(input, sourcepos, warn) local parser = Parser:new(input, warn) return ast.to_ast(parser, sourcepos or false) @@ -82,7 +82,7 @@ local function parse_events(input, warn) end --- Render a document's AST in human-readable form. ---- @param doc (AST) the AST +--- @param doc (djotAST) the AST --- @return (string) rendered AST local function render_ast_pretty(doc) local handle = StringHandle:new() @@ -91,14 +91,14 @@ local function render_ast_pretty(doc) end --- Render a document's AST in JSON. ---- @param doc (AST) the AST +--- @param doc (djotAST) the AST --- @return (string) rendered AST (JSON string) local function render_ast_json(doc) return json.encode(doc) .. "\n" end --- Render a document as HTML. ---- @param doc (AST) the AST +--- @param doc (djotAST) the AST --- @return (string) rendered document (HTML string) local function render_html(doc) local handle = StringHandle:new() diff --git a/src/lib/djot/djot/ast.lua b/src/lib/djot/djot/ast.lua index cd875b5a..6bbb81cf 100644 --- a/src/lib/djot/djot/ast.lua +++ b/src/lib/djot/djot/ast.lua @@ -5,10 +5,10 @@ --- @field class? string --- @field id? string ---- @class AST +--- @class djotAST --- @field t string tag for the node --- @field s? string text for the node ---- @field c AST[] child node +--- @field c djotAST[] child node --- @field alias string --- @field level integer --- @field startidx integer @@ -236,7 +236,7 @@ end, function(k) return displaykeys[k] or k end) --- Create a new AST node. --- @param tag (string) tag for the node ---- @return (AST) node (table) +--- @return (djotAST) node (table) local function new_node(tag) local node = { t = tag, c = nil } setmetatable(node, mt) @@ -244,8 +244,8 @@ local function new_node(tag) end --- Add `child` as a child of `node`. ---- @param node (AST) node parent node ---- @param child (AST) node child node +--- @param node (djotAST) node parent node +--- @param child (djotAST) node child node local function add_child(node, child) if (not node.c) then node.c = { child } @@ -255,7 +255,7 @@ local function add_child(node, child) end --- Returns true if `node` has children. ---- @param node (AST) node to check +--- @param node (djotAST) node to check --- @return (boolean) true if node has children local function has_children(node) return (node.c and #node.c > 0) @@ -303,8 +303,8 @@ local function copy_attributes(target, source) end end ---- @param targetnode (AST) ---- @param cs (AST) +--- @param targetnode (djotAST) +--- @param cs (djotAST) local function insert_attributes_from_nodes(targetnode, cs) targetnode.attr = targetnode.attr or new_attributes() local i = 1 @@ -325,7 +325,7 @@ local function insert_attributes_from_nodes(targetnode, cs) end end ---- @param node (AST) +--- @param node (djotAST) local function make_definition_list_item(node) node.t = "definition_list_item" if not has_children(node) then @@ -669,7 +669,7 @@ local function to_ast(parser, sourcepos) elseif node.t == "attributes" then -- parse attributes, add to last node local tip = containers[#containers] - --- @type AST|false + --- @type djotAST|false local prevnode = has_children(tip) and tip.c[#tip.c] if prevnode then local endswithspace = false @@ -987,7 +987,7 @@ end --- Render an AST in human-readable form, with indentation --- showing the hierarchy. ---- @param doc (AST) djot AST +--- @param doc (djotAST) djot AST --- @param handle (StringHandle) handle to which to write content local function render(doc, handle) render_node(doc, handle, 0) diff --git a/src/lib/djot/djot/filter.lua b/src/lib/djot/djot/filter.lua index bd28fc2b..f9fd4fd3 100644 --- a/src/lib/djot/djot/filter.lua +++ b/src/lib/djot/djot/filter.lua @@ -104,7 +104,7 @@ local function traverse(node, filterpart) end --- Apply a filter to a document. ---- @param node (AST) +--- @param node (djotAST) --- @param filter table the filter to apply local function apply_filter(node, filter) for _, filterpart in ipairs(filter) do diff --git a/src/model/interpreter/eval/evaluator.lua b/src/model/interpreter/eval/evaluator.lua index 12161efb..642870be 100644 --- a/src/model/interpreter/eval/evaluator.lua +++ b/src/model/interpreter/eval/evaluator.lua @@ -48,7 +48,7 @@ end --- @param s string[] --- @return boolean ok --- @return str content|errpr ---- @return AST? ast +--- @return luaAST? ast local function default_apply(self, s) local valid, errors = validate(self, s) local parser = self.parser diff --git a/src/model/lang/lua/analyze.lua b/src/model/lang/lua/analyze.lua index 410e91d5..1cd29ff7 100644 --- a/src/model/lang/lua/analyze.lua +++ b/src/model/lang/lua/analyze.lua @@ -31,7 +31,7 @@ for _, kw in pairs(keywords_list) do keywords[kw] = true end ---- @param n AST +--- @param n luaAST --- @return number? local function get_line_number(n) local li = n.lineinfo @@ -41,7 +41,7 @@ end --- assignments ---- @param ast AST +--- @param ast luaAST --- @return string? local function get_idx_stack(ast) --- @return string? @@ -59,7 +59,7 @@ local function get_idx_stack(ast) return go(ast) end ---- @param node AST +--- @param node luaAST --- @return boolean local function is_idx_stack(node) local st = get_idx_stack(node) @@ -75,7 +75,7 @@ local function is_ident(id) return string["match"](id, "^[%a_][%w_]*$") and not keywords[id] end ---- @param node AST +--- @param node luaAST --- @return table? local function definition_extractor(node) local deftags = { 'Local', 'Localrec', 'Set' } @@ -191,7 +191,7 @@ local function defmatch(name) end end ---- @param ast AST +--- @param ast luaAST --- @return Assignment[] local function get_assignments(ast) local sets = table.flatten( @@ -225,7 +225,7 @@ end --- requires ---- @param node AST +--- @param node luaAST --- @return Require[] local function req_extractor(node) local calltags = { 'Call' } @@ -247,7 +247,7 @@ local function req_extractor(node) return {} end ---- @param ast AST +--- @param ast luaAST --- @return Require[] local function get_requires(ast) local candidates = table.flatten( @@ -260,7 +260,7 @@ local function get_requires(ast) return reqs end ---- @param ast AST +--- @param ast luaAST --- @return string[] local function analyze(ast) local assignments = get_assignments(ast) diff --git a/src/model/lang/lua/parser.lua b/src/model/lang/lua/parser.lua index b5c66baa..c204e689 100644 --- a/src/model/lang/lua/parser.lua +++ b/src/model/lang/lua/parser.lua @@ -5,7 +5,7 @@ require("util.debug") require("util.string.string") require("util.dequeue") ---- @class luaAST : token[] +--- @class luaAST : token --- @alias CPos 'first'|'last' @@ -288,6 +288,7 @@ return function(lib) local w = wrap or 80 local ok, r = parse(code) if ok then + --- @diagnostic disable-next-line: param-type-mismatch local src = ast_to_src(r, {}, w) return string.lines(src) end diff --git a/src/model/lang/md/parser.lua b/src/model/lang/md/parser.lua index 6d724b93..d5fccf89 100644 --- a/src/model/lang/md/parser.lua +++ b/src/model/lang/md/parser.lua @@ -69,7 +69,7 @@ end --- @param input str --- @param skip_posinfo boolean? ---- @return AST -- djot AST, distinct from metalua +--- @return djotAST local function parse(input, skip_posinfo) local text = string.unlines(input) local posinfo = not (skip_posinfo == true) From 0366fe433286abccd3cc5af94dc20ec322d99463 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 8 Oct 2025 12:36:28 +0200 Subject: [PATCH 024/103] refactor: fix linter warnings --- src/harmony/init.lua | 2 +- src/lib/djot/djot/ast.lua | 2 +- src/lib/hump/timer.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/harmony/init.lua b/src/harmony/init.lua index 2edc0d3a..1d6fd109 100644 --- a/src/harmony/init.lua +++ b/src/harmony/init.lua @@ -150,7 +150,7 @@ local function utils() if not love.harmony then return end if love.harmony.utils then return end - gfx = love.graphics + local gfx = love.graphics --- @param name love.Event local love_event = function(name, ...) diff --git a/src/lib/djot/djot/ast.lua b/src/lib/djot/djot/ast.lua index 6bbb81cf..85cd2a2d 100644 --- a/src/lib/djot/djot/ast.lua +++ b/src/lib/djot/djot/ast.lua @@ -416,7 +416,7 @@ local function to_ast(parser, sourcepos) local subject = parser.subject local warn = parser.warn if not warn then - warn = function() end + warn = function(o) end end local sourceposmap if sourcepos then diff --git a/src/lib/hump/timer.lua b/src/lib/hump/timer.lua index cd425bb8..f441a262 100644 --- a/src/lib/hump/timer.lua +++ b/src/lib/hump/timer.lua @@ -124,7 +124,7 @@ local function func_tween(tween, self, len, subject, target, method, after, to_func_tween[subject] = {} end - ref = {getter(subject)} + local ref = { getter(subject) } to_func_tween[subject][k] = {ref, setter} if type(v) == 'number' or #ref == 1 then v = {v} From 924f2cde046cea6b425777cd4348a83990945c23 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 8 Oct 2025 12:36:53 +0200 Subject: [PATCH 025/103] style: format timer lib source --- src/lib/hump/timer.lua | 377 +++++++++++++++++++++-------------------- 1 file changed, 189 insertions(+), 188 deletions(-) diff --git a/src/lib/hump/timer.lua b/src/lib/hump/timer.lua index f441a262..f7f9933a 100644 --- a/src/lib/hump/timer.lua +++ b/src/lib/hump/timer.lua @@ -22,7 +22,7 @@ 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 Timer = {} Timer.__index = Timer @@ -31,219 +31,220 @@ local function _nothing_() end local unpack = unpack or table.unpack local function updateTimerHandle(handle, dt) - -- handle: { - -- time = , - -- after = , - -- during = , - -- limit = , - -- count = , - -- } - handle.time = handle.time + dt - handle.during(dt, math.max(handle.limit - handle.time, 0)) - - while handle.time >= handle.limit and handle.count > 0 do - if handle.after(handle.after) == false then - handle.count = 0 - break - end - handle.time = handle.time - handle.limit - handle.count = handle.count - 1 - end + -- handle: { + -- time = , + -- after = , + -- during = , + -- limit = , + -- count = , + -- } + handle.time = handle.time + dt + handle.during(dt, math.max(handle.limit - handle.time, 0)) + + while handle.time >= handle.limit and handle.count > 0 do + if handle.after(handle.after) == false then + handle.count = 0 + break + end + handle.time = handle.time - handle.limit + handle.count = handle.count - 1 + end end function Timer:update(dt) - -- timers may create new timers, which leads to undefined behavior - -- in pairs() - so we need to put them in a different table first - local to_update = {} - for handle in pairs(self.functions) do - to_update[handle] = handle - end - - for handle in pairs(to_update) do - if self.functions[handle] then - updateTimerHandle(handle, dt) - if handle.count == 0 then - self.functions[handle] = nil - end - end - end + -- timers may create new timers, which leads to undefined behavior + -- in pairs() - so we need to put them in a different table first + local to_update = {} + for handle in pairs(self.functions) do + to_update[handle] = handle + end + + for handle in pairs(to_update) do + if self.functions[handle] then + updateTimerHandle(handle, dt) + if handle.count == 0 then + self.functions[handle] = nil + end + end + end end function Timer:during(delay, during, after) - local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 } - self.functions[handle] = true - return handle + local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 } + self.functions[handle] = true + return handle end function Timer:after(delay, func) - return self:during(delay, _nothing_, func) + return self:during(delay, _nothing_, func) end function Timer:every(delay, after, count) - local count = count or math.huge -- exploit below: math.huge - 1 = math.huge - local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count } - self.functions[handle] = true - return handle + local count = count or math.huge -- exploit below: math.huge - 1 = math.huge + local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count } + self.functions[handle] = true + return handle end function Timer:cancel(handle) - self.functions[handle] = nil + self.functions[handle] = nil end function Timer:clear() - self.functions = {} + self.functions = {} end function Timer:script(f) - local co = coroutine.wrap(f) - co(function(t) - self:after(t, co) - coroutine.yield() - end) + local co = coroutine.wrap(f) + co(function(t) + self:after(t, co) + coroutine.yield() + end) end local function func_tween(tween, self, len, subject, target, method, after, - setters_and_getters, ...) - -- recursively collects fields that are defined in both subject and target into a flat list - -- re-use of ref is confusing - local to_func_tween = {} - local function set_and_get(subject, k, v) - setters_and_getters = setters_and_getters or {} - - local setter, getter - if setters_and_getters[k] then - setter, getter = unpack(setters_and_getters[k]) - else - setter = subject['set'..k] - getter = subject['get'..k] + setters_and_getters, ...) + -- recursively collects fields that are defined in both subject and target into a flat list + -- re-use of ref is confusing + local to_func_tween = {} + local function set_and_get(subject, k, v) + setters_and_getters = setters_and_getters or {} + + local setter, getter + if setters_and_getters[k] then + setter, getter = unpack(setters_and_getters[k]) + else + setter = subject['set' .. k] + getter = subject['get' .. k] + end + assert(setter and getter, + "key's value in subject is nil with no set/getter") + + if to_func_tween[subject] == nil then + to_func_tween[subject] = {} + end + + local ref = { getter(subject) } + to_func_tween[subject][k] = { ref, setter } + if type(v) == 'number' or #ref == 1 then + v = { v } + end + return ref, v + end + + local function tween_collect_payload(subject, target, out) + for k, v in pairs(target) do + -- this might not be the smoothest way to do this + local ref = subject[k] + if ref == nil then + ref, v = set_and_get(subject, k, v) end - assert(setter and getter, - "key's value in subject is nil with no set/getter") - - if to_func_tween[subject] == nil then - to_func_tween[subject] = {} - end - - local ref = { getter(subject) } - to_func_tween[subject][k] = {ref, setter} - if type(v) == 'number' or #ref == 1 then - v = {v} - end - return ref, v - end - - local function tween_collect_payload(subject, target, out) - for k,v in pairs(target) do - - -- this might not be the smoothest way to do this - local ref = subject[k] - if ref == nil then - ref, v = set_and_get(subject, k, v) - end - assert(type(v) == type(ref), 'Type mismatch in field "'..k..'". ' - ..type(v)..' vs '.. type(ref)) - if type(v) == 'table' then - tween_collect_payload(ref, v, out) - else - local ok, delta = pcall(function() return (v-ref)*1 end) - assert(ok, 'Field "'..k..'" does not support arithmetic operations') - out[#out+1] = {subject, k, delta} - end - end - return out - end - - method = tween[method or 'linear'] -- see __index - local payload, t, args = tween_collect_payload(subject, target, {}), 0, {...} - - local last_s = 0 - return self:during(len, function(dt) - t = t + dt - local s = method(math.min(1, t/len), unpack(args)) - local ds = s - last_s - last_s = s - for _, info in ipairs(payload) do - local ref, key, delta = unpack(info) - ref[key] = ref[key] + delta * ds + assert(type(v) == type(ref), 'Type mismatch in field "' .. k .. '". ' + .. type(v) .. ' vs ' .. type(ref)) + if type(v) == 'table' then + tween_collect_payload(ref, v, out) + else + local ok, delta = pcall(function() return (v - ref) * 1 end) + assert(ok, 'Field "' .. k .. '" does not support arithmetic operations') + out[#out + 1] = { subject, k, delta } end - for ref, t in pairs(to_func_tween) do - for key, value in pairs(t) do - local setter_args, setter = unpack(value) - if not pcall(function() setter(ref, unpack(setter_args)) end) then - setter(unpack(setter_args)) - end - end + end + return out + end + + method = tween[method or 'linear'] -- see __index + local payload, t, args = tween_collect_payload(subject, target, {}), 0, { ... } + + local last_s = 0 + return self:during(len, function(dt) + t = t + dt + local s = method(math.min(1, t / len), unpack(args)) + local ds = s - last_s + last_s = s + for _, info in ipairs(payload) do + local ref, key, delta = unpack(info) + ref[key] = ref[key] + delta * ds + end + for ref, t in pairs(to_func_tween) do + for key, value in pairs(t) do + local setter_args, setter = unpack(value) + if not pcall(function() setter(ref, unpack(setter_args)) end) then + setter(unpack(setter_args)) + end end - end, after) + end + end, after) end local function plain_tween(tween, self, len, subject, target, method, after, ...) - return func_tween(tween, self, len, subject, target, method, after, nil, ...) + return func_tween(tween, self, len, subject, target, method, after, nil, ...) end local function def_tween(func) - return setmetatable( - { - -- helper functions - out = function(f) -- 'rotates' a function - return function(s, ...) return 1 - f(1-s, ...) end - end, - chain = function(f1, f2) -- concatenates two functions - return function(s, ...) return (s < .5 and f1(2*s, ...) or 1 + f2(2*s-1, ...)) * .5 end - end, - - -- useful tweening functions - linear = function(s) return s end, - quad = function(s) return s*s end, - cubic = function(s) return s*s*s end, - quart = function(s) return s*s*s*s end, - quint = function(s) return s*s*s*s*s end, - sine = function(s) return 1-math.cos(s*math.pi/2) end, - expo = function(s) return 2^(10*(s-1)) end, - circ = function(s) return 1 - math.sqrt(1-s*s) end, - - back = function(s,bounciness) - bounciness = bounciness or 1.70158 - return s*s*((bounciness+1)*s - bounciness) - end, - - bounce = function(s) -- magic numbers ahead - local a,b = 7.5625, 1/2.75 - return math.min(a*s^2, a*(s-1.5*b)^2 + .75, a*(s-2.25*b)^2 + .9375, a*(s-2.625*b)^2 + .984375) - end, - - elastic = function(s, amp, period) - amp, period = amp and math.max(1, amp) or 1, period or .3 - return (-amp * math.sin(2*math.pi/period * (s-1) - math.asin(1/amp))) * 2^(10*(s-1)) - end, - - - }, { - - -- register new tween - __call = func, - - -- fetches function and generated compositions for method `key` - __index = function(tweens, key) - if type(key) == 'function' then return key end - - assert(type(key) == 'string', 'Method must be function or string.') - if rawget(tweens, key) then return rawget(tweens, key) end - - local function construct(pattern, f) - local method = rawget(tweens, key:match(pattern)) - if method then return f(method) end - return nil - end - - local out, chain = rawget(tweens,'out'), rawget(tweens,'chain') - return construct('^in%-([^-]+)$', function(...) return ... end) - or construct('^out%-([^-]+)$', out) - or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end) - or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end) - or error('Unknown interpolation method: ' .. key) - end}) + return setmetatable( + { + -- helper functions + out = function(f) -- 'rotates' a function + return function(s, ...) return 1 - f(1 - s, ...) end + end, + chain = function(f1, f2) -- concatenates two functions + return function(s, ...) return (s < .5 and f1(2 * s, ...) or 1 + f2(2 * s - 1, ...)) * .5 end + end, + + -- useful tweening functions + linear = function(s) return s end, + quad = function(s) return s * s end, + cubic = function(s) return s * s * s end, + quart = function(s) return s * s * s * s end, + quint = function(s) return s * s * s * s * s end, + sine = function(s) return 1 - math.cos(s * math.pi / 2) end, + expo = function(s) return 2 ^ (10 * (s - 1)) end, + circ = function(s) return 1 - math.sqrt(1 - s * s) end, + + back = function(s, bounciness) + bounciness = bounciness or 1.70158 + return s * s * ((bounciness + 1) * s - bounciness) + end, + + bounce = function(s) -- magic numbers ahead + local a, b = 7.5625, 1 / 2.75 + return math.min(a * s ^ 2, a * (s - 1.5 * b) ^ 2 + .75, a * (s - 2.25 * b) ^ 2 + .9375, a * (s - 2.625 * b) ^ 2 + + .984375) + end, + + elastic = function(s, amp, period) + amp, period = amp and math.max(1, amp) or 1, period or .3 + return (-amp * math.sin(2 * math.pi / period * (s - 1) - math.asin(1 / amp))) * 2 ^ (10 * (s - 1)) + end, + + + }, { + + -- register new tween + __call = func, + + -- fetches function and generated compositions for method `key` + __index = function(tweens, key) + if type(key) == 'function' then return key end + + assert(type(key) == 'string', 'Method must be function or string.') + if rawget(tweens, key) then return rawget(tweens, key) end + + local function construct(pattern, f) + local method = rawget(tweens, key:match(pattern)) + if method then return f(method) end + return nil + end + + local out, chain = rawget(tweens, 'out'), rawget(tweens, 'chain') + return construct('^in%-([^-]+)$', function(...) return ... end) + or construct('^out%-([^-]+)$', out) + or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end) + or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end) + or error('Unknown interpolation method: ' .. key) + end + }) end @@ -252,7 +253,7 @@ Timer.func_tween = def_tween(func_tween) -- Timer instancing function Timer.new() - return setmetatable({functions = {}, tween = Timer.tween}, Timer) + return setmetatable({ functions = {}, tween = Timer.tween }, Timer) end -- default instance @@ -261,14 +262,14 @@ local default = Timer.new() -- module forwards calls to default instance local module = {} for k in pairs(Timer) do - if k ~= "__index" then - module[k] = function(...) return default[k](default, ...) end - end + if k ~= "__index" then + module[k] = function(...) return default[k](default, ...) end + end end module.tween = setmetatable({}, { - __index = Timer.tween, - __newindex = function(k,v) Timer.tween[k] = v end, - __call = function(t, ...) return default:tween(...) end, + __index = Timer.tween, + __newindex = function(k, v) Timer.tween[k] = v end, + __call = function(t, ...) return default:tween(...) end, }) -return setmetatable(module, {__call = Timer.new}) +return setmetatable(module, { __call = Timer.new }) From 68aed025c0bedd63b715142f7d54b60a228ab2a9 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 8 Oct 2025 13:54:11 +0200 Subject: [PATCH 026/103] feat(editor): add 'back' capability --- src/controller/editorController.lua | 28 +++++++++++++++++++++++++--- src/model/editor/editorModel.lua | 4 ++-- tests/editor/editor_spec.lua | 7 +++---- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index 697cafe6..684119af 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -85,12 +85,20 @@ function EditorController:open(name, content, save) end local b = BufferModel(name, content, save, ch, hl, pp) - self.model.buffer = b + self.model.buffers:push_front(b) self.view.buffer:open(b) self:update_status() self:set_state() end +--- @private +function EditorController:_dump_bufferlist() + for i, v in ipairs(self.model.buffers) do + Log.debug(i, v.name) + end + orig_print() +end + function EditorController:follow_require() local buf = self:get_active_buffer() if not buf.semantic then return end @@ -99,12 +107,22 @@ function EditorController:follow_require() local reqsel = table.find_by_v(reqs, function(r) return r.block == bn end) + if reqsel then local name = reqsel.name self.console:edit(name .. '.lua') end end +function EditorController:pop_buffer() + local bs = self.model.buffers + local n_buffers = bs:length() + if n_buffers < 2 then return end + bs:pop_front() + local b = bs:first() + self.view.buffer:open(b) +end + --- @param m EditorMode --- @return boolean local function is_normal(m) @@ -216,7 +234,7 @@ end --- @return BufferModel function EditorController:get_active_buffer() - return self.model.buffer + return self.model.buffers:first() end --- @private @@ -629,7 +647,11 @@ function EditorController:_normal_mode_keys(k) -- step into if k == "f4" then - self:follow_require() + if not Key.shift() then + self:follow_require() + else + self:pop_buffer() + end end end local function clear() diff --git a/src/model/editor/editorModel.lua b/src/model/editor/editorModel.lua index 75bda4ba..c9fb4cbe 100644 --- a/src/model/editor/editorModel.lua +++ b/src/model/editor/editorModel.lua @@ -6,13 +6,13 @@ local class = require('util.class') --- @class EditorModel --- @field input UserInputModel ---- @field buffer BufferModel? +--- @field buffers Dequeue --- @field search Search --- @field cfg Config EditorModel = class.create(function(cfg) return { input = UserInputModel(cfg, LuaEval()), - buffer = nil, + buffers = Dequeue.new({}, 'BufferModel'), search = Search(cfg), cfg = cfg, } diff --git a/tests/editor/editor_spec.lua b/tests/editor/editor_spec.lua index 4e052c41..9a48777f 100644 --- a/tests/editor/editor_spec.lua +++ b/tests/editor/editor_spec.lua @@ -130,7 +130,7 @@ describe('Editor #editor', function() assert.same(start_sel - 1, buffer:get_selection()) mock.keystroke('up', press) assert.same(start_sel - 2, buffer:get_selection()) - assert.same(turtle_doc[2], model.buffer:get_selected_text()) + assert.same(turtle_doc[2], buffer:get_selected_text()) --- load it local input = function() return controller.input:get_text():items() @@ -190,7 +190,7 @@ describe('Editor #editor', function() local save = TU.get_save_function(sierpinski) --- use it as plaintext for this test controller:open('sierpinski.txt', sierpinski, save) - view.buffer:open(model.buffer) + view.buffer:open(controller:get_active_buffer()) local visible = view.buffer.content local scroll = view.buffer.SCROLL_BY @@ -238,7 +238,6 @@ describe('Editor #editor', function() local l = 6 local controller, _, view = wire(getMockConf(27, l)) - local model = controller.model local save = TU.get_save_function(sierpinski) controller:open('sierpinski.txt', sierpinski, save) @@ -250,7 +249,7 @@ describe('Editor #editor', function() local buffer = controller:get_active_buffer() --- @type BufferView local bv = view.buffer - bv:open(model.buffer) + bv:open(buffer) local visible = view.buffer.content local scroll = view.buffer.SCROLL_BY From 303607e4fed2774dfb4d2ec526c8ab8a0781eadd Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 8 Oct 2025 13:54:29 +0200 Subject: [PATCH 027/103] refactor: cleanup --- src/util/dequeue.lua | 2 +- src/view/editor/bufferView.lua | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/util/dequeue.lua b/src/util/dequeue.lua index 99955256..fb5c4917 100644 --- a/src/util/dequeue.lua +++ b/src/util/dequeue.lua @@ -172,7 +172,7 @@ function Dequeue:_checked(i, add, f) return ok, err end ---- Insert element at index +--- Pop element at index --- @param i integer --- @return boolean --- @return string|any err_or_result diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index e6bac7c3..837c8b7d 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -53,16 +53,16 @@ function BufferView:open(buffer) if not self.buffer then error('no buffer') end - local cont = buffer.content_type - self.content_type = cont + local ct = buffer.content_type + self.content_type = ct - if cont == 'plain' or cont == 'md' then + if ct == 'plain' or ct == 'md' then local bufcon = buffer:get_text_content() self.buffer:highlight() self.content = VisibleContent( self.w, bufcon, self.SCROLL_BY, L) self.hl = self.buffer:get_highlight() - elseif cont == 'lua' then + elseif ct == 'lua' then local bufcon = buffer:get_content() self.content = VisibleStructuredContent( From d4448095b3b19297e3c1847693604ed537bcdf44 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 8 Oct 2025 16:37:29 +0200 Subject: [PATCH 028/103] fix(turtle): infinite loop --- src/examples/turtle/action.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/examples/turtle/action.lua b/src/examples/turtle/action.lua index ac80980c..1d62f00f 100644 --- a/src/examples/turtle/action.lua +++ b/src/examples/turtle/action.lua @@ -14,7 +14,7 @@ function moveRight(d) tx = tx + (d or (2 * incr)) end -function pause(msg) +function pause_game(msg) pause(msg or "user paused the game") end @@ -27,7 +27,7 @@ local actions = { l = moveLeft, right = moveRight, r = moveRight, - pause = pause + pause = pause_game } return actions From c74d17021340154e028496cbc96db185df31f2a6 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 11:16:11 +0200 Subject: [PATCH 029/103] doc: hardware keys --- doc/development/keyboard.md | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 doc/development/keyboard.md diff --git a/doc/development/keyboard.md b/doc/development/keyboard.md new file mode 100644 index 00000000..ce67fa6c --- /dev/null +++ b/doc/development/keyboard.md @@ -0,0 +1,38 @@ + +| Label | KeyConstant | ok? | +| :------------------- | :---------- | :--- | +| Esc | escape | ✓ | +| F1 | f1 | ✗ | +| F2 | f2 | ✗ | +| F3 | f3 | ✗ | +| F4 | f4 | ✗ | +| F5 | f5 | ✗ | +| F6 | f6 | ✗ | +| F7 | f7 | ✗ | +| F8 | f8 | ✗ | +| F9 | f9 | ✗ | +| F10 | f10 | ✓ | +| F11 | f11 | - | +| F12 | f12 | - | +| numlk | code 143 | ✗ | +| Delete | delete | ✓ | +| Tab | tab | ✓ | +| Enter | return | ✓ | +| Caps Lock | capslock | ✓ | +| ⇧ Shift | lshift | ✓ | +| Shift | rshift | ✓ | +| Ctrl | | ✗ | +| Alt | lalt | ✓ | +| PauseBreak | pause | ✓ | +| Menu | menu | ✓ | +| Insert (Fn+F12) | insert | ✓ | +| Scroll Lock (Fn+F10) | scrolllock | ✓ | +| Home (Fn+Left) | home | ✓ | +| End (Fn+Right) | end | ✓ | +| Mute (Fn+F5) | code 164 | ✗ | +| PgUp (Fn+Up) | pageup | ✓ | +| PgDn (Fn+Down) | pagedown | ✓ | +| Next (Fn+F6) | audioplay | ✓ | +| Prev (Fn+F7) | audioprev | ✓ | +| Next (Fn+F8) | audionext | ✓ | +| | | | From 4aae7b08e2cf80e4de78956ee6abdcab4f9a70df Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 14:44:04 +0200 Subject: [PATCH 030/103] fix: change open require shortcut to Ctrl-O Sadly, the F-keys don't work on the target device with the integrated keyboard. --- src/controller/editorController.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index 684119af..c94c4b89 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -646,11 +646,13 @@ function EditorController:_normal_mode_keys(k) end -- step into - if k == "f4" then - if not Key.shift() then - self:follow_require() - else - self:pop_buffer() + if Key.ctrl() then + if k == "o" then + if not Key.shift() then + self:follow_require() + else + self:pop_buffer() + end end end end From 17f5d6cd7a4cfd111af7a26e039cedc799b46d13 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 14:47:20 +0200 Subject: [PATCH 031/103] feat: change open required semantics The shortcut opens the required in the editor, or returns to the previously edited file if the selection is not on a require line. --- src/controller/editorController.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index c94c4b89..16c904e8 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -111,6 +111,8 @@ function EditorController:follow_require() if reqsel then local name = reqsel.name self.console:edit(name .. '.lua') + else + self:pop_buffer() end end @@ -648,11 +650,7 @@ function EditorController:_normal_mode_keys(k) -- step into if Key.ctrl() then if k == "o" then - if not Key.shift() then - self:follow_require() - else - self:pop_buffer() - end + self:follow_require() end end end From 9e5dbd48028a930371716f191942567e8002bf21 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 14:49:34 +0200 Subject: [PATCH 032/103] doc: 'open required' shortcut --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e9573df6..5210c0f3 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ a project must be selected first. | Search definitions | Ctrl+F | | Exit search | Esc | | Jump to selected definition | Enter ⏎ | +| Edit required file / return to previous | Ctrl+O | ## Projects From a9622baf036d1a0062ed84300c35aaa1de449da0 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 14:50:18 +0200 Subject: [PATCH 033/103] chore: add 'pause' to globals --- .luarc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.luarc.json b/.luarc.json index ecedad0a..ccc8cc99 100644 --- a/.luarc.json +++ b/.luarc.json @@ -17,6 +17,7 @@ "write_to_input", "validated_input", "stop", + "pause", "continue", // luautils "prequire", From d6b785e2fc50e7a5a08ccb6dcc280dd3a0a21404 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 14:51:28 +0200 Subject: [PATCH 034/103] fix(editor): reanalyze content on save The semanticDB needs to be redone when the contents change. Duh. More thourough testing is needed, this should have been caught with search already, not just now with the require. --- src/controller/editorController.lua | 1 + src/model/editor/bufferModel.lua | 34 +++++++++++++++---------- src/model/editor/bufferSemanticInfo.lua | 1 + 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index 16c904e8..cb84dd18 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -85,6 +85,7 @@ function EditorController:open(name, content, save) end local b = BufferModel(name, content, save, ch, hl, pp) + b:analyze() --- TODO come up with a nicer lateinit self.model.buffers:push_front(b) self.view.buffer:open(b) self:update_status() diff --git a/src/model/editor/bufferModel.lua b/src/model/editor/bufferModel.lua index 109ff87a..79a48e6b 100644 --- a/src/model/editor/bufferModel.lua +++ b/src/model/editor/bufferModel.lua @@ -37,7 +37,6 @@ end local function new(name, content, save, chunker, highlighter, printer) local _content, sel, ct, semantic - local revmap = {} local readonly = false local lines = string.lines(content or '') @@ -51,21 +50,10 @@ local function new(name, content, save, --- @param chk function local function luacontent(chk) ct = 'lua' - local ok, blocks, ast = chk(lines) + local ok, blocks = chk(lines) if ok then local len = #blocks sel = len + 1 - local anaok, ana = pcall(analyzer.analyze, ast) - if anaok then - for bi, v in ipairs(blocks) do - if (v.pos) then - for _, l in ipairs(v.pos:enumerate()) do - revmap[l] = bi - end - end - end - semantic = bsi.convert(ana, revmap) - end else readonly = true sel = 1 @@ -90,6 +78,7 @@ local function new(name, content, save, chunker = chunker, highlighter = highlighter, printer = printer, + revmap = {}, semantic = semantic, selection = sel, readonly = readonly @@ -105,6 +94,7 @@ end --- @field loaded integer? --- @field readonly boolean --- @field semantic BufferSemanticInfo? +--- @field revmap table? --- --- @field chunker Chunker --- @field highlighter Highlighter @@ -117,6 +107,23 @@ end --- @field get_text_content function BufferModel = class.create(new) +function BufferModel:analyze() + if self.content_type ~= 'lua' then return end + local lines = string.lines(self:get_text_content()) + local ok, blocks, ast = self.chunker(lines) + if not ok then return end + local anaok, ana = pcall(analyzer.analyze, ast) + if not anaok then return end + for bi, v in ipairs(blocks) do + if (v.pos) then + for _, l in ipairs(v.pos:enumerate()) do + self.revmap[l] = bi + end + end + end + self.semantic = bsi.convert(ana, self.revmap) +end + function BufferModel:rechunk() if self.content_type ~= 'lua' then return end local content = self:get_text_content() @@ -127,6 +134,7 @@ end function BufferModel:save() self:highlight() local text = self:get_text_content() + self:analyze() return self.save_file(text) end diff --git a/src/model/editor/bufferSemanticInfo.lua b/src/model/editor/bufferSemanticInfo.lua index 3895c2b0..4d854ec8 100644 --- a/src/model/editor/bufferSemanticInfo.lua +++ b/src/model/editor/bufferSemanticInfo.lua @@ -10,6 +10,7 @@ require('util.table') --- @class BufferSemanticInfo --- @field definitions Definition[] +--- @field requires RequireCall[] --- @param si SemanticInfo --- @param rev table From 1260a81e28da161181b3e4ff2acbdb87e1ca67b1 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 14:54:53 +0200 Subject: [PATCH 035/103] fix: change quickswitch shortcut to Ctrl-T The F-keys don't work on the target device with the integrated keyboard :/ --- src/controller/controller.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index 946b6a63..1a963b27 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -528,7 +528,7 @@ Controller = { handlers.keypressed = function(k) local function quickswitch() - if k == 'f8' then + if Key.ctrl() and k == 't' then if love.state.app_state == 'running' or love.state.app_state == 'inspect' or love.state.app_state == 'project_open' From 5d0694925c75cf594bd63856bae21729d71242b6 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 16:14:01 +0200 Subject: [PATCH 036/103] feat(class): lateinit --- doc/development/OOP.md | 68 +++++++++++++++++++++++++++++++++++------- src/util/class.lua | 9 ++++-- 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/doc/development/OOP.md b/doc/development/OOP.md index 24cc3a92..f7e4400b 100644 --- a/doc/development/OOP.md +++ b/doc/development/OOP.md @@ -1,14 +1,14 @@ -### OOP +## OOP -Even though lua is not an object oriented language per se, it can approximate -some OO behaviors with clever use of metatables. +Even though lua is not an object oriented language per se, it +can approximate some OO behaviors with clever use of metatables. See: - [http://lua-users.org/wiki/ObjectOrientedProgramming][oo1] - [http://lua-users.org/wiki/ObjectOrientationTutorial][oo2] -#### Class factory +### Class factory To automate this, a class factory utility was added. @@ -20,26 +20,73 @@ local class = require('util.class') Then it can be used in the following ways: -- passing a constructor (record/dataclass pattern) +#### passing a constructor (record/dataclass pattern) ```lua A = class.create(function() return { a = 'a' } end ) -local a = A() --- results in an instance with the preset values, not very useful +--- results in an instance with the preset values +local a = A() B = class.create(function(x, y) return { x = x, y = y } end) -local b = B(1, 2) --- results in a B instance where x = 1 and y = 2 +--- results in a B instance where x = 1 and y = 2 +local b = B(1, 2) +``` + +Note: in the codebase, the function is often not defined inline, +and very likely is named `new`, _however_ be careful not to +confuse it with the [`new` method][n]. If the function is not a +member of the class, it's this pattern. + +##### late init + +Sometimes it's useful to extract some behavior that's required +both on init but also later on-demand. An instance method is a +fine solution for this, but in order to invoke it, a full-blown +properly initialized instance is necessary, so it can't be done +in the constructor. Also, it would be nice to not have to do it +on each call site where an instance is created. Hence, +`lateinit`. + +Let's say we have Text objects, storing text line-by-line, and +we want to know the average line length. This of course needs to +be calculated on first creation, and any time the text changes. + +```lua +local function new(text) + return { + text = string.lines(text or '') + } +end +local function lateinit(self) + local n = #(self.text) + if n ~= 0 then + local lens = table.map(self.text, string.ulen) + local l = 0 + for _, v in ipairs(lens) do + l = l + v + end + self.avg_len = l / n + else + self.avg_len = 0 + end +end +--- @class Text +--- @field text string[] +--- @field avg_len number +Text = class.create(new, lateinit) ``` -For more advanced use cases, it will probably be necessary to manually control the -metatable setup, this is achieved with the +#### `new` method -- `new()` method +For more advanced use cases, it will probably be necessary to +manually control the metatable setup, this is achieved by +defining the `new()` method on the class. ```lua N = class.create() @@ -61,3 +108,4 @@ local n = N({width = 80, height = 25}) [oo1]: https://archive.vn/B3buW [oo2]: https://archive.vn/muhJx +[n]: #new-method diff --git a/src/util/class.lua b/src/util/class.lua index a33b2818..206b8b3d 100644 --- a/src/util/class.lua +++ b/src/util/class.lua @@ -1,7 +1,8 @@ return { --- Simple factory, to spare boilerplate --- @param constructor function? - create = function(constructor) + --- @param lateinit function? + create = function(constructor, lateinit) local ret = {} ret.__index = ret local function new(...) @@ -16,8 +17,10 @@ return { if type(cls.new) == "function" then return cls.new(...) else - local instance = new(...) - setmetatable(instance, cls) + local instance = setmetatable(new(...), cls) + if type(lateinit) == "function" then + lateinit(instance) + end return instance end end, From 3ea948ec5b641ce3a40e744602e00685e302b2b6 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 16:14:20 +0200 Subject: [PATCH 037/103] refactor: use lateinit for BufferModel --- src/controller/editorController.lua | 1 - src/model/editor/bufferModel.lua | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index cb84dd18..16c904e8 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -85,7 +85,6 @@ function EditorController:open(name, content, save) end local b = BufferModel(name, content, save, ch, hl, pp) - b:analyze() --- TODO come up with a nicer lateinit self.model.buffers:push_front(b) self.view.buffer:open(b) self:update_status() diff --git a/src/model/editor/bufferModel.lua b/src/model/editor/bufferModel.lua index 79a48e6b..4f9a02e2 100644 --- a/src/model/editor/bufferModel.lua +++ b/src/model/editor/bufferModel.lua @@ -85,6 +85,11 @@ local function new(name, content, save, } end +--- @param self BufferModel +local function lateinit(self) + self:analyze() +end + --- @class BufferModel --- @field name string --- @field content Dequeue -- Content @@ -105,7 +110,7 @@ end --- @field delete_selected_text function --- @field replace_selected_text function --- @field get_text_content function -BufferModel = class.create(new) +BufferModel = class.create(new, lateinit) function BufferModel:analyze() if self.content_type ~= 'lua' then return end From a433482630421fbbca8517a46a490f9ac91083f8 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 10 Oct 2025 17:31:44 +0200 Subject: [PATCH 038/103] chore: fix scrollableContent annotation --- src/util/scrollableContent.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/scrollableContent.lua b/src/util/scrollableContent.lua index b2e59e95..a5d4b98e 100644 --- a/src/util/scrollableContent.lua +++ b/src/util/scrollableContent.lua @@ -2,7 +2,7 @@ require("util.wrapped_text") require("util.scrollable") require("util.range") ---- @class ScrollableContent +--- @class ScrollableContent: WrappedText --- @field range Range? --- @field size integer --- @field size_max integer From deeb4d2cf2c8e8fce1e4f5e35ffb7b171587948f Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 15 Oct 2025 12:28:17 +0200 Subject: [PATCH 039/103] refactor: rename VisibleBlocks field for clarity --- doc/mermaid/editor.md | 2 +- src/view/editor/bufferView.lua | 2 +- src/view/editor/visibleStructuredContent.lua | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/mermaid/editor.md b/doc/mermaid/editor.md index 3552900b..f80d117c 100644 --- a/doc/mermaid/editor.md +++ b/doc/mermaid/editor.md @@ -73,7 +73,7 @@ class VisibleContent { class VisibleStructuredContent { text: string[] - blocks: VisibleBlock[] + v_blocks: VisibleBlock[] reverse_map: ReverseMap range: Range? diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index 837c8b7d..7de683ab 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -139,7 +139,7 @@ function BufferView:refresh(moved) local sel = self.buffer:get_selection() if self.content_type == 'lua' then local vsc = self.content - local blocks = vsc.blocks + local blocks = vsc.v_blocks blocks:move(moved, sel) vsc:recalc_range() else diff --git a/src/view/editor/visibleStructuredContent.lua b/src/view/editor/visibleStructuredContent.lua index 4c63c50d..f7521ec8 100644 --- a/src/view/editor/visibleStructuredContent.lua +++ b/src/view/editor/visibleStructuredContent.lua @@ -10,7 +10,7 @@ require("util.range") --- @field overscroll_max integer --- @field size_max integer --- @field range Range? ---- @field blocks Dequeue +--- @field v_blocks Dequeue --- @field reverse_map ReverseMap --- --- @field set_range fun(self, Range) @@ -93,12 +93,12 @@ function VisibleStructuredContent:load_blocks(blocks) WrappedText._init(self, self.w, fulltext) self:_init() self.reverse_map = revmap - self.blocks = visible_blocks + self.v_blocks = visible_blocks end function VisibleStructuredContent:recalc_range() local ln, aln = 1, 1 - for _, v in ipairs(self.blocks) do + for _, v in ipairs(self.v_blocks) do local l = #(v.wrapped.orig) local al = #(v.wrapped.text) v.pos = Range(ln, ln + l - 1) @@ -168,7 +168,7 @@ function VisibleStructuredContent:get_visible_blocks() local si = self.wrap_reverse[self.range.start] local ei = self.wrap_reverse[self.range.fin] local sbi, sei = self.reverse_map[si], self.reverse_map[ei] - return table.slice(self.blocks, sbi, sei) + return table.slice(self.v_blocks, sbi, sei) end --- @return integer @@ -179,22 +179,22 @@ end --- @param bn integer --- @return Range? function VisibleStructuredContent:get_block_pos(bn) - local cl = #(self.blocks) + local cl = #(self.v_blocks) if bn > 0 and bn <= cl then - return self.blocks[bn].pos + return self.v_blocks[bn].pos elseif cl == 0 then --- empty/new file Range.singleton(1) elseif bn == cl + 1 then - return Range.singleton(self.blocks[cl].pos.fin + 1) + return Range.singleton(self.v_blocks[cl].pos.fin + 1) end end --- @param bn integer --- @return Range? function VisibleStructuredContent:get_block_app_pos(bn) - local cl = #(self.blocks) + local cl = #(self.v_blocks) if bn > 0 and bn <= cl then - return self.blocks[bn].app_pos + return self.v_blocks[bn].app_pos elseif bn == cl + 1 then local wr = self.wrap_reverse return Range.singleton(#wr) From dc4093bc6a084e737778aa8ca7817c9fcb315620 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 15 Oct 2025 12:45:31 +0200 Subject: [PATCH 040/103] refactor: cleanup in EditorController --- src/controller/editorController.lua | 56 +++++++++++++---------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index 16c904e8..cd75df2f 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -34,27 +34,11 @@ end --- @field view EditorView? --- @field state EditorState? --- @field mode EditorMode ---- ---- @field open function ---- @field get_state function ---- @field set_state function ---- @field save_state function ---- @field restore_state function ---- @field get_clipboard function ---- @field set_clipboard function ---- @field set_mode function ---- @field get_mode function ---- @field close function ---- @field get_active_buffer function ---- @field get_input function ---- @field update_status function ---- @field textinput function ---- @field keypressed function EditorController = class.create(new) --- @param name string ---- @param content string? +--- @param content str? --- @param save function function EditorController:open(name, content, save) local w = self.model.cfg.view.drawableChars @@ -86,7 +70,7 @@ function EditorController:open(name, content, save) local b = BufferModel(name, content, save, ch, hl, pp) self.model.buffers:push_front(b) - self.view.buffer:open(b) + self.view:open(b) self:update_status() self:set_state() end @@ -122,7 +106,9 @@ function EditorController:pop_buffer() if n_buffers < 2 then return end bs:pop_front() local b = bs:first() - self.view.buffer:open(b) + self.view:get_current_buffer():open(b) +end + end --- @param m EditorMode @@ -138,10 +124,12 @@ function EditorController:set_mode(mode) self:save_state() end local init_search = function() - self:save_state() local db = buf.semantic - local ds = db.definitions - self.search:load(ds) + if db then + self:save_state() + local ds = db.definitions + self.search:load(ds) + end end local current = self.mode @@ -150,7 +138,6 @@ function EditorController:set_mode(mode) set_reorg() end if mode == 'search' then - if not buf.semantic then return end init_search() end self.mode = mode @@ -186,8 +173,9 @@ end --- @param clipboard string? function EditorController:set_state(clipboard) - local buf_view_state = self.view.buffer:get_state() local buf = self:get_active_buffer() + local bid = buf:get_id() + local buf_view_state = self.view:get_buffer(bid):get_state() if self.state then self.state.buffer = buf_view_state self.state.moved = buf:get_selection() @@ -217,7 +205,7 @@ function EditorController:restore_state(state) local sel = state.buffer.selection local off = state.buffer.offset buf:set_selection(sel) - self.view.buffer:scroll_to(off) + self.view:get_current_buffer():scroll_to(off) local clip = state.clipboard if string.is_non_empty_string(clip) then love.system.setClipboardText(clip or '') @@ -239,6 +227,12 @@ function EditorController:get_active_buffer() return self.model.buffers:first() end +--- @return Id +function EditorController:get_active_buffer_id() + local buf = self:get_active_buffer() + return buf:get_id() +end + --- @private --- @param sel integer --- @return CustomStatus @@ -246,7 +240,7 @@ function EditorController:_generate_status(sel) --- @type BufferModel local buffer = self:get_active_buffer() local len = buffer:get_content_length() + 1 - local bufview = self.view.buffer + local bufview = self.view:get_buffer(buffer:get_id()) local more = bufview.content:get_more() local cs local m = self.mode @@ -356,7 +350,7 @@ function EditorController:_move_sel(dir, by, warp, moved) local m = buf:move_selection(dir, by, warp, mv) if m then if mv then self.view:refresh(moved) end - self.view.buffer:follow_selection() + self.view:get_current_buffer():follow_selection() self:update_status() end end @@ -366,7 +360,7 @@ end --- @param warp boolean? --- @param by integer? function EditorController:_scroll(dir, warp, by) - self.view.buffer:scroll(dir, by, warp) + self.view:get_current_buffer():scroll(dir, by, warp) self:update_status() end @@ -451,7 +445,7 @@ function EditorController:_search_mode_keys(k) local bn = jump.block local ln = jump.line - 1 buf:set_selection(bn) - self.view.buffer:scroll_to_line(ln) + self.view:get_current_buffer():scroll_to_line(ln) self:set_mode('edit') self.search:clear() end @@ -556,7 +550,7 @@ function EditorController:_normal_mode_keys(k) --- handlers local function submit() if not Key.ctrl() and not Key.shift() and Key.is_enter(k) then - local bufv = self.view.buffer + local bufv = self.view:get_current_buffer() local function go(newtext) if bufv:is_selection_visible() then if buf:loaded_is_sel(true) then @@ -695,7 +689,7 @@ function EditorController:keypressed(k) if love.debug then local buf = self:get_active_buffer() - local bufview = self.view.buffer + local bufview = self.view:get_buffer(buf:get_id()) if k == 'f5' then if Key.ctrl() then buf:rechunk() end bufview:refresh() From 329810cc618c6cdc863c9443327bac992fa9eb47 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 15 Oct 2025 12:46:26 +0200 Subject: [PATCH 041/103] refactor(harmony): shortcut constants --- src/harmony/init.lua | 7 +++++++ src/harmony/scenarios/editor.lua | 10 +++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/harmony/init.lua b/src/harmony/init.lua index 1d6fd109..07518df0 100644 --- a/src/harmony/init.lua +++ b/src/harmony/init.lua @@ -182,6 +182,9 @@ local function utils() lgui = false, rgui = false, } + local shortcuts = { + toggle = 'C-t' + } --- @param tag string @@ -234,6 +237,7 @@ local function utils() --- @field love_text function --- @field screenshot function --- @field release_keys function + --- @field shortcuts table return { patch_isDown = function() local down = love.keyboard.isDown @@ -291,6 +295,8 @@ local function utils() release_keys = release_keys, + shortcuts = shortcuts, + screenshot = function(tag) timer:script(function(wait) wait(frame_time) @@ -362,6 +368,7 @@ local function runner() end) scrun = coroutine.create(function() + -- timer = Timer.new() for _, v in ipairs(scenarios) do local tag = v.id local sc = v.sc diff --git a/src/harmony/scenarios/editor.lua b/src/harmony/scenarios/editor.lua index c175b393..b9d1e64a 100644 --- a/src/harmony/scenarios/editor.lua +++ b/src/harmony/scenarios/editor.lua @@ -131,14 +131,14 @@ local function editor() wait(.1) h.screenshot('open') wait(.2) - h.love_key('f8') + h.love_key(h.shortcuts.toggle) wait(2) wait(.1) h.screenshot('before') wait(.2) - h.love_key('f8') + h.love_key(h.shortcuts.toggle) wait(.2) h.love_key('C-home') wait(.1) @@ -199,12 +199,12 @@ local function editor() wait(.1) h.screenshot('edited') wait(.2) - h.love_key('f8') + h.love_key(h.shortcuts.toggle) wait(.1) h.screenshot('after') wait(.4) - h.love_key('f8') + h.love_key(h.shortcuts.toggle) wait(.1) h.love_key('C-f') wait(.01) @@ -232,7 +232,7 @@ local function editor() wait(.1) h.screenshot('edited-2') wait(.2) - h.love_key('f8') + h.love_key(h.shortcuts.toggle) wait(.1) h.screenshot('after-2') From 1a81bc3e602996f8e247b5bcb904ba0190dc8cc3 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 15 Oct 2025 12:47:01 +0200 Subject: [PATCH 042/103] feat(class): add base Object type --- src/util/class.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/util/class.lua b/src/util/class.lua index 206b8b3d..ac72966d 100644 --- a/src/util/class.lua +++ b/src/util/class.lua @@ -1,3 +1,7 @@ +--- @alias Id string +--- @class Object +--- @field id Id + return { --- Simple factory, to spare boilerplate --- @param constructor function? From c46ead691f9011606f6bf1a258b41bdaee2c1678 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 15 Oct 2025 12:48:57 +0200 Subject: [PATCH 043/103] feat(editor): store views for each buffer --- src/view/editor/bufferView.lua | 6 +++++- src/view/editor/editorView.lua | 37 ++++++++++++++++++++++++++++++---- tests/editor/editor_spec.lua | 19 ++++++++--------- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index 7de683ab..ef9a2fbe 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -28,7 +28,7 @@ end --- @class BufferView : ViewBase --- @field content VisibleContent|VisibleStructuredContent --- @field content_type ContentType ---- @field buffer BufferModel +--- @field buffers Dequeue --- --- @field LINES integer --- @field SCROLL_BY integer @@ -253,6 +253,10 @@ function BufferView:follow_selection() end end +-------------- +--- draw --- +-------------- + --- @param special boolean function BufferView:draw(special) local gfx = love.graphics diff --git a/src/view/editor/editorView.lua b/src/view/editor/editorView.lua index e86b68e0..f6cd20a4 100644 --- a/src/view/editor/editorView.lua +++ b/src/view/editor/editorView.lua @@ -12,7 +12,7 @@ local function new(cfg, ctrl) cfg = cfg, controller = ctrl, input = UserInputView(cfg, ctrl.input), - buffer = BufferView(cfg), + buffers = {}, search = SearchView(cfg, ctrl.search), } --- hook the view in the controller @@ -23,7 +23,7 @@ end --- @class EditorView : ViewBase --- @field controller EditorController --- @field input UserInputView ---- @field buffer BufferView +--- @field buffers { [string]: BufferView } --- @field search SearchView EditorView = class.create(new) @@ -34,7 +34,8 @@ function EditorView:draw() self.search:draw(ctrl.search:get_input()) else local spec = mode == 'reorder' - self.buffer:draw(spec) + local bv = self:get_current_buffer() + bv:draw(spec) if ViewUtils.conditional_draw('show_input') then local input = ctrl:get_input() self.input:draw(input) @@ -42,7 +43,35 @@ function EditorView:draw() end end +--- @param buffer BufferModel +--- @return BufferView +function EditorView:open(buffer) + local bid = buffer:get_id() + local opn = self.buffers[bid] + if not opn then + local v = BufferView(self.cfg) + self.buffers[bid] = v + v:open(buffer) + return v + end + return opn +end + +--- @return BufferView +function EditorView:get_current_buffer() + local ctrl = self.controller + local bm = ctrl:get_active_buffer() + local bid = bm:get_id() + return self.buffers[bid] +end + +--- @param bid string +--- @return BufferView +function EditorView:get_buffer(bid) + return self.buffers[bid] +end + --- @param moved integer? function EditorView:refresh(moved) - self.buffer:refresh(moved) + self:get_current_buffer():refresh(moved) end diff --git a/tests/editor/editor_spec.lua b/tests/editor/editor_spec.lua index 9a48777f..21693cc4 100644 --- a/tests/editor/editor_spec.lua +++ b/tests/editor/editor_spec.lua @@ -190,10 +190,11 @@ describe('Editor #editor', function() local save = TU.get_save_function(sierpinski) --- use it as plaintext for this test controller:open('sierpinski.txt', sierpinski, save) - view.buffer:open(controller:get_active_buffer()) + local buf = controller:get_active_buffer() + local bv = view:open(buf) - local visible = view.buffer.content - local scroll = view.buffer.SCROLL_BY + local visible = bv.content + local scroll = bv.SCROLL_BY local off = #sierpinski - l + 1 local start_range = Range(off + 1, #sierpinski + 1) @@ -201,7 +202,7 @@ describe('Editor #editor', function() it('loads', function() --- inital scroll is at EOF, meaning last l lines are visible --- plus the phantom line - assert.same(off, view.buffer.offset) + assert.same(off, bv.offset) assert.same(start_range, visible.range) end) local base = Range(1, l) @@ -248,11 +249,11 @@ describe('Editor #editor', function() local buffer = controller:get_active_buffer() --- @type BufferView - local bv = view.buffer - bv:open(buffer) + local bv = view:open(buffer) + -- bv:open(buffer) - local visible = view.buffer.content - local scroll = view.buffer.SCROLL_BY + local visible = bv.content + local scroll = bv.SCROLL_BY local clen = visible:get_content_length() local off = clen - l + 1 @@ -260,7 +261,7 @@ describe('Editor #editor', function() it('loads', function() --- inital scroll is at EOF, meaning last l lines are visible --- plus the phantom line - assert.same(off, view.buffer.offset) + assert.same(off, bv.offset) assert.same(start_range, visible.range) end) local base = Range(1, l) From 118ad22b930ec0683e80976e0c20c3979d396142 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 15 Oct 2025 12:50:05 +0200 Subject: [PATCH 044/103] style: cleanup --- README.md | 2 +- doc/development/editor/visible.md | 3 +++ src/model/editor/bufferModel.lua | 11 ++++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5210c0f3..f8cdf686 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ To modify an existing line, navigate there with ![capitalized](./doc/interface/hello_cap.apng) Happy with the modifications now, we can quit by pressing -Ctrl-Shift-Q +Ctrl-Shift-S ![quit](./doc/interface/quit_editor.apng) diff --git a/doc/development/editor/visible.md b/doc/development/editor/visible.md index 6bf41649..14d2ed32 100644 --- a/doc/development/editor/visible.md +++ b/doc/development/editor/visible.md @@ -45,6 +45,9 @@ loin█ bresaola veni | 5 son. | ``` We arrive at 'visible coords'. + +#### convert + What is `visible(3, 3)` in the others? * `wrapped(11, 3)` * `normal(4, 41)` diff --git a/src/model/editor/bufferModel.lua b/src/model/editor/bufferModel.lua index 4f9a02e2..c592a6e0 100644 --- a/src/model/editor/bufferModel.lua +++ b/src/model/editor/bufferModel.lua @@ -28,14 +28,19 @@ end --- @alias Content Dequeue|Dequeue --- @param name string ---- @param content string +--- @param content str --- @param save function --- @param chunker Chunker? --- @param highlighter Highlighter? --- @param printer function? --- @return BufferModel? -local function new(name, content, save, - chunker, highlighter, printer) +local function new( + name, + content, + save, + chunker, + highlighter, + printer) local _content, sel, ct, semantic local readonly = false From 3045483c7c62aaeb5e3b9a30a35dc1f9ad2b1126 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 15 Oct 2025 12:50:37 +0200 Subject: [PATCH 045/103] feat(editor): inherit Object in BufferModel --- src/model/editor/bufferModel.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/model/editor/bufferModel.lua b/src/model/editor/bufferModel.lua index c592a6e0..32280249 100644 --- a/src/model/editor/bufferModel.lua +++ b/src/model/editor/bufferModel.lua @@ -75,7 +75,7 @@ local function new( plaintext() end - return { + local self = { name = name or 'untitled', content = _content, content_type = ct, @@ -88,6 +88,9 @@ local function new( selection = sel, readonly = readonly } + local id = tostring(self):gsub('table: ', '') + self.id = id + return self end --- @param self BufferModel @@ -95,7 +98,7 @@ local function lateinit(self) self:analyze() end ---- @class BufferModel +--- @class BufferModel : Object --- @field name string --- @field content Dequeue -- Content --- @field content_type ContentType @@ -117,6 +120,10 @@ end --- @field get_text_content function BufferModel = class.create(new, lateinit) +function BufferModel:get_id() + return self.id +end + function BufferModel:analyze() if self.content_type ~= 'lua' then return end local lines = string.lines(self:get_text_content()) From 16bf47b2cca19cd2fe8e522d56a0cfa21cf03b14 Mon Sep 17 00:00:00 2001 From: aldum Date: Sun, 19 Oct 2025 16:04:58 +0200 Subject: [PATCH 046/103] refactor(test): merge Visible tests --- tests/editor/visible_content_spec.lua | 57 +++++++++++++++++++++++- tests/util/visible_spec.lua | 63 --------------------------- 2 files changed, 56 insertions(+), 64 deletions(-) delete mode 100644 tests/util/visible_spec.lua diff --git a/tests/editor/visible_content_spec.lua b/tests/editor/visible_content_spec.lua index 9ad20188..d6445c01 100644 --- a/tests/editor/visible_content_spec.lua +++ b/tests/editor/visible_content_spec.lua @@ -1,14 +1,69 @@ require("view.editor.visibleContent") +require("model.input.cursor") require("util.string.string") -describe('VisibleContent #wrap', function() +local md_ex = [[### Input validation + +As an extension to the user input functionality, `validated_input()` allows arbitrary user-specified filters. +A "filter" is a function, which takes a string as input and returns a boolean value of whether it is valid and an optional `Error`. +The `Error` is structure which contains the error message (`msg`), and the location the error comes from, with line and character fields (`l` and `c`). + +#### Helper functions + +* `string.ulen(s)` - as opposed to the builtin `len()`, this works for unicode strings +* `string.usub(s, from, to)` - unicode substrings +* `Char.is_alpha(c)` - is `c` a letter +* `Char.is_alnum(c)` - is `c` a letter or a number (alphanumeric) +]] + +describe('VisibleContent #visible', function() + local md_text = string.lines(md_ex) + local w = 64 local turtle_doc = { '', 'Turtle graphics game inspired the LOGO family of languages.', '', } + it('translates', function() + local visible = VisibleContent(w, md_text, 1, 8) + --- scroll to the top + visible:move_range(- #md_text) + local cur11 = Cursor() + local cur33 = Cursor(3, 3) + local cur3w = Cursor(3, w) + local cur3wp1 = Cursor(3, w + 1) + local cur44 = Cursor(4, 4) + assert.same(cur11, visible:translate_to_wrapped(cur11)) + assert.same(cur33, visible:translate_to_wrapped(cur33)) + assert.same(cur3w, visible:translate_to_wrapped(cur3w)) + assert.same(Cursor(4, 1), visible:translate_to_wrapped(cur3wp1)) + + assert.same(cur33, visible:translate_from_visible(cur33)) + local cur3_67 = Cursor(3, 3 + w) + local exp3_67 = Cursor(4, 3) + assert.same(exp3_67, visible:translate_to_wrapped(cur3_67)) + + --- scroll to bottom + visible:to_end() + -- #01: '' + -- #02: '* `string.ulen(s)` - as opposed to the builtin `len()`, this wor' + -- #03: 'ks for unicode strings' + -- #04: '* `string.usub(s, from, to)` - unicode substrings' + -- #05: '* `Char.is_alpha(c)` - is `c` a letter' + -- #06: '* `Char.is_alnum(c)` - is `c` a letter or a number (alphanumeric' + -- #07: ')' + -- #08: '' + assert.same(Cursor(9, 3 + w), + visible:translate_from_visible(cur33)) + assert.same(Cursor(10, 4), + visible:translate_from_visible(cur44)) + assert.is_nil(visible:translate_from_visible(Cursor(5, 40))) + local cur71 = Cursor(7, 1) + assert.same(Cursor(12, 65), + visible:translate_from_visible(cur71)) + end) local os_max = 8 local input_max = 16 diff --git a/tests/util/visible_spec.lua b/tests/util/visible_spec.lua deleted file mode 100644 index 97fa932f..00000000 --- a/tests/util/visible_spec.lua +++ /dev/null @@ -1,63 +0,0 @@ -require("view.editor.visibleContent") -require("model.input.cursor") -require("util.string.string") -require("util.debug") - -local md_ex = [[### Input validation - -As an extension to the user input functionality, `validated_input()` allows arbitrary user-specified filters. -A "filter" is a function, which takes a string as input and returns a boolean value of whether it is valid and an optional `Error`. -The `Error` is structure which contains the error message (`msg`), and the location the error comes from, with line and character fields (`l` and `c`). - -#### Helper functions - -* `string.ulen(s)` - as opposed to the builtin `len()`, this works for unicode strings -* `string.usub(s, from, to)` - unicode substrings -* `Char.is_alpha(c)` - is `c` a letter -* `Char.is_alnum(c)` - is `c` a letter or a number (alphanumeric) -]] -local text = string.lines(md_ex) -local w = 64 - -describe('VisibleContent #visible', function() - local visible = VisibleContent(w, text, 1, 8) - - -- Log.debug(Debug.terse_ast(visible, true, 'lua')) - it('translates', function() - --- scroll to the top - visible:move_range(- #text) - local cur11 = Cursor() - local cur33 = Cursor(3, 3) - local cur3w = Cursor(3, w) - local cur3wp1 = Cursor(3, w + 1) - local cur44 = Cursor(4, 4) - assert.same(cur11, visible:translate_to_wrapped(cur11)) - assert.same(cur33, visible:translate_to_wrapped(cur33)) - assert.same(cur3w, visible:translate_to_wrapped(cur3w)) - assert.same(Cursor(4, 1), visible:translate_to_wrapped(cur3wp1)) - - assert.same(cur33, visible:translate_from_visible(cur33)) - local cur3_67 = Cursor(3, 3 + w) - local exp3_67 = Cursor(4, 3) - assert.same(exp3_67, visible:translate_to_wrapped(cur3_67)) - - --- scroll to bottom - visible:to_end() - -- #01: '' - -- #02: '* `string.ulen(s)` - as opposed to the builtin `len()`, this wor' - -- #03: 'ks for unicode strings' - -- #04: '* `string.usub(s, from, to)` - unicode substrings' - -- #05: '* `Char.is_alpha(c)` - is `c` a letter' - -- #06: '* `Char.is_alnum(c)` - is `c` a letter or a number (alphanumeric' - -- #07: ')' - -- #08: '' - assert.same(Cursor(9, 3 + w), - visible:translate_from_visible(cur33)) - assert.same(Cursor(10, 4), - visible:translate_from_visible(cur44)) - assert.is_nil(visible:translate_from_visible(Cursor(5, 40))) - local cur71 = Cursor(7, 1) - assert.same(Cursor(12, 65), - visible:translate_from_visible(cur71)) - end) -end) From 39ed54028b27278a7b079adb8196b0395dbe4a16 Mon Sep 17 00:00:00 2001 From: aldum Date: Sun, 19 Oct 2025 16:19:03 +0200 Subject: [PATCH 047/103] fix(vsc): import Scrollable --- src/view/editor/visibleStructuredContent.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/editor/visibleStructuredContent.lua b/src/view/editor/visibleStructuredContent.lua index f7521ec8..1af7cd0e 100644 --- a/src/view/editor/visibleStructuredContent.lua +++ b/src/view/editor/visibleStructuredContent.lua @@ -1,6 +1,7 @@ require("view.editor.visibleBlock") require("util.wrapped_text") +require("util.scrollable") require("util.range") --- @alias ReverseMap Dequeue From f1d9245e166d73b71f8defd54ab3947966eff6e3 Mon Sep 17 00:00:00 2001 From: aldum Date: Sun, 19 Oct 2025 16:54:52 +0200 Subject: [PATCH 048/103] refactor(vsc): introduce VSCOpts This also allows including the vievcfg --- src/controller/editorController.lua | 2 -- src/view/editor/bufferView.lua | 12 ++++++----- src/view/editor/visibleStructuredContent.lua | 21 +++++++++++++------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index cd75df2f..50d59040 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -109,8 +109,6 @@ function EditorController:pop_buffer() self.view:get_current_buffer():open(b) end -end - --- @param m EditorMode --- @return boolean local function is_normal(m) diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index ef9a2fbe..19cf84f3 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -65,12 +65,14 @@ function BufferView:open(buffer) elseif ct == 'lua' then local bufcon = buffer:get_content() self.content = - VisibleStructuredContent( - self.w, + VisibleStructuredContent({ + w = self.w, + overscroll = self.SCROLL_BY, + size_max = L, + view_config = self.cfg, + }, bufcon, - buffer.highlighter, - self.SCROLL_BY, - L) + buffer.highlighter) else error 'unknown filetype' end diff --git a/src/view/editor/visibleStructuredContent.lua b/src/view/editor/visibleStructuredContent.lua index 1af7cd0e..8f7712fc 100644 --- a/src/view/editor/visibleStructuredContent.lua +++ b/src/view/editor/visibleStructuredContent.lua @@ -4,6 +4,11 @@ require("util.wrapped_text") require("util.scrollable") require("util.range") +--- @class VSCOpts +--- @field w integer +--- @field size_max integer +--- @field overscroll integer +--- @field view_config ViewConfig --- @alias ReverseMap Dequeue --- Inverse mapping from line number to block index @@ -38,18 +43,20 @@ setmetatable(VisibleStructuredContent, { }) --- @param w integer +--- @param opts VSCOpts --- @param blocks Block[] --- @param highlighter fun(c: string[]): SyntaxColoring ---- @param overscroll integer ---- @param size_max integer --- @return VisibleStructuredContent -function VisibleStructuredContent.new(w, blocks, highlighter, - overscroll, size_max) +function VisibleStructuredContent.new( + opts, + blocks, + highlighter) local self = setmetatable({ + size_max = opts.size_max, + overscroll_max = opts.overscroll, + w = opts.w, + cfg = opts.view_config, highlighter = highlighter, - size_max = size_max, - overscroll_max = overscroll, - w = w, }, VisibleStructuredContent) self:load_blocks(blocks) self:to_end() From 5f83c853ca92ff1709ff3848734c6677ef44a41a Mon Sep 17 00:00:00 2001 From: aldum Date: Sun, 19 Oct 2025 16:55:51 +0200 Subject: [PATCH 049/103] feat(util): add more table helpers --- src/util/table.lua | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/util/table.lua b/src/util/table.lua index ed9f9c0d..dbf93fb3 100644 --- a/src/util/table.lua +++ b/src/util/table.lua @@ -363,3 +363,27 @@ function table.map(self, f) end return ret end + +--- Tabulate array values with index (returns new table) +--- @param self table +--- @param f function +--- @return table +function table.imap(self, f) + local ret = {} + for i, v in ipairs(self) do + ret[i] = f(v, i) + end + return ret +end + +--- Create a table of `n` elements by running `f` +--- @param n integer +--- @param f function +--- @return table +function table.fill(n, f) + local ret = {} + for i = 1, n do + ret[i] = f() + end + return ret +end From e98e35281a7db71fa9162f51cd03229d368d4c95 Mon Sep 17 00:00:00 2001 From: aldum Date: Sun, 19 Oct 2025 16:57:59 +0200 Subject: [PATCH 050/103] feat(parser): add trunc function Truncates a block to `n` lines --- src/model/lang/lua/parser.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/model/lang/lua/parser.lua b/src/model/lang/lua/parser.lua index c204e689..ad3cf448 100644 --- a/src/model/lang/lua/parser.lua +++ b/src/model/lang/lua/parser.lua @@ -420,11 +420,24 @@ return function(lib) end end + --- @param code str + --- @param n integer? + --- @return string[]? + local function trunc(code, n) + local lines = n or 1 + if lines == 1 then + local txt = string.lines(code) + local line1 = txt[1]:sub(1, -1) .. '…' + return { line1 } + end + end + return { parse = parse, pprint = pprint, highlighter = highlighter, ast_to_src = ast_to_src, chunker = chunker, + trunc = trunc, } end From b09d160f055259d2f5f807e6b8e05587dc44054d Mon Sep 17 00:00:00 2001 From: aldum Date: Sun, 19 Oct 2025 16:59:14 +0200 Subject: [PATCH 051/103] style: type annotations --- src/util/wrapped_text.lua | 2 +- src/view/editor/bufferView.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/wrapped_text.lua b/src/util/wrapped_text.lua index 28b1aab1..e375eede 100644 --- a/src/util/wrapped_text.lua +++ b/src/util/wrapped_text.lua @@ -37,7 +37,7 @@ require("util.lua") --- @field n_breaks integer --- --- @field wrap function ---- @field get_text function +--- @field get_text fun(self): Dequeue --- @field get_line function --- @field get_text_length function WrappedText = class.create() diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index 19cf84f3..b8579a87 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -30,6 +30,7 @@ end --- @field content_type ContentType --- @field buffers Dequeue --- +--- @field cfg ViewConfig --- @field LINES integer --- @field SCROLL_BY integer --- @field w integer From 312f4fdfafc2a703f2fde8e6e4ab74b5f12bc845 Mon Sep 17 00:00:00 2001 From: aldum Date: Sun, 19 Oct 2025 16:59:58 +0200 Subject: [PATCH 052/103] feat: add fold_lines to viewcfg --- src/main.lua | 2 ++ src/types.lua | 1 + 2 files changed, 3 insertions(+) diff --git a/src/main.lua b/src/main.lua index 46bc7ebe..8c24ad5a 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,5 +1,6 @@ local redirect_to = require("model.io.redirect") local OS = require("util.os") + require("model.consoleModel") require("controller.controller") require("controller.consoleController") @@ -104,6 +105,7 @@ local config_view = function(flags) drawableWidth = drawableWidth, drawableChars = drawableChars, + fold_lines = 1, drawtest = tf.draw, sizedebug = tf.size, } diff --git a/src/types.lua b/src/types.lua index 1a644972..25c60141 100644 --- a/src/types.lua +++ b/src/types.lua @@ -57,6 +57,7 @@ --- @field debugwidth integer --- @field drawableWidth number --- @field drawableChars integer +--- @field fold_lines integer --- @field drawtest boolean --- @field sizedebug boolean From 622a8f3bcd066eef848e4ab597b45b178de1461e Mon Sep 17 00:00:00 2001 From: aldum Date: Sun, 19 Oct 2025 17:00:27 +0200 Subject: [PATCH 053/103] feat(editor): add truncer to BufferModel --- src/controller/editorController.lua | 7 +++++-- src/model/editor/bufferModel.lua | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index 50d59040..a98bd0ae 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -44,7 +44,7 @@ function EditorController:open(name, content, save) local w = self.model.cfg.view.drawableChars local is_lua = string.match(name, '.lua$') local is_md = string.match(name, '.md$') - local ch, hl, pp + local ch, hl, pp, tr if is_lua then self.input:set_eval(LuaEditorEval) @@ -60,6 +60,9 @@ function EditorController:open(name, content, save) pp = function(t) return parser.pprint(t, w) end + tr = function(code) + return parser.trunc(code, self.model.cfg.view.fold_lines) + end elseif is_md then local mdEval = MdEval(name) hl = mdEval.highlighter @@ -68,7 +71,7 @@ function EditorController:open(name, content, save) self.input:set_eval(TextEval) end - local b = BufferModel(name, content, save, ch, hl, pp) + local b = BufferModel(name, content, save, ch, hl, pp, tr) self.model.buffers:push_front(b) self.view:open(b) self:update_status() diff --git a/src/model/editor/bufferModel.lua b/src/model/editor/bufferModel.lua index 32280249..75e51a07 100644 --- a/src/model/editor/bufferModel.lua +++ b/src/model/editor/bufferModel.lua @@ -32,7 +32,8 @@ end --- @param save function --- @param chunker Chunker? --- @param highlighter Highlighter? ---- @param printer function? +--- @param printer Printer? +--- @param truncer function? --- @return BufferModel? local function new( name, @@ -40,7 +41,8 @@ local function new( save, chunker, highlighter, - printer) + printer, + truncer) local _content, sel, ct, semantic local readonly = false @@ -83,6 +85,7 @@ local function new( chunker = chunker, highlighter = highlighter, printer = printer, + truncer = truncer, revmap = {}, semantic = semantic, selection = sel, @@ -112,6 +115,7 @@ end --- @field chunker Chunker --- @field highlighter Highlighter --- @field printer Printer +--- @field truncer function --- @field move_selection function --- @field get_selection function --- @field get_selected_text function From 6dbc146c4c7f898320e0407cc55e33b945374350 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 21 Oct 2025 16:57:02 +0200 Subject: [PATCH 054/103] fix(util): don't fail on missing love in fire_once --- src/util/debug.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/debug.lua b/src/util/debug.lua index 8e83f1cc..5656a121 100644 --- a/src/util/debug.lua +++ b/src/util/debug.lua @@ -522,7 +522,7 @@ Log = { once = once, fire_once = function() - if not love.DEBUG then return end + if not love or not love.DEBUG then return end love.debug.once = love.debug.once + 1 end, --- @param color integer From d5b82569f1267a55c27dafa04017ad8b12ffb9ad Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 22 Oct 2025 14:51:22 +0200 Subject: [PATCH 055/103] feat(test): add viewConfig mock values to TestUtil --- tests/testutil.lua | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/testutil.lua b/tests/testutil.lua index 7c1e4c1b..8b324f3e 100644 --- a/tests/testutil.lua +++ b/tests/testutil.lua @@ -1,6 +1,9 @@ require('util.table') require('util.string.string') +local w = 64 + +local noop = function() end --- @param init str --- @return fun(str): boolean, string? --- @return reftable handle @@ -18,5 +21,16 @@ local get_save_function = function(init) end return { - get_save_function = get_save_function + get_save_function = get_save_function, + noop = noop, + LINES = 16, + SCROLL_BY = 8, + w = w, + mock_view_cfg = { + view = { + drawableChars = w, + lines = 16, + input_max = 14 + }, + } } From ddfea3ea359a0960224aa6afa1daa8f99f75216f Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 22 Oct 2025 14:56:25 +0200 Subject: [PATCH 056/103] refactor: use VSCOpts internally --- src/view/editor/bufferView.lua | 2 +- src/view/editor/visibleStructuredContent.lua | 28 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index b8579a87..0aa15646 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -70,7 +70,7 @@ function BufferView:open(buffer) w = self.w, overscroll = self.SCROLL_BY, size_max = L, - view_config = self.cfg, + cfg = self.cfg, }, bufcon, buffer.highlighter) diff --git a/src/view/editor/visibleStructuredContent.lua b/src/view/editor/visibleStructuredContent.lua index 8f7712fc..a3ec91c4 100644 --- a/src/view/editor/visibleStructuredContent.lua +++ b/src/view/editor/visibleStructuredContent.lua @@ -5,15 +5,17 @@ require("util.scrollable") require("util.range") --- @class VSCOpts ---- @field w integer +--- @field wrap_w integer --- @field size_max integer ---- @field overscroll integer ---- @field view_config ViewConfig +--- @field overscroll_max integer +--- @field cfg ViewConfig --- @alias ReverseMap Dequeue --- Inverse mapping from line number to block index --- @class VisibleStructuredContent: WrappedText ---- @field overscroll_max integer +--- @field offset integer +--- @field overscroll integer +--- @field highlighter fun(c: string[]): SyntaxColoring --- @field size_max integer --- @field range Range? --- @field v_blocks Dequeue @@ -42,7 +44,6 @@ setmetatable(VisibleStructuredContent, { end, }) ---- @param w integer --- @param opts VSCOpts --- @param blocks Block[] --- @param highlighter fun(c: string[]): SyntaxColoring @@ -52,10 +53,8 @@ function VisibleStructuredContent.new( blocks, highlighter) local self = setmetatable({ - size_max = opts.size_max, - overscroll_max = opts.overscroll, - w = opts.w, - cfg = opts.view_config, + overscroll = opts.overscroll_max, + opts = opts, highlighter = highlighter, }, VisibleStructuredContent) self:load_blocks(blocks) @@ -67,7 +66,7 @@ end --- Set the visible range so that last of the content is visible function VisibleStructuredContent:to_end() self.range = Scrollable.to_end( - self.size_max, self:get_text_length()) + self.opts.size_max, self:get_text_length()) self.offset = self.range.start - 1 end @@ -78,16 +77,17 @@ function VisibleStructuredContent:load_blocks(blocks) local revmap = Dequeue.typed('integer') local visible_blocks = Dequeue() local off = 0 + local w = self.opts.wrap_w for bi, v in ipairs(blocks) do if v:is_empty() then fulltext:append('') local npos = v.pos:translate(off) visible_blocks:append( - VisibleBlock(self.w, { '' }, {}, v.pos, npos)) + VisibleBlock(w, { '' }, {}, v.pos, npos)) else fulltext:append_all(v.lines) local hl = self.highlighter(v.lines) - local vblock = VisibleBlock(self.w, v.lines, hl, + local vblock = VisibleBlock(w, v.lines, hl, v.pos, v.pos:translate(off)) off = off + vblock.wrapped.n_breaks visible_blocks:append(vblock) @@ -98,7 +98,7 @@ function VisibleStructuredContent:load_blocks(blocks) end end end - WrappedText._init(self, self.w, fulltext) + WrappedText._init(self, self.opts.wrap_w, fulltext) self:_init() self.reverse_map = revmap self.v_blocks = visible_blocks @@ -129,7 +129,7 @@ end --- @protected function VisibleStructuredContent:_update_overscroll() local len = WrappedText.get_text_length(self) - local over = math.min(self.overscroll_max, len) + local over = math.min(self.opts.overscroll_max, len) self.overscroll = over end From dca062fa90995515f2d13babd5734166d5e04ff7 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 22 Oct 2025 14:56:25 +0200 Subject: [PATCH 057/103] refactor(editor): rename w in BufferView --- src/view/editor/bufferView.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index 0aa15646..13638c49 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -15,7 +15,7 @@ local function new(cfg) cfg = cfg, LINES = l, SCROLL_BY = math.floor(l / 2), - w = cfg.drawableChars, + wrap_w = cfg.drawableChars, content = nil, content_type = nil, @@ -33,8 +33,8 @@ end --- @field cfg ViewConfig --- @field LINES integer --- @field SCROLL_BY integer ---- @field w integer --- @field offset integer +--- @field wrap_w integer --- @field more More --- --- @field open function @@ -61,14 +61,14 @@ function BufferView:open(buffer) local bufcon = buffer:get_text_content() self.buffer:highlight() self.content = VisibleContent( - self.w, bufcon, self.SCROLL_BY, L) + self.wrap_w, bufcon, self.SCROLL_BY, L) self.hl = self.buffer:get_highlight() elseif ct == 'lua' then local bufcon = buffer:get_content() self.content = VisibleStructuredContent({ - w = self.w, - overscroll = self.SCROLL_BY, + wrap_w = self.wrap_w, + overscroll_max = self.SCROLL_BY, size_max = L, cfg = self.cfg, }, From 37aa1fd0d3ad44d8bebbb63a6b3c48cc5a2f20a1 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 22 Oct 2025 14:56:25 +0200 Subject: [PATCH 058/103] refactor(editor): move offset from buffer into content --- src/view/editor/bufferView.lua | 26 ++++++++++---------- src/view/editor/visibleContent.lua | 1 + src/view/editor/visibleStructuredContent.lua | 2 ++ tests/editor/editor_spec.lua | 4 +-- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index 13638c49..4f43eb64 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -20,7 +20,7 @@ local function new(cfg) content = nil, content_type = nil, more = { up = false, down = false }, - offset = 0, + buffer = nil } end @@ -33,7 +33,6 @@ end --- @field cfg ViewConfig --- @field LINES integer --- @field SCROLL_BY integer ---- @field offset integer --- @field wrap_w integer --- @field more More --- @@ -78,10 +77,7 @@ function BufferView:open(buffer) error 'unknown filetype' end - -- TODO clean this up - local clen = self.content:get_text_length() - self.offset = math.max(clen - L, 0) - local off = self.offset + local off = self.content.offset if off > 0 then self.more.up = true end @@ -121,7 +117,7 @@ function BufferView:get_state() return { filename = buf.name, selection = buf.selection, - offset = self.offset, + offset = self.content.offset, } end @@ -153,7 +149,7 @@ function BufferView:refresh(moved) end local clen = self.content:get_content_length() - local off = self.offset + local off = self.content.offset local si = 1 + off local ei = math.min(self.LINES, clen + 1) + off self:_update_visible(Range(si, ei)) @@ -163,6 +159,11 @@ end --- scrolling --- ------------------- +--- @return integer +function BufferView:get_offset() + return self.content.offset +end + --- @private --- @return Range function BufferView:_get_end_range() @@ -193,8 +194,7 @@ function BufferView:scroll(dir, by, warp) end end end)() - local o = self.content:move_range(n) - self.offset = self.offset + o + self.content:move_range(n) end --- @param off integer @@ -303,7 +303,7 @@ function BufferView:draw(special) gfx.rectangle('fill', 0, l_y, width, fh) end - local off = self.offset + local off = self.content.offset for _, w in ipairs(ws) do for _, v in ipairs(w) do if self.content.range:inc(v) then @@ -330,7 +330,7 @@ function BufferView:draw(special) local text = wt:get_text() local highlight = { hl = block.highlight } local ltf = function(l) - return l + rs - 1 - self.offset + return l + rs - 1 - self.content.offset end local ctf = function(a) return a end local limit = self.cfg.lines @@ -382,7 +382,7 @@ function BufferView:draw(special) local seen = {} for ln = 1, self.LINES do local l_y = (ln - 1) * fh - local vln = ln + self.offset + local vln = ln + self.content.offset local ln_w = self.content.wrap_reverse[vln] if ln_w then local l = string.format('%3d', ln_w) diff --git a/src/view/editor/visibleContent.lua b/src/view/editor/visibleContent.lua index 68223f78..9ab4d7f6 100644 --- a/src/view/editor/visibleContent.lua +++ b/src/view/editor/visibleContent.lua @@ -134,6 +134,7 @@ function VisibleContent:move_range(by) if r then local nr, n = r:translate_limit(by, 1, upper) self:set_range(nr) + self.offset = nr.start - 1 return n end end diff --git a/src/view/editor/visibleStructuredContent.lua b/src/view/editor/visibleStructuredContent.lua index a3ec91c4..d0cddb37 100644 --- a/src/view/editor/visibleStructuredContent.lua +++ b/src/view/editor/visibleStructuredContent.lua @@ -56,6 +56,7 @@ function VisibleStructuredContent.new( overscroll = opts.overscroll_max, opts = opts, highlighter = highlighter, + offset = 0, }, VisibleStructuredContent) self:load_blocks(blocks) self:to_end() @@ -162,6 +163,7 @@ function VisibleStructuredContent:move_range(by) local upper = self:get_text_length() + self.overscroll local nr, n = r:translate_limit(by, 1, upper) self:set_range(nr) + self.offset = nr.start - 1 return n end return 0 diff --git a/tests/editor/editor_spec.lua b/tests/editor/editor_spec.lua index 21693cc4..f0953a10 100644 --- a/tests/editor/editor_spec.lua +++ b/tests/editor/editor_spec.lua @@ -202,7 +202,7 @@ describe('Editor #editor', function() it('loads', function() --- inital scroll is at EOF, meaning last l lines are visible --- plus the phantom line - assert.same(off, bv.offset) + assert.same(off, bv:get_offset()) assert.same(start_range, visible.range) end) local base = Range(1, l) @@ -261,7 +261,7 @@ describe('Editor #editor', function() it('loads', function() --- inital scroll is at EOF, meaning last l lines are visible --- plus the phantom line - assert.same(off, bv.offset) + assert.same(off, bv:get_offset()) assert.same(start_range, visible.range) end) local base = Range(1, l) From 7ffec2e6f4dac35ef4891dce0dadd0fa5d71b004 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 15:56:02 +0200 Subject: [PATCH 059/103] fix: set app_state on project open --- src/controller/consoleController.lua | 1 + src/view/input/statusline.lua | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 2210eb7c..e96a1bc2 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -590,6 +590,7 @@ function ConsoleController:open_project(name, play) then table.insert(package.loaders, 1, project_loader) end + love.state.app_state = 'project_open' end if open then print('Project ' .. name .. ' opened') diff --git a/src/view/input/statusline.lua b/src/view/input/statusline.lua index f3c0d037..e1cf7669 100644 --- a/src/view/input/statusline.lua +++ b/src/view/input/statusline.lua @@ -73,7 +73,8 @@ function Statusline:draw(status, nLines, time) if love.state.testing then gfx.print('testing', midX - (8 * cf.fw), start_text.y) end - gfx.print(love.state.app_state, midX - (13 * cf.fw), start_text.y) + gfx.print((love.state.app_state or '???'), + midX - (13 * cf.fw), start_text.y) if time then gfx.print(tostring(time), midX, start_text.y) end From 01bf1c175f6b4c218e39747bef7b2d338f64fd28 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 15:58:01 +0200 Subject: [PATCH 060/103] feat(editor): buffer close --- src/controller/consoleController.lua | 12 ++++++++++-- src/controller/controller.lua | 2 +- src/controller/editorController.lua | 10 ++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index e96a1bc2..451af7b8 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -658,8 +658,11 @@ function ConsoleController:edit(name, state) if ex then text = self:_readfile(filename) end - love.state.prev_state = love.state.app_state - love.state.app_state = 'editor' + + if love.state.app_state ~= 'editor' then + love.state.prev_state = love.state.app_state + love.state.app_state = 'editor' + end local save = function(newcontent) return self:_writefile(filename, newcontent) end @@ -668,6 +671,11 @@ function ConsoleController:edit(name, state) self.editor:restore_state(state) end +--- @return EditorState? +function ConsoleController:close_buffer() + self.editor:close_buffer() +end + --- @return EditorState? function ConsoleController:finish_edit() self.editor:save_state() diff --git a/src/controller/controller.lua b/src/controller/controller.lua index 1a963b27..01283d12 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -563,7 +563,7 @@ Controller = { if love.state.app_state == 'running' then C:stop_project_run() elseif love.state.app_state == 'editor' then - C:finish_edit() + C:close_buffer() end end if k == "r" then diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index a98bd0ae..3e2e94c9 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -112,6 +112,16 @@ function EditorController:pop_buffer() self.view:get_current_buffer():open(b) end +function EditorController:close_buffer() + local bs = self.model.buffers + local n_buffers = bs:length() + if n_buffers < 2 then + self.console:finish_edit() + else + self:pop_buffer() + end +end + --- @param m EditorMode --- @return boolean local function is_normal(m) From 1fc0f10b1f9e55c7f45523af580fd5d0938d48d6 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 15:58:57 +0200 Subject: [PATCH 061/103] dev: hide follow_require behind DEBUG flag --- src/controller/consoleController.lua | 1 + src/controller/editorController.lua | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 451af7b8..0ab31176 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -684,6 +684,7 @@ function ConsoleController:finish_edit() if ok then love.state.app_state = love.state.prev_state love.state.prev_state = nil + --- TODO clear bufferlist return self.editor:get_state() else print(err) diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index 3e2e94c9..58d8ba72 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -116,8 +116,10 @@ function EditorController:close_buffer() local bs = self.model.buffers local n_buffers = bs:length() if n_buffers < 2 then + -- Log.debug('fin', n_buffers) self.console:finish_edit() else + -- Log.debug(':bd', n_buffers) self:pop_buffer() end end @@ -184,6 +186,7 @@ end --- @param clipboard string? function EditorController:set_state(clipboard) + --- TODO: multibuffer support local buf = self:get_active_buffer() local bid = buf:get_id() local buf_view_state = self.view:get_buffer(bid):get_state() @@ -206,11 +209,13 @@ function EditorController:get_state() end function EditorController:save_state() + --- TODO: multibuffer support self:set_state(love.system.getClipboardText()) end --- @param state EditorState? function EditorController:restore_state(state) + --- TODO: multibuffer support if state then local buf = self:get_active_buffer() local sel = state.buffer.selection @@ -653,7 +658,7 @@ function EditorController:_normal_mode_keys(k) end -- step into - if Key.ctrl() then + if love.DEBUG and Key.ctrl() then if k == "o" then self:follow_require() end From de2fd494a432f94a3aab397c3389ebbbfbde61ba Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 22 Oct 2025 14:56:25 +0200 Subject: [PATCH 062/103] dev: profiling support --- src/assets/fonts/fraps.otf | Bin 0 -> 8692 bytes src/conf.lua | 12 ++ src/controller/consoleController.lua | 5 + src/controller/controller.lua | 57 ++++++- src/controller/profiler.lua | 69 +++++++++ src/examples/life/main.lua | 1 + src/lib/profile.lua | 222 +++++++++++++++++++++++++++ src/main.lua | 5 +- src/types.lua | 12 ++ src/view/view.lua | 24 +++ 10 files changed, 402 insertions(+), 5 deletions(-) create mode 100644 src/assets/fonts/fraps.otf create mode 100644 src/controller/profiler.lua create mode 100644 src/lib/profile.lua diff --git a/src/assets/fonts/fraps.otf b/src/assets/fonts/fraps.otf new file mode 100644 index 0000000000000000000000000000000000000000..2660b1b9fc1eb9357c65dc463aea0083c06f8d0c GIT binary patch literal 8692 zcmeHMdu&@*8UL>D!%6Hs+%6qsZn=d@ly;4iwrkg^W9x%Vt;Sk4RrgT0&Pkm3HV=EY z)27viO86%u)T#zZlNg8zsYs~ok7+Q8QHejsL3$==o7ZC`kG-5w(2dqm5=vAbu_gG8he zt^OFl4f}dx8#8P6y^g6i+&|Sf5>E$yVLeF{cmZ?s$wa(=!;`s}@a!DETauUvy>5IH z@Q3)WNsi>kmxCK{)A+6(iI1mg?|!~-z+jEWM-tZQ&0C3Xzm2Hrm+4eCN57|j$o~VL z+pGzHpZ#q0xg)LH|3wtMh#C5DYx`^E-^J(5X>%I)4HOU*g2tR)e2&O`gQ#>BPlXmt zqprO~&;ug{^w+dAWRjtOq<;kLZ^>W4{g}sPC=&csaOv(jcz_-tn@*b3B@42qwd1y? zmHt>_SZHK}dXY$n)*3YAUq8%q?tI{K`MHZ%>(}`Djp5azz1|#K>v}lV&sl$0cZQ9( zZj48@ELb;yua56+7-4C|h(Z4p_idOl=u7lvT1R(L2W_Lr=?I-H!P2n5JKIV3V>VtY zl|Cr_wKQFNzx0>Vd!=_v7fWwfdJa_oyVw8Wnl&pV)^In8ZO*7VQ-}7)+N9UDOFA6?a zPW{k69?#mnW6p5SKJMg_c6Vwt*OSYP_2rs&rP3!d&R{ZUKjMtr8#}u?1(AIqF*r6H z&qz&lOEP@$R&;hvFa*r`Noyt_ zEQk?kpaMLX=Xs1vD1Zyz{3Jg=*)A!;1(b`=C~WJp;9p!93~8k4lq3ofH50WGMH6eo+6e{#w5)1b+p@9cv6i2;Ty2fE##?{W7HK=& z_EOYXoPFnf`}~>Wedhd`h5O9$AGH_c4Q8UVF9&B|SrIi41jWMwW6b}gxL@Kde#eqC ze_r^$Fm=>`?P2Sd+5V|1zwuiN=|X5G!Zw@}7M;l#LhZ#8-z{K_z`pD&xI@twLd8;h zVUiK%CxgX_6&}1`E|e;G&-Me^WZu$SGt7oGMsSNv;cK zWju)``E{I&D80&yD3*#7V$IMYQr1UG@oXVAlv=S45!PAiBUiz2p0$-4E=(*;R2YDb zJJntYxwe%&@~p4*n6PQ_r@UZy^M)*ehF#M~f!B#n~iN1L6_#^C4w|qUYvf zNqW`GESVqB*vl+rnI}r+BH}|3s&w^ZSHah!l%au9hK9^$1j`anDQT-$LataMn^J`# zipJ8yP^*5_)EP#pA+1uzT)rT~QAuRy)h|;*MTC`4#WKNl3Kpx#egiG6BBBQVAI3FmyL-_5;_SPzk|{)gfR3*XbU@EnKp^b`wG5 zNU5{j4A>4gBndJKA(jLSGJIXBa41Z3Gc+Q%iY80oAW-GNw?D>WU|Z4 zl)Qu?z!S!)!OpGWqAU99$Xp(2!hs}${f5K5YF8!0)$Kz|f<|<0=>g$w7AbR5I1koP zf>pi?`+*y3QH`Yt%ZREi${KtEGJh=jdy-ee%B=+k1qQ}zdsCE5p`QvpLF1?N@ctQOP5k5 z&P^j}X((BbN_JmVd#p%qWFuVsq(FESYpqA>c@)TyqjYp@5;p^{Rz)hCwETzU_Bas2 z{UU$S{gFFTre|NzHRV4)ZV84nplsQS;DZ|(OiL16{U?yvB4p(*9=dZFb6fZSCnz2* zOd=p;q^Z9SvX_WB zzY-OXWf7>i9MKo5S_e!g{?1UodKZ< zdYQKp<|r1F7)2(l&LH73$Lc?|*!N9%l~J}$E~oBtu|JW@$pQr@c;1sH5B7-jkiyT) z*(P-pX~S|w&T++-RmPWXBI&E#E{*~>1&(@0f@?fT!%4Y&7oZlLJjzSQ{B{K!^m*!5 zut_WEDFs`=Ii}zs9imeTZomm9&U(ewNS4N@o_saYZgDEkaEL~=hZw*Wrf+M%k{}L! z_6K$=xP=Y}aB45E<#ZtMsRVIw+NSUH&O4Xs2NbN4srM^5KtbJ6u#Wk(f(>fc#}sT* zNS{!!1)S3g4$^x4c?CDn3jMr-8%ftMDY%JN>z5TAqOJOjg2VJRqeH>XwAR2`pU@hX zHGZJr<@6Qff`sv~!T5|%gN;Vutuba548NGG6b!$ZYZVN?m|GMKznBjw7=AGyS1|ly zrWFjom?sqsznB*l48NH33Wi^-fP&!{t6RbFi}k94;g{f76%4-w^9qJvf`6AV`=wz! z?V=Q==>%nPPB)07yd2r|2;g!2Zlq4?!gl~q?)i?pvZvI zft*qB>_W`?Id+Tr9Q8nQ2AunZ1`a4}$gst19B*I}G$LdUVJ-#9He#GPR#J9=J_Sr( zM^0#bFU9Z~P`FvaPfBrd#cJ(nc{TP*a6b-dtV1t!au%W*(C@UAVp>87OC)=63^ z3yrhjmV(3>ZHCRZ(LETqh?Tg$xS2K9mX;Ep@yptzq}`3*7^(&PxgV>}3g{T#W@Lpw z2Qen_+=0E5h@lMb;;@Z_xdiOPQ8j`Odx)*L1JBsMj<6@=$!O>SHAfQD?0^i1h5)-O zlfLV#;x0Yx^6mhq3}P}SsQW<85)=5wAnOEe02BwjNyJ?YQRC{^3uq8>pN5CySjQ0V zN5#xZ(5ImVX9a726f$H4j9^a22IogF@cL;dXa+G3pr$6tsdJ9+KgO>(*gurWWS!Kg zjTZ%CzWhqScCvQd&Sm2LiII3_$W9IT-WPQArACsuTsnJiEH;3*2-zIJMc`SyKKPU1 zbgBN^hT4=qM$jl5%O+FDW1G9S-P5%tCOBS~ER|7In?QE==Gd{}c)yb!jvqUcO=JcW ziDOP*a#to1&pF2u_O8^(2*lVsa=DDtJCD;P(vd-VCe4@(Us(f4v|EtP}9TC4R=j(T 0 then click_timer = click_timer - dt end @@ -396,7 +404,9 @@ Controller = { then wrap(uup, dt) end - Controller.snapshot() + if _mode ~= 'play' then + Controller.snapshot() + end if love.harmony then love.harmony.timer_update(dt) end @@ -417,6 +427,7 @@ Controller = { set_love_draw = function(C, CV) local function draw() View.draw(C, CV) + View.drawFPS() end love.draw = draw @@ -470,8 +481,9 @@ Controller = { --- public --- ---------------- --- @param C ConsoleController - init = function(C) + init = function(C, mode) _C = C + _mode = mode end, --- @param C ConsoleController --- @param CV ConsoleView @@ -527,6 +539,7 @@ Controller = { local handlers = love.handlers handlers.keypressed = function(k) + --- Power shortcuts local function quickswitch() if Key.ctrl() and k == 't' then if love.state.app_state == 'running' @@ -577,15 +590,40 @@ Controller = { C:restart() end end + local function profile() + if Key.ctrl() and Key.alt() and k == "p" then + if Key.shift() then + Prof.stop_profiler() + else + -- Prof.start_profiler() + Prof.start_oneshot() + end + end + if k == "f10" then + if love.PROFILE.fpsc == 'off' then + love.PROFILE.fpsc = 'T_L' + elseif love.PROFILE.fpsc == 'T_L' then + love.PROFILE.fpsc = 'T_R' + elseif love.PROFILE.fpsc == 'T_R' then + love.PROFILE.fpsc = 'off' + end + end + end if playback then if love.state.app_state == 'shutdown' then love.event.quit() end restart() + if love.PROFILE then + profile() + end else restart() quickswitch() + if love.PROFILE then + profile() + end project_state_change() end @@ -768,4 +806,15 @@ Controller = { Controller._userhandlers = {} View.clear_snapshot() end, + + oneshot = function() + if not love.PROFILE then return end + Prof.start_oneshot() + end, + + report = function() + if not love.PROFILE then return end + local report = Prof.report() + Log.debug(report) + end, } diff --git a/src/controller/profiler.lua b/src/controller/profiler.lua new file mode 100644 index 00000000..41ba7050 --- /dev/null +++ b/src/controller/profiler.lua @@ -0,0 +1,69 @@ +if not love.profiler then + Log.debug('profreq') + love.profiler = require('lib.profile') +end + +--- Run `f` if profiling is enabled +local guard = function(f) + local pr = love.PROFILE + if type(pr) ~= 'table' then return end + + return function() + f() + end +end + +local start = function(oneshot) + Log.debug('Starting profiler') + love.PROFILE.running = true + love.PROFILE.oneshot = oneshot + + love.profiler.start() +end + +local stop = function() + local pr = love.PROFILE + if type(pr) ~= 'table' then return end + Log.debug('Stopping profiling') + love.PROFILE.running = false + love.profiler.stop() +end + +local update = function() + local pr = love.PROFILE + if pr.running then + local n = pr.n_frames + local r = pr.n_rows + pr.frame = pr.frame + 1 + if pr.frame % n == 0 then + local rep = love.profiler.report(r) + Log.debug('Report:\n', rep) + if not pr.reports then + pr.reports = {} + end + table.insert(pr.reports, rep) + if pr.oneshot then + stop() + end + end + end +end + +local report = function() + local r = table.clone(love.PROFILE.report) + love.PROFILE.report = nil + love.profiler.reset() + return r +end + +local p = { + start_profiler = guard(start), + start_oneshot = guard(function() + start(true) + end), + stop_profiler = guard(stop), + update = guard(update), + report = guard(report), +} + +return p diff --git a/src/examples/life/main.lua b/src/examples/life/main.lua index c3412a62..ad393c99 100644 --- a/src/examples/life/main.lua +++ b/src/examples/life/main.lua @@ -1,3 +1,4 @@ +--- Conway's Game of Life --- original from https://github.com/Aethelios/Conway-s-Game-of-Life-in-Lua-and-Love2D gfx = love.graphics diff --git a/src/lib/profile.lua b/src/lib/profile.lua new file mode 100644 index 00000000..e6af5f6f --- /dev/null +++ b/src/lib/profile.lua @@ -0,0 +1,222 @@ +--[[ +This file is a part of the "profile.lua" library. +https://github.com/2dengine/profile.lua + +MIT License + +Copyright (c) 2015 2dengine LLC + +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 clock = os.clock + +--- The "profile" module controls when to start or stop collecting data and can be used to generate reports. +-- @module profile +-- @alias profile +local profile = {} + +-- function labels +local _labeled = {} +-- function definitions +local _defined = {} +-- time of last call +local _tcalled = {} +-- total execution time +local _telapsed = {} +-- number of calls +local _ncalls = {} +-- list of internal profiler functions +local _internal = {} + +--- This is an internal function. +-- @tparam string event Event type +-- @tparam number line Line number +-- @tparam[opt] table info Debug info table +function profile.hooker(event, line, info) + info = info or debug.getinfo(2, 'fnS') + local f = info.func + -- ignore the profiler itself + if _internal[f] or info.what ~= "Lua" then + return + end + -- get the function name if available + if info.name then + _labeled[f] = info.name + end + -- find the line definition + if not _defined[f] then + _defined[f] = info.short_src .. ":" .. info.linedefined + _ncalls[f] = 0 + _telapsed[f] = 0 + end + if _tcalled[f] then + local dt = clock() - _tcalled[f] + _telapsed[f] = _telapsed[f] + dt + _tcalled[f] = nil + end + if event == "tail call" then + local prev = debug.getinfo(3, 'fnS') + profile.hooker("return", line, prev) + profile.hooker("call", line, info) + elseif event == 'call' then + _tcalled[f] = clock() + else + _ncalls[f] = _ncalls[f] + 1 + end +end + +--- Sets a clock function to be used by the profiler. +-- @tparam function func Clock function that returns a number +function profile.setclock(f) + assert(type(f) == "function", "clock must be a function") + clock = f +end + +--- Starts collecting data. +function profile.start() + if rawget(_G, 'jit') then + jit.off() + jit.flush() + end + debug.sethook(profile.hooker, "cr") +end + +--- Stops collecting data. +function profile.stop() + debug.sethook() + for f in pairs(_tcalled) do + local dt = clock() - _tcalled[f] + _telapsed[f] = _telapsed[f] + dt + _tcalled[f] = nil + end + -- merge closures + local lookup = {} + for f, d in pairs(_defined) do + local id = (_labeled[f] or '?') .. d + local f2 = lookup[id] + if f2 then + _ncalls[f2] = _ncalls[f2] + (_ncalls[f] or 0) + _telapsed[f2] = _telapsed[f2] + (_telapsed[f] or 0) + _defined[f], _labeled[f] = nil, nil + _ncalls[f], _telapsed[f] = nil, nil + else + lookup[id] = f + end + end + collectgarbage('collect') +end + +--- Resets all collected data. +function profile.reset() + for f in pairs(_ncalls) do + _ncalls[f] = 0 + end + for f in pairs(_telapsed) do + _telapsed[f] = 0 + end + for f in pairs(_tcalled) do + _tcalled[f] = nil + end + collectgarbage('collect') +end + +--- This is an internal function. +-- @tparam function a First function +-- @tparam function b Second function +-- @treturn boolean True if "a" should rank higher than "b" +function profile.comp(a, b) + local dt = _telapsed[b] - _telapsed[a] + if dt == 0 then + return _ncalls[b] < _ncalls[a] + end + return dt < 0 +end + +--- Generates a report of functions that have been called since the profile was started. +-- Returns the report as a numeric table of rows containing the rank, function label, number of calls, total execution time and source code line number. +-- @tparam[opt] number limit Maximum number of rows +-- @treturn table Table of rows +function profile.query(limit) + local t = {} + for f, n in pairs(_ncalls) do + if n > 0 then + t[#t + 1] = f + end + end + table.sort(t, profile.comp) + if limit then + while #t > limit do + table.remove(t) + end + end + for i, f in ipairs(t) do + local dt = 0 + if _tcalled[f] then + dt = clock() - _tcalled[f] + end + t[i] = { i, _labeled[f] or '?', _ncalls[f], _telapsed[f] + dt, _defined[f] } + end + return t +end + +local cols = { 3, 29, 11, 24, 32 } + +--- Generates a text report of functions that have been called since the profile was started. +-- Returns the report as a string that can be printed to the console. +-- @tparam[opt] number limit Maximum number of rows +-- @treturn string Text-based profiling report +function profile.report(n) + local out = {} + local report = profile.query(n) + for i, row in ipairs(report) do + for j = 1, 5 do + local s = row[j] + local l2 = cols[j] + s = tostring(s) + local l1 = s:len() + if l1 < l2 then + s = s .. (' '):rep(l2 - l1) + elseif l1 > l2 then + s = s:sub(l1 - l2 + 1, l1) + end + row[j] = s + end + out[i] = table.concat(row, ' | ') + end + + local row = + " +-----+-------------------------------+-------------+--------------------------+----------------------------------+ \n" + local col = + " | # | Function | Calls | Time | Code | \n" + local sz = row .. col .. row + if #out > 0 then + sz = sz .. ' | ' .. table.concat(out, ' | \n | ') .. ' | \n' + end + return '\n' .. sz .. row +end + +-- store all internal profiler functions +for _, v in pairs(profile) do + if type(v) == "function" then + _internal[v] = true + end +end + +return profile diff --git a/src/main.lua b/src/main.lua index 8c24ad5a..fee2f708 100644 --- a/src/main.lua +++ b/src/main.lua @@ -264,6 +264,9 @@ function love.load() local autotest = mode == 'test' and startup.testflags.auto or false local playback = mode == 'play' + if love.PROFILE then + love.profiler = require('lib.profile') + end if playback and not string.is_non_empty_string(startup.path) then exit(messages.play_no_project) @@ -340,7 +343,7 @@ function love.load() local CV = ConsoleView(baseconf, CC) CC:set_view(CV) - ctrl.init(CC) + ctrl.init(CC, mode) ctrl.setup_callback_handlers(CC) ctrl.set_default_handlers(CC, CV) diff --git a/src/types.lua b/src/types.lua index 25c60141..c2b5f785 100644 --- a/src/types.lua +++ b/src/types.lua @@ -166,3 +166,15 @@ --- --- @field tokenize fun(str): table --- @field syntax_hl fun(table): SyntaxColoring + +---@alias FPSC +---| 'off' +---| 'T_L" +---| 'T_R" + +--- @class Profile +--- @field report table +--- @field frame integer +--- @field n_frames integer +--- @field n_rows integer +--- @field fpsc FPSC diff --git a/src/view/view.lua b/src/view/view.lua index b56302ad..81dc6794 100644 --- a/src/view/view.lua +++ b/src/view/view.lua @@ -1,6 +1,10 @@ +local gfx = love.graphics + --- @type love.Image? local canvas_snapshot = nil +local FPSfont = gfx.newFont("assets/fonts/fraps.otf", 24) + View = { prev_draw = nil, main_draw = nil, @@ -30,6 +34,26 @@ View = { clear_snapshot = function() canvas_snapshot = nil end, + + drawFPS = function() + local pr = love.PROFILE + if type(pr) ~= 'table' then return end + if love.PROFILE.fpsc == 'off' then return end + + local fps = tostring(love.timer.getFPS()) + local w = FPSfont:getWidth(fps) + local x + if love.PROFILE.fpsc == 'T_L' then + x = 10 + elseif love.PROFILE.fpsc == 'T_R' then + x = gfx.getWidth() - 10 - w + end + gfx.push('all') + gfx.setColor(Color[Color.yellow]) + gfx.setFont(FPSfont) + gfx.print(fps, x, 10) + gfx.pop() + end } --- @class ViewBase From 92819e2e4265cf73ba52cd640777822215fab944 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 22 Oct 2025 14:56:25 +0200 Subject: [PATCH 063/103] stash: testutil usage refactor --- tests/editor/editor_spec.lua | 23 +++++------------------ tests/input/user_input_view_spec.lua | 12 ++++-------- tests/testutil.lua | 25 ++++++++++++++++--------- 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/tests/editor/editor_spec.lua b/tests/editor/editor_spec.lua index f0953a10..10cd8336 100644 --- a/tests/editor/editor_spec.lua +++ b/tests/editor/editor_spec.lua @@ -25,17 +25,6 @@ describe('Editor #editor', function() 'Turtle graphics game inspired the LOGO family of languages.', '', } - --- @param w integer - --- @param l integer? - local function getMockConf(w, l) - return { - view = { - drawableChars = w, - lines = l or 16, - input_max = 14 - }, - } - end --- @param cfg Config --- @return EditorController @@ -75,7 +64,7 @@ describe('Editor #editor', function() describe('opens', function() it('no wrap needed', function() local w = 80 - local controller = wire(getMockConf(w)) + local controller = wire(TU.mock_view_cfg(w)) local save = TU.get_save_function(turtle_doc) controller:open('turtle', turtle_doc, save) @@ -100,7 +89,7 @@ describe('Editor #editor', function() local w = 16 love.state.app_state = 'editor' - local controller, press = wire(getMockConf(w)) + local controller, press = wire(TU.mock_view_cfg(w)) local model = controller.model local save = TU.get_save_function(turtle_doc) @@ -184,7 +173,7 @@ describe('Editor #editor', function() describe('with scroll', function() local l = 6 - local controller, _, view = wire(getMockConf(80, l)) + local controller, _, view = wire(TU.mock_view_cfg(80, l)) local model = controller.model local save = TU.get_save_function(sierpinski) @@ -238,7 +227,7 @@ describe('Editor #editor', function() describe('with scroll and wrap', function() local l = 6 - local controller, _, view = wire(getMockConf(27, l)) + local controller, _, view = wire(TU.mock_view_cfg(27, l)) local save = TU.get_save_function(sierpinski) controller:open('sierpinski.txt', sierpinski, save) @@ -439,9 +428,7 @@ describe('Editor #editor', function() --- end plaintext describe('structured (lua) works', function() - local l = 16 - - local controller, press = wire(getMockConf(64, l)) + local controller, press = wire(TU.mock_view_cfg()) local save, savefile = TU.get_save_function(sierpinski) controller:open('sierpinski.lua', sierpinski, save) diff --git a/tests/input/user_input_view_spec.lua b/tests/input/user_input_view_spec.lua index ad2ae602..8b3f5bba 100644 --- a/tests/input/user_input_view_spec.lua +++ b/tests/input/user_input_view_spec.lua @@ -3,15 +3,11 @@ require("model.input.userInputModel") require("controller.userInputController") require("view.input.userInputView") +TU = require('tests.testutil') + describe("input view spec #input", function() - local w = 24 - local mockConf = { - view = { - drawableChars = w, - lines = 16, - input_max = 14 - }, - } + local w = 8 + local mockConf = TU.mock_view_cfg(w) mock = require("tests.mock") local love = { diff --git a/tests/testutil.lua b/tests/testutil.lua index 8b324f3e..846b1fb1 100644 --- a/tests/testutil.lua +++ b/tests/testutil.lua @@ -1,7 +1,7 @@ require('util.table') require('util.string.string') -local w = 64 +local wrap = 64 local noop = function() end --- @param init str @@ -20,17 +20,24 @@ local get_save_function = function(init) return save, handle end +--- @param w integer? +--- @param l integer? +local function getMockConf(w, l) + return { + view = { + drawableChars = w or wrap, + lines = l or 16, + input_max = 14, + fh = 32, + }, + } +end + return { get_save_function = get_save_function, noop = noop, LINES = 16, SCROLL_BY = 8, - w = w, - mock_view_cfg = { - view = { - drawableChars = w, - lines = 16, - input_max = 14 - }, - } + w = wrap, + mock_view_cfg = getMockConf, } From c4a5ad07560cd8590ec8f4bfd6a5dad18f56611a Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 21:52:58 +0200 Subject: [PATCH 064/103] feat: remove time from statusline --- src/view/input/statusline.lua | 6 +----- src/view/input/userInputView.lua | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/view/input/statusline.lua b/src/view/input/statusline.lua index e1cf7669..1a132826 100644 --- a/src/view/input/statusline.lua +++ b/src/view/input/statusline.lua @@ -8,8 +8,7 @@ end) --- @param status Status --- @param nLines integer ---- @param time number? -function Statusline:draw(status, nLines, time) +function Statusline:draw(status, nLines) local gfx = love.graphics local cf = self.cfg local colors = (function() @@ -75,9 +74,6 @@ function Statusline:draw(status, nLines, time) end gfx.print((love.state.app_state or '???'), midX - (13 * cf.fw), start_text.y) - if time then - gfx.print(tostring(time), midX, start_text.y) - end gfx.setColor(colors.fg) end diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 625ec029..9b1c472d 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -122,7 +122,7 @@ function UserInputView:draw_input(input, time) local visible = vc:get_visible() gfx.setFont(self.cfg.font) drawBackground() - self.statusline:draw(status, apparentLines, time) + self.statusline:draw(status, apparentLines) if highlight then local hl = highlight.hl From 9e778fa5575b6f8a24f8ccc3753eeddd7d0dd987 Mon Sep 17 00:00:00 2001 From: aldum Date: Wed, 29 Oct 2025 13:47:10 +0100 Subject: [PATCH 065/103] feat: remove time from input --- src/controller/controller.lua | 7 ++----- src/view/consoleView.lua | 6 +----- src/view/input/userInputView.lua | 8 +++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index 649ea447..0838cd65 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -109,6 +109,7 @@ local set_handlers = function(userlove) local draw = userlove.draw if draw and draw ~= View.main_draw then + --- @diagnostic disable-next-line: duplicate-set-field love.draw = function() draw() View.drawFPS() @@ -387,11 +388,7 @@ Controller = { end local user_input = get_user_input() if user_input then - if love.DEBUG then - user_input.V:draw(user_input.C:get_input(), C.time) - else - user_input.V:draw(user_input.C:get_input()) - end + user_input.V:draw(user_input.C:get_input()) end end View.prev_draw = draw diff --git a/src/view/consoleView.lua b/src/view/consoleView.lua index 2f8be205..417b09e5 100644 --- a/src/view/consoleView.lua +++ b/src/view/consoleView.lua @@ -50,11 +50,7 @@ function ConsoleView:draw(terminal, canvas, input, snapshot) self.drawable_height, snapshot) if ViewUtils.conditional_draw('show_input') then - local time = nil - if self.cfg.view.show_debug_timer then - time = self.controller:get_timestamp() - end - self.input:draw(input, time) + self.input:draw(input) end end diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 9b1c472d..4e8de32e 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -23,8 +23,7 @@ end UserInputView = class.create(new) --- @param input InputDTO ---- @param time number? -function UserInputView:draw_input(input, time) +function UserInputView:draw_input(input) local gfx = love.graphics local cfg = self.cfg @@ -263,8 +262,7 @@ function UserInputView:draw_input(input, time) end --- @param input InputDTO ---- @param time number? -function UserInputView:draw(input, time) +function UserInputView:draw(input) local err_text = input.wrapped_error or {} local isError = string.is_non_empty_string_array(err_text) @@ -294,7 +292,7 @@ function UserInputView:draw(input, time) ViewUtils.write_line(l, str, start_y, breaks, self.cfg) end else - self:draw_input(input, time) + self:draw_input(input) end end From 61c76ca3692c3eb71912f203b2ca9e1262cd34d4 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 21:52:58 +0200 Subject: [PATCH 066/103] feat(status): add start_y param --- src/view/input/statusline.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/view/input/statusline.lua b/src/view/input/statusline.lua index 1a132826..8384e50e 100644 --- a/src/view/input/statusline.lua +++ b/src/view/input/statusline.lua @@ -8,7 +8,8 @@ end) --- @param status Status --- @param nLines integer -function Statusline:draw(status, nLines) +--- @param start_y integer? +function Statusline:draw(status, nLines, start_y) local gfx = love.graphics local cf = self.cfg local colors = (function() @@ -22,7 +23,8 @@ function Statusline:draw(status, nLines) return cf.colors.statusline.console end end)() - local h = cf.h + + local h = start_y or cf.h local w = cf.w local fh = cf.fh local font = cf.font From 656828d2a4dbe454c28b68702085cb109844d448 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 21:52:58 +0200 Subject: [PATCH 067/103] refactor: tweaks --- src/controller/controller.lua | 4 +++- src/controller/profiler.lua | 1 - src/model/input/history.lua | 2 +- src/util/debug.lua | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index 0838cd65..acb49dc1 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -812,6 +812,8 @@ Controller = { report = function() if not love.PROFILE then return end local report = Prof.report() - Log.debug(report) + if report then + Log.debug(report) + end end, } diff --git a/src/controller/profiler.lua b/src/controller/profiler.lua index 41ba7050..f7aff218 100644 --- a/src/controller/profiler.lua +++ b/src/controller/profiler.lua @@ -1,5 +1,4 @@ if not love.profiler then - Log.debug('profreq') love.profiler = require('lib.profile') end diff --git a/src/model/input/history.lua b/src/model/input/history.lua index 63d24585..d40e2284 100644 --- a/src/model/input/history.lua +++ b/src/model/input/history.lua @@ -106,5 +106,5 @@ function History:_dump(f) end) local i = self.index or '-' local l = ' [' .. self:length() .. '] ' - log(i .. l .. Debug.text_table(t, false, nil, 64)) + log(i .. l .. Debug.text_table(t, false, nil, 64) ) end diff --git a/src/util/debug.lua b/src/util/debug.lua index 5656a121..1209f057 100644 --- a/src/util/debug.lua +++ b/src/util/debug.lua @@ -313,7 +313,7 @@ Debug = { --- @param t string[]? --- @param no_ln boolean? --- @param skip integer? - --- @param trunc boolean? + --- @param trunc number? --- @return string text_table = function(t, no_ln, skip, trunc) local res = '\n' From 9844ac96df5080cdb827b7b35c63e946cbba0777 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 00:17:11 +0100 Subject: [PATCH 068/103] style: cleanup --- tests/editor/editor_spec.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/editor/editor_spec.lua b/tests/editor/editor_spec.lua index 10cd8336..2e983e33 100644 --- a/tests/editor/editor_spec.lua +++ b/tests/editor/editor_spec.lua @@ -90,7 +90,6 @@ describe('Editor #editor', function() love.state.app_state = 'editor' local controller, press = wire(TU.mock_view_cfg(w)) - local model = controller.model local save = TU.get_save_function(turtle_doc) From 49c05b94a53045a605c2d56dad75a7efabee07af Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 13:46:51 +0100 Subject: [PATCH 069/103] feat(debug): extend hide main view component to editor --- src/controller/controller.lua | 1 + src/main.lua | 3 ++- src/view/editor/editorView.lua | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index acb49dc1..00463204 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -176,6 +176,7 @@ Controller = { if love.DEBUG then if k == "1" then table.toggle(love.debug, 'show_terminal') + table.toggle(love.debug, 'show_buffer') end if k == "2" then table.toggle(love.debug, 'show_snapshot') diff --git a/src/main.lua b/src/main.lua index fee2f708..331af29b 100644 --- a/src/main.lua +++ b/src/main.lua @@ -306,8 +306,9 @@ function love.load() } if love.DEBUG then love.debug = { - show_snapshot = true, show_terminal = true, + show_buffer = true, + show_snapshot = true, show_canvas = true, show_input = true, once = 0 diff --git a/src/view/editor/editorView.lua b/src/view/editor/editorView.lua index f6cd20a4..80e92b01 100644 --- a/src/view/editor/editorView.lua +++ b/src/view/editor/editorView.lua @@ -35,7 +35,10 @@ function EditorView:draw() else local spec = mode == 'reorder' local bv = self:get_current_buffer() - bv:draw(spec) + + if ViewUtils.conditional_draw('show_buffer') then + bv:draw(spec) + end if ViewUtils.conditional_draw('show_input') then local input = ctrl:get_input() self.input:draw(input) From 5464ca5f422f4003f69dc5d1596e94aabde5366e Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 13:46:51 +0100 Subject: [PATCH 070/103] feat(debug): screenshot a canvas --- src/util/view.lua | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/util/view.lua b/src/util/view.lua index 2dfec3d6..cd4fb48f 100644 --- a/src/util/view.lua +++ b/src/util/view.lua @@ -1,3 +1,5 @@ +if not love or not love.graphics then return end + require("util.string.string") --- @param cfg ViewConfig @@ -196,12 +198,22 @@ local function draw_hl_text(text, highlight, cfg, options) end end +--- @param canvas love.Canvas? +local function screenshot(canvas) + if not love.DEBUG then return end + local cv = canvas or gfx.getCanvas() + local img = cv:newImageData() + local filename = os.time() .. ".png" + img:encode("png", filename) +end + ViewUtils = { get_drawable_height = get_drawable_height, write_line = write_line, write_token = write_token, draw_hl_text = draw_hl_text, conditional_draw = conditional_draw, + screenshot = screenshot, blendModes = blendModes, } From 54444a8bdbf8aadc7fb599abcf486d4f23786804 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 4 Nov 2025 12:36:27 +0100 Subject: [PATCH 071/103] fix: create love.state earlier Necessary for player early fails --- src/main.lua | 16 +++++++++------- src/types.lua | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main.lua b/src/main.lua index 331af29b..314c79b3 100644 --- a/src/main.lua +++ b/src/main.lua @@ -268,6 +268,12 @@ function love.load() love.profiler = require('lib.profile') end + --- @type LoveState + love.state = { + testing = false, + app_state = 'starting', + } + if playback and not string.is_non_empty_string(startup.path) then exit(messages.play_no_project) return @@ -297,13 +303,9 @@ function love.load() load_project(startup.path or '', paths) end - --- @type LoveState - love.state = { - testing = false, - has_removable = has_removable, - user_input = nil, - app_state = 'ready' - } + love.state.has_removable = has_removable + love.state.app_state = 'ready' + if love.DEBUG then love.debug = { show_terminal = true, diff --git a/src/types.lua b/src/types.lua index c2b5f785..9084bff5 100644 --- a/src/types.lua +++ b/src/types.lua @@ -133,7 +133,7 @@ --- @class LoveState table --- @field testing boolean ---- @field has_removable boolean +--- @field has_removable boolean? --- @field user_input UserInput? --- @field app_state AppState --- @field prev_state AppState? From 98e48fa152082f916cb08eb82ce1524787e01c25 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 4 Nov 2025 13:26:04 +0100 Subject: [PATCH 072/103] refactor: remove dir creation --- src/main.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main.lua b/src/main.lua index 314c79b3..f928ecb3 100644 --- a/src/main.lua +++ b/src/main.lua @@ -157,8 +157,7 @@ local setup_storage = function(mode) else if OS.get_name() == 'Android' then if mode == 'play' then - local savedir = love.filesystem.getSaveDirectory() - FS.mkdirp(savedir) + --- initializing directory moved to app code else local ok, sd_path = android_storage_find() if not ok then @@ -189,8 +188,10 @@ local setup_storage = function(mode) project_path = project_path, } for _, d in pairs(paths) do - local ok, err = FS.mkdir(d) - if not ok then Log(err) end + if mode ~= 'play' then + local ok, err = FS.mkdirp(d) + if not ok then Log(err) end + end end --- this is virtual, we don't want to actually create it paths.play_path = '/play' From 8ea342cfe968447868c7e20b97a82cad8e3ca5b6 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 4 Nov 2025 13:37:28 +0100 Subject: [PATCH 073/103] fix: test mocks --- tests/mock.lua | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/mock.lua b/tests/mock.lua index 5d53fae8..360369f7 100644 --- a/tests/mock.lua +++ b/tests/mock.lua @@ -20,12 +20,24 @@ local mods = { M = 'lalt', } +local W = 1024 +local H = 600 + --- @param t love local function mock_love(t) local love = { keyboard = { isDown = function(k) return held[k] end - } + }, + graphics = { + mock = true, + getWidth = function() return W end, + getHeight = function() return H end, + getDimensions = function() return W, H end, + newCanvas = function() end, + setCanvas = function() end, + clear = function() end, + }, } for k, v in pairs(t) do love[k] = v From 6bf55afaf0a105c65abe3ac9191029f547fcaafb Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 21:52:58 +0200 Subject: [PATCH 074/103] feat(status): render start vertical coord --- src/view/input/statusline.lua | 24 +++++++++++++----------- src/view/input/userInputView.lua | 3 ++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/view/input/statusline.lua b/src/view/input/statusline.lua index 8384e50e..af412222 100644 --- a/src/view/input/statusline.lua +++ b/src/view/input/statusline.lua @@ -7,37 +7,37 @@ end) --- @param status Status ---- @param nLines integer --- @param start_y integer? -function Statusline:draw(status, nLines, start_y) +function Statusline:draw(status, start_y) local gfx = love.graphics local cf = self.cfg local colors = (function() - if love.state.app_state == 'inspect' then + local state = love.state.app_state + if state == 'inspect' then return cf.colors.statusline.inspect - elseif love.state.app_state == 'running' then + elseif state == 'running' then return cf.colors.statusline.user - elseif love.state.app_state == 'editor' then + elseif state == 'editor' then return cf.colors.statusline.editor else return cf.colors.statusline.console end end)() - local h = start_y or cf.h + local h = start_y or 0 local w = cf.w local fh = cf.fh local font = cf.font - local sy = h - (1 + nLines) * fh - local start_box = { x = 0, y = sy } + local start_box = { x = 0, y = h } local endTextX = start_box.x + w - fh local midX = (start_box.x + w) / 2 local function drawBackground() gfx.setColor(colors.bg) gfx.setFont(font) - local corr = 2 -- correct for fractional slit left under the terminal + --- correct for fractional slit left under the terminal + local corr = 2 gfx.rectangle("fill", start_box.x, start_box.y - corr, w, fh + corr) end @@ -58,6 +58,7 @@ function Statusline:draw(status, nLines, start_y) end local function drawStatus() + local state = love.state.app_state local custom = status.custom local start_text = { x = start_box.x + fh, @@ -74,8 +75,9 @@ function Statusline:draw(status, nLines, start_y) if love.state.testing then gfx.print('testing', midX - (8 * cf.fw), start_text.y) end - gfx.print((love.state.app_state or '???'), - midX - (13 * cf.fw), start_text.y) + local lw = font:getWidth(state) / 2 + gfx.print((state or '???'), + midX - lw, start_text.y) gfx.setColor(colors.fg) end diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 4e8de32e..583cd8cc 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -121,7 +121,8 @@ function UserInputView:draw_input(input) local visible = vc:get_visible() gfx.setFont(self.cfg.font) drawBackground() - self.statusline:draw(status, apparentLines) + local sl_y = start_y - fh + self.statusline:draw(status, sl_y) if highlight then local hl = highlight.hl From 7001fcb5557188de7c51352054bb854e6b11a874 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 21:52:58 +0200 Subject: [PATCH 075/103] refactor(ui): init_view --- src/controller/consoleController.lua | 28 +++++++++++++++----------- src/controller/editorController.lua | 5 +++++ src/controller/userInputController.lua | 6 ++++++ src/main.lua | 1 - src/view/consoleView.lua | 5 ++++- src/view/editor/editorView.lua | 2 +- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 2d6c080c..c83f5302 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -67,8 +67,9 @@ function ConsoleController.new(M, main_ctrl) end --- @param V ConsoleView -function ConsoleController:set_view(V) +function ConsoleController:init_view(V) self.view = V + self.input:init_view(V.input) end --- @param name string @@ -346,24 +347,24 @@ function ConsoleController.prepare_project_env(cc) require("controller.userInputController") require("model.input.userInputModel") require("view.input.userInputView") - local cfg = cc.model.cfg + local cfg = cc.model.cfg ---@type table - local project_env = cc:get_pre_env_c() - project_env.gfx = love.graphics + local project_env = cc:get_pre_env_c() + project_env.gfx = love.graphics - project_env.require = function(name) + project_env.require = function(name) return project_require(cc, name) end --- @param msg string? - project_env.pause = function(msg) + project_env.pause = function(msg) cc:suspend_run(msg) end - project_env.stop = function() + project_env.stop = function() cc:stop_project_run() end - project_env.continue = function() + project_env.continue = function() if love.state.app_state == 'inspect' then -- resume love.state.app_state = 'running' @@ -377,7 +378,7 @@ function ConsoleController.prepare_project_env(cc) close_project(cc) end - local ui_model, input_ref + local ui_model, ui_con, input_ref local create_input_handle = function() input_ref = table.new_reftable() end @@ -393,10 +394,12 @@ function ConsoleController.prepare_project_env(cc) if not input_ref then return end ui_model = UserInputModel(cfg, eval, true, prompt) ui_model:set_text(init) - local inp_con = UserInputController(ui_model, input_ref, true) - local view = UserInputView(cfg.view, inp_con) + ui_con = UserInputController(ui_model, input_ref, true) + local view = UserInputView(cfg.view, ui_con) + ui_con:init_view(view) + love.state.user_input = { - M = ui_model, C = inp_con, V = view + M = ui_model, C = ui_con, V = view } return input_ref end @@ -423,6 +426,7 @@ function ConsoleController.prepare_project_env(cc) return end ui_model:set_text(content) + ui_con:update_view() end --- @param filters table diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index 58d8ba72..c2b72356 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -36,6 +36,11 @@ end --- @field mode EditorMode EditorController = class.create(new) +--- @param v EditorView +function EditorController:init_view(v) + self.view = v + self.input:init_view(self.view.input) +end --- @param name string --- @param content str? diff --git a/src/controller/userInputController.lua b/src/controller/userInputController.lua index e101a5cf..601465cc 100644 --- a/src/controller/userInputController.lua +++ b/src/controller/userInputController.lua @@ -15,10 +15,16 @@ end --- @class UserInputController --- @field model UserInputModel +--- @field view UserInputView? --- @field result table --- @field disable_selection boolean UserInputController = class.create(new) +--- @param v UserInputView +function UserInputController:init_view(v) + self.view = v +end + --------------- -- entered -- --------------- diff --git a/src/main.lua b/src/main.lua index f928ecb3..aa01dde2 100644 --- a/src/main.lua +++ b/src/main.lua @@ -345,7 +345,6 @@ function love.load() redirect_to(CM) local CC = ConsoleController(CM, ctrl) local CV = ConsoleView(baseconf, CC) - CC:set_view(CV) ctrl.init(CC, mode) ctrl.setup_callback_handlers(CC) diff --git a/src/view/consoleView.lua b/src/view/consoleView.lua index 417b09e5..52e0c1d6 100644 --- a/src/view/consoleView.lua +++ b/src/view/consoleView.lua @@ -13,7 +13,7 @@ local gfx = love.graphics --- @param cfg Config --- @param ctrl ConsoleController local function new(cfg, ctrl) - return { + local self = { title = TitleView, canvas = CanvasView(cfg.view), input = UserInputView(cfg.view, ctrl.input), @@ -22,6 +22,9 @@ local function new(cfg, ctrl) cfg = cfg, drawable_height = ViewUtils.get_drawable_height(cfg.view), } + --- hook the view in the controller + ctrl:init_view(self) + return self end --- @class ConsoleView diff --git a/src/view/editor/editorView.lua b/src/view/editor/editorView.lua index 80e92b01..1b996c16 100644 --- a/src/view/editor/editorView.lua +++ b/src/view/editor/editorView.lua @@ -16,7 +16,7 @@ local function new(cfg, ctrl) search = SearchView(cfg, ctrl.search), } --- hook the view in the controller - ctrl.view = ev + ctrl:init_view(ev) return ev end From 204ee6ceae57964f631a72ba118b2455ccc6eec5 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 21:52:58 +0200 Subject: [PATCH 076/103] style(uic): annotate private methods --- src/controller/userInputController.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controller/userInputController.lua b/src/controller/userInputController.lua index 601465cc..81365152 100644 --- a/src/controller/userInputController.lua +++ b/src/controller/userInputController.lua @@ -407,6 +407,7 @@ end -- mouse -- --------------- +--- @private --- @param x integer --- @param y integer --- @return integer c @@ -423,6 +424,7 @@ function UserInputController:_translate_to_input_grid(x, y) return char, line end +--- @private --- @param x integer --- @param y integer --- @param btn integer From 44bd675ac49c10d26dab915681b5c74ff88ec28c Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 21:52:58 +0200 Subject: [PATCH 077/103] refactor(uiv): split colors getter --- src/view/input/userInputView.lua | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 583cd8cc..5545fb2f 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -22,6 +22,16 @@ end --- @field oneshot boolean UserInputView = class.create(new) +local get_colors = function(cf_colors) + if love.state.app_state == 'inspect' then + return cf_colors.input.inspect + elseif love.state.app_state == 'running' then + return cf_colors.input.user + else + return cf_colors.input.console + end +end + --- @param input InputDTO function UserInputView:draw_input(input) local gfx = love.graphics @@ -29,15 +39,7 @@ function UserInputView:draw_input(input) local cfg = self.cfg local status = self.controller:get_status() local cf_colors = cfg.colors - local colors = (function() - if love.state.app_state == 'inspect' then - return cf_colors.input.inspect - elseif love.state.app_state == 'running' then - return cf_colors.input.user - else - return cf_colors.input.console - end - end)() + local colors = get_colors(cf_colors) local fh = cfg.fh local fw = cfg.fw From 17abe50577d50e7a11736b9d5329a19fd64788d9 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 21:52:58 +0200 Subject: [PATCH 078/103] refactor(uiv): split overflow calc --- src/view/input/userInputView.lua | 47 ++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 5545fb2f..6b9c3840 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -32,6 +32,33 @@ local get_colors = function(cf_colors) end end +--- The Overflow +--- When the cursor is on the last character of the line +--- we display it at the start of the next line as that's +--- where newly added text will appear +--- However, when the line also happens to be wrap-length, +--- there either is no next line yet, or it would look the +--- same as if it was at the start of the next. +--- Hence, the overflow phantom line. +local calc_overflow = function(w, text, cursor) + local cl, cc = cursor.l, cursor.c + local acc = cc - 1 + --- overflow binary and actual height (in lines) + local overflow = 0 + local of_h = 0 + local curline = text[cl] + local clen = string.ulen(curline) + local q, rem = math.modf(acc / w) + local ofpos = rem == 0 and acc == clen and clen > 0 + if ofpos + and string.is_non_empty_string_array(text) + then + overflow = 1 + of_h = q + end + return overflow, of_h, ofpos +end + --- @param input InputDTO function UserInputView:draw_input(input) local gfx = love.graphics @@ -64,24 +91,8 @@ function UserInputView:draw_input(input) vc:get_content_length(), cfg.input_max) - --- The Overflow - --- When the cursor is on the last character of the line - --- we display it at the start of the next line as that's - --- where newly added text will appear - --- However, when the line also happens to be wrap-length, - --- there either is no next line yet, or it would look the - --- same as if it was at the start of the next. - --- Hence, the overflow phantom line. - local curline = text[cl] - local clen = string.ulen(curline) - local q, rem = math.modf(acc / w) - local ofpos = rem == 0 and acc == clen and clen > 0 - if ofpos - and string.is_non_empty_string_array(text) - then - overflow = 1 - of_h = q - end + local overflow, of_h, ofpos = calc_overflow( + w, text, cursorInfo.cursor) local apparentLines = inLines + overflow local inHeight = inLines * fh From 47bb5e1a9fcd44fa1e63fa06dde1a3635675272d Mon Sep 17 00:00:00 2001 From: aldum Date: Thu, 13 Nov 2025 13:23:25 +0100 Subject: [PATCH 079/103] feat: add compy namespace --- src/controller/consoleController.lua | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index c83f5302..b6dbe575 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -326,6 +326,28 @@ function ConsoleController.prepare_env(cc) cc:run_project(name) end + local terminal = cc.model.output.terminal + local compy_namespace = { + terminal = { + --- @param x number + --- @param y number + gotoxy = function(x, y) + return terminal:move_to(x, y) + end, + show_cursor = function() + return terminal:show_cursor() + end, + hide_cursor = function() + return terminal:hide_cursor() + end, + clear = function() + return terminal:clear() + end + } + } + prepared.compy = compy_namespace + prepared.tty = compy_namespace.terminal + prepared.run = prepared.run_project prepared.eval = LANG.eval From 50e177781356dce9312f00169d5ece73685f3b95 Mon Sep 17 00:00:00 2001 From: aldum Date: Thu, 13 Nov 2025 14:54:11 +0100 Subject: [PATCH 080/103] fix(uic): remove not yet existing call --- src/controller/consoleController.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index b6dbe575..6cc22e7d 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -448,7 +448,6 @@ function ConsoleController.prepare_project_env(cc) return end ui_model:set_text(content) - ui_con:update_view() end --- @param filters table From 19cdaf8e964f451739c1e3d724ab44f3ae7b835b Mon Sep 17 00:00:00 2001 From: aldum Date: Thu, 13 Nov 2025 14:57:15 +0100 Subject: [PATCH 081/103] feat(example): add pong --- src/examples/pong/README.md | 316 ++++++++++++++++++++++ src/examples/pong/constants.lua | 26 ++ src/examples/pong/main.lua | 446 ++++++++++++++++++++++++++++++++ src/examples/pong/strategy.lua | 41 +++ 4 files changed, 829 insertions(+) create mode 100644 src/examples/pong/README.md create mode 100644 src/examples/pong/constants.lua create mode 100644 src/examples/pong/main.lua create mode 100644 src/examples/pong/strategy.lua diff --git a/src/examples/pong/README.md b/src/examples/pong/README.md new file mode 100644 index 00000000..a08016e8 --- /dev/null +++ b/src/examples/pong/README.md @@ -0,0 +1,316 @@ +# Pong2 with square ball + +This example shows how to build a small +real-time game step by step. +It demonstrates how to structure +a program, store all state in one table, +update the world in small bounded time steps, +and draw the result every frame. + +The goal of this example is also to show +how discrete simulation works in practice — +how the game world advances in small, +fixed slices of time instead of depending +on the speed of the device. + +The approach used here also helps overcome +the limited performance +of Compy hardware by keeping updates +deterministic and efficient. + +--- + +### 1. Files and purpose + +The project has three files: +- **constants.lua** — numbers that never change: +sizes, colors, speeds. +- **strategy.lua** — code that decides +how the right paddle moves. +It can follow the ball (AI) or be controlled +by a second player. +- **main.lua** — the main program. +It sets up the screen, initializes state, +runs the update loop and draws the picture. + +Separating logic, constants, and behavior +keeps the program readable and efficient. + +--- + +### 2. Constants + +```lua +PADDLE_WIDTH = 10 +PADDLE_HEIGHT = 60 +BALL_SIZE = 10 +AI_DEADZONE = 4 +COLOR_FG = {1, 1, 1} +COLOR_BG = {0, 0, 0} +``` +These numbers define proportions, not absolute pixels. +The actual paddle and ball sizes are computed +in main.lua from the current screen height, +so the game scales to different displays. +They are still read-only during the run. + +--- + +### 3. Game state + +All moving objects and scores live in one table S: +```lua +S = { + player = { x, y, w, h, dy }, + opp = { x, y, w, h, dy }, + ball = { x, y, dx, dy, size }, + playerScore = 0, + oppScore = 0, + state = "start" +} +``` +The program updates these values each step +and then draws them. Keeping everything +together makes it easy to inspect and debug. + +--- + +### 4. Setting up the screen + +At startup, cache_dims() measures +the screen once and stores: +```lua +screen_w = G.getWidth() +screen_h = G.getHeight() +local base_h = 480 +local scale = screen_h / base_h +paddle_w = math.floor(PADDLE_WIDTH * scale + 0.5) +paddle_h = math.floor(PADDLE_HEIGHT * scale + 0.5) +ball_size = math.floor(BALL_SIZE * scale + 0.5) +paddle_max_y = screen_h - paddle_h +ball_max_y = screen_h - ball_size +center_x = math.floor(screen_w / 2 + 0.5) +``` +This way, the same proportions are kept, +but real sizes follow the +actual screen size. + +--- + +### 5. Drawing the scene + +All drawing happens inside love.draw(): + 1. Clear the screen. + 2. Draw the cached divider canvas. + 3. Draw paddles, ball, and scores. + 4. Draw text messages such as “Press Space”. + +Example: +```lua +function love.draw() + cache_dims() + G.clear(COLOR_BG) + G.setColor(COLOR_FG) + G.draw(CENTER_CANVAS) + draw_paddle(S.player) + draw_paddle(S.opp) + draw_ball(S.ball) + draw_scores() + draw_state_text() +end +``` +The center divider is drawn once on a canvas. +Its segment height uses the same `ball_size` +that was computed from the screen, +so the whole scene stays proportional. +Drawing only from cached data keeps rendering +predictable even on slow devices. + +--- + +### 6. Input and control + +The left paddle can be moved with the mouse +or keys Q and A. +The right paddle uses a strategy selected +at startup: +```lua +strategy.set_opp_strategy("ai") +``` +for a computer opponent, or +```lua +strategy.set_opp_strategy("manual") +``` +for a second human using arrow keys. + +Press Space to start or restart; +Escape to quit.Input handling +is done via a small key_actions +table instead of long if-chains, +making it easy to extend for +new game states. + +--- + +### 7. The update loop + +`love.update(dt)` is called many times per second. +The program measures real time since the previous +frame and advances the simulation in small bounded +time steps. Each integration step uses the same duration +(`FIXED_DT`), but the total number of steps per frame +is limited by `MAX_STEPS`. This prevents the game +from getting stuck if one frame takes too long. + +```lua +acc = acc + rdt +while acc >= FIXED_DT and steps < MAX_STEPS do + step_game(FIXED_DT) + acc = acc - FIXED_DT + steps = steps + 1 +end +``` +Originally this bounded-step loop +was added to compensate for +an expensive per-frame screenshot +on Compy. After removing that feature, +the loop is no longer strictly required +for performance. It remains good practice, +because random long frames can still +happen when the garbage collector (GC) runs. + +`FIXED_DT` is the duration of +one physics step (1/60 s). +`MAX_STEPS` limits the number +of updates per frame, ensuring +consistent gameplay across devices. + +### 8. One step and ball logic + +Each call to step_game(dt) advances +the world by one quantum of time: +```lua +update_player(dt) +strategy.update(S, dt) +step_ball(S.ball, dt) +handle_score() +``` +These operations are simple arithmetic updates, +chosen to be fast enough for Compy’s limitations. +The combination of short steps and minimal math +gives smooth motion without heavy load. + +When checking for goals, the code compares +`b.x + b.size` with `screen_w`, +not with a constant, because the ball size +was scaled earlier in `cache_dims()` +and copied into `S.ball.size` during layout. + +--- + +### 9. Handling score + +After each step, the game checks if a goal +was scored or the ball left the screen: +```lua +function handle_score() + if check_scored(S.ball.x) then + reset_ball() + return true + end + if S.ball.x < 0 or + screen_w < S.ball.x + S.ball.size + then + reset_ball() + return true + end + return false +end +``` +This helper centralizes score handling. +It replaces the old check_score() calls +and ensures the ball resets correctly. + +--- + +### 10. Opponent strategies + +`strategy.lua` defines how the right paddle +moves. + +AI strategy +```lua +local d = (S.ball.y + S.ball.size/2) - + (S.opp.y + S.opp.h/2) +if math.abs(d) > AI_DEADZONE then + move_paddle(S.opp, (d > 0) and 1 or -1, dt) +end +``` +The paddle follows the ball but pauses inside +a small “dead zone” so it does not react instantly. + +Manual strategy: +```lua +if love.keyboard.isDown("up") then dir = -1 +elseif love.keyboard.isDown("down") then dir = 1 end +move_paddle(S.opp, dir, dt) +``` +Any new behavior can be added as: +```lua +strategy.set_opp_strategy("custom", function(S, dt) + -- your logic here +end) +``` +Because the module is separate, +the game code stays clean. + +--- + +### 11. Discrete simulation and Compy performance + +The discrete, bounded-step simulation +is not only a teaching tool. +It is also a performance solution. + +On Compy, frame rate and CPU speed +can vary between devices. +If physics were tied directly to dt, +motion would become slower +or faster depending on load. + +By processing time in small, +bounded slices the game stays predictable +even when rendering slows down +on Compy devices. + +Originally, bounded updates were introduced +to offset the performance loss from taking +a full-frame screenshot each cycle. +After that feature was removed, +this logic became optional for speed, +yet still valuable to absorb random long frames +caused by the garbage collector (GC). + +In short, discrete time keeps the game fair +and efficient even on limited hardware. + +--- + +### 12. Common issues + + * Tunneling: a fast ball may skip + a paddle if the time step is too large. + Reduce ball speed or lower `FIXED_DT`. + * Frame drop: if too many updates pile up, + the loop stops at `MAX_STEPS` and + the game slows slightly instead of freezing. + * Mixed timing: always use fixed `dt` + for physics and real `dt` only for animation + or timers. + * High SPEED_SCALE: makes movement faster + but less accurate. + * Bounded steps also protect the game + from rare GC pauses. + * Bounded steps also protect the game + from rare GC pauses that can freeze + animation for a moment. diff --git a/src/examples/pong/constants.lua b/src/examples/pong/constants.lua new file mode 100644 index 00000000..1d240a57 --- /dev/null +++ b/src/examples/pong/constants.lua @@ -0,0 +1,26 @@ +-- constants.lua +-- static game parameters + +COLOR_BG = { + 0, + 0, + 0 +} +COLOR_FG = { + 1, + 1, + 1 +} + +PADDLE_WIDTH = 10 +PADDLE_HEIGHT = 60 +PADDLE_SPEED = 180 +PADDLE_OFFSET_X = 0 + +BALL_SIZE = 10 +BALL_SPEED_X = 240 +BALL_SPEED_Y = 120 + +WIN_SCORE = 10 +SCORE_OFFSET_Y = 20 +AI_DEADZONE = 4 diff --git a/src/examples/pong/main.lua b/src/examples/pong/main.lua new file mode 100644 index 00000000..c0b59c35 --- /dev/null +++ b/src/examples/pong/main.lua @@ -0,0 +1,446 @@ +-- main.lua + +require "constants" +require "strategy" + +gfx = love.graphics + +-- virtual game space +VIRTUAL_W = 640 +VIRTUAL_H = 480 + +-- runtime configuration +USE_FIXED = true +FIXED_DT = 1 / 60 +MAX_STEPS = 5 +SPEED_SCALE = 1.5 +MOUSE_SENSITIVITY = 1 + +-- runtime variables +view_tf = nil +screen_w, screen_h = 0, 0 +paddle_max_y = 0 +ball_max_y = 0 + +inited = false +mouse_enabled = false +time_t, acc = 0, 0 + +-- game state + +S = { } + +S.player = { + x = PADDLE_OFFSET_X, + y = 0, + dy = 0 +} +S.opp = { + x = 0, + y = 0, + dy = 0 +} +S.ball = { + x = 0, + y = 0, + dx = BALL_SPEED_X, + dy = BALL_SPEED_Y +} +S.score = { + player = 0, + opp = 0 +} +S.state = "start" + +S.strategy = { + fn = nil, + text = nil +} + +-- ui resources +font = nil +texts = { } +center_canvas = nil + +-- screen helpers + +function update_view_transform(w, h) + screen_w = w + screen_h = h + local sx = w / VIRTUAL_W + local sy = h / VIRTUAL_H + view_tf = love.math.newTransform() + view_tf:scale(sx, sy) +end + +function cache_dims() + local w = gfx.getWidth() + local h = gfx.getHeight() + update_view_transform(w, h) +end + +function layout() + S.player.x = PADDLE_OFFSET_X + S.player.y = (VIRTUAL_H - PADDLE_HEIGHT) / 2 + S.opp.x = (VIRTUAL_W - PADDLE_OFFSET_X) - PADDLE_WIDTH + S.opp.y = (VIRTUAL_H - PADDLE_HEIGHT) / 2 + S.ball.x = (VIRTUAL_W - BALL_SIZE) / 2 + S.ball.y = (VIRTUAL_H - BALL_SIZE) / 2 + paddle_max_y = VIRTUAL_H - PADDLE_HEIGHT + ball_max_y = VIRTUAL_H - BALL_SIZE +end + +-- text helpers + +function set_text(name, str) + local old = texts[name] + if old then + old:release() + end + texts[name] = gfx.newText(font, str) +end + +function rebuild_score_texts() + set_text("score_l", tostring(S.score.player)) + set_text("score_r", tostring(S.score.opp)) +end + +function rebuild_opp_texts() + set_text("easy", "1 Player (easy)") + set_text("hard", "1 Player (hard)") + set_text("manual", "2 Players (keyboard)") +end + +-- canvas + +function draw_center_line() + local x = VIRTUAL_W / 2 - 2 + local step = BALL_SIZE * 2 + local y = 0 + while y < VIRTUAL_H do + gfx.rectangle("fill", x, y, 4, BALL_SIZE) + y = y + step + end +end + +function build_center_canvas() + if center_canvas then + center_canvas:release() + end + center_canvas = gfx.newCanvas(VIRTUAL_W, VIRTUAL_H) + gfx.setCanvas(center_canvas) + gfx.clear(0, 0, 0, 0) + gfx.setColor(COLOR_FG) + draw_center_line() + gfx.setCanvas() +end + +-- initialization + +function build_static_texts() + font = gfx.getFont() + set_text("start", "Press Space to Start") + set_text("gameover", "Game Over") + rebuild_opp_texts() + rebuild_score_texts() +end + +function set_strategy(name) + S.strategy.fn = strategy[name] + S.strategy.text = texts[name] +end + +function do_init() + cache_dims() + layout() + build_center_canvas() + build_static_texts() + mouse_enabled = true + time_t = love.timer.getTime() + inited = true + set_strategy("hard") +end + +function ensure_init() + if not inited then + do_init() + end +end + +-- paddle and ball movement + +function clamp_paddle(p) + if p.y < 0 then + p.y = 0 + end + if VIRTUAL_H < p.y + PADDLE_HEIGHT then + p.y = VIRTUAL_H - PADDLE_HEIGHT + end +end + +function move_paddle(p, dir, dt) + p.dy = PADDLE_SPEED * dir + p.y = p.y + p.dy * dt + clamp_paddle(p) +end + +function check_scored(bx) + if bx < 0 then + return "opp" + end + if VIRTUAL_W < bx + BALL_SIZE then + return "player" + end + return nil +end + +function move_ball(b, dt) + b.x = b.x + b.dx * dt + b.y = b.y + b.dy * dt + if b.y < 0 then + b.y = 0 + b.dy = -b.dy + end + if VIRTUAL_H < b.y + BALL_SIZE then + b.y = VIRTUAL_H - BALL_SIZE + b.dy = -b.dy + end + return check_scored(b.x) +end + +function bounce_ball(b) + if b.y < 0 then + b.y = 0 + b.dy = -b.dy + end + if ball_max_y < b.y then + b.y = ball_max_y + b.dy = -b.dy + end +end + +-- collision and score + +function hit_offset(b, p) + local pc = p.y + PADDLE_HEIGHT / 2 + local bc = b.y + BALL_SIZE / 2 + return (bc - pc) / (PADDLE_HEIGHT / 2) +end + +function collide(b, p, off) + local hx1 = b.x < p.x + PADDLE_WIDTH + local hx2 = p.x < b.x + BALL_SIZE + local hy1 = b.y < p.y + PADDLE_HEIGHT + local hy2 = p.y < b.y + BALL_SIZE + if hx1 and hx2 and hy1 and hy2 then + b.x = p.x + off + b.dx = -b.dx + b.dy = b.dy + hit_offset(b, p) * (BALL_SPEED_Y * 0.75) + end +end + +function scored(side) + local s = S.score + s[side] = s[side] + 1 + rebuild_score_texts() + if WIN_SCORE <= s[side] then + S.state = "gameover" + love.mouse.setRelativeMode(false) + return true + end + return false +end + +function reset_ball() + local b = S.ball + b.x = (VIRTUAL_W - BALL_SIZE) / 2 + b.y = (VIRTUAL_H - BALL_SIZE) / 2 + local total = S.score.player + S.score.opp + local dir = (total % 2 == 0) and 1 or -1 + b.dx = dir * BALL_SPEED_X + b.dy = ((total % 3 - 1) * BALL_SPEED_Y) * 0.3 +end + +-- control and update + +key_actions = { + start = { }, + play = { }, + gameover = { } +} + +function key_actions.start.space() + S.state = "play" + love.mouse.setRelativeMode(true) + reset_ball() +end + +function key_actions.start.e() + set_strategy("easy") +end + +function key_actions.start.h() + set_strategy("hard") +end + +key_actions.start["1"] = function() + if S.strategy.fn ~= strategy.easy then + set_strategy("hard") + end +end + +key_actions.start["2"] = function() + set_strategy("manual") +end + +function key_actions.play.space() + +end + +function key_actions.play.r() + S.score.player = 0 + S.score.opp = 0 + rebuild_score_texts() + layout() + S.state = "start" + love.mouse.setRelativeMode(false) +end + +key_actions.gameover.space = key_actions.play.r + +for name in pairs(key_actions) do + key_actions[name].escape = love.event.quit +end + +function love.keypressed(k) + local group = key_actions[S.state] + if group and group[k] then + group[k]() + end +end + +keydown = { + q = -1, + a = 1 +} + +function update_player(dt) + local dir = 0 + for k, v in pairs(keydown) do + if love.keyboard.isDown(k) then + dir = v + end + end + move_paddle(S.player, dir, dt) +end + +function love.mousemoved(x, y, dx, dy, t) + if not mouse_enabled or t + or S.state ~= "play" + then + return + end + local p = S.player + p.y = p.y + dy * MOUSE_SENSITIVITY + clamp_paddle(p) +end + +-- main step/update + +function step_ball(b, dt) + move_ball(b, dt) + bounce_ball(b) + collide(b, S.player, PADDLE_WIDTH) + collide(b, S.opp, -BALL_SIZE) +end + +function handle_score() + local side = check_scored(S.ball.x) + if side then + scored(side) + reset_ball() + return true + end + return false +end + +function step_game(dt) + if S.state ~= "play" then + return + end + local sdt = dt * SPEED_SCALE + update_player(sdt) + S.strategy.fn(S, sdt) + step_ball(S.ball, sdt) + handle_score() +end + +function update_fixed(rdt) + acc = acc + rdt + local steps = 0 + while FIXED_DT <= acc and steps < MAX_STEPS do + step_game(FIXED_DT) + acc = acc - FIXED_DT + steps = steps + 1 + end +end + +function love.update(dt) + ensure_init() + local now = love.timer.getTime() + local rdt = now - time_t + time_t = now + if USE_FIXED then + update_fixed(rdt) + else + step_game(rdt) + end +end + +-- drawing + +function draw_bg() + gfx.clear(COLOR_BG) + gfx.setColor(COLOR_FG) +end + +function draw_paddle(p) + gfx.rectangle("fill", p.x, p.y, PADDLE_WIDTH, PADDLE_HEIGHT) +end + +function draw_ball(b) + gfx.rectangle("fill", b.x, b.y, BALL_SIZE, BALL_SIZE) +end + +function draw_scores() + gfx.draw(texts.score_l, VIRTUAL_W / 2 - 60, SCORE_OFFSET_Y) + gfx.draw(texts.score_r, VIRTUAL_W / 2 + 40, SCORE_OFFSET_Y) +end + +function draw_state_text(s) + local t = texts[s] + if t then + gfx.draw(t, VIRTUAL_W / 2 - 40, VIRTUAL_H / 2 - 16) + end + if s == "start" and S.strategy.text then + gfx.draw(S.strategy.text, VIRTUAL_W / 2 - 40, VIRTUAL_H / 2) + end +end + +function love.draw() + draw_bg() + gfx.push() + gfx.applyTransform(view_tf) + gfx.draw(center_canvas) + draw_paddle(S.player) + draw_paddle(S.opp) + draw_ball(S.ball) + draw_scores() + draw_state_text(S.state) + gfx.pop() +end + +function love.resize(w, h) + update_view_transform(w, h) + build_center_canvas() +end diff --git a/src/examples/pong/strategy.lua b/src/examples/pong/strategy.lua new file mode 100644 index 00000000..af99b85d --- /dev/null +++ b/src/examples/pong/strategy.lua @@ -0,0 +1,41 @@ +-- strategy.lua +-- opponent behavior module + +strategy = { } + +-- fast AI (hard) +function strategy.hard(S, dt) + local c = S.opp.y + PADDLE_HEIGHT / 2 + local by = S.ball.y + BALL_SIZE / 2 + local d = by - c + if math.abs(d) < AI_DEADZONE then + S.opp.dy = 0 + else + local dir = (0 < d) and 1 or -1 + move_paddle(S.opp, dir, dt) + end +end + +-- slow AI (easy) +function strategy.easy(S, dt) + local c = S.opp.y + PADDLE_HEIGHT / 2 + local by = S.ball.y + BALL_SIZE / 2 + local d = by - c + if math.abs(d) < AI_DEADZONE then + S.opp.dy = 0 + else + local dir = (0 < d) and 1 or -1 + move_paddle(S.opp, dir, dt * 0.6) + end +end + +-- Manual (second player) +function strategy.manual(S, dt) + local dir = 0 + if love.keyboard.isDown("up") then + dir = -1 + elseif love.keyboard.isDown("down") then + dir = 1 + end + move_paddle(S.opp, dir, dt) +end From e5218958ba80246c4ec696089239cab2ddc7952e Mon Sep 17 00:00:00 2001 From: aldum Date: Thu, 13 Nov 2025 14:57:15 +0100 Subject: [PATCH 082/103] refactor: rename CC param --- src/controller/controller.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index 00463204..d2ac566c 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -478,9 +478,9 @@ Controller = { ---------------- --- public --- ---------------- - --- @param C ConsoleController - init = function(C, mode) - _C = C + --- @param CC ConsoleController + init = function(CC, mode) + _C = CC _mode = mode end, --- @param C ConsoleController From 491d491c34a9a050024c64fd7f5a229010c7d24b Mon Sep 17 00:00:00 2001 From: aldum Date: Thu, 13 Nov 2025 14:57:15 +0100 Subject: [PATCH 083/103] feat: proper love.draw snapshotting --- src/controller/consoleController.lua | 18 ++++++++++++++---- src/controller/controller.lua | 19 +++++++++---------- src/types.lua | 1 + src/view/view.lua | 20 ++++---------------- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 6cc22e7d..36a795c9 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -575,16 +575,17 @@ function ConsoleController:_set_base_env(t) table.protect(t) end ---- @param msg string? -function ConsoleController:suspend_run(msg) - local runner_env = self:get_project_env() - if love.state.app_state ~= 'running' then +function ConsoleController:suspend() + if love.state.app_state ~= 'snapshot' then return end + local runner_env = self:get_project_env() Log.info('Suspending project run') love.state.app_state = 'inspect' + local msg = love.state.suspend_msg if msg then self.input:set_error({ tostring(msg) }) + love.state.suspend_msg = nil end self.model.output:invalidate_terminal() @@ -593,6 +594,15 @@ function ConsoleController:suspend_run(msg) self.main_ctrl.set_default_handlers(self, self.view) end +--- @param msg string? +function ConsoleController:suspend_run(msg) + if love.state.app_state ~= 'running' then + return + end + love.state.app_state = 'snapshot' + love.state.suspend_msg = msg +end + --- @param name string --- @param play boolean --- @return boolean success diff --git a/src/controller/controller.lua b/src/controller/controller.lua index d2ac566c..8885be92 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -400,11 +400,16 @@ Controller = { local uup = Controller._userhandlers.update if user_update and uup then + if love.state.app_state == 'snapshot' then + gfx.captureScreenshot(function(img) + local snap = gfx.newImage(img) + View.snapshot = snap + C:suspend() + end) + end wrap(uup, dt) end - if _mode ~= 'play' then - Controller.snapshot() - end + if love.harmony then love.harmony.timer_update(dt) end @@ -468,13 +473,6 @@ Controller = { love.quit = quit end, - --- @private - snapshot = function() - if user_draw then - View.snap_canvas() - end - end, - ---------------- --- public --- ---------------- @@ -793,6 +791,7 @@ Controller = { for _, a in pairs(_supported) do save_if_differs(a) end + save_if_differs('draw') end, diff --git a/src/types.lua b/src/types.lua index 9084bff5..968b541b 100644 --- a/src/types.lua +++ b/src/types.lua @@ -138,6 +138,7 @@ --- @field app_state AppState --- @field prev_state AppState? --- @field editor EditorState? +--- @field suspend_msg string? --- @class LoveDebug table --- @field show_snapshot boolean diff --git a/src/view/view.lua b/src/view/view.lua index 81dc6794..5d62a772 100644 --- a/src/view/view.lua +++ b/src/view/view.lua @@ -1,11 +1,10 @@ local gfx = love.graphics ---- @type love.Image? -local canvas_snapshot = nil - local FPSfont = gfx.newFont("assets/fonts/fraps.otf", 24) View = { + --- @type love.Image? + snapshot = nil, prev_draw = nil, main_draw = nil, end_draw = nil, @@ -16,23 +15,12 @@ View = { local terminal = C:get_terminal() local canvas = C:get_canvas() local input = C.input:get_input() - CV:draw(terminal, canvas, input, canvas_snapshot) + CV:draw(terminal, canvas, input, View.snapshot) gfx.pop() end, - snap_canvas = function() - -- gfx.captureScreenshot(os.time() .. ".png") - if canvas_snapshot then - View.clear_snapshot() - collectgarbage() - end - gfx.captureScreenshot(function(img) - canvas_snapshot = gfx.newImage(img) - end) - end, - clear_snapshot = function() - canvas_snapshot = nil + View.snapshot = nil end, drawFPS = function() From 4ee9cf05933b493f8fb776520a875de0bbf68aee Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 24 Oct 2025 21:52:58 +0200 Subject: [PATCH 084/103] refactor(ui): render UserInputView to separate canvas --- src/controller/consoleController.lua | 1 + src/controller/controller.lua | 2 +- src/controller/editorController.lua | 3 + src/controller/userInputController.lua | 13 +++- src/view/consoleView.lua | 5 +- src/view/editor/editorView.lua | 11 ++- src/view/input/userInputView.lua | 98 ++++++++++++++++---------- src/view/view.lua | 6 +- 8 files changed, 93 insertions(+), 46 deletions(-) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 36a795c9..7648b802 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -448,6 +448,7 @@ function ConsoleController.prepare_project_env(cc) return end ui_model:set_text(content) + ui_con:update_view() end --- @param filters table diff --git a/src/controller/controller.lua b/src/controller/controller.lua index 8885be92..ce013217 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -389,7 +389,7 @@ Controller = { end local user_input = get_user_input() if user_input then - user_input.V:draw(user_input.C:get_input()) + user_input.V:draw() end end View.prev_draw = draw diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index c2b72356..8b07beb7 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -81,6 +81,7 @@ function EditorController:open(name, content, save) self.view:open(b) self:update_status() self:set_state() + self.input:update_view() end --- @private @@ -284,6 +285,7 @@ end --- @param t string function EditorController:textinput(t) + self.view:update_input() if self.mode == 'edit' then local input = self.model.input if input:has_error() then @@ -689,6 +691,7 @@ end --- @param k string function EditorController:keypressed(k) + self.input:update_view() local mode = self.mode if Key.ctrl() then diff --git a/src/controller/userInputController.lua b/src/controller/userInputController.lua index 81365152..553365db 100644 --- a/src/controller/userInputController.lua +++ b/src/controller/userInputController.lua @@ -25,6 +25,12 @@ function UserInputController:init_view(v) self.view = v end +function UserInputController:update_view() + local input = self.model:get_input() + local status = self:get_status() + self.view:render(input, status) +end + --------------- -- entered -- --------------- @@ -42,6 +48,7 @@ end --- @param t str function UserInputController:set_text(t) self.model:set_text(t) + self:update_view() end --- @return boolean @@ -132,6 +139,7 @@ end --- @return boolean --- @return Error[] function UserInputController:evaluate() + self:update_view() return self.model:handle(true) end @@ -170,6 +178,7 @@ end --- @param k string --- @return boolean? limit function UserInputController:keypressed(k) + self:update_view() if _G.web and k == 'space' then self:textinput(' ') end @@ -368,7 +377,7 @@ function UserInputController:keypressed(k) submit() end - + self:update_view() return ret end @@ -381,6 +390,7 @@ function UserInputController:textinput(t) return end self.model:add_text(t) + self:update_view() end --- @param k string @@ -401,6 +411,7 @@ function UserInputController:keyreleased(k) end selection() + self:update_view() end --------------- diff --git a/src/view/consoleView.lua b/src/view/consoleView.lua index 52e0c1d6..9c0d4f4e 100644 --- a/src/view/consoleView.lua +++ b/src/view/consoleView.lua @@ -39,9 +39,8 @@ ConsoleView = class.create(new) --- @param terminal table --- @param canvas love.Canvas ---- @param input InputDTO --- @param snapshot love.Image? -function ConsoleView:draw(terminal, canvas, input, snapshot) +function ConsoleView:draw(terminal, canvas, snapshot) if love.DEBUG then self:draw_placeholder() end @@ -53,7 +52,7 @@ function ConsoleView:draw(terminal, canvas, input, snapshot) self.drawable_height, snapshot) if ViewUtils.conditional_draw('show_input') then - self.input:draw(input) + self.input:draw() end end diff --git a/src/view/editor/editorView.lua b/src/view/editor/editorView.lua index 1b996c16..9bdf0f50 100644 --- a/src/view/editor/editorView.lua +++ b/src/view/editor/editorView.lua @@ -40,8 +40,7 @@ function EditorView:draw() bv:draw(spec) end if ViewUtils.conditional_draw('show_input') then - local input = ctrl:get_input() - self.input:draw(input) + self.input:draw() end end end @@ -77,4 +76,12 @@ end --- @param moved integer? function EditorView:refresh(moved) self:get_current_buffer():refresh(moved) + self:update_input() +end + +function EditorView:update_input() + local ctrl = self.controller + local input = ctrl:get_input() + local status = ctrl.input:get_status() + self.input:render(input, status) end diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 6b9c3840..1250a88b 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -8,11 +8,17 @@ require("util.view") --- @param cfg ViewConfig --- @param ctrl UserInputController local new = function(cfg, ctrl) + local gfx = love.graphics + local w = gfx.getWidth() + --- max lines + statusline + local h = cfg.input_max * cfg.fh + cfg.fh return { cfg = cfg, controller = ctrl, statusline = Statusline(cfg), oneshot = ctrl.model.oneshot, + start_h = h, + canvas = gfx.newCanvas(w, h), } end @@ -20,6 +26,7 @@ end --- @field controller UserInputController --- @field statusline table --- @field oneshot boolean +--- @field canvas love.Canvas UserInputView = class.create(new) local get_colors = function(cf_colors) @@ -60,17 +67,16 @@ local calc_overflow = function(w, text, cursor) end --- @param input InputDTO -function UserInputView:draw_input(input) +--- @param status Status +function UserInputView:render_input(input, status) local gfx = love.graphics - local cfg = self.cfg - local status = self.controller:get_status() local cf_colors = cfg.colors local colors = get_colors(cf_colors) local fh = cfg.fh local fw = cfg.fw - local h = cfg.h + local h = 0 local drawableWidth = cfg.drawableWidth local w = cfg.drawableChars -- drawtest hack @@ -81,9 +87,6 @@ function UserInputView:draw_input(input) local cursorInfo = self.controller:get_cursor_info() local cl, cc = cursorInfo.cursor.l, cursorInfo.cursor.c local acc = cc - 1 - --- overflow binary and actual height (in lines) - local overflow = 0 - local of_h = 0 local text = input.text local vc = input.visible @@ -97,12 +100,14 @@ function UserInputView:draw_input(input) local apparentLines = inLines + overflow local inHeight = inLines * fh local apparentHeight = inHeight - local y = h - (#text * fh) + local y = fh local wrap_forward = vc.wrap_forward local wrap_reverse = vc.wrap_reverse local start_y = h - apparentLines * fh + local vpH = gfx.getHeight() + self.start_h = vpH - (inLines + 1) * fh local function drawCursor() local y_offset = math.floor(acc / w) @@ -125,7 +130,7 @@ function UserInputView:draw_input(input) gfx.setColor(colors.bg) gfx.rectangle("fill", 0, - start_y, + 0, drawableWidth, apparentHeight * fh) end @@ -134,8 +139,8 @@ function UserInputView:draw_input(input) local visible = vc:get_visible() gfx.setFont(self.cfg.font) drawBackground() - local sl_y = start_y - fh - self.statusline:draw(status, sl_y) + + self.statusline:draw(status, 0) if highlight then local hl = highlight.hl @@ -275,39 +280,58 @@ function UserInputView:draw_input(input) drawCursor() end +--- @param err_text string[] +function UserInputView:render_error(err_text) + local colors = self.cfg.colors + local fh = self.cfg.fh + local h = self.start_h + local drawableWidth = self.cfg.drawableWidth + local inLines = #err_text + local inHeight = inLines * fh + local apparentHeight = #err_text + local start_y = h - inHeight + + local drawBackground = function() + gfx.setColor(colors.input.error_bg) + gfx.rectangle("fill", + 0, + start_y, + drawableWidth, + apparentHeight * fh) + end + + drawBackground() + + gfx.setColor(colors.input.error) + + for l, str in ipairs(err_text) do + local breaks = 0 -- starting height is already calculated + ViewUtils.write_line(l, str, start_y, breaks, self.cfg) + end +end + --- @param input InputDTO -function UserInputView:draw(input) +--- @param status Status +function UserInputView:render(input, status) + local gfx = love.graphics + --- @diagnostic disable-next-line: undefined-field + if gfx.mock then return end local err_text = input.wrapped_error or {} local isError = string.is_non_empty_string_array(err_text) - local colors = self.cfg.colors - local fh = self.cfg.fh - local h = self.cfg.h - + gfx.setCanvas(self.canvas) if isError then - local drawableWidth = self.cfg.drawableWidth - local inLines = #err_text - local inHeight = inLines * fh - local apparentHeight = #err_text - local start_y = h - inHeight - local drawBackground = function() - gfx.setColor(colors.input.error_bg) - gfx.rectangle("fill", - 0, - start_y, - drawableWidth, - apparentHeight * fh) - end - - drawBackground() - gfx.setColor(colors.input.error) - for l, str in ipairs(err_text) do - local breaks = 0 -- starting height is already calculated - ViewUtils.write_line(l, str, start_y, breaks, self.cfg) - end + self:render_error(err_text) else - self:draw_input(input) + self:render_input(input, status) end + gfx.setCanvas() +end + +--- Draw the pre-rendered canvas to screen +function UserInputView:draw() + local h = self.start_h + love.graphics.draw(self.canvas, 0, h) end --- Whether the cursor is at limit, accounting for word wrap. diff --git a/src/view/view.lua b/src/view/view.lua index 5d62a772..23e47ea3 100644 --- a/src/view/view.lua +++ b/src/view/view.lua @@ -14,8 +14,7 @@ View = { gfx.push('all') local terminal = C:get_terminal() local canvas = C:get_canvas() - local input = C.input:get_input() - CV:draw(terminal, canvas, input, View.snapshot) + CV:draw(terminal, canvas, View.snapshot) gfx.pop() end, @@ -37,9 +36,12 @@ View = { x = gfx.getWidth() - 10 - w end gfx.push('all') + local prevCanvas = gfx.getCanvas() + gfx.setCanvas() gfx.setColor(Color[Color.yellow]) gfx.setFont(FPSfont) gfx.print(fps, x, 10) + gfx.setCanvas(prevCanvas) gfx.pop() end } From f826c032aff0cf11be2e3bc2997aa9016c497fb7 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 17:55:35 +0100 Subject: [PATCH 085/103] fix(uiv): cursor height --- src/view/input/userInputView.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 1250a88b..484bab17 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -117,12 +117,13 @@ function UserInputView:render_input(input, status) if vcl < 1 then return end - local ch = start_y + (vcl - 1) * fh + local ch = vcl * fh local x_offset = math.fmod(acc, w) + local x = (x_offset - .5) * fw gfx.push('all') gfx.setColor(cf_colors.input.cursor) - gfx.print('|', (x_offset - .5) * fw, ch) + gfx.print('|', x, ch) gfx.pop() end From 10224a3e3a59f31ca94a1539b8de6e2fe969048d Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 17:55:35 +0100 Subject: [PATCH 086/103] feat(uic): add is_oneshot --- src/controller/userInputController.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/controller/userInputController.lua b/src/controller/userInputController.lua index 553365db..c45f8bd1 100644 --- a/src/controller/userInputController.lua +++ b/src/controller/userInputController.lua @@ -20,6 +20,11 @@ end --- @field disable_selection boolean UserInputController = class.create(new) +--- @return boolean +function UserInputController:is_oneshot() + return self.model.oneshot +end + --- @param v UserInputView function UserInputController:init_view(v) self.view = v From 12275e3e5ba4ef7186c0f8e6486500bf9b5243dc Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 17:55:35 +0100 Subject: [PATCH 087/103] fix(editor): search view --- src/controller/searchController.lua | 9 ++++++++- src/view/editor/editorView.lua | 2 +- src/view/editor/search/searchView.lua | 9 +++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/controller/searchController.lua b/src/controller/searchController.lua index 090d72bf..caaf363a 100644 --- a/src/controller/searchController.lua +++ b/src/controller/searchController.lua @@ -3,10 +3,11 @@ local class = require('util.class') --- @param model Search --- @param uic UserInputController local function new(model, uic) - return { + local self = { model = model, input = uic, } + return self end --- @class SearchController @@ -14,6 +15,12 @@ end --- @field input UserInputController SearchController = class.create(new) +--- @param v SearchView +function SearchController:init_view(v) + self.view = v + self.input:init_view(self.view.input) +end + --- @param items table[] function SearchController:load(items) self.model:load(items) diff --git a/src/view/editor/editorView.lua b/src/view/editor/editorView.lua index 9bdf0f50..a9d1cba0 100644 --- a/src/view/editor/editorView.lua +++ b/src/view/editor/editorView.lua @@ -31,7 +31,7 @@ function EditorView:draw() local ctrl = self.controller local mode = ctrl:get_mode() if mode == 'search' then - self.search:draw(ctrl.search:get_input()) + self.search:draw() else local spec = mode == 'reorder' local bv = self:get_current_buffer() diff --git a/src/view/editor/search/searchView.lua b/src/view/editor/search/searchView.lua index 6e8fe433..634c75f2 100644 --- a/src/view/editor/search/searchView.lua +++ b/src/view/editor/search/searchView.lua @@ -5,11 +5,13 @@ require("view.input.userInputView") --- @param cfg ViewConfig --- @param ctrl SearchController local function new(cfg, ctrl) - return { + local self = { controller = ctrl, results = ResultsView(cfg), input = UserInputView(cfg, ctrl.input) } + ctrl:init_view(self) + return self end --- @class SearchView @@ -18,12 +20,11 @@ end --- @field input UserInputView SearchView = class.create(new) ---- @param input InputDTO -function SearchView:draw(input) +function SearchView:draw() local ctrl = self.controller local rs = ctrl:get_results() self.results:draw(rs) if ViewUtils.conditional_draw('show_input') then - self.input:draw(input) + self.input:draw() end end From dbe31b665c770b239b798ffbb6080d4f8d7aed8b Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 17:55:35 +0100 Subject: [PATCH 088/103] fix(uiv): background bleed --- src/view/input/userInputView.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 484bab17..b36f7135 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -301,6 +301,7 @@ function UserInputView:render_error(err_text) apparentHeight * fh) end + gfx.push('all') drawBackground() gfx.setColor(colors.input.error) @@ -309,6 +310,7 @@ function UserInputView:render_error(err_text) local breaks = 0 -- starting height is already calculated ViewUtils.write_line(l, str, start_y, breaks, self.cfg) end + gfx.pop() end --- @param input InputDTO From 7b4422c2c6cc910fd0e44bb32006b6466ff16e6b Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 17:55:35 +0100 Subject: [PATCH 089/103] feat(uiv): render before draw on non-oneshot inputs This is a transitional workaround until rerenders are worked out. --- src/view/input/userInputView.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index b36f7135..0abafd48 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -333,6 +333,9 @@ end --- Draw the pre-rendered canvas to screen function UserInputView:draw() + if not self.controller:is_oneshot() then + self.controller:update_view() + end local h = self.start_h love.graphics.draw(self.canvas, 0, h) end From 0f1ad345adad0e42e89ab39e594c88c013d4af53 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 17:55:35 +0100 Subject: [PATCH 090/103] fix(uiv): error render --- src/view/input/userInputView.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 0abafd48..0028363f 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -285,18 +285,19 @@ end function UserInputView:render_error(err_text) local colors = self.cfg.colors local fh = self.cfg.fh - local h = self.start_h - local drawableWidth = self.cfg.drawableWidth + local vpH = gfx.getHeight() + local inLines = #err_text - local inHeight = inLines * fh + self.start_h = vpH - (inLines + 1) * fh + local drawableWidth = self.cfg.drawableWidth local apparentHeight = #err_text - local start_y = h - inHeight + local start_y = fh -- statusline local drawBackground = function() gfx.setColor(colors.input.error_bg) gfx.rectangle("fill", 0, - start_y, + fh, drawableWidth, apparentHeight * fh) end @@ -323,6 +324,7 @@ function UserInputView:render(input, status) local isError = string.is_non_empty_string_array(err_text) gfx.setCanvas(self.canvas) + gfx.clear(0,0,0) if isError then self:render_error(err_text) else From dd2c4bb7d83ae138abd84a64b08173a2790b48d2 Mon Sep 17 00:00:00 2001 From: aldum Date: Tue, 11 Nov 2025 17:55:35 +0100 Subject: [PATCH 091/103] fix(uiv): fire updates --- src/controller/consoleController.lua | 4 ++++ src/controller/editorController.lua | 1 + src/controller/searchController.lua | 4 ++++ src/controller/userInputController.lua | 2 ++ 4 files changed, 11 insertions(+) diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 7648b802..4a4213a0 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -70,6 +70,7 @@ end function ConsoleController:init_view(V) self.view = V self.input:init_view(V.input) + self.input:update_view() end --- @param name string @@ -419,6 +420,7 @@ function ConsoleController.prepare_project_env(cc) ui_con = UserInputController(ui_model, input_ref, true) local view = UserInputView(cfg.view, ui_con) ui_con:init_view(view) + ui_con:update_view() love.state.user_input = { M = ui_model, C = ui_con, V = view @@ -822,11 +824,13 @@ function ConsoleController:keypressed(k) end end end + input:update_view() end --- @param k string function ConsoleController:keyreleased(k) self.input:keyreleased(k) + self.input:update_view() end --- @param x integer diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index 8b07beb7..b60b92ac 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -462,6 +462,7 @@ function EditorController:_search_mode_keys(k) return end + self.input:update_view() local jump = self.search:keypressed(k) if jump then local buf = self:get_active_buffer() diff --git a/src/controller/searchController.lua b/src/controller/searchController.lua index caaf363a..4f549dc0 100644 --- a/src/controller/searchController.lua +++ b/src/controller/searchController.lua @@ -24,6 +24,7 @@ end --- @param items table[] function SearchController:load(items) self.model:load(items) + self.input:update_view() end --- @return ResultsDTO @@ -71,12 +72,14 @@ end --- @param by integer? function SearchController:_scroll(dir, warp, by) self.model:scroll(dir, by, warp) + self.input:update_view() -- self:update_status() -- TODO: show more arrows end --- @param k string --- @return table? jump function SearchController:keypressed(k) + self.input:update_view() local function navigate() -- move selection if k == "up" then @@ -134,6 +137,7 @@ end --- @param t string function SearchController:textinput(t) + self.input:update_view() self.input:add_text(t) self:update_results() end diff --git a/src/controller/userInputController.lua b/src/controller/userInputController.lua index c45f8bd1..60885cd8 100644 --- a/src/controller/userInputController.lua +++ b/src/controller/userInputController.lua @@ -388,6 +388,7 @@ end --- @param t string function UserInputController:textinput(t) + self:update_view() if self.model:has_error() then return end @@ -401,6 +402,7 @@ end --- @param k string function UserInputController:keyreleased(k) local input = self.model + self:update_view() if input:has_error() then if k == 'space' then From 7700fe9ed41cfd1f5f11612bcc086a201fb9babf Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 092/103] fix(uiv): starting position of non-hl text --- src/view/input/userInputView.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 0028363f..64f976c7 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -273,9 +273,9 @@ function UserInputView:render_input(input, status) end end else + gfx.setColor(colors.fg) for l, str in ipairs(visible) do - gfx.setColor(colors.fg) - ViewUtils.write_line(l, str, start_y, 0, self.cfg) + ViewUtils.write_line(l, str, fh, 0, self.cfg) end end drawCursor() From 32b26feedb6e14ea63a1facb0e3d2a450a7e5c8a Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 093/103] fix(uiv): gfx hygiene --- src/view/editor/search/searchView.lua | 2 ++ src/view/input/userInputView.lua | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/view/editor/search/searchView.lua b/src/view/editor/search/searchView.lua index 634c75f2..ed8cb704 100644 --- a/src/view/editor/search/searchView.lua +++ b/src/view/editor/search/searchView.lua @@ -23,8 +23,10 @@ SearchView = class.create(new) function SearchView:draw() local ctrl = self.controller local rs = ctrl:get_results() + gfx.push("all") self.results:draw(rs) if ViewUtils.conditional_draw('show_input') then self.input:draw() end + gfx.pop() end diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 64f976c7..4f063dbf 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -141,7 +141,9 @@ function UserInputView:render_input(input, status) gfx.setFont(self.cfg.font) drawBackground() + gfx.push('all') self.statusline:draw(status, 0) + gfx.pop() if highlight then local hl = highlight.hl @@ -273,10 +275,12 @@ function UserInputView:render_input(input, status) end end else + gfx.push('all') gfx.setColor(colors.fg) for l, str in ipairs(visible) do ViewUtils.write_line(l, str, fh, 0, self.cfg) end + gfx.pop() end drawCursor() end @@ -324,7 +328,7 @@ function UserInputView:render(input, status) local isError = string.is_non_empty_string_array(err_text) gfx.setCanvas(self.canvas) - gfx.clear(0,0,0) + gfx.clear(0, 0, 0, 1) if isError then self:render_error(err_text) else @@ -339,7 +343,11 @@ function UserInputView:draw() self.controller:update_view() end local h = self.start_h + gfx.push('all') + gfx.setBlendMode("replace") love.graphics.draw(self.canvas, 0, h) + gfx.setBlendMode("alpha") + gfx.pop() end --- Whether the cursor is at limit, accounting for word wrap. From ebb600d3ea63c09eafc312ac253589e5b82143e3 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 094/103] feat: background for FPS counter --- src/conf.lua | 2 +- src/controller/controller.lua | 4 ++++ src/types.lua | 4 +++- src/view/view.lua | 18 +++++++++++++++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/conf.lua b/src/conf.lua index a11abd79..0e41f9a9 100644 --- a/src/conf.lua +++ b/src/conf.lua @@ -71,7 +71,7 @@ function love.conf(t) frame = 0, n_frames = frames, n_rows = 7, - fpsc = 'T_R' + fpsc = 'T_R_B' } end diff --git a/src/controller/controller.lua b/src/controller/controller.lua index ce013217..d5ad66fc 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -597,6 +597,10 @@ Controller = { end if k == "f10" then if love.PROFILE.fpsc == 'off' then + love.PROFILE.fpsc = 'T_L_B' + elseif love.PROFILE.fpsc == 'T_L_B' then + love.PROFILE.fpsc = 'T_R_B' + elseif love.PROFILE.fpsc == 'T_R_B' then love.PROFILE.fpsc = 'T_L' elseif love.PROFILE.fpsc == 'T_L' then love.PROFILE.fpsc = 'T_R' diff --git a/src/types.lua b/src/types.lua index 968b541b..2afa5b51 100644 --- a/src/types.lua +++ b/src/types.lua @@ -169,9 +169,11 @@ --- @field syntax_hl fun(table): SyntaxColoring ---@alias FPSC ----| 'off' ---| 'T_L" ---| 'T_R" +---| 'off' +---| 'T_L_B" +---| 'T_R_B" --- @class Profile --- @field report table diff --git a/src/view/view.lua b/src/view/view.lua index 23e47ea3..c5d04842 100644 --- a/src/view/view.lua +++ b/src/view/view.lua @@ -28,19 +28,31 @@ View = { if love.PROFILE.fpsc == 'off' then return end local fps = tostring(love.timer.getFPS()) + local mode = love.PROFILE.fpsc local w = FPSfont:getWidth(fps) + local fh = FPSfont:getHeight() + local y = 10 local x - if love.PROFILE.fpsc == 'T_L' then + if mode == 'T_L' + or mode == 'T_L_B' + then x = 10 - elseif love.PROFILE.fpsc == 'T_R' then + elseif mode == 'T_R' + or mode == 'T_R_B' + then x = gfx.getWidth() - 10 - w end gfx.push('all') local prevCanvas = gfx.getCanvas() gfx.setCanvas() + if mode == 'T_L_B' or mode == 'T_R_B' then + gfx.setColor(Color[Color.black]) + gfx.rectangle("fill", + x, y, w, fh - 5) + end gfx.setColor(Color[Color.yellow]) gfx.setFont(FPSfont) - gfx.print(fps, x, 10) + gfx.print(fps, x, y) gfx.setCanvas(prevCanvas) gfx.pop() end From 80f03f08cae1a4a53536cbabe6cc07d3cee09cf0 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 095/103] fix: user draw stack overflow Fix the logic of creating new draw functions when a user draw is introduced _or_ userinput needs to be drawn. With this change, it won't create umpteenth new functions eventually running out of stack space --- src/controller/controller.lua | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index d5ad66fc..6364831f 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -106,14 +106,15 @@ local set_handlers = function(userlove) end -- drawing - separate table - local draw = userlove.draw - - if draw and draw ~= View.main_draw then + local udr = userlove.draw + local mdr = View.main_draw + if udr and udr ~= mdr then --- @diagnostic disable-next-line: duplicate-set-field - love.draw = function() - draw() + local ndr = function() + udr() View.drawFPS() end + love.draw = ndr user_draw = true end end @@ -379,19 +380,20 @@ Controller = { end click_count = 0 end + local ddr = View.prev_draw local ldr = love.draw - local ui = get_user_input() - if ldr ~= ddr or ui then - local function draw() + if ldr ~= ddr then + local draw =function() if ldr then wrap(ldr) end - local user_input = get_user_input() - if user_input then - user_input.V:draw() + local ui = get_user_input() + if ui then + ui.V:draw() end end + View.prev_draw = draw love.draw = draw end From 0b2bc96f8e6cd1a80d54c419c9b24247dc1e47f1 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 096/103] fix: wrap in web Turns out, the stack overflow was not just affecting the web platform, with that fixed, wrapping can be reintroduced. --- src/controller/controller.lua | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index 6364831f..76ab7c69 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -60,15 +60,11 @@ end --- @return any ... local function wrap(f, ...) if _G.web then - -- local ok, r = pcall(f, ...) - -- if not ok then - -- user_error_handler(r) - -- end - -- return r - -- return xpcall(f, user_error_handler, ...) - --- TODO no error handling, sorry, it leads to a stack overflow - --- in love.wasm - return f(...) + local ok, r = pcall(f, ...) + if not ok then + user_error_handler(r) + end + return r else return xpcall(f, user_error_handler, ...) end From 69b07742a60943ab56df9695db84497162f73e3a Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 097/103] feat(util): add table.logset --- src/util/table.lua | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/util/table.lua b/src/util/table.lua index dbf93fb3..81772d6d 100644 --- a/src/util/table.lua +++ b/src/util/table.lua @@ -98,6 +98,36 @@ function table.protect(t, fields) return t, orig end +--- Log set operations +--- @param t table +--- @param fields? table +--- @return table prepared +--- @return table original +function table.logset(t, fields) + local orig = t + local proxy = {} + + setmetatable(proxy, { + __newindex = function(_, k, v) + if not fields then + Log(string.format("%s = %s", k, v)) + else + local fs = {} + for _, f in ipairs(fields) do + fs[f] = f + end + if fs[k] then + Log(string.format("%s = %s", k, v)) + end + end + orig[k] = v + end, + }) + -- getmetatable(proxy).__metatable = 'no-no' + t = proxy + return t, orig +end + --- @diagnostic disable-next-line: duplicate-set-field function table.pack(...) --- @class t From 6c5dea03ef8d00bac7dd5ea62bf16493ebfb86cc Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 098/103] fix(uiv): start height --- src/main.lua | 1 + src/types.lua | 1 + src/view/input/statusline.lua | 13 ++++++++----- src/view/input/userInputView.lua | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main.lua b/src/main.lua index aa01dde2..aaf88408 100644 --- a/src/main.lua +++ b/src/main.lua @@ -86,6 +86,7 @@ local config_view = function(flags) return { font = font_main, iconfont = font_icon, + statusline_border = 4, fh = fh, fw = fw, lh = lh, diff --git a/src/types.lua b/src/types.lua index 2afa5b51..e53f9f24 100644 --- a/src/types.lua +++ b/src/types.lua @@ -42,6 +42,7 @@ --- @class ViewConfig table --- @field font love.Font --- @field iconfont love.Font +--- @field statusline_border integer --- @field fh integer -- font height --- @field fw integer -- font width --- @field lh integer -- line height diff --git a/src/view/input/statusline.lua b/src/view/input/statusline.lua index af412222..46742572 100644 --- a/src/view/input/statusline.lua +++ b/src/view/input/statusline.lua @@ -28,6 +28,8 @@ function Statusline:draw(status, start_y) local w = cf.w local fh = cf.fh local font = cf.font + local sb = cf.statusline_border + local corr = sb / 2 local start_box = { x = 0, y = h } local endTextX = start_box.x + w - fh @@ -36,9 +38,8 @@ function Statusline:draw(status, start_y) local function drawBackground() gfx.setColor(colors.bg) gfx.setFont(font) - --- correct for fractional slit left under the terminal - local corr = 2 - gfx.rectangle("fill", start_box.x, start_box.y - corr, w, fh + corr) + gfx.rectangle("fill", + start_box.x, start_box.y - corr, w, fh + sb) end --- @param m More? @@ -62,7 +63,7 @@ function Statusline:draw(status, start_y) local custom = status.custom local start_text = { x = start_box.x + fh, - y = start_box.y - 2, + y = start_box.y, } gfx.setColor(colors.fg) @@ -73,7 +74,9 @@ function Statusline:draw(status, start_y) if love.DEBUG then gfx.setColor(cf.colors.debug) if love.state.testing then - gfx.print('testing', midX - (8 * cf.fw), start_text.y) + gfx.print('testing', + midX - (8 * cf.fw), + start_text.y + corr) end local lw = font:getWidth(state) / 2 gfx.print((state or '???'), diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 4f063dbf..ae4d5180 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -342,7 +342,8 @@ function UserInputView:draw() if not self.controller:is_oneshot() then self.controller:update_view() end - local h = self.start_h + local b = self.cfg.statusline_border / 2 + local h = self.start_h - b gfx.push('all') gfx.setBlendMode("replace") love.graphics.draw(self.canvas, 0, h) From d66ae97353edd50c0485603f205c19cc8edaf137 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 099/103] fix(uiv): color bleeding --- src/controller/controller.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controller/controller.lua b/src/controller/controller.lua index 76ab7c69..ed63a764 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -380,9 +380,11 @@ Controller = { local ddr = View.prev_draw local ldr = love.draw if ldr ~= ddr then - local draw =function() + local draw = function() if ldr then + gfx.push('all') wrap(ldr) + gfx.pop() end local ui = get_user_input() if ui then From 6fb273bab8ff01ef26fcdca8275368c16c7ccb6f Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 100/103] refactor(uiv): user renderTo --- src/view/input/userInputView.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index ae4d5180..1a331106 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -327,14 +327,14 @@ function UserInputView:render(input, status) local err_text = input.wrapped_error or {} local isError = string.is_non_empty_string_array(err_text) - gfx.setCanvas(self.canvas) - gfx.clear(0, 0, 0, 1) - if isError then - self:render_error(err_text) - else - self:render_input(input, status) - end - gfx.setCanvas() + self.canvas:renderTo(function () + gfx.clear(0, 0, 0, 1) + if isError then + self:render_error(err_text) + else + self:render_input(input, status) + end + end) end --- Draw the pre-rendered canvas to screen From 2bad1e435a490c7bca7d0cb1c6359466823b3b6f Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 101/103] feat(util): bool to string converter --- src/util/lua.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/util/lua.lua b/src/util/lua.lua index d9fd65a0..de1572fa 100644 --- a/src/util/lua.lua +++ b/src/util/lua.lua @@ -31,6 +31,9 @@ local t = { end end, codeload = codeload, + b2s = function(b) + return b and '#t' or '#f' + end, } for k, v in pairs(t) do From 237e625ff85bee9c90d88f81fba5aff42bbe528d Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 102/103] fix(uiv): proper drawing of wrapped lines + DRY --- src/view/input/userInputView.lua | 179 ++++++++++--------------------- 1 file changed, 57 insertions(+), 122 deletions(-) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index 1a331106..e9febd4a 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -97,17 +97,15 @@ function UserInputView:render_input(input, status) local overflow, of_h, ofpos = calc_overflow( w, text, cursorInfo.cursor) - local apparentLines = inLines + overflow - local inHeight = inLines * fh + local inHeight = (inLines + overflow) * fh local apparentHeight = inHeight - local y = fh local wrap_forward = vc.wrap_forward local wrap_reverse = vc.wrap_reverse - local start_y = h - apparentLines * fh + local vpH = gfx.getHeight() - self.start_h = vpH - (inLines + 1) * fh + self.start_h = vpH - (inLines + overflow + 1) * fh local function drawCursor() local y_offset = math.floor(acc / w) @@ -147,131 +145,68 @@ function UserInputView:render_input(input, status) if highlight then local hl = highlight.hl - if highlight.parse_err then - --- grammar = 'lua' - local color = colors.fg - local perr = highlight.parse_err - local el, ec - if perr then - el = perr.l - ec = perr.c - end - for l, s in ipairs(visible) do - local ln = l + vc.offset - local tl = string.ulen(s) - - if not tl then return end - - for c = 1, tl do - local char = string.usub(s, c, c) - - local hl_li = wrap_reverse[ln] - local tlc = vc:translate_from_visible(Cursor(l, c)) - - if tlc then - local ci = (function() - if hl[tlc.l] then - return hl[tlc.l][tlc.c] - end - end)() - if ci then - color = Color[ci] or colors.fg - end - end - if perr and ln > el or - (ln == el and (c > ec or ec == 1)) then - color = cf_colors.input.error - end - local selected = (function() - local sel = input.selection - local startl = sel.start and sel.start.l - local endl = sel.fin and sel.fin.l - if startl then - local startc = sel.start.c - local endc = sel.fin.c - if startc and endc then - if startl == endl then - local sc = math.min(sel.start.c, sel.fin.c) - local endi = math.max(sel.start.c, sel.fin.c) - return l == startl and c >= sc and c < endi - else - return - (l == startl and c >= sel.start.c) or - (l > startl and l < endl) or - (l == endl and c < sel.fin.c) - end - end + local perr = highlight.parse_err + local el, ec + if perr then + el = perr.l + ec = perr.c + end + for l, s in ipairs(visible) do + local ln = l + vc.offset + local tl = string.ulen(s) + if not tl then return end + + for c = 1, tl do + local char = string.usub(s, c, c) + local color = colors.fg + + local hl_li = wrap_reverse[ln] + local tlc = vc:translate_from_visible(Cursor(l, c)) + + if tlc then + local ci = (function() + if hl[tlc.l] then + return hl[tlc.l][tlc.c] end end)() - --- number of lines back from EOF - local diffset = #text - vc.range.fin - local of = overflow - --- push any further lines down to display phantom line - if ofpos and hl_li > cl then - of = of - 1 + if ci then + color = Color[ci] or colors.fg end - local dy = y - (-ln - diffset + 1 + of) * fh - local dx = (c - 1) * fw - ViewUtils.write_token(dy, dx, - char, color, colors.bg, selected) end - end - else - --- grammar = 'md' - for l, s in ipairs(visible) do - local ln = l + vc.offset - local tl = string.ulen(s) - if not tl then return end - - for c = 1, tl do - local char = string.usub(s, c, c) - local color = colors.fg - - --- @diagnostic disable-next-line: param-type-mismatch - local tlc = vc:translate_from_visible(Cursor(l, c)) - - if tlc then - local row = hl[tlc.l] - local lex_t = row[tlc.c] - if lex_t then - color = Color[lex_t] or colors.fg - end - end - local hl_li = wrap_reverse[ln] - - local selected = (function() - local sel = input.selection - local startl = sel.start and sel.start.l - local endl = sel.fin and sel.fin.l - if startl then - local startc = sel.start.c - local endc = sel.fin.c - if startc and endc then - if startl == endl then - local sc = math.min(sel.start.c, sel.fin.c) - local endi = math.max(sel.start.c, sel.fin.c) - return l == startl and c >= sc and c < endi - else - return - (l == startl and c >= sel.start.c) or - (l > startl and l < endl) or - (l == endl and c < sel.fin.c) - end + if perr and ln > el or + (ln == el and (c > ec or ec == 1)) then + color = cf_colors.input.error + end + local selected = (function() + local sel = input.selection + local startl = sel.start and sel.start.l + local endl = sel.fin and sel.fin.l + if startl then + local startc = sel.start.c + local endc = sel.fin.c + if startc and endc then + if startl == endl then + local sc = math.min(sel.start.c, sel.fin.c) + local endi = math.max(sel.start.c, sel.fin.c) + return l == startl and c >= sc and c < endi + else + return + (l == startl and c >= sel.start.c) or + (l > startl and l < endl) or + (l == endl and c < sel.fin.c) end end - end)() - --- number of lines back from EOF - local diffset = #text - vc.range.fin - local of = overflow - --- push any further lines down to display phantom line - if ofpos and hl_li > cl then - of = of - 1 end - local dy = y - (-ln - diffset + 1 + of) * fh - local dx = (c - 1) * fw - ViewUtils.write_token(dy, dx, - char, color, colors.bg, selected) + end)() + local of = calc_overflow(w, text, cursorInfo.cursor) + --- push any further lines down to display phantom line + if ofpos and hl_li > cl then + of = of - 1 end + local dy = (l) * fh + local dx = (c - 1) * fw + ViewUtils.write_token(dy, dx, + char, color, colors.bg, selected) end end else From ad206802d508fcfcfc2ac0c0861801a45da32813 Mon Sep 17 00:00:00 2001 From: aldum Date: Fri, 14 Nov 2025 21:34:17 +0100 Subject: [PATCH 103/103] style(uiv): format --- src/view/input/userInputView.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua index e9febd4a..5617a225 100644 --- a/src/view/input/userInputView.lua +++ b/src/view/input/userInputView.lua @@ -262,13 +262,13 @@ function UserInputView:render(input, status) local err_text = input.wrapped_error or {} local isError = string.is_non_empty_string_array(err_text) - self.canvas:renderTo(function () - gfx.clear(0, 0, 0, 1) - if isError then - self:render_error(err_text) - else - self:render_input(input, status) - end + self.canvas:renderTo(function() + gfx.clear(0, 0, 0, 1) + if isError then + self:render_error(err_text) + else + self:render_input(input, status) + end end) end