diff --git a/issues/02-backtick-lexing-parsing.md b/issues/02-backtick-lexing-parsing.md index 2a6d05bf..46c5953c 100644 --- a/issues/02-backtick-lexing-parsing.md +++ b/issues/02-backtick-lexing-parsing.md @@ -1,6 +1,6 @@ # 02 — Complete backtick command lexing and parsing -**Status:** open +**Status:** resolved **Blocked by:** #01 **Blocks:** #03 diff --git a/issues/03-command-execution-runtime.md b/issues/03-command-execution-runtime.md index 93444f47..6fda95f1 100644 --- a/issues/03-command-execution-runtime.md +++ b/issues/03-command-execution-runtime.md @@ -1,6 +1,6 @@ # 03 — Implement direct command execution runtime (no shell) -**Status:** open +**Status:** resolved **Blocked by:** #02 **Blocks:** #06, #07 diff --git a/issues/04-argv-parsing.md b/issues/04-argv-parsing.md index a750cd34..ec4cf35c 100644 --- a/issues/04-argv-parsing.md +++ b/issues/04-argv-parsing.md @@ -1,6 +1,6 @@ # 04 — Implement argv parsing (tokenize command strings) -**Status:** open +**Status:** resolved **Blocked by:** #01 **Blocks:** #06 diff --git a/issues/08-configuration.md b/issues/08-configuration.md new file mode 100644 index 00000000..2e2a9ec4 --- /dev/null +++ b/issues/08-configuration.md @@ -0,0 +1,29 @@ +# 08 — Configuration support + +**Status:** open + +Users should be able to programmatically configure the shell at startup. + +## Config location + +- `~/.config/lush/config` — main config file (Lua script) +- `~/.config/lush/config.d/` — drop-in directory, files sourced in lexicographic order + +## What can be configured + +- **Prompt** — see #09 +- **Aliases** — command aliases (e.g. `alias("ll", "ls -l")`) +- **Environment setup** — set/modify env vars at startup +- **Autocompletion rules** — TBD, mechanism for registering custom completions + +## Startup behaviour + +1. If `~/.config/lush/config` exists, execute it as a Lua script +2. If `~/.config/lush/config.d/` exists, execute each `.lua` file in lexicographic order +3. Config files run in the same Lua state as the interactive session — locals, globals, and env changes persist + +## Open questions + +- Should there be a system-wide config (`/etc/lush/config`) sourced before user config? +- What API surface to expose for autocompletion registration? +- Should config errors be fatal or just print a warning and continue? diff --git a/issues/09-prompt.md b/issues/09-prompt.md new file mode 100644 index 00000000..e58f358d --- /dev/null +++ b/issues/09-prompt.md @@ -0,0 +1,58 @@ +# 09 — Programmable prompt + +**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`. + +## Design + +- The prompt is generated by calling a global Lua function (e.g. `__prompt()`) +- The function returns a string which is used as the prompt +- If the function is not defined or errors, fall back to a sensible default + +## Default prompt + +A built-in default prompt is provided (implemented on the C side) until the user overrides `__prompt()` in their config. The default could be something like: + +``` +lush> +``` + +or include the current directory: + +``` +~/Code/project> +``` + +## User customization + +Users override the prompt in their config (`~/.config/lush/config`): + +```lua +function __prompt() + return $PWD .. "> " +end +``` + +Or with colours/git info/etc: + +```lua +function __prompt() + local cwd = $PWD or "?" + local branch = `git branch --show-current 2>/dev/null` + local git = (branch.code == 0) and " (" .. branch.stdout:gsub("\n","") .. ")" or "" + return cwd .. git .. "> " +end +``` + +## Implementation considerations + +- The REPL loop (in `lua.c`) calls `__prompt()` before each input line +- If `__prompt` is not a function or the call fails, use the default C-side prompt +- The function runs in the normal Lua state so it has access to all globals, env vars, commands, etc. + +## Open questions + +- Should there be a separate `__continuation_prompt()` for multiline input? +- Should the prompt function receive any arguments (e.g. exit code of last command)? diff --git a/issues/10-interactive-command-execution.md b/issues/10-interactive-command-execution.md new file mode 100644 index 00000000..4a50877b --- /dev/null +++ b/issues/10-interactive-command-execution.md @@ -0,0 +1,114 @@ +# Issue #10 — Interactive command execution + +## Problem + +All backtick commands redirect stdout/stderr to pipes and capture output into a table `{code, stdout, stderr}`. Interactive programs lose their tty connection: + +```lua +`bash` -- launches bash but it can't read/write the terminal +`vim foo` -- screen is blank, no input works +``` + +A real shell needs to hand the terminal to a child process and wait for it to finish. + +## What interactive execution means + +The child process inherits all three file descriptors (stdin, stdout, stderr) directly from the parent. The parent `waitpid()`s until the child exits. The terminal behaves as if lush isn't there — the child owns it. + +## Syntax + +### Option A: `!` prefix (recommended starting point) + +A `!` token marks a command as interactive. The command string extends to end-of-line (no closing delimiter needed): + +```lua +!bash +!vim foo.lua +!ssh user@host +``` + +`!` doesn't conflict with Lua syntax (`not` is the logical-not keyword). Supports `${}` interpolation like backticks: + +```lua +local f = "foo.lua" +!vim ${f} +``` + +### Option B: Bare-word REPL fallback (separate issue?) + +If a line fails to parse as Lua, interpret it as an interactive command: + +``` +> vim foo.lua -- not valid Lua, runs interactively +> ls -la -- same +> local x = 1 -- valid Lua, runs as Lua +``` + +Only applies in the REPL, not scripts. Could be combined with Option A. Touches `lua.c` (REPL loop) rather than the lexer/parser, so may warrant its own issue. + +### Option C: Built-in function + +```lua +exec("bash") +exec("vim", "foo.lua") +``` + +No new syntax. Could serve as the low-level primitive that `!` compiles down to. + +## Return value + +Interactive commands return the exit code as a plain integer: + +```lua +local code = !bash +assert(type(code) == "number") +``` + +No stdout/stderr to capture — they go to the terminal. Returning an integer (not a table) clearly distinguishes interactive from captured commands. + +When used as a statement (no assignment), the return value is discarded. + +## Runtime implementation + +Add `luaB_interactive` to `lcmd.c`, registered as `__interactive` global: + +- `fork()` without creating any pipes +- Child: `execvp()` directly (inherits parent's stdin/stdout/stderr) +- Parent: ignore `SIGINT`/`SIGQUIT` (so Ctrl-C goes to child, not lush), then `waitpid()` and return exit code as integer +- Child: restore `SIG_DFL` for `SIGINT`/`SIGQUIT` before `execvp()` +- Reuses existing `parse_argv()` for command string tokenization + +## Lexer changes (if using `!` prefix) + +- Add `TK_ICMD` and `TK_ICMD_INTERP` tokens to enum in `llex.h` +- Add `in_icmd` flag to `LexState` (mirrors `in_command` for backticks) +- `read_icmd_body()`: like `read_command_body()` but terminates on newline/EOF instead of closing backtick +- `read_icmd()`: skips leading whitespace after `!`, then calls `read_icmd_body()` +- `luaX_readicmdcont()`: continuation reader after `${}` interpolation (mirrors `luaX_readcommandcont()`) +- Handle `!` case in `llex()` switch: when `!` is followed by a non-`=` character at statement position, read as interactive command + +## Parser changes (if using `!` prefix) + +- Add `interactiveexp()` in `lparser.c` — mirrors `commandexp()` but compiles to `__interactive(cmdstring)` instead of `__command(cmdstring)` +- Add `TK_ICMD` / `TK_ICMD_INTERP` cases in `simpleexp()` +- Both simple and interpolated forms follow the same pattern as backtick commands + +## Open questions + +- Which syntax option(s) to pursue? Could start with `!` prefix and add others later. +- Job control: should `Ctrl-Z` suspend the child and return to lush? Requires `tcsetpgrp()` / process group work. Probably defer to a later issue. +- Should bare-word REPL fallback (Option B) be a separate issue? +- `PATH` lookup: `execvp` already searches `PATH`, so this should just work. +- Should `!` support multi-line commands (e.g. with `\` continuation)? Probably not — keep it simple. + +## Files touched + +| File | Description | +|------|-------------| +| `lcmd.c` | Add `luaB_interactive` — fork/exec without pipes, return exit code | +| `lcmd.h` | Declare `luaB_interactive` | +| `linit.c` | Register `__interactive` global in `opencommand()` | +| `llex.h` | Add `TK_ICMD`, `TK_ICMD_INTERP` tokens; `in_icmd` flag on `LexState`; declare `luaX_readicmdcont()` | +| `llex.c` | Add `read_icmd_body()`, `read_icmd()`, `luaX_readicmdcont()`; handle `!` in `llex()` | +| `lparser.c` | Add `interactiveexp()`; handle `TK_ICMD`/`TK_ICMD_INTERP` in `simpleexp()` | +| `lua.c` | *(Only if doing Option B)* REPL fallback logic in `loadline()` |