Files
aish/ffi/readline.lua
T
marfrit a75118b2ae readline: bind() via rl_bind_keyseq; repl reserves \C-n no-op
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) <noreply@anthropic.com>
2026-05-10 19:26:58 +00:00

77 lines
2.3 KiB
Lua

-- ffi/readline.lua — GNU readline binding.
-- 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")
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
-- not assumed to be installed on the runtime host; fall back to versioned
-- sonames so a base Debian/Arch with just libreadline runtime works.
local function load_readline()
local errs = {}
for _, name in ipairs({"readline", "readline.so.8", "readline.so.7"}) do
local ok, lib = pcall(ffi.load, name)
if ok then return lib end
errs[#errs+1] = name .. ": " .. tostring(lib)
end
error("libreadline not loadable: " .. table.concat(errs, "; "))
end
local rl = load_readline()
local C = ffi.C
local M = {}
-- Read one line of input.
-- Returns:
-- string : the line (no trailing newline)
-- nil : EOF (Ctrl-D on empty line)
function M.readline(prompt)
local cstr = rl.readline(prompt)
if cstr == nil then return nil end
local s = ffi.string(cstr)
C.free(cstr)
return s
end
-- Append a non-empty line to readline's in-memory history.
function M.add_history(line)
if line and #line > 0 then
rl.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