Skip to content

Add optional Acme-inspired Look and Execute commands - #3

Closed
dharmatech wants to merge 2 commits into
nooga:mainfrom
dharmatech:codex/acme-look
Closed

dharmatech wants to merge 2 commits into
nooga:mainfrom
dharmatech:codex/acme-look

Conversation

@dharmatech

@dharmatech dharmatech commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add an optional Acme-inspired F8 Look command for opening files and traversing directories as ordinary editable text buffers
  • add an optional F7 Execute command for running the active region or token as an rc expression and appending output to a reusable dir/+Errors buffer
  • add general per-buffer directory context and reusable named-output-buffer infrastructure
  • include focused tests, a design note, and a minimal opt-in configuration example

Approach

This explores what a keyboard-driven Acme interaction model feels like inside LegMacs on Plan 9. The directory view deliberately remains plain editable text rather than introducing a Dired-style mode. The extension is opt-in so the existing editor behavior is unchanged unless a user enables it.

Design and impact

  • the active region wins; otherwise Look and Execute use conservative Acme-style token expansion
  • Look opens files normally and represents directories as reusable editable listings, with directory entries suffixed by /
  • Execute runs synchronously and non-interactively in the current buffer's directory and separates standard error in the output buffer
  • each buffer can carry :default-directory; file, generated, REPL, help, and fallback buffers receive useful directory context
  • a private rfork e environment and rc -p prevent inherited CPU-session function definitions from breaking nested Plan 9 scripts such as lc
  • asynchronous jobs and interactive subprocess support are intentionally deferred

This draft is also intended to invite architectural feedback. The core additions are designed as general extension facilities, but generated-directory metadata and the named-output API are useful areas to review before treating the experiment as final.

Validation

  • full Letgo test suite under WSL: 258 tests, 1,011 assertions, 0 failures, 0 errors
  • manual 9front testing through Windows Drawterm: F8 file opening and directory traversal; F7 selected/token execution; reusable editable output; pwd and nested lc / execution without the inherited-function stderr failure
  • LegMacs resize behavior and clean exit remained intact during the manual checks

@dharmatech

Copy link
Copy Markdown
Contributor Author

Here's a demo of the legmacs acme extension:

https://youtu.be/zhKd5NLm4oA

@dharmatech

Copy link
Copy Markdown
Contributor Author

Mostly just sharing this as an example of an extension targeting plan9.

It also demonstrates what had to be extended in the core to accommodate the extension.
So that might provide insight on how to extend core to support similar extensions in the future.

Leaving this in a draft state since I wouldn't say it's ready to merge.

@nooga

nooga commented Aug 19, 2026 •

Copy link
Copy Markdown
Owner

Thanks for this — really nice piece of work, and a fun exploration of what Acme's text-as-interface model looks like inside legmacs. I went through it against the design docs in CLAUDE.md and it fits the grain of the codebase well: defcommand/bind-key! through the normal registries, :append-named reuses the existing :help-style find-or-create-by-name shape in apply-buffer-command instead of inventing a new mechanism, :default-directory rides along as an ordinary per-buffer field, and write-buffer-to correctly scrubs the generated-buffer markers when a directory/errors buffer gets saved to a real file. Ran the full suite locally too: 258 tests, 1011 assertions, 0 failures, 0 errors.

A couple of small things worth a pass before landing, nothing blocking:

  1. execute-at-point blocks synchronously — the os/sh call happens inline in the command, and the design doc says as much: "LegMacs pauses while the command runs." That matches the existing shell-command (M-!), which already blocks the same way, so it's not a new pattern in this codebase — but for a longer-running rc invocation it'd be worth thinking about how to keep the UI from freezing (e.g. painting a status frame before the call runs, or eventually an async job model), especially since the design doc already flags this as a known limitation to revisit.

  2. modeline.lg's buffer-label change (preferring :buffer-name over :filename) is a good fix — it looks like generated buffers (*repl*, *Help*, etc.) were always falling through to "*scratch*" in that segment before — but it's a global behavior change bundled in here without its own test. Worth splitting out or adding a small modeline test so it's covered independently of the Acme feature.

  3. Minor dedup nit: the find-by-name lambdas in :append-named and :help in buffers.lg are near-identical — could factor into one find-buffer-by-name helper.

Happy to see this land once those are addressed — great contribution.

@nooga

nooga commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Rebased this onto current main and took a pass at the three points above. Pushed to acme-look-rebased on this repo rather than force-pushing over your branch: https://github.com/nooga/legmacs/tree/acme-look-rebased — your two commits are on top, authorship intact, so pull it or cherry-pick as you like.

One real conflict in the rebase: new-file-state had moved on both sides. main grew a first-line content sniff for shebang mode detection (672517a), and your commit added :default-directory to the same two return paths. Both wanted, so it now threads (first lines) and the directory together.

On the three points:

3, the dedup. find-buffer-by-name + reuse-named-buffer in buffers.lg; :help and :append-named each collapse to a line, with the reuse policy — replace the content vs append to it — passed in as the only difference. That reads better than I expected: the two ops really are one op with a different merge function.

2, the modeline change. Turned out modeline.lg had no test file at all, so there's a new test/modeline_test.lg covering the :buffer-name > :filename > "*scratch*" precedence, the modified marker/color pairing, and the registry contract around it. The buffer-label fix stands on its own now, independent of the Acme feature.

1, the blocking Execute. Went with the general version of the status-frame idea rather than a local fix. There's a new workspace-shared :pending-task field: a command that has slow work to do returns a message plus a (fn [state] -> state), and main.lg's loop paints that frame and then runs the task in place of reading a key. The task threads through the same bufs/put-current fold as any command, so it can set a :buffer-command — and dropping the field is what consumes it, which means a task can arm another task.

That chaining is the part I actually care about, because it's the seam the async job model can grow from later: you can poll a running process across loop iterations without anything below main.lg learning what a process is. For now execute-at-point shows Execute: running <cmd> ... while rc runs.

To be clear about the limit: rc still runs to completion before the next key is read. This only fixes the frozen-frame half, where the screen sat on the state from before the keystroke with nothing saying why. That was the part that read as a bug; the waiting itself doesn't.

I left shell-command (M-!) alone even though converting it is a one-liner and is arguably the whole point of making the facility general — that's another global behavior change, which is exactly what point 2 was about. Separate commit later.

Two notes on the let-go side, since both changes introduce closures and #766 is silent:

  • execute-task is a named function building the closure, so execute-at-point's if-let branch contains a call rather than an fn literal. Same reason replaced-buffer-state is top-level instead of an inline (fn [_ x] x) in the case.
  • Ran ./build.sh to check: 524 fns lowered, native binary builds, and a scan of the whole generated tree for the if {} else {} + return rt.BoxNativeFn( pattern comes back empty.

Suite is at 300 tests, 1269 assertions, 0 failures.

@nooga

nooga commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Landed on main via #15 (rebased onto current main, review notes addressed, :pending-task as the shared deferred-work seam). Closing this draft as superseded.

@nooga nooga closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants