From b2814422ea607aaeb9758c5fc293abd9f0893cfe Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Mon, 9 Feb 2026 16:40:12 +0100 Subject: [PATCH 1/2] chore(docs): add AI agents steering documents --- AGENTS.md | 73 +++++++++++ ARCHITECTURE.md | 315 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 390 insertions(+) create mode 100644 AGENTS.md create mode 100644 ARCHITECTURE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..289a3431 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,73 @@ +# Guidance for AI agents + +This file is the main entry point for AI tools working in this repository. It provides project context and points to authoritative sources for conventions. + +## What this project is + +terminal.lua is a cross-platform terminal library for Lua (Windows/Unix/Mac). It provides terminal handling, text handling, and UI building blocks (panels, prompts, etc.). + +## Project layout + +| Path | Purpose | +|------|---------| +| `src/terminal/` | Lua source (modules under `terminal.*`, `terminal.cli.*`, `terminal.ui.*`) | +| `spec/` | Tests (Busted); mirror source layout where relevant | +| `examples/` | Example scripts | +| `doc_topics/` | Documentation source (markdown); generated output in `docs/` | +| `experimental/` | Experimental or non-stable code | +| `.luacheckrc` | LuaCheck lint configuration (authoritative for lint) | +| `.editorconfig` | Editor/formatting preferences | +| `config.ld` | ldoc configuration for API docs | + +Spec files live in a flat `spec/` directory and are named by module (e.g. `18-prompt_spec.lua` for `terminal.cli.prompt`). + +## Conventions and workflow + +Details are in **[CONTRIBUTING.md](CONTRIBUTING.md)**. Summary: + +- **Getting started:** Use the `Makefile` from the repository root; LuaRocks should use the user tree (see CONTRIBUTING). +- **Commits:** Atomic commits, [conventional-commits](https://www.conventionalcommits.org/) format (type + scope, present tense, 50-char header, 72-char body). +- **Testing:** `make test` (Busted), `make lint` (LuaCheck). Coverage: LuaCov, output in `luacov.report.out`. +- **Documentation:** ldoc; sources in `config.ld`. Run `make docs` to generate; do not commit generated docs (`make clean` will revert generated docs). Update comments and examples during development. + +## Test isolation + +In Busted the test files are ran as Lua files, all `describe` blocks are executed at load time. All other blocks only at test-runtime. Hence all initialization MUST be in `setup` and `before_each` blocks, and NEVER in `describe` blocks. The variables to hold the setup-stuff can be defined at the `describe` level (to keep them in scope for all tests within the block), but no values should be assigned to them at that point. +The only exception is code that generates tests, like table-tests, etc. since they are designed to run at the `describe` level. + +For example: + + describe("some block", function() + + local my_module -- only define here, no values! + + setup(function() + my_module = require "my.module.something" -- initialize the value here + end) + + it("a test", function() + -- test goes here + end) + end) + + +## Code style + +Code style is defined by [.luacheckrc](.luacheckrc) and [.editorconfig](.editorconfig). Follow CONTRIBUTING’s “Code style” section when present. + +In Busted test files in `./spec` the vertical whitespace is important: +- 3 lines between `describe` blocks +- 1 line at the start of a `describe` block +- 2 lines between other blocks (`it`, `setup`, `before_each`, etc) +- 3 lines between initialization (`setup`/`teardown`/etc) and the first `it` or `describe` block +- 1 line between multiple closing `end)` statements + +## Architecture + +High-level design and module boundaries will be described in **[ARCHITECTURE.md](ARCHITECTURE.md)**. + +## Other references + +- [CHANGELOG.md](CHANGELOG.md) — version history and SemVer +- [LICENSE.md](LICENSE.md) — license and copyright +- [Online documentation](https://lunarmodules.github.io/terminal.lua/) — API and usage diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..d571063c --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,315 @@ +# Architecture + +This document describes the high-level architecture of `terminal.lua`: the main concepts, module layout, and how the pieces fit together. + +--- + +## 1. Design goals + +`terminal.lua` is a cross-platform terminal library for Lua (Windows/Unix/Mac), built on top of [`luasystem`](https://github.com/lunarmodules/luasystem). Its key goals are: + +- **Cross-platform terminal control** without requiring a full curses-style stack. +- **Async-friendly input**: integrate cleanly with coroutine-based event loops. +- **Reversible terminal state** via **stacks** for cursor, colors, scroll region, etc. +- **Composable output** via **functions vs `*_seq` variants** and the `Sequence` class. +- **Higher-level building blocks** for **CLI input** and **panel-based UIs**. + +--- + +## 2. Core concepts + +### 2.1 Initialization & lifecycle + +The terminal must be initialized before use and restored afterwards: + +- `terminal.initialize(opts)`: + - Configures the underlying TTY / console (canonical mode, echo, non-blocking input). + - Sets up `sys.sleep` hooks (`_sleep` / `_bsleep`) for async integration. + - Optionally switches to an alternate screen buffer and backs up the display. + - Handles most of the platform specifics. +- `terminal.shutdown()`: + - Restores terminal settings and screen buffer. + - Restores stacks (cursor position, scroll region, text attributes, etc.). + +Preferred usage is via a wrapper (e.g. `initwrap`) that guarantees cleanup even on error. + +### 2.2 Functions vs `*_seq` variants + +Most low-level operations come in two forms: + +- **Effectful function** – writes directly to the configured output stream: + - e.g. `terminal.clear.eol()`, `terminal.cursor.shape.set("block_blink")` +- **`*_seq` function** – returns the ANSI sequence as a **string**, without writing: + - e.g. `terminal.clear.eol_seq()`, `terminal.cursor.shape.set_seq("block_blink")` + +This enables two styles: + +- Simple imperative use: call the effectful functions directly. +- Composed / batched output: collect `_seq` strings, concatenate, and write once to reduce flicker and improve performance. This is powerful when used with the `Sequence` class. + +### 2.3 Stacks + +Terminal state (colors, cursor shape, scroll region, etc.) is **global and hard to query**. To make state reversible, the library uses stack-based APIs: + +Stacks exist for: + +- **Cursor shape** (`terminal.cursor.shape.stack`) +- **Cursor visibility** (`terminal.cursor.visible.stack`) +- **Cursor position** (`terminal.cursor.position.stack`) -- preferably NOT used due to slow querying. +- **Scroll region** (`terminal.scroll.stack`) +- **Text attributes and color** (`terminal.text.stack`) + +Typical operations: + +- `push(values...)` – pushes new state, applies it. +- `pop(n)` – pops and reapplies the previous state. +- `apply()` – reapplies the top of the stack. + +> Important: stack-based functions are **not** suited to be baked into reusable strings, because their effect depends on call-time state, not creation-time state. The workaround here is to use a `Sequence` class, which supports functions/lambda's, the stack operations can be wrapped in a function. + +> Important: the cursor position stack uses a query to find the current position and hence should **NOT** be used if possible. Querying is slow. If there is no risk of yielding in a coroutine implementation, then it is save to use the terminal function for backup and restore of the position which work without querying. + +### 2.4 Sequence class + +`terminal.sequence` provides a small **Sequence** type: + +- A sequence is an array of **strings or functions**. +- Converting a sequence to a string: + - Executes any functions and concatenates their return values. +- Sequences can be: + - Instantiated by calling the class: `Seq("a", "b")` + - Concatenated with `+` to form new sequences. + - Nested inside each other. + +This allows dynamic assembly of output that still works nicely with the `*_seq` pattern and stack-based functions (functions are executed at render time). + +--- + +## 3. Module overview + +The main entry point is `src/terminal/init.lua`, which exposes the `terminal` module table and wires submodules: + +- `terminal.input` +- `terminal.output` +- `terminal.clear` +- `terminal.scroll` +- `terminal.cursor` +- `terminal.text` +- `terminal.draw` +- `terminal.progress` +- `terminal.sequence` +- `terminal.editline` +- `terminal.ui.panel.*` +- `terminal.cli.*` +- `terminal.utils` + +### 3.1 Core modules + +- **`terminal`** (`src/terminal/init.lua`) + - Holds version metadata and high-level helpers: + - `terminal.size()` – wrapper around `system.termsize`. + - `terminal.bell()` / `terminal.bell_seq()` – terminal bell. + - `terminal.preload_widths()` – preloads characters into the width cache for box drawing and progress spinners. + - Manages initialization/shutdown and integration with `system`: + - Console flags, non-blocking input, code page, alternate screen buffer. + - Sleep function wiring for async usage. + +- **`terminal.input`** + - Reads keyboard input, including async / coroutine-based flows. + - Handles query-response patterns for terminal state (e.g. cursor position) by buffering extra incoming data. + - Uses configuration (`sleep`, `bsleep`) set during `terminal.initialize`. + +- **`terminal.output`** + - Centralizes all writes to the terminal. + - Can patch Lua’s standard IO functions if needed (see docs). + - All other modules write via this layer to keep behavior consistent. + +- **`terminal.clear` / `terminal.scroll` / `terminal.cursor` / `terminal.text`** + - Provide focused operations: + - `clear`: clear screen / lines. + - `scroll`: scroll region operations and stack. + - `cursor`: cursor position / shape / visibility (including stacks). + - `text`: text attributes, colors, and the text stack. + - All follow the **functions vs `*_seq`** pattern, plus stacks where applicable. + +- **`terminal.text.width`** + - Computes display width for UTF-8 text (handles full-width / ambiguous-width characters). + - Used by higher-level components (e.g. `EditLine`, prompts, panel titles) to keep alignment correct. + +- **`terminal.utils`** + - Shared helpers: + - Small class system (`utils.class`). + - UTF-8 substring helpers (`utf8sub`, `utf8sub_col`). + - Misc utilities used across modules. + +### 3.2 Higher-level building blocks + +#### 3.2.1 Sequence and EditLine + +- **`terminal.sequence`**: + - The `Sequence` class (see §2.4) for constructing complex output lazily. + +- **`terminal.editline`**: + - Line-editing abstraction that: + - Tracks cursor position both in UTF-8 characters and display columns. + - Provides editing operations (move left/right, delete, word-wise operations). + - Provides formatting helpers (e.g. `:format{ width = ..., wordwrap = ... }`) that are used by CLI widgets. + +These are the core primitives for advanced text handling and interactive inputs. + +#### 3.2.2 CLI widgets (`terminal.cli.*`) + +- **`terminal.cli.prompt`** (`cli.Prompt`) + - High-level prompt widget: + - Renders a prompt string and an editable input value. + - Uses `terminal.input.keymap` for key bindings. + - Uses `EditLine`, `terminal.text.width`, `terminal.output`, and `Sequence`. + - Contract: + - Requires `terminal.initialize` to be called before use. + - Provides both `Prompt{...}:run()` and callable-shortcut `Prompt{...}()` styles. + - Handles cancellation (Esc / Ctrl-C) when configured. + +- **`terminal.cli.select`** + - Selection widget (list-style selection) built on top of the same primitives: + - Uses `terminal.input`, `EditLine`, etc. + - Follows the same initialization requirements as `Prompt`. + +These widgets are examples of how to build higher-level interactive components on the core terminal primitives. + +#### 3.2.3 Panel-based UI (`terminal.ui.panel.*`) + +- **`terminal.ui.panel`** (Panel system) + - Provides the `ui.Panel` class and related helpers: + - Tree of panels, each either: + - A **content panel** with a `content(self)` callback, or + - A **divided panel** with two child panels and a split orientation. + - Orientation constants: `Panel.orientations.horizontal` / `.vertical`. + - Panel types: content vs split. + - Size constraints: `min_height`, `max_height`, `min_width`, `max_width`. + - Layout calculation: `calculate_layout(row, col, height, width)`. + - Rendering that uses `terminal.draw`, `terminal.cursor`, `terminal.text`, etc. + - Supports: + - Nested layouts. + - Borders (via `terminal.draw.box_fmt` and text attributes). + - Named panel lookup via `panel.panels[name]`. + +- **`terminal.ui.panel.screen` / `bar` / `key_bar` / `tab_strip` / `text` / `set`** + - Additional components built on top of `ui.Panel`: + - `Screen` – root screen abstraction for a full-terminal layout. + - `Bar`, `KeyBar` – bar-style UI elements (status bars, key hints). + - `TabStrip` – tab-like UI along an edge. + - `Text` – panel for text content. + - `Set` – collection / grouping of panels. + +These modules demonstrate using the core drawing and layout primitives to construct complex UIs. + +--- + +## 4. Async model and terminal handling + +### 4.1 Async input + +`terminal.lua` is designed to work in coroutine-based environments: + +- Input: + - `terminal.input.readansi` and related functions cooperate with a `sleep` function supplied via `terminal.initialize`. + - In a coroutine-based loop, this sleep function can yield instead of blocking. +- Output: + - Remains synchronous (writes to the terminal are assumed to be fast), but can be batched via `_seq` + `Sequence`. + +The async model is largely controlled by the user-supplied `sleep` / `bsleep` functions and any event loop they integrate with. + +### 4.2 Querying terminal state + +For query operations (e.g. cursor position): + +- A query sequence is written (e.g. via `cursor.position.get`). +- The response is read back from STDIN. +- Any unrelated data already in the input buffer must be buffered and re-used later. + +This is encapsulated by **`terminal.input`** (e.g. `preread` and `read_query_answer`) so higher-level code does not have to manage raw buffers. + +--- + +## 5. Text handling in the UI + +Terminal UI must align and truncate text by **display columns**, not by bytes or UTF-8 character count. Characters can be one or two columns wide (e.g. CJK, emojis), and some have ambiguous width. This section describes how to handle width, substrings, and formatted display so text renders correctly. + +### 5.1 Display width + +- **`terminal.text.width`** provides the width primitives: + - **`utf8cwidth(char)`** – width in columns of a single character (string or codepoint). Uses a cache when available; otherwise falls back to `system.utf8cwidth`. + - **`utf8swidth(str)`** – total display width of a string in columns. +- **Width cache:** Not all characters have a fixed width (e.g. East Asian ambiguous). The library maintains a cache of **tested** widths. To populate it: + - **`terminal.text.width.test(str)`** – writes characters invisibly, measures cursor movement, and records each character’s width. Call during startup or when you first display unknown glyphs. + - **`terminal.preload_widths(str)`** – convenience that tests the library’s own box-drawing and progress characters plus any optional `str`. Call once after `terminal.initialize` if you use `terminal.draw` or `terminal.progress`. +- Use **`terminal.size()`** to get terminal dimensions (rows × columns) so you can fit text to the visible area. + +**Rule of thumb:** For correct alignment and truncation, always reason in **columns**. Use `utf8swidth` to measure strings and `utf8cwidth` for per-character width when implementing substrings or cursors. + +### 5.2 Substrings by characters vs columns + +**`terminal.utils`** provides two substring functions that behave like `string.sub` but respect UTF-8 and display width: + +- **`terminal.utils.utf8sub(str, i, j)`** + - Operates on **UTF-8 character indices** (not bytes). Supports negative indices. Use when you need “first N characters” or “from character i to j” in a way that is safe across Lua 5.1 / 5.2 / LuaJIT (where `string.sub` is byte-based). +- **`terminal.utils.utf8sub_col(str, i, j, no_pad)`** + - Operates on **display columns** `i` to `j` (1-based, non-negative). Uses `terminal.text.width.utf8cwidth` for each character. Use for: + - Truncating a string to fit a fixed column width (e.g. a panel or status bar). + - Extracting a slice of the display (e.g. “columns 3–10”). + - **`no_pad`**: when the range starts or ends in the middle of a double-width character, the result can include a leading/trailing space so the returned string’s display width matches the requested column span. Set `no_pad = true` to omit that padding (result may span one column less at the edges). + +**Example:** To show a string in a 20‑column slot, either truncate with `utils.utf8sub_col(s, 1, 20)` or use `EditLine` and its `format` method for multi-line or editable content. + +### 5.3 EditLine: cursor, columns, and formatted display + +**`terminal.editline`** (**EditLine** class) is the right tool when you need: + +- Editable line(s) with a cursor. +- Positions and lengths in both **UTF-8 characters** and **display columns**. +- Word-wrapped or fixed-width formatted display (e.g. for prompts or text panels). + +EditLine maintains: + +- **`chars`** – array of UTF-8 characters. +- **`widths`** – per-character display width (single/double). +- **Cursor:** `pos_char()` (character index) and `pos_col()` (column index). + +Key methods for display and layout: + +- **`len_char()`** / **`len_col()`** – length in characters vs columns. +- **`format(opts)`** – splits the content into lines that fit a given width. Options include: + - **`width`** – target line width in columns. + - **`first_width`** – width of the first line (e.g. after a prompt). + - **`wordwrap`** – wrap by words vs hard break. + - **`pad`** / **`pad_last`** – whether to pad lines to full width. +- The **`format(opts)`** method returns a table of EditLine instances (one per line) plus the cursor’s line and column in that formatted view. Used by **`cli.Prompt`** and similar widgets to render multi-line input and place the cursor correctly. + +**When to use what:** + +- **Simple truncation or fixed-width slice:** use **`utils.utf8sub_col(str, 1, max_col)`** (and optionally ellipsis). +- **Editable single/multi-line text with cursor and word wrap:** use **EditLine** and **`EditLine:format(...)`**. +- **Measuring or testing width:** use **`terminal.text.width.utf8swidth`** / **`utf8cwidth`** and **`terminal.text.width.test`** / **`terminal.preload_widths`** as above. + +All terminal output must go through **`terminal.output`** (e.g. `terminal.output.write`), not raw `print` or `io.write`, so that the library’s stream and any patching behave correctly. + +--- + +## 6. How to extend + +When adding new functionality: + +- **New terminal capabilities** (e.g. new escape sequences): + - Prefer adding to an existing module (`clear`, `cursor`, `text`, `scroll`, `draw`) following: + - Function + `_seq` pattern. + - Stack pattern when stateful and reversible. +- **New UI components**: + - Build on `terminal.sequence`, `terminal.output`, and `terminal.text.*`. + - For layout-heavy components, use `ui.Panel` as a basis. +- **New CLI widgets**: + - Reuse `EditLine`, `terminal.input.keymap`, and existing patterns from `cli.Prompt` and `cli.Select`. +- **Async-aware features**: + - Respect the `sleep` / `bsleep` hooks managed by `terminal.initialize`. + - Avoid introducing hard-blocking operations inside tight loops. + +This keeps new code aligned with the library’s core patterns and predictable for users. diff --git a/README.md b/README.md index 8df0c00a..48bf3f86 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ See [online documentation](https://lunarmodules.github.io/terminal.lua/) See [CONTRIBUTING.md](CONTRIBUTING.md) +**For AI agents:** Project context and conventions for AI tools are in [AGENTS.md](AGENTS.md). Read that file first. + ## Changelog & Versioning See [CHANGELOG.md](CHANGELOG.md) From 7ff6d6e2d73e630bb3f7028ebf5c5ab18aeea2ae Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Mon, 9 Feb 2026 20:34:12 +0100 Subject: [PATCH 2/2] chore(test): update test files according to AI agent instructions --- spec/02-input_spec.lua | 18 +++++--- spec/04-scroll_spec.lua | 1 - spec/06-cursor_spec.lua | 1 + spec/07-color_spec.lua | 1 + spec/09-editline_spec.lua | 1 + spec/11-screen_spec.lua | 1 + spec/12-draw_spec.lua | 1 + spec/13-bar_spec.lua | 2 + spec/14-text_panel_spec.lua | 9 ++++ spec/18-prompt_spec.lua | 87 +++++++++++++++++-------------------- 10 files changed, 67 insertions(+), 55 deletions(-) diff --git a/spec/02-input_spec.lua b/spec/02-input_spec.lua index e6ff0d99..f71ba0d3 100644 --- a/spec/02-input_spec.lua +++ b/spec/02-input_spec.lua @@ -1,7 +1,7 @@ describe("input:", function() local t, sys, old_readkey - local keyboard_buffer = "" + local keyboard_buffer lazy_setup(function() sys = require("system") @@ -103,12 +103,17 @@ describe("input:", function() describe("read_query_answer()", function() - -- returns an ANSWER sequence to the cursor-position query - local add_cpos = function(row, col) - keyboard_buffer = keyboard_buffer .. ("\027[%d;%dR"):format(row, col) - end + local add_cpos + local cursor_answer_pattern + + setup(function() + -- returns an ANSWER sequence to the cursor-position query + add_cpos = function(row, col) + keyboard_buffer = keyboard_buffer .. ("\027[%d;%dR"):format(row, col) + end + cursor_answer_pattern = "^\27%[(%d+);(%d+)R$" + end) - local cursor_answer_pattern = "^\27%[(%d+);(%d+)R$" it("returns the cursor positions read", function() @@ -170,5 +175,4 @@ describe("input:", function() end) - end) diff --git a/spec/04-scroll_spec.lua b/spec/04-scroll_spec.lua index 9b019d5c..05be6d54 100644 --- a/spec/04-scroll_spec.lua +++ b/spec/04-scroll_spec.lua @@ -21,7 +21,6 @@ describe("Scroll Module Tests", function() - it("should return default scroll region reset sequence", function() assert.are.equal("\27[r", scroll.reset_seq()) end) diff --git a/spec/06-cursor_spec.lua b/spec/06-cursor_spec.lua index 2811e599..a72498d0 100644 --- a/spec/06-cursor_spec.lua +++ b/spec/06-cursor_spec.lua @@ -314,6 +314,7 @@ describe("Cursor", function() assert.are.equal("\27[1A", cursor.position.up_seq()) end) + it("does nothing if 0", function() assert.are.equal("", cursor.position.up_seq(0)) end) diff --git a/spec/07-color_spec.lua b/spec/07-color_spec.lua index 7a9560cd..9bd081d8 100644 --- a/spec/07-color_spec.lua +++ b/spec/07-color_spec.lua @@ -6,6 +6,7 @@ describe("text.color", function() color = require("terminal.text.color") end) + after_each(function() color = nil end) diff --git a/spec/09-editline_spec.lua b/spec/09-editline_spec.lua index 15efb125..bec20d6e 100644 --- a/spec/09-editline_spec.lua +++ b/spec/09-editline_spec.lua @@ -80,6 +80,7 @@ describe("EditLine:", function() }, line.word_delimiters) end) + it("initializes cursor position at the end", function() local line = EditLine("hello") assert.are.equal(6, line:pos_char()) -- cursor should be at the end of "hello" diff --git a/spec/11-screen_spec.lua b/spec/11-screen_spec.lua index 34576edf..8be93162 100644 --- a/spec/11-screen_spec.lua +++ b/spec/11-screen_spec.lua @@ -22,6 +22,7 @@ describe("terminal.ui.screen", function() end) + describe("init()", function() it("creates a screen with body panel only", function() diff --git a/spec/12-draw_spec.lua b/spec/12-draw_spec.lua index 31e76052..cd742805 100644 --- a/spec/12-draw_spec.lua +++ b/spec/12-draw_spec.lua @@ -12,6 +12,7 @@ describe("terminal.draw", function() end) + it("creates title sequence with empty title", function() local result = line.title_seq(10, "") assert.are.equal("──────────", result) diff --git a/spec/13-bar_spec.lua b/spec/13-bar_spec.lua index 298313b6..b11a3cb6 100644 --- a/spec/13-bar_spec.lua +++ b/spec/13-bar_spec.lua @@ -24,6 +24,7 @@ describe("terminal.ui.panel.bar", function() } end) + teardown(function() -- Unset modules for clean test isolation Bar = nil @@ -31,6 +32,7 @@ describe("terminal.ui.panel.bar", function() end) + describe("init()", function() it("creates a bar with default values", function() diff --git a/spec/14-text_panel_spec.lua b/spec/14-text_panel_spec.lua index 7c8cab38..0532e067 100644 --- a/spec/14-text_panel_spec.lua +++ b/spec/14-text_panel_spec.lua @@ -42,6 +42,7 @@ describe("terminal.ui.panel.text", function() end) + describe("init()", function() it("creates a text panel with default values", function() @@ -76,6 +77,7 @@ describe("terminal.ui.panel.text", function() end) + describe("format_line_truncate()", function() it("returns padded string for nil input", function() @@ -113,6 +115,7 @@ describe("terminal.ui.panel.text", function() end) + describe("set_position()", function() it("goes to specified position", function() @@ -154,6 +157,7 @@ describe("terminal.ui.panel.text", function() end) + describe("scroll_up()", function() it("scrolls up by scroll_step", function() @@ -191,6 +195,7 @@ describe("terminal.ui.panel.text", function() end) + describe("scroll_down()", function() it("scrolls down by scroll_step", function() @@ -230,6 +235,7 @@ describe("terminal.ui.panel.text", function() end) + describe("page_up()", function() it("scrolls up by page size", function() @@ -267,6 +273,7 @@ describe("terminal.ui.panel.text", function() end) + describe("page_down()", function() it("scrolls down by page size", function() @@ -304,6 +311,7 @@ describe("terminal.ui.panel.text", function() end) + describe("get_position()", function() it("returns current position", function() @@ -318,6 +326,7 @@ describe("terminal.ui.panel.text", function() end) + describe("get_line_count()", function() it("returns number of lines", function() diff --git a/spec/18-prompt_spec.lua b/spec/18-prompt_spec.lua index 48c17507..fd37e968 100644 --- a/spec/18-prompt_spec.lua +++ b/spec/18-prompt_spec.lua @@ -6,9 +6,46 @@ describe("terminal.cli.prompt", function() local Prompt local t local old_size, old_write, old_print, old_readansi + local input_queue + local ENTER_KEY, ESC_KEY, CTRL_C_KEY + local queue_key + + setup(function() + local keymap_module = require("terminal.input.keymap") + local keys = keymap_module.default_keys + local default_key_map = keymap_module.default_key_map + + for raw_key, key_name in pairs(default_key_map) do + if key_name == keys.enter then + ENTER_KEY = raw_key + break + end + end + assert(ENTER_KEY, "Could not find Enter key in keymap") + + for raw_key, key_name in pairs(default_key_map) do + if key_name == keys.escape then + ESC_KEY = raw_key + break + end + end + assert(ESC_KEY, "Could not find Escape key in keymap") + + for raw_key, key_name in pairs(default_key_map) do + if key_name == keys.ctrl_c then + CTRL_C_KEY = raw_key + break + end + end + assert(CTRL_C_KEY, "Could not find Ctrl+C key in keymap") + + queue_key = function(key, keytype) + assert(type(key) == "string", "queue_key: 'key' must be a string, got " .. type(key)) + assert(type(keytype) == "string", "queue_key: 'keytype' must be a string, got " .. type(keytype)) + table.insert(input_queue, { key = key, keytype = keytype }) + end + end) - -- Input queue for mocked readansi - local input_queue = {} before_each(function() t = require("terminal") @@ -36,6 +73,7 @@ describe("terminal.cli.prompt", function() Prompt = require("terminal.cli.prompt") end) + after_each(function() t.size = old_size t.output.write = old_write @@ -47,51 +85,6 @@ describe("terminal.cli.prompt", function() end) - -- Helper to queue a key press - -- @param key string: the key character or escape sequence - -- @param keytype string: the type of key (e.g., "char", "ansi") - local function queue_key(key, keytype) - assert(type(key) == "string", "queue_key: 'key' must be a string, got " .. type(key)) - assert(type(keytype) == "string", "queue_key: 'keytype' must be a string, got " .. type(keytype)) - table.insert(input_queue, { key = key, keytype = keytype }) - end - - -- Derive the Enter key from the keymap (same as production code) - local keymap_module = require("terminal.input.keymap") - local keys = keymap_module.default_keys - local default_key_map = keymap_module.default_key_map - - -- Find the raw key that maps to the "enter" key name - local ENTER_KEY - for raw_key, key_name in pairs(default_key_map) do - if key_name == keys.enter then - ENTER_KEY = raw_key - break - end - end - assert(ENTER_KEY, "Could not find Enter key in keymap") - - -- Find the raw key that maps to the "escape" key name - local ESC_KEY - for raw_key, key_name in pairs(default_key_map) do - if key_name == keys.escape then - ESC_KEY = raw_key - break - end - end - assert(ESC_KEY, "Could not find Escape key in keymap") - - -- Find the raw key that maps to the "ctrl_c" key name (Ctrl+C) - local CTRL_C_KEY - for raw_key, key_name in pairs(default_key_map) do - if key_name == keys.ctrl_c then - CTRL_C_KEY = raw_key - break - end - end - assert(CTRL_C_KEY, "Could not find Ctrl+C key in keymap") - - describe("init() with empty value", function()