From a75118b2aef4ae1db18d2fe17ca36091ea157da9 Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Sun, 10 May 2026 19:26:58 +0000 Subject: [PATCH] readline: bind() via rl_bind_keyseq; repl reserves \C-n no-op MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 readline binding wiring per PHASE1.md §7. ffi/readline: M.bind(seq, lua_fn) -> bool Wraps lua_fn as a C callback (signature `int (int, int)` per readline's rl_command_func_t) and registers it via rl_bind_keyseq(seq, cb). Returns true on success (rl returns 0). Trampolines are pinned in module-local state so they outlive the bind call — readline retains the function pointer for the process lifetime. Rebinding the same seq frees the previous trampoline. Bound handlers are pcall-wrapped so a Lua error doesn't crash readline's input loop. repl: Binds \C-n to a no-op that emits "[aish] Norris mode not yet implemented (Phase 3)" Verifies the mechanism end-to-end; Phase 3 (Norris autonomous mode) replaces the body with the actual toggle. Smoke covers bind / rebind-same-seq (exercises the :free path) / bind-different-seq with no errors. Live keyboard verification waits on user-test. Phase 1's 8(+1) inner loop is now functionally through `implement`; next inner phase is `verify` (review pass) followed by memory-update. Co-Authored-By: Claude Opus 4.7 (1M context) --- ffi/readline.lua | 30 ++++++++++++++++++++++++++++-- repl.lua | 7 +++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/ffi/readline.lua b/ffi/readline.lua index fdcad90..682203a 100644 --- a/ffi/readline.lua +++ b/ffi/readline.lua @@ -1,6 +1,7 @@ -- ffi/readline.lua — GNU readline binding. --- Phase 0: readline + add_history + EOF handling. Phase 1: custom key bindings. --- See docs/PHASE0.md §9. +-- Phase 0: readline + add_history + EOF handling. +-- Phase 1: custom key bindings via rl_bind_keyseq. +-- See docs/PHASE0.md §9 and docs/PHASE1.md §7. local ffi = require("ffi") @@ -8,6 +9,9 @@ ffi.cdef[[ char *readline(const char *prompt); void add_history(const char *line); void free(void *ptr); + +typedef int (*rl_command_func_t)(int, int); +int rl_bind_keyseq(const char *keyseq, rl_command_func_t function); ]] -- libreadline-dev (which ships the unversioned `libreadline.so` symlink) is @@ -47,4 +51,26 @@ function M.add_history(line) end end +-- Bind `seq` (e.g. "\\C-n") to a Lua function that runs when the user types +-- that key sequence at the readline prompt. The Lua fn takes no arguments +-- (readline passes count + key, but Phase 1 consumers don't need them). +-- Callback trampolines are pinned in module-local state so they outlive the +-- M.bind call — readline retains the function pointer indefinitely. +local _bound = {} + +function M.bind(seq, fn) + if _bound[seq] then + _bound[seq]:free() + end + local cb = ffi.cast("rl_command_func_t", function(_count, _key) + local ok, err = pcall(fn) + if not ok then + io.stderr:write("ffi/readline bind handler error: " .. tostring(err) .. "\n") + end + return 0 + end) + _bound[seq] = cb + return rl.rl_bind_keyseq(seq, cb) == 0 +end + return M diff --git a/repl.lua b/repl.lua index 9c82ad2..b84b5d8 100644 --- a/repl.lua +++ b/repl.lua @@ -67,6 +67,13 @@ function M.run(config) return ("[aish:%s]> "):format(active_name) end + -- Phase 1 reserved-key wiring (PHASE1.md §7). The mechanism is real; the + -- handlers are placeholders that emit a status. Phase 3 (Norris) is the + -- first consumer that replaces the body with real work. + rl.bind("\\C-n", function() + renderer.status("Norris mode not yet implemented (Phase 3)") + end) + local function status_evictions(n) if n and n > 0 then renderer.status(("oldest %d turns evicted"):format(n))