Redesign prompt system: revert #09, use standard _PROMPT (issue #21)

Revert the bespoke __prompt()/__prompt2() function mechanism from issue
#09 and return to stock Lua's _PROMPT/_PROMPT2 globals (which already
support dynamic prompts via __tostring metatables). Set default
_PROMPT = "lush> " in doREPL if not already defined by config.
This commit is contained in:
Cormac Shannon
2026-03-04 23:53:04 +00:00
parent 9e75175c79
commit b3aa1d9c63
4 changed files with 38 additions and 118 deletions

View File

@@ -1,6 +1,7 @@
# 09 — Programmable prompt # 09 — Programmable prompt
**Status:** done **Status:** open
**Blocked by:** #08
Users should be able to customize the shell prompt by defining a Lua function, similar to how other shells use `PS1`/`PROMPT_COMMAND`/`precmd`. Users should be able to customize the shell prompt by defining a Lua function, similar to how other shells use `PS1`/`PROMPT_COMMAND`/`precmd`.

View File

@@ -1,6 +1,6 @@
# 21 — Prompt redesign # 21 — Prompt redesign
**Status:** open **Status:** done
Reconsider the prompt system. Some observations: Reconsider the prompt system. Some observations:

55
lua.c
View File

@@ -541,38 +541,19 @@ static void lua_initreadline (lua_State *L) {
#endif /* } */ #endif /* } */
static int last_exit_code = 0; /* tracks exit code for __prompt() */
/* /*
** Return the string to be used as a prompt by the interpreter. Leave ** Return the string to be used as a prompt by the interpreter. Leave
** the string (or nil, if using the default value) on the stack, to keep ** the string (or nil, if using the default value) on the stack, to keep
** it anchored. ** it anchored.
**
** Fallback chain: __prompt()/__prompt2() → _PROMPT/_PROMPT2 → "> "/">> "
*/ */
static const char *get_prompt (lua_State *L, int firstline) { static const char *get_prompt (lua_State *L, int firstline) {
const char *fname = firstline ? "__prompt" : "__prompt2"; if (lua_getglobal(L, firstline ? "_PROMPT" : "_PROMPT2") == LUA_TNIL)
if (lua_getglobal(L, fname) == LUA_TFUNCTION) { return (firstline ? LUA_PROMPT : LUA_PROMPT2); /* use the default */
lua_pushinteger(L, last_exit_code); else { /* apply 'tostring' over the value */
if (lua_pcall(L, 1, 1, 0) == LUA_OK) {
const char *p = lua_tostring(L, -1);
if (p != NULL) return p; /* string stays on stack, anchored */
}
lua_pop(L, 1); /* pop error message or non-string result */
} else {
lua_pop(L, 1); /* pop the non-function value */
}
/* fallback: check _PROMPT/_PROMPT2 globals (standard Lua compat) */
if (lua_getglobal(L, firstline ? "_PROMPT" : "_PROMPT2") != LUA_TNIL) {
const char *p = luaL_tolstring(L, -1, NULL); const char *p = luaL_tolstring(L, -1, NULL);
lua_remove(L, -2); /* remove original value */ lua_remove(L, -2); /* remove original value */
return p; return p;
} }
lua_pop(L, 1); /* pop nil */
/* final fallback: push default string so pushline can pop it */
lua_pushstring(L, firstline ? LUA_PROMPT : LUA_PROMPT2);
return lua_tostring(L, -1);
} }
/* mark in error messages for incomplete statements */ /* mark in error messages for incomplete statements */
@@ -692,26 +673,6 @@ static int loadline (lua_State *L) {
} }
/*
** Install default __prompt function if not already defined (e.g. by config).
** Shows current directory with ~ substitution for HOME.
*/
static void install_default_prompt (lua_State *L) {
if (lua_getglobal(L, "__prompt") == LUA_TNIL) {
luaL_dostring(L,
"function __prompt(exitcode)\n"
" local cwd = os.getenv('PWD') or '?'\n"
" local home = os.getenv('HOME') or ''\n"
" if home ~= '' and cwd:sub(1, #home) == home then\n"
" cwd = '~' .. cwd:sub(#home + 1)\n"
" end\n"
" return cwd .. '> '\n"
"end\n");
}
lua_pop(L, 1); /* pop the getglobal result */
}
/* /*
** Prints (calling the Lua 'print' function) any values on the stack ** Prints (calling the Lua 'print' function) any values on the stack
*/ */
@@ -737,7 +698,14 @@ static void doREPL (lua_State *L) {
const char *oldprogname = progname; const char *oldprogname = progname;
progname = NULL; /* no 'progname' on errors in interactive mode */ progname = NULL; /* no 'progname' on errors in interactive mode */
lua_initreadline(L); lua_initreadline(L);
install_default_prompt(L); /* set default _PROMPT if not already defined (e.g. by config) */
if (lua_getglobal(L, "_PROMPT") == LUA_TNIL) {
lua_pop(L, 1);
lua_pushliteral(L, "lush> ");
lua_setglobal(L, "_PROMPT");
} else {
lua_pop(L, 1);
}
setsignal(SIGINT, repl_sigint_handler); setsignal(SIGINT, repl_sigint_handler);
if (sigsetjmp(repl_jmp, 1)) { if (sigsetjmp(repl_jmp, 1)) {
/* Ctrl-C during input — print newline, clear stack, re-prompt */ /* Ctrl-C during input — print newline, clear stack, re-prompt */
@@ -748,7 +716,6 @@ static void doREPL (lua_State *L) {
while ((status = loadline(L)) != -1) { while ((status = loadline(L)) != -1) {
if (status == LUA_OK) if (status == LUA_OK)
status = docall(L, 0, LUA_MULTRET); status = docall(L, 0, LUA_MULTRET);
last_exit_code = status; /* track for __prompt() */
if (status == LUA_OK) l_print(L); if (status == LUA_OK) l_print(L);
else report(L, status); else report(L, status);
setsignal(SIGINT, repl_sigint_handler); /* re-install after docall */ setsignal(SIGINT, repl_sigint_handler); /* re-install after docall */

View File

@@ -1,7 +1,7 @@
-- testes/lush/prompt.lua -- testes/lush/prompt.lua
-- Tests for programmable prompt (issue #09). -- Tests for prompt system (issue #21).
print "testing programmable prompt" print "testing prompt system"
-- helper: get a unique temp directory -- helper: get a unique temp directory
local tmpbase = os.tmpname() local tmpbase = os.tmpname()
@@ -39,95 +39,47 @@ local function run_lua(input, config_content)
end end
-- ===== TEST 1: Default __prompt is installed ===== -- ===== TEST 1: Default _PROMPT is "lush> " =====
-- After starting the REPL, __prompt should be a function
do do
local out = run_lua('"print(type(__prompt))"') local out = run_lua('"print(_PROMPT)"')
assert(out:find("function"), assert(out:find("lush> ", 1, true),
"default __prompt should be installed as function: " .. out) "default _PROMPT should be 'lush> ': " .. out)
end end
-- ===== TEST 2: Default __prompt returns cwd ===== -- ===== TEST 2: _PROMPT can be overridden by config =====
do do
local cwd = os.getenv("PWD") or "" local out = run_lua('"print(_PROMPT)"',
local home = os.getenv("HOME") or "" '_PROMPT = "custom> "\n')
local expected
if home ~= "" and cwd:sub(1, #home) == home then
expected = "~" .. cwd:sub(#home + 1)
else
expected = cwd
end
local out = run_lua('"print(__prompt(0))"')
assert(out:find(expected, 1, true),
"default __prompt should return cwd: expected '" ..
expected .. "' in: " .. out)
end
-- ===== TEST 3: Custom __prompt() overrides default =====
do
local out = run_lua('"print(__prompt(0))"',
'function __prompt() return "custom> " end\n')
assert(out:find("custom> ", 1, true), assert(out:find("custom> ", 1, true),
"custom __prompt should be used: " .. out) "config should override _PROMPT: " .. out)
end end
-- ===== TEST 4: __prompt() receives exit code ===== -- ===== TEST 3: _PROMPT with __tostring metatable (dynamic prompt) =====
do do
local out = run_lua( local out = run_lua('"print(_PROMPT)"',
'"error()" "print(__prompt())" "os.exit()"', '_PROMPT = setmetatable({}, { __tostring = function() return "dynamic> " end })\n')
'function __prompt(code)\n' assert(out:find("dynamic> ", 1, true),
.. ' return "exit:" .. tostring(code) .. "> "\n' "_PROMPT with __tostring should work: " .. out)
.. 'end\n')
-- After error(), the next prompt call should receive non-zero code.
-- But since we're calling __prompt() manually via print, we test
-- the mechanism differently: call it with a code directly.
local out2 = run_lua('"print(__prompt(42))"',
'function __prompt(code)\n'
.. ' return "exit:" .. tostring(code) .. "> "\n'
.. 'end\n')
assert(out2:find("exit:42> ", 1, true),
"exit code should be passed to __prompt: " .. out2)
end end
-- ===== TEST 5: __prompt2 for continuation ===== -- ===== TEST 4: _PROMPT2 works for continuation =====
do do
local out = run_lua('"print(type(__prompt2))"') local out = run_lua('"print(_PROMPT2)"',
-- __prompt2 is not installed by default '_PROMPT2 = "...> "\n')
assert(out:find("nil") or out:find("function"), assert(out:find("...> ", 1, true),
"__prompt2 should be nil or function: " .. out) "_PROMPT2 should be settable: " .. out)
end end
-- ===== TEST 6: Broken __prompt() falls back gracefully ===== -- ===== TEST 5: _PROMPT = nil falls back gracefully =====
do do
local out = run_lua('"print(42)"', -- Set _PROMPT to nil during the REPL; get_prompt falls back to LUA_PROMPT.
'function __prompt() error("boom") end\n') local out = run_lua('"_PROMPT = nil" "print(42)"')
assert(out:find("42"), assert(out:find("42"),
"broken __prompt should still allow REPL to work: " .. out) "_PROMPT = nil should still allow REPL to work: " .. out)
end
-- ===== TEST 7: _PROMPT still works (backward compat) =====
do
-- When __prompt is defined (default), it takes priority.
-- To test _PROMPT fallback, unset __prompt.
local out = run_lua('"print(42)"',
'__prompt = nil\n_PROMPT = "old> "\n')
assert(out:find("42"),
"_PROMPT fallback should still work: " .. out)
end
-- ===== TEST 8: Config-defined __prompt is not overwritten =====
do
local out = run_lua('"print(__prompt(0))"',
'function __prompt() return "from_config> " end\n')
assert(out:find("from_config> ", 1, true),
"config __prompt should not be overwritten: " .. out)
end end