From 6f92a80bb1dc76f41023d0c843ad259ce751e892 Mon Sep 17 00:00:00 2001 From: Cormac Shannon Date: Thu, 9 Oct 2025 16:57:56 +0100 Subject: [PATCH] Add raw mode support to REPL for immediate key handling (Ctrl+L/Ctrl+C/Ctrl+D) and update README with usage notes --- README.md | 4 +++ luash | 102 ++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 95 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2bf6b62..784e086 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,10 @@ The interactive shell supports: See [examples/08_repl_guide.luash](examples/08_repl_guide.luash) for a comprehensive guide. +Notes: +- Raw terminal mode (immediate Ctrl+L/Ctrl+C/Ctrl+D) auto-enables when attached to a TTY. +- Force enable/disable with `LUASH_RAW=1` or `LUASH_RAW=0`. + ### Debug Mode ```bash LUASH_DEBUG=1 ./luash script.luash diff --git a/luash b/luash index e3fa995..6a61184 100755 --- a/luash +++ b/luash @@ -210,22 +210,96 @@ function start_repl() return s:match("^%s*(.-)%s*$") end + -- Raw mode (optional) for immediate key handling like Ctrl+L + local function detect_tty() + local r = os.execute("test -t 0 >/dev/null 2>&1") + if type(r) == "number" then return r == 0 end + return r == true + end + local env_raw = os.getenv("LUASH_RAW") + local raw_mode_enabled = (env_raw == "1") or (env_raw == nil and detect_tty()) + + local function set_raw_mode(enable) + if enable then + os.execute("stty -echo -icanon -isig min 1 time 0") + else + os.execute("stty sane") + end + end + + local function raw_read_line(prompt) + io.write(prompt) + io.flush() + local buf = {} + while true do + local ch = io.read(1) + if not ch then + return nil + end + local b = string.byte(ch) + if b == 10 or b == 13 then -- Enter + io.write("\n") + return table.concat(buf) + elseif b == 12 then -- Ctrl+L + -- Clear screen and reprint banner + prompt + current buffer + repl.buffer = table.concat(buf) + io.write("\27[2J\27[H") + io.flush() + print("🚀 Luash Interactive Shell") + print("Type Lua expressions, use $VAR for env vars, `commands` for shell") + print("Special commands: .help .exit .clear .history (Ctrl+L to clear, Ctrl+D to exit)") + print("") + io.write(prompt) + io.write(repl.buffer) + io.flush() + -- Keep current input for usability, but allow immediate Ctrl+D by not forcing buf non-empty + -- buf remains as-is + elseif b == 3 then -- Ctrl+C + return "__CTRL_C__" + elseif b == 4 then -- Ctrl+D + -- Exit immediately (treat as EOF) regardless of current buffer state + return nil + elseif b == 127 or b == 8 then -- Backspace/Delete + if #buf > 0 then + table.remove(buf) + io.write("\b \b") + io.flush() + end + else + table.insert(buf, ch) + io.write(ch) + io.flush() + end + end + end + -- Main REPL loop print("🚀 Luash Interactive Shell") print("Type Lua expressions, use $VAR for env vars, `commands` for shell") print("Special commands: .help .exit .clear .history (Ctrl+L to clear, Ctrl+D to exit)") print("") + if raw_mode_enabled then set_raw_mode(true) end + local ok_loop, loop_err = pcall(function() while true do local prompt = repl.buffer == "" and "luash> " or " >> " - io.write(prompt) - io.flush() - - -- Safely read a line; handle Ctrl+C gracefully - local ok, line = pcall(io.read, "*line") - if not ok then - print("\nInterrupted (Ctrl+C)") - break + local line + if raw_mode_enabled then + line = raw_read_line(prompt) + if line == "__CTRL_C__" then + print("\nInterrupted (Ctrl+C)") + break + end + else + io.write(prompt) + io.flush() + -- Safely read a line; handle Ctrl+C gracefully + local ok, l = pcall(io.read, "*line") + if not ok then + print("\nInterrupted (Ctrl+C)") + break + end + line = l end if not line then -- EOF (Ctrl+D) @@ -233,8 +307,8 @@ function start_repl() break end - -- Handle Ctrl+L (form feed) to clear screen - if line:find(string.char(12), 1, true) then + -- Handle Ctrl+L (form feed) to clear screen in non-raw mode + if (not raw_mode_enabled) and line:find(string.char(12), 1, true) then repl.buffer = "" io.write("\27[2J\27[H") -- Clear screen and move cursor to top io.flush() @@ -342,6 +416,12 @@ Multi-line input supported for functions, loops, etc.]]) end ::continue:: end + end) + if raw_mode_enabled then set_raw_mode(false) end + if not ok_loop then + if raw_mode_enabled then set_raw_mode(false) end + print("\nREPL error: " .. tostring(loop_err)) + end end -- 1. Check for an input file or REPL mode @@ -378,7 +458,7 @@ end -- Preprocessing functions local function process_env_vars(code) - local lines = {} +local lines = {} for line in code:gmatch("([^\r\n]*)") do if not line:match("^%s*%-%-") then -- Skip comments