diff --git a/CLAUDE.md b/CLAUDE.md index 944a6d0..a200c66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,20 +35,23 @@ The binary is built to `dist/try` from C source files in `src/`. Object files ar The tool uses a unique architecture where it emits shell commands that are sourced by the parent shell: -1. User runs `try cd` or similar command -2. The C binary executes and prints shell commands to stdout -3. A shell wrapper function sources the output, executing it in the current shell context -4. This allows the tool to change the current directory (impossible for normal subprocesses) +1. User runs `try` or similar command +2. The C binary executes and builds a shell script +3. In exec mode: script is printed to stdout for the shell wrapper to eval +4. In direct mode: script is executed via `system()`, with cd commands printed as hints +5. This allows the tool to change the current directory (impossible for normal subprocesses) -Commands are emitted via `emit_task()` in `src/commands.c`, which prints shell snippets chained with `&&`. Each command chain ends with `true\n`. +Commands return shell scripts as `zstr` strings. The `run_script()` function either prints (exec mode) or executes (direct mode) the script. ### Core Components **Command Layer** (`src/commands.c`, `src/commands.h`): - `cmd_init()`: Prints shell function definition for integration -- `cmd_clone()`: Generates shell commands to clone repo into dated directory -- `cmd_cd()`: Launches interactive selector, emits cd command -- `cmd_worktree()`: Placeholder (not implemented) +- `cmd_clone()`: Returns shell script to clone repo into dated directory +- `cmd_selector()`: Launches interactive selector, returns cd/mkdir script +- `cmd_worktree()`: Returns shell script to create git worktree +- `cmd_route()`: Routes subcommands for exec mode +- `run_script()`: Executes or prints a shell script **Interactive TUI** (`src/tui.c`, `src/tui.h`): - `run_selector()`: Main interactive loop using raw terminal mode @@ -56,10 +59,17 @@ Commands are emitted via `emit_task()` in `src/commands.c`, which prints shell s - Returns `SelectionResult` with action type and path - Supports fuzzy filtering and keyboard navigation +**TUI Styling** (`src/tui_style.c`, `src/tui_style.h`): +- ANSI escape code constants and semantic style aliases +- `TuiStyleString`: Stack-based style management for proper nesting +- `tui_push()`/`tui_pop()`: Push/pop styles with automatic targeted resets +- `tui_print()`/`tui_printf()`: Styled text output +- Screen API for full-screen TUI rendering + **Fuzzy Matching** (`src/fuzzy.c`, `src/fuzzy.h`): - `fuzzy_match()`: Updates TryEntry score and rendered output in-place - `calculate_score()`: Combines fuzzy match score with recency (mtime) -- `highlight_matches()`: Inserts `{highlight}` tokens around matched characters +- `highlight_matches()`: Wraps matched characters with ANSI highlight codes - Algorithm favors consecutive character matches and recent access times - **Documentation**: See `spec/fuzzy_matching.md` for complete algorithm specification @@ -69,12 +79,6 @@ Commands are emitted via `emit_task()` in `src/commands.c`, which prints shell s - Window size detection - Cursor visibility control -**Token System** (`src/tokens.rl`, `src/tokens.c`, `src/tokens.h`): -- Ragel-based state machine for token expansion -- Stack-based style nesting with `{/}` pop operation -- Full color palette support (standard, bright, 256-color) -- Control sequences for cursor and screen management - **Utilities** (`src/utils.c`, `src/utils.h`): - Path utilities: `join_path()`, `mkdir_p()`, directory existence checks - Time formatting: `format_relative_time()` for human-readable timestamps @@ -102,69 +106,14 @@ Manual cleanup still required in some cases: ### Data Flow -1. User invokes `try cd [query]` +1. User invokes `try [query]` 2. `main()` parses `--path` flag or uses default tries directory -3. `cmd_cd()` calls `run_selector()` with optional initial filter +3. `cmd_selector()` calls `run_selector()` with optional initial filter 4. `run_selector()` scans directories with `scan_tries()` 5. Interactive loop: user types to filter, arrows to navigate 6. On Enter: returns `SelectionResult` with action and path -7. `cmd_cd()` emits shell commands to cd to selected path -8. Shell wrapper sources output, executing the cd command - -### Token System - -The UI uses a stack-based token formatting system implemented with [Ragel](https://www.colm.net/open-source/ragel/). Tokens are placeholder strings embedded in text that get expanded to ANSI escape codes via `zstr_expand_tokens()`. This allows formatting to be defined declaratively without hardcoding ANSI sequences throughout the codebase. - -**Implementation**: `src/tokens.rl` (Ragel source) generates `src/tokens.c` - -**Documentation**: See `spec/token_system.md` for complete token specifications and usage patterns. - -**Stack Semantics**: Each style token pushes its previous state onto a stack. `{/}` pops one level, restoring the previous state. This enables proper nesting of styles. - -**Key Features:** -- **Deferred Emission**: ANSI codes only emit when an actual character is printed -- **Redundancy Avoidance**: Repeated identical styles don't emit duplicate codes -- **Auto-Reset at Newlines**: All styles automatically reset before each newline - -**Available Tokens:** - -| Category | Tokens | -|----------|--------| -| **Semantic** | `{b}` (bold), `{highlight}` (bold+yellow), `{h1}` (bold+orange), `{h2}` (bold+blue), `{dim}` (gray), `{section}` (bold), `{danger}` (red bg) | -| **Attributes** | `{bold}` `{B}`, `{italic}` `{I}`, `{underline}` `{U}`, `{reverse}`, `{strikethrough}`, `{strike}` | -| **Colors** | `{red}`, `{green}`, `{blue}`, `{yellow}`, `{cyan}`, `{magenta}`, `{white}`, `{black}`, `{gray}`/`{grey}` | -| **Bright Colors** | `{bright:red}`, `{bright:green}`, etc. | -| **256-Color** | `{fg:N}`, `{bg:N}` where N is 0-255 | -| **Background** | `{bg:red}`, `{bg:green}`, etc. | -| **Reset/Pop** | `{/}` (pop), `{/name}` (e.g., `{/highlight}`), `{reset}` (full reset), `{/fg}`, `{/bg}` | -| **Control** | `{clr}` (clear line), `{cls}` (clear screen), `{home}`, `{hide}`, `{show}` | -| **Special** | `{cursor}` (cursor position tracking) | - -**Usage Pattern:** -```c -// Simple formatting -Z_CLEANUP(zstr_free) zstr result = zstr_expand_tokens("Status: {b}OK{/}"); - -// Nested styles (stack-based) -Z_CLEANUP(zstr_free) zstr result = zstr_expand_tokens("{bold}Bold {red}and red{/} just bold{/} normal"); - -// 256-color support -Z_CLEANUP(zstr_free) zstr result = zstr_expand_tokens("{fg:214}Orange text{/}"); -``` - -**In Fuzzy Matching:** -The fuzzy matching system in `src/fuzzy.c` inserts `{highlight}` tokens around matched characters. These tokens are preserved through the rendering pipeline and expanded to ANSI codes when displayed: - -```c -// Input: "2025-11-29-test", query: "te" -// Output: "2025-11-29-{highlight}te{/}st" -// Displayed: "2025-11-29-[bold yellow]te[reset]st" -``` - -**Regenerating Token Parser:** -```bash -ragel -C -G2 src/tokens.rl -o src/tokens.c -``` +7. `cmd_selector()` builds shell script for the action +8. `run_script()` executes or prints the script ## Configuration @@ -207,19 +156,17 @@ vec_push(&entries, entry); vec_free_TryEntry(&entries); // Free vector (not contents!) ``` -**Emitting shell commands:** -```c -emit_task("cd", "/some/path", NULL); -printf("true\n"); // End command chain -``` - -**Token expansion for UI text:** +**Styled text output:** ```c -Z_CLEANUP(zstr_free) zstr formatted = zstr_from("{dim}Path:{/fg} {b}"); -zstr_cat(&formatted, some_path); -zstr_cat(&formatted, "{/b}"); -Z_CLEANUP(zstr_free) zstr expanded = zstr_expand_tokens(zstr_cstr(&formatted)); -// Use expanded for output +// Simple styled text to zstr +tui_zstr_printf(&output, TUI_BOLD, "Hello"); +zstr_cat(&output, " world\n"); + +// Stack-based styling for complex output +TuiStyleString ss = tui_start_zstr(&buf); +tui_push(&ss, TUI_SELECTED); // Push background +tui_print(&ss, TUI_BOLD, "Name"); // Styled text (auto-resets, keeps bg) +tui_pop(&ss); // Restore previous style ``` ## String and Array Management @@ -235,17 +182,14 @@ Z_CLEANUP(zstr_free) zstr expanded = zstr_expand_tokens(zstr_cstr(&formatted)); External dependencies are minimal: - Standard C library (POSIX) - Math library (`-lm` for fuzzy scoring) -- Ragel (`ragel`) - only needed if modifying `src/tokens.rl` The `src/libs/` directory contains bundled single-header libraries (zstr, zvec, zlist) that are self-contained. -Note: The generated `src/tokens.c` is checked into the repository, so Ragel is only required when modifying the token system. - ## Directory Structure - `src/` - C source and header files - `src/libs/` - Bundled single-header libraries (z-libs: zstr, zvec, zlist) -- `spec/` - Try specifications (CLI structure, fuzzy matching, token system, TUI) +- `spec/` - Try specifications (CLI structure, fuzzy matching, TUI) - `docs/` - Reference implementation and z-libs documentation - `test/` - Test suite (test.sh) - `obj/` - Object files (created by make, gitignored) @@ -257,11 +201,10 @@ Note: The generated `src/tokens.c` is checked into the repository, so Ragel is o - `src/main.c` - Entry point, argument parsing - `src/tui.c` - Interactive selector implementation +- `src/tui_style.c` - TUI styling and screen API - `src/fuzzy.c` - Scoring and highlighting logic -- `src/tokens.rl` - Token system (Ragel source) -- `src/tokens.c` - Token system (generated, do not edit) - `src/utils.c` - Shared utilities -- `src/commands.c` - Command implementations, shell emission +- `src/commands.c` - Command implementations, script building - `Makefile` - Build configuration - `docs/try.reference.rb` - Ruby reference implementation (source of truth for features) @@ -269,12 +212,6 @@ Note: The generated `src/tokens.c` is checked into the repository, so Ragel is o **IMPORTANT**: When making changes to certain subsystems, their corresponding documentation files must be updated: -- **Token system** (`src/tokens.rl`): - - Update `spec/token_system.md` with any new tokens or changed ANSI codes - - Update the token table in this file (CLAUDE.md) - - Update `src/tokens.h` header documentation - - Regenerate C code: `ragel -C -G2 src/tokens.rl -o src/tokens.c` - - **Fuzzy matching algorithm** (`src/fuzzy.c`, `fuzzy_match()`): - Update `spec/fuzzy_matching.md` with algorithm changes - Update scoring examples if formulas change diff --git a/Makefile b/Makefile index 415f035..b6785e9 100644 --- a/Makefile +++ b/Makefile @@ -11,27 +11,10 @@ DIST_DIR = dist BIN = $(DIST_DIR)/try SRCS = $(wildcard $(SRC_DIR)/*.c) -OBJS = obj/commands.o obj/main.o obj/terminal.o obj/tui.o obj/utils.o obj/fuzzy.o obj/tokens.o - -# Ragel-generated sources (tokens.c is checked in, ragel only needed for modifications) -RAGEL_SRCS = $(SRC_DIR)/tokens.c +OBJS = obj/commands.o obj/main.o obj/terminal.o obj/tui.o obj/tui_style.o obj/utils.o obj/fuzzy.o all: $(BIN) -# Generate C from Ragel (only if .rl is newer than .c AND ragel is available) -# tokens.c is read-only to prevent accidental edits - modify tokens.rl instead -$(SRC_DIR)/tokens.c: $(SRC_DIR)/tokens.rl - @if command -v ragel >/dev/null 2>&1; then \ - chmod +w $@ 2>/dev/null || true; \ - ragel -C -G2 $< -o $@; \ - chmod -w $@; \ - echo "Regenerated tokens.c from tokens.rl"; \ - else \ - echo "WARNING: ragel not installed. Using existing tokens.c"; \ - echo " Install ragel if you need to modify tokens.rl"; \ - touch $@; \ - fi - $(BIN): $(OBJS) | $(DIST_DIR) $(CC) $(LDFLAGS) -o $@ $^ -lm @@ -47,11 +30,6 @@ $(DIST_DIR): clean: rm -rf $(OBJ_DIR) $(DIST_DIR) -# tokens.o depends on the generated tokens.c -# Ragel-generated code has intentional switch fallthrough and unused variables -$(OBJ_DIR)/tokens.o: $(SRC_DIR)/tokens.c | $(OBJ_DIR) - $(CC) $(CFLAGS) -Wno-unused-const-variable -Wno-implicit-fallthrough -c -o $@ $< - install: $(BIN) install -m 755 $(BIN) /usr/local/bin/try @@ -67,12 +45,7 @@ test-valgrind: $(BIN) spec-update @echo "Running spec tests under valgrind..." spec/upstream/tests/runner.sh "valgrind -q --leak-check=full ./dist/try" -test-unit: $(OBJ_DIR)/tokens.o | $(DIST_DIR) - @echo "Building and running unit tests..." - $(CC) $(CFLAGS) -o $(DIST_DIR)/tokens_test src/tokens_test.c $(OBJ_DIR)/tokens.o - $(DIST_DIR)/tokens_test - -test: test-unit test-fast +test: test-fast @command -v valgrind >/dev/null 2>&1 && $(MAKE) test-valgrind || echo "Skipping valgrind tests (valgrind not installed)" # Update PKGBUILD and .SRCINFO with current VERSION @@ -81,4 +54,4 @@ update-pkg: @makepkg --printsrcinfo > .SRCINFO @echo "Updated PKGBUILD and .SRCINFO to version $(VERSION)" -.PHONY: all clean install test test-fast test-valgrind test-unit spec-update update-pkg +.PHONY: all clean install test test-fast test-valgrind spec-update update-pkg diff --git a/flake.nix b/flake.nix index 1926aa3..98076f7 100644 --- a/flake.nix +++ b/flake.nix @@ -96,7 +96,6 @@ gnumake valgrind gdb - ragel # For modifying src/tokens.rl ]; shellHook = '' diff --git a/spec/token_system.md b/spec/token_system.md deleted file mode 100644 index 7c51c3b..0000000 --- a/spec/token_system.md +++ /dev/null @@ -1,246 +0,0 @@ -# Token System Specification - -The token system provides a declarative way to apply ANSI formatting in terminal output. It uses a stack-based approach that allows proper nesting and clean restoration of previous styles. - -> **Note**: It is totally OK to only implement a subset of this system to make try work. This spec is extensive because it's being used outside of just try. - -## Implementation - -The token parser is implemented using [Ragel](https://www.colm.net/open-source/ragel/) to generate a fast state machine. The source is in `src/tokens.rl` and generates `src/tokens.c`. - -To regenerate after modifying `tokens.rl`: -```bash -ragel -C -G2 src/tokens.rl -o src/tokens.c -``` - -## Stack Semantics - -Each style-setting token pushes its previous state onto a stack. The `{/}` token pops one level, restoring the previous state. This enables proper nesting: - -```c -// Input: "Normal {highlight}bold yellow {red}red{/} yellow{/} normal" -// Output: "Normal [bold+yellow]bold yellow [red]red[yellow] yellow[reset] normal" -``` - -Composite tokens (like `{highlight}` which sets both bold and yellow) push multiple attributes but are grouped so a single `{/}` restores all of them. - -## Deferred Emission - -ANSI codes are only emitted when an actual character is about to be printed. This provides several benefits: - -1. **Redundancy avoidance**: `{dim}{dim}{dim}x` outputs only one `\033[37m` code -2. **No unused codes**: `{b}{/}x` outputs just `x` with no ANSI codes -3. **Efficient output**: Codes are batched when possible (e.g., `\033[1;33m` instead of `\033[1m\033[33m`) - -## Auto-Reset at Newlines - -All active styles are automatically reset before each newline character. This ensures: -- Clean line boundaries without trailing ANSI codes -- Proper terminal behavior when lines are scrolled or redrawn -- No style bleeding across lines - -## Token Reference - -### Semantic Tokens (Composite Styles) - -| Token | Effect | ANSI Codes | -|-------|--------|------------| -| `{b}` | Bold only (same as `{strong}`) | `\033[1m` | -| `{highlight}` | Bold + yellow foreground (for highlighting matches) | `\033[1;33m` | -| `{h1}` | Bold + orange (256-color 214) | `\033[1;38;5;214m` | -| `{h2}` | Bold + blue | `\033[1;34m` | -| `{h3}` - `{h6}` | Bold + white | `\033[1;37m` | -| `{strong}` | Bold | `\033[1m` | -| `{dim}` | White foreground (softer than bright white) | `\033[37m` | -| `{dark}` | Medium gray foreground (256-color 245, for TUI secondary text) | `\033[38;5;245m` | -| `{section}` | Bold + dark gray background (for selection highlight) | `\033[1;48;5;237m` | -| `{danger}` | Dark red background (for warnings/deletions) | `\033[48;5;52m` | -| `{strike}` | Legacy alias for `{danger}` | `\033[48;5;52m` | -| `{text}` | Full reset | `\033[0m` | - -### Attribute Tokens - -| Token | Effect | ANSI Code | -|-------|--------|-----------| -| `{bold}`, `{B}` | Bold text | `\033[1m` | -| `{italic}`, `{I}`, `{i}` | Italic text | `\033[3m` | -| `{underline}`, `{U}`, `{u}` | Underlined text | `\033[4m` | -| `{reverse}` | Reverse/inverse video | `\033[7m` | -| `{strikethrough}` | Strikethrough text | `\033[9m` | -| `{bright}` | Brighten current foreground color | Converts 30-37 to 90-97 | - -### Foreground Colors - -Standard colors: -| Token | ANSI Code | -|-------|-----------| -| `{black}` | `\033[30m` | -| `{red}` | `\033[31m` | -| `{green}` | `\033[32m` | -| `{yellow}` | `\033[33m` | -| `{blue}` | `\033[34m` | -| `{magenta}` | `\033[35m` | -| `{cyan}` | `\033[36m` | -| `{white}` | `\033[37m` | -| `{gray}`, `{grey}` | `\033[90m` | - -Bright colors: -| Token | ANSI Code | -|-------|-----------| -| `{bright:black}` | `\033[90m` | -| `{bright:red}` | `\033[91m` | -| `{bright:green}` | `\033[92m` | -| `{bright:yellow}` | `\033[93m` | -| `{bright:blue}` | `\033[94m` | -| `{bright:magenta}` | `\033[95m` | -| `{bright:cyan}` | `\033[96m` | -| `{bright:white}` | `\033[97m` | - -256-color palette: -| Token | ANSI Code | -|-------|-----------| -| `{fg:N}` | `\033[38;5;Nm` where N is 0-255 | - -### Background Colors - -Standard colors: -| Token | ANSI Code | -|-------|-----------| -| `{bg:black}` | `\033[40m` | -| `{bg:red}` | `\033[41m` | -| `{bg:green}` | `\033[42m` | -| `{bg:yellow}` | `\033[43m` | -| `{bg:blue}` | `\033[44m` | -| `{bg:magenta}` | `\033[45m` | -| `{bg:cyan}` | `\033[46m` | -| `{bg:white}` | `\033[47m` | -| `{bg:gray}`, `{bg:grey}` | `\033[100m` | - -256-color palette: -| Token | ANSI Code | -|-------|-----------| -| `{bg:N}` | `\033[48;5;Nm` where N is 0-255 | - -### Reset/Pop Tokens - -| Token | Effect | -|-------|--------| -| `{/}` | Pop one level, restore previous style | -| `{reset}` | Full reset, clear entire stack | -| `{/fg}` | Reset foreground color to default | -| `{/bg}` | Reset background color to default | -| `{/name}` | Same as `{/}` - name is ignored (e.g., `{/highlight}`, `{/b}`) | - -### Control Tokens - -| Token | Effect | ANSI Code | -|-------|--------|-----------| -| `{clr}` | Clear to end of line | `\033[K` | -| `{cls}` | Clear to end of screen | `\033[J` | -| `{home}` | Move cursor to home | `\033[H` | -| `{hide}` | Hide cursor | `\033[?25l` | -| `{show}` | Show cursor | `\033[?25h` | -| `{goto:row,col}` | Move cursor to position | `\033[row;colH` | -| `{goto_cursor}` | Move cursor to `{cursor}` marker position | `\033[row;colH` | - -### Special Tokens - -| Token | Effect | -|-------|--------| -| `{cursor}` | Mark cursor position for cursor tracking | - -## API - -### `zstr_expand_tokens(const char *text)` - -Expands tokens in the input text, returning a zstr with ANSI codes. - -```c -Z_CLEANUP(zstr_free) zstr result = zstr_expand_tokens("{b}Hello{/} World"); -printf("%s\n", zstr_cstr(&result)); -// Output: [bold yellow]Hello[reset] World -``` - -### `zstr_expand_tokens_with_cursor(const char *text)` - -Same as above, but also tracks the `{cursor}` marker position. - -```c -TokenExpansion result = zstr_expand_tokens_with_cursor("Search: {cursor}"); -// result.expanded contains the expanded text -// result.cursor_pos contains the visual column (1-indexed) of {cursor} -zstr_free(&result.expanded); -``` - -### Global Flags - -| Flag | Effect | -|------|--------| -| `zstr_disable_token_expansion` | If true, tokens pass through unchanged | -| `zstr_no_colors` | If true, tokens expand to empty strings (no ANSI codes) | - -## Usage Examples - -### Basic Formatting - -```c -// Simple bold highlight -zstr s = zstr_expand_tokens("Status: {b}OK{/}"); - -// Nested styles -zstr s = zstr_expand_tokens("{bold}Bold {italic}and italic{/} just bold{/} normal"); - -// Colors -zstr s = zstr_expand_tokens("{red}Error:{/} Something went wrong"); -``` - -### 256-Color Support - -```c -// Orange text (color 214) -zstr s = zstr_expand_tokens("{fg:214}Orange text{/}"); - -// Custom background -zstr s = zstr_expand_tokens("{bg:52}Dark red background{/}"); -``` - -### TUI Building - -```c -// Clear line and write header -zstr line = zstr_expand_tokens("{h1}Title{/}{clr}\n"); - -// Status with cursor tracking -TokenExpansion te = zstr_expand_tokens_with_cursor("Input: {cursor}{clr}"); -// Use te.cursor_pos to position the terminal cursor -``` - -## Design Notes - -### Why Ragel? - -Ragel generates highly optimized state machines that are: -- Fast: No backtracking, O(n) parsing -- Compact: Efficient goto-based code generation with `-G2` -- Correct: Formal grammar prevents parsing bugs - -### Stack Implementation - -The stack stores restore actions, not full state. When a token like `{bold}` is applied: -1. The previous bold state (on/off) is pushed onto the stack -2. Bold is enabled -3. When `{/}` is called, the stack is popped and the previous state is restored - -This approach minimizes memory usage and allows efficient restoration. - -### Composite Token Handling - -Tokens like `{b}` that set multiple attributes use a "composite marker" on the stack. When pushed: -1. Individual attribute changes are pushed (bold state, color state) -2. A composite marker is pushed with the count of changes - -When popped: -1. The composite marker is popped -2. All individual changes are popped and restored - -This ensures a single `{/}` can undo a composite style change. diff --git a/src/commands.c b/src/commands.c index f190209..42a59a2 100644 --- a/src/commands.c +++ b/src/commands.c @@ -17,154 +17,94 @@ #include // ============================================================================ -// Script building and execution +// Script execution // ============================================================================ -// Build a script and either execute it (direct mode) or print it (exec mode) -// Returns 0 on success, 1 on failure -static int run_script(const char *script, Mode *mode) { - if (mode->type == MODE_EXEC) { +int run_script(const char *script, bool exec_mode) { + if (!script || !*script) { + return 1; + } + + if (exec_mode) { // Exec mode: print script with header for alias to eval printf(SCRIPT_HEADER); printf("%s", script); return 0; - } else { - // Direct mode: execute via bash, then print cd hint if present - // We run everything except cd (which can't work in subprocess) - // Then print the cd command as a hint - - // Find the cd line (starts with " cd '" on its own line - 2-space indent) - const char *cd_line = NULL; - const char *p = script; - while (*p) { - // Check if this line starts with " cd '" (2-space indent) - if (strncmp(p, " cd '", 6) == 0) { - cd_line = p; - break; - } - // Skip to next line - while (*p && *p != '\n') p++; - if (*p == '\n') p++; - } + } - // Build script without cd line for execution - Z_CLEANUP(zstr_free) zstr exec_script = zstr_init(); - if (cd_line && cd_line != script) { - // Copy everything before the cd line, minus trailing " && \\\n" - size_t len = cd_line - script; - // Remove " && \\\n" from end (6 chars) - if (len >= 6) len -= 6; - zstr_cat_len(&exec_script, script, len); - } else if (!cd_line) { - // No cd, execute whole script - zstr_cat(&exec_script, script); + // Direct mode: execute via bash, then print cd hint if present + // We run everything except cd (which can't work in subprocess) + // Then print the cd command as a hint + + // Find the cd line (starts with " cd '" on its own line - 2-space indent) + const char *cd_line = NULL; + const char *p = script; + while (*p) { + // Check if this line starts with " cd '" (2-space indent) + if (strncmp(p, " cd '", 6) == 0) { + cd_line = p; + break; } - // If cd is the only command, exec_script stays empty - - // Execute the non-cd part if any - if (zstr_len(&exec_script) > 0) { - // Remove trailing newlines/continuations - while (zstr_len(&exec_script) > 0) { - char last = zstr_cstr(&exec_script)[zstr_len(&exec_script) - 1]; - if (last == '\n' || last == '\\' || last == ' ') { - zstr_pop_char(&exec_script); - } else { - break; - } - } - - Z_CLEANUP(zstr_free) zstr cmd = zstr_from("/usr/bin/env bash -c '"); - // Escape single quotes in script - const char *s = zstr_cstr(&exec_script); - while (*s) { - if (*s == '\'') { - zstr_cat(&cmd, "'\\''"); - } else { - zstr_push(&cmd, *s); - } - s++; - } - zstr_cat(&cmd, "'"); + // Skip to next line + while (*p && *p != '\n') p++; + if (*p == '\n') p++; + } - int rc = system(zstr_cstr(&cmd)); - if (rc != 0) { - return 1; + // Build script without cd line for execution + Z_CLEANUP(zstr_free) zstr exec_script = zstr_init(); + if (cd_line && cd_line != script) { + // Copy everything before the cd line, minus trailing " && \\\n" + size_t len = cd_line - script; + // Remove " && \\\n" from end (6 chars) + if (len >= 6) len -= 6; + zstr_cat_len(&exec_script, script, len); + } else if (!cd_line) { + // No cd, execute whole script + zstr_cat(&exec_script, script); + } + // If cd is the only command, exec_script stays empty + + // Execute the non-cd part if any + if (zstr_len(&exec_script) > 0) { + // Remove trailing newlines/continuations + while (zstr_len(&exec_script) > 0) { + char last = zstr_cstr(&exec_script)[zstr_len(&exec_script) - 1]; + if (last == '\n' || last == '\\' || last == ' ') { + zstr_pop_char(&exec_script); + } else { + break; } } - // Print cd hint - if (cd_line) { - const char *path_start = cd_line + 6; // Skip " cd '" - const char *path_end = strchr(path_start, '\''); - if (path_end) { - printf("cd '%.*s'\n", (int)(path_end - path_start), path_start); + Z_CLEANUP(zstr_free) zstr cmd = zstr_from("/usr/bin/env bash -c '"); + // Escape single quotes in script + const char *s = zstr_cstr(&exec_script); + while (*s) { + if (*s == '\'') { + zstr_cat(&cmd, "'\\''"); + } else { + zstr_push(&cmd, *s); } + s++; } + zstr_cat(&cmd, "'"); - return 0; - } -} - -// Helper to generate date-prefixed directory name for clone -// URL format: https://github.com/user/repo.git -> 2025-11-30-user-repo -// git@github.com:user/repo.git -> 2025-11-30-user-repo -static zstr make_clone_dirname(const char *url, const char *name) { - zstr dir_name = zstr_init(); - - time_t now = time(NULL); - struct tm *t = localtime(&now); - char date_prefix[20]; - strftime(date_prefix, sizeof(date_prefix), "%Y-%m-%d", t); - zstr_cat(&dir_name, date_prefix); - zstr_cat(&dir_name, "-"); - - if (name) { - zstr_cat(&dir_name, name); - } else { - // Extract user/repo from URL - // Find the repo name (after last / or :) - const char *last_slash = strrchr(url, '/'); - const char *last_colon = strrchr(url, ':'); - const char *repo_start = last_slash ? last_slash + 1 : - (last_colon ? last_colon + 1 : url); - - // Find the user name (between second-to-last separator and last separator) - const char *user_start = NULL; - const char *user_end = NULL; - - if (last_slash && last_slash > url) { - // Walk back to find the previous / or : - const char *p = last_slash - 1; - while (p > url && *p != '/' && *p != ':') p--; - if (*p == '/' || *p == ':') { - user_start = p + 1; - user_end = last_slash; - } - } else if (last_colon && last_colon > url) { - // git@github.com:user/repo format - user is between : and / - user_start = last_colon + 1; - const char *slash_after = strchr(user_start, '/'); - if (slash_after) { - user_end = slash_after; - } + int rc = system(zstr_cstr(&cmd)); + if (rc != 0) { + return 1; } + } - // Append user- if found - if (user_start && user_end && user_end > user_start) { - zstr_cat_len(&dir_name, user_start, user_end - user_start); - zstr_cat(&dir_name, "-"); - } - - // Append repo name (strip .git suffix) - const char *dot_git = strstr(repo_start, ".git"); - if (dot_git) { - zstr_cat_len(&dir_name, repo_start, dot_git - repo_start); - } else { - zstr_cat(&dir_name, repo_start); + // Print cd hint + if (cd_line) { + const char *path_start = cd_line + 6; // Skip " cd '" + const char *path_end = strchr(path_start, '\''); + if (path_end) { + printf("cd '%.*s'\n", (int)(path_end - path_start), path_start); } } - return dir_name; + return 0; } // ============================================================================ @@ -218,6 +158,14 @@ static zstr build_clone_script(const char *url, const char *path) { return script; } +static zstr build_worktree_script(const char *worktree_path) { + zstr script = zstr_init(); + Z_CLEANUP(zstr_free) zstr escaped_path = shell_escape(worktree_path); + zstr_fmt(&script, "git worktree add %s && \\\n", zstr_cstr(&escaped_path)); + zstr_fmt(&script, " cd %s\n", zstr_cstr(&escaped_path)); + return script; +} + static zstr build_delete_script(const char *base_path, vec_zstr *names) { zstr script = zstr_init(); @@ -250,6 +198,102 @@ static zstr build_delete_script(const char *base_path, vec_zstr *names) { return script; } +// Helper to generate date-prefixed directory name for clone +// URL format: https://github.com/user/repo.git -> 2025-11-30-user-repo +// git@github.com:user/repo.git -> 2025-11-30-user-repo +static zstr make_clone_dirname(const char *url, const char *name) { + zstr dir_name = zstr_init(); + + time_t now = time(NULL); + struct tm *t = localtime(&now); + char date_prefix[20]; + strftime(date_prefix, sizeof(date_prefix), "%Y-%m-%d", t); + zstr_cat(&dir_name, date_prefix); + zstr_cat(&dir_name, "-"); + + if (name) { + zstr_cat(&dir_name, name); + } else { + // Extract user/repo from URL + // Find the repo name (after last / or :) + const char *last_slash = strrchr(url, '/'); + const char *last_colon = strrchr(url, ':'); + const char *repo_start = last_slash ? last_slash + 1 : + (last_colon ? last_colon + 1 : url); + + // Find the user name (between second-to-last separator and last separator) + const char *user_start = NULL; + const char *user_end = NULL; + + if (last_slash && last_slash > url) { + // Walk back to find the previous / or : + const char *p = last_slash - 1; + while (p > url && *p != '/' && *p != ':') p--; + if (*p == '/' || *p == ':') { + user_start = p + 1; + user_end = last_slash; + } + } else if (last_colon && last_colon > url) { + // git@github.com:user/repo format - user is between : and / + user_start = last_colon + 1; + const char *slash_after = strchr(user_start, '/'); + if (slash_after) { + user_end = slash_after; + } + } + + // Append user- if found + if (user_start && user_end && user_end > user_start) { + zstr_cat_len(&dir_name, user_start, user_end - user_start); + zstr_cat(&dir_name, "-"); + } + + // Append repo name (strip .git suffix) + const char *dot_git = strstr(repo_start, ".git"); + if (dot_git) { + zstr_cat_len(&dir_name, repo_start, dot_git - repo_start); + } else { + zstr_cat(&dir_name, repo_start); + } + } + + return dir_name; +} + +static bool is_in_git_repo(void) { + // Check if .git exists in current directory or any parent + char cwd[4096]; + if (getcwd(cwd, sizeof(cwd)) == NULL) return false; + + Z_CLEANUP(zstr_free) zstr path = zstr_from(cwd); + + while (zstr_len(&path) > 0) { + Z_CLEANUP(zstr_free) zstr git_path = zstr_dup(&path); + zstr_cat(&git_path, "/.git"); + + if (access(zstr_cstr(&git_path), F_OK) == 0) return true; + + // Go up one directory + char *p = zstr_data(&path); + char *last_slash = strrchr(p, '/'); + + if (last_slash == p) { + // At root + return access("/.git", F_OK) == 0; + } + + if (last_slash) { + *last_slash = '\0'; + // Update length + if (path.is_long) path.l.len = last_slash - p; + else path.s.len = last_slash - p; + } else { + break; + } + } + return false; +} + // ============================================================================ // Init command - outputs shell function definition // ============================================================================ @@ -326,13 +370,13 @@ void cmd_init(int argc, char **argv, const char *tries_path) { } // ============================================================================ -// Clone command +// Clone command - returns script // ============================================================================ -int cmd_clone(int argc, char **argv, const char *tries_path, Mode *mode) { +zstr cmd_clone(int argc, char **argv, const char *tries_path) { if (argc < 1) { fprintf(stderr, "Usage: try clone [name]\n"); - return 1; + return zstr_init(); // Empty = error } const char *url = argv[0]; @@ -340,61 +384,18 @@ int cmd_clone(int argc, char **argv, const char *tries_path, Mode *mode) { Z_CLEANUP(zstr_free) zstr dir_name = make_clone_dirname(url, name); Z_CLEANUP(zstr_free) zstr full_path = join_path(tries_path, zstr_cstr(&dir_name)); - Z_CLEANUP(zstr_free) zstr script = build_clone_script(url, zstr_cstr(&full_path)); - return run_script(zstr_cstr(&script), mode); + return build_clone_script(url, zstr_cstr(&full_path)); } // ============================================================================ -// Worktree command +// Worktree command - returns script // ============================================================================ -static zstr build_worktree_script(const char *worktree_path) { - zstr script = zstr_init(); - Z_CLEANUP(zstr_free) zstr escaped_path = shell_escape(worktree_path); - zstr_fmt(&script, "git worktree add %s && \\\n", zstr_cstr(&escaped_path)); - zstr_fmt(&script, " cd %s\n", zstr_cstr(&escaped_path)); - return script; -} - -static bool is_in_git_repo(void) { - // Check if .git exists in current directory or any parent - char cwd[4096]; - if (getcwd(cwd, sizeof(cwd)) == NULL) return false; - - Z_CLEANUP(zstr_free) zstr path = zstr_from(cwd); - - while (zstr_len(&path) > 0) { - Z_CLEANUP(zstr_free) zstr git_path = zstr_dup(&path); - zstr_cat(&git_path, "/.git"); - - if (access(zstr_cstr(&git_path), F_OK) == 0) return true; - - // Go up one directory - char *p = zstr_data(&path); - char *last_slash = strrchr(p, '/'); - - if (last_slash == p) { - // At root - return access("/.git", F_OK) == 0; - } - - if (last_slash) { - *last_slash = '\0'; - // Update length - if (path.is_long) path.l.len = last_slash - p; - else path.s.len = last_slash - p; - } else { - break; - } - } - return false; -} - -int cmd_worktree(int argc, char **argv, const char *tries_path, Mode *mode) { +zstr cmd_worktree(int argc, char **argv, const char *tries_path) { if (argc < 1) { fprintf(stderr, "Usage: try worktree \n"); - return 1; + return zstr_init(); // Empty = error } const char *name = argv[0]; @@ -413,115 +414,105 @@ int cmd_worktree(int argc, char **argv, const char *tries_path, Mode *mode) { // Check if we're in a git repo if (is_in_git_repo()) { - Z_CLEANUP(zstr_free) zstr script = build_worktree_script(zstr_cstr(&full_path)); - return run_script(zstr_cstr(&script), mode); + return build_worktree_script(zstr_cstr(&full_path)); } else { // Not in a git repo, just mkdir - Z_CLEANUP(zstr_free) zstr script = build_mkdir_script(zstr_cstr(&full_path)); - return run_script(zstr_cstr(&script), mode); + return build_mkdir_script(zstr_cstr(&full_path)); } } // ============================================================================ -// Selector command (interactive directory picker) +// Selector command - returns script // ============================================================================ -int cmd_selector(int argc, char **argv, const char *tries_path, Mode *mode) { +zstr cmd_selector(int argc, char **argv, const char *tries_path, TestParams *test) { const char *initial_filter = (argc > 0) ? argv[0] : NULL; - SelectionResult result = run_selector(tries_path, initial_filter, mode); + SelectionResult result = run_selector(tries_path, initial_filter, test); + + zstr script = zstr_init(); if (result.type == ACTION_CD) { - Z_CLEANUP(zstr_free) zstr script = build_cd_script(zstr_cstr(&result.path)); - zstr_free(&result.path); - return run_script(zstr_cstr(&script), mode); + script = build_cd_script(zstr_cstr(&result.path)); } else if (result.type == ACTION_MKDIR) { - Z_CLEANUP(zstr_free) zstr script = build_mkdir_script(zstr_cstr(&result.path)); - zstr_free(&result.path); - return run_script(zstr_cstr(&script), mode); + script = build_mkdir_script(zstr_cstr(&result.path)); } else if (result.type == ACTION_DELETE) { - Z_CLEANUP(zstr_free) zstr script = build_delete_script(tries_path, &result.delete_names); + script = build_delete_script(tries_path, &result.delete_names); // Free the delete_names vector zstr *iter; vec_foreach(&result.delete_names, iter) { zstr_free(iter); } vec_free_zstr(&result.delete_names); - zstr_free(&result.path); - return run_script(zstr_cstr(&script), mode); } else { - // Cancelled - zstr_free(&result.path); - printf("Cancelled.\n"); - return 1; + // Cancelled - return empty script but print message + fprintf(stderr, "Cancelled.\n"); } + + zstr_free(&result.path); + return script; } // ============================================================================ -// Exec mode entry point +// Route subcommands (for exec mode or main routing) // ============================================================================ -int cmd_exec(int argc, char **argv, const char *tries_path, Mode *mode) { +zstr cmd_route(int argc, char **argv, const char *tries_path, TestParams *test) { // No subcommand = interactive selector if (argc == 0) { - return cmd_selector(0, NULL, tries_path, mode); + return cmd_selector(0, NULL, tries_path, test); } const char *subcmd = argv[0]; // Handle flags that may be passed through shell wrapper - // (normally handled by main.c, but handle here as fallback) if (strcmp(subcmd, "--version") == 0 || strcmp(subcmd, "-v") == 0) { printf("try %s\n", TRY_VERSION); - return 0; + return zstr_init(); // Empty but not an error } if (strcmp(subcmd, "--help") == 0 || strcmp(subcmd, "-h") == 0) { - // Return non-zero to signal shell wrapper to not eval the help output - // (help is printed to stderr by main.c, so we just exit here) - return 1; + // Help handled by main.c + return zstr_init(); } if (strcmp(subcmd, "--no-colors") == 0) { - zstr_no_colors = true; + extern bool tui_no_colors; + tui_no_colors = true; // Continue with remaining args - return cmd_exec(argc - 1, argv + 1, tries_path, mode); - } - if (strcmp(subcmd, "--no-expand-tokens") == 0) { - zstr_disable_token_expansion = true; - return cmd_exec(argc - 1, argv + 1, tries_path, mode); + return cmd_route(argc - 1, argv + 1, tries_path, test); } if (strcmp(subcmd, "init") == 0) { - // Delegate to init command + // Init always prints directly cmd_init(argc - 1, argv + 1, tries_path); - return 0; + return zstr_init(); } else if (strcmp(subcmd, "cd") == 0) { // Check if argument is a URL (clone shorthand) if (argc > 1 && (strncmp(argv[1], "https://", 8) == 0 || strncmp(argv[1], "http://", 7) == 0 || strncmp(argv[1], "git@", 4) == 0)) { - return cmd_clone(argc - 1, argv + 1, tries_path, mode); + return cmd_clone(argc - 1, argv + 1, tries_path); } // Explicit cd command - return cmd_selector(argc - 1, argv + 1, tries_path, mode); + return cmd_selector(argc - 1, argv + 1, tries_path, test); } else if (strcmp(subcmd, "clone") == 0) { - return cmd_clone(argc - 1, argv + 1, tries_path, mode); + return cmd_clone(argc - 1, argv + 1, tries_path); } else if (strcmp(subcmd, "worktree") == 0) { - return cmd_worktree(argc - 1, argv + 1, tries_path, mode); + return cmd_worktree(argc - 1, argv + 1, tries_path); } else if (strncmp(subcmd, "https://", 8) == 0 || strncmp(subcmd, "http://", 7) == 0 || strncmp(subcmd, "git@", 4) == 0) { // URL shorthand for clone - return cmd_clone(argc, argv, tries_path, mode); + return cmd_clone(argc, argv, tries_path); } else if (strcmp(subcmd, ".") == 0) { // Dot shorthand for worktree (requires name) if (argc < 2) { fprintf(stderr, "Usage: try . \n"); fprintf(stderr, "The name argument is required for worktree creation.\n"); - return 1; + return zstr_init(); } - return cmd_worktree(argc - 1, argv + 1, tries_path, mode); + return cmd_worktree(argc - 1, argv + 1, tries_path); } else { // Treat as query for selector (cd is default) - return cmd_selector(argc, argv, tries_path, mode); + return cmd_selector(argc, argv, tries_path, test); } } diff --git a/src/commands.h b/src/commands.h index 2ce39bb..249f2d7 100644 --- a/src/commands.h +++ b/src/commands.h @@ -6,19 +6,20 @@ // Script header for exec mode output #define SCRIPT_HEADER "# if you can read this, you didn't launch try from an alias. run try --help.\n" -// Init command - outputs shell function definition +// Init command - outputs shell function definition (always prints directly) void cmd_init(int argc, char **argv, const char *tries_path); -// Clone command (works in both modes) -int cmd_clone(int argc, char **argv, const char *tries_path, Mode *mode); +// Commands return shell scripts to execute +// Returns empty zstr on error (after printing error to stderr) +zstr cmd_clone(int argc, char **argv, const char *tries_path); +zstr cmd_worktree(int argc, char **argv, const char *tries_path); +zstr cmd_selector(int argc, char **argv, const char *tries_path, TestParams *test); -// Worktree command (works in both modes) -int cmd_worktree(int argc, char **argv, const char *tries_path, Mode *mode); +// Route subcommands (for exec mode) +zstr cmd_route(int argc, char **argv, const char *tries_path, TestParams *test); -// Exec mode entry point (routes to selector or subcommands) -int cmd_exec(int argc, char **argv, const char *tries_path, Mode *mode); - -// Interactive selector -int cmd_selector(int argc, char **argv, const char *tries_path, Mode *mode); +// Execute or print a script +// exec_mode: true = print with header, false = execute via bash +int run_script(const char *script, bool exec_mode); #endif // COMMANDS_H diff --git a/src/fuzzy.c b/src/fuzzy.c index d7263b3..35cc76f 100644 --- a/src/fuzzy.c +++ b/src/fuzzy.c @@ -1,6 +1,6 @@ #include "fuzzy.h" #include "tui.h" -#include "utils.h" +#include "tui.h" #include #include #include @@ -19,8 +19,8 @@ void fuzzy_match(TryEntry *entry, const char *query) { // Reset score entry->score = 0.0; - // Reset rendered string (reuse capacity if possible) - zstr_clear(&entry->rendered); + // Style string for proper nesting (dark date section + match highlights) + TuiStyleString ss = tui_start_zstr(&entry->rendered); const char *text = zstr_cstr(&entry->name); @@ -28,10 +28,10 @@ void fuzzy_match(TryEntry *entry, const char *query) { if (!query || !*query) { // Check for date prefix and render with dimming if (has_date_prefix(text)) { - // Render date prefix (YYYY-MM-DD-) with {dark}, including the trailing dash - zstr_cat(&entry->rendered, "{dark}"); + // Render date prefix (YYYY-MM-DD-) with dark color, including the trailing dash + tui_push(&ss, TUI_DARK); zstr_cat_len(&entry->rendered, text, 11); // Date + dash is 11 chars - zstr_cat(&entry->rendered, "{/fg}"); + tui_pop(&ss); zstr_cat(&entry->rendered, text + 11); // Rest after dash } else { zstr_cat(&entry->rendered, text); @@ -73,7 +73,7 @@ void fuzzy_match(TryEntry *entry, const char *query) { while (*t_ptr) { // Handle date prefix dimming (including the trailing dash at position 10) if (has_date && current_pos == 0) { - zstr_cat(&entry->rendered, "{dark}"); + tui_push(&ss, TUI_DARK); in_date_section = true; } @@ -95,18 +95,18 @@ void fuzzy_match(TryEntry *entry, const char *query) { last_pos = current_pos; query_idx++; - // Append highlighted char (bold+yellow) - zstr_cat(&entry->rendered, "{highlight}"); - zstr_push(&entry->rendered, *orig_ptr); - zstr_cat(&entry->rendered, "{/}"); + // Append highlighted char (yellow fg, preserves dark if in date section) + tui_push(&ss, TUI_MATCH); + tui_putc(&ss, *orig_ptr); + tui_pop(&ss); } else { // No match, append regular char - zstr_push(&entry->rendered, *orig_ptr); + tui_putc(&ss, *orig_ptr); } // Close dim section after the trailing dash (position 10) if (has_date && current_pos == 10 && in_date_section) { - zstr_cat(&entry->rendered, "{/fg}"); + tui_pop(&ss); in_date_section = false; } diff --git a/src/fuzzy.h b/src/fuzzy.h index b6556d3..eea50b7 100644 --- a/src/fuzzy.h +++ b/src/fuzzy.h @@ -5,13 +5,10 @@ #include // Updates entry->score and entry->rendered in-place +// Rendered string contains ANSI codes for highlighting matched characters void fuzzy_match(TryEntry *entry, const char *query); // Legacy/Convenience: just calculate score (read-only) float calculate_score(const char *text, const char *query, time_t mtime); -// Highlight matching characters in text with {highlight} tokens -// Caller must free the returned string -char *highlight_matches(const char *text, const char *query); - #endif // FUZZY_H diff --git a/src/main.c b/src/main.c index 6962a86..57e4210 100644 --- a/src/main.c +++ b/src/main.c @@ -8,40 +8,107 @@ #include "commands.h" #include "config.h" #include "utils.h" +#include "tui.h" #include #include #include +// Global flag for disabling colors (shared with other modules) +bool tui_no_colors = false; + // Compact help for direct mode static void print_help(void) { Z_CLEANUP(zstr_free) zstr default_path = get_default_tries_path(); - Z_CLEANUP(zstr_free) zstr help = zstr_from( - "{h1}try{/} v" TRY_VERSION " - ephemeral workspace manager\n\n" - "{h1}To use try, add to your shell config:{/}\n\n" - " {bright:blue}# bash/zsh (~/.bashrc or ~/.zshrc){/}\n" - " eval \"$(try init ~/src/tries)\"\n\n" - " {bright:blue}# fish (~/.config/fish/config.fish){/}\n" - " eval (try init ~/src/tries | string collect)\n\n" - "{h1}Commands:{/}\n" - " {b}try{/} [query|url] {dim}Interactive selector, or clone if URL{/}\n" - " {b}try clone{/} {dim}Clone repo into dated directory{/}\n" - " {b}try worktree{/} {dim}Create worktree from current git repo{/}\n" - " {b}try exec{/} [query] {dim}Output shell script (for manual eval){/}\n" - " {b}try --help{/} {dim}Show this help{/}\n\n" - "{h1}Defaults:{/}\n" - " Path: {b}~/src/tries{/} (override with {b}--path{/} on init)\n" - " Current: {b}"); - zstr_cat(&help, zstr_cstr(&default_path)); - zstr_cat(&help, "{/}\n\n" - "{h1}Examples:{/}\n" - " try clone https://github.com/user/repo.git {bright:blue}# YYYY-MM-DD-user-repo{/}\n" - " try clone https://github.com/user/repo.git foo {bright:blue}# YYYY-MM-DD-foo{/}\n" - " try https://github.com/user/repo.git {bright:blue}# shorthand for clone{/}\n" - " try ./my-project worktree feature {bright:blue}# YYYY-MM-DD-feature{/}\n"); - - Z_CLEANUP(zstr_free) zstr expanded = zstr_expand_tokens(zstr_cstr(&help)); - fprintf(stderr, "%s", zstr_cstr(&expanded)); + Z_CLEANUP(zstr_free) zstr help = zstr_init(); + + // Title + tui_zstr_printf(&help, TUI_H1, "try"); + zstr_cat(&help, " v" TRY_VERSION " - ephemeral workspace manager\n\n"); + + // Setup section + tui_zstr_printf(&help, TUI_H1, "To use try, add to your shell config:"); + zstr_cat(&help, "\n\n"); + + zstr_cat(&help, " "); + tui_zstr_printf(&help, ANSI_BRIGHT_BLUE, "# bash/zsh (~/.bashrc or ~/.zshrc)"); + zstr_cat(&help, "\n"); + zstr_cat(&help, " eval \"$(try init ~/src/tries)\"\n\n"); + + zstr_cat(&help, " "); + tui_zstr_printf(&help, ANSI_BRIGHT_BLUE, "# fish (~/.config/fish/config.fish)"); + zstr_cat(&help, "\n"); + zstr_cat(&help, " eval (try init ~/src/tries | string collect)\n\n"); + + // Commands section + tui_zstr_printf(&help, TUI_H1, "Commands:"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " "); + tui_zstr_printf(&help, TUI_BOLD, "try"); + zstr_cat(&help, " [query|url] "); + tui_zstr_printf(&help, TUI_DIM, "Interactive selector, or clone if URL"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " "); + tui_zstr_printf(&help, TUI_BOLD, "try clone"); + zstr_cat(&help, " "); + tui_zstr_printf(&help, TUI_DIM, "Clone repo into dated directory"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " "); + tui_zstr_printf(&help, TUI_BOLD, "try worktree"); + zstr_cat(&help, " "); + tui_zstr_printf(&help, TUI_DIM, "Create worktree from current git repo"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " "); + tui_zstr_printf(&help, TUI_BOLD, "try exec"); + zstr_cat(&help, " [query] "); + tui_zstr_printf(&help, TUI_DIM, "Output shell script (for manual eval)"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " "); + tui_zstr_printf(&help, TUI_BOLD, "try --help"); + zstr_cat(&help, " "); + tui_zstr_printf(&help, TUI_DIM, "Show this help"); + zstr_cat(&help, "\n\n"); + + // Defaults section + tui_zstr_printf(&help, TUI_H1, "Defaults:"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " Path: "); + tui_zstr_printf(&help, TUI_BOLD, "~/src/tries"); + zstr_cat(&help, " (override with "); + tui_zstr_printf(&help, TUI_BOLD, "--path"); + zstr_cat(&help, " on init)\n"); + + zstr_cat(&help, " Current: "); + tui_zstr_printf(&help, TUI_BOLD, zstr_cstr(&default_path)); + zstr_cat(&help, "\n\n"); + + // Examples section + tui_zstr_printf(&help, TUI_H1, "Examples:"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " try clone https://github.com/user/repo.git "); + tui_zstr_printf(&help, ANSI_BRIGHT_BLUE, "# YYYY-MM-DD-user-repo"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " try clone https://github.com/user/repo.git foo "); + tui_zstr_printf(&help, ANSI_BRIGHT_BLUE, "# YYYY-MM-DD-foo"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " try https://github.com/user/repo.git "); + tui_zstr_printf(&help, ANSI_BRIGHT_BLUE, "# shorthand for clone"); + zstr_cat(&help, "\n"); + + zstr_cat(&help, " try ./my-project worktree feature "); + tui_zstr_printf(&help, ANSI_BRIGHT_BLUE, "# YYYY-MM-DD-feature"); + zstr_cat(&help, "\n"); + + fprintf(stderr, "%s", zstr_cstr(&help)); } // Parse a --flag=value or --flag value option, returns value or NULL @@ -71,11 +138,12 @@ int main(int argc, char **argv) { // Check NO_COLOR environment variable (https://no-color.org/) if (getenv("NO_COLOR") != NULL) { - zstr_no_colors = true; + tui_no_colors = true; } - // Mode configuration - Mode mode = {.type = MODE_DIRECT}; + // Testing parameters (only used for automated tests) + TestParams test = {0}; + bool exec_mode = false; // Parse arguments - options can appear anywhere for (int i = 1; i < argc; i++) { @@ -94,15 +162,11 @@ int main(int argc, char **argv) { return 0; } if (strcmp(arg, "--no-colors") == 0) { - zstr_no_colors = true; - continue; - } - if (strcmp(arg, "--no-expand-tokens") == 0) { - zstr_disable_token_expansion = true; + tui_no_colors = true; continue; } if (strcmp(arg, "--and-exit") == 0) { - mode.render_once = true; + test.render_once = true; continue; } @@ -114,7 +178,7 @@ int main(int argc, char **argv) { continue; } if ((value = parse_option_value(arg, next, "--and-keys", &skip))) { - mode.inject_keys = value; + test.inject_keys = value; i += skip; continue; } @@ -156,23 +220,48 @@ int main(int argc, char **argv) { cmd_init((int)cmd_args.length - 1, cmd_args.data + 1, path_cstr); return 0; } else if (strcmp(command, "exec") == 0) { - // Exec mode - mode.type = MODE_EXEC; - return cmd_exec((int)cmd_args.length - 1, cmd_args.data + 1, path_cstr, &mode); + // Exec mode - route subcommand and print script + exec_mode = true; + Z_CLEANUP(zstr_free) zstr script = cmd_route( + (int)cmd_args.length - 1, cmd_args.data + 1, path_cstr, &test); + if (zstr_is_empty(&script)) { + return 1; // Error or special case (like init) + } + return run_script(zstr_cstr(&script), exec_mode); } else if (strcmp(command, "cd") == 0) { // Direct mode cd (interactive selector) - return cmd_selector((int)cmd_args.length - 1, cmd_args.data + 1, path_cstr, &mode); + Z_CLEANUP(zstr_free) zstr script = cmd_selector( + (int)cmd_args.length - 1, cmd_args.data + 1, path_cstr, &test); + if (zstr_is_empty(&script)) { + return 1; + } + return run_script(zstr_cstr(&script), exec_mode); } else if (strcmp(command, "clone") == 0) { // Direct mode clone - return cmd_clone((int)cmd_args.length - 1, cmd_args.data + 1, path_cstr, &mode); + Z_CLEANUP(zstr_free) zstr script = cmd_clone( + (int)cmd_args.length - 1, cmd_args.data + 1, path_cstr); + if (zstr_is_empty(&script)) { + return 1; + } + return run_script(zstr_cstr(&script), exec_mode); } else if (strcmp(command, "worktree") == 0) { // Direct mode worktree - return cmd_worktree((int)cmd_args.length - 1, cmd_args.data + 1, path_cstr, &mode); + Z_CLEANUP(zstr_free) zstr script = cmd_worktree( + (int)cmd_args.length - 1, cmd_args.data + 1, path_cstr); + if (zstr_is_empty(&script)) { + return 1; + } + return run_script(zstr_cstr(&script), exec_mode); } else if (strncmp(command, "https://", 8) == 0 || strncmp(command, "http://", 7) == 0 || strncmp(command, "git@", 4) == 0) { // URL shorthand for clone: try = try clone - return cmd_clone((int)cmd_args.length, cmd_args.data, path_cstr, &mode); + Z_CLEANUP(zstr_free) zstr script = cmd_clone( + (int)cmd_args.length, cmd_args.data, path_cstr); + if (zstr_is_empty(&script)) { + return 1; + } + return run_script(zstr_cstr(&script), exec_mode); } else { // Unknown command - show help fprintf(stderr, "Unknown command: %s\n\n", command); diff --git a/src/terminal.c b/src/terminal.c index 8ccf163..6925700 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -46,6 +46,11 @@ static struct termios orig_termios; static int raw_mode_enabled = 0; static int alternate_screen_enabled = 0; +// Window size cache +static int cached_rows = 0; +static int cached_cols = 0; +static int window_size_valid = 0; + /* * Emergency cleanup - called on signal or atexit * Ensures terminal is always restored even on abnormal exit @@ -82,6 +87,28 @@ void disable_raw_mode(void) { } } +void tui_drain_input(void) { + // Consume any remaining bytes in the input buffer + // (e.g., leftover escape sequences from mouse events) + struct termios current; + if (tcgetattr(STDIN_FILENO, ¤t) != 0) + return; + + // Need raw mode to read individual bytes (not line-buffered) + struct termios drain = current; + drain.c_lflag &= ~(ICANON | ECHO); // Disable canonical mode + drain.c_cc[VMIN] = 0; + drain.c_cc[VTIME] = 1; // 0.1s timeout to catch late-arriving bytes + tcsetattr(STDIN_FILENO, TCSANOW, &drain); + + char discard; + while (read(STDIN_FILENO, &discard, 1) == 1) { + // Consume all pending input + } + + tcsetattr(STDIN_FILENO, TCSANOW, ¤t); +} + void enable_raw_mode(void) { if (tcgetattr(STDIN_FILENO, &orig_termios) == -1) return; // Not a TTY? @@ -137,8 +164,10 @@ int read_key(void) { if (nread == -1) { if (errno == EAGAIN) continue; - if (errno == EINTR) + if (errno == EINTR) { + window_size_valid = 0; // Invalidate cache on resize return KEY_RESIZE; // Signal interrupted read (likely SIGWINCH) + } return -1; } if (nread == 0) { @@ -182,6 +211,39 @@ int read_key(void) { tcsetattr(STDIN_FILENO, TCSANOW, &original_state); if (seq[0] == '[') { + // SGR mouse: \x1b[= '0' && seq[1] <= '9') { if (read(STDIN_FILENO, &seq[2], 1) != 1) return KEY_UNKNOWN; @@ -202,35 +264,23 @@ int read_key(void) { case '8': return END_KEY; } - } - // Extended sequence like \x1b[1;5B (Ctrl+Down) - consume rest and ignore - if (seq[2] == ';') { - char discard; - // Read modifier and final character (e.g., "5B") - while (read(STDIN_FILENO, &discard, 1) == 1) { - if (discard >= 'A' && discard <= 'Z') - break; - if (discard >= 'a' && discard <= 'z') - break; - if (discard == '~') - break; - } return KEY_UNKNOWN; } - } else { - switch (seq[1]) { - case 'A': - return ARROW_UP; - case 'B': - return ARROW_DOWN; - case 'C': - return ARROW_RIGHT; - case 'D': - return ARROW_LEFT; - case 'H': - return HOME_KEY; - case 'F': - return END_KEY; + // Any other CSI sequence starting with digit - consume until terminator + // CSI sequences end with a byte in range 0x40-0x7E (@ through ~) + char last = seq[2]; + while (!(last >= 0x40 && last <= 0x7E)) { + if (read(STDIN_FILENO, &last, 1) != 1) + break; + } + return KEY_UNKNOWN; + } + // Any other unrecognized CSI sequence - consume until terminator + if (!(seq[1] >= 0x40 && seq[1] <= 0x7E)) { + char last = seq[1]; + while (!(last >= 0x40 && last <= 0x7E)) { + if (read(STDIN_FILENO, &last, 1) != 1) + break; } } } else if (seq[0] == 'O') { @@ -248,6 +298,13 @@ int read_key(void) { } int get_window_size(int *rows, int *cols) { + // Return cached values if valid + if (window_size_valid) { + *rows = cached_rows; + *cols = cached_cols; + return 0; + } + // Check TRY_WIDTH/TRY_HEIGHT env vars first (for testing) const char *env_width = getenv("TRY_WIDTH"); const char *env_height = getenv("TRY_HEIGHT"); @@ -255,13 +312,13 @@ int get_window_size(int *rows, int *cols) { *cols = atoi(env_width); *rows = atoi(env_height); if (*cols > 0 && *rows > 0) { - return 0; + goto cache_and_return; } } else if (env_width) { *cols = atoi(env_width); if (*cols > 0) { *rows = 24; // Default height - return 0; + goto cache_and_return; } } @@ -271,7 +328,7 @@ int get_window_size(int *rows, int *cols) { if (ioctl(STDERR_FILENO, TIOCGWINSZ, &ws) != -1 && ws.ws_col != 0) { *cols = ws.ws_col; *rows = ws.ws_row; - return 0; + goto cache_and_return; } // 2. Try tput @@ -283,7 +340,7 @@ int get_window_size(int *rows, int *cols) { if (fp) { if (fscanf(fp, "%d", rows) == 1) { pclose(fp); - return 0; + goto cache_and_return; } pclose(fp); } @@ -295,6 +352,11 @@ int get_window_size(int *rows, int *cols) { // 3. Fallback defaults *cols = 80; *rows = 24; + +cache_and_return: + cached_rows = *rows; + cached_cols = *cols; + window_size_valid = 1; return 0; } diff --git a/src/terminal.h b/src/terminal.h index 85fa473..39f2eb4 100644 --- a/src/terminal.h +++ b/src/terminal.h @@ -23,6 +23,7 @@ enum EditorKey { void enable_raw_mode(void); void disable_raw_mode(void); +void tui_drain_input(void); // Consume remaining stdin after TUI exit int get_window_size(int *rows, int *cols); int read_key(void); void enable_alternate_screen(void); diff --git a/src/tokens.c b/src/tokens.c deleted file mode 100644 index cf72158..0000000 --- a/src/tokens.c +++ /dev/null @@ -1,3942 +0,0 @@ - -#line 1 "src/tokens.rl" -/* - * Token expansion state machine - Ragel grammar - * - * This provides a stack-based token system for ANSI escape code generation. - * Tokens like {b}, {dim}, {red} push state, and {/} pops to restore. - * - * Generated with: ragel -C -G2 tokens.rl -o tokens.c - */ - -#include "tokens.h" -#include "zstr.h" -#include -#include -#include - -/* Maximum nesting depth for style stack */ -#define MAX_STACK_DEPTH 32 - -/* - * Global style configuration for semantic tokens. - * These can be overridden at runtime to customize the appearance. - * Format: ANSI escape sequence string (without the leading \033[) - */ -const char *token_style_h1 = "1m\033[38;5;214"; /* Bold + orange (256-color 214) */ -const char *token_style_h2 = "1;34"; /* Bold + blue */ -const char *token_style_h3 = "1;37"; /* Bold + white */ -const char *token_style_h4 = "1;37"; /* Bold + white */ -const char *token_style_h5 = "1;37"; /* Bold + white */ -const char *token_style_h6 = "1;37"; /* Bold + white */ -const char *token_style_b = "1"; /* Bold (same as strong) */ -const char *token_style_strong = "1"; /* Bold */ -const char *token_style_highlight = "1;33"; /* Bold + yellow */ -const char *token_style_dim = "37"; /* White (softer than bright white) */ -const char *token_style_danger = "48;5;52"; /* Dark red bg */ - -/* Style attribute types */ -typedef enum { - ATTR_NONE = 0, - ATTR_BOLD, - ATTR_DIM, - ATTR_ITALIC, - ATTR_UNDERLINE, - ATTR_REVERSE, - ATTR_STRIKETHROUGH, - ATTR_FG, /* Foreground color */ - ATTR_BG, /* Background color */ - ATTR_COMPOSITE, /* Multiple attributes (for semantic tokens) */ -} AttrType; - -/* Represents a single attribute value */ -typedef struct { - AttrType type; - int value; /* For colors: ANSI code, for bools: 0/1 */ -} AttrValue; - -/* Stack entry - stores what to restore when popping */ -typedef struct { - AttrType type; - int prev_value; - int count; /* For composite: how many individual attrs were pushed */ -} StackEntry; - -/* Parser state */ -typedef struct { - zstr *out; /* Output buffer */ - StackEntry stack[MAX_STACK_DEPTH]; - int stack_depth; - int cursor_col; /* Visual column of {cursor} (1-indexed), -1 if no cursor */ - int cursor_row; /* Visual row of {cursor} (1-indexed), -1 if no cursor */ - bool has_cursor; /* True if {cursor} was encountered */ - int visual_col; /* Current visual column */ - int visual_row; /* Current visual row */ - bool no_colors; /* If true, don't emit ANSI codes */ - bool disabled; /* If true, pass through without expansion */ - - /* Desired style state (what tokens request) */ - int fg_color; /* 0 = default, else ANSI code */ - int bg_color; - bool bold; - bool dim; - bool italic; - bool underline; - bool reverse; - bool strikethrough; - - /* Emitted style state (what terminal currently has) */ - int emitted_fg; - int emitted_bg; - bool emitted_bold; - bool emitted_dim; - bool emitted_italic; - bool emitted_underline; - bool emitted_reverse; - bool emitted_strikethrough; - bool dirty; /* True if desired != emitted */ -} TokenParser; - -/* ANSI escape code helpers */ -static void emit_ansi(TokenParser *p, const char *code) { - if (!p->no_colors) { - zstr_cat(p->out, code); - } -} - -static void emit_ansi_num(TokenParser *p, const char *prefix, int n, const char *suffix) { - if (!p->no_colors) { - char buf[32]; - snprintf(buf, sizeof(buf), "%s%d%s", prefix, n, suffix); - zstr_cat(p->out, buf); - } -} - -/* Mark state as dirty (deferred emission) */ -static void mark_dirty(TokenParser *p) { - p->dirty = true; -} - -/* Sync emitted state with desired state - emit codes only when outputting a character */ -static void sync_styles(TokenParser *p) { - if (!p->dirty || p->no_colors) return; - - /* Check if we need a full reset (going back to default state) */ - bool need_reset = false; - - /* If any attribute is turning OFF, we need reset then re-apply actives */ - if ((p->emitted_bold && !p->bold) || - (p->emitted_dim && !p->dim) || - (p->emitted_italic && !p->italic) || - (p->emitted_underline && !p->underline) || - (p->emitted_reverse && !p->reverse) || - (p->emitted_strikethrough && !p->strikethrough) || - (p->emitted_fg != 0 && p->fg_color == 0) || - (p->emitted_bg != 0 && p->bg_color == 0)) { - need_reset = true; - } - - if (need_reset) { - /* Reset and re-apply all active styles */ - zstr_cat(p->out, "\033[0"); - if (p->bold) zstr_cat(p->out, ";1"); - if (p->dim) zstr_cat(p->out, ";2"); - if (p->italic) zstr_cat(p->out, ";3"); - if (p->underline) zstr_cat(p->out, ";4"); - if (p->reverse) zstr_cat(p->out, ";7"); - if (p->strikethrough) zstr_cat(p->out, ";9"); - if (p->fg_color != 0) { - char buf[20]; - if (p->fg_color >= 1000 && p->fg_color < 2000) { - /* 256-color foreground */ - snprintf(buf, sizeof(buf), ";38;5;%d", p->fg_color - 1000); - } else { - snprintf(buf, sizeof(buf), ";%d", p->fg_color); - } - zstr_cat(p->out, buf); - } - if (p->bg_color != 0) { - char buf[20]; - if (p->bg_color >= 2000) { - /* 256-color background */ - snprintf(buf, sizeof(buf), ";48;5;%d", p->bg_color - 2000); - } else { - snprintf(buf, sizeof(buf), ";%d", p->bg_color); - } - zstr_cat(p->out, buf); - } - zstr_cat(p->out, "m"); - } else { - /* Apply only changed attributes (turning ON) */ - bool first = true; - char buf[64]; - buf[0] = '\0'; - - if (p->bold && !p->emitted_bold) { - strcat(buf, first ? "1" : ";1"); first = false; - } - if (p->dim && !p->emitted_dim) { - strcat(buf, first ? "2" : ";2"); first = false; - } - if (p->italic && !p->emitted_italic) { - strcat(buf, first ? "3" : ";3"); first = false; - } - if (p->underline && !p->emitted_underline) { - strcat(buf, first ? "4" : ";4"); first = false; - } - if (p->reverse && !p->emitted_reverse) { - strcat(buf, first ? "7" : ";7"); first = false; - } - if (p->strikethrough && !p->emitted_strikethrough) { - strcat(buf, first ? "9" : ";9"); first = false; - } - if (p->fg_color != p->emitted_fg && p->fg_color != 0) { - char tmp[20]; - if (p->fg_color >= 1000 && p->fg_color < 2000) { - snprintf(tmp, sizeof(tmp), "%s38;5;%d", first ? "" : ";", p->fg_color - 1000); - } else { - snprintf(tmp, sizeof(tmp), "%s%d", first ? "" : ";", p->fg_color); - } - strcat(buf, tmp); first = false; - } - if (p->bg_color != p->emitted_bg && p->bg_color != 0) { - char tmp[20]; - if (p->bg_color >= 2000) { - snprintf(tmp, sizeof(tmp), "%s48;5;%d", first ? "" : ";", p->bg_color - 2000); - } else { - snprintf(tmp, sizeof(tmp), "%s%d", first ? "" : ";", p->bg_color); - } - strcat(buf, tmp); first = false; - } - - if (!first) { - zstr_cat(p->out, "\033["); - zstr_cat(p->out, buf); - zstr_cat(p->out, "m"); - } - } - - /* Update emitted state to match desired */ - p->emitted_bold = p->bold; - p->emitted_dim = p->dim; - p->emitted_italic = p->italic; - p->emitted_underline = p->underline; - p->emitted_reverse = p->reverse; - p->emitted_strikethrough = p->strikethrough; - p->emitted_fg = p->fg_color; - p->emitted_bg = p->bg_color; - p->dirty = false; -} - -/* Push a restore entry onto the stack */ -static void push_attr(TokenParser *p, AttrType type, int prev_value) { - if (p->stack_depth < MAX_STACK_DEPTH) { - p->stack[p->stack_depth].type = type; - p->stack[p->stack_depth].prev_value = prev_value; - p->stack[p->stack_depth].count = 1; - p->stack_depth++; - } -} - -/* Push a composite (multiple attrs at once) */ -static void push_composite(TokenParser *p, int count) { - if (p->stack_depth < MAX_STACK_DEPTH) { - p->stack[p->stack_depth].type = ATTR_COMPOSITE; - p->stack[p->stack_depth].prev_value = 0; - p->stack[p->stack_depth].count = count; - p->stack_depth++; - } -} - -/* Restore an attribute to its previous value (deferred - just updates state) */ -static void restore_attr(TokenParser *p, AttrType type, int prev_value) { - switch (type) { - case ATTR_BOLD: - p->bold = prev_value; - break; - case ATTR_DIM: - p->dim = prev_value; - break; - case ATTR_ITALIC: - p->italic = prev_value; - break; - case ATTR_UNDERLINE: - p->underline = prev_value; - break; - case ATTR_REVERSE: - p->reverse = prev_value; - break; - case ATTR_STRIKETHROUGH: - p->strikethrough = prev_value; - break; - case ATTR_FG: - p->fg_color = prev_value; - break; - case ATTR_BG: - p->bg_color = prev_value; - break; - default: - break; - } -} - -/* Pop one entry from the stack and restore */ -static void pop_style(TokenParser *p) { - if (p->stack_depth > 0) { - p->stack_depth--; - StackEntry *e = &p->stack[p->stack_depth]; - - if (e->type == ATTR_COMPOSITE) { - /* Pop multiple individual entries */ - for (int i = 0; i < e->count && p->stack_depth > 0; i++) { - p->stack_depth--; - StackEntry *inner = &p->stack[p->stack_depth]; - restore_attr(p, inner->type, inner->prev_value); - } - } else { - restore_attr(p, e->type, e->prev_value); - } - mark_dirty(p); - } -} - -/* Check if any styles are active */ -static bool has_active_styles(TokenParser *p) { - return p->bold || p->dim || p->italic || p->underline || - p->reverse || p->strikethrough || p->fg_color != 0 || p->bg_color != 0; -} - -/* Reset all styles and clear stack (deferred - just updates state) */ -static void reset_all(TokenParser *p) { - p->fg_color = 0; - p->bg_color = 0; - p->bold = false; - p->dim = false; - p->italic = false; - p->underline = false; - p->reverse = false; - p->strikethrough = false; - p->stack_depth = 0; - mark_dirty(p); -} - -/* Reset all styles at newline - emits immediately since newline follows */ -static void reset_line_styles(TokenParser *p) { - /* Check if emitted state has any active styles */ - if (p->emitted_bold || p->emitted_dim || p->emitted_italic || - p->emitted_underline || p->emitted_reverse || p->emitted_strikethrough || - p->emitted_fg != 0 || p->emitted_bg != 0) { - emit_ansi(p, "\033[0m"); - /* Update emitted state */ - p->emitted_bold = false; - p->emitted_dim = false; - p->emitted_italic = false; - p->emitted_underline = false; - p->emitted_reverse = false; - p->emitted_strikethrough = false; - p->emitted_fg = 0; - p->emitted_bg = 0; - } - /* Reset desired state too */ - p->fg_color = 0; - p->bg_color = 0; - p->bold = false; - p->dim = false; - p->italic = false; - p->underline = false; - p->reverse = false; - p->strikethrough = false; - p->dirty = false; - /* Keep stack_depth as-is so {/} still works across lines if needed */ -} - -/* Style application functions - deferred emission (just update state) */ -static void apply_bold(TokenParser *p) { - push_attr(p, ATTR_BOLD, p->bold); - p->bold = true; - mark_dirty(p); -} - -static void apply_dim(TokenParser *p) { - push_attr(p, ATTR_DIM, p->dim); - p->dim = true; - mark_dirty(p); -} - -static void apply_italic(TokenParser *p) { - push_attr(p, ATTR_ITALIC, p->italic); - p->italic = true; - mark_dirty(p); -} - -static void apply_underline(TokenParser *p) { - push_attr(p, ATTR_UNDERLINE, p->underline); - p->underline = true; - mark_dirty(p); -} - -static void apply_reverse(TokenParser *p) { - push_attr(p, ATTR_REVERSE, p->reverse); - p->reverse = true; - mark_dirty(p); -} - -static void apply_strikethrough(TokenParser *p) { - push_attr(p, ATTR_STRIKETHROUGH, p->strikethrough); - p->strikethrough = true; - mark_dirty(p); -} - -static void apply_fg_code(TokenParser *p, int code) { - push_attr(p, ATTR_FG, p->fg_color); - p->fg_color = code; - mark_dirty(p); -} - -static void apply_fg_256(TokenParser *p, int n) { - push_attr(p, ATTR_FG, p->fg_color); - p->fg_color = 1000 + n; /* Marker for 256-color */ - mark_dirty(p); -} - -static void apply_bg_code(TokenParser *p, int code) { - push_attr(p, ATTR_BG, p->bg_color); - p->bg_color = code; - mark_dirty(p); -} - -static void apply_bg_256(TokenParser *p, int n) { - push_attr(p, ATTR_BG, p->bg_color); - p->bg_color = 2000 + n; /* Marker for 256-color bg */ - mark_dirty(p); -} - -/* Semantic token applications - deferred emission (just update state) */ -static void apply_token_b(TokenParser *p) { - /* {b} = bold only (same as strong) */ - push_attr(p, ATTR_BOLD, p->bold); - p->bold = true; - mark_dirty(p); -} - -static void apply_token_highlight(TokenParser *p) { - /* {highlight} = bold + yellow */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 33; /* Yellow */ - mark_dirty(p); -} - -static void apply_token_h1(TokenParser *p) { - /* {h1} = bold + orange (256-color 214) */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 1214; /* 256-color marker for 214 */ - mark_dirty(p); -} - -static void apply_token_h2(TokenParser *p) { - /* {h2} = bold + blue */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 34; /* Blue */ - mark_dirty(p); -} - -static void apply_token_h3(TokenParser *p) { - /* {h3} = bold + white */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 37; /* White */ - mark_dirty(p); -} - -static void apply_token_h4(TokenParser *p) { - /* {h4} = bold + white */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 37; - mark_dirty(p); -} - -static void apply_token_h5(TokenParser *p) { - /* {h5} = bold + white */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 37; - mark_dirty(p); -} - -static void apply_token_h6(TokenParser *p) { - /* {h6} = bold + white */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 37; - mark_dirty(p); -} - -static void apply_token_strong(TokenParser *p) { - /* {strong} = bold */ - push_attr(p, ATTR_BOLD, p->bold); - p->bold = true; - mark_dirty(p); -} - -static void apply_token_dim_style(TokenParser *p) { - /* {dim} = white (standard color, softer than bright white) */ - push_attr(p, ATTR_FG, p->fg_color); - p->fg_color = 37; /* White */ - mark_dirty(p); -} - -static void apply_token_dark_style(TokenParser *p) { - /* {dark} = 256-color 245 (medium gray, for TUI secondary text) */ - push_attr(p, ATTR_FG, p->fg_color); - p->fg_color = 1245; /* 256-color marker for 245 */ - mark_dirty(p); -} - -static void apply_token_section(TokenParser *p) { - /* {section} = bold + subtle dark gray background (256-color 237) */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_BG, p->bg_color); - push_composite(p, 2); - p->bold = true; - p->bg_color = 2237; /* 256-color marker for 237 */ - mark_dirty(p); -} - -static void apply_token_danger(TokenParser *p) { - /* {danger} = dark red background (256-color 52) */ - push_attr(p, ATTR_BG, p->bg_color); - p->bg_color = 2052; /* 256-color marker for 52 */ - mark_dirty(p); -} - -static void apply_token_text(TokenParser *p) { - /* {text} = full reset */ - reset_all(p); -} - -/* Brighten current foreground color */ -static void apply_bright(TokenParser *p) { - push_attr(p, ATTR_FG, p->fg_color); - /* If current color is 30-37 (standard), convert to 90-97 (bright) */ - if (p->fg_color >= 30 && p->fg_color <= 37) { - p->fg_color = p->fg_color + 60; - } else { - /* Already bright or custom - just set bright white */ - p->fg_color = 97; - } - mark_dirty(p); -} - -/* Parse a number from string, returns -1 on failure */ -static int parse_number(const char *start, const char *end) { - int n = 0; - for (const char *p = start; p < end; p++) { - if (*p < '0' || *p > '9') return -1; - n = n * 10 + (*p - '0'); - } - return n; -} - - -#line 560 "src/tokens.c" -static const int token_parser_start = 244; -static const int token_parser_first_final = 244; -static const int token_parser_error = 0; - -static const int token_parser_en_main = 244; - - -#line 897 "src/tokens.rl" - - -/* Global flags */ -bool zstr_disable_token_expansion = false; -bool zstr_no_colors = false; - -TokenExpansion expand_tokens_with_cursor(const char *text) { - TokenExpansion result = {0}; - result.expanded = zstr_init(); - result.cursor_col = -1; - result.cursor_row = -1; - result.has_cursor = false; - result.final_col = 1; - result.final_row = 1; - - if (!text || !*text) { - return result; - } - - /* If expansion is disabled, just copy */ - if (zstr_disable_token_expansion) { - zstr_cat(&result.expanded, text); - return result; - } - - /* Pre-reserve buffer space to avoid reallocations. - * Estimate: input length + overhead for ANSI codes (~50% extra) */ - size_t input_len = strlen(text); - zstr_reserve(&result.expanded, input_len + input_len / 2 + 64); - - /* Initialize parser state */ - TokenParser parser = {0}; - parser.out = &result.expanded; - parser.cursor_col = -1; - parser.cursor_row = -1; - parser.visual_col = 1; - parser.visual_row = 1; - parser.no_colors = zstr_no_colors; - - /* Ragel variables */ - int cs; - int act; - const char *p = text; - const char *pe = text + strlen(text); - const char *eof = pe; - const char *ts = NULL; /* Token start */ - const char *te = NULL; /* Token end */ - const char *tok_start = NULL; - const char *tok_end = NULL; - const char *goto_row_start = NULL; - const char *goto_row_end = NULL; - const char *goto_col_start = NULL; - const char *goto_col_end = NULL; - - (void)tok_start; - (void)tok_end; - (void)goto_row_start; - (void)goto_row_end; - (void)goto_col_start; - (void)goto_col_end; - (void)eof; - (void)ts; - (void)te; - (void)act; - (void)token_parser_en_main; - - -#line 636 "src/tokens.c" - { - cs = token_parser_start; - ts = 0; - te = 0; - act = 0; - } - -#line 964 "src/tokens.rl" - -#line 646 "src/tokens.c" - { - if ( p == pe ) - goto _test_eof; - switch ( cs ) - { -tr2: -#line 868 "src/tokens.rl" - {te = p+1;{ - for (const char *c = ts; c < te; c++) { - zstr_push(parser.out, *c); - } - }} - goto st244; -tr3: -#line 855 "src/tokens.rl" - {{p = ((te))-1;}{ - sync_styles(&parser); /* Emit any pending style changes */ - zstr_push(parser.out, *ts); - parser.visual_col++; - }} - goto st244; -tr294: -#line 855 "src/tokens.rl" - {te = p+1;{ - sync_styles(&parser); /* Emit any pending style changes */ - zstr_push(parser.out, *ts); - parser.visual_col++; - }} - goto st244; -tr295: -#line 861 "src/tokens.rl" - {te = p+1;{ - /* Auto-reset all active styles before newline */ - reset_line_styles(&parser); - zstr_push(parser.out, *ts); - parser.visual_col = 1; - }} - goto st244; -tr298: -#line 855 "src/tokens.rl" - {te = p;p--;{ - sync_styles(&parser); /* Emit any pending style changes */ - zstr_push(parser.out, *ts); - parser.visual_col++; - }} - goto st244; -tr314: -#line 591 "src/tokens.rl" - { pop_style(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr315: -#line 591 "src/tokens.rl" - { pop_style(&parser); } -#line 598 "src/tokens.rl" - { - push_attr(&parser, ATTR_BG, parser.bg_color); - parser.bg_color = 0; - mark_dirty(&parser); - } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr316: -#line 591 "src/tokens.rl" - { pop_style(&parser); } -#line 593 "src/tokens.rl" - { - push_attr(&parser, ATTR_FG, parser.fg_color); - parser.fg_color = 0; - mark_dirty(&parser); - } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr317: -#line 621 "src/tokens.rl" - { apply_bold(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr318: -#line 622 "src/tokens.rl" - { apply_italic(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr319: -#line 623 "src/tokens.rl" - { apply_underline(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr320: -#line 668 "src/tokens.rl" - { - int n = parse_number(tok_start, tok_end); - if (n >= 0 && n <= 255) { - apply_bg_256(&parser, n); - } - } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr321: -#line 650 "src/tokens.rl" - { apply_bg_code(&parser, 40); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr322: -#line 654 "src/tokens.rl" - { apply_bg_code(&parser, 44); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr323: -#line 656 "src/tokens.rl" - { apply_bg_code(&parser, 46); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr324: -#line 658 "src/tokens.rl" - { apply_bg_code(&parser, 100); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr325: -#line 652 "src/tokens.rl" - { apply_bg_code(&parser, 42); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr326: -#line 655 "src/tokens.rl" - { apply_bg_code(&parser, 45); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr327: -#line 651 "src/tokens.rl" - { apply_bg_code(&parser, 41); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr328: -#line 657 "src/tokens.rl" - { apply_bg_code(&parser, 47); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr329: -#line 653 "src/tokens.rl" - { apply_bg_code(&parser, 43); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr330: -#line 629 "src/tokens.rl" - { apply_fg_code(&parser, 30); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr331: -#line 633 "src/tokens.rl" - { apply_fg_code(&parser, 34); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr332: -#line 640 "src/tokens.rl" - { apply_fg_code(&parser, 90); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr333: -#line 644 "src/tokens.rl" - { apply_fg_code(&parser, 94); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr334: -#line 646 "src/tokens.rl" - { apply_fg_code(&parser, 96); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr335: -#line 642 "src/tokens.rl" - { apply_fg_code(&parser, 92); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr336: -#line 645 "src/tokens.rl" - { apply_fg_code(&parser, 95); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr337: -#line 641 "src/tokens.rl" - { apply_fg_code(&parser, 91); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr338: -#line 647 "src/tokens.rl" - { apply_fg_code(&parser, 97); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr339: -#line 643 "src/tokens.rl" - { apply_fg_code(&parser, 93); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr340: -#line 626 "src/tokens.rl" - { apply_bright(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr341: -#line 605 "src/tokens.rl" - { apply_token_b(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr342: -#line 748 "src/tokens.rl" - { emit_ansi(&parser, "\033[K"); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr343: -#line 749 "src/tokens.rl" - { emit_ansi(&parser, "\033[J"); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr344: -#line 578 "src/tokens.rl" - { - parser.cursor_col = parser.visual_col; - parser.cursor_row = parser.visual_row; - parser.has_cursor = true; - } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr345: -#line 635 "src/tokens.rl" - { apply_fg_code(&parser, 36); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr346: -#line 617 "src/tokens.rl" - { apply_token_danger(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr347: -#line 615 "src/tokens.rl" - { apply_token_dark_style(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr348: -#line 614 "src/tokens.rl" - { apply_token_dim_style(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr349: -#line 661 "src/tokens.rl" - { - int n = parse_number(tok_start, tok_end); - if (n >= 0 && n <= 255) { - apply_fg_256(&parser, n); - } - } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr350: -#line 759 "src/tokens.rl" - { - int row = parse_number(goto_row_start, goto_row_end); - int col = parse_number(goto_col_start, goto_col_end); - if (row >= 0 && col >= 0) { - emit_ansi_num(&parser, "\033[", row, ";"); - emit_ansi_num(&parser, "", col, "H"); - } - } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr351: -#line 584 "src/tokens.rl" - { - if (parser.cursor_row > 0 && parser.cursor_col > 0) { - emit_ansi_num(&parser, "\033[", parser.cursor_row, ";"); - emit_ansi_num(&parser, "", parser.cursor_col, "H"); - } - } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr352: -#line 637 "src/tokens.rl" - { apply_fg_code(&parser, 90); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr353: -#line 631 "src/tokens.rl" - { apply_fg_code(&parser, 32); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr354: -#line 607 "src/tokens.rl" - { apply_token_h1(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr355: -#line 608 "src/tokens.rl" - { apply_token_h2(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr356: -#line 609 "src/tokens.rl" - { apply_token_h3(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr357: -#line 610 "src/tokens.rl" - { apply_token_h4(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr358: -#line 611 "src/tokens.rl" - { apply_token_h5(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr359: -#line 612 "src/tokens.rl" - { apply_token_h6(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr360: -#line 751 "src/tokens.rl" - { emit_ansi(&parser, "\033[?25l"); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr361: -#line 606 "src/tokens.rl" - { apply_token_highlight(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr362: -#line 750 "src/tokens.rl" - { emit_ansi(&parser, "\033[H"); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr363: -#line 634 "src/tokens.rl" - { apply_fg_code(&parser, 35); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr364: -#line 630 "src/tokens.rl" - { apply_fg_code(&parser, 31); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr365: -#line 592 "src/tokens.rl" - { reset_all(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr366: -#line 624 "src/tokens.rl" - { apply_reverse(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr367: -#line 616 "src/tokens.rl" - { apply_token_section(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr368: -#line 752 "src/tokens.rl" - { emit_ansi(&parser, "\033[?25h"); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr369: -#line 625 "src/tokens.rl" - { apply_strikethrough(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr370: -#line 613 "src/tokens.rl" - { apply_token_strong(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr371: -#line 618 "src/tokens.rl" - { apply_token_text(&parser); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr372: -#line 636 "src/tokens.rl" - { apply_fg_code(&parser, 37); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -tr373: -#line 632 "src/tokens.rl" - { apply_fg_code(&parser, 33); } -#line 889 "src/tokens.rl" - {te = p;p--;} - goto st244; -st244: -#line 1 "NONE" - {ts = 0;} - if ( ++p == pe ) - goto _test_eof244; -case 244: -#line 1 "NONE" - {ts = p;} -#line 1099 "src/tokens.c" - switch( (*p) ) { - case 10: goto tr295; - case 27: goto st1; - case 123: goto tr297; - } - goto tr294; -st1: - if ( ++p == pe ) - goto _test_eof1; -case 1: - if ( (*p) == 91 ) - goto st2; - goto st0; -st0: -cs = 0; - goto _out; -st2: - if ( ++p == pe ) - goto _test_eof2; -case 2: - if ( (*p) > 90 ) { - if ( 97 <= (*p) && (*p) <= 122 ) - goto tr2; - } else if ( (*p) >= 65 ) - goto tr2; - goto st2; -tr297: -#line 1 "NONE" - {te = p+1;} - goto st245; -st245: - if ( ++p == pe ) - goto _test_eof245; -case 245: -#line 1134 "src/tokens.c" - switch( (*p) ) { - case 47: goto st3; - case 66: goto st9; - case 73: goto st10; - case 85: goto st11; - case 98: goto st12; - case 99: goto st106; - case 100: goto st118; - case 102: goto st128; - case 103: goto st132; - case 104: goto st153; - case 105: goto st173; - case 109: goto st178; - case 114: goto st185; - case 115: goto st196; - case 116: goto st221; - case 117: goto st225; - case 119: goto st233; - case 121: goto st238; - } - goto tr298; -st3: - if ( ++p == pe ) - goto _test_eof3; -case 3: - switch( (*p) ) { - case 98: goto st5; - case 102: goto st7; - case 125: goto st246; - } - if ( 97 <= (*p) && (*p) <= 122 ) - goto st4; - goto tr3; -st4: - if ( ++p == pe ) - goto _test_eof4; -case 4: - if ( (*p) == 125 ) - goto st246; - if ( 97 <= (*p) && (*p) <= 122 ) - goto st4; - goto tr3; -st246: - if ( ++p == pe ) - goto _test_eof246; -case 246: - goto tr314; -st5: - if ( ++p == pe ) - goto _test_eof5; -case 5: - switch( (*p) ) { - case 103: goto st6; - case 125: goto st246; - } - if ( 97 <= (*p) && (*p) <= 122 ) - goto st4; - goto tr3; -st6: - if ( ++p == pe ) - goto _test_eof6; -case 6: - if ( (*p) == 125 ) - goto st247; - if ( 97 <= (*p) && (*p) <= 122 ) - goto st4; - goto tr3; -st247: - if ( ++p == pe ) - goto _test_eof247; -case 247: - goto tr315; -st7: - if ( ++p == pe ) - goto _test_eof7; -case 7: - switch( (*p) ) { - case 103: goto st8; - case 125: goto st246; - } - if ( 97 <= (*p) && (*p) <= 122 ) - goto st4; - goto tr3; -st8: - if ( ++p == pe ) - goto _test_eof8; -case 8: - if ( (*p) == 125 ) - goto st248; - if ( 97 <= (*p) && (*p) <= 122 ) - goto st4; - goto tr3; -st248: - if ( ++p == pe ) - goto _test_eof248; -case 248: - goto tr316; -st9: - if ( ++p == pe ) - goto _test_eof9; -case 9: - if ( (*p) == 125 ) - goto st249; - goto tr3; -st249: - if ( ++p == pe ) - goto _test_eof249; -case 249: - goto tr317; -st10: - if ( ++p == pe ) - goto _test_eof10; -case 10: - if ( (*p) == 125 ) - goto st250; - goto tr3; -st250: - if ( ++p == pe ) - goto _test_eof250; -case 250: - goto tr318; -st11: - if ( ++p == pe ) - goto _test_eof11; -case 11: - if ( (*p) == 125 ) - goto st251; - goto tr3; -st251: - if ( ++p == pe ) - goto _test_eof251; -case 251: - goto tr319; -st12: - if ( ++p == pe ) - goto _test_eof12; -case 12: - switch( (*p) ) { - case 103: goto st13; - case 108: goto st55; - case 111: goto st61; - case 114: goto st63; - case 125: goto st273; - } - goto tr3; -st13: - if ( ++p == pe ) - goto _test_eof13; -case 13: - if ( (*p) == 58 ) - goto st14; - goto tr3; -st14: - if ( ++p == pe ) - goto _test_eof14; -case 14: - switch( (*p) ) { - case 98: goto st16; - case 99: goto st23; - case 103: goto st27; - case 109: goto st34; - case 114: goto st41; - case 119: goto st44; - case 121: goto st49; - } - if ( 48 <= (*p) && (*p) <= 57 ) - goto tr21; - goto tr3; -tr21: -#line 562 "src/tokens.rl" - { tok_start = p; } - goto st15; -st15: - if ( ++p == pe ) - goto _test_eof15; -case 15: -#line 1311 "src/tokens.c" - if ( (*p) == 125 ) - goto tr30; - if ( 48 <= (*p) && (*p) <= 57 ) - goto st15; - goto tr3; -tr30: -#line 563 "src/tokens.rl" - { tok_end = p; } - goto st252; -st252: - if ( ++p == pe ) - goto _test_eof252; -case 252: -#line 1325 "src/tokens.c" - goto tr320; -st16: - if ( ++p == pe ) - goto _test_eof16; -case 16: - if ( (*p) == 108 ) - goto st17; - goto tr3; -st17: - if ( ++p == pe ) - goto _test_eof17; -case 17: - switch( (*p) ) { - case 97: goto st18; - case 117: goto st21; - } - goto tr3; -st18: - if ( ++p == pe ) - goto _test_eof18; -case 18: - if ( (*p) == 99 ) - goto st19; - goto tr3; -st19: - if ( ++p == pe ) - goto _test_eof19; -case 19: - if ( (*p) == 107 ) - goto st20; - goto tr3; -st20: - if ( ++p == pe ) - goto _test_eof20; -case 20: - if ( (*p) == 125 ) - goto st253; - goto tr3; -st253: - if ( ++p == pe ) - goto _test_eof253; -case 253: - goto tr321; -st21: - if ( ++p == pe ) - goto _test_eof21; -case 21: - if ( (*p) == 101 ) - goto st22; - goto tr3; -st22: - if ( ++p == pe ) - goto _test_eof22; -case 22: - if ( (*p) == 125 ) - goto st254; - goto tr3; -st254: - if ( ++p == pe ) - goto _test_eof254; -case 254: - goto tr322; -st23: - if ( ++p == pe ) - goto _test_eof23; -case 23: - if ( (*p) == 121 ) - goto st24; - goto tr3; -st24: - if ( ++p == pe ) - goto _test_eof24; -case 24: - if ( (*p) == 97 ) - goto st25; - goto tr3; -st25: - if ( ++p == pe ) - goto _test_eof25; -case 25: - if ( (*p) == 110 ) - goto st26; - goto tr3; -st26: - if ( ++p == pe ) - goto _test_eof26; -case 26: - if ( (*p) == 125 ) - goto st255; - goto tr3; -st255: - if ( ++p == pe ) - goto _test_eof255; -case 255: - goto tr323; -st27: - if ( ++p == pe ) - goto _test_eof27; -case 27: - if ( (*p) == 114 ) - goto st28; - goto tr3; -st28: - if ( ++p == pe ) - goto _test_eof28; -case 28: - switch( (*p) ) { - case 97: goto st29; - case 101: goto st31; - } - goto tr3; -st29: - if ( ++p == pe ) - goto _test_eof29; -case 29: - if ( (*p) == 121 ) - goto st30; - goto tr3; -st30: - if ( ++p == pe ) - goto _test_eof30; -case 30: - if ( (*p) == 125 ) - goto st256; - goto tr3; -st256: - if ( ++p == pe ) - goto _test_eof256; -case 256: - goto tr324; -st31: - if ( ++p == pe ) - goto _test_eof31; -case 31: - switch( (*p) ) { - case 101: goto st32; - case 121: goto st30; - } - goto tr3; -st32: - if ( ++p == pe ) - goto _test_eof32; -case 32: - if ( (*p) == 110 ) - goto st33; - goto tr3; -st33: - if ( ++p == pe ) - goto _test_eof33; -case 33: - if ( (*p) == 125 ) - goto st257; - goto tr3; -st257: - if ( ++p == pe ) - goto _test_eof257; -case 257: - goto tr325; -st34: - if ( ++p == pe ) - goto _test_eof34; -case 34: - if ( (*p) == 97 ) - goto st35; - goto tr3; -st35: - if ( ++p == pe ) - goto _test_eof35; -case 35: - if ( (*p) == 103 ) - goto st36; - goto tr3; -st36: - if ( ++p == pe ) - goto _test_eof36; -case 36: - if ( (*p) == 101 ) - goto st37; - goto tr3; -st37: - if ( ++p == pe ) - goto _test_eof37; -case 37: - if ( (*p) == 110 ) - goto st38; - goto tr3; -st38: - if ( ++p == pe ) - goto _test_eof38; -case 38: - if ( (*p) == 116 ) - goto st39; - goto tr3; -st39: - if ( ++p == pe ) - goto _test_eof39; -case 39: - if ( (*p) == 97 ) - goto st40; - goto tr3; -st40: - if ( ++p == pe ) - goto _test_eof40; -case 40: - if ( (*p) == 125 ) - goto st258; - goto tr3; -st258: - if ( ++p == pe ) - goto _test_eof258; -case 258: - goto tr326; -st41: - if ( ++p == pe ) - goto _test_eof41; -case 41: - if ( (*p) == 101 ) - goto st42; - goto tr3; -st42: - if ( ++p == pe ) - goto _test_eof42; -case 42: - if ( (*p) == 100 ) - goto st43; - goto tr3; -st43: - if ( ++p == pe ) - goto _test_eof43; -case 43: - if ( (*p) == 125 ) - goto st259; - goto tr3; -st259: - if ( ++p == pe ) - goto _test_eof259; -case 259: - goto tr327; -st44: - if ( ++p == pe ) - goto _test_eof44; -case 44: - if ( (*p) == 104 ) - goto st45; - goto tr3; -st45: - if ( ++p == pe ) - goto _test_eof45; -case 45: - if ( (*p) == 105 ) - goto st46; - goto tr3; -st46: - if ( ++p == pe ) - goto _test_eof46; -case 46: - if ( (*p) == 116 ) - goto st47; - goto tr3; -st47: - if ( ++p == pe ) - goto _test_eof47; -case 47: - if ( (*p) == 101 ) - goto st48; - goto tr3; -st48: - if ( ++p == pe ) - goto _test_eof48; -case 48: - if ( (*p) == 125 ) - goto st260; - goto tr3; -st260: - if ( ++p == pe ) - goto _test_eof260; -case 260: - goto tr328; -st49: - if ( ++p == pe ) - goto _test_eof49; -case 49: - if ( (*p) == 101 ) - goto st50; - goto tr3; -st50: - if ( ++p == pe ) - goto _test_eof50; -case 50: - if ( (*p) == 108 ) - goto st51; - goto tr3; -st51: - if ( ++p == pe ) - goto _test_eof51; -case 51: - if ( (*p) == 108 ) - goto st52; - goto tr3; -st52: - if ( ++p == pe ) - goto _test_eof52; -case 52: - if ( (*p) == 111 ) - goto st53; - goto tr3; -st53: - if ( ++p == pe ) - goto _test_eof53; -case 53: - if ( (*p) == 119 ) - goto st54; - goto tr3; -st54: - if ( ++p == pe ) - goto _test_eof54; -case 54: - if ( (*p) == 125 ) - goto st261; - goto tr3; -st261: - if ( ++p == pe ) - goto _test_eof261; -case 261: - goto tr329; -st55: - if ( ++p == pe ) - goto _test_eof55; -case 55: - switch( (*p) ) { - case 97: goto st56; - case 117: goto st59; - } - goto tr3; -st56: - if ( ++p == pe ) - goto _test_eof56; -case 56: - if ( (*p) == 99 ) - goto st57; - goto tr3; -st57: - if ( ++p == pe ) - goto _test_eof57; -case 57: - if ( (*p) == 107 ) - goto st58; - goto tr3; -st58: - if ( ++p == pe ) - goto _test_eof58; -case 58: - if ( (*p) == 125 ) - goto st262; - goto tr3; -st262: - if ( ++p == pe ) - goto _test_eof262; -case 262: - goto tr330; -st59: - if ( ++p == pe ) - goto _test_eof59; -case 59: - if ( (*p) == 101 ) - goto st60; - goto tr3; -st60: - if ( ++p == pe ) - goto _test_eof60; -case 60: - if ( (*p) == 125 ) - goto st263; - goto tr3; -st263: - if ( ++p == pe ) - goto _test_eof263; -case 263: - goto tr331; -st61: - if ( ++p == pe ) - goto _test_eof61; -case 61: - if ( (*p) == 108 ) - goto st62; - goto tr3; -st62: - if ( ++p == pe ) - goto _test_eof62; -case 62: - if ( (*p) == 100 ) - goto st9; - goto tr3; -st63: - if ( ++p == pe ) - goto _test_eof63; -case 63: - if ( (*p) == 105 ) - goto st64; - goto tr3; -st64: - if ( ++p == pe ) - goto _test_eof64; -case 64: - if ( (*p) == 103 ) - goto st65; - goto tr3; -st65: - if ( ++p == pe ) - goto _test_eof65; -case 65: - if ( (*p) == 104 ) - goto st66; - goto tr3; -st66: - if ( ++p == pe ) - goto _test_eof66; -case 66: - if ( (*p) == 116 ) - goto st67; - goto tr3; -st67: - if ( ++p == pe ) - goto _test_eof67; -case 67: - switch( (*p) ) { - case 58: goto st68; - case 125: goto st272; - } - goto tr3; -st68: - if ( ++p == pe ) - goto _test_eof68; -case 68: - switch( (*p) ) { - case 98: goto st69; - case 99: goto st76; - case 103: goto st80; - case 109: goto st85; - case 114: goto st92; - case 119: goto st95; - case 121: goto st100; - } - goto tr3; -st69: - if ( ++p == pe ) - goto _test_eof69; -case 69: - if ( (*p) == 108 ) - goto st70; - goto tr3; -st70: - if ( ++p == pe ) - goto _test_eof70; -case 70: - switch( (*p) ) { - case 97: goto st71; - case 117: goto st74; - } - goto tr3; -st71: - if ( ++p == pe ) - goto _test_eof71; -case 71: - if ( (*p) == 99 ) - goto st72; - goto tr3; -st72: - if ( ++p == pe ) - goto _test_eof72; -case 72: - if ( (*p) == 107 ) - goto st73; - goto tr3; -st73: - if ( ++p == pe ) - goto _test_eof73; -case 73: - if ( (*p) == 125 ) - goto st264; - goto tr3; -st264: - if ( ++p == pe ) - goto _test_eof264; -case 264: - goto tr332; -st74: - if ( ++p == pe ) - goto _test_eof74; -case 74: - if ( (*p) == 101 ) - goto st75; - goto tr3; -st75: - if ( ++p == pe ) - goto _test_eof75; -case 75: - if ( (*p) == 125 ) - goto st265; - goto tr3; -st265: - if ( ++p == pe ) - goto _test_eof265; -case 265: - goto tr333; -st76: - if ( ++p == pe ) - goto _test_eof76; -case 76: - if ( (*p) == 121 ) - goto st77; - goto tr3; -st77: - if ( ++p == pe ) - goto _test_eof77; -case 77: - if ( (*p) == 97 ) - goto st78; - goto tr3; -st78: - if ( ++p == pe ) - goto _test_eof78; -case 78: - if ( (*p) == 110 ) - goto st79; - goto tr3; -st79: - if ( ++p == pe ) - goto _test_eof79; -case 79: - if ( (*p) == 125 ) - goto st266; - goto tr3; -st266: - if ( ++p == pe ) - goto _test_eof266; -case 266: - goto tr334; -st80: - if ( ++p == pe ) - goto _test_eof80; -case 80: - if ( (*p) == 114 ) - goto st81; - goto tr3; -st81: - if ( ++p == pe ) - goto _test_eof81; -case 81: - if ( (*p) == 101 ) - goto st82; - goto tr3; -st82: - if ( ++p == pe ) - goto _test_eof82; -case 82: - if ( (*p) == 101 ) - goto st83; - goto tr3; -st83: - if ( ++p == pe ) - goto _test_eof83; -case 83: - if ( (*p) == 110 ) - goto st84; - goto tr3; -st84: - if ( ++p == pe ) - goto _test_eof84; -case 84: - if ( (*p) == 125 ) - goto st267; - goto tr3; -st267: - if ( ++p == pe ) - goto _test_eof267; -case 267: - goto tr335; -st85: - if ( ++p == pe ) - goto _test_eof85; -case 85: - if ( (*p) == 97 ) - goto st86; - goto tr3; -st86: - if ( ++p == pe ) - goto _test_eof86; -case 86: - if ( (*p) == 103 ) - goto st87; - goto tr3; -st87: - if ( ++p == pe ) - goto _test_eof87; -case 87: - if ( (*p) == 101 ) - goto st88; - goto tr3; -st88: - if ( ++p == pe ) - goto _test_eof88; -case 88: - if ( (*p) == 110 ) - goto st89; - goto tr3; -st89: - if ( ++p == pe ) - goto _test_eof89; -case 89: - if ( (*p) == 116 ) - goto st90; - goto tr3; -st90: - if ( ++p == pe ) - goto _test_eof90; -case 90: - if ( (*p) == 97 ) - goto st91; - goto tr3; -st91: - if ( ++p == pe ) - goto _test_eof91; -case 91: - if ( (*p) == 125 ) - goto st268; - goto tr3; -st268: - if ( ++p == pe ) - goto _test_eof268; -case 268: - goto tr336; -st92: - if ( ++p == pe ) - goto _test_eof92; -case 92: - if ( (*p) == 101 ) - goto st93; - goto tr3; -st93: - if ( ++p == pe ) - goto _test_eof93; -case 93: - if ( (*p) == 100 ) - goto st94; - goto tr3; -st94: - if ( ++p == pe ) - goto _test_eof94; -case 94: - if ( (*p) == 125 ) - goto st269; - goto tr3; -st269: - if ( ++p == pe ) - goto _test_eof269; -case 269: - goto tr337; -st95: - if ( ++p == pe ) - goto _test_eof95; -case 95: - if ( (*p) == 104 ) - goto st96; - goto tr3; -st96: - if ( ++p == pe ) - goto _test_eof96; -case 96: - if ( (*p) == 105 ) - goto st97; - goto tr3; -st97: - if ( ++p == pe ) - goto _test_eof97; -case 97: - if ( (*p) == 116 ) - goto st98; - goto tr3; -st98: - if ( ++p == pe ) - goto _test_eof98; -case 98: - if ( (*p) == 101 ) - goto st99; - goto tr3; -st99: - if ( ++p == pe ) - goto _test_eof99; -case 99: - if ( (*p) == 125 ) - goto st270; - goto tr3; -st270: - if ( ++p == pe ) - goto _test_eof270; -case 270: - goto tr338; -st100: - if ( ++p == pe ) - goto _test_eof100; -case 100: - if ( (*p) == 101 ) - goto st101; - goto tr3; -st101: - if ( ++p == pe ) - goto _test_eof101; -case 101: - if ( (*p) == 108 ) - goto st102; - goto tr3; -st102: - if ( ++p == pe ) - goto _test_eof102; -case 102: - if ( (*p) == 108 ) - goto st103; - goto tr3; -st103: - if ( ++p == pe ) - goto _test_eof103; -case 103: - if ( (*p) == 111 ) - goto st104; - goto tr3; -st104: - if ( ++p == pe ) - goto _test_eof104; -case 104: - if ( (*p) == 119 ) - goto st105; - goto tr3; -st105: - if ( ++p == pe ) - goto _test_eof105; -case 105: - if ( (*p) == 125 ) - goto st271; - goto tr3; -st271: - if ( ++p == pe ) - goto _test_eof271; -case 271: - goto tr339; -st272: - if ( ++p == pe ) - goto _test_eof272; -case 272: - goto tr340; -st273: - if ( ++p == pe ) - goto _test_eof273; -case 273: - goto tr341; -st106: - if ( ++p == pe ) - goto _test_eof106; -case 106: - switch( (*p) ) { - case 108: goto st107; - case 117: goto st110; - case 121: goto st115; - } - goto tr3; -st107: - if ( ++p == pe ) - goto _test_eof107; -case 107: - switch( (*p) ) { - case 114: goto st108; - case 115: goto st109; - } - goto tr3; -st108: - if ( ++p == pe ) - goto _test_eof108; -case 108: - if ( (*p) == 125 ) - goto st274; - goto tr3; -st274: - if ( ++p == pe ) - goto _test_eof274; -case 274: - goto tr342; -st109: - if ( ++p == pe ) - goto _test_eof109; -case 109: - if ( (*p) == 125 ) - goto st275; - goto tr3; -st275: - if ( ++p == pe ) - goto _test_eof275; -case 275: - goto tr343; -st110: - if ( ++p == pe ) - goto _test_eof110; -case 110: - if ( (*p) == 114 ) - goto st111; - goto tr3; -st111: - if ( ++p == pe ) - goto _test_eof111; -case 111: - if ( (*p) == 115 ) - goto st112; - goto tr3; -st112: - if ( ++p == pe ) - goto _test_eof112; -case 112: - if ( (*p) == 111 ) - goto st113; - goto tr3; -st113: - if ( ++p == pe ) - goto _test_eof113; -case 113: - if ( (*p) == 114 ) - goto st114; - goto tr3; -st114: - if ( ++p == pe ) - goto _test_eof114; -case 114: - if ( (*p) == 125 ) - goto st276; - goto tr3; -st276: - if ( ++p == pe ) - goto _test_eof276; -case 276: - goto tr344; -st115: - if ( ++p == pe ) - goto _test_eof115; -case 115: - if ( (*p) == 97 ) - goto st116; - goto tr3; -st116: - if ( ++p == pe ) - goto _test_eof116; -case 116: - if ( (*p) == 110 ) - goto st117; - goto tr3; -st117: - if ( ++p == pe ) - goto _test_eof117; -case 117: - if ( (*p) == 125 ) - goto st277; - goto tr3; -st277: - if ( ++p == pe ) - goto _test_eof277; -case 277: - goto tr345; -st118: - if ( ++p == pe ) - goto _test_eof118; -case 118: - switch( (*p) ) { - case 97: goto st119; - case 105: goto st126; - } - goto tr3; -st119: - if ( ++p == pe ) - goto _test_eof119; -case 119: - switch( (*p) ) { - case 110: goto st120; - case 114: goto st124; - } - goto tr3; -st120: - if ( ++p == pe ) - goto _test_eof120; -case 120: - if ( (*p) == 103 ) - goto st121; - goto tr3; -st121: - if ( ++p == pe ) - goto _test_eof121; -case 121: - if ( (*p) == 101 ) - goto st122; - goto tr3; -st122: - if ( ++p == pe ) - goto _test_eof122; -case 122: - if ( (*p) == 114 ) - goto st123; - goto tr3; -st123: - if ( ++p == pe ) - goto _test_eof123; -case 123: - if ( (*p) == 125 ) - goto st278; - goto tr3; -st278: - if ( ++p == pe ) - goto _test_eof278; -case 278: - goto tr346; -st124: - if ( ++p == pe ) - goto _test_eof124; -case 124: - if ( (*p) == 107 ) - goto st125; - goto tr3; -st125: - if ( ++p == pe ) - goto _test_eof125; -case 125: - if ( (*p) == 125 ) - goto st279; - goto tr3; -st279: - if ( ++p == pe ) - goto _test_eof279; -case 279: - goto tr347; -st126: - if ( ++p == pe ) - goto _test_eof126; -case 126: - if ( (*p) == 109 ) - goto st127; - goto tr3; -st127: - if ( ++p == pe ) - goto _test_eof127; -case 127: - if ( (*p) == 125 ) - goto st280; - goto tr3; -st280: - if ( ++p == pe ) - goto _test_eof280; -case 280: - goto tr348; -st128: - if ( ++p == pe ) - goto _test_eof128; -case 128: - if ( (*p) == 103 ) - goto st129; - goto tr3; -st129: - if ( ++p == pe ) - goto _test_eof129; -case 129: - if ( (*p) == 58 ) - goto st130; - goto tr3; -st130: - if ( ++p == pe ) - goto _test_eof130; -case 130: - if ( 48 <= (*p) && (*p) <= 57 ) - goto tr161; - goto tr3; -tr161: -#line 562 "src/tokens.rl" - { tok_start = p; } - goto st131; -st131: - if ( ++p == pe ) - goto _test_eof131; -case 131: -#line 2308 "src/tokens.c" - if ( (*p) == 125 ) - goto tr163; - if ( 48 <= (*p) && (*p) <= 57 ) - goto st131; - goto tr3; -tr163: -#line 563 "src/tokens.rl" - { tok_end = p; } - goto st281; -st281: - if ( ++p == pe ) - goto _test_eof281; -case 281: -#line 2322 "src/tokens.c" - goto tr349; -st132: - if ( ++p == pe ) - goto _test_eof132; -case 132: - switch( (*p) ) { - case 111: goto st133; - case 114: goto st147; - } - goto tr3; -st133: - if ( ++p == pe ) - goto _test_eof133; -case 133: - if ( (*p) == 116 ) - goto st134; - goto tr3; -st134: - if ( ++p == pe ) - goto _test_eof134; -case 134: - if ( (*p) == 111 ) - goto st135; - goto tr3; -st135: - if ( ++p == pe ) - goto _test_eof135; -case 135: - switch( (*p) ) { - case 58: goto st136; - case 95: goto st140; - } - goto tr3; -st136: - if ( ++p == pe ) - goto _test_eof136; -case 136: - if ( 48 <= (*p) && (*p) <= 57 ) - goto tr170; - goto tr3; -tr170: -#line 755 "src/tokens.rl" - { goto_row_start = p; } - goto st137; -st137: - if ( ++p == pe ) - goto _test_eof137; -case 137: -#line 2371 "src/tokens.c" - if ( (*p) == 44 ) - goto tr171; - if ( 48 <= (*p) && (*p) <= 57 ) - goto st137; - goto tr3; -tr171: -#line 756 "src/tokens.rl" - { goto_row_end = p; } - goto st138; -st138: - if ( ++p == pe ) - goto _test_eof138; -case 138: -#line 2385 "src/tokens.c" - if ( 48 <= (*p) && (*p) <= 57 ) - goto tr173; - goto tr3; -tr173: -#line 757 "src/tokens.rl" - { goto_col_start = p; } - goto st139; -st139: - if ( ++p == pe ) - goto _test_eof139; -case 139: -#line 2397 "src/tokens.c" - if ( (*p) == 125 ) - goto tr175; - if ( 48 <= (*p) && (*p) <= 57 ) - goto st139; - goto tr3; -tr175: -#line 758 "src/tokens.rl" - { goto_col_end = p; } - goto st282; -st282: - if ( ++p == pe ) - goto _test_eof282; -case 282: -#line 2411 "src/tokens.c" - goto tr350; -st140: - if ( ++p == pe ) - goto _test_eof140; -case 140: - if ( (*p) == 99 ) - goto st141; - goto tr3; -st141: - if ( ++p == pe ) - goto _test_eof141; -case 141: - if ( (*p) == 117 ) - goto st142; - goto tr3; -st142: - if ( ++p == pe ) - goto _test_eof142; -case 142: - if ( (*p) == 114 ) - goto st143; - goto tr3; -st143: - if ( ++p == pe ) - goto _test_eof143; -case 143: - if ( (*p) == 115 ) - goto st144; - goto tr3; -st144: - if ( ++p == pe ) - goto _test_eof144; -case 144: - if ( (*p) == 111 ) - goto st145; - goto tr3; -st145: - if ( ++p == pe ) - goto _test_eof145; -case 145: - if ( (*p) == 114 ) - goto st146; - goto tr3; -st146: - if ( ++p == pe ) - goto _test_eof146; -case 146: - if ( (*p) == 125 ) - goto st283; - goto tr3; -st283: - if ( ++p == pe ) - goto _test_eof283; -case 283: - goto tr351; -st147: - if ( ++p == pe ) - goto _test_eof147; -case 147: - switch( (*p) ) { - case 97: goto st148; - case 101: goto st150; - } - goto tr3; -st148: - if ( ++p == pe ) - goto _test_eof148; -case 148: - if ( (*p) == 121 ) - goto st149; - goto tr3; -st149: - if ( ++p == pe ) - goto _test_eof149; -case 149: - if ( (*p) == 125 ) - goto st284; - goto tr3; -st284: - if ( ++p == pe ) - goto _test_eof284; -case 284: - goto tr352; -st150: - if ( ++p == pe ) - goto _test_eof150; -case 150: - switch( (*p) ) { - case 101: goto st151; - case 121: goto st149; - } - goto tr3; -st151: - if ( ++p == pe ) - goto _test_eof151; -case 151: - if ( (*p) == 110 ) - goto st152; - goto tr3; -st152: - if ( ++p == pe ) - goto _test_eof152; -case 152: - if ( (*p) == 125 ) - goto st285; - goto tr3; -st285: - if ( ++p == pe ) - goto _test_eof285; -case 285: - goto tr353; -st153: - if ( ++p == pe ) - goto _test_eof153; -case 153: - switch( (*p) ) { - case 49: goto st154; - case 50: goto st155; - case 51: goto st156; - case 52: goto st157; - case 53: goto st158; - case 54: goto st159; - case 105: goto st160; - case 111: goto st170; - } - goto tr3; -st154: - if ( ++p == pe ) - goto _test_eof154; -case 154: - if ( (*p) == 125 ) - goto st286; - goto tr3; -st286: - if ( ++p == pe ) - goto _test_eof286; -case 286: - goto tr354; -st155: - if ( ++p == pe ) - goto _test_eof155; -case 155: - if ( (*p) == 125 ) - goto st287; - goto tr3; -st287: - if ( ++p == pe ) - goto _test_eof287; -case 287: - goto tr355; -st156: - if ( ++p == pe ) - goto _test_eof156; -case 156: - if ( (*p) == 125 ) - goto st288; - goto tr3; -st288: - if ( ++p == pe ) - goto _test_eof288; -case 288: - goto tr356; -st157: - if ( ++p == pe ) - goto _test_eof157; -case 157: - if ( (*p) == 125 ) - goto st289; - goto tr3; -st289: - if ( ++p == pe ) - goto _test_eof289; -case 289: - goto tr357; -st158: - if ( ++p == pe ) - goto _test_eof158; -case 158: - if ( (*p) == 125 ) - goto st290; - goto tr3; -st290: - if ( ++p == pe ) - goto _test_eof290; -case 290: - goto tr358; -st159: - if ( ++p == pe ) - goto _test_eof159; -case 159: - if ( (*p) == 125 ) - goto st291; - goto tr3; -st291: - if ( ++p == pe ) - goto _test_eof291; -case 291: - goto tr359; -st160: - if ( ++p == pe ) - goto _test_eof160; -case 160: - switch( (*p) ) { - case 100: goto st161; - case 103: goto st163; - } - goto tr3; -st161: - if ( ++p == pe ) - goto _test_eof161; -case 161: - if ( (*p) == 101 ) - goto st162; - goto tr3; -st162: - if ( ++p == pe ) - goto _test_eof162; -case 162: - if ( (*p) == 125 ) - goto st292; - goto tr3; -st292: - if ( ++p == pe ) - goto _test_eof292; -case 292: - goto tr360; -st163: - if ( ++p == pe ) - goto _test_eof163; -case 163: - if ( (*p) == 104 ) - goto st164; - goto tr3; -st164: - if ( ++p == pe ) - goto _test_eof164; -case 164: - if ( (*p) == 108 ) - goto st165; - goto tr3; -st165: - if ( ++p == pe ) - goto _test_eof165; -case 165: - if ( (*p) == 105 ) - goto st166; - goto tr3; -st166: - if ( ++p == pe ) - goto _test_eof166; -case 166: - if ( (*p) == 103 ) - goto st167; - goto tr3; -st167: - if ( ++p == pe ) - goto _test_eof167; -case 167: - if ( (*p) == 104 ) - goto st168; - goto tr3; -st168: - if ( ++p == pe ) - goto _test_eof168; -case 168: - if ( (*p) == 116 ) - goto st169; - goto tr3; -st169: - if ( ++p == pe ) - goto _test_eof169; -case 169: - if ( (*p) == 125 ) - goto st293; - goto tr3; -st293: - if ( ++p == pe ) - goto _test_eof293; -case 293: - goto tr361; -st170: - if ( ++p == pe ) - goto _test_eof170; -case 170: - if ( (*p) == 109 ) - goto st171; - goto tr3; -st171: - if ( ++p == pe ) - goto _test_eof171; -case 171: - if ( (*p) == 101 ) - goto st172; - goto tr3; -st172: - if ( ++p == pe ) - goto _test_eof172; -case 172: - if ( (*p) == 125 ) - goto st294; - goto tr3; -st294: - if ( ++p == pe ) - goto _test_eof294; -case 294: - goto tr362; -st173: - if ( ++p == pe ) - goto _test_eof173; -case 173: - switch( (*p) ) { - case 116: goto st174; - case 125: goto st250; - } - goto tr3; -st174: - if ( ++p == pe ) - goto _test_eof174; -case 174: - if ( (*p) == 97 ) - goto st175; - goto tr3; -st175: - if ( ++p == pe ) - goto _test_eof175; -case 175: - if ( (*p) == 108 ) - goto st176; - goto tr3; -st176: - if ( ++p == pe ) - goto _test_eof176; -case 176: - if ( (*p) == 105 ) - goto st177; - goto tr3; -st177: - if ( ++p == pe ) - goto _test_eof177; -case 177: - if ( (*p) == 99 ) - goto st10; - goto tr3; -st178: - if ( ++p == pe ) - goto _test_eof178; -case 178: - if ( (*p) == 97 ) - goto st179; - goto tr3; -st179: - if ( ++p == pe ) - goto _test_eof179; -case 179: - if ( (*p) == 103 ) - goto st180; - goto tr3; -st180: - if ( ++p == pe ) - goto _test_eof180; -case 180: - if ( (*p) == 101 ) - goto st181; - goto tr3; -st181: - if ( ++p == pe ) - goto _test_eof181; -case 181: - if ( (*p) == 110 ) - goto st182; - goto tr3; -st182: - if ( ++p == pe ) - goto _test_eof182; -case 182: - if ( (*p) == 116 ) - goto st183; - goto tr3; -st183: - if ( ++p == pe ) - goto _test_eof183; -case 183: - if ( (*p) == 97 ) - goto st184; - goto tr3; -st184: - if ( ++p == pe ) - goto _test_eof184; -case 184: - if ( (*p) == 125 ) - goto st295; - goto tr3; -st295: - if ( ++p == pe ) - goto _test_eof295; -case 295: - goto tr363; -st185: - if ( ++p == pe ) - goto _test_eof185; -case 185: - if ( (*p) == 101 ) - goto st186; - goto tr3; -st186: - if ( ++p == pe ) - goto _test_eof186; -case 186: - switch( (*p) ) { - case 100: goto st187; - case 115: goto st188; - case 118: goto st191; - } - goto tr3; -st187: - if ( ++p == pe ) - goto _test_eof187; -case 187: - if ( (*p) == 125 ) - goto st296; - goto tr3; -st296: - if ( ++p == pe ) - goto _test_eof296; -case 296: - goto tr364; -st188: - if ( ++p == pe ) - goto _test_eof188; -case 188: - if ( (*p) == 101 ) - goto st189; - goto tr3; -st189: - if ( ++p == pe ) - goto _test_eof189; -case 189: - if ( (*p) == 116 ) - goto st190; - goto tr3; -st190: - if ( ++p == pe ) - goto _test_eof190; -case 190: - if ( (*p) == 125 ) - goto st297; - goto tr3; -st297: - if ( ++p == pe ) - goto _test_eof297; -case 297: - goto tr365; -st191: - if ( ++p == pe ) - goto _test_eof191; -case 191: - if ( (*p) == 101 ) - goto st192; - goto tr3; -st192: - if ( ++p == pe ) - goto _test_eof192; -case 192: - if ( (*p) == 114 ) - goto st193; - goto tr3; -st193: - if ( ++p == pe ) - goto _test_eof193; -case 193: - if ( (*p) == 115 ) - goto st194; - goto tr3; -st194: - if ( ++p == pe ) - goto _test_eof194; -case 194: - if ( (*p) == 101 ) - goto st195; - goto tr3; -st195: - if ( ++p == pe ) - goto _test_eof195; -case 195: - if ( (*p) == 125 ) - goto st298; - goto tr3; -st298: - if ( ++p == pe ) - goto _test_eof298; -case 298: - goto tr366; -st196: - if ( ++p == pe ) - goto _test_eof196; -case 196: - switch( (*p) ) { - case 101: goto st197; - case 104: goto st203; - case 116: goto st206; - } - goto tr3; -st197: - if ( ++p == pe ) - goto _test_eof197; -case 197: - if ( (*p) == 99 ) - goto st198; - goto tr3; -st198: - if ( ++p == pe ) - goto _test_eof198; -case 198: - if ( (*p) == 116 ) - goto st199; - goto tr3; -st199: - if ( ++p == pe ) - goto _test_eof199; -case 199: - if ( (*p) == 105 ) - goto st200; - goto tr3; -st200: - if ( ++p == pe ) - goto _test_eof200; -case 200: - if ( (*p) == 111 ) - goto st201; - goto tr3; -st201: - if ( ++p == pe ) - goto _test_eof201; -case 201: - if ( (*p) == 110 ) - goto st202; - goto tr3; -st202: - if ( ++p == pe ) - goto _test_eof202; -case 202: - if ( (*p) == 125 ) - goto st299; - goto tr3; -st299: - if ( ++p == pe ) - goto _test_eof299; -case 299: - goto tr367; -st203: - if ( ++p == pe ) - goto _test_eof203; -case 203: - if ( (*p) == 111 ) - goto st204; - goto tr3; -st204: - if ( ++p == pe ) - goto _test_eof204; -case 204: - if ( (*p) == 119 ) - goto st205; - goto tr3; -st205: - if ( ++p == pe ) - goto _test_eof205; -case 205: - if ( (*p) == 125 ) - goto st300; - goto tr3; -st300: - if ( ++p == pe ) - goto _test_eof300; -case 300: - goto tr368; -st206: - if ( ++p == pe ) - goto _test_eof206; -case 206: - if ( (*p) == 114 ) - goto st207; - goto tr3; -st207: - if ( ++p == pe ) - goto _test_eof207; -case 207: - switch( (*p) ) { - case 105: goto st208; - case 111: goto st218; - } - goto tr3; -st208: - if ( ++p == pe ) - goto _test_eof208; -case 208: - if ( (*p) == 107 ) - goto st209; - goto tr3; -st209: - if ( ++p == pe ) - goto _test_eof209; -case 209: - if ( (*p) == 101 ) - goto st210; - goto tr3; -st210: - if ( ++p == pe ) - goto _test_eof210; -case 210: - switch( (*p) ) { - case 116: goto st211; - case 125: goto st301; - } - goto tr3; -st211: - if ( ++p == pe ) - goto _test_eof211; -case 211: - if ( (*p) == 104 ) - goto st212; - goto tr3; -st212: - if ( ++p == pe ) - goto _test_eof212; -case 212: - if ( (*p) == 114 ) - goto st213; - goto tr3; -st213: - if ( ++p == pe ) - goto _test_eof213; -case 213: - if ( (*p) == 111 ) - goto st214; - goto tr3; -st214: - if ( ++p == pe ) - goto _test_eof214; -case 214: - if ( (*p) == 117 ) - goto st215; - goto tr3; -st215: - if ( ++p == pe ) - goto _test_eof215; -case 215: - if ( (*p) == 103 ) - goto st216; - goto tr3; -st216: - if ( ++p == pe ) - goto _test_eof216; -case 216: - if ( (*p) == 104 ) - goto st217; - goto tr3; -st217: - if ( ++p == pe ) - goto _test_eof217; -case 217: - if ( (*p) == 125 ) - goto st301; - goto tr3; -st301: - if ( ++p == pe ) - goto _test_eof301; -case 301: - goto tr369; -st218: - if ( ++p == pe ) - goto _test_eof218; -case 218: - if ( (*p) == 110 ) - goto st219; - goto tr3; -st219: - if ( ++p == pe ) - goto _test_eof219; -case 219: - if ( (*p) == 103 ) - goto st220; - goto tr3; -st220: - if ( ++p == pe ) - goto _test_eof220; -case 220: - if ( (*p) == 125 ) - goto st302; - goto tr3; -st302: - if ( ++p == pe ) - goto _test_eof302; -case 302: - goto tr370; -st221: - if ( ++p == pe ) - goto _test_eof221; -case 221: - if ( (*p) == 101 ) - goto st222; - goto tr3; -st222: - if ( ++p == pe ) - goto _test_eof222; -case 222: - if ( (*p) == 120 ) - goto st223; - goto tr3; -st223: - if ( ++p == pe ) - goto _test_eof223; -case 223: - if ( (*p) == 116 ) - goto st224; - goto tr3; -st224: - if ( ++p == pe ) - goto _test_eof224; -case 224: - if ( (*p) == 125 ) - goto st303; - goto tr3; -st303: - if ( ++p == pe ) - goto _test_eof303; -case 303: - goto tr371; -st225: - if ( ++p == pe ) - goto _test_eof225; -case 225: - switch( (*p) ) { - case 110: goto st226; - case 125: goto st251; - } - goto tr3; -st226: - if ( ++p == pe ) - goto _test_eof226; -case 226: - if ( (*p) == 100 ) - goto st227; - goto tr3; -st227: - if ( ++p == pe ) - goto _test_eof227; -case 227: - if ( (*p) == 101 ) - goto st228; - goto tr3; -st228: - if ( ++p == pe ) - goto _test_eof228; -case 228: - if ( (*p) == 114 ) - goto st229; - goto tr3; -st229: - if ( ++p == pe ) - goto _test_eof229; -case 229: - if ( (*p) == 108 ) - goto st230; - goto tr3; -st230: - if ( ++p == pe ) - goto _test_eof230; -case 230: - if ( (*p) == 105 ) - goto st231; - goto tr3; -st231: - if ( ++p == pe ) - goto _test_eof231; -case 231: - if ( (*p) == 110 ) - goto st232; - goto tr3; -st232: - if ( ++p == pe ) - goto _test_eof232; -case 232: - if ( (*p) == 101 ) - goto st11; - goto tr3; -st233: - if ( ++p == pe ) - goto _test_eof233; -case 233: - if ( (*p) == 104 ) - goto st234; - goto tr3; -st234: - if ( ++p == pe ) - goto _test_eof234; -case 234: - if ( (*p) == 105 ) - goto st235; - goto tr3; -st235: - if ( ++p == pe ) - goto _test_eof235; -case 235: - if ( (*p) == 116 ) - goto st236; - goto tr3; -st236: - if ( ++p == pe ) - goto _test_eof236; -case 236: - if ( (*p) == 101 ) - goto st237; - goto tr3; -st237: - if ( ++p == pe ) - goto _test_eof237; -case 237: - if ( (*p) == 125 ) - goto st304; - goto tr3; -st304: - if ( ++p == pe ) - goto _test_eof304; -case 304: - goto tr372; -st238: - if ( ++p == pe ) - goto _test_eof238; -case 238: - if ( (*p) == 101 ) - goto st239; - goto tr3; -st239: - if ( ++p == pe ) - goto _test_eof239; -case 239: - if ( (*p) == 108 ) - goto st240; - goto tr3; -st240: - if ( ++p == pe ) - goto _test_eof240; -case 240: - if ( (*p) == 108 ) - goto st241; - goto tr3; -st241: - if ( ++p == pe ) - goto _test_eof241; -case 241: - if ( (*p) == 111 ) - goto st242; - goto tr3; -st242: - if ( ++p == pe ) - goto _test_eof242; -case 242: - if ( (*p) == 119 ) - goto st243; - goto tr3; -st243: - if ( ++p == pe ) - goto _test_eof243; -case 243: - if ( (*p) == 125 ) - goto st305; - goto tr3; -st305: - if ( ++p == pe ) - goto _test_eof305; -case 305: - goto tr373; - } - _test_eof244: cs = 244; goto _test_eof; - _test_eof1: cs = 1; goto _test_eof; - _test_eof2: cs = 2; goto _test_eof; - _test_eof245: cs = 245; goto _test_eof; - _test_eof3: cs = 3; goto _test_eof; - _test_eof4: cs = 4; goto _test_eof; - _test_eof246: cs = 246; goto _test_eof; - _test_eof5: cs = 5; goto _test_eof; - _test_eof6: cs = 6; goto _test_eof; - _test_eof247: cs = 247; goto _test_eof; - _test_eof7: cs = 7; goto _test_eof; - _test_eof8: cs = 8; goto _test_eof; - _test_eof248: cs = 248; goto _test_eof; - _test_eof9: cs = 9; goto _test_eof; - _test_eof249: cs = 249; goto _test_eof; - _test_eof10: cs = 10; goto _test_eof; - _test_eof250: cs = 250; goto _test_eof; - _test_eof11: cs = 11; goto _test_eof; - _test_eof251: cs = 251; goto _test_eof; - _test_eof12: cs = 12; goto _test_eof; - _test_eof13: cs = 13; goto _test_eof; - _test_eof14: cs = 14; goto _test_eof; - _test_eof15: cs = 15; goto _test_eof; - _test_eof252: cs = 252; goto _test_eof; - _test_eof16: cs = 16; goto _test_eof; - _test_eof17: cs = 17; goto _test_eof; - _test_eof18: cs = 18; goto _test_eof; - _test_eof19: cs = 19; goto _test_eof; - _test_eof20: cs = 20; goto _test_eof; - _test_eof253: cs = 253; goto _test_eof; - _test_eof21: cs = 21; goto _test_eof; - _test_eof22: cs = 22; goto _test_eof; - _test_eof254: cs = 254; goto _test_eof; - _test_eof23: cs = 23; goto _test_eof; - _test_eof24: cs = 24; goto _test_eof; - _test_eof25: cs = 25; goto _test_eof; - _test_eof26: cs = 26; goto _test_eof; - _test_eof255: cs = 255; goto _test_eof; - _test_eof27: cs = 27; goto _test_eof; - _test_eof28: cs = 28; goto _test_eof; - _test_eof29: cs = 29; goto _test_eof; - _test_eof30: cs = 30; goto _test_eof; - _test_eof256: cs = 256; goto _test_eof; - _test_eof31: cs = 31; goto _test_eof; - _test_eof32: cs = 32; goto _test_eof; - _test_eof33: cs = 33; goto _test_eof; - _test_eof257: cs = 257; goto _test_eof; - _test_eof34: cs = 34; goto _test_eof; - _test_eof35: cs = 35; goto _test_eof; - _test_eof36: cs = 36; goto _test_eof; - _test_eof37: cs = 37; goto _test_eof; - _test_eof38: cs = 38; goto _test_eof; - _test_eof39: cs = 39; goto _test_eof; - _test_eof40: cs = 40; goto _test_eof; - _test_eof258: cs = 258; goto _test_eof; - _test_eof41: cs = 41; goto _test_eof; - _test_eof42: cs = 42; goto _test_eof; - _test_eof43: cs = 43; goto _test_eof; - _test_eof259: cs = 259; goto _test_eof; - _test_eof44: cs = 44; goto _test_eof; - _test_eof45: cs = 45; goto _test_eof; - _test_eof46: cs = 46; goto _test_eof; - _test_eof47: cs = 47; goto _test_eof; - _test_eof48: cs = 48; goto _test_eof; - _test_eof260: cs = 260; goto _test_eof; - _test_eof49: cs = 49; goto _test_eof; - _test_eof50: cs = 50; goto _test_eof; - _test_eof51: cs = 51; goto _test_eof; - _test_eof52: cs = 52; goto _test_eof; - _test_eof53: cs = 53; goto _test_eof; - _test_eof54: cs = 54; goto _test_eof; - _test_eof261: cs = 261; goto _test_eof; - _test_eof55: cs = 55; goto _test_eof; - _test_eof56: cs = 56; goto _test_eof; - _test_eof57: cs = 57; goto _test_eof; - _test_eof58: cs = 58; goto _test_eof; - _test_eof262: cs = 262; goto _test_eof; - _test_eof59: cs = 59; goto _test_eof; - _test_eof60: cs = 60; goto _test_eof; - _test_eof263: cs = 263; goto _test_eof; - _test_eof61: cs = 61; goto _test_eof; - _test_eof62: cs = 62; goto _test_eof; - _test_eof63: cs = 63; goto _test_eof; - _test_eof64: cs = 64; goto _test_eof; - _test_eof65: cs = 65; goto _test_eof; - _test_eof66: cs = 66; goto _test_eof; - _test_eof67: cs = 67; goto _test_eof; - _test_eof68: cs = 68; goto _test_eof; - _test_eof69: cs = 69; goto _test_eof; - _test_eof70: cs = 70; goto _test_eof; - _test_eof71: cs = 71; goto _test_eof; - _test_eof72: cs = 72; goto _test_eof; - _test_eof73: cs = 73; goto _test_eof; - _test_eof264: cs = 264; goto _test_eof; - _test_eof74: cs = 74; goto _test_eof; - _test_eof75: cs = 75; goto _test_eof; - _test_eof265: cs = 265; goto _test_eof; - _test_eof76: cs = 76; goto _test_eof; - _test_eof77: cs = 77; goto _test_eof; - _test_eof78: cs = 78; goto _test_eof; - _test_eof79: cs = 79; goto _test_eof; - _test_eof266: cs = 266; goto _test_eof; - _test_eof80: cs = 80; goto _test_eof; - _test_eof81: cs = 81; goto _test_eof; - _test_eof82: cs = 82; goto _test_eof; - _test_eof83: cs = 83; goto _test_eof; - _test_eof84: cs = 84; goto _test_eof; - _test_eof267: cs = 267; goto _test_eof; - _test_eof85: cs = 85; goto _test_eof; - _test_eof86: cs = 86; goto _test_eof; - _test_eof87: cs = 87; goto _test_eof; - _test_eof88: cs = 88; goto _test_eof; - _test_eof89: cs = 89; goto _test_eof; - _test_eof90: cs = 90; goto _test_eof; - _test_eof91: cs = 91; goto _test_eof; - _test_eof268: cs = 268; goto _test_eof; - _test_eof92: cs = 92; goto _test_eof; - _test_eof93: cs = 93; goto _test_eof; - _test_eof94: cs = 94; goto _test_eof; - _test_eof269: cs = 269; goto _test_eof; - _test_eof95: cs = 95; goto _test_eof; - _test_eof96: cs = 96; goto _test_eof; - _test_eof97: cs = 97; goto _test_eof; - _test_eof98: cs = 98; goto _test_eof; - _test_eof99: cs = 99; goto _test_eof; - _test_eof270: cs = 270; goto _test_eof; - _test_eof100: cs = 100; goto _test_eof; - _test_eof101: cs = 101; goto _test_eof; - _test_eof102: cs = 102; goto _test_eof; - _test_eof103: cs = 103; goto _test_eof; - _test_eof104: cs = 104; goto _test_eof; - _test_eof105: cs = 105; goto _test_eof; - _test_eof271: cs = 271; goto _test_eof; - _test_eof272: cs = 272; goto _test_eof; - _test_eof273: cs = 273; goto _test_eof; - _test_eof106: cs = 106; goto _test_eof; - _test_eof107: cs = 107; goto _test_eof; - _test_eof108: cs = 108; goto _test_eof; - _test_eof274: cs = 274; goto _test_eof; - _test_eof109: cs = 109; goto _test_eof; - _test_eof275: cs = 275; goto _test_eof; - _test_eof110: cs = 110; goto _test_eof; - _test_eof111: cs = 111; goto _test_eof; - _test_eof112: cs = 112; goto _test_eof; - _test_eof113: cs = 113; goto _test_eof; - _test_eof114: cs = 114; goto _test_eof; - _test_eof276: cs = 276; goto _test_eof; - _test_eof115: cs = 115; goto _test_eof; - _test_eof116: cs = 116; goto _test_eof; - _test_eof117: cs = 117; goto _test_eof; - _test_eof277: cs = 277; goto _test_eof; - _test_eof118: cs = 118; goto _test_eof; - _test_eof119: cs = 119; goto _test_eof; - _test_eof120: cs = 120; goto _test_eof; - _test_eof121: cs = 121; goto _test_eof; - _test_eof122: cs = 122; goto _test_eof; - _test_eof123: cs = 123; goto _test_eof; - _test_eof278: cs = 278; goto _test_eof; - _test_eof124: cs = 124; goto _test_eof; - _test_eof125: cs = 125; goto _test_eof; - _test_eof279: cs = 279; goto _test_eof; - _test_eof126: cs = 126; goto _test_eof; - _test_eof127: cs = 127; goto _test_eof; - _test_eof280: cs = 280; goto _test_eof; - _test_eof128: cs = 128; goto _test_eof; - _test_eof129: cs = 129; goto _test_eof; - _test_eof130: cs = 130; goto _test_eof; - _test_eof131: cs = 131; goto _test_eof; - _test_eof281: cs = 281; goto _test_eof; - _test_eof132: cs = 132; goto _test_eof; - _test_eof133: cs = 133; goto _test_eof; - _test_eof134: cs = 134; goto _test_eof; - _test_eof135: cs = 135; goto _test_eof; - _test_eof136: cs = 136; goto _test_eof; - _test_eof137: cs = 137; goto _test_eof; - _test_eof138: cs = 138; goto _test_eof; - _test_eof139: cs = 139; goto _test_eof; - _test_eof282: cs = 282; goto _test_eof; - _test_eof140: cs = 140; goto _test_eof; - _test_eof141: cs = 141; goto _test_eof; - _test_eof142: cs = 142; goto _test_eof; - _test_eof143: cs = 143; goto _test_eof; - _test_eof144: cs = 144; goto _test_eof; - _test_eof145: cs = 145; goto _test_eof; - _test_eof146: cs = 146; goto _test_eof; - _test_eof283: cs = 283; goto _test_eof; - _test_eof147: cs = 147; goto _test_eof; - _test_eof148: cs = 148; goto _test_eof; - _test_eof149: cs = 149; goto _test_eof; - _test_eof284: cs = 284; goto _test_eof; - _test_eof150: cs = 150; goto _test_eof; - _test_eof151: cs = 151; goto _test_eof; - _test_eof152: cs = 152; goto _test_eof; - _test_eof285: cs = 285; goto _test_eof; - _test_eof153: cs = 153; goto _test_eof; - _test_eof154: cs = 154; goto _test_eof; - _test_eof286: cs = 286; goto _test_eof; - _test_eof155: cs = 155; goto _test_eof; - _test_eof287: cs = 287; goto _test_eof; - _test_eof156: cs = 156; goto _test_eof; - _test_eof288: cs = 288; goto _test_eof; - _test_eof157: cs = 157; goto _test_eof; - _test_eof289: cs = 289; goto _test_eof; - _test_eof158: cs = 158; goto _test_eof; - _test_eof290: cs = 290; goto _test_eof; - _test_eof159: cs = 159; goto _test_eof; - _test_eof291: cs = 291; goto _test_eof; - _test_eof160: cs = 160; goto _test_eof; - _test_eof161: cs = 161; goto _test_eof; - _test_eof162: cs = 162; goto _test_eof; - _test_eof292: cs = 292; goto _test_eof; - _test_eof163: cs = 163; goto _test_eof; - _test_eof164: cs = 164; goto _test_eof; - _test_eof165: cs = 165; goto _test_eof; - _test_eof166: cs = 166; goto _test_eof; - _test_eof167: cs = 167; goto _test_eof; - _test_eof168: cs = 168; goto _test_eof; - _test_eof169: cs = 169; goto _test_eof; - _test_eof293: cs = 293; goto _test_eof; - _test_eof170: cs = 170; goto _test_eof; - _test_eof171: cs = 171; goto _test_eof; - _test_eof172: cs = 172; goto _test_eof; - _test_eof294: cs = 294; goto _test_eof; - _test_eof173: cs = 173; goto _test_eof; - _test_eof174: cs = 174; goto _test_eof; - _test_eof175: cs = 175; goto _test_eof; - _test_eof176: cs = 176; goto _test_eof; - _test_eof177: cs = 177; goto _test_eof; - _test_eof178: cs = 178; goto _test_eof; - _test_eof179: cs = 179; goto _test_eof; - _test_eof180: cs = 180; goto _test_eof; - _test_eof181: cs = 181; goto _test_eof; - _test_eof182: cs = 182; goto _test_eof; - _test_eof183: cs = 183; goto _test_eof; - _test_eof184: cs = 184; goto _test_eof; - _test_eof295: cs = 295; goto _test_eof; - _test_eof185: cs = 185; goto _test_eof; - _test_eof186: cs = 186; goto _test_eof; - _test_eof187: cs = 187; goto _test_eof; - _test_eof296: cs = 296; goto _test_eof; - _test_eof188: cs = 188; goto _test_eof; - _test_eof189: cs = 189; goto _test_eof; - _test_eof190: cs = 190; goto _test_eof; - _test_eof297: cs = 297; goto _test_eof; - _test_eof191: cs = 191; goto _test_eof; - _test_eof192: cs = 192; goto _test_eof; - _test_eof193: cs = 193; goto _test_eof; - _test_eof194: cs = 194; goto _test_eof; - _test_eof195: cs = 195; goto _test_eof; - _test_eof298: cs = 298; goto _test_eof; - _test_eof196: cs = 196; goto _test_eof; - _test_eof197: cs = 197; goto _test_eof; - _test_eof198: cs = 198; goto _test_eof; - _test_eof199: cs = 199; goto _test_eof; - _test_eof200: cs = 200; goto _test_eof; - _test_eof201: cs = 201; goto _test_eof; - _test_eof202: cs = 202; goto _test_eof; - _test_eof299: cs = 299; goto _test_eof; - _test_eof203: cs = 203; goto _test_eof; - _test_eof204: cs = 204; goto _test_eof; - _test_eof205: cs = 205; goto _test_eof; - _test_eof300: cs = 300; goto _test_eof; - _test_eof206: cs = 206; goto _test_eof; - _test_eof207: cs = 207; goto _test_eof; - _test_eof208: cs = 208; goto _test_eof; - _test_eof209: cs = 209; goto _test_eof; - _test_eof210: cs = 210; goto _test_eof; - _test_eof211: cs = 211; goto _test_eof; - _test_eof212: cs = 212; goto _test_eof; - _test_eof213: cs = 213; goto _test_eof; - _test_eof214: cs = 214; goto _test_eof; - _test_eof215: cs = 215; goto _test_eof; - _test_eof216: cs = 216; goto _test_eof; - _test_eof217: cs = 217; goto _test_eof; - _test_eof301: cs = 301; goto _test_eof; - _test_eof218: cs = 218; goto _test_eof; - _test_eof219: cs = 219; goto _test_eof; - _test_eof220: cs = 220; goto _test_eof; - _test_eof302: cs = 302; goto _test_eof; - _test_eof221: cs = 221; goto _test_eof; - _test_eof222: cs = 222; goto _test_eof; - _test_eof223: cs = 223; goto _test_eof; - _test_eof224: cs = 224; goto _test_eof; - _test_eof303: cs = 303; goto _test_eof; - _test_eof225: cs = 225; goto _test_eof; - _test_eof226: cs = 226; goto _test_eof; - _test_eof227: cs = 227; goto _test_eof; - _test_eof228: cs = 228; goto _test_eof; - _test_eof229: cs = 229; goto _test_eof; - _test_eof230: cs = 230; goto _test_eof; - _test_eof231: cs = 231; goto _test_eof; - _test_eof232: cs = 232; goto _test_eof; - _test_eof233: cs = 233; goto _test_eof; - _test_eof234: cs = 234; goto _test_eof; - _test_eof235: cs = 235; goto _test_eof; - _test_eof236: cs = 236; goto _test_eof; - _test_eof237: cs = 237; goto _test_eof; - _test_eof304: cs = 304; goto _test_eof; - _test_eof238: cs = 238; goto _test_eof; - _test_eof239: cs = 239; goto _test_eof; - _test_eof240: cs = 240; goto _test_eof; - _test_eof241: cs = 241; goto _test_eof; - _test_eof242: cs = 242; goto _test_eof; - _test_eof243: cs = 243; goto _test_eof; - _test_eof305: cs = 305; goto _test_eof; - - _test_eof: {} - if ( p == eof ) - { - switch ( cs ) { - case 245: goto tr298; - case 3: goto tr3; - case 4: goto tr3; - case 246: goto tr314; - case 5: goto tr3; - case 6: goto tr3; - case 247: goto tr315; - case 7: goto tr3; - case 8: goto tr3; - case 248: goto tr316; - case 9: goto tr3; - case 249: goto tr317; - case 10: goto tr3; - case 250: goto tr318; - case 11: goto tr3; - case 251: goto tr319; - case 12: goto tr3; - case 13: goto tr3; - case 14: goto tr3; - case 15: goto tr3; - case 252: goto tr320; - case 16: goto tr3; - case 17: goto tr3; - case 18: goto tr3; - case 19: goto tr3; - case 20: goto tr3; - case 253: goto tr321; - case 21: goto tr3; - case 22: goto tr3; - case 254: goto tr322; - case 23: goto tr3; - case 24: goto tr3; - case 25: goto tr3; - case 26: goto tr3; - case 255: goto tr323; - case 27: goto tr3; - case 28: goto tr3; - case 29: goto tr3; - case 30: goto tr3; - case 256: goto tr324; - case 31: goto tr3; - case 32: goto tr3; - case 33: goto tr3; - case 257: goto tr325; - case 34: goto tr3; - case 35: goto tr3; - case 36: goto tr3; - case 37: goto tr3; - case 38: goto tr3; - case 39: goto tr3; - case 40: goto tr3; - case 258: goto tr326; - case 41: goto tr3; - case 42: goto tr3; - case 43: goto tr3; - case 259: goto tr327; - case 44: goto tr3; - case 45: goto tr3; - case 46: goto tr3; - case 47: goto tr3; - case 48: goto tr3; - case 260: goto tr328; - case 49: goto tr3; - case 50: goto tr3; - case 51: goto tr3; - case 52: goto tr3; - case 53: goto tr3; - case 54: goto tr3; - case 261: goto tr329; - case 55: goto tr3; - case 56: goto tr3; - case 57: goto tr3; - case 58: goto tr3; - case 262: goto tr330; - case 59: goto tr3; - case 60: goto tr3; - case 263: goto tr331; - case 61: goto tr3; - case 62: goto tr3; - case 63: goto tr3; - case 64: goto tr3; - case 65: goto tr3; - case 66: goto tr3; - case 67: goto tr3; - case 68: goto tr3; - case 69: goto tr3; - case 70: goto tr3; - case 71: goto tr3; - case 72: goto tr3; - case 73: goto tr3; - case 264: goto tr332; - case 74: goto tr3; - case 75: goto tr3; - case 265: goto tr333; - case 76: goto tr3; - case 77: goto tr3; - case 78: goto tr3; - case 79: goto tr3; - case 266: goto tr334; - case 80: goto tr3; - case 81: goto tr3; - case 82: goto tr3; - case 83: goto tr3; - case 84: goto tr3; - case 267: goto tr335; - case 85: goto tr3; - case 86: goto tr3; - case 87: goto tr3; - case 88: goto tr3; - case 89: goto tr3; - case 90: goto tr3; - case 91: goto tr3; - case 268: goto tr336; - case 92: goto tr3; - case 93: goto tr3; - case 94: goto tr3; - case 269: goto tr337; - case 95: goto tr3; - case 96: goto tr3; - case 97: goto tr3; - case 98: goto tr3; - case 99: goto tr3; - case 270: goto tr338; - case 100: goto tr3; - case 101: goto tr3; - case 102: goto tr3; - case 103: goto tr3; - case 104: goto tr3; - case 105: goto tr3; - case 271: goto tr339; - case 272: goto tr340; - case 273: goto tr341; - case 106: goto tr3; - case 107: goto tr3; - case 108: goto tr3; - case 274: goto tr342; - case 109: goto tr3; - case 275: goto tr343; - case 110: goto tr3; - case 111: goto tr3; - case 112: goto tr3; - case 113: goto tr3; - case 114: goto tr3; - case 276: goto tr344; - case 115: goto tr3; - case 116: goto tr3; - case 117: goto tr3; - case 277: goto tr345; - case 118: goto tr3; - case 119: goto tr3; - case 120: goto tr3; - case 121: goto tr3; - case 122: goto tr3; - case 123: goto tr3; - case 278: goto tr346; - case 124: goto tr3; - case 125: goto tr3; - case 279: goto tr347; - case 126: goto tr3; - case 127: goto tr3; - case 280: goto tr348; - case 128: goto tr3; - case 129: goto tr3; - case 130: goto tr3; - case 131: goto tr3; - case 281: goto tr349; - case 132: goto tr3; - case 133: goto tr3; - case 134: goto tr3; - case 135: goto tr3; - case 136: goto tr3; - case 137: goto tr3; - case 138: goto tr3; - case 139: goto tr3; - case 282: goto tr350; - case 140: goto tr3; - case 141: goto tr3; - case 142: goto tr3; - case 143: goto tr3; - case 144: goto tr3; - case 145: goto tr3; - case 146: goto tr3; - case 283: goto tr351; - case 147: goto tr3; - case 148: goto tr3; - case 149: goto tr3; - case 284: goto tr352; - case 150: goto tr3; - case 151: goto tr3; - case 152: goto tr3; - case 285: goto tr353; - case 153: goto tr3; - case 154: goto tr3; - case 286: goto tr354; - case 155: goto tr3; - case 287: goto tr355; - case 156: goto tr3; - case 288: goto tr356; - case 157: goto tr3; - case 289: goto tr357; - case 158: goto tr3; - case 290: goto tr358; - case 159: goto tr3; - case 291: goto tr359; - case 160: goto tr3; - case 161: goto tr3; - case 162: goto tr3; - case 292: goto tr360; - case 163: goto tr3; - case 164: goto tr3; - case 165: goto tr3; - case 166: goto tr3; - case 167: goto tr3; - case 168: goto tr3; - case 169: goto tr3; - case 293: goto tr361; - case 170: goto tr3; - case 171: goto tr3; - case 172: goto tr3; - case 294: goto tr362; - case 173: goto tr3; - case 174: goto tr3; - case 175: goto tr3; - case 176: goto tr3; - case 177: goto tr3; - case 178: goto tr3; - case 179: goto tr3; - case 180: goto tr3; - case 181: goto tr3; - case 182: goto tr3; - case 183: goto tr3; - case 184: goto tr3; - case 295: goto tr363; - case 185: goto tr3; - case 186: goto tr3; - case 187: goto tr3; - case 296: goto tr364; - case 188: goto tr3; - case 189: goto tr3; - case 190: goto tr3; - case 297: goto tr365; - case 191: goto tr3; - case 192: goto tr3; - case 193: goto tr3; - case 194: goto tr3; - case 195: goto tr3; - case 298: goto tr366; - case 196: goto tr3; - case 197: goto tr3; - case 198: goto tr3; - case 199: goto tr3; - case 200: goto tr3; - case 201: goto tr3; - case 202: goto tr3; - case 299: goto tr367; - case 203: goto tr3; - case 204: goto tr3; - case 205: goto tr3; - case 300: goto tr368; - case 206: goto tr3; - case 207: goto tr3; - case 208: goto tr3; - case 209: goto tr3; - case 210: goto tr3; - case 211: goto tr3; - case 212: goto tr3; - case 213: goto tr3; - case 214: goto tr3; - case 215: goto tr3; - case 216: goto tr3; - case 217: goto tr3; - case 301: goto tr369; - case 218: goto tr3; - case 219: goto tr3; - case 220: goto tr3; - case 302: goto tr370; - case 221: goto tr3; - case 222: goto tr3; - case 223: goto tr3; - case 224: goto tr3; - case 303: goto tr371; - case 225: goto tr3; - case 226: goto tr3; - case 227: goto tr3; - case 228: goto tr3; - case 229: goto tr3; - case 230: goto tr3; - case 231: goto tr3; - case 232: goto tr3; - case 233: goto tr3; - case 234: goto tr3; - case 235: goto tr3; - case 236: goto tr3; - case 237: goto tr3; - case 304: goto tr372; - case 238: goto tr3; - case 239: goto tr3; - case 240: goto tr3; - case 241: goto tr3; - case 242: goto tr3; - case 243: goto tr3; - case 305: goto tr373; - } - } - - _out: {} - } - -#line 965 "src/tokens.rl" - - result.cursor_col = parser.cursor_col; - result.cursor_row = parser.cursor_row; - result.has_cursor = parser.has_cursor; - result.final_col = parser.visual_col; - result.final_row = parser.visual_row; - return result; -} - -zstr expand_tokens(const char *text) { - TokenExpansion result = expand_tokens_with_cursor(text); - return result.expanded; -} - -void zstr_expand_to(FILE *stream, const char *text) { - if (!text || !*text) return; - - Z_CLEANUP(zstr_free) zstr expanded = expand_tokens(text); - fwrite(zstr_cstr(&expanded), 1, zstr_len(&expanded), stream); -} - -void token_expansion_render(FILE *stream, TokenExpansion *te) { - if (!te) return; - - /* Write the expanded content */ - fwrite(zstr_cstr(&te->expanded), 1, zstr_len(&te->expanded), stream); - - /* If cursor was marked, clear to end of line and position cursor */ - if (te->has_cursor) { - /* Clear from current position to end of line (gives cursor room) */ - if (!zstr_no_colors) { - fprintf(stream, "\033[K"); - } - - /* Position cursor at marked location and show it */ - if (!zstr_no_colors && te->cursor_row > 0 && te->cursor_col > 0) { - fprintf(stream, "\033[%d;%dH\033[?25h", te->cursor_row, te->cursor_col); - } - } -} diff --git a/src/tokens.h b/src/tokens.h deleted file mode 100644 index 11a4c5e..0000000 --- a/src/tokens.h +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Token expansion system - Header - * - * Provides stack-based ANSI escape code generation from markup tokens. - * Generated by Ragel from tokens.rl - * - * Token Types: - * - * SEMANTIC TOKENS (composite styles, configurable via globals): - * {b} - Bold only (same as {strong}) - * {highlight} - Bold + yellow (for highlighting, e.g., fuzzy matches) - * {h1}-{h6} - Heading styles (default: h1=orange, h2=blue, h3-h6=white) - * {strong} - Strong/bold text - * {dim} - Gray foreground (bright black) - * {section} - Bold + dark gray background, for selection highlighting - * {danger} - Dark red background (for warnings/deletions) - * {strike} - Strikethrough text (alias for {strikethrough}) - * {text} - Full reset (alias for {reset}) - * - * ATTRIBUTE TOKENS: - * {bold}, {B} - Bold text - * {italic}, {I}, {i} - Italic text - * {underline}, {U}, {u} - Underlined text - * {reverse} - Reverse/inverse video - * {strikethrough} - Strikethrough text - * {bright} - Brighten current foreground color - * - * FOREGROUND COLORS: - * {black}, {red}, {green}, {yellow}, {blue}, {magenta}, {cyan}, {white} - * {gray}, {grey} - Gray (bright black) - * {bright:COLOR} - Bright variant (e.g., {bright:red}) - * {fg:N} - 256-color foreground (N = 0-255) - * - * BACKGROUND COLORS: - * {bg:COLOR} - Background color (e.g., {bg:red}) - * {bg:N} - 256-color background (N = 0-255) - * - * RESET/POP TOKENS: - * {/} - Pop one level, restore previous style - * {reset} - Full reset, clear stack - * {/fg} - Reset foreground color only - * {/bg} - Reset background color only - * {/b} - Legacy: same as {/} - * {/section} - Legacy: same as {/} - * {/strike} - Legacy: same as {/} - * - * CONTROL TOKENS (terminal control sequences): - * {clr} - Clear to end of line (ESC[K) - * {cls} - Clear to end of screen (ESC[J) - * {home} - Move cursor to home position (ESC[H) - * {hide} - Hide cursor (ESC[?25l) - * {show} - Show cursor (ESC[?25h) - * {goto:row,col} - Move cursor to position (ESC[row;colH) - * {goto_cursor} - Move cursor to {cursor} marker position - * - * SPECIAL: - * {cursor} - Mark cursor position (for cursor tracking) - * - * Stack Semantics: - * Each style-setting token pushes its previous state onto a stack. - * {/} pops one level, restoring the previous state. - * Composite tokens (like {highlight}) push multiple attrs and a composite marker, - * so a single {/} restores all of them at once. - * - * Auto-reset: - * All styles are automatically reset at the end of each line (before newline). - * This ensures clean line boundaries without trailing ANSI codes. - * - * Redundancy avoidance: - * Repeated identical styles don't emit redundant codes. - * e.g., "{dim}{dim}{dim}x" outputs same as "{dim}x" - * - * Example: - * "Normal {highlight}bold yellow {red}now red{/} back to yellow{/} normal" - * "Nested: {bold}{italic}bold+italic{/}just bold{/}normal" - */ - -#ifndef TOKENS_H -#define TOKENS_H - -#include "zstr.h" -#include - -/* - * Global style configuration for semantic tokens. - * These can be modified at runtime to customize appearance. - * Format: ANSI SGR parameter string (without leading \033[ or trailing m) - */ -extern const char *token_style_h1; /* Default: "1m\033[38;5;214" (bold+orange) */ -extern const char *token_style_h2; /* Default: "1;34" (bold+blue) */ -extern const char *token_style_h3; /* Default: "1;37" (bold+white) */ -extern const char *token_style_h4; /* Default: "1;37" (bold+white) */ -extern const char *token_style_h5; /* Default: "1;37" (bold+white) */ -extern const char *token_style_h6; /* Default: "1;37" (bold+white) */ -extern const char *token_style_b; /* Default: "1" (bold) */ -extern const char *token_style_strong; /* Default: "1" (bold) */ -extern const char *token_style_highlight; /* Default: "1;33" (bold+yellow) */ -extern const char *token_style_dim; /* Default: "90" (gray) */ -extern const char *token_style_danger; /* Default: "48;5;52" (dark red bg) */ - -/* Global flags for controlling token expansion */ -extern bool zstr_disable_token_expansion; /* If true, pass through unchanged */ -extern bool zstr_no_colors; /* If true, tokens expand to empty strings */ - -/* Result of token expansion with state tracking */ -typedef struct { - zstr expanded; /* The expanded string with ANSI codes */ - int cursor_col; /* Visual column of {cursor} marker (1-indexed), or -1 if not found */ - int cursor_row; /* Visual row of {cursor} marker (1-indexed), or -1 if not found */ - bool has_cursor; /* True if {cursor} was found in the input */ - int final_col; /* Final visual column after expansion */ - int final_row; /* Final visual row after expansion */ -} TokenExpansion; - -/* - * Core expansion functions (operate on C strings) - */ - -/* Expand tokens, returning expanded string and state */ -TokenExpansion expand_tokens_with_cursor(const char *text); - -/* Expand tokens, returning just the expanded string */ -zstr expand_tokens(const char *text); - -/* Free a TokenExpansion (just frees the expanded zstr) */ -static inline void token_expansion_free(TokenExpansion *te) { - zstr_free(&te->expanded); -} - -/* - * zstr wrapper functions (for convenience when working with zstr) - */ - -/* Expand tokens from C string, returning expanded string and state */ -static inline TokenExpansion zstr_expand_tokens_with_cursor(const char *text) { - return expand_tokens_with_cursor(text); -} - -/* Expand tokens from C string, returning just the expanded string */ -static inline zstr zstr_expand_tokens(const char *text) { - return expand_tokens(text); -} - -/* - * Direct output functions - expand tokens and write directly to stream - */ - -#include - -/* Expand tokens and write to FILE* stream (e.g., stdout, stderr) */ -void zstr_expand_to(FILE *stream, const char *text); - -/* - * Render a TokenExpansion to a stream. - * If has_cursor is true, appends {goto_cursor}{show} and clears to end of line. - * This provides a complete render with proper cursor positioning. - */ -void token_expansion_render(FILE *stream, TokenExpansion *te); - -#endif /* TOKENS_H */ diff --git a/src/tokens.rl b/src/tokens.rl deleted file mode 100644 index 06dd872..0000000 --- a/src/tokens.rl +++ /dev/null @@ -1,1004 +0,0 @@ -/* - * Token expansion state machine - Ragel grammar - * - * This provides a stack-based token system for ANSI escape code generation. - * Tokens like {b}, {dim}, {red} push state, and {/} pops to restore. - * - * Generated with: ragel -C -G2 tokens.rl -o tokens.c - */ - -#include "tokens.h" -#include "zstr.h" -#include -#include -#include - -/* Maximum nesting depth for style stack */ -#define MAX_STACK_DEPTH 32 - -/* - * Global style configuration for semantic tokens. - * These can be overridden at runtime to customize the appearance. - * Format: ANSI escape sequence string (without the leading \033[) - */ -const char *token_style_h1 = "1m\033[38;5;214"; /* Bold + orange (256-color 214) */ -const char *token_style_h2 = "1;34"; /* Bold + blue */ -const char *token_style_h3 = "1;37"; /* Bold + white */ -const char *token_style_h4 = "1;37"; /* Bold + white */ -const char *token_style_h5 = "1;37"; /* Bold + white */ -const char *token_style_h6 = "1;37"; /* Bold + white */ -const char *token_style_b = "1"; /* Bold (same as strong) */ -const char *token_style_strong = "1"; /* Bold */ -const char *token_style_highlight = "1;33"; /* Bold + yellow */ -const char *token_style_dim = "37"; /* White (softer than bright white) */ -const char *token_style_danger = "48;5;52"; /* Dark red bg */ - -/* Style attribute types */ -typedef enum { - ATTR_NONE = 0, - ATTR_BOLD, - ATTR_DIM, - ATTR_ITALIC, - ATTR_UNDERLINE, - ATTR_REVERSE, - ATTR_STRIKETHROUGH, - ATTR_FG, /* Foreground color */ - ATTR_BG, /* Background color */ - ATTR_COMPOSITE, /* Multiple attributes (for semantic tokens) */ -} AttrType; - -/* Represents a single attribute value */ -typedef struct { - AttrType type; - int value; /* For colors: ANSI code, for bools: 0/1 */ -} AttrValue; - -/* Stack entry - stores what to restore when popping */ -typedef struct { - AttrType type; - int prev_value; - int count; /* For composite: how many individual attrs were pushed */ -} StackEntry; - -/* Parser state */ -typedef struct { - zstr *out; /* Output buffer */ - StackEntry stack[MAX_STACK_DEPTH]; - int stack_depth; - int cursor_col; /* Visual column of {cursor} (1-indexed), -1 if no cursor */ - int cursor_row; /* Visual row of {cursor} (1-indexed), -1 if no cursor */ - bool has_cursor; /* True if {cursor} was encountered */ - int visual_col; /* Current visual column */ - int visual_row; /* Current visual row */ - bool no_colors; /* If true, don't emit ANSI codes */ - bool disabled; /* If true, pass through without expansion */ - - /* Desired style state (what tokens request) */ - int fg_color; /* 0 = default, else ANSI code */ - int bg_color; - bool bold; - bool dim; - bool italic; - bool underline; - bool reverse; - bool strikethrough; - - /* Emitted style state (what terminal currently has) */ - int emitted_fg; - int emitted_bg; - bool emitted_bold; - bool emitted_dim; - bool emitted_italic; - bool emitted_underline; - bool emitted_reverse; - bool emitted_strikethrough; - bool dirty; /* True if desired != emitted */ -} TokenParser; - -/* ANSI escape code helpers */ -static void emit_ansi(TokenParser *p, const char *code) { - if (!p->no_colors) { - zstr_cat(p->out, code); - } -} - -static void emit_ansi_num(TokenParser *p, const char *prefix, int n, const char *suffix) { - if (!p->no_colors) { - char buf[32]; - snprintf(buf, sizeof(buf), "%s%d%s", prefix, n, suffix); - zstr_cat(p->out, buf); - } -} - -/* Mark state as dirty (deferred emission) */ -static void mark_dirty(TokenParser *p) { - p->dirty = true; -} - -/* Sync emitted state with desired state - emit codes only when outputting a character */ -static void sync_styles(TokenParser *p) { - if (!p->dirty || p->no_colors) return; - - /* Check if we need a full reset (going back to default state) */ - bool need_reset = false; - - /* If any attribute is turning OFF, we need reset then re-apply actives */ - if ((p->emitted_bold && !p->bold) || - (p->emitted_dim && !p->dim) || - (p->emitted_italic && !p->italic) || - (p->emitted_underline && !p->underline) || - (p->emitted_reverse && !p->reverse) || - (p->emitted_strikethrough && !p->strikethrough) || - (p->emitted_fg != 0 && p->fg_color == 0) || - (p->emitted_bg != 0 && p->bg_color == 0)) { - need_reset = true; - } - - if (need_reset) { - /* Reset and re-apply all active styles */ - zstr_cat(p->out, "\033[0"); - if (p->bold) zstr_cat(p->out, ";1"); - if (p->dim) zstr_cat(p->out, ";2"); - if (p->italic) zstr_cat(p->out, ";3"); - if (p->underline) zstr_cat(p->out, ";4"); - if (p->reverse) zstr_cat(p->out, ";7"); - if (p->strikethrough) zstr_cat(p->out, ";9"); - if (p->fg_color != 0) { - char buf[20]; - if (p->fg_color >= 1000 && p->fg_color < 2000) { - /* 256-color foreground */ - snprintf(buf, sizeof(buf), ";38;5;%d", p->fg_color - 1000); - } else { - snprintf(buf, sizeof(buf), ";%d", p->fg_color); - } - zstr_cat(p->out, buf); - } - if (p->bg_color != 0) { - char buf[20]; - if (p->bg_color >= 2000) { - /* 256-color background */ - snprintf(buf, sizeof(buf), ";48;5;%d", p->bg_color - 2000); - } else { - snprintf(buf, sizeof(buf), ";%d", p->bg_color); - } - zstr_cat(p->out, buf); - } - zstr_cat(p->out, "m"); - } else { - /* Apply only changed attributes (turning ON) */ - bool first = true; - char buf[64]; - buf[0] = '\0'; - - if (p->bold && !p->emitted_bold) { - strcat(buf, first ? "1" : ";1"); first = false; - } - if (p->dim && !p->emitted_dim) { - strcat(buf, first ? "2" : ";2"); first = false; - } - if (p->italic && !p->emitted_italic) { - strcat(buf, first ? "3" : ";3"); first = false; - } - if (p->underline && !p->emitted_underline) { - strcat(buf, first ? "4" : ";4"); first = false; - } - if (p->reverse && !p->emitted_reverse) { - strcat(buf, first ? "7" : ";7"); first = false; - } - if (p->strikethrough && !p->emitted_strikethrough) { - strcat(buf, first ? "9" : ";9"); first = false; - } - if (p->fg_color != p->emitted_fg && p->fg_color != 0) { - char tmp[20]; - if (p->fg_color >= 1000 && p->fg_color < 2000) { - snprintf(tmp, sizeof(tmp), "%s38;5;%d", first ? "" : ";", p->fg_color - 1000); - } else { - snprintf(tmp, sizeof(tmp), "%s%d", first ? "" : ";", p->fg_color); - } - strcat(buf, tmp); first = false; - } - if (p->bg_color != p->emitted_bg && p->bg_color != 0) { - char tmp[20]; - if (p->bg_color >= 2000) { - snprintf(tmp, sizeof(tmp), "%s48;5;%d", first ? "" : ";", p->bg_color - 2000); - } else { - snprintf(tmp, sizeof(tmp), "%s%d", first ? "" : ";", p->bg_color); - } - strcat(buf, tmp); first = false; - } - - if (!first) { - zstr_cat(p->out, "\033["); - zstr_cat(p->out, buf); - zstr_cat(p->out, "m"); - } - } - - /* Update emitted state to match desired */ - p->emitted_bold = p->bold; - p->emitted_dim = p->dim; - p->emitted_italic = p->italic; - p->emitted_underline = p->underline; - p->emitted_reverse = p->reverse; - p->emitted_strikethrough = p->strikethrough; - p->emitted_fg = p->fg_color; - p->emitted_bg = p->bg_color; - p->dirty = false; -} - -/* Push a restore entry onto the stack */ -static void push_attr(TokenParser *p, AttrType type, int prev_value) { - if (p->stack_depth < MAX_STACK_DEPTH) { - p->stack[p->stack_depth].type = type; - p->stack[p->stack_depth].prev_value = prev_value; - p->stack[p->stack_depth].count = 1; - p->stack_depth++; - } -} - -/* Push a composite (multiple attrs at once) */ -static void push_composite(TokenParser *p, int count) { - if (p->stack_depth < MAX_STACK_DEPTH) { - p->stack[p->stack_depth].type = ATTR_COMPOSITE; - p->stack[p->stack_depth].prev_value = 0; - p->stack[p->stack_depth].count = count; - p->stack_depth++; - } -} - -/* Restore an attribute to its previous value (deferred - just updates state) */ -static void restore_attr(TokenParser *p, AttrType type, int prev_value) { - switch (type) { - case ATTR_BOLD: - p->bold = prev_value; - break; - case ATTR_DIM: - p->dim = prev_value; - break; - case ATTR_ITALIC: - p->italic = prev_value; - break; - case ATTR_UNDERLINE: - p->underline = prev_value; - break; - case ATTR_REVERSE: - p->reverse = prev_value; - break; - case ATTR_STRIKETHROUGH: - p->strikethrough = prev_value; - break; - case ATTR_FG: - p->fg_color = prev_value; - break; - case ATTR_BG: - p->bg_color = prev_value; - break; - default: - break; - } -} - -/* Pop one entry from the stack and restore */ -static void pop_style(TokenParser *p) { - if (p->stack_depth > 0) { - p->stack_depth--; - StackEntry *e = &p->stack[p->stack_depth]; - - if (e->type == ATTR_COMPOSITE) { - /* Pop multiple individual entries */ - for (int i = 0; i < e->count && p->stack_depth > 0; i++) { - p->stack_depth--; - StackEntry *inner = &p->stack[p->stack_depth]; - restore_attr(p, inner->type, inner->prev_value); - } - } else { - restore_attr(p, e->type, e->prev_value); - } - mark_dirty(p); - } -} - -/* Check if any styles are active */ -static bool has_active_styles(TokenParser *p) { - return p->bold || p->dim || p->italic || p->underline || - p->reverse || p->strikethrough || p->fg_color != 0 || p->bg_color != 0; -} - -/* Reset all styles and clear stack (deferred - just updates state) */ -static void reset_all(TokenParser *p) { - p->fg_color = 0; - p->bg_color = 0; - p->bold = false; - p->dim = false; - p->italic = false; - p->underline = false; - p->reverse = false; - p->strikethrough = false; - p->stack_depth = 0; - mark_dirty(p); -} - -/* Reset all styles at newline - emits immediately since newline follows */ -static void reset_line_styles(TokenParser *p) { - /* Check if emitted state has any active styles */ - if (p->emitted_bold || p->emitted_dim || p->emitted_italic || - p->emitted_underline || p->emitted_reverse || p->emitted_strikethrough || - p->emitted_fg != 0 || p->emitted_bg != 0) { - emit_ansi(p, "\033[0m"); - /* Update emitted state */ - p->emitted_bold = false; - p->emitted_dim = false; - p->emitted_italic = false; - p->emitted_underline = false; - p->emitted_reverse = false; - p->emitted_strikethrough = false; - p->emitted_fg = 0; - p->emitted_bg = 0; - } - /* Reset desired state too */ - p->fg_color = 0; - p->bg_color = 0; - p->bold = false; - p->dim = false; - p->italic = false; - p->underline = false; - p->reverse = false; - p->strikethrough = false; - p->dirty = false; - /* Keep stack_depth as-is so {/} still works across lines if needed */ -} - -/* Style application functions - deferred emission (just update state) */ -static void apply_bold(TokenParser *p) { - push_attr(p, ATTR_BOLD, p->bold); - p->bold = true; - mark_dirty(p); -} - -static void apply_dim(TokenParser *p) { - push_attr(p, ATTR_DIM, p->dim); - p->dim = true; - mark_dirty(p); -} - -static void apply_italic(TokenParser *p) { - push_attr(p, ATTR_ITALIC, p->italic); - p->italic = true; - mark_dirty(p); -} - -static void apply_underline(TokenParser *p) { - push_attr(p, ATTR_UNDERLINE, p->underline); - p->underline = true; - mark_dirty(p); -} - -static void apply_reverse(TokenParser *p) { - push_attr(p, ATTR_REVERSE, p->reverse); - p->reverse = true; - mark_dirty(p); -} - -static void apply_strikethrough(TokenParser *p) { - push_attr(p, ATTR_STRIKETHROUGH, p->strikethrough); - p->strikethrough = true; - mark_dirty(p); -} - -static void apply_fg_code(TokenParser *p, int code) { - push_attr(p, ATTR_FG, p->fg_color); - p->fg_color = code; - mark_dirty(p); -} - -static void apply_fg_256(TokenParser *p, int n) { - push_attr(p, ATTR_FG, p->fg_color); - p->fg_color = 1000 + n; /* Marker for 256-color */ - mark_dirty(p); -} - -static void apply_bg_code(TokenParser *p, int code) { - push_attr(p, ATTR_BG, p->bg_color); - p->bg_color = code; - mark_dirty(p); -} - -static void apply_bg_256(TokenParser *p, int n) { - push_attr(p, ATTR_BG, p->bg_color); - p->bg_color = 2000 + n; /* Marker for 256-color bg */ - mark_dirty(p); -} - -/* Semantic token applications - deferred emission (just update state) */ -static void apply_token_b(TokenParser *p) { - /* {b} = bold only (same as strong) */ - push_attr(p, ATTR_BOLD, p->bold); - p->bold = true; - mark_dirty(p); -} - -static void apply_token_highlight(TokenParser *p) { - /* {highlight} = bold + yellow */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 33; /* Yellow */ - mark_dirty(p); -} - -static void apply_token_h1(TokenParser *p) { - /* {h1} = bold + orange (256-color 214) */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 1214; /* 256-color marker for 214 */ - mark_dirty(p); -} - -static void apply_token_h2(TokenParser *p) { - /* {h2} = bold + blue */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 34; /* Blue */ - mark_dirty(p); -} - -static void apply_token_h3(TokenParser *p) { - /* {h3} = bold + white */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 37; /* White */ - mark_dirty(p); -} - -static void apply_token_h4(TokenParser *p) { - /* {h4} = bold + white */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 37; - mark_dirty(p); -} - -static void apply_token_h5(TokenParser *p) { - /* {h5} = bold + white */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 37; - mark_dirty(p); -} - -static void apply_token_h6(TokenParser *p) { - /* {h6} = bold + white */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_FG, p->fg_color); - push_composite(p, 2); - p->bold = true; - p->fg_color = 37; - mark_dirty(p); -} - -static void apply_token_strong(TokenParser *p) { - /* {strong} = bold */ - push_attr(p, ATTR_BOLD, p->bold); - p->bold = true; - mark_dirty(p); -} - -static void apply_token_dim_style(TokenParser *p) { - /* {dim} = white (standard color, softer than bright white) */ - push_attr(p, ATTR_FG, p->fg_color); - p->fg_color = 37; /* White */ - mark_dirty(p); -} - -static void apply_token_dark_style(TokenParser *p) { - /* {dark} = 256-color 245 (medium gray, for TUI secondary text) */ - push_attr(p, ATTR_FG, p->fg_color); - p->fg_color = 1245; /* 256-color marker for 245 */ - mark_dirty(p); -} - -static void apply_token_section(TokenParser *p) { - /* {section} = bold + subtle dark gray background (256-color 237) */ - push_attr(p, ATTR_BOLD, p->bold); - push_attr(p, ATTR_BG, p->bg_color); - push_composite(p, 2); - p->bold = true; - p->bg_color = 2237; /* 256-color marker for 237 */ - mark_dirty(p); -} - -static void apply_token_danger(TokenParser *p) { - /* {danger} = dark red background (256-color 52) */ - push_attr(p, ATTR_BG, p->bg_color); - p->bg_color = 2052; /* 256-color marker for 52 */ - mark_dirty(p); -} - -static void apply_token_text(TokenParser *p) { - /* {text} = full reset */ - reset_all(p); -} - -/* Brighten current foreground color */ -static void apply_bright(TokenParser *p) { - push_attr(p, ATTR_FG, p->fg_color); - /* If current color is 30-37 (standard), convert to 90-97 (bright) */ - if (p->fg_color >= 30 && p->fg_color <= 37) { - p->fg_color = p->fg_color + 60; - } else { - /* Already bright or custom - just set bright white */ - p->fg_color = 97; - } - mark_dirty(p); -} - -/* Parse a number from string, returns -1 on failure */ -static int parse_number(const char *start, const char *end) { - int n = 0; - for (const char *p = start; p < end; p++) { - if (*p < '0' || *p > '9') return -1; - n = n * 10 + (*p - '0'); - } - return n; -} - -%%{ - machine token_parser; - - # Character classes - num_char = [0-9]; - - action mark_start { tok_start = p; } - action mark_end { tok_end = p; } - - # Actions use % to fire AFTER the pattern is complete (leaving action) - - action emit_char { - zstr_push(parser.out, *p); - parser.visual_col++; - } - - action handle_newline { - zstr_push(parser.out, *p); - parser.visual_col = 1; - parser.visual_row++; - } - - action cursor_mark { - parser.cursor_col = parser.visual_col; - parser.cursor_row = parser.visual_row; - parser.has_cursor = true; - } - - action emit_goto_cursor { - if (parser.cursor_row > 0 && parser.cursor_col > 0) { - emit_ansi_num(&parser, "\033[", parser.cursor_row, ";"); - emit_ansi_num(&parser, "", parser.cursor_col, "H"); - } - } - - action pop_style { pop_style(&parser); } - action reset_all { reset_all(&parser); } - action reset_fg { - push_attr(&parser, ATTR_FG, parser.fg_color); - parser.fg_color = 0; - mark_dirty(&parser); - } - action reset_bg { - push_attr(&parser, ATTR_BG, parser.bg_color); - parser.bg_color = 0; - mark_dirty(&parser); - } - - # Semantic tokens - action tok_b { apply_token_b(&parser); } - action tok_highlight { apply_token_highlight(&parser); } - action tok_h1 { apply_token_h1(&parser); } - action tok_h2 { apply_token_h2(&parser); } - action tok_h3 { apply_token_h3(&parser); } - action tok_h4 { apply_token_h4(&parser); } - action tok_h5 { apply_token_h5(&parser); } - action tok_h6 { apply_token_h6(&parser); } - action tok_strong { apply_token_strong(&parser); } - action tok_dim { apply_token_dim_style(&parser); } - action tok_dark { apply_token_dark_style(&parser); } - action tok_section { apply_token_section(&parser); } - action tok_danger { apply_token_danger(&parser); } - action tok_text { apply_token_text(&parser); } - - # Attribute tokens - action tok_bold { apply_bold(&parser); } - action tok_italic { apply_italic(&parser); } - action tok_underline { apply_underline(&parser); } - action tok_reverse { apply_reverse(&parser); } - action tok_strikethrough { apply_strikethrough(&parser); } - action tok_bright { apply_bright(&parser); } - - # Color tokens - standard foreground - action fg_black { apply_fg_code(&parser, 30); } - action fg_red { apply_fg_code(&parser, 31); } - action fg_green { apply_fg_code(&parser, 32); } - action fg_yellow { apply_fg_code(&parser, 33); } - action fg_blue { apply_fg_code(&parser, 34); } - action fg_magenta { apply_fg_code(&parser, 35); } - action fg_cyan { apply_fg_code(&parser, 36); } - action fg_white { apply_fg_code(&parser, 37); } - action fg_gray { apply_fg_code(&parser, 90); } - - # Color tokens - bright foreground - action fg_bright_black { apply_fg_code(&parser, 90); } - action fg_bright_red { apply_fg_code(&parser, 91); } - action fg_bright_green { apply_fg_code(&parser, 92); } - action fg_bright_yellow { apply_fg_code(&parser, 93); } - action fg_bright_blue { apply_fg_code(&parser, 94); } - action fg_bright_magenta { apply_fg_code(&parser, 95); } - action fg_bright_cyan { apply_fg_code(&parser, 96); } - action fg_bright_white { apply_fg_code(&parser, 97); } - - # Color tokens - standard background - action bg_black { apply_bg_code(&parser, 40); } - action bg_red { apply_bg_code(&parser, 41); } - action bg_green { apply_bg_code(&parser, 42); } - action bg_yellow { apply_bg_code(&parser, 43); } - action bg_blue { apply_bg_code(&parser, 44); } - action bg_magenta { apply_bg_code(&parser, 45); } - action bg_cyan { apply_bg_code(&parser, 46); } - action bg_white { apply_bg_code(&parser, 47); } - action bg_gray { apply_bg_code(&parser, 100); } - - # 256-color handling - action fg_256 { - int n = parse_number(tok_start, tok_end); - if (n >= 0 && n <= 255) { - apply_fg_256(&parser, n); - } - } - - action bg_256 { - int n = parse_number(tok_start, tok_end); - if (n >= 0 && n <= 255) { - apply_bg_256(&parser, n); - } - } - - # Token patterns - use % (leaving action) instead of @ (finishing action) - number = num_char+ >mark_start %mark_end; - - # Reset/pop tokens - # {/} or {/name} all pop one level (name is ignored for flexibility) - tok_pop = "{/" [a-z]* "}" %pop_style; - tok_reset = "{reset}" %reset_all; - tok_reset_fg = "{/fg}" %reset_fg; - tok_reset_bg = "{/bg}" %reset_bg; - - # Semantic tokens - tok_b_open = "{b}" %tok_b; - tok_highlight_open = "{highlight}" %tok_highlight; - tok_h1 = "{h1}" %tok_h1; - tok_h2 = "{h2}" %tok_h2; - tok_h3 = "{h3}" %tok_h3; - tok_h4 = "{h4}" %tok_h4; - tok_h5 = "{h5}" %tok_h5; - tok_h6 = "{h6}" %tok_h6; - tok_strong = "{strong}" %tok_strong; - tok_dim_style = "{dim}" %tok_dim; - tok_dark_style = "{dark}" %tok_dark; - tok_section_open = "{section}" %tok_section; - tok_danger_open = "{danger}" %tok_danger; - tok_strike_open = "{strike}" %tok_strikethrough; # Strikethrough text - tok_text = "{text}" %tok_text; - tok_cursor = "{cursor}" %cursor_mark; - - # Attribute tokens - tok_bold = ("{bold}" | "{B}") %tok_bold; - tok_italic = ("{italic}" | "{I}" | "{i}") %tok_italic; - tok_underline = ("{underline}" | "{U}" | "{u}") %tok_underline; - tok_reverse = "{reverse}" %tok_reverse; - tok_strikethrough = "{strikethrough}" %tok_strikethrough; - tok_bright = "{bright}" %tok_bright; - - # Standard foreground colors - tok_fg_black = "{black}" %fg_black; - tok_fg_red = "{red}" %fg_red; - tok_fg_green = "{green}" %fg_green; - tok_fg_yellow = "{yellow}" %fg_yellow; - tok_fg_blue = "{blue}" %fg_blue; - tok_fg_magenta = "{magenta}" %fg_magenta; - tok_fg_cyan = "{cyan}" %fg_cyan; - tok_fg_white = "{white}" %fg_white; - tok_fg_gray = ("{gray}" | "{grey}") %fg_gray; - - # Bright foreground colors - tok_fg_bright_black = "{bright:black}" %fg_bright_black; - tok_fg_bright_red = "{bright:red}" %fg_bright_red; - tok_fg_bright_green = "{bright:green}" %fg_bright_green; - tok_fg_bright_yellow = "{bright:yellow}" %fg_bright_yellow; - tok_fg_bright_blue = "{bright:blue}" %fg_bright_blue; - tok_fg_bright_magenta = "{bright:magenta}" %fg_bright_magenta; - tok_fg_bright_cyan = "{bright:cyan}" %fg_bright_cyan; - tok_fg_bright_white = "{bright:white}" %fg_bright_white; - - # Standard background colors - tok_bg_black = "{bg:black}" %bg_black; - tok_bg_red = "{bg:red}" %bg_red; - tok_bg_green = "{bg:green}" %bg_green; - tok_bg_yellow = "{bg:yellow}" %bg_yellow; - tok_bg_blue = "{bg:blue}" %bg_blue; - tok_bg_magenta = "{bg:magenta}" %bg_magenta; - tok_bg_cyan = "{bg:cyan}" %bg_cyan; - tok_bg_white = "{bg:white}" %bg_white; - tok_bg_gray = ("{bg:gray}" | "{bg:grey}") %bg_gray; - - # 256-color tokens - tok_fg_256 = "{fg:" number "}" %fg_256; - tok_bg_256 = "{bg:" number "}" %bg_256; - - # Control sequence tokens (for convenience in TUI code) - action emit_clear_line { emit_ansi(&parser, "\033[K"); } - action emit_clear_screen { emit_ansi(&parser, "\033[J"); } - action emit_home { emit_ansi(&parser, "\033[H"); } - action emit_hide_cursor { emit_ansi(&parser, "\033[?25l"); } - action emit_show_cursor { emit_ansi(&parser, "\033[?25h"); } - - # Cursor positioning: {goto:row,col} - action mark_row_start { goto_row_start = p; } - action mark_row_end { goto_row_end = p; } - action mark_col_start { goto_col_start = p; } - action mark_col_end { goto_col_end = p; } - action emit_goto { - int row = parse_number(goto_row_start, goto_row_end); - int col = parse_number(goto_col_start, goto_col_end); - if (row >= 0 && col >= 0) { - emit_ansi_num(&parser, "\033[", row, ";"); - emit_ansi_num(&parser, "", col, "H"); - } - } - - tok_clear_line = "{clr}" %emit_clear_line; - tok_clear_screen = "{cls}" %emit_clear_screen; - tok_home = "{home}" %emit_home; - tok_hide_cursor = "{hide}" %emit_hide_cursor; - tok_show_cursor = "{show}" %emit_show_cursor; - tok_goto = "{goto:" (num_char+ >mark_row_start %mark_row_end) "," (num_char+ >mark_col_start %mark_col_end) "}" %emit_goto; - tok_goto_cursor = "{goto_cursor}" %emit_goto_cursor; - - # All tokens combined - token = ( - tok_pop | - tok_reset | - tok_reset_fg | - tok_reset_bg | - tok_b_open | - tok_highlight_open | - tok_h1 | - tok_h2 | - tok_h3 | - tok_h4 | - tok_h5 | - tok_h6 | - tok_strong | - tok_dim_style | - tok_dark_style | - tok_section_open | - tok_danger_open | - tok_strike_open | - tok_text | - tok_cursor | - tok_bold | - tok_italic | - tok_underline | - tok_reverse | - tok_strikethrough | - tok_bright | - tok_fg_black | - tok_fg_red | - tok_fg_green | - tok_fg_yellow | - tok_fg_blue | - tok_fg_magenta | - tok_fg_cyan | - tok_fg_white | - tok_fg_gray | - tok_fg_bright_black | - tok_fg_bright_red | - tok_fg_bright_green | - tok_fg_bright_yellow | - tok_fg_bright_blue | - tok_fg_bright_magenta | - tok_fg_bright_cyan | - tok_fg_bright_white | - tok_bg_black | - tok_bg_red | - tok_bg_green | - tok_bg_yellow | - tok_bg_blue | - tok_bg_magenta | - tok_bg_cyan | - tok_bg_white | - tok_bg_gray | - tok_fg_256 | - tok_bg_256 | - tok_clear_line | - tok_clear_screen | - tok_home | - tok_hide_cursor | - tok_show_cursor | - tok_goto | - tok_goto_cursor - ); - - # Helper action to emit the matched token text - action emit_token { - for (const char *c = ts; c < te; c++) { - zstr_push(parser.out, *c); - if (*c != '\033' && *c != '[' && (*c < '0' || *c > '9') && *c != ';') { - if (*c != '\n') { - parser.visual_col++; - } else { - parser.visual_col = 1; - } - } - } - } - - action emit_regular_char { - sync_styles(&parser); /* Emit any pending style changes */ - zstr_push(parser.out, *ts); - parser.visual_col++; - } - - action emit_newline { - /* Auto-reset all active styles before newline */ - reset_line_styles(&parser); - zstr_push(parser.out, *ts); - parser.visual_col = 1; - } - - action emit_ansi_passthrough { - for (const char *c = ts; c < te; c++) { - zstr_push(parser.out, *c); - } - } - - # ANSI escape sequence passthrough (don't count as visible) - ansi_seq = 0x1B '[' (any - alpha)* alpha; - - # Regular character (not start of token, not escape, not newline) - regular_char = (any - '{' - 0x1B - '\n'); - - # Newline handling - newline = '\n'; - - # Unrecognized { - just emit it - open_brace = '{'; - - # Main scanner using |* for proper longest-match scanning - # Use => for scanner actions (fire on token acceptance) - main := |* - token => {}; - ansi_seq => emit_ansi_passthrough; - newline => emit_newline; - regular_char => emit_regular_char; - open_brace => emit_regular_char; - *|; - - write data; -}%% - -/* Global flags */ -bool zstr_disable_token_expansion = false; -bool zstr_no_colors = false; - -TokenExpansion expand_tokens_with_cursor(const char *text) { - TokenExpansion result = {0}; - result.expanded = zstr_init(); - result.cursor_col = -1; - result.cursor_row = -1; - result.has_cursor = false; - result.final_col = 1; - result.final_row = 1; - - if (!text || !*text) { - return result; - } - - /* If expansion is disabled, just copy */ - if (zstr_disable_token_expansion) { - zstr_cat(&result.expanded, text); - return result; - } - - /* Pre-reserve buffer space to avoid reallocations. - * Estimate: input length + overhead for ANSI codes (~50% extra) */ - size_t input_len = strlen(text); - zstr_reserve(&result.expanded, input_len + input_len / 2 + 64); - - /* Initialize parser state */ - TokenParser parser = {0}; - parser.out = &result.expanded; - parser.cursor_col = -1; - parser.cursor_row = -1; - parser.visual_col = 1; - parser.visual_row = 1; - parser.no_colors = zstr_no_colors; - - /* Ragel variables */ - int cs; - int act; - const char *p = text; - const char *pe = text + strlen(text); - const char *eof = pe; - const char *ts = NULL; /* Token start */ - const char *te = NULL; /* Token end */ - const char *tok_start = NULL; - const char *tok_end = NULL; - const char *goto_row_start = NULL; - const char *goto_row_end = NULL; - const char *goto_col_start = NULL; - const char *goto_col_end = NULL; - - (void)tok_start; - (void)tok_end; - (void)goto_row_start; - (void)goto_row_end; - (void)goto_col_start; - (void)goto_col_end; - (void)eof; - (void)ts; - (void)te; - (void)act; - (void)token_parser_en_main; - - %% write init; - %% write exec; - - result.cursor_col = parser.cursor_col; - result.cursor_row = parser.cursor_row; - result.has_cursor = parser.has_cursor; - result.final_col = parser.visual_col; - result.final_row = parser.visual_row; - return result; -} - -zstr expand_tokens(const char *text) { - TokenExpansion result = expand_tokens_with_cursor(text); - return result.expanded; -} - -void zstr_expand_to(FILE *stream, const char *text) { - if (!text || !*text) return; - - Z_CLEANUP(zstr_free) zstr expanded = expand_tokens(text); - fwrite(zstr_cstr(&expanded), 1, zstr_len(&expanded), stream); -} - -void token_expansion_render(FILE *stream, TokenExpansion *te) { - if (!te) return; - - /* Write the expanded content */ - fwrite(zstr_cstr(&te->expanded), 1, zstr_len(&te->expanded), stream); - - /* If cursor was marked, clear to end of line and position cursor */ - if (te->has_cursor) { - /* Clear from current position to end of line (gives cursor room) */ - if (!zstr_no_colors) { - fprintf(stream, "\033[K"); - } - - /* Position cursor at marked location and show it */ - if (!zstr_no_colors && te->cursor_row > 0 && te->cursor_col > 0) { - fprintf(stream, "\033[%d;%dH\033[?25h", te->cursor_row, te->cursor_col); - } - } -} diff --git a/src/tokens_test.c b/src/tokens_test.c deleted file mode 100644 index 5de7eab..0000000 --- a/src/tokens_test.c +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Token system unit tests - * Uses acutest: https://github.com/mity/acutest - */ - -#include "acutest.h" -#include "tokens.h" -#include - -/* Helper to check if a string contains a substring */ -static int contains(const char *haystack, const char *needle) { - return strstr(haystack, needle) != NULL; -} - -/* Helper to count occurrences of a substring */ -static int count_occurrences(const char *haystack, const char *needle) { - int count = 0; - const char *p = haystack; - size_t needle_len = strlen(needle); - while ((p = strstr(p, needle)) != NULL) { - count++; - p += needle_len; - } - return count; -} - -/* - * Test: {b} token should emit just bold, not bold+yellow - */ -void test_b_is_just_bold(void) { - zstr result = expand_tokens("{b}test{/}"); - const char *s = zstr_cstr(&result); - - /* Should contain bold code */ - TEST_CHECK(contains(s, "\033[1m") || contains(s, "\033[0;1m")); - /* Should NOT contain yellow code 33 */ - TEST_CHECK(!contains(s, ";33m") && !contains(s, "[33m")); - TEST_CHECK(contains(s, "test")); - - zstr_free(&result); -} - -/* - * Test: {highlight} token should emit bold+yellow - */ -void test_highlight_is_bold_yellow(void) { - zstr result = expand_tokens("{highlight}test{/}"); - const char *s = zstr_cstr(&result); - - /* Should contain both bold and yellow */ - TEST_CHECK(contains(s, "1") && contains(s, "33")); - TEST_CHECK(contains(s, "test")); - - zstr_free(&result); -} - -/* - * Test: Redundant tokens should not emit multiple codes - * "{dim}{dim}{dim}b" should emit only one \033[90m - */ -void test_redundant_tokens_no_duplicate_codes(void) { - zstr result = expand_tokens("{dim}{dim}{dim}b"); - const char *s = zstr_cstr(&result); - - /* Should contain exactly one dim code (37 = white) */ - int count = count_occurrences(s, "\033[37m"); - TEST_CHECK_(count == 1, "Expected 1 dim code, got %d", count); - TEST_CHECK(contains(s, "b")); - - zstr_free(&result); -} - -/* - * Test: Deferred emission - codes only emit when there's a character - * "{b}{/}x" should emit just "x" with no ANSI codes - */ -void test_deferred_emission_no_unused_codes(void) { - zstr result = expand_tokens("{b}{/}x"); - const char *s = zstr_cstr(&result); - - /* Should just be "x" with no escape codes */ - TEST_CHECK_(!contains(s, "\033"), "Expected no ANSI codes, got: %s", s); - TEST_CHECK(strcmp(s, "x") == 0); - - zstr_free(&result); -} - -/* - * Test: Auto-reset at newlines - * "{b}bold text\nnormal" should have reset before newline - */ -void test_auto_reset_at_newline(void) { - zstr result = expand_tokens("{b}bold text\nnormal"); - const char *s = zstr_cstr(&result); - - /* Should contain reset code before the newline */ - const char *newline_pos = strchr(s, '\n'); - TEST_CHECK(newline_pos != NULL); - - /* The reset (0m) should appear before the newline */ - const char *reset_pos = strstr(s, "\033[0m"); - TEST_CHECK(reset_pos != NULL); - TEST_CHECK(reset_pos < newline_pos); - - /* "normal" should NOT have bold applied (comes after newline) */ - const char *normal_start = newline_pos + 1; - /* Check that there's no bold code right before "normal" */ - TEST_CHECK(strncmp(normal_start, "normal", 6) == 0); - - zstr_free(&result); -} - -/* - * Test: Stack-based nesting works correctly - */ -void test_stack_nesting(void) { - zstr result = expand_tokens("{bold}{red}both{/}just bold{/}normal"); - const char *s = zstr_cstr(&result); - - TEST_CHECK(contains(s, "both")); - TEST_CHECK(contains(s, "just bold")); - TEST_CHECK(contains(s, "normal")); - - zstr_free(&result); -} - -/* - * Test: {danger} token works (renamed from {strike}) - */ -void test_danger_token(void) { - zstr result = expand_tokens("{danger}warning{/}"); - const char *s = zstr_cstr(&result); - - /* Should contain background color 48;5;52 (dark red bg) */ - TEST_CHECK(contains(s, "48;5;52") || contains(s, "48;5")); - TEST_CHECK(contains(s, "warning")); - - zstr_free(&result); -} - -/* - * Test: {strike} is strikethrough text - */ -void test_strike_is_strikethrough(void) { - zstr result = expand_tokens("{strike}crossed{/}"); - const char *s = zstr_cstr(&result); - - /* Should contain strikethrough code (9) */ - TEST_CHECK(contains(s, "9") || contains(s, ";9")); - TEST_CHECK(contains(s, "crossed")); - - zstr_free(&result); -} - -/* - * Test: {/name} works as generic pop (e.g., {/highlight}) - */ -void test_generic_pop(void) { - zstr result1 = expand_tokens("{highlight}a{/highlight}b"); - zstr result2 = expand_tokens("{highlight}a{/}b"); - - /* Both should produce same output */ - TEST_CHECK(strcmp(zstr_cstr(&result1), zstr_cstr(&result2)) == 0); - - zstr_free(&result1); - zstr_free(&result2); -} - -/* - * Test: Heading tokens work - */ -void test_heading_tokens(void) { - zstr h1 = expand_tokens("{h1}H1{/}"); - zstr h2 = expand_tokens("{h2}H2{/}"); - zstr h3 = expand_tokens("{h3}H3{/}"); - - /* All should contain bold (1) */ - TEST_CHECK(contains(zstr_cstr(&h1), "H1")); - TEST_CHECK(contains(zstr_cstr(&h2), "H2")); - TEST_CHECK(contains(zstr_cstr(&h3), "H3")); - - /* h1 should have 256-color orange (38;5;214) */ - TEST_CHECK(contains(zstr_cstr(&h1), "38;5;214")); - - /* h2 should have blue (34) */ - TEST_CHECK(contains(zstr_cstr(&h2), "34")); - - zstr_free(&h1); - zstr_free(&h2); - zstr_free(&h3); -} - -/* - * Test: 256-color foreground - */ -void test_256_color_fg(void) { - zstr result = expand_tokens("{fg:214}orange{/}"); - const char *s = zstr_cstr(&result); - - TEST_CHECK(contains(s, "38;5;214")); - TEST_CHECK(contains(s, "orange")); - - zstr_free(&result); -} - -/* - * Test: 256-color background - */ -void test_256_color_bg(void) { - zstr result = expand_tokens("{bg:52}darkred{/}"); - const char *s = zstr_cstr(&result); - - TEST_CHECK(contains(s, "48;5;52")); - TEST_CHECK(contains(s, "darkred")); - - zstr_free(&result); -} - -/* - * Test: Cursor position tracking - */ -void test_cursor_tracking(void) { - TokenExpansion te = expand_tokens_with_cursor("Hello {cursor}World"); - - /* Cursor should be at column 7 (after "Hello "), row 1 */ - TEST_CHECK_(te.cursor_col == 7, "Expected cursor_col at 7, got %d", te.cursor_col); - TEST_CHECK_(te.cursor_row == 1, "Expected cursor_row at 1, got %d", te.cursor_row); - TEST_CHECK_(te.has_cursor == true, "Expected has_cursor to be true"); - TEST_CHECK(contains(zstr_cstr(&te.expanded), "Hello")); - TEST_CHECK(contains(zstr_cstr(&te.expanded), "World")); - - token_expansion_free(&te); -} - -/* - * Test: Control tokens ({clr}, {home}, etc.) - */ -void test_control_tokens(void) { - zstr clr = expand_tokens("{clr}"); - zstr home = expand_tokens("{home}"); - zstr hide = expand_tokens("{hide}"); - zstr show = expand_tokens("{show}"); - - TEST_CHECK(strcmp(zstr_cstr(&clr), "\033[K") == 0); - TEST_CHECK(strcmp(zstr_cstr(&home), "\033[H") == 0); - TEST_CHECK(strcmp(zstr_cstr(&hide), "\033[?25l") == 0); - TEST_CHECK(strcmp(zstr_cstr(&show), "\033[?25h") == 0); - - zstr_free(&clr); - zstr_free(&home); - zstr_free(&hide); - zstr_free(&show); -} - -/* - * Test: zstr_no_colors flag disables output - */ -void test_no_colors_flag(void) { - zstr_no_colors = true; - zstr result = expand_tokens("{b}text{/}"); - - /* Should just be "text" with no ANSI codes */ - TEST_CHECK(strcmp(zstr_cstr(&result), "text") == 0); - - zstr_free(&result); - zstr_no_colors = false; -} - -/* - * Test: zstr_disable_token_expansion flag - */ -void test_disable_expansion_flag(void) { - zstr_disable_token_expansion = true; - zstr result = expand_tokens("{b}text{/}"); - - /* Should pass through unchanged */ - TEST_CHECK(strcmp(zstr_cstr(&result), "{b}text{/}") == 0); - - zstr_free(&result); - zstr_disable_token_expansion = false; -} - -/* - * Test: {strong} is same as {b} - */ -void test_strong_same_as_b(void) { - zstr b_result = expand_tokens("{b}x{/}"); - zstr strong_result = expand_tokens("{strong}x{/}"); - - /* Both should produce same output */ - TEST_CHECK(strcmp(zstr_cstr(&b_result), zstr_cstr(&strong_result)) == 0); - - zstr_free(&b_result); - zstr_free(&strong_result); -} - -/* - * Test: {dim} token - */ -void test_dim_token(void) { - zstr result = expand_tokens("{dim}dimmed{/}"); - const char *s = zstr_cstr(&result); - - /* Should contain code 37 (white - softer than bright white) */ - TEST_CHECK(contains(s, "37")); - TEST_CHECK(contains(s, "dimmed")); - - zstr_free(&result); -} - -/* - * Test: ANSI passthrough (existing escape sequences are preserved) - */ -void test_ansi_passthrough(void) { - zstr result = expand_tokens("hello\033[31mred\033[0mworld"); - const char *s = zstr_cstr(&result); - - /* Original ANSI codes should be preserved */ - TEST_CHECK(contains(s, "\033[31m")); - TEST_CHECK(contains(s, "\033[0m")); - TEST_CHECK(contains(s, "hello")); - TEST_CHECK(contains(s, "red")); - TEST_CHECK(contains(s, "world")); - - zstr_free(&result); -} - -/* - * Test: Empty input - */ -void test_empty_input(void) { - zstr result = expand_tokens(""); - TEST_CHECK(zstr_len(&result) == 0); - zstr_free(&result); - - zstr result2 = expand_tokens(NULL); - TEST_CHECK(zstr_len(&result2) == 0); - zstr_free(&result2); -} - -/* - * Test: Unrecognized tokens pass through - */ -void test_unrecognized_tokens(void) { - zstr result = expand_tokens("{unknown}text"); - const char *s = zstr_cstr(&result); - - /* Unrecognized {unknown} should be passed through as-is */ - TEST_CHECK(contains(s, "{unknown}")); - TEST_CHECK(contains(s, "text")); - - zstr_free(&result); -} - -/* - * Test: Tokens before newline don't output control sequences - * "{bold}\n" should just output "\n" with no ANSI codes - */ -void test_tokens_before_newline_no_codes(void) { - zstr result = expand_tokens("{bold}\n"); - const char *s = zstr_cstr(&result); - - /* Should just be a newline with no escape codes */ - TEST_CHECK_(strcmp(s, "\n") == 0, "Expected just newline, got: %s", s); - - zstr_free(&result); - - /* Multiple tokens before newline */ - zstr result2 = expand_tokens("{red}{blue}{bold}\n"); - const char *s2 = zstr_cstr(&result2); - TEST_CHECK_(strcmp(s2, "\n") == 0, "Expected just newline, got: %s", s2); - - zstr_free(&result2); -} - -/* - * Test: Pop back to same state doesn't emit redundant codes - * {green}a{blue}{/}b should emit green once, then output "ab" - * because after popping blue, we're back to green (already emitted) - */ -void test_pop_to_same_state_no_redundant_codes(void) { - zstr result = expand_tokens("{green}a{blue}{/}b"); - const char *s = zstr_cstr(&result); - - /* Should contain green code exactly once */ - int green_count = count_occurrences(s, "\033[32m"); - TEST_CHECK_(green_count == 1, "Expected 1 green code, got %d", green_count); - - /* Should NOT contain blue code (no character printed while blue was active) */ - TEST_CHECK_(!contains(s, "\033[34m"), "Should not contain blue code"); - - /* Should contain both a and b */ - TEST_CHECK(contains(s, "a")); - TEST_CHECK(contains(s, "b")); - - zstr_free(&result); -} - -/* - * Test: Multiple pushes with deferred emission - * Tokens before a character don't emit until the character - * {blue}{green}{blue}{red}{green}a should emit only one code (green) right before 'a' - */ -void test_multiple_pushes_deferred(void) { - zstr result = expand_tokens("{blue}{green}{blue}{red}{green}a"); - const char *s = zstr_cstr(&result); - - /* Should contain exactly one escape sequence (green) followed by 'a' */ - /* The format should be \033[32ma or similar */ - TEST_CHECK(contains(s, "32")); /* green color code */ - TEST_CHECK(contains(s, "a")); - - /* Count total escape sequences - should be exactly 1 */ - int esc_count = count_occurrences(s, "\033["); - TEST_CHECK_(esc_count == 1, "Expected 1 escape sequence, got %d", esc_count); - - zstr_free(&result); -} - -/* - * Test: Complex push/pop sequence - * After multiple pops, only emit if state actually changes from last emitted - */ -void test_complex_push_pop_sequence(void) { - /* {green}a{red}b{/}c - after pop, back to green, same as before red was pushed */ - zstr result = expand_tokens("{green}a{red}b{/}c"); - const char *s = zstr_cstr(&result); - - /* Should have green, red, then green again (because we popped back) */ - TEST_CHECK(contains(s, "a")); - TEST_CHECK(contains(s, "b")); - TEST_CHECK(contains(s, "c")); - - /* Count green codes - should be 2 (initial green for 'a', restored green for 'c') */ - int green_count = count_occurrences(s, "\033[32m"); - TEST_CHECK_(green_count == 2, "Expected 2 green codes, got %d", green_count); - - /* Count red codes - should be 1 (for 'b') */ - int red_count = count_occurrences(s, "\033[31m"); - TEST_CHECK_(red_count == 1, "Expected 1 red code, got %d", red_count); - - zstr_free(&result); -} - -/* - * Test list - */ -TEST_LIST = { - { "b_is_just_bold", test_b_is_just_bold }, - { "highlight_is_bold_yellow", test_highlight_is_bold_yellow }, - { "redundant_tokens_no_duplicate_codes", test_redundant_tokens_no_duplicate_codes }, - { "deferred_emission_no_unused_codes", test_deferred_emission_no_unused_codes }, - { "auto_reset_at_newline", test_auto_reset_at_newline }, - { "stack_nesting", test_stack_nesting }, - { "danger_token", test_danger_token }, - { "strike_is_strikethrough", test_strike_is_strikethrough }, - { "generic_pop", test_generic_pop }, - { "heading_tokens", test_heading_tokens }, - { "256_color_fg", test_256_color_fg }, - { "256_color_bg", test_256_color_bg }, - { "cursor_tracking", test_cursor_tracking }, - { "control_tokens", test_control_tokens }, - { "no_colors_flag", test_no_colors_flag }, - { "disable_expansion_flag", test_disable_expansion_flag }, - { "strong_same_as_b", test_strong_same_as_b }, - { "dim_token", test_dim_token }, - { "ansi_passthrough", test_ansi_passthrough }, - { "empty_input", test_empty_input }, - { "unrecognized_tokens", test_unrecognized_tokens }, - { "tokens_before_newline_no_codes", test_tokens_before_newline_no_codes }, - { "pop_to_same_state_no_redundant_codes", test_pop_to_same_state_no_redundant_codes }, - { "multiple_pushes_deferred", test_multiple_pushes_deferred }, - { "complex_push_pop_sequence", test_complex_push_pop_sequence }, - { NULL, NULL } -}; diff --git a/src/tui.c b/src/tui.c index 8a43fa8..1425d13 100644 --- a/src/tui.c +++ b/src/tui.c @@ -31,12 +31,25 @@ Z_VEC_GENERATE_IMPL(TryEntry *, TryEntryPtr) static vec_TryEntry all_tries = {0}; static vec_TryEntryPtr filtered_ptrs = {0}; -static zstr filter_buffer = {0}; -static int filter_cursor = 0; // Cursor position in filter_buffer +static TuiInput filter_input = {0}; static int selected_index = 0; static int scroll_offset = 0; static int marked_count = 0; // Number of items marked for deletion +// Memoized separator line +static zstr cached_sep_line = {0}; +static int cached_sep_width = 0; + +static const char *get_separator_line(int cols) { + if (cols != cached_sep_width) { + zstr_clear(&cached_sep_line); + for (int i = 0; i < cols; i++) + zstr_cat(&cached_sep_line, "─"); + cached_sep_width = cols; + } + return zstr_cstr(&cached_sep_line); +} + /* * SIGWINCH handler - called when terminal is resized. * We don't need to do anything here; the signal delivery itself @@ -65,8 +78,6 @@ static void clear_state(void) { // filtered_ptrs just contains pointers, no need to free entries vec_free_TryEntryPtr(&filtered_ptrs); - - filter_cursor = 0; } static int compare_tries_by_score(const void *a, const void *b) { @@ -117,7 +128,7 @@ static void scan_tries(const char *base_path) { static void filter_tries(void) { vec_clear_TryEntryPtr(&filtered_ptrs); - const char *query = zstr_cstr(&filter_buffer); + const char *query = zstr_cstr(&filter_input.text); TryEntry *iter; vec_foreach(&all_tries, iter) { @@ -126,7 +137,7 @@ static void filter_tries(void) { // Update score and rendered string fuzzy_match(entry, query); - if (zstr_len(&filter_buffer) > 0 && entry->score <= 0.0) { + if (zstr_len(&filter_input.text) > 0 && entry->score <= 0.0) { continue; } @@ -141,89 +152,6 @@ static void filter_tries(void) { } } -// Handle readline-style keybindings for text editing -// Returns true if the key was handled, false if it should be treated as regular input -static bool handle_readline_keybinding(zstr *buffer, int *cursor, int key) { - if (key == 1) { // Ctrl-A (move to start) - *cursor = 0; - return true; - } else if (key == 5) { // Ctrl-E (move to end) - *cursor = (int)zstr_len(buffer); - return true; - } else if (key == 2 || key == ARROW_LEFT) { // Ctrl-B or LEFT (move left) - if (*cursor > 0) - (*cursor)--; - return true; - } else if (key == 6 || key == ARROW_RIGHT) { // Ctrl-F or RIGHT (move right) - if (*cursor < (int)zstr_len(buffer)) - (*cursor)++; - return true; - } else if (key == 11) { // Ctrl-K (kill after cursor) - int buffer_len = (int)zstr_len(buffer); - if (*cursor < buffer_len) { - char *data = zstr_data(buffer); - Z_CLEANUP(zstr_free) zstr new_buffer = zstr_init(); - for (int i = 0; i < *cursor; i++) { - zstr_push(&new_buffer, data[i]); - } - zstr_free(buffer); - *buffer = new_buffer; - } - return true; - } else if (key == 23) { // Ctrl-W (kill word) - int buffer_len = (int)zstr_len(buffer); - if (*cursor > 0) { - char *data = zstr_data(buffer); - - // Move back past any trailing non-word characters - int end_pos = *cursor - 1; - while (end_pos >= 0 && !isalnum((unsigned char)data[end_pos])) { - end_pos--; - } - - // Move back past the word itself (alphanumeric characters) - int start_pos = end_pos; - while (start_pos >= 0 && isalnum((unsigned char)data[start_pos])) { - start_pos--; - } - start_pos++; // Move to the first char of the word - - // Build new buffer without the deleted word - Z_CLEANUP(zstr_free) zstr new_buffer = zstr_init(); - for (int i = 0; i < start_pos; i++) { - zstr_push(&new_buffer, data[i]); - } - for (int i = *cursor; i < buffer_len; i++) { - zstr_push(&new_buffer, data[i]); - } - - zstr_free(buffer); - *buffer = new_buffer; - *cursor = start_pos; - } - return true; - } else if (key == BACKSPACE || key == 127 || key == 8) { // Backspace, DEL, or Ctrl-H - if (*cursor > 0) { - // Delete character before cursor - int buffer_len = (int)zstr_len(buffer); - Z_CLEANUP(zstr_free) zstr new_buffer = zstr_init(); - char *data = zstr_data(buffer); - - for (int i = 0; i < buffer_len; i++) { - if (i != *cursor - 1) { // Skip the character before cursor - zstr_push(&new_buffer, data[i]); - } - } - - zstr_free(buffer); - *buffer = new_buffer; - (*cursor)--; - } - return true; - } - return false; -} - // Parse symbolic key name to key code // Supports: ENTER, RETURN, ESC, ESCAPE, UP, DOWN, LEFT, RIGHT, BACKSPACE, TAB, SPACE // Also: CTRL-X (where X is A-Z) @@ -263,13 +191,13 @@ static int parse_symbolic_key(const char *token, int len) { // Supports both raw escape sequences AND symbolic format (comma-separated) // Symbolic: "CTRL-J,DOWN,ENTER" or "beta,ENTER" // Raw: "\x0a\x1b[B\r" (legacy) -static int read_test_key(Mode *mode) { - if (mode->inject_keys[mode->key_index] == '\0') { +static int read_test_key(TestParams *test) { + if (test->inject_keys[test->key_index] == '\0') { return -1; // End of keys } - const char *keys = mode->inject_keys; - int *idx = &mode->key_index; + const char *keys = test->inject_keys; + int *idx = &test->key_index; // Skip leading comma while (keys[*idx] == ',') (*idx)++; @@ -334,9 +262,8 @@ static int read_test_key(Mode *mode) { // Render confirmation dialog for deletion // Returns true if user typed "YES", false otherwise -static bool render_delete_confirmation(const char *base_path, Mode *mode) { - int rows, cols; - get_window_size(&rows, &cols); +static bool render_delete_confirmation(const char *base_path, TestParams *test) { + (void)base_path; // Collect marked items vec_TryEntryPtr marked_items = {0}; @@ -346,173 +273,103 @@ static bool render_delete_confirmation(const char *base_path, Mode *mode) { } } - zstr confirm_input = zstr_init(); - int confirm_cursor = 0; + TuiInput input = tui_input_init(); + input.placeholder = "YES"; bool confirmed = false; - bool is_test = (mode && mode->inject_keys); + bool is_test = (test && test->inject_keys); + + int max_show = 10; + if (max_show > (int)marked_items.length) max_show = (int)marked_items.length; while (1) { - zstr_expand_to(stderr, "{hide}{home}"); + Tui t = tui_begin_screen(stderr); // Title - Z_CLEANUP(zstr_free) zstr title = zstr_from("{b}Delete "); - char count_str[32]; - snprintf(count_str, sizeof(count_str), "%zu", marked_items.length); - zstr_cat(&title, count_str); - zstr_cat(&title, " director"); - zstr_cat(&title, marked_items.length == 1 ? "y" : "ies"); - zstr_cat(&title, "?{/b}{clr}\n{clr}\n"); - zstr_expand_to(stderr, zstr_cstr(&title)); - - // List items (max 10) - int max_show = 10; - if (max_show > (int)marked_items.length) max_show = (int)marked_items.length; + TuiStyleString line = tui_screen_line(&t); + tui_printf(&line, TUI_BOLD, "Delete %zu director%s?", + marked_items.length, marked_items.length == 1 ? "y" : "ies"); + tui_screen_write(&t, &line); + tui_screen_empty(&t); + + // List items for (int i = 0; i < max_show; i++) { - Z_CLEANUP(zstr_free) zstr item = zstr_from(" {dark}-{reset} "); - zstr_cat(&item, zstr_cstr(&marked_items.data[i]->name)); - zstr_cat(&item, "{clr}\n"); - zstr_expand_to(stderr, zstr_cstr(&item)); + line = tui_screen_line(&t); + tui_print(&line, NULL, " "); + tui_print(&line, TUI_DARK, "-"); + tui_print(&line, NULL, " "); + tui_print(&line, NULL, zstr_cstr(&marked_items.data[i]->name)); + tui_screen_write(&t, &line); } if ((int)marked_items.length > max_show) { - char more[80]; - snprintf(more, sizeof(more), " {dark}...and %zu more{reset}{clr}\n", - marked_items.length - max_show); - zstr_expand_to(stderr, more); + line = tui_screen_line(&t); + tui_printf(&line, TUI_DARK, " ...and %zu more", marked_items.length - max_show); + tui_screen_write(&t, &line); } - // Prompt - Z_CLEANUP(zstr_free) zstr prompt = zstr_from("{clr}\n{dark}Type {/}{highlight}YES{/}{dark} to confirm:{reset} "); - - const char *confirm_cstr = zstr_cstr(&confirm_input); - int confirm_len = (int)zstr_len(&confirm_input); - - // Clamp cursor to valid range - int cursor = confirm_cursor; - if (cursor < 0) cursor = 0; - if (cursor > confirm_len) cursor = confirm_len; - - // Add text before cursor, cursor token, and text after cursor - zstr_cat_len(&prompt, confirm_cstr, cursor); - zstr_cat(&prompt, "{cursor}"); - zstr_cat_len(&prompt, confirm_cstr + cursor, confirm_len - cursor); - zstr_cat(&prompt, "\n{cls}"); + // Prompt line with input + tui_screen_empty(&t); + line = tui_screen_line(&t); + tui_print(&line, TUI_DARK, "Type "); + tui_print(&line, TUI_HIGHLIGHT, "YES"); + tui_print(&line, TUI_DARK, " to confirm: "); + tui_screen_input(&t, &input); + tui_clr(line.str); // Cut off overflow + tui_screen_write(&t, &line); - TokenExpansion te = zstr_expand_tokens_with_cursor(zstr_cstr(&prompt)); - token_expansion_render(stderr, &te); - token_expansion_free(&te); + tui_free(&t); // Read key - int c; - if (is_test) { - c = read_test_key(mode); - } else { - c = read_key(); - } + int c = is_test ? read_test_key(test) : read_key(); if (c == -1 || c == ESC_KEY || c == 3) { break; } else if (c == ENTER_KEY) { - // Check if input is exactly "YES" - if (strcmp(zstr_cstr(&confirm_input), "YES") == 0) { + if (strcmp(zstr_cstr(&input.text), "YES") == 0) { confirmed = true; } break; - } else if (handle_readline_keybinding(&confirm_input, &confirm_cursor, c)) { - // Keybinding was handled - } else if (!iscntrl(c) && c < 128) { - // Insert character at cursor position - int buffer_len = (int)zstr_len(&confirm_input); - Z_CLEANUP(zstr_free) zstr new_buffer = zstr_init(); - char *data = zstr_data(&confirm_input); - - for (int i = 0; i < buffer_len; i++) { - if (i == confirm_cursor) { - zstr_push(&new_buffer, (char)c); - } - zstr_push(&new_buffer, data[i]); - } - - // If cursor is at the end, just append - if (confirm_cursor >= buffer_len) { - zstr_push(&new_buffer, (char)c); - } - - zstr_free(&confirm_input); - confirm_input = new_buffer; - confirm_cursor++; + } else { + tui_input_handle_key(&input, c); } } vec_free_TryEntryPtr(&marked_items); - zstr_free(&confirm_input); - - (void)base_path; + tui_input_free(&input); return confirmed; } static void render(const char *base_path) { + (void)base_path; int rows, cols; get_window_size(&rows, &cols); + const char *sep = get_separator_line(cols); - zstr_expand_to(stderr, "{hide}{home}"); - - // Build separator line dynamically (handles any terminal width) - Z_CLEANUP(zstr_free) zstr sep_line = zstr_init(); - for (int i = 0; i < cols; i++) - zstr_cat(&sep_line, "─"); + Z_CLEANUP(tui_free) Tui t = tui_begin_screen(stderr); // Header - { - Z_CLEANUP(zstr_free) - zstr header_fmt = - zstr_from("{h1}🏠 Try Directory Selection{reset}{clr}\n{dark}"); - zstr_cat(&header_fmt, zstr_cstr(&sep_line)); - zstr_cat(&header_fmt, "{reset}{clr}\n"); - zstr_expand_to(stderr, zstr_cstr(&header_fmt)); - } - - // Search bar - track cursor position - int search_cursor_col = -1; - int search_line = 3; - { - Z_CLEANUP(zstr_free) - zstr search_fmt = zstr_from("{b}Search:{/b} "); - - // Add filter buffer up to cursor position - const char *filter_cstr = zstr_cstr(&filter_buffer); - int buffer_len = (int)zstr_len(&filter_buffer); - - // Clamp cursor to valid range - int cursor = filter_cursor; - if (cursor < 0) cursor = 0; - if (cursor > buffer_len) cursor = buffer_len; - - // Add text before cursor - for (int i = 0; i < cursor; i++) { - zstr_push(&search_fmt, filter_cstr[i]); - } - - zstr_cat(&search_fmt, "{cursor}"); - - // Add text after cursor - for (int i = cursor; i < buffer_len; i++) { - zstr_push(&search_fmt, filter_cstr[i]); - } - - zstr_cat(&search_fmt, "{clr}\n{dark}"); - zstr_cat(&search_fmt, zstr_cstr(&sep_line)); - zstr_cat(&search_fmt, "{reset}{clr}\n"); - - TokenExpansion search_exp = zstr_expand_tokens_with_cursor(zstr_cstr(&search_fmt)); - search_cursor_col = search_exp.cursor_col; - fwrite(zstr_cstr(&search_exp.expanded), 1, zstr_len(&search_exp.expanded), stderr); - token_expansion_free(&search_exp); - } - - // List - int list_height = rows - 8; - if (list_height < 1) - list_height = 1; + TuiStyleString line = tui_screen_line(&t); + tui_print(&line, TUI_H1, "🏠 Try Directory Selection"); + tui_screen_write_truncated(&t, &line, "… "); + + line = tui_screen_line(&t); + tui_print(&line, TUI_DARK, sep); + tui_screen_write_truncated(&t, &line, NULL); + + // Search bar with input + line = tui_screen_line(&t); + tui_print(&line, TUI_BOLD, "Search:"); + tui_print(&line, NULL, " "); + tui_screen_input(&t, &filter_input); + tui_clr(line.str); + tui_screen_write_truncated(&t, &line, "… "); + + line = tui_screen_line(&t); + tui_print(&line, TUI_DARK, sep); + tui_screen_write_truncated(&t, &line, NULL); + + // List (rows minus 4 header lines, 2 footer lines, and 1 for final newline) + int list_height = rows - 7; + if (list_height < 1) list_height = 1; if (selected_index < scroll_offset) scroll_offset = selected_index; @@ -524,227 +381,122 @@ static void render(const char *base_path) { if (idx < (int)filtered_ptrs.length) { TryEntry *entry = filtered_ptrs.data[idx]; - int is_selected = (idx == selected_index); - - Z_CLEANUP(zstr_free) zstr line = zstr_init(); + bool is_selected = (idx == selected_index); + bool is_marked = entry->marked_for_delete; - // Calculate available space - // Prefix is 2 chars ("→ " or " "), icon is 2 chars (emoji), space after icon is implicit in count - int prefix_len = 5; // 2 (arrow/spaces) + 2 (emoji) + 1 (space) + // Determine line background + const char *line_bg = NULL; + if (is_marked) { + line_bg = TUI_DANGER; + } else if (is_selected) { + line_bg = TUI_SELECTED; + } - // Build metadata strings + // Write right-aligned metadata first (will be partially overwritten) Z_CLEANUP(zstr_free) zstr rel_time = format_relative_time(entry->mtime); - char score_text[16]; - snprintf(score_text, sizeof(score_text), ", %.1f", entry->score); - - // Calculate max length for directory name - allow it to use almost full width - // Don't reserve space for metadata here; we'll check if it fits after - int max_name_len = cols - prefix_len - 1; // Just leave 1 char for safety - - int plain_len = zstr_len(&entry->name); - bool name_truncated = false; - - Z_CLEANUP(zstr_free) zstr display_name = zstr_init(); - if (plain_len > max_name_len && max_name_len > 4) { - // Truncate and add ellipsis - // Copy the rendered name but truncate it - const char *rendered = zstr_cstr(&entry->rendered); - // This is approximate since rendered has tokens, but good enough - int chars_to_copy = max_name_len - 1; // Leave room for ellipsis - - // Copy character by character, skipping tokens - int visible_count = 0; - const char *p = rendered; - while (*p && visible_count < chars_to_copy) { - if (*p == '{') { - // Copy token - zstr_push(&display_name, *p++); - while (*p && *p != '}') { - zstr_push(&display_name, *p++); - } - if (*p == '}') { - zstr_push(&display_name, *p++); - } - } else { - zstr_push(&display_name, *p++); - visible_count++; - } - } - zstr_cat(&display_name, "…"); - name_truncated = true; - } else { - zstr_cat(&display_name, zstr_cstr(&entry->rendered)); - } + char score_buf[16]; + snprintf(score_buf, sizeof(score_buf), ", %.1f", entry->score); - // Render the directory entry - bool is_marked = entry->marked_for_delete; + TuiStyleString ralign = tui_screen_line(&t); + tui_print(&ralign, TUI_DARK, zstr_cstr(&rel_time)); + tui_print(&ralign, TUI_DARK, score_buf); + tui_screen_rwrite(&t, &ralign, line_bg); + + // Now write main content over top (bg already set by rwrite) + line = tui_screen_line(&t); + if (line_bg) tui_push(&line, line_bg); + + // Render entry prefix and name if (is_selected) { - if (is_marked) { - zstr_cat(&line, "{section}{highlight}→ {/}🗑️ {danger}"); - zstr_cat(&line, zstr_cstr(&display_name)); - zstr_cat(&line, "{/}"); - } else { - zstr_cat(&line, "{section}{highlight}→ {/}📁 "); - zstr_cat(&line, zstr_cstr(&display_name)); - } + tui_print(&line, TUI_HIGHLIGHT, "→ "); + tui_print(&line, NULL, is_marked ? "🗑️ " : "📁 "); } else { - if (is_marked) { - zstr_cat(&line, " 🗑️ {danger}"); - zstr_cat(&line, zstr_cstr(&display_name)); - zstr_cat(&line, "{/}"); - } else { - zstr_cat(&line, " 📁 "); - zstr_cat(&line, zstr_cstr(&display_name)); - } + tui_print(&line, NULL, is_marked ? " 🗑️ " : " 📁 "); } + tui_print(&line, NULL, zstr_cstr(&entry->rendered)); + tui_putc(&line, ' '); // Trailing space (ignored by truncation) - // Build full metadata string - Z_CLEANUP(zstr_free) zstr full_meta = zstr_init(); - zstr_cat(&full_meta, zstr_cstr(&rel_time)); - zstr_cat(&full_meta, score_text); - int full_meta_len = (int)zstr_len(&full_meta); - - // Calculate positions - metadata is always right-aligned at screen edge - int actual_name_len = name_truncated ? max_name_len : plain_len; - int path_end_pos = prefix_len + actual_name_len; - int meta_end_pos = cols - 1; // -1 because cols is 1-indexed width - int meta_start_pos = meta_end_pos - full_meta_len; - int available_space = meta_start_pos - path_end_pos; - - // Show metadata if there's more than 2 chars gap, truncating from left if needed - if (available_space > 2) { - // Full metadata fits with gap - add padding and full metadata - int padding_len = available_space; - for (int p = 0; p < padding_len; p++) { - zstr_push(&line, ' '); - } - zstr_cat(&line, "{dark}"); - zstr_cat(&line, zstr_cstr(&full_meta)); - zstr_cat(&line, "{reset}"); - } else if (available_space > -full_meta_len + 3) { - // Partial overlap - show truncated metadata (cut from left) - // available_space can be negative if name extends into metadata area - int chars_to_skip = (available_space < 1) ? (1 - available_space) : 0; - int chars_to_show = full_meta_len - chars_to_skip; - if (chars_to_show > 2) { - zstr_cat(&line, " {dark}"); - const char *meta_str = zstr_cstr(&full_meta); - zstr_cat(&line, meta_str + chars_to_skip); - zstr_cat(&line, "{reset}"); - } - } + if (line_bg) tui_pop(&line); + tui_screen_write_truncated(&t, &line, "… "); - zstr_cat(&line, "{clr}\n"); - zstr_expand_to(stderr, zstr_cstr(&line)); + } else if (idx == (int)filtered_ptrs.length && zstr_len(&filter_input.text) > 0) { + // Separator before "Create new" + tui_screen_empty(&t); + i++; - } else if (idx == (int)filtered_ptrs.length && zstr_len(&filter_buffer) > 0) { - // Add separator line before "Create new" - zstr_expand_to(stderr, "{clr}\n"); - i++; // Skip next iteration since we used a line for separator - - // Generate preview of what the directory name will be + // Generate preview name time_t now = time(NULL); - struct tm *t = localtime(&now); + struct tm *tm = localtime(&now); char date_prefix[20]; - strftime(date_prefix, sizeof(date_prefix), "%Y-%m-%d", t); + strftime(date_prefix, sizeof(date_prefix), "%Y-%m-%d", tm); Z_CLEANUP(zstr_free) zstr preview = zstr_from(date_prefix); zstr_cat(&preview, "-"); - - // Add filter text with spaces replaced by dashes - const char *filter_text = zstr_cstr(&filter_buffer); - for (size_t j = 0; j < zstr_len(&filter_buffer); j++) { - if (isspace(filter_text[j])) { - zstr_push(&preview, '-'); - } else { - zstr_push(&preview, filter_text[j]); - } + const char *filter_text = zstr_cstr(&filter_input.text); + for (size_t j = 0; j < zstr_len(&filter_input.text); j++) { + zstr_push(&preview, isspace(filter_text[j]) ? '-' : filter_text[j]); } + line = (idx == selected_index) ? tui_screen_line_selected(&t) : tui_screen_line(&t); if (idx == selected_index) { - Z_CLEANUP(zstr_free) - zstr line = zstr_from("{section}{highlight}→ {/}📂 Create new: {dark}"); - zstr_cat(&line, zstr_cstr(&preview)); - zstr_cat(&line, "{/}{clr}\n"); - zstr_expand_to(stderr, zstr_cstr(&line)); + tui_print(&line, TUI_HIGHLIGHT, "→ "); } else { - Z_CLEANUP(zstr_free) - zstr line = zstr_from(" 📂 Create new: {dark}"); - zstr_cat(&line, zstr_cstr(&preview)); - zstr_cat(&line, "{reset}{clr}\n"); - zstr_expand_to(stderr, zstr_cstr(&line)); + tui_print(&line, NULL, " "); } + tui_print(&line, NULL, "📂 Create new: "); + tui_print(&line, TUI_DARK, zstr_cstr(&preview)); + tui_screen_write_truncated(&t, &line, "… "); } else { - zstr_expand_to(stderr, "{clr}\n"); + tui_screen_empty(&t); } } - zstr_expand_to(stderr, "{cls}"); - // Footer - { - Z_CLEANUP(zstr_free) zstr footer_fmt = zstr_from("{dark}"); - zstr_cat(&footer_fmt, zstr_cstr(&sep_line)); - zstr_cat(&footer_fmt, "{reset}{clr}\n"); - - if (marked_count > 0) { - // Delete mode footer - char count_str[32]; - snprintf(count_str, sizeof(count_str), "%d", marked_count); - zstr_cat(&footer_fmt, "{highlight}DELETE MODE{/} | "); - zstr_cat(&footer_fmt, count_str); - zstr_cat(&footer_fmt, " marked | {dark}Ctrl-D: Toggle Enter: Confirm Esc: Cancel{reset}{clr}\n"); - } else { - // Normal footer - zstr_cat(&footer_fmt, "{dark}↑/↓: Navigate Enter: Select Ctrl-D: Delete Esc: Cancel{reset}{clr}\n"); - } - - // Position cursor in search field and show it - if (search_cursor_col >= 0) { - char goto_cmd[32]; - snprintf(goto_cmd, sizeof(goto_cmd), "{goto:%d,%d}{show}", search_line, search_cursor_col); - zstr_cat(&footer_fmt, goto_cmd); - } else { - zstr_cat(&footer_fmt, "{show}"); - } - zstr_expand_to(stderr, zstr_cstr(&footer_fmt)); + line = tui_screen_line(&t); + tui_print(&line, TUI_DARK, sep); + tui_screen_write_truncated(&t, &line, NULL); + + line = tui_screen_line(&t); + if (marked_count > 0) { + tui_print(&line, TUI_HIGHLIGHT, "DELETE MODE"); + tui_printf(&line, NULL, " | %d marked | ", marked_count); + tui_print(&line, TUI_DARK, "Ctrl-D: Toggle Enter: Confirm Esc: Cancel"); + } else { + tui_print(&line, TUI_DARK, "↑/↓: Navigate Enter: Select Ctrl-D: Delete Esc: Cancel"); } - - (void)base_path; + tui_screen_write_truncated(&t, &line, NULL); + // tui_free(&t) called automatically via Z_CLEANUP } SelectionResult run_selector(const char *base_path, const char *initial_filter, - Mode *mode) { - // Initialize - if (zstr_len(&filter_buffer) == 0 && !filter_buffer.is_long) { - // First time - already zero-initialized - filter_buffer = zstr_init(); + TestParams *test) { + // Initialize filter input + if (zstr_len(&filter_input.text) == 0 && !filter_input.text.is_long) { + filter_input = tui_input_init(); } else { - zstr_clear(&filter_buffer); + tui_input_clear(&filter_input); } - filter_cursor = 0; - if (initial_filter) { - zstr_cat(&filter_buffer, initial_filter); - filter_cursor = (int)zstr_len(&filter_buffer); // Move cursor to end after initial filter + zstr_cat(&filter_input.text, initial_filter); + filter_input.cursor = (int)zstr_len(&filter_input.text); } scan_tries(base_path); filter_tries(); - bool is_test = (mode && (mode->render_once || mode->inject_keys)); + bool is_test = (test && (test->render_once || test->inject_keys)); // Test mode: render once and exit - if (is_test && mode->render_once) { + if (is_test && test->render_once) { render(base_path); SelectionResult result = {.type = ACTION_CANCEL, .path = zstr_init()}; return result; } // Only setup TTY if not in test mode or if we need to read keys - if (!is_test || !mode->inject_keys) { + if (!is_test || !test->inject_keys) { enable_raw_mode(); struct sigaction sa; @@ -761,14 +513,14 @@ SelectionResult run_selector(const char *base_path, SelectionResult result = {.type = ACTION_CANCEL, .path = zstr_init()}; while (1) { - if (!is_test || !mode->inject_keys) { + if (!is_test || !test->inject_keys) { render(base_path); } // Read key from injected keys or real input int c; - if (is_test && mode->inject_keys) { - c = read_test_key(mode); + if (is_test && test->inject_keys) { + c = read_test_key(test); } else { c = read_key(); } @@ -805,7 +557,7 @@ SelectionResult run_selector(const char *base_path, } else if (c == ENTER_KEY) { // If items are marked, show confirmation dialog if (marked_count > 0) { - bool confirmed = render_delete_confirmation(base_path, mode); + bool confirmed = render_delete_confirmation(base_path, test); if (confirmed) { // Collect all marked paths result.type = ACTION_DELETE; @@ -826,7 +578,7 @@ SelectionResult run_selector(const char *base_path, result.path = zstr_dup(&filtered_ptrs.data[selected_index]->path); } else { // Create new - validate and normalize name first - Z_CLEANUP(zstr_free) zstr normalized = normalize_dir_name(zstr_cstr(&filter_buffer)); + Z_CLEANUP(zstr_free) zstr normalized = normalize_dir_name(zstr_cstr(&filter_input.text)); if (zstr_len(&normalized) == 0) { // Invalid name - don't create directory break; @@ -851,51 +603,31 @@ SelectionResult run_selector(const char *base_path, selected_index--; } else if (c == ARROW_DOWN || c == 14) { // DOWN or Ctrl-N int max_idx = filtered_ptrs.length; - if (zstr_len(&filter_buffer) > 0) + if (zstr_len(&filter_input.text) > 0) max_idx++; if (selected_index < max_idx - 1) selected_index++; - } else if (handle_readline_keybinding(&filter_buffer, &filter_cursor, c)) { - // Readline keybinding was handled - re-filter if buffer changed - filter_tries(); - } else if (!iscntrl(c) && c < 128) { - // Insert character at cursor position - int buffer_len = (int)zstr_len(&filter_buffer); - Z_CLEANUP(zstr_free) zstr new_buffer = zstr_init(); - char *data = zstr_data(&filter_buffer); - - for (int i = 0; i < buffer_len; i++) { - if (i == filter_cursor) { - zstr_push(&new_buffer, (char)c); - } - zstr_push(&new_buffer, data[i]); - } - - // If cursor is at the end, just append - if (filter_cursor >= buffer_len) { - zstr_push(&new_buffer, (char)c); - } - - zstr_free(&filter_buffer); - filter_buffer = new_buffer; - filter_cursor++; + } else if (tui_input_handle_key(&filter_input, c)) { + // Input was handled - re-filter filter_tries(); } } - if (!is_test || !mode->inject_keys) { + if (!is_test || !test->inject_keys) { // Disable alternate screen buffer (restores original screen) disable_alternate_screen(); // Reset terminal state disable_raw_mode(); + // Consume any remaining input (e.g., leftover escape sequences) + tui_drain_input(); // Reset all attributes - zstr_expand_to(stderr, "{reset}"); + tui_write_reset(stderr); fflush(stderr); } clear_state(); vec_free_TryEntryPtr(&filtered_ptrs); - zstr_free(&filter_buffer); + tui_input_free(&filter_input); marked_count = 0; return result; diff --git a/src/tui.h b/src/tui.h index e9a3275..e9d87df 100644 --- a/src/tui.h +++ b/src/tui.h @@ -1,48 +1,49 @@ #ifndef TUI_H #define TUI_H -#include "utils.h" -#include "zstr.h" -#include +#include "tui_style.h" +#include "libs/zvec.h" #include -typedef enum { ACTION_NONE, ACTION_CD, ACTION_MKDIR, ACTION_CANCEL, ACTION_DELETE } ActionType; +// Generate vec_zstr type +Z_VEC_GENERATE_IMPL(zstr, zstr) + +// ============================================================================ +// Selector Types +// ============================================================================ + +typedef enum { + ACTION_NONE, + ACTION_CD, + ACTION_MKDIR, + ACTION_CANCEL, + ACTION_DELETE +} ActionType; typedef struct { - zstr path; // Full path - zstr name; // Directory name - zstr rendered; // Pre-rendered string with tokens + zstr path; + zstr name; + zstr rendered; time_t mtime; float score; - bool marked_for_delete; // Delete mode: marked for deletion + bool marked_for_delete; } TryEntry; typedef struct { ActionType type; zstr path; - vec_zstr delete_names; // For ACTION_DELETE: vector of names to delete + vec_zstr delete_names; } SelectionResult; -// Execution mode -typedef enum { - MODE_DIRECT, // Direct invocation (immediate execution, print cd hint) - MODE_EXEC // Via alias (return shell script) -} ModeType; - -// Mode configuration +// Testing parameters (for automated tests) typedef struct { - ModeType type; - // Test mode options (orthogonal to type) - bool render_once; // Render once and exit (--and-exit) - const char *inject_keys; // Keys to inject (--and-keys) - int key_index; // Current position in inject_keys -} Mode; - -// Run the interactive selector -// base_path: directory to scan for tries -// initial_filter: initial search term (can be NULL) -// mode: execution mode configuration + bool render_once; // Render one frame and exit + const char *inject_keys; // Simulate keypresses + int key_index; // Current position in inject_keys +} TestParams; + +// Selector SelectionResult run_selector(const char *base_path, const char *initial_filter, - Mode *mode); + TestParams *test); -#endif // TUI_H +#endif /* TUI_H */ diff --git a/src/tui_style.c b/src/tui_style.c new file mode 100644 index 0000000..ba72e23 --- /dev/null +++ b/src/tui_style.c @@ -0,0 +1,646 @@ +#include "tui_style.h" +#include "terminal.h" +#include +#include +#include + +// ============================================================================ +// Style Parsing +// ============================================================================ + +int tui_style_flags(const char *style) { + if (!style) + return 0; + int flags = 0; + const char *p = style; + while (*p) { + if (*p == '\033' && *(p + 1) == '[') { + p += 2; + while (*p) { + int code = 0; + while (*p >= '0' && *p <= '9') { + code = code * 10 + (*p - '0'); + p++; + } + if (code == 1) + flags |= TUI_CHANGES_BOLD; + else if (code == 2) + flags |= TUI_CHANGES_DIM; + else if ((code >= 30 && code <= 37) || code == 38 || code == 39 || + (code >= 90 && code <= 97)) + flags |= TUI_CHANGES_FG; + else if ((code >= 40 && code <= 47) || code == 48 || code == 49 || + (code >= 100 && code <= 107)) + flags |= TUI_CHANGES_BG; + if (*p == ';') + p++; + else if (*p == 'm') { + p++; + break; + } else + break; + } + } else { + p++; + } + } + return flags; +} + +// ============================================================================ +// Style Stack +// ============================================================================ + +TuiStyles tui_styles(void) { + TuiStyles st = {0}; + st.stack[0] = (TuiStyleFrame){.fg = -1, .bg = -1, .bold = false}; + st.depth = 0; + return st; +} + +// ============================================================================ +// TuiStyleString Creation +// ============================================================================ + +TuiStyleString tui_start_zstr(zstr *s) { + zstr_clear(s); + return (TuiStyleString){.str = s, .styles = tui_styles()}; +} + +TuiStyleString tui_start_line(zstr *s) { return tui_start_zstr(s); } + +TuiStyleString tui_wrap_zstr(zstr *s) { + return (TuiStyleString){.str = s, .styles = tui_styles()}; +} + +// ============================================================================ +// Internal Helpers +// ============================================================================ + +// Write style code to zstr only if colors are enabled +static inline void tui_style(zstr *s, const char *style) { + if (!tui_no_colors && style && *style) + zstr_cat(s, style); +} + +static void tui_reemit_flags(TuiStyleString *ss, int flags) { + if (tui_no_colors) + return; + for (int i = 1; i <= ss->styles.depth; i++) { + if (ss->styles.style_flags[i] & flags) { + zstr_cat(ss->str, ss->styles.style_strs[i]); + } + } +} + +static void tui_emit_resets(TuiStyleString *ss, int flags) { + if (tui_no_colors) + return; + if (flags & TUI_CHANGES_BOLD) + zstr_cat(ss->str, ANSI_BOLD_OFF); + if (flags & TUI_CHANGES_DIM) + zstr_cat(ss->str, ANSI_DIM_OFF); + if (flags & TUI_CHANGES_FG) + zstr_cat(ss->str, ANSI_RESET_FG); + if (flags & TUI_CHANGES_BG) + zstr_cat(ss->str, ANSI_RESET_BG); +} + +// ============================================================================ +// TuiStyleString Operations +// ============================================================================ + +void tui_push(TuiStyleString *ss, const char *style) { + if (!style || !*style) + return; + if (ss->styles.depth >= TUI_STYLE_STACK_MAX - 1) + return; + + ss->styles.depth++; + size_t len = strlen(style); + if (len >= TUI_STYLE_STR_MAX) + len = TUI_STYLE_STR_MAX - 1; + memcpy(ss->styles.style_strs[ss->styles.depth], style, len); + ss->styles.style_strs[ss->styles.depth][len] = '\0'; + ss->styles.style_flags[ss->styles.depth] = tui_style_flags(style); + + tui_style(ss->str, style); +} + +void tui_pop(TuiStyleString *ss) { + if (ss->styles.depth <= 0) + return; + int flags = ss->styles.style_flags[ss->styles.depth]; + ss->styles.depth--; + if (!tui_no_colors && flags) { + tui_emit_resets(ss, flags); + tui_reemit_flags(ss, flags); + } +} + +void tui_print(TuiStyleString *ss, const char *style, const char *text) { + int flags = 0; + if (!tui_no_colors && style && *style) { + flags = tui_style_flags(style); + zstr_cat(ss->str, style); + } + zstr_cat(ss->str, text); + if (flags) { // flags is 0 when tui_no_colors, so no redundant check needed + tui_emit_resets(ss, flags); + tui_reemit_flags(ss, flags); + } +} + +void tui_putc(TuiStyleString *ss, char c) { zstr_push(ss->str, c); } + +void tui_printf(TuiStyleString *ss, const char *style, const char *fmt, ...) { + int flags = 0; + if (!tui_no_colors && style && *style) { + flags = tui_style_flags(style); + zstr_cat(ss->str, style); + } + va_list args, args2; + va_start(args, fmt); + va_copy(args2, args); + int len = vsnprintf(NULL, 0, fmt, args); + va_end(args); + if (len > 0) { + size_t old_len = zstr_len(ss->str); + zstr_reserve(ss->str, old_len + len); + vsnprintf(zstr_data(ss->str) + old_len, len + 1, fmt, args2); + if (ss->str->is_long) + ss->str->l.len += len; + else + ss->str->s.len += (uint8_t)len; + } + va_end(args2); + if (flags) { + tui_emit_resets(ss, flags); + tui_reemit_flags(ss, flags); + } +} + +// ============================================================================ +// Screen API +// ============================================================================ + +Tui tui_begin_screen(FILE *f) { + int rows, cols; + get_window_size(&rows, &cols); + (void)rows; + fputs(ANSI_HIDE_CURSOR ANSI_HOME, f); + return (Tui){.file = f, + .line_buf = zstr_init(), + .row = 1, + .cols = cols, + .cursor_row = -1, + .cursor_col = -1, + .line_has_selection = false, + .line_has_rwrite = false, + .active_input = NULL}; +} + +TuiStyleString tui_screen_line(Tui *t) { + zstr_clear(&t->line_buf); + t->line_has_selection = false; + // Don't clear line_has_rwrite here - it persists until the line is written + return (TuiStyleString){.str = &t->line_buf, .styles = tui_styles()}; +} + +TuiStyleString tui_screen_line_selected(Tui *t) { + zstr_clear(&t->line_buf); + t->line_has_selection = true; + TuiStyleString ss = {.str = &t->line_buf, .styles = tui_styles()}; + tui_push(&ss, TUI_SELECTED); + return ss; +} + +void tui_screen_write(Tui *t, TuiStyleString *line) { + if (t->line_has_selection) { + tui_pop(line); + t->line_has_selection = false; + } + // Don't clear to EOL if rwrite was used (would erase right-aligned content) + const char *eol = t->line_has_rwrite ? "\n" : ANSI_CLR "\n"; + zstr_cat(&t->line_buf, eol); + fwrite(zstr_cstr(&t->line_buf), 1, zstr_len(&t->line_buf), t->file); + t->row++; + t->line_has_rwrite = false; // Reset for next line +} + +// Decode UTF-8 codepoint starting at s[i], return codepoint and advance i +static unsigned int decode_utf8(const char *s, size_t len, size_t *i) { + unsigned char c = (unsigned char)s[*i]; + unsigned int cp = 0; + if ((c & 0x80) == 0) { + cp = c; + } else if ((c & 0xE0) == 0xC0 && *i + 1 < len) { + cp = (c & 0x1F) << 6 | ((unsigned char)s[*i + 1] & 0x3F); + (*i)++; + } else if ((c & 0xF0) == 0xE0 && *i + 2 < len) { + cp = (c & 0x0F) << 12 | ((unsigned char)s[*i + 1] & 0x3F) << 6 | + ((unsigned char)s[*i + 2] & 0x3F); + (*i) += 2; + } else if ((c & 0xF8) == 0xF0 && *i + 3 < len) { + cp = (c & 0x07) << 18 | ((unsigned char)s[*i + 1] & 0x3F) << 12 | + ((unsigned char)s[*i + 2] & 0x3F) << 6 | + ((unsigned char)s[*i + 3] & 0x3F); + (*i) += 3; + } + return cp; +} + +// Get display width of a Unicode codepoint +static int codepoint_width(unsigned int cp) { + if (cp < 0x80) return 1; // ASCII + if (cp >= 0x1F300 && cp <= 0x1FAFF) return 2; // Emojis + if (cp >= 0x2600 && cp <= 0x27BF) return 2; // Misc symbols, dingbats + return 1; // Default: arrows, box drawing, most other chars +} + +// Calculate visible width of string (excluding ANSI escape sequences) +static int visible_width(const char *s, size_t len) { + int width = 0; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == '\033' && i + 1 < len && s[i + 1] == '[') { + // Skip ANSI escape sequence + i += 2; + while (i < len && !((s[i] >= 'A' && s[i] <= 'Z') || + (s[i] >= 'a' && s[i] <= 'z'))) { + i++; + } + } else if ((c & 0xC0) == 0x80) { + // Skip UTF-8 continuation bytes (already counted) + continue; + } else { + unsigned int cp = decode_utf8(s, len, &i); + width += codepoint_width(cp); + } + } + return width; +} + +// Truncate string to max visible width, preserving ANSI sequences +// Returns byte offset where truncation should occur +static size_t truncate_at_width(const char *s, size_t len, int max_width) { + int width = 0; + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == '\033' && i + 1 < len && s[i + 1] == '[') { + // Skip ANSI escape sequence (include it in output) + i += 2; + while (i < len && !((s[i] >= 'A' && s[i] <= 'Z') || + (s[i] >= 'a' && s[i] <= 'z'))) { + i++; + } + } else if ((c & 0xC0) == 0x80) { + // Skip UTF-8 continuation bytes (already counted) + continue; + } else { + size_t start = i; + unsigned int cp = decode_utf8(s, len, &i); + int char_width = codepoint_width(cp); + if (width + char_width > max_width) { + return start; + } + width += char_width; + } + } + return len; +} + +void tui_screen_write_truncated(Tui *t, TuiStyleString *line, + const char *overflow) { + if (t->line_has_selection) { + tui_pop(line); + t->line_has_selection = false; + } + + const char *buf = zstr_cstr(&t->line_buf); + size_t len = zstr_len(&t->line_buf); + int width = visible_width(buf, len); + int overflow_len = overflow ? visible_width(overflow, strlen(overflow)) : 0; + + // Don't clear to EOL if rwrite was used (would erase right-aligned content) + const char *eol = t->line_has_rwrite ? "\n" : ANSI_CLR "\n"; + + if (width >= t->cols) { + // Need to truncate + int max_content = t->cols - overflow_len; + if (max_content < 0) max_content = 0; + size_t trunc_pos = truncate_at_width(buf, len, max_content); + + // Strip trailing spaces before overflow indicator + while (trunc_pos > 0 && buf[trunc_pos - 1] == ' ') { + trunc_pos--; + } + + // Write truncated content + fwrite(buf, 1, trunc_pos, t->file); + // Write overflow indicator (inherits current styles including background) + if (overflow) fputs(overflow, t->file); + // Reset styles after overflow, then end line + fputs(ANSI_RESET, t->file); + fputs(eol, t->file); + } else { + // No truncation needed + zstr_cat(&t->line_buf, eol); + fwrite(zstr_cstr(&t->line_buf), 1, zstr_len(&t->line_buf), t->file); + } + t->row++; + t->line_has_rwrite = false; // Reset for next line +} + +void tui_screen_rwrite(Tui *t, TuiStyleString *line, const char *bg) { + // Write right-aligned content, then carriage return (stay on same line) + if (t->line_has_selection) { + tui_pop(line); + t->line_has_selection = false; + } + + // If background style provided, set it before clearing so CLR fills with it + if (bg && *bg) { + fputs(bg, t->file); + } + + // Clear line (fills with current background) + fputs(ANSI_CLR, t->file); + + const char *buf = zstr_cstr(&t->line_buf); + size_t len = zstr_len(&t->line_buf); + int width = visible_width(buf, len); + + // Position cursor at (cols - width + 1) to right-align + int col = t->cols - width + 1; + if (col < 1) col = 1; + fprintf(t->file, "\033[%dG", col); + + // Write content + fwrite(buf, 1, len, t->file); + + // Reset foreground only (keep background for main content), then \r + if (bg && *bg) { + fputs(ANSI_RESET_FG "\r", t->file); + } else { + fputs(ANSI_RESET "\r", t->file); + } + + // Mark that rwrite was used - subsequent write should not clear to EOL + t->line_has_rwrite = true; + // Note: don't increment row - we stay on the same line +} + +void tui_screen_empty(Tui *t) { + fputs(ANSI_CLR "\n", t->file); + t->row++; + t->line_has_rwrite = false; +} + +void tui_screen_clear_rest(Tui *t) { fputs(ANSI_CLS, t->file); } + +void tui_free(Tui *t) { + fputs(ANSI_CLS, t->file); // Clear from cursor to end of screen + if (t->cursor_row >= 0 && t->cursor_col >= 0) { + fprintf(t->file, "\033[%d;%dH", t->cursor_row, t->cursor_col); + } + fputs(ANSI_SHOW_CURSOR, t->file); + zstr_free(&t->line_buf); +} + +void tui_screen_input(Tui *t, TuiInput *input) { + t->active_input = input; + + const char *text = zstr_cstr(&input->text); + int len = (int)zstr_len(&input->text); + int cursor_pos = input->cursor; + if (cursor_pos < 0) + cursor_pos = 0; + if (cursor_pos > len) + cursor_pos = len; + + // Calculate visual column (skip ANSI codes) + int visual_col = 0; + const char *p = zstr_cstr(&t->line_buf); + while (*p) { + if (*p == '\033') { + while (*p && *p != 'm') + p++; + if (*p) + p++; + } else { + visual_col++; + p++; + } + } + + // Set cursor position (before any text/placeholder) + t->cursor_col = visual_col + cursor_pos + 1; + t->cursor_row = t->row; + + // Check if input matches placeholder prefix + bool matches_placeholder = false; + int placeholder_len = 0; + if (input->placeholder) { + placeholder_len = (int)strlen(input->placeholder); + matches_placeholder = + (len <= placeholder_len) && + (len == 0 || strncmp(text, input->placeholder, len) == 0); + } + + if (matches_placeholder && placeholder_len > 0) { + // Show typed text in normal style + zstr_cat_len(&t->line_buf, text, cursor_pos); + // Cursor goes here + if (cursor_pos < len) { + zstr_cat(&t->line_buf, text + cursor_pos); + } + // Show remaining placeholder in dim + if (len < placeholder_len) { + zstr_cat(&t->line_buf, TUI_DIM); + zstr_cat(&t->line_buf, input->placeholder + len); + zstr_cat(&t->line_buf, ANSI_DIM_OFF); + } + } else { + // Input diverged from placeholder - just show the text + zstr_cat_len(&t->line_buf, text, cursor_pos); + if (cursor_pos < len) { + zstr_cat(&t->line_buf, text + cursor_pos); + } + } +} + +// ============================================================================ +// zstr Utilities +// ============================================================================ + +void tui_clr(zstr *s) { zstr_cat(s, ANSI_CLR); } + +void tui_zstr_printf(zstr *s, const char *style, const char *text) { + tui_style(s, style); + zstr_cat(s, text); + if (style && *style) + tui_style(s, ANSI_RESET); +} + +// ============================================================================ +// Direct Output +// ============================================================================ + +void tui_write(FILE *f, const char *s) { fputs(s, f); } +void tui_write_clr(FILE *f) { fputs(ANSI_CLR, f); } +void tui_write_cls(FILE *f) { fputs(ANSI_CLS, f); } +void tui_write_home(FILE *f) { fputs(ANSI_HOME, f); } +void tui_write_reset(FILE *f) { fputs(ANSI_RESET, f); } +void tui_write_hide_cursor(FILE *f) { fputs(ANSI_HIDE_CURSOR, f); } +void tui_write_show_cursor(FILE *f) { fputs(ANSI_SHOW_CURSOR, f); } +void tui_write_goto(FILE *f, int row, int col) { + fprintf(f, "\033[%d;%dH", row, col); +} +void tui_flush(FILE *f, zstr *s) { fwrite(zstr_cstr(s), 1, zstr_len(s), f); } + +// ============================================================================ +// Input Field Management +// ============================================================================ + +TuiInput tui_input_init(void) { + return (TuiInput){.text = zstr_init(), .cursor = 0, .placeholder = NULL}; +} + +void tui_input_free(TuiInput *input) { + zstr_free(&input->text); + input->cursor = 0; +} + +void tui_input_clear(TuiInput *input) { + zstr_clear(&input->text); + input->cursor = 0; +} + +bool tui_input_handle_key(TuiInput *input, int key) { + zstr *buffer = &input->text; + int *cursor = &input->cursor; + int len = (int)zstr_len(buffer); + + // Navigation + if (key == 1) { // Ctrl-A (start) + *cursor = 0; + return true; + } + if (key == 5) { // Ctrl-E (end) + *cursor = len; + return true; + } + if (key == 2 || key == ARROW_LEFT) { // Ctrl-B or LEFT + if (*cursor > 0) + (*cursor)--; + return true; + } + if (key == 6 || key == ARROW_RIGHT) { // Ctrl-F or RIGHT + if (*cursor < len) + (*cursor)++; + return true; + } + + // Deletion + if (key == BACKSPACE || key == 8) { // Backspace or Ctrl-H + if (*cursor > 0) { + char *data = zstr_data(buffer); + zstr new_buf = zstr_init(); + for (int i = 0; i < len; i++) { + if (i != *cursor - 1) + zstr_push(&new_buf, data[i]); + } + zstr_free(buffer); + *buffer = new_buf; + (*cursor)--; + } + return true; + } + if (key == DEL_KEY) { + if (*cursor < len) { + char *data = zstr_data(buffer); + zstr new_buf = zstr_init(); + for (int i = 0; i < len; i++) { + if (i != *cursor) + zstr_push(&new_buf, data[i]); + } + zstr_free(buffer); + *buffer = new_buf; + } + return true; + } + if (key == 11) { // Ctrl-K (kill to end) + if (*cursor < len) { + char *data = zstr_data(buffer); + zstr new_buf = zstr_init(); + for (int i = 0; i < *cursor; i++) { + zstr_push(&new_buf, data[i]); + } + zstr_free(buffer); + *buffer = new_buf; + } + return true; + } + if (key == 21) { // Ctrl-U (kill to start) + if (*cursor > 0) { + char *data = zstr_data(buffer); + zstr new_buf = zstr_init(); + for (int i = *cursor; i < len; i++) { + zstr_push(&new_buf, data[i]); + } + zstr_free(buffer); + *buffer = new_buf; + *cursor = 0; + } + return true; + } + if (key == 23) { // Ctrl-W (kill word) + if (*cursor > 0) { + char *data = zstr_data(buffer); + int end_pos = *cursor - 1; + while (end_pos >= 0 && !isalnum((unsigned char)data[end_pos])) + end_pos--; + int start_pos = end_pos; + while (start_pos >= 0 && isalnum((unsigned char)data[start_pos])) + start_pos--; + start_pos++; + + zstr new_buf = zstr_init(); + for (int i = 0; i < start_pos; i++) + zstr_push(&new_buf, data[i]); + for (int i = *cursor; i < len; i++) + zstr_push(&new_buf, data[i]); + zstr_free(buffer); + *buffer = new_buf; + *cursor = start_pos; + } + return true; + } + + // Character insertion + if (!iscntrl(key) && key >= 32 && key < 127) { + char *data = zstr_data(buffer); + zstr new_buf = zstr_init(); + for (int i = 0; i < len; i++) { + if (i == *cursor) + zstr_push(&new_buf, (char)key); + zstr_push(&new_buf, data[i]); + } + if (*cursor >= len) + zstr_push(&new_buf, (char)key); + zstr_free(buffer); + *buffer = new_buf; + (*cursor)++; + return true; + } + + return false; // Key not handled +} + +bool tui_handle_key(Tui *t, int key) { + if (t->active_input) { + return tui_input_handle_key(t->active_input, key); + } + return false; +} diff --git a/src/tui_style.h b/src/tui_style.h new file mode 100644 index 0000000..6299d3f --- /dev/null +++ b/src/tui_style.h @@ -0,0 +1,197 @@ +#ifndef TUI_STYLE_H +#define TUI_STYLE_H + +#include "libs/zstr.h" +#include +#include + +// ============================================================================ +// ANSI Escape Code Constants +// ============================================================================ + +// Attributes +#define ANSI_RESET "\033[0m" +#define ANSI_BOLD "\033[1m" +#define ANSI_DIM "\033[2m" +#define ANSI_ITALIC "\033[3m" +#define ANSI_UNDERLINE "\033[4m" +#define ANSI_REVERSE "\033[7m" +#define ANSI_STRIKE "\033[9m" + +// Standard foreground colors +#define ANSI_BLACK "\033[30m" +#define ANSI_RED "\033[31m" +#define ANSI_GREEN "\033[32m" +#define ANSI_YELLOW "\033[33m" +#define ANSI_BLUE "\033[34m" +#define ANSI_MAGENTA "\033[35m" +#define ANSI_CYAN "\033[36m" +#define ANSI_WHITE "\033[37m" +#define ANSI_GRAY "\033[90m" +#define ANSI_GREY "\033[90m" + +// Bright foreground colors +#define ANSI_BRIGHT_BLACK "\033[90m" +#define ANSI_BRIGHT_RED "\033[91m" +#define ANSI_BRIGHT_GREEN "\033[92m" +#define ANSI_BRIGHT_YELLOW "\033[93m" +#define ANSI_BRIGHT_BLUE "\033[94m" +#define ANSI_BRIGHT_MAGENTA "\033[95m" +#define ANSI_BRIGHT_CYAN "\033[96m" +#define ANSI_BRIGHT_WHITE "\033[97m" + +// Standard background colors +#define ANSI_BG_BLACK "\033[40m" +#define ANSI_BG_RED "\033[41m" +#define ANSI_BG_GREEN "\033[42m" +#define ANSI_BG_YELLOW "\033[43m" +#define ANSI_BG_BLUE "\033[44m" +#define ANSI_BG_MAGENTA "\033[45m" +#define ANSI_BG_CYAN "\033[46m" +#define ANSI_BG_WHITE "\033[47m" +#define ANSI_BG_GRAY "\033[100m" + +// Semantic composite styles +#define ANSI_HIGHLIGHT "\033[1;33m" +#define ANSI_H1 "\033[1;38;5;214m" +#define ANSI_H2 "\033[1;34m" +#define ANSI_H3 "\033[1;37m" +#define ANSI_DARK "\033[38;5;245m" +#define ANSI_SECTION "\033[1;48;5;237m" +#define ANSI_DANGER "\033[48;5;52m" + +// TUI_* semantic style aliases +#define TUI_BOLD ANSI_BOLD +#define TUI_DIM ANSI_DIM +#define TUI_DARK ANSI_DARK +#define TUI_H1 ANSI_H1 +#define TUI_H2 ANSI_H2 +#define TUI_HIGHLIGHT ANSI_HIGHLIGHT +#define TUI_MATCH "\033[38;5;11m" +#define TUI_SELECTED "\033[48;5;237m" +#define TUI_DANGER ANSI_DANGER +#define TUI_BG_RED ANSI_BG_RED +#define TUI_BG_BLUE ANSI_BG_BLUE + +// Control sequences +#define ANSI_CLR "\033[K" +#define ANSI_CLS "\033[J" +#define ANSI_HOME "\033[H" +#define ANSI_HIDE_CURSOR "\033[?25l" +#define ANSI_SHOW_CURSOR "\033[?25h" + +// Reset specific attributes +#define ANSI_RESET_FG "\033[39m" +#define ANSI_RESET_BG "\033[49m" +#define ANSI_BOLD_OFF "\033[22m" +#define ANSI_DIM_OFF "\033[22m" + +// Style flags +#define TUI_CHANGES_FG (1 << 0) +#define TUI_CHANGES_BG (1 << 1) +#define TUI_CHANGES_BOLD (1 << 2) +#define TUI_CHANGES_DIM (1 << 3) + +// ============================================================================ +// Global Color Toggle +// ============================================================================ + +extern bool tui_no_colors; + +// ============================================================================ +// Types +// ============================================================================ + +typedef struct { + int fg; + int bg; + bool bold; +} TuiStyleFrame; + +#define TUI_STYLE_STACK_MAX 8 +#define TUI_STYLE_STR_MAX 32 + +typedef struct { + TuiStyleFrame stack[TUI_STYLE_STACK_MAX]; + char style_strs[TUI_STYLE_STACK_MAX][TUI_STYLE_STR_MAX]; + int style_flags[TUI_STYLE_STACK_MAX]; + int depth; +} TuiStyles; + +typedef struct { + zstr *str; + TuiStyles styles; +} TuiStyleString; + +// Text input field state (forward declare) +typedef struct { + zstr text; + int cursor; + const char *placeholder; // Optional placeholder shown when empty +} TuiInput; + +typedef struct { + FILE *file; + zstr line_buf; + int row; + int cols; // Terminal width + int cursor_row; + int cursor_col; + bool line_has_selection; + bool line_has_rwrite; // rwrite was used, don't clear to EOL + TuiInput *active_input; // Input field with cursor (if any) +} Tui; + +// ============================================================================ +// Function Declarations +// ============================================================================ + +int tui_style_flags(const char *style); +TuiStyles tui_styles(void); + +TuiStyleString tui_start_zstr(zstr *s); +TuiStyleString tui_start_line(zstr *s); +TuiStyleString tui_wrap_zstr(zstr *s); + +void tui_push(TuiStyleString *ss, const char *style); +void tui_pop(TuiStyleString *ss); +void tui_print(TuiStyleString *ss, const char *style, const char *text); +void tui_putc(TuiStyleString *ss, char c); +void tui_printf(TuiStyleString *ss, const char *style, const char *fmt, ...) + __attribute__((format(printf, 3, 4))); + +Tui tui_begin_screen(FILE *f); +TuiStyleString tui_screen_line(Tui *t); +TuiStyleString tui_screen_line_selected(Tui *t); +void tui_screen_write(Tui *t, TuiStyleString *line); +void tui_screen_write_truncated(Tui *t, TuiStyleString *line, + const char *overflow); +void tui_screen_rwrite(Tui *t, TuiStyleString *line, const char *bg); // Right-align with optional bg fill +void tui_screen_empty(Tui *t); +void tui_screen_clear_rest(Tui *t); +void tui_free(Tui *t); // Use with Z_CLEANUP(tui_free) +void tui_screen_input(Tui *t, TuiInput *input); + +// Input field management +TuiInput tui_input_init(void); +void tui_input_free(TuiInput *input); +void tui_input_clear(TuiInput *input); +bool tui_input_handle_key(TuiInput *input, int key); + +// Convenience: handle key for active input on screen +bool tui_handle_key(Tui *t, int key); + +void tui_clr(zstr *s); +void tui_zstr_printf(zstr *s, const char *style, const char *text); + +void tui_write(FILE *f, const char *s); +void tui_write_clr(FILE *f); +void tui_write_cls(FILE *f); +void tui_write_home(FILE *f); +void tui_write_reset(FILE *f); +void tui_write_hide_cursor(FILE *f); +void tui_write_show_cursor(FILE *f); +void tui_write_goto(FILE *f, int row, int col); +void tui_flush(FILE *f, zstr *s); + +#endif /* TUI_STYLE_H */ diff --git a/src/utils.c b/src/utils.c index cb8b96a..7d6e09b 100644 --- a/src/utils.c +++ b/src/utils.c @@ -17,8 +17,6 @@ #include #include -// Token expansion is now in tokens.c (generated from tokens.rl) - char *trim(char *str) { char *end; while (isspace((unsigned char)*str)) diff --git a/src/utils.h b/src/utils.h index 77b2e08..43c5193 100644 --- a/src/utils.h +++ b/src/utils.h @@ -1,28 +1,17 @@ #ifndef UTILS_H #define UTILS_H -#include "zstr.h" -#include "zvec.h" -#include "tokens.h" +#include "libs/zstr.h" +#include "libs/zvec.h" +#include "tui.h" #include #include #include -// Generate common vector types -Z_VEC_GENERATE_IMPL(zstr, zstr) +// Generate common vector types (vec_zstr is in tui.h) Z_VEC_GENERATE_IMPL(char *, char_ptr) -// ANSI Colors (legacy defines, prefer using tokens instead) -#define ANSI_RESET "\033[0m" -#define ANSI_BOLD "\033[1m" -#define ANSI_DIM "\033[2m" -#define ANSI_RED "\033[31m" -#define ANSI_GREEN "\033[32m" -#define ANSI_YELLOW "\033[33m" -#define ANSI_BLUE "\033[34m" -#define ANSI_MAGENTA "\033[35m" -#define ANSI_CYAN "\033[36m" -#define ANSI_WHITE "\033[37m" +// ANSI colors moved to tui.h // Defer helper for standard pointers (zstr has Z_CLEANUP(zstr_free)) static inline void cleanup_free(void *p) { free(*(void **)p); }