From fb3bae2f3c41aec1bf2b4dbf926d4b034f2b339e Mon Sep 17 00:00:00 2001 From: deftio Date: Mon, 27 Apr 2026 00:25:56 -0700 Subject: [PATCH 1/3] updated arg parse and also enabled optional command history --- AGENTS.md | 29 +- CHANGELOG.md | 25 + CMakeLists.txt | 2 +- README.md | 47 +- dev/arg_parse_updates.md | 335 ++++++++ dev/size_profiles.sh | 1 + dev/xelp-todo.md | 38 +- docs/api-reference.md | 58 +- docs/build-profiles.md | 2 + docs/configuration.md | 2 + docs/examples.md | 1 + docs/porting.md | 15 + docs/tutorial.md | 13 + examples/README.md | 4 +- examples/posix-simple/README.md | 2 + examples/posix-simple/xelp-example.c | 1 + idf_component.yml | 6 +- library.json | 2 +- library.properties | 2 +- llms.txt | 2 +- pages/api-reference.html | 54 +- pages/configuration.html | 5 + pages/index.html | 46 +- pages/releases.html | 15 + release_management.md | 4 +- src/xelp.c | 156 +++- src/xelp.h | 26 +- src/xelpcfg.h | 23 +- tests/xelp_unit_tests.c | 1070 ++++++++++++++++++++++++-- 29 files changed, 1845 insertions(+), 141 deletions(-) create mode 100644 dev/arg_parse_updates.md diff --git a/AGENTS.md b/AGENTS.md index fa2dace..28de466 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ compiler. for the life of the instance (string literals, static buffers, or globals -- never stack-local buffers that go out of scope). -## Function signatures (v0.3.1+) +## Function signatures (v0.3.2+) ### CLI command functions @@ -149,6 +149,27 @@ XELPRESULT cmd_divmod(XELP *ths, const char *args, int len) { Tokens are NOT null-terminated. Use `tok.s`..`tok.p` or `XELP_XB_LEN(tok)`. +## Direct argument access (XelpArgInt / XelpArgStr) + +For one-shot access to a specific argument by index: + +```c +XELPRESULT cmd_set(XELP *ths, const char *args, int len) { + const char *key; + int klen, value; + XelpArgStr(args, len, 1, &key, &klen); /* arg 1 = key name */ + XelpArgInt(args, len, 2, &value); /* arg 2 = int value */ + return XELP_S_OK; +} +``` + +| Function | Purpose | +|----------|---------| +| `XelpArgInt(args, len, n, &val)` | Get arg N as int (wraps TokN + ParseNum) | +| `XelpArgStr(args, len, n, &s, &slen)` | Get arg N as string span (pointer + length) | + +Arg 0 is the command name (per argc/argv convention). + ### Random-access alternative (XelpTokN) For random-access by index, use `XelpTokN`. Note the `(char*)` cast @@ -236,9 +257,11 @@ Edit `src/xelpcfg.h` to enable/disable features: | `XELP_ENABLE_KEY` | Single keypress mode | ~200-500 bytes | | `XELP_ENABLE_THR` | Pass-through mode | ~50-125 bytes | | `XELP_ENABLE_HELP` | Built-in help command | ~180-350 bytes | +| `XELP_ENABLE_HISTORY` | Command history (UP/DOWN recall) | ~420 bytes | | `XELP_ENABLE_FULL` | All of the above | All combined | Buffer size: `XELP_CMDBUFSZ` (default 64 bytes). +History depth: `XELP_HIST_DEPTH` (default 4 commands). Override at compile time or via `XELP_CONFIG_OVERRIDE`. ## Three modes @@ -258,7 +281,7 @@ Default mode switch keys: ESC (KEY), CTRL-P (CLI), CTRL-T (THR). 6. Hardcoding `&global_instance` instead of using `ths` in commands. 7. Calling `malloc` or stdlib functions in embedded contexts. -## Registers (v0.3.1+) +## Registers (v0.3.2+) Each XELP instance has 4 return registers (`mR[0..3]`), accessed via macros. Convention: **callee-clobbers-all** -- any command call may @@ -308,7 +331,7 @@ XELP_SET_ECHO(*ths, XELP_ECHO_NORMAL); /* restore after ENTER */ ## File structure ``` -src/xelp.c -- implementation (~980 lines) +src/xelp.c -- implementation (~1130 lines) src/xelp.h -- public API (types, macros, function declarations) src/xelpcfg.h -- compile-time feature flags and settings ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 92c47cf..c588cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,31 @@ Versions always use three-component semver (e.g. `0.3.0`, never `0.3`). ## [Unreleased] +## [0.3.2] - 2026-04-26 + +### Added +- **Command history** (`XELP_ENABLE_HISTORY`): UP/DOWN arrow recall of + previously entered commands using a fixed-slot ring buffer. Requires + `XELP_ENABLE_CLI` and `XELP_ENABLE_LINE_EDIT`. Configurable depth via + `XELP_HIST_DEPTH` (default 4). Consecutive duplicate suppression, empty + command filtering, in-progress line save/restore on browse. + RAM cost: `XELP_HIST_DEPTH * XELP_CMDBUFSZ + XELP_CMDBUFSZ + 4` bytes + per instance. Code cost: ~420 bytes on ARM Thumb (`-Os`). +- `XelpArgInt(args, len, n, &val)` -- get argument N as an integer in one + call. Wraps `XelpTokN` + `XelpParseNum`. ~108 bytes ARM Thumb (combined + with XelpArgStr). +- `XelpArgStr(args, len, n, &s, &slen)` -- get argument N as a string + span (pointer + length) in one call. Wraps `XelpTokN`. + +### Fixed +- `XELPKEY_BKSP` was defined as 0x07 (BEL), not 0x08 (ASCII BS). Added + `XELPKEY_BS` (0x08) and both backspace paths in `XelpParseKey` now + accept 0x07, 0x08, and 0x7F (DEL). + +### Changed +- Test suite expanded to 47 units, 598 test cases (from 39/531). +- `dev/size_profiles.sh` updated with profile 10 (Full + history). + ## [0.3.1] - 2026-04-26 ### Changed diff --git a/CMakeLists.txt b/CMakeLists.txt index 0969676..0af39cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,7 +62,7 @@ endif() # ---- Plain CMake library ---- cmake_minimum_required(VERSION 3.10) project(xelp - VERSION 0.3.1 + VERSION 0.3.2 LANGUAGES C DESCRIPTION "Embedded CLI/script interpreter -- no malloc, multi-instance" ) diff --git a/README.md b/README.md index bd43d19..8d3ef67 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -![Version](https://img.shields.io/badge/version-0.3.1-blue.svg) +![Version](https://img.shields.io/badge/version-0.3.2-blue.svg) [![License](https://img.shields.io/badge/License-BSD%202--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause) [![CI](https://github.com/deftio/xelp/actions/workflows/ci.yml/badge.svg)](https://github.com/deftio/xelp/actions/workflows/ci.yml) ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg) @@ -75,6 +75,9 @@ interactive configuration. #define XELP_ENABLE_HELP 1 ``` +Optional: add `XELP_ENABLE_HISTORY` for UP/DOWN arrow command recall +(~420 bytes, requires `XELP_ENABLE_LINE_EDIT`). + ### Full (~3-6 KB) Adds THR pass-through mode (~50-125 bytes more) for forwarding all @@ -209,7 +212,7 @@ make clean # remove test build artifacts make clean-all # clean tests + all examples ``` -39 test units, 531 test cases, 100% line coverage of `xelp.c`. +47 test units, 598 test cases, 100% line coverage of `xelp.c`. Feature profile sizes: `dev/size_profiles.sh` (uses Docker for ARM Cortex-M0, falls back to host GCC). @@ -245,26 +248,26 @@ features). Even the largest full build is under 7 KB. | CPU | Width | Compiler | KEY (bytes) | CLI (bytes) | FULL (bytes) | |-----|------:|----------|------------:|------------:|-------------:| -| AVR (ATtiny85) | 8 | avr-gcc | 1046 | 4038 | 4096 | -| AVR (ATmega328P) | 8 | avr-gcc | 1054 | 4132 | 4190 | -| Z80 | 8 | SDCC | 2121 | 6966 | 7074 | -| 6800 (HC08) | 8 | SDCC | 2471 | 8147 | 8288 | -| MSP430 | 16 | msp430-gcc | 770 | 3260 | 3306 | -| 68HC11 | 16 | m68hc11-gcc | 2369 | 6570 | 6641 | -| Xtensa LX7 (ESP32-S3) | 32 | xtensa-esp-elf-gcc | 576 | 2508 | 2540 | -| ARM Thumb | 32 | arm-none-eabi-gcc | 580 | 2482 | 2526 | -| RISC-V (rv32) | 32 | riscv64-unknown-elf-gcc | 722 | 2970 | 3008 | -| Xtensa LX106 (ESP8266) | 32 | xtensa-lx106-elf-gcc | 723 | 2831 | 2863 | -| m68k | 32 | m68k-linux-gnu-gcc | 728 | 3146 | 3194 | -| ARM32 | 32 | arm-none-eabi-gcc | 980 | 3746 | 3806 | -| x86-32 | 32 | GCC | 1081 | 4604 | 4654 | -| MIPS32 | 32 | mipsel-linux-gnu-gcc | 1296 | 4888 | 4936 | -| PowerPC | 32 | powerpc-linux-gnu-gcc | 1504 | 5674 | 5738 | -| RISC-V (rv64) | 64 | riscv64-linux-gnu-gcc | 756 | 3332 | 3366 | -| x86-64 | 64 | Clang | 1043 | 5013 | 5055 | -| x86-64 | 64 | GCC | 1063 | 4787 | 4836 | -| AArch64 (ARM64) | 64 | aarch64-linux-gnu-gcc | 1324 | 5178 | 5234 | -| MIPS64 | 64 | mips64el-linux-gnuabi64-gcc | 1360 | 5512 | 5560 | +| AVR (ATtiny85) | 8 | avr-gcc | 1046 | 4266 | 4324 | +| AVR (ATmega328P) | 8 | avr-gcc | 1054 | 4366 | 4424 | +| Z80 | 8 | SDCC | 2121 | 7280 | 7388 | +| 6800 (HC08) | 8 | SDCC | 2471 | 8614 | 8715 | +| MSP430 | 16 | msp430-gcc | 770 | 3482 | 3528 | +| 68HC11 | 16 | m68hc11-gcc | 2369 | 6884 | 6955 | +| Xtensa LX7 (ESP32-S3) | 32 | xtensa-esp-elf-gcc | 576 | 2592 | 2624 | +| ARM Thumb | 32 | arm-none-eabi-gcc | 580 | 2598 | 2642 | +| RISC-V (rv32) | 32 | riscv64-unknown-elf-gcc | 722 | 3094 | 3132 | +| Xtensa LX106 (ESP8266) | 32 | xtensa-lx106-elf-gcc | 723 | 2955 | 2987 | +| m68k | 32 | m68k-linux-gnu-gcc | 728 | 3332 | 3380 | +| ARM32 | 32 | arm-none-eabi-gcc | 980 | 3930 | 3990 | +| x86-32 | 32 | GCC | 1081 | 4916 | 4966 | +| MIPS32 | 32 | mipsel-linux-gnu-gcc | 1296 | 5224 | 5272 | +| PowerPC | 32 | powerpc-linux-gnu-gcc | 1504 | 6058 | 6122 | +| RISC-V (rv64) | 64 | riscv64-linux-gnu-gcc | 756 | 3548 | 3582 | +| x86-64 | 64 | Clang | 1043 | 5268 | 5310 | +| x86-64 | 64 | GCC | 1063 | 5136 | 5185 | +| AArch64 (ARM64) | 64 | aarch64-linux-gnu-gcc | 1324 | 5566 | 5622 | +| MIPS64 | 64 | mips64el-linux-gnuabi64-gcc | 1360 | 5864 | 5928 | x86-64 GCC row is measured directly; others from cross-compilation via diff --git a/dev/arg_parse_updates.md b/dev/arg_parse_updates.md new file mode 100644 index 0000000..01e14ee --- /dev/null +++ b/dev/arg_parse_updates.md @@ -0,0 +1,335 @@ +# Argument Parsing Ergonomics for xelp + +Design notes for improving how command handlers access their arguments. +Goal: reduce per-command boilerplate while keeping zero-malloc, C89, and +small code size. + +## The Problem + +Every command handler that takes arguments currently looks like this: + +```c +XELPRESULT cmd_led(XELP *ths, const char *args, int len) { + XelpBuf b, tok; + int val; + XELP_XB_INIT(b, (char*)args, len); + if (XelpTokN(&b, 1, &tok) == XELP_S_OK) { + XelpParseNum(tok.s, (int)(tok.p - tok.s), &val); + /* finally do something with val */ + } + return XELP_S_OK; +} +``` + +That's 4 lines of parsing machinery for one integer argument. The XelpArgs +iterator (added in 0.3.1) is better but still verbose: + +```c +XELPRESULT cmd_led(XELP *ths, const char *args, int len) { + XelpArgs a; + int val; + XelpArgsInit(&a, args, len); + XelpNextTok(&a, NULL); /* skip command name */ + XelpNextInt(&a, &val); + /* finally do something with val */ + return XELP_S_OK; +} +``` + +Compare with what the JS/Python world expects: + +```js +cli.addCommand("led", (args) => { led(parseInt(args[1])); }); +``` + +We can't match that in C89, but we can get closer. + +## What Other Embedded CLI Libraries Do + +### funbiscuit/embedded-cli (~2KB code, 1.5KB RAM) + +Handler receives `(EmbeddedCli *cli, char *args, void *context)`. +Arguments pre-tokenized if flag set during registration. Access via: + +```c +const char *arg = embeddedCliGetToken(args, 1); /* 1-indexed */ +uint8_t count = embeddedCliGetTokenCount(args); +``` + +**Trade-off**: Modifies the input buffer in-place (inserts nulls between +tokens). Tokens are null-terminated C strings. Simple to use, but +destructive -- can't re-parse or use const input. xelp deliberately +avoids this (scripts are const/ROM-able). + +### Helius/microrl + +Handler receives `(int argc, char **argv)` -- classic main() style. +Library tokenizes into a pre-allocated argv array. + +**Trade-off**: Requires a fixed-size `char *argv[N]` array. Each token +is null-terminated (destructive). Simple and familiar, but the argv +array costs RAM (N * sizeof(char*)) and limits max arguments. + +### AndreRenaud/EmbeddedCLI (~1KB code, 200B RAM minimal) + +Also parses into argc/argv. Supports quoted strings and escapes. +Suggests pairing with a separate "Simple Options" library for +`-flag value` style parsing. + +### MicroShell (marcinbor85) + +Filesystem-like command tree (ls, cat, pwd). Not really comparable -- +different problem domain. No dynamic allocation, callback-based. + +### Summary: Industry Patterns + +| Library | Arg Interface | Destructive? | Null-terminated? | Extra RAM | +|---------|-------------|-------------|-----------------|-----------| +| embedded-cli | getToken(args, N) | Yes | Yes | N/A (in-place) | +| microrl | argc/argv | Yes | Yes | argv array | +| EmbeddedCLI | argc/argv | Yes | Yes | argv array | +| xelp (current) | XelpTokN / XelpArgs | No | No | XelpBuf on stack | + +**Key observation**: Every other library modifies the input buffer to +null-terminate tokens. xelp is the only one that preserves const input +(needed for ROM-able scripts). This is a genuine differentiator but +it costs ergonomics -- tokens come as (pointer, length) pairs instead +of C strings. + +## Options Evaluated + +### Option A: Direct-access convenience functions (CHOSEN) + +Add `XelpArgInt` and `XelpArgStr` as functions (not macros) that +combine "get Nth argument" into one call. No new types, no new +concepts -- just fewer lines per command. + +Functions are the right choice over macros: the linker includes each +function body once regardless of how many commands call it. A macro +would expand the full XelpTokN + XelpParseNum sequence at every call +site -- ~50 bytes per invocation instead of once. Five commands using +XelpArgInt as a function: ~50 bytes total. As a macro: ~250 bytes. + +```c +/* Get argument N as an integer. Arg 0 is the command name. */ +XELPRESULT XelpArgInt(const char *args, int len, int n, int *val); + +/* Get argument N as a string span. Sets *s and *slen. */ +XELPRESULT XelpArgStr(const char *args, int len, int n, + const char **s, int *slen); +``` + +**Pros**: +- Dead simple, self-documenting +- No new types or state +- Works with existing const/non-destructive parsing +- Function body included once by linker, called from many sites + +**Cons**: +- O(N) per call (re-scans from start each time). Fine for commands + with 1-3 arguments. Bad if someone calls it in a loop for 20 args. +- Doesn't cover the "iterate all args" case (use existing XelpArgs) + +### Option B: Enhanced XelpArgs iterator (NOT CHOSEN) + +Would have added `XelpArgsBegin` (auto-skip command name) and typed +accessors (`XelpArgsInt`, `XelpArgsStr`). + +**Rejected** because: +- Auto-skipping arg 0 violates the argc/argv convention. Arg 0 is the + command name in every C program and every other CLI library. A command + registered under two names (`"help"` and `"?"`) may need to know which + name invoked it. Silently skipping it would be surprising. +- The existing XelpArgs iterator (XelpArgsInit + XelpNextTok + + XelpNextInt) already covers the stateful iteration case adequately. +- Adding more functions to the iterator increases API surface for + marginal benefit. + +### Option C: argc/argv with pre-allocated array (NOT CHOSEN) + +Tokenize into a fixed-size XelpBuf array. + +**Rejected** because: +- Stack cost per call (N * 12 bytes on 32-bit) +- Tokens are still (ptr, len), not null-terminated C strings -- + so you can't pass them to printf("%s") or strcmp() directly. + This reduces the ergonomic win vs. other libraries. +- XELP_MAX_ARGS is a footgun (silent truncation if exceeded) + +### Option D: Destructive argc/argv (NOT CHOSEN) + +Insert null bytes into the args buffer to give real C strings. + +**Rejected** because: +- **Breaks xelp's const-input guarantee.** Scripts can't live in ROM. + This is a fundamental design principle of xelp. +- Requires user to copy input to a mutable buffer first + +## Final Proposal + +Two new functions added to the base CLI API. No new compile flag -- +compiled whenever `XELP_ENABLE_CLI` is on (these are useless without +the CLI tokenizer anyway). Implemented as functions, not macros. + +```c +XELPRESULT XelpArgInt(const char *args, int len, int n, int *val); +XELPRESULT XelpArgStr(const char *args, int len, int n, + const char **s, int *slen); +``` + +Arg 0 is the command name, arg 1 is the first real argument. Follows +the argc/argv convention exactly. + +Internally: thin wrappers over existing `XelpTokN` + `XelpParseNum`. +No new types, no new state, no behavior changes to existing functions. + +### Code Size + +| Function | Est. ARM Thumb | Notes | +|----------|---------------|-------| +| `XelpArgInt` | ~50 bytes | XelpTokN + XelpParseNum wrapper | +| `XelpArgStr` | ~40 bytes | XelpTokN wrapper, returns span | +| **Total** | **~90 bytes** | Part of base CLI, no extra flag | + +For context, the full CLI build is ~2500 bytes on ARM Thumb. This adds +~3.5%. Two functions that improve every command handler in the project. + +## Before/After Comparison + +### Simple command (1 int arg) + +**Before** (4 lines of boilerplate): +```c +XELPRESULT cmd_led(XELP *ths, const char *args, int len) { + XelpBuf b, tok; + int val; + XELP_XB_INIT(b, (char*)args, len); + if (XelpTokN(&b, 1, &tok) == XELP_S_OK) { + XelpParseNum(tok.s, (int)(tok.p - tok.s), &val); + set_led(val); + } + return XELP_S_OK; +} +``` + +**After** (1 line): +```c +XELPRESULT cmd_led(XELP *ths, const char *args, int len) { + int val; + if (XelpArgInt(args, len, 1, &val) == XELP_S_OK) + set_led(val); + return XELP_S_OK; +} +``` + +### Multi-arg command (2 ints) + +**Before**: +```c +XELPRESULT cmd_divmod(XELP *ths, const char *args, int len) { + XelpBuf b, tok; + int a, d; + XELP_XB_INIT(b, (char*)args, len); + if (XelpTokN(&b, 1, &tok) != XELP_S_OK) goto usage; + a = XelpStr2Int(tok.s, (int)(tok.p - tok.s)); + XELP_XB_TOP(b); + if (XelpTokN(&b, 2, &tok) != XELP_S_OK) goto usage; + d = XelpStr2Int(tok.s, (int)(tok.p - tok.s)); + /* ... */ +} +``` + +**After**: +```c +XELPRESULT cmd_divmod(XELP *ths, const char *args, int len) { + int a, d; + if (XelpArgInt(args, len, 1, &a) != XELP_S_OK) goto usage; + if (XelpArgInt(args, len, 2, &d) != XELP_S_OK) goto usage; + /* ... */ +} +``` + +### String argument + +**Before**: +```c +XELPRESULT cmd_ssid(XELP *ths, const char *args, int len) { + XelpBuf b, tok; + XELP_XB_INIT(b, (char*)args, len); + if (XelpTokN(&b, 1, &tok) == XELP_S_OK) { + int slen = (int)(tok.p - tok.s); + memcpy(gSsid, tok.s, slen); + gSsid[slen] = '\0'; + } + return XELP_S_OK; +} +``` + +**After**: +```c +XELPRESULT cmd_ssid(XELP *ths, const char *args, int len) { + const char *s; int slen; + if (XelpArgStr(args, len, 1, &s, &slen) == XELP_S_OK) { + memcpy(gSsid, s, slen); + gSsid[slen] = '\0'; + } + return XELP_S_OK; +} +``` + +## Design Decisions + +1. **Arg 0 is the command name.** Follows argc/argv convention. The + handler picks which index it wants. No implicit skipping. + +2. **Functions, not macros.** Linker includes the body once. Macros + would duplicate ~50 bytes of XelpTokN + XelpParseNum at every call + site. For something called from every command handler, this matters. + +3. **No new compile flag.** These live in the base CLI API, gated only + by `XELP_ENABLE_CLI`. Users who enable CLI want arg parsing. Users + on KEY-only don't have arguments to parse. + +4. **No changes to existing API.** XelpArgs, XelpTokN, XelpNextTok, + XelpNextInt all stay as-is. The new functions are additive. + +5. **Hex auto-detection.** XelpArgInt calls XelpParseNum internally, + which already handles `0x1A` and `1Ah` formats. No decision needed. + +6. **Error semantics.** If arg N doesn't exist, return XELP_E_ERR and + leave *val unchanged. Caller can set a default before calling. + Matches existing XelpParseNum behavior. + +7. **O(N) is acceptable.** XelpArgInt re-scans from the start each + call. For commands with 1-3 args (the vast majority), this is + negligible. Commands with many args should use the XelpArgs + iterator, which is O(1) per token. + +## Existing API (unchanged) + +For reference, the existing argument APIs remain available: + +```c +/* Random access (O(N) per call) */ +XELPRESULT XelpTokN(XelpBuf *buf, int n, XelpBuf *tok); +XELPRESULT XelpNumToks(XelpBuf *b, int *n); + +/* Sequential iterator (O(1) per call) */ +XELPRESULT XelpArgsInit(XelpArgs *a, const char *args, int len); +XELPRESULT XelpNextTok(XelpArgs *a, XelpBuf *tok); +XELPRESULT XelpNextInt(XelpArgs *a, int *val); +XELPRESULT XelpArgCount(XelpArgs *a, int *n); + +/* Low-level */ +int XelpStr2Int(const char *s, int maxlen); +XELPRESULT XelpParseNum(const char *s, int maxlen, int *n); +``` + +The new `XelpArgInt` and `XelpArgStr` are sugar over these for the +common case. Power users retain full access to the tokenizer. + +## Implementation Priority + +Developer-ergonomics improvement, not a functional change. Ship when +convenient -- fully backward-compatible, no API breaks. Good candidate +for the next release (0.3.2 or 0.4.0). diff --git a/dev/size_profiles.sh b/dev/size_profiles.sh index c2fe77f..c62e368 100755 --- a/dev/size_profiles.sh +++ b/dev/size_profiles.sh @@ -116,5 +116,6 @@ build "6. CLI + line edit" XELP_ENABLE_CLI XELP_ENABLE_LINE_EDIT build "7. CLI + line edit + help" XELP_ENABLE_CLI XELP_ENABLE_LINE_EDIT XELP_ENABLE_HELP build "8. CLI + LE + help + key" XELP_ENABLE_CLI XELP_ENABLE_LINE_EDIT XELP_ENABLE_HELP XELP_ENABLE_KEY build "9. Full (all features)" XELP_ENABLE_CLI XELP_ENABLE_LINE_EDIT XELP_ENABLE_HELP XELP_ENABLE_KEY XELP_ENABLE_THR +build "10. Full + history" XELP_ENABLE_CLI XELP_ENABLE_LINE_EDIT XELP_ENABLE_HELP XELP_ENABLE_KEY XELP_ENABLE_THR XELP_ENABLE_HISTORY echo "" diff --git a/dev/xelp-todo.md b/dev/xelp-todo.md index 1a17e14..bdd3b37 100644 --- a/dev/xelp-todo.md +++ b/dev/xelp-todo.md @@ -3,7 +3,7 @@ Future work for xelp, organized by area. Items marked with a design doc link have detailed specifications in `dev/`. -## Recently Completed (0.3.0 – 0.3.1) +## Recently Completed (0.3.0 – 0.3.2) - [x] Breaking API change: `XELP *ths` on all command/key signatures - [x] XelpBuf macro normalization (SCREAMING_CASE) @@ -27,6 +27,12 @@ link have detailed specifications in `dev/`. - [x] Cross-build multi-config (KEY/CLI/FULL, `extract_size.py`, 18 targets) - [x] README rewrite (3-column size table, grouped by word size) - [x] CI aligned with local validation (`make validate` in all workflows) +- [x] Command history (`XELP_ENABLE_HISTORY`): ring-buffer recall with + UP/DOWN arrows, consecutive dup suppression, in-progress save/restore. + ~420 bytes ARM Thumb. +- [x] `XelpArgInt` / `XelpArgStr` convenience functions for direct argument + access by index. +- [x] Test suite: 47 units, 598 cases, 100% line coverage ## Scripting Engine (deferred -- clean up core first) @@ -51,13 +57,41 @@ include xelp as the text front-end. Parked here for reference. Design doc: [dev/xelp_vm.md](xelp_vm.md) +## Argument Parsing Ergonomics + +Two-phase plan to reduce per-command boilerplate. Design doc: +[dev/arg_parse_updates.md](arg_parse_updates.md). + +### 0.3.2: Non-breaking convenience functions + +- [x] **`XelpArgInt(args, len, n, &val)`** -- get arg N as int, one call. + Wrapper over XelpTokN + XelpParseNum. ~50 bytes ARM Thumb. +- [x] **`XelpArgStr(args, len, n, &s, &slen)`** -- get arg N as string + span. ~40 bytes ARM Thumb. + +Functions (not macros) in the base CLI API. No new flag. Arg 0 is the +command name per argc/argv convention. + +### 0.4.0: Breaking handler signature change (argc/argv) + +- [ ] **Change CLI handler signature** from + `fn(XELP *ths, const char *args, int len)` to + `fn(XELP *ths, int argc, XelpBuf *argv)`. + Dispatcher pre-tokenizes into stack-allocated `XelpBuf argv[]`. + `XelpArgInt`/`XelpArgStr` simplify to take `XelpBuf *` directly. + +Last planned breaking change before the scripting engine. Stack cost +(~12 bytes per arg on 32-bit) is acceptable -- the primary audience +is 32-bit targets (ARM, ESP32, RISC-V). 8/16-bit builds remain +supported but are not the optimization target. + ## CLI Ergonomics Quality-of-life improvements for interactive use. Each is compile-time optional. Design constraint: must not bloat the core or break existing bare-metal use cases. -- [ ] **Command history** -- circular buffer of last N command lines, +- [x] **Command history** -- ring buffer of last N command lines, up/down arrow recall. User-supplied buffer (e.g. `char hist[4][64]`). Multi-byte key detection is already in place (`XELPKEYCODE` handles ESC sequences). Up/Down are currently silently dropped -- reserved diff --git a/docs/api-reference.md b/docs/api-reference.md index b28d866..693e78f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,6 +1,6 @@ # API Reference -All public types, functions, and macros defined in `xelp.h`. Version 0.3.1. +All public types, functions, and macros defined in `xelp.h`. Version 0.3.2. ## Types @@ -208,6 +208,41 @@ XELPRESULT cmd_divmod(XELP *ths, const char *args, int len) { } ``` +### `XelpArgInt` + +```c +XELPRESULT XelpArgInt(const char *args, int len, int n, int *val); +``` + +Get argument `n` (0-indexed) and parse it as an integer in a single call. +Wraps `XelpTokN` + `XelpParseNum`. Returns `XELP_S_OK` on success, +`XELP_E_ERR` if the token does not exist or is not a valid number. + +### `XelpArgStr` + +```c +XELPRESULT XelpArgStr(const char *args, int len, int n, + const char **s, int *slen); +``` + +Get argument `n` (0-indexed) as a string span. On success, `*s` points to +the first character of the token and `*slen` is its length. The token is +NOT null-terminated. Returns `XELP_S_OK` on success, `XELP_E_ERR` if the +token does not exist. + +### Example + +```c +XELPRESULT cmd_set(XELP *ths, const char *args, int len) { + const char *key; + int klen, value; + XelpArgStr(args, len, 1, &key, &klen); /* arg 1 = key name */ + XelpArgInt(args, len, 2, &value); /* arg 2 = int value */ + /* ... use key/klen and value ... */ + return XELP_S_OK; +} +``` + ## String Utilities ### `XelpStrLen` @@ -341,7 +376,7 @@ natural value (e.g. `'a'` == 0x61). Multi-byte keys are >= 0x100. | Constant | Value | Description | |----------|-------|-------------| -| `XELP_VERSION` | 0x00000301 | Library version (32-bit hex: `0x00MMmmpp`) | +| `XELP_VERSION` | 0x00000302 | Library version (32-bit hex: `0x00MMmmpp`) | | `XELP_VER_MAJOR(v)` | | Extract major version byte | | `XELP_VER_MINOR(v)` | | Extract minor version byte | | `XELP_VER_PATCH(v)` | | Extract patch version byte | @@ -359,15 +394,16 @@ Cortex-M0 (Thumb, `-Os`): | Profile | .text (bytes) | Flags | |---------|------------:|-------| -| CLI only | 1396 | `XELP_ENABLE_CLI` | -| CLI + help | 1496 | + `XELP_ENABLE_HELP` | -| CLI + key | 1500 | + `XELP_ENABLE_KEY` | -| CLI + help + key | 1874 | + both | -| CLI + help + key + thru | 1910 | + `XELP_ENABLE_THR` | -| CLI + line edit | 1840 | + `XELP_ENABLE_LINE_EDIT` | -| CLI + line edit + help | 1936 | + both | -| CLI + LE + help + key | 2358 | + all three | -| Full (all features) | 2394 | all flags | +| CLI only | 1508 | `XELP_ENABLE_CLI` | +| CLI + help | 1608 | + `XELP_ENABLE_HELP` | +| CLI + key | 1612 | + `XELP_ENABLE_KEY` | +| CLI + help + key | 1986 | + both | +| CLI + help + key + thru | 2026 | + `XELP_ENABLE_THR` | +| CLI + line edit | 1952 | + `XELP_ENABLE_LINE_EDIT` | +| CLI + line edit + help | 2048 | + both | +| CLI + LE + help + key | 2466 | + all three | +| Full (all features) | 2506 | all flags | +| Full + history | 2922 | all flags + `XELP_ENABLE_HISTORY` | Use `dev/size_profiles.sh` to regenerate this table for your toolchain. diff --git a/docs/build-profiles.md b/docs/build-profiles.md index 95b3c30..0140a94 100644 --- a/docs/build-profiles.md +++ b/docs/build-profiles.md @@ -71,6 +71,7 @@ without KEY, or KEY without HELP. | `XELP_ENABLE_KEY` | Single key press mode (menus, immediate actions) | -- | ~200-500 bytes | | `XELP_ENABLE_CLI` | Command line prompt, backspace, command dispatch, scripting, tokenizer | -- | Core (~2 KB) | | `XELP_ENABLE_LINE_EDIT` | Cursor movement (left/right, Home/End), insert-at-cursor, Delete, multi-byte ANSI key recognition | `XELP_ENABLE_CLI` | ~800-1000 bytes | +| `XELP_ENABLE_HISTORY` | Command history (UP/DOWN arrow recall of previous commands) | `XELP_ENABLE_CLI` + `XELP_ENABLE_LINE_EDIT` | ~420 bytes | | `XELP_ENABLE_THR` | Pass-through mode -- redirect all keys to another peripheral | -- | ~50-125 bytes | | `XELP_ENABLE_HELP` | Built-in help command listing all registered commands | -- | ~180-350 bytes | | `XELP_ENABLE_FULL` | Shorthand: enables KEY, CLI, THR, and HELP | -- | All combined | @@ -182,6 +183,7 @@ XELP_SET_VAL_CLI_PROMPT(myXelp, "ser1>"); #define XELP_ENABLE_LINE_EDIT 1 #define XELP_ENABLE_KEY 1 #define XELP_ENABLE_HELP 1 +#define XELP_ENABLE_HISTORY 1 /* optional: UP/DOWN arrow command recall */ #define XELP_CLI_PROMPT "mydev>" diff --git a/docs/configuration.md b/docs/configuration.md index ae26606..4f98d4d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,6 +13,7 @@ compiled out saves code space. | `XELP_ENABLE_KEY` | Single key press mode (menus, immediate actions) | ~200--500 bytes | | `XELP_ENABLE_THR` | Pass-through mode (redirect keys to another peripheral) | ~50--125 bytes | | `XELP_ENABLE_HELP` | Built-in help function listing all commands | ~180--350 bytes | +| `XELP_ENABLE_HISTORY` | Command history (UP/DOWN arrow recall). Requires `XELP_ENABLE_CLI` + `XELP_ENABLE_LINE_EDIT`. | ~420 bytes | | `XELP_ENABLE_FULL` | Enable all of the above | All combined | ## Key Mappings @@ -39,6 +40,7 @@ Override by redefining in `xelpcfg.h`, e.g. `#define XELPKEY_CLI ('c')` | Define | Default | Purpose | |--------|---------|---------| | `XELP_CMDBUFSZ` | 64 | Command line buffer size in bytes | +| `XELP_HIST_DEPTH` | 4 | Number of commands stored in history ring (requires `XELP_ENABLE_HISTORY`) | | `XELP_REGS_SZ` | 4 | Number of callee-clobbers-all return registers (minimum 4). R0 is command status, R1-R3 are command-specific. | | `XELPREG` | `int` | Register type (change for platforms where `int` is not ideal) | diff --git a/docs/examples.md b/docs/examples.md index 17b2844..f173a72 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -143,6 +143,7 @@ Requires ncurses (`sudo apt-get install libncurses5-dev` on Debian/Ubuntu). - Backspace handling with ncurses `delch()` - Mode change callback showing mode transitions - Token parsing and numeric argument handling +- Command history: UP/DOWN arrow recall of previous commands ### Architecture diff --git a/docs/porting.md b/docs/porting.md index 8df1684..c49c794 100644 --- a/docs/porting.md +++ b/docs/porting.md @@ -52,6 +52,21 @@ void handle_bksp(void) { XELP_SET_FN_BKSP(myXelp, &handle_bksp); ``` +## Optional: Command History + +If you enable `XELP_ENABLE_HISTORY` (requires `XELP_ENABLE_CLI` + `XELP_ENABLE_LINE_EDIT`), +users can recall previous commands with UP/DOWN arrows. No extra setup needed -- +it works automatically. Be aware of the RAM cost: + +``` +RAM = XELP_HIST_DEPTH * XELP_CMDBUFSZ + XELP_CMDBUFSZ + 4 bytes + = 4 * 64 + 64 + 4 = 324 bytes (default settings) +``` + +Reduce `XELP_HIST_DEPTH` (default 4) or `XELP_CMDBUFSZ` (default 64) if RAM is tight. +Override via compiler flag (`-DXELP_HIST_DEPTH=2`) or `xelp_ovr.h` when using +`XELP_CONFIG_OVERRIDE`. + ## Optional: Other Callbacks | Callback | Signature | Purpose | diff --git a/docs/tutorial.md b/docs/tutorial.md index 5310e8a..ab01b7f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -292,6 +292,19 @@ beyond standard VT100/ANSI support. Without `XELP_ENABLE_LINE_EDIT`, CLI uses append-only input with backspace via the `mpfBksp` callback. +## 10a. Command history + +When `XELP_ENABLE_HISTORY` is defined (requires `XELP_ENABLE_LINE_EDIT`), +the CLI remembers previously entered commands. Press UP to recall older +commands, DOWN to return to newer ones. + +- Empty commands are never stored +- Consecutive duplicates are suppressed (typing "help" three times stores one entry) +- In-progress text is saved when you first press UP and restored when you press DOWN past the newest entry +- The number of stored commands is configurable via `XELP_HIST_DEPTH` (default 4) + +No API calls are needed -- history works automatically once enabled. + ## 11. Mode change callback Get notified when the user switches modes: diff --git a/examples/README.md b/examples/README.md index 0d05521..54a75a4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,8 +3,8 @@ ## posix-simple Interactive CLI demo using ncurses for terminal handling on Linux/macOS. -Demonstrates CLI commands, KEY mode, THR mode, math operations, and -token parsing. +Demonstrates CLI commands, KEY mode, THR mode, command history (UP/DOWN +arrow recall), line editing, math operations, and token parsing. ```bash cd posix-simple && make diff --git a/examples/posix-simple/README.md b/examples/posix-simple/README.md index 629662c..8f21ad9 100644 --- a/examples/posix-simple/README.md +++ b/examples/posix-simple/README.md @@ -70,3 +70,5 @@ make clean # remove build artifacts - Token parsing and numeric arguments - Math operations dispatched to the same handler function - Register file (R0-R3) for command return values +- Line editing (left/right, Home/End, Delete) and multi-byte key recognition +- Command history: UP/DOWN arrow recall of previous commands diff --git a/examples/posix-simple/xelp-example.c b/examples/posix-simple/xelp-example.c index 5dc61f8..d0d3b43 100644 --- a/examples/posix-simple/xelp-example.c +++ b/examples/posix-simple/xelp-example.c @@ -351,6 +351,7 @@ int main (int argc, char *argv[]) "ESC : single-key mode (x = exit, h = help)\n" "CTRL-P : CLI mode (type command + ENTER)\n" "CTRL-T : pass-through mode\n" + "UP/DOWN : recall previous commands\n" "\n"; XelpInit(&example, pAboutStr); diff --git a/idf_component.yml b/idf_component.yml index 5b088ff..efabd36 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -3,13 +3,13 @@ # This file allows xelp to be published to the ESP Component Registry # (components.espressif.com) and installed in any ESP-IDF project with: # -# idf.py add-dependency "deftio/xelp==0.3.1" +# idf.py add-dependency "deftio/xelp==0.3.2" # # Or by adding to your project's main/idf_component.yml: # # dependencies: # deftio/xelp: -# version: "~0.3.1" +# version: "~0.3.2" # # After installation, use in your C or C++ source: # @@ -21,7 +21,7 @@ # # See: https://docs.espressif.com/projects/idf-component-manager/en/latest/reference/manifest_file.html -version: "0.3.1" +version: "0.3.2" description: >- Tiny CLI and script interpreter for embedded systems. 2KB flash, zero malloc, multi-instance. diff --git a/library.json b/library.json index 38ff7dd..72a22e9 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "xelp", - "version": "0.3.1", + "version": "0.3.2", "description": "Tiny CLI and script interpreter for embedded systems. 2KB flash, zero malloc, multi-instance. Interactive serial command line with scriptable dispatch, single-key menus, and pass-through mode.", "keywords": [ "cli", diff --git a/library.properties b/library.properties index d5536de..1a24ace 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=xelp -version=0.3.1 +version=0.3.2 author=M. A. Chatterjee maintainer=M. A. Chatterjee sentence=Tiny CLI and script interpreter for embedded systems. 2KB, zero malloc, multi-instance. diff --git a/llms.txt b/llms.txt index 3b32da6..af490e9 100644 --- a/llms.txt +++ b/llms.txt @@ -22,7 +22,7 @@ commands, single-key menus, and pass-through mode. Three source files ## Code - [xelp.h](https://github.com/deftio/xelp/blob/master/src/xelp.h): Public API header -- types, macros, function declarations -- [xelp.c](https://github.com/deftio/xelp/blob/master/src/xelp.c): Implementation (~980 lines) +- [xelp.c](https://github.com/deftio/xelp/blob/master/src/xelp.c): Implementation (~1100 lines) - [xelpcfg.h](https://github.com/deftio/xelp/blob/master/src/xelpcfg.h): Compile-time configuration ## Examples diff --git a/pages/api-reference.html b/pages/api-reference.html index 9829cf3..5a0b5fe 100644 --- a/pages/api-reference.html +++ b/pages/api-reference.html @@ -27,7 +27,7 @@

API Reference

All public types, functions, and macros defined in xelp.h. -Version 0.3.1.

+Version 0.3.2.

Types

@@ -106,7 +106,7 @@

XelpParseXB

Same as XelpParse but takes a XelpBuf.

XelpExecKC

-
XELPRESULT XelpExecKC(XELP *ths, char key);
+
XELPRESULT XelpExecKC(XELP *ths, XELPKEYCODE key);
 

Execute a single-key command directly (bypasses mode checking).

@@ -114,7 +114,7 @@

XelpOut

XELPRESULT XelpOut(XELP *ths, const char *msg, int maxlen);
 

Output a string through the instance’s output function. If -maxlen > 0, prints exactly that many characters. If +maxlen > 0, prints at most that many characters. If maxlen == 0, prints until null terminator.

XelpHelp

@@ -141,6 +141,52 @@

XelpNumToks

Count the number of tokens in a buffer.

+

XelpArgs — Sequential Argument Iterator

+ +

A left-to-right token iterator for CLI command handlers. O(1) per token.

+ +

XelpArgsInit

+
XELPRESULT XelpArgsInit(XelpArgs *a, const char *args, int len);
+
+

Initialize an argument iterator from the callback’s args and len.

+ +

XelpNextTok

+
XELPRESULT XelpNextTok(XelpArgs *a, XelpBuf *tok);
+
+

Get the next token. Pass NULL for tok to skip.

+ +

XelpNextInt

+
XELPRESULT XelpNextInt(XelpArgs *a, int *val);
+
+

Get the next token and parse it as an integer (decimal or hex).

+ +

XelpArgCount

+
XELPRESULT XelpArgCount(XelpArgs *a, int *n);
+
+

Count total tokens without consuming them.

+ +

Direct Argument Access

+ +

XelpArgInt

+
XELPRESULT XelpArgInt(const char *args, int len, int n, int *val);
+
+

Get argument n (0-indexed) as an integer in one call. +Wraps XelpTokN + XelpParseNum.

+ +

XelpArgStr

+
XELPRESULT XelpArgStr(const char *args, int len, int n,
+                      const char **s, int *slen);
+
+

Get argument n (0-indexed) as a string span. On success, +*s points to the token and *slen is its length. +Not null-terminated.

+ +

XelpPutc

+
XELPRESULT XelpPutc(XELP *ths, char c);
+
+

Output a single character through the instance’s output function. +Respects mOutEnable.

+

String utilities

XelpStrLen

@@ -218,7 +264,7 @@

Constants

- + diff --git a/pages/configuration.html b/pages/configuration.html index 85f8d19..5e0d702 100644 --- a/pages/configuration.html +++ b/pages/configuration.html @@ -58,6 +58,9 @@

CLI (~3–5 KB)

#define XELP_ENABLE_HELP 1 +

Optional: add XELP_ENABLE_HISTORY for UP/DOWN arrow command +recall (~420 bytes, requires XELP_ENABLE_LINE_EDIT).

+

Representative size: ~2600 bytes on ARM Thumb, ~4200 bytes on AVR.

@@ -84,6 +87,7 @@

Feature flags

+
ConstantValueDescription
XELP_VERSION0x00000300Library version (32-bit hex: 0x00MMmmpp)
XELP_VERSION0x00000302Library version (32-bit hex: 0x00MMmmpp)
XELP_VER_MAJOR(v)Extract major version byte
XELP_VER_MINOR(v)Extract minor version byte
XELP_VER_PATCH(v)Extract patch version byte
XELP_ENABLE_KEYSingle key press mode (menus, immediate actions)~200–500 bytes
XELP_ENABLE_THRPass-through mode (redirect keys to another peripheral)~50–125 bytes
XELP_ENABLE_HELPBuilt-in help function listing all commands~180–350 bytes
XELP_ENABLE_HISTORYCommand history (UP/DOWN arrow recall). Requires XELP_ENABLE_CLI + XELP_ENABLE_LINE_EDIT.~420 bytes
XELP_ENABLE_FULLEnable all of the aboveAll combined
@@ -120,6 +124,7 @@

Buffer and register sizes

DefineDefaultPurpose XELP_CMDBUFSZ64Command line buffer size in bytes +XELP_HIST_DEPTH4Number of commands stored in history ring (requires XELP_ENABLE_HISTORY) XELP_REGS_SZ4Number of callee-clobbers-all return registers (minimum 4). R0 is command status, R1–R3 are command-specific. XELPREGintRegister type (change for platforms where int is not ideal) diff --git a/pages/index.html b/pages/index.html index 9035e73..1d96c20 100644 --- a/pages/index.html +++ b/pages/index.html @@ -29,7 +29,7 @@

A command line interpreter and script engine for embedded systems.

- Version + Version License CI Coverage @@ -57,6 +57,8 @@

Features

with a one-line table entry, callable interactively or from scripts
  • Multiple independent instances on different serial ports, BLE, USB CDC
  • Char-at-a-time parsing — feed from UART, ISR, or any byte stream
  • +
  • Command history — UP/DOWN arrow recall of previous commands + (optional, XELP_ENABLE_HISTORY)
  • Built-in tokenizer with quoted strings, escape sequences, # comments
  • Scriptable — run multi-command const strings from ROM or RAM
  • Zero dependencies (no stdio.h, string.h, etc.)
  • @@ -169,26 +171,26 @@

    Compiled sizes

    - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +
    CPUWidthCompilerKEY (bytes)CLI (bytes)FULL (bytes)
    AVR (ATtiny85)8avr-gcc104640384096
    AVR (ATmega328P)8avr-gcc105441324190
    Z808SDCC212169667074
    6800 (HC08)8SDCC247181478288
    MSP43016msp430-gcc77032603306
    68HC1116m68hc11-gcc236965706641
    Xtensa LX7 (ESP32-S3)32xtensa-esp-elf-gcc57625082540
    ARM Thumb32arm-none-eabi-gcc58024822526
    RISC-V (rv32)32riscv64-unknown-elf-gcc72229703008
    Xtensa LX106 (ESP8266)32xtensa-lx106-elf-gcc72328312863
    m68k32m68k-linux-gnu-gcc72831463194
    ARM3232arm-none-eabi-gcc98037463806
    x86-3232GCC108146044654
    MIPS3232mipsel-linux-gnu-gcc129648884936
    PowerPC32powerpc-linux-gnu-gcc150456745738
    RISC-V (rv64)64riscv64-linux-gnu-gcc75633323366
    x86-6464Clang104350135055
    x86-6464GCC106347874836
    AArch64 (ARM64)64aarch64-linux-gnu-gcc132451785234
    MIPS6464mips64el-linux-gnuabi64-gcc136055125560
    AVR (ATtiny85)8avr-gcc104642664324
    AVR (ATmega328P)8avr-gcc105443664424
    Z808SDCC212172807388
    6800 (HC08)8SDCC247186148715
    MSP43016msp430-gcc77034823528
    68HC1116m68hc11-gcc236968846955
    Xtensa LX7 (ESP32-S3)32xtensa-esp-elf-gcc57625922624
    ARM Thumb32arm-none-eabi-gcc58025982642
    RISC-V (rv32)32riscv64-unknown-elf-gcc72230943132
    Xtensa LX106 (ESP8266)32xtensa-lx106-elf-gcc72329552987
    m68k32m68k-linux-gnu-gcc72833323380
    ARM3232arm-none-eabi-gcc98039303990
    x86-3232GCC108149164966
    MIPS3232mipsel-linux-gnu-gcc129652245272
    PowerPC32powerpc-linux-gnu-gcc150460586122
    RISC-V (rv64)64riscv64-linux-gnu-gcc75635483582
    x86-6464Clang104352685310
    x86-6464GCC106351365185
    AArch64 (ARM64)64aarch64-linux-gnu-gcc132455665622
    MIPS6464mips64el-linux-gnuabi64-gcc136058645928
    @@ -207,7 +209,7 @@

    Building and testing

    make clean-all # remove all build artifacts -

    39 test units, 531 test cases, 100% line coverage of xelp.c.

    +

    47 test units, 598 test cases, 100% line coverage of xelp.c.

    Documentation

    diff --git a/pages/releases.html b/pages/releases.html index bf8cf2c..1377e23 100644 --- a/pages/releases.html +++ b/pages/releases.html @@ -29,6 +29,21 @@

    Release History

    See also: CHANGELOG.md on GitHub for the full changelog.

    +

    v0.3.2 (2026-04-26)

    +
      +
    • Command history (XELP_ENABLE_HISTORY): + UP/DOWN arrow recall of previously entered commands. Fixed-slot ring + buffer, configurable depth (XELP_HIST_DEPTH, default 4). + Consecutive duplicate suppression, in-progress line save/restore. + ~420 bytes ARM Thumb.
    • +
    • XelpArgInt / XelpArgStr: direct-access + convenience functions for getting the Nth argument as an integer or + string span in a single call.
    • +
    • Fixed XELPKEY_BKSP (0x07 BEL): added XELPKEY_BS + (0x08) so both backspace codes are accepted.
    • +
    • Test suite expanded to 47 units, 598 test cases.
    • +
    +

    v0.3.1 (2026-04-26)

    • Function rename: all public functions renamed from diff --git a/release_management.md b/release_management.md index 6e87fc6..f49a22f 100644 --- a/release_management.md +++ b/release_management.md @@ -7,14 +7,14 @@ Reference for building, testing, and releasing xelp. The `XELP_VERSION` macro in `src/xelp.h` is the single source of truth: ```c -#define XELP_VERSION (0x00000301UL) /* 32-bit: 0x00MMmmpp */ +#define XELP_VERSION (0x00000302UL) /* 32-bit: 0x00MMmmpp */ #define XELP_VER_MAJOR(v) (((v) >> 16) & 0xFF) #define XELP_VER_MINOR(v) (((v) >> 8) & 0xFF) #define XELP_VER_PATCH(v) ( (v) & 0xFF) ``` The 32-bit hex format encodes `0x00MMmmpp` (major.minor.patch), one byte -each. Example: `0x00010000` = version 1.0.0, `0x00000301` = version 0.3.1. +each. Example: `0x00010000` = version 1.0.0, `0x00000302` = version 0.3.2. Accessor macros resolve to constants at compile time on all targets. The build tool `tools/extract_version.c` reads the version via the C diff --git a/src/xelp.c b/src/xelp.c index 149f7a1..df3ed7a 100755 --- a/src/xelp.c +++ b/src/xelp.c @@ -162,6 +162,112 @@ static void _xelpRedrawFromCursor(XELP *ths) { } #endif +#if defined(XELP_ENABLE_CLI) && defined(XELP_ENABLE_LINE_EDIT) && defined(XELP_ENABLE_HISTORY) + +/***************************************** + _xelpHistReplaceLine() - clear displayed line and load new content. + */ +static void _xelpHistReplaceLine(XELP *ths, const char *src, int slen) { + int oldlen = (int)(ths->mCmdXB.p - ths->mCmdXB.s); + int i; + + /* move cursor to beginning of line (visual) */ + while (ths->mCur > ths->mCmdXB.s) { + ths->mCur--; + _CURSOR('\b'); + } + /* overwrite old content with spaces */ + for (i = 0; i < oldlen; i++) _CURSOR(' '); + /* backspace back to start */ + for (i = 0; i < oldlen; i++) _CURSOR('\b'); + + /* copy new content into command buffer */ + for (i = 0; i < slen && i < XELP_CMDBUFSZ - 1; i++) + ths->mCmdMsgBuf[i] = src[i]; + ths->mCmdXB.p = ths->mCmdXB.s + slen; + ths->mCur = ths->mCmdXB.p; + + /* echo new content */ + for (i = 0; i < slen; i++) _ECHO(src[i]); +} + +/***************************************** + _xelpHistSave() - save command to history ring. + Called from _xelpHandleEnter before buffer reset. + Skips empty commands and consecutive duplicates. + */ +static void _xelpHistSave(XELP *ths) { + int len = (int)(ths->mCmdXB.p - ths->mCmdXB.s); + int prev; + ths->mHistBrowse = -1; /* ENTER always ends browse session */ + if (len <= 0) return; + + /* skip consecutive duplicate */ + if (ths->mHistCount > 0) { + prev = ((int)ths->mHistWrite - 1 + XELP_HIST_DEPTH) % XELP_HIST_DEPTH; + if (XelpStrEq(ths->mCmdXB.s, len, ths->mHistBuf[prev]) == XELP_S_OK) + return; + } + + /* copy command into ring slot */ + { + char *dst = ths->mHistBuf[(int)ths->mHistWrite]; + int i; + for (i = 0; i < len && i < XELP_CMDBUFSZ - 1; i++) + dst[i] = ths->mCmdXB.s[i]; + dst[i] = 0; + } + ths->mHistWrite = (char)(((int)ths->mHistWrite + 1) % XELP_HIST_DEPTH); + if (ths->mHistCount < XELP_HIST_DEPTH) + ths->mHistCount++; +} + +/***************************************** + _xelpHistRecall() - handle UP/DOWN arrow for history browsing. + dir: -1 = UP (older), +1 = DOWN (newer) + */ +static void _xelpHistRecall(XELP *ths, int dir) { + if (dir < 0) { + /* UP: go to older entry */ + if (ths->mHistCount == 0) return; + + if (ths->mHistBrowse == -1) { + /* first UP: save in-progress line */ + int len = (int)(ths->mCmdXB.p - ths->mCmdXB.s); + int i; + for (i = 0; i < len; i++) + ths->mHistSaved[i] = ths->mCmdXB.s[i]; + ths->mHistSaved[len] = 0; + ths->mHistSavedLen = (char)len; + /* start at most recent entry */ + ths->mHistBrowse = ths->mHistCount - 1; + } else if (ths->mHistBrowse > 0) { + ths->mHistBrowse--; + } else { + return; /* already at oldest */ + } + } else { + /* DOWN: go to newer entry */ + if (ths->mHistBrowse == -1) return; /* not browsing */ + + if (ths->mHistBrowse < ths->mHistCount - 1) { + ths->mHistBrowse++; + } else { + /* past newest: restore in-progress line */ + ths->mHistBrowse = -1; + _xelpHistReplaceLine(ths, ths->mHistSaved, (int)ths->mHistSavedLen); + return; + } + } + + /* load the entry at mHistBrowse (0=oldest, count-1=newest) */ + { + int slot = ((int)ths->mHistWrite - (int)ths->mHistCount + (int)ths->mHistBrowse + XELP_HIST_DEPTH) % XELP_HIST_DEPTH; + _xelpHistReplaceLine(ths, ths->mHistBuf[slot], XelpStrLen(ths->mHistBuf[slot])); + } +} +#endif /* XELP_ENABLE_CLI && XELP_ENABLE_LINE_EDIT && XELP_ENABLE_HISTORY */ + #ifdef XELP_ENABLE_HELP /***************************************** _xelpPrintKeyName() - print human-readable name for a keycode in help output @@ -286,6 +392,9 @@ XELPRESULT XelpInit ( #endif #if defined(XELP_ENABLE_CLI) && defined(XELP_ENABLE_LINE_EDIT) ths->mCur = ths->mCmdXB.s; +#endif +#if defined(XELP_ENABLE_CLI) && defined(XELP_ENABLE_HISTORY) + ths->mHistBrowse = -1; #endif /* comand mode mssage index ths->mCmdMsgIndex = 0; //set to 0 by ptr loop at top @@ -711,6 +820,36 @@ XELPRESULT XelpArgCount (XelpArgs *a, int *n) XELP_XB_COPY(save, a->buf); return XELP_S_OK; } + +/******************************************************** + XelpArgInt() - get the Nth argument as an integer (random access). + Arg 0 is the command name. Returns XELP_E_ERR if arg N doesn't exist + or is not a valid number. + */ +XELPRESULT XelpArgInt (const char *args, int len, int n, int *val) +{ + XelpBuf b, tok; + XELP_XB_INIT(b, (char*)args, len); + if (XelpTokN(&b, n, &tok) != XELP_S_OK) return XELP_E_ERR; + return XelpParseNum(tok.s, (int)(tok.p - tok.s), val); +} + +/******************************************************** + XelpArgStr() - get the Nth argument as a string span (random access). + Sets *s to start of token and *slen to its length. + Token is NOT null-terminated (buffer is not modified). + Returns XELP_E_ERR if arg N doesn't exist. + */ +XELPRESULT XelpArgStr (const char *args, int len, int n, + const char **s, int *slen) +{ + XelpBuf b, tok; + XELP_XB_INIT(b, (char*)args, len); + if (XelpTokN(&b, n, &tok) != XELP_S_OK) return XELP_E_ERR; + *s = tok.s; + *slen = (int)(tok.p - tok.s); + return XELP_S_OK; +} #endif /* XELP_ENABLE_CLI */ /******************************************************** @@ -739,9 +878,12 @@ static void _xelpCursorMove(XELP *ths, int dir, int all) { #endif #ifdef XELP_ENABLE_CLI -/* handle ENTER: echo newline, execute buffer, reset, show prompt */ +/* handle ENTER: echo newline, save to history, execute buffer, reset, show prompt */ static void _xelpHandleEnter(XELP *ths) { XelpBuf line; +#if defined(XELP_ENABLE_LINE_EDIT) && defined(XELP_ENABLE_HISTORY) + _xelpHistSave(ths); +#endif _PUTC(XELPKEY_ENTER); XELP_XB_INIT_PTRS(line, ths->mCmdXB.s, ths->mCmdXB.s, ths->mCmdXB.p); XelpParseXB(ths, &line); @@ -839,8 +981,14 @@ XELPRESULT XelpParseKey (XELP *ths, char key) break; } case XELP_KEYCODE_UP: +#ifdef XELP_ENABLE_HISTORY + _xelpHistRecall(ths, -1); +#endif + break; case XELP_KEYCODE_DOWN: - /* silently drop (reserved for future history) */ +#ifdef XELP_ENABLE_HISTORY + _xelpHistRecall(ths, +1); +#endif break; default: /* silently drop other multi-byte keys */ @@ -849,7 +997,7 @@ XELPRESULT XelpParseKey (XELP *ths, char key) } else { /* single-char key in CLI mode with line editing */ char ch = (char)keycode; - if (ch == XELPKEY_BKSP || ch == XELPKEY_DEL) { + if (ch == XELPKEY_BKSP || ch == XELPKEY_BS || ch == XELPKEY_DEL) { /* delete char before cursor */ if (ths->mCur > ths->mCmdXB.s) { int tail = (int)(ths->mCmdXB.p - ths->mCur); @@ -891,7 +1039,7 @@ XELPRESULT XelpParseKey (XELP *ths, char key) /* silently drop multi-byte keys */ } else { char ch = (char)keycode; - if (ch == XELPKEY_BKSP) { + if (ch == XELPKEY_BKSP || ch == XELPKEY_BS) { if (ths->mCmdXB.p > ths->mCmdXB.s) { (ths->mCmdXB.p)--; if (ths->mpfBksp) diff --git a/src/xelp.h b/src/xelp.h index 7bd6f47..a14b161 100755 --- a/src/xelp.h +++ b/src/xelp.h @@ -44,7 +44,7 @@ extern "C" { #endif -#define XELP_VERSION (0x00000301UL) /* 32-bit version: 0x00MMmmpp (major.minor.patch) */ +#define XELP_VERSION (0x00000302UL) /* 32-bit version: 0x00MMmmpp (major.minor.patch) */ #define XELP_VER_MAJOR(v) (((v) >> 16) & 0xFF) #define XELP_VER_MINOR(v) (((v) >> 8) & 0xFF) #define XELP_VER_PATCH(v) ( (v) & 0xFF) @@ -140,7 +140,11 @@ typedef unsigned long XELPKEYCODE; #define XELP_T_OK(r) ((r)>=0) /* simple macro for testing OK or warning (e.g. not a failure) */ -#define XELP_CMDBUFSZ (64) +#ifndef XELP_HIST_DEPTH +#define XELP_HIST_DEPTH (4) /* history ring depth (overridable) */ +#endif + +#define XELP_CMDBUFSZ (64) /** used by tokenizer funciton @@ -238,7 +242,8 @@ typedef struct #define XELPKEY_ENTER ('\n') /* Enter Key for Cmd Mode */ #define XELPKEY_SPC (0x20) /* space char */ -#define XELPKEY_BKSP (0x7) /* back space */ +#define XELPKEY_BKSP (0x7) /* back space (legacy) */ +#define XELPKEY_BS (0x08) /* ASCII BS */ #define XELPKEY_DEL (0x7f) /* DEL */ #define XELPKEY_ESC (0x1b) /* Escape */ @@ -302,6 +307,15 @@ typedef struct XELP_tag char* mCur; /* cursor position in [mCmdXB.s .. mCmdXB.p] */ #endif +#if defined(XELP_ENABLE_CLI) && defined(XELP_ENABLE_HISTORY) + char mHistBuf[XELP_HIST_DEPTH][XELP_CMDBUFSZ]; /* history ring */ + char mHistWrite; /* next write slot (ring index) */ + char mHistCount; /* entries stored (0..DEPTH) */ + char mHistBrowse; /* browse position (-1 = not browsing) */ + char mHistSaved[XELP_CMDBUFSZ]; /* stash of in-progress line on first UP */ + char mHistSavedLen; /* length of saved in-progress line */ +#endif + /**** platform dependant dispatch functions (light-weight hardware abstraction layer) note that if any are left unset (zero) this is OK as system will not call null ptrs. @@ -406,6 +420,12 @@ XELPRESULT XelpNextTok (XelpArgs *a, XelpBuf *tok); XELPRESULT XelpNextInt (XelpArgs *a, int *val); XELPRESULT XelpArgCount (XelpArgs *a, int *n); +/* Direct-access argument helpers (random access, O(N) per call). + Arg 0 is the command name, arg 1 is the first real argument. */ +XELPRESULT XelpArgInt (const char *args, int len, int n, int *val); +XELPRESULT XelpArgStr (const char *args, int len, int n, + const char **s, int *slen); + /* XELPNEXTTOK get next token in a string buffer. This is just a macro call to XelpTokLine */ /* #define XELPNEXTTOK(buf,blen,tok_s,tok_e) (XelpTokLine(buf, buf+blen, tok_s, tok_e, 0, XELP_TOK_ONLY)) */ int XelpStrLen(const char* c); /* compute length of null terminated string. */ diff --git a/src/xelpcfg.h b/src/xelpcfg.h index 4411cad..87f975e 100755 --- a/src/xelpcfg.h +++ b/src/xelpcfg.h @@ -15,8 +15,16 @@ #define __XELP_CONFIG_H__ #ifdef XELP_CONFIG_OVERRIDE -#include "xelp_ovr.h" /* 8.3 filenaming convention used due to old-school compilers and filesystem support */ -#else /* use the rest of this file's conventions */ +/* + To use your own configuration without modifying this file: + 1. Define XELP_CONFIG_OVERRIDE in your compiler flags (-DXELP_CONFIG_OVERRIDE) + 2. Create xelp_ovr.h in your include path with the defines you need + 3. Any XELP_XXX define not set in xelp_ovr.h will use the default from xelp.h + This keeps the library source untouched across updates. + See docs/build-profiles.md for details and examples. +*/ +#include "xelp_ovr.h" +#else /* use the defaults below */ /**************************************************************************************************** @@ -53,6 +61,17 @@ */ #define XELP_ENABLE_LINE_EDIT 1 +/**************************************************************************************************** + Enable Command History (UP/DOWN arrow recall). + When defined, provides a ring buffer of previously entered commands that can + be browsed with UP/DOWN arrows. Requires XELP_ENABLE_CLI and XELP_ENABLE_LINE_EDIT. + XELP_HIST_DEPTH sets the number of commands stored (default 4, overridable via + compiler flag or xelp_ovr.h when XELP_CONFIG_OVERRIDE is defined). + RAM cost: XELP_HIST_DEPTH * XELP_CMDBUFSZ + XELP_CMDBUFSZ + 4 bytes per instance. + Code cost: ~420 bytes on ARM Thumb (-Os). + */ +#define XELP_ENABLE_HISTORY 1 + /**************************************************************************************************** Enable KEY Mode. diff --git a/tests/xelp_unit_tests.c b/tests/xelp_unit_tests.c index 82c6cea..435fb21 100755 --- a/tests/xelp_unit_tests.c +++ b/tests/xelp_unit_tests.c @@ -2506,10 +2506,17 @@ XELPRESULT test_KeyAccumulator() { if (JB_ASSERT(XELP_XB_POS(x.mCmdXB) != 0, "accum CSI stalls")) return XELP_E_ERR; - /* ESC + '[' + 'A' = UP arrow: silently dropped in CLI (no change to buf) */ + /* ESC + '[' + 'A' = UP arrow in CLI */ XelpParseKey(&x, 'A'); +#ifdef XELP_ENABLE_HISTORY + /* history recalls "a" (typed above): buf should have 1 char */ + if (JB_ASSERT(XELP_XB_POS(x.mCmdXB) != 1, "accum UP arrow recalls from history")) + return XELP_E_ERR; + XelpParseKey(&x, XELPKEY_ENTER); /* reset for next test */ +#else if (JB_ASSERT(XELP_XB_POS(x.mCmdXB) != 0, "accum UP arrow dropped in CLI")) return XELP_E_ERR; +#endif /* 4-byte sequence: ESC [ 3 ~ (KDEL) at empty buf: no effect */ XelpParseKey(&x, 0x1B); @@ -2711,6 +2718,200 @@ XELPRESULT test_CLILineEdit_Backspace() { return XELP_S_OK; } +/* ==================================================================== + test_CLIBackspaceBS() -- comprehensive tests for 0x08 (ASCII BS) + Verifies that 0x08 works identically to XELPKEY_BKSP (0x07) in all contexts. + */ +XELPRESULT test_CLIBackspaceBS() { + XELP x; + char buf[64]; + + /* --- 1. 0x08 deletes char at end of line --- */ + { + XelpInit(&x,"TestBS1"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "abc"); + XelpParseKey(&x, XELPKEY_BS); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "ab"), + "BS 0x08 delete at end")) + return XELP_E_ERR; + } + + /* --- 2. 0x08 deletes char mid-line (with line editing) --- */ + { + XelpInit(&x,"TestBS2"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "hello"); + feedKeycode(&x, XELP_KEYCODE_LEFT); + feedKeycode(&x, XELP_KEYCODE_LEFT); + XelpParseKey(&x, XELPKEY_BS); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "helo"), + "BS 0x08 mid-line")) + return XELP_E_ERR; + } + + /* --- 3. 0x08 at start of line: no-op (no crash, no change) --- */ + { + XelpInit(&x,"TestBS3"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "ab"); + feedKeycode(&x, XELP_KEYCODE_HOME); + XelpParseKey(&x, XELPKEY_BS); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "ab"), + "BS 0x08 at start no-op")) + return XELP_E_ERR; + } + + /* --- 4. 0x08 on empty buffer: no crash --- */ + { + XelpInit(&x,"TestBS4"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + XelpParseKey(&x, XELPKEY_BS); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XelpStrLen(buf) != 0, + "BS 0x08 empty buf no crash")) + return XELP_E_ERR; + } + + /* --- 5. 0x08 and 0x07 produce identical results --- */ + { + XELP x1, x2; + char buf1[64], buf2[64]; + + XelpInit(&x1,"TestBS5a"); + XELP_SET_FN_CLI(x1,gMyCLICommands); + XELP_SET_FN_OUT(x1,dummyOut); + XelpInit(&x2,"TestBS5b"); + XELP_SET_FN_CLI(x2,gMyCLICommands); + XELP_SET_FN_OUT(x2,dummyOut); + + feedString(&x1, "test"); + feedKeycode(&x1, XELP_KEYCODE_LEFT); + XelpParseKey(&x1, XELPKEY_BKSP); + + feedString(&x2, "test"); + feedKeycode(&x2, XELP_KEYCODE_LEFT); + XelpParseKey(&x2, XELPKEY_BS); + + getCmdBuf(&x1, buf1, sizeof(buf1)); + getCmdBuf(&x2, buf2, sizeof(buf2)); + + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf1, XelpStrLen(buf1), buf2), + "BS 0x08 == BKSP 0x07")) + return XELP_E_ERR; + } + + /* --- 6. Multiple 0x08 deletions --- */ + { + XelpInit(&x,"TestBS6"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "abcde"); + XelpParseKey(&x, XELPKEY_BS); + XelpParseKey(&x, XELPKEY_BS); + XelpParseKey(&x, XELPKEY_BS); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "ab"), + "BS 0x08 triple delete")) + return XELP_E_ERR; + } + + /* --- 7. 0x08 after insert mid-line --- */ + { + XelpInit(&x,"TestBS7"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "abcd"); + feedKeycode(&x, XELP_KEYCODE_LEFT); + feedKeycode(&x, XELP_KEYCODE_LEFT); + feedString(&x, "XY"); + XelpParseKey(&x, XELPKEY_BS); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "abXcd"), + "BS 0x08 after insert")) + return XELP_E_ERR; + } + + /* --- 8. 0x08 delete all chars one by one --- */ + { + int i; + XelpInit(&x,"TestBS8"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "abcd"); + for (i = 0; i < 4; i++) + XelpParseKey(&x, XELPKEY_BS); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XelpStrLen(buf) != 0, + "BS 0x08 delete all")) + return XELP_E_ERR; + + /* one more should be harmless */ + XelpParseKey(&x, XELPKEY_BS); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XelpStrLen(buf) != 0, + "BS 0x08 past empty")) + return XELP_E_ERR; + } + + /* --- 9. 0x08 with echo mask --- */ + { + XelpInit(&x,"TestBS9"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,gDummyBufOut); + XELP_SET_ECHO(x, '*'); + + resetDummyBuf(); + feedString(&x, "abc"); + XelpParseKey(&x, XELPKEY_BS); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "ab"), + "BS 0x08 with mask buffer")) + return XELP_E_ERR; + } + + /* --- 10. Mixed 0x07 and 0x08 in same session --- */ + { + XelpInit(&x,"TestBS10"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "abcdef"); + XelpParseKey(&x, XELPKEY_BKSP); /* 0x07: remove 'f' */ + XelpParseKey(&x, XELPKEY_BS); /* 0x08: remove 'e' */ + XelpParseKey(&x, XELPKEY_BKSP); /* 0x07: remove 'd' */ + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "abc"), + "BS mixed 0x07 0x08")) + return XELP_E_ERR; + } + + return XELP_S_OK; +} + /* ==================================================================== test_CLIArrowsDrop() - UP/DOWN in CLI: no corruption */ @@ -3250,6 +3451,109 @@ XELPRESULT test_XelpArgs() { return XELP_S_OK; } +/* ==================================================================== + test_XelpArgIntStr() - direct-access argument helpers + */ +XELPRESULT test_XelpArgIntStr() { + XELPRESULT r; + int val; + const char *s; + int slen; + + /* 1. XelpArgInt: get arg 1 as int */ + { + char buf[] = "led 42"; + r = XelpArgInt(buf, XelpStrLen(buf), 1, &val); + if (JB_ASSERT(r != XELP_S_OK || val != 42, "ArgInt basic")) + return XELP_E_ERR; + } + + /* 2. XelpArgInt: arg 0 is the command name */ + { + char buf[] = "divmod 10 3"; + r = XelpArgInt(buf, XelpStrLen(buf), 0, &val); + /* "divmod" is not a number */ + if (JB_ASSERT(r != XELP_E_ERR, "ArgInt arg0 not a number")) + return XELP_E_ERR; + } + + /* 3. XelpArgInt: multi args */ + { + char buf[] = "divmod 10 3"; + r = XelpArgInt(buf, XelpStrLen(buf), 1, &val); + if (JB_ASSERT(r != XELP_S_OK || val != 10, "ArgInt arg1=10")) + return XELP_E_ERR; + r = XelpArgInt(buf, XelpStrLen(buf), 2, &val); + if (JB_ASSERT(r != XELP_S_OK || val != 3, "ArgInt arg2=3")) + return XELP_E_ERR; + } + + /* 4. XelpArgInt: hex */ + { + char buf[] = "cmd 0xFF"; + r = XelpArgInt(buf, XelpStrLen(buf), 1, &val); + if (JB_ASSERT(r != XELP_S_OK || val != 255, "ArgInt hex 0xFF")) + return XELP_E_ERR; + } + + /* 5. XelpArgInt: arg past end */ + { + char buf[] = "cmd 1"; + val = -1; + r = XelpArgInt(buf, XelpStrLen(buf), 5, &val); + if (JB_ASSERT(r != XELP_E_ERR, "ArgInt past end")) + return XELP_E_ERR; + if (JB_ASSERT(val != -1, "ArgInt past end val unchanged")) + return XELP_E_ERR; + } + + /* 6. XelpArgInt: negative */ + { + char buf[] = "adj -7"; + r = XelpArgInt(buf, XelpStrLen(buf), 1, &val); + if (JB_ASSERT(r != XELP_S_OK || val != -7, "ArgInt negative")) + return XELP_E_ERR; + } + + /* 7. XelpArgStr: basic */ + { + char buf[] = "ssid MyNetwork"; + r = XelpArgStr(buf, XelpStrLen(buf), 1, &s, &slen); + if (JB_ASSERT(r != XELP_S_OK || slen != 9, "ArgStr basic len")) + return XELP_E_ERR; + if (JB_ASSERT(s[0] != 'M' || s[8] != 'k', "ArgStr basic content")) + return XELP_E_ERR; + } + + /* 8. XelpArgStr: arg 0 = command name */ + { + char buf[] = "echo hello"; + r = XelpArgStr(buf, XelpStrLen(buf), 0, &s, &slen); + if (JB_ASSERT(r != XELP_S_OK || slen != 4, "ArgStr arg0 len")) + return XELP_E_ERR; + if (JB_ASSERT(XELP_S_OK != XelpStrEq(s, slen, "echo"), "ArgStr arg0 echo")) + return XELP_E_ERR; + } + + /* 9. XelpArgStr: past end */ + { + char buf[] = "help"; + r = XelpArgStr(buf, XelpStrLen(buf), 1, &s, &slen); + if (JB_ASSERT(r != XELP_E_ERR, "ArgStr past end")) + return XELP_E_ERR; + } + + /* 10. XelpArgStr: empty buffer */ + { + char buf[] = ""; + r = XelpArgStr(buf, 0, 0, &s, &slen); + if (JB_ASSERT(r != XELP_E_ERR, "ArgStr empty buf")) + return XELP_E_ERR; + } + + return XELP_S_OK; +} + /* ==================================================================== test_CursorWithEcho() Verify arrow keys, HOME/END, insert, delete, backspace with echo @@ -3858,69 +4162,721 @@ XELPRESULT test_EchoControl() { return XELP_S_OK; } -/* ************************************************ - Xelp Simple Unit Test suite. -*/ -FILE *logfile; -int flogout (char x) { - if (logfile) { - fputc(x,logfile); - fflush(logfile); +/* ==================================================================== + Command History tests -- guarded by both XELP_ENABLE_LINE_EDIT and + XELP_ENABLE_HISTORY so they compile out when history is disabled. + */ +#if defined(XELP_ENABLE_LINE_EDIT) && defined(XELP_ENABLE_HISTORY) + +/* ==================================================================== + test_HistoryBasic() -- ~8 cases covering fundamental history recall + */ +XELPRESULT test_HistoryBasic() { + char buf[64]; + + /* 1. Fresh init: UP does nothing, buffer stays empty */ + { + XELP x; + XelpInit(&x,"HB1"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XelpStrLen(buf) != 0, "Fresh UP does nothing")) + return XELP_E_ERR; } - return 0; -} -int putcharc (char x) { - return putchar(x); -} -int run_tests() { - JumpBug_InitGlobal("Xelp", putcharc,flogout); + /* 2. Type "hello" + ENTER, UP recalls "hello" */ + { + XELP x; + XelpInit(&x,"HB2"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); - JumpBug_RunUnit(test_XelpStrLen,"XelpStrLen"); - JumpBug_RunUnit(test_XelpStr2Int,"XelpStr2Int"); - JumpBug_RunUnit(test_XelpStrEq, "StrEq"); - JumpBug_RunUnit(test_XelpStrEq2, "StrEq2"); - JumpBug_RunUnit(test_XelpBufCmp,"XelpBufCmp"); - JumpBug_RunUnit(test_XelpFindTok,"XelpFindTok"); - JumpBug_RunUnit(test_XelpTokLineXB,"XelpTokLineXB"); + feedString(&x, "hello"); + XelpParseKey(&x, XELPKEY_ENTER); + feedKeycode(&x, XELP_KEYCODE_UP); - JumpBug_RunUnit(test_XelpTokN,"XelpTokN"); - JumpBug_RunUnit(test_XelpNumToks,"XelpNumToks"); - JumpBug_RunUnit(test_XelpInit,"XelpInit"); - JumpBug_RunUnit(test_XelpOut_comprehensive,"XelpOut"); - JumpBug_RunUnit(test_XelpExecKC,"XelpExecKC"); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "hello"), + "UP recalls hello")) + return XELP_E_ERR; + } - JumpBug_RunUnit(test_XelpParseKey,"XelpParseKey"); - JumpBug_RunUnit(test_XelpParse,"XelpParse"); - JumpBug_RunUnit(test_XelpParseXB,"XelpParseXB"); - JumpBug_RunUnit(test_XelpHelp,"XelpHelp"); - JumpBug_RunUnit(test_XelpParseNum,"XelpParseNum"); - JumpBug_RunUnit(test_XelpBufMacros,"XelpBufMacros"); - JumpBug_RunUnit(test_default_handlers,"DefaultHandlers"); - JumpBug_RunUnit(test_buffer_boundaries,"BufferBoundaries"); - JumpBug_RunUnit(test_stress_malformed,"StressMalformed"); - JumpBug_RunUnit(test_XelpRegisters,"XelpRegisters"); - JumpBug_RunUnit(test_KeyAccumulator,"KeyAccumulator"); - JumpBug_RunUnit(test_MultiByteKeyDispatch,"MultiByteKeyDispatch"); -#ifdef XELP_ENABLE_LINE_EDIT - JumpBug_RunUnit(test_CLILineEdit_Insert,"LineEditInsert"); - JumpBug_RunUnit(test_CLILineEdit_Delete,"LineEditDelete"); - JumpBug_RunUnit(test_CLILineEdit_HomeEnd,"LineEditHomeEnd"); - JumpBug_RunUnit(test_CLILineEdit_Backspace,"LineEditBackspace"); - JumpBug_RunUnit(test_CLIArrowsDrop,"CLIArrowsDrop"); - JumpBug_RunUnit(test_CLILineEdit_BufferFull,"LineEditBufferFull"); - JumpBug_RunUnit(test_CLILineEdit_Right,"LineEditRight"); -#endif - JumpBug_RunUnit(test_HelpMultiByteKeys,"HelpMultiByteKeys"); - JumpBug_RunUnit(test_AccumOverflow,"AccumOverflow"); - JumpBug_RunUnit(test_CLIMalformedKeys,"CLIMalformedKeys"); - JumpBug_RunUnit(test_MultiInstance,"MultiInstance"); - JumpBug_RunUnit(test_XelpArgs,"XelpArgs"); -#ifdef XELP_ENABLE_LINE_EDIT - JumpBug_RunUnit(test_CursorWithEcho,"CursorWithEcho"); -#endif + /* 3. "aaa" ENTER, "bbb" ENTER, UP=bbb, UP=aaa */ + { + XELP x; + XelpInit(&x,"HB3"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "bbb"); XelpParseKey(&x, XELPKEY_ENTER); + + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "bbb"), + "UP1 = bbb")) + return XELP_E_ERR; + + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "aaa"), + "UP2 = aaa")) + return XELP_E_ERR; + } + + /* 4. DOWN after UP: returns to more recent entry */ + { + XELP x; + XelpInit(&x,"HB4"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "bbb"); XelpParseKey(&x, XELPKEY_ENTER); + + feedKeycode(&x, XELP_KEYCODE_UP); /* bbb */ + feedKeycode(&x, XELP_KEYCODE_UP); /* aaa */ + feedKeycode(&x, XELP_KEYCODE_DOWN); /* bbb */ + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "bbb"), + "DOWN returns to bbb")) + return XELP_E_ERR; + } + + /* 5. DOWN past newest: restores empty line */ + { + XELP x; + XelpInit(&x,"HB5"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedKeycode(&x, XELP_KEYCODE_UP); /* aaa */ + feedKeycode(&x, XELP_KEYCODE_DOWN); /* past newest → empty */ + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XelpStrLen(buf) != 0, "DOWN past newest = empty")) + return XELP_E_ERR; + } + + /* 6. UP past oldest: stays on oldest, no crash */ + { + XELP x; + XelpInit(&x,"HB6"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "bbb"); XelpParseKey(&x, XELPKEY_ENTER); + + feedKeycode(&x, XELP_KEYCODE_UP); /* bbb */ + feedKeycode(&x, XELP_KEYCODE_UP); /* aaa */ + feedKeycode(&x, XELP_KEYCODE_UP); /* clamped at aaa */ + feedKeycode(&x, XELP_KEYCODE_UP); /* still aaa */ + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "aaa"), + "UP past oldest stays on aaa")) + return XELP_E_ERR; + } + + /* 7. ENTER on recalled line dispatches it */ + { + XELP x; + XelpInit(&x,"HB7"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + gGlobalCallbackData.c1 = 0; + feedString(&x, "foo"); XelpParseKey(&x, XELPKEY_ENTER); + if (JB_ASSERT(gGlobalCallbackData.c1 != 1, "foo executed first time")) + return XELP_E_ERR; + + /* reset and recall */ + gGlobalCallbackData.c1 = 0; + feedKeycode(&x, XELP_KEYCODE_UP); + XelpParseKey(&x, XELPKEY_ENTER); + if (JB_ASSERT(gGlobalCallbackData.c1 != 1, "Recalled foo executes")) + return XELP_E_ERR; + } + + /* 8. Empty ENTER does NOT store in history */ + { + XELP x; + XelpInit(&x,"HB8"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + XelpParseKey(&x, XELPKEY_ENTER); /* empty */ + XelpParseKey(&x, XELPKEY_ENTER); /* empty */ + feedKeycode(&x, XELP_KEYCODE_UP); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XelpStrLen(buf) != 0, "Empty ENTER not stored")) + return XELP_E_ERR; + } + + return XELP_S_OK; +} + +/* ==================================================================== + test_HistoryInProgressSave() -- ~4 cases for in-progress line stashing + */ +XELPRESULT test_HistoryInProgressSave() { + char buf[64]; + + /* 1. Type "partial", UP, DOWN: "partial" restored */ + { + XELP x; + XelpInit(&x,"HIP1"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "partial"); + feedKeycode(&x, XELP_KEYCODE_UP); /* saves "partial", shows "aaa" */ + feedKeycode(&x, XELP_KEYCODE_DOWN); /* restores "partial" */ + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "partial"), + "In-progress restored after UP DOWN")) + return XELP_E_ERR; + } + + /* 2. Type "partial", UP, UP, DOWN, DOWN: "partial" restored exactly */ + { + XELP x; + XelpInit(&x,"HIP2"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "bbb"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "partial"); + + feedKeycode(&x, XELP_KEYCODE_UP); /* bbb */ + feedKeycode(&x, XELP_KEYCODE_UP); /* aaa */ + feedKeycode(&x, XELP_KEYCODE_DOWN); /* bbb */ + feedKeycode(&x, XELP_KEYCODE_DOWN); /* partial */ + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "partial"), + "In-progress restored multi bounce")) + return XELP_E_ERR; + } + + /* 3. Type "partial", UP, type over, ENTER: new text executes + saves */ + { + XELP x; + XelpInit(&x,"HIP3"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "partial"); + feedKeycode(&x, XELP_KEYCODE_UP); /* "aaa" recalled */ + + /* clear and type "bar" */ + feedKeycode(&x, XELP_KEYCODE_HOME); + { + int j; + for (j = 0; j < 3; j++) feedKeycode(&x, XELP_KEYCODE_KDEL); + } + gGlobalCallbackData.c2 = -1; + feedString(&x, "bar"); + XelpParseKey(&x, XELPKEY_ENTER); + if (JB_ASSERT(gGlobalCallbackData.c2 != 2, "Overtyped recalled cmd executes bar")) + return XELP_E_ERR; + + /* "bar" should be in history now */ + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "bar"), + "Overtyped cmd saved in history")) + return XELP_E_ERR; + } + + /* 4. Type "partial", UP, ENTER: partial is lost */ + { + XELP x; + XelpInit(&x,"HIP4"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "partial"); + feedKeycode(&x, XELP_KEYCODE_UP); /* "aaa" recalled */ + XelpParseKey(&x, XELPKEY_ENTER); /* executes "aaa" */ + + /* history should now have "aaa" at top (re-executed), not "partial" */ + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "aaa"), + "Partial lost after recall+enter")) + return XELP_E_ERR; + } + + return XELP_S_OK; +} + +/* ==================================================================== + test_HistoryFull() -- ~4 cases for ring buffer capacity/eviction + */ +XELPRESULT test_HistoryFull() { + char buf[64]; + + /* 1. Fill to capacity, verify all recallable */ + { + XELP x; + int i; + char cmd[8]; + + XelpInit(&x,"HF1"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + for (i = 0; i < XELP_HIST_DEPTH; i++) { + cmd[0] = 'a' + (char)i; cmd[1] = 0; + feedString(&x, cmd); + XelpParseKey(&x, XELPKEY_ENTER); + } + /* UP should recall in reverse: last entered first */ + for (i = XELP_HIST_DEPTH - 1; i >= 0; i--) { + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + cmd[0] = 'a' + (char)i; cmd[1] = 0; + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), cmd), + "Full ring recall")) + return XELP_E_ERR; + } + } + + /* 2. Overfill: oldest evicted, newest stored */ + { + XELP x; + int i; + char cmd[8]; + + XelpInit(&x,"HF2"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + /* store DEPTH+1 entries: "a","b","c","d","e" (for DEPTH=4) */ + for (i = 0; i <= XELP_HIST_DEPTH; i++) { + cmd[0] = 'a' + (char)i; cmd[1] = 0; + feedString(&x, cmd); + XelpParseKey(&x, XELPKEY_ENTER); + } + /* "a" should be evicted; first UP gives last entered */ + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + cmd[0] = 'a' + (char)XELP_HIST_DEPTH; cmd[1] = 0; + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), cmd), + "Overfill newest at top")) + return XELP_E_ERR; + + /* go to oldest -- should NOT be "a" */ + for (i = 1; i < XELP_HIST_DEPTH; i++) + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK == XelpStrEq(buf, XelpStrLen(buf), "a"), + "Overfill oldest evicted")) + return XELP_E_ERR; + } + + /* 3. Fill + evict several times, verify ring integrity */ + { + XELP x; + int i; + char cmd[8]; + + XelpInit(&x,"HF3"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + /* push 3 * DEPTH entries */ + for (i = 0; i < 3 * XELP_HIST_DEPTH; i++) { + cmd[0] = 'A' + (char)(i % 26); cmd[1] = 0; + feedString(&x, cmd); + XelpParseKey(&x, XELPKEY_ENTER); + } + /* last DEPTH entries should be recallable */ + for (i = 3 * XELP_HIST_DEPTH - 1; i >= 3 * XELP_HIST_DEPTH - XELP_HIST_DEPTH; i--) { + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + cmd[0] = 'A' + (char)(i % 26); cmd[1] = 0; + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), cmd), + "Ring integrity after many evictions")) + return XELP_E_ERR; + } + } + + /* 4. Very long command near XELP_CMDBUFSZ: stored and recalled */ + { + XELP x; + int i, len; + + XelpInit(&x,"HF4"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + len = XELP_CMDBUFSZ - 2; /* max usable (CMDBUFSZ-1 is buf limit, -1 for safety) */ + for (i = 0; i < len; i++) + XelpParseKey(&x, 'Z'); + XelpParseKey(&x, XELPKEY_ENTER); + + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XelpStrLen(buf) != len, "Long cmd recalled correct len")) + return XELP_E_ERR; + if (JB_ASSERT(buf[0] != 'Z' || buf[len-1] != 'Z', "Long cmd content correct")) + return XELP_E_ERR; + } + + return XELP_S_OK; +} + +/* ==================================================================== + test_HistoryWithEditing() -- ~5 cases for cursor editing of recalled cmds + */ +XELPRESULT test_HistoryWithEditing() { + char buf[64]; + + /* 1. Recall with UP, LEFT/RIGHT works on recalled text */ + { + XELP x; + XelpInit(&x,"HE1"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "abcde"); XelpParseKey(&x, XELPKEY_ENTER); + feedKeycode(&x, XELP_KEYCODE_UP); + feedKeycode(&x, XELP_KEYCODE_LEFT); + feedKeycode(&x, XELP_KEYCODE_LEFT); + XelpParseKey(&x, 'X'); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "abcXde"), + "Edit recalled: insert X")) + return XELP_E_ERR; + } + + /* 2. Recall, HOME, type prefix, ENTER: modified version executes */ + { + XELP x; + XelpInit(&x,"HE2"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "oo"); XelpParseKey(&x, XELPKEY_ENTER); + feedKeycode(&x, XELP_KEYCODE_UP); + feedKeycode(&x, XELP_KEYCODE_HOME); + XelpParseKey(&x, 'f'); + + gGlobalCallbackData.c1 = 0; + XelpParseKey(&x, XELPKEY_ENTER); + if (JB_ASSERT(gGlobalCallbackData.c1 != 1, "Modified recalled cmd foo executes")) + return XELP_E_ERR; + } + + /* 3. Recall, KDEL at cursor */ + { + XELP x; + XelpInit(&x,"HE3"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "abcd"); XelpParseKey(&x, XELPKEY_ENTER); + feedKeycode(&x, XELP_KEYCODE_UP); + feedKeycode(&x, XELP_KEYCODE_HOME); + feedKeycode(&x, XELP_KEYCODE_KDEL); /* delete 'a' */ + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "bcd"), + "Recalled KDEL deletes char")) + return XELP_E_ERR; + } + + /* 4. Recall, backspace */ + { + XELP x; + XelpInit(&x,"HE4"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "abcd"); XelpParseKey(&x, XELPKEY_ENTER); + feedKeycode(&x, XELP_KEYCODE_UP); + XelpParseKey(&x, XELPKEY_BKSP); /* delete last char 'd' */ + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "abc"), + "Recalled bksp works")) + return XELP_E_ERR; + } + + /* 5. Recall, HOME, END: cursor at correct positions */ + { + XELP x; + XelpInit(&x,"HE5"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "hello"); XelpParseKey(&x, XELPKEY_ENTER); + feedKeycode(&x, XELP_KEYCODE_UP); + feedKeycode(&x, XELP_KEYCODE_HOME); + XelpParseKey(&x, 'A'); /* insert at start */ + feedKeycode(&x, XELP_KEYCODE_END); + XelpParseKey(&x, 'Z'); /* append at end */ + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "AhelloZ"), + "HOME/END on recalled line")) + return XELP_E_ERR; + } + + return XELP_S_OK; +} + +/* ==================================================================== + test_HistoryDuplicates() -- ~3 cases for consecutive duplicate suppression + */ +XELPRESULT test_HistoryDuplicates() { + char buf[64]; + + /* 1. "aaa" three times: only one entry (skip consecutive dups) */ + { + XELP x; + XelpInit(&x,"HD1"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + + feedKeycode(&x, XELP_KEYCODE_UP); /* aaa */ + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "aaa"), + "Dup: first UP = aaa")) + return XELP_E_ERR; + + feedKeycode(&x, XELP_KEYCODE_UP); /* should still be aaa (no more entries) */ + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "aaa"), + "Dup: second UP still aaa (only 1 entry)")) + return XELP_E_ERR; + } + + /* 2. "aaa", "bbb", "aaa": all three stored (non-consecutive) */ + { + XELP x; + XelpInit(&x,"HD2"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "bbb"); XelpParseKey(&x, XELPKEY_ENTER); + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + + feedKeycode(&x, XELP_KEYCODE_UP); /* aaa (newest) */ + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "aaa"), + "Non-consec: UP1 = aaa")) + return XELP_E_ERR; + + feedKeycode(&x, XELP_KEYCODE_UP); /* bbb */ + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "bbb"), + "Non-consec: UP2 = bbb")) + return XELP_E_ERR; + + feedKeycode(&x, XELP_KEYCODE_UP); /* aaa (oldest) */ + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "aaa"), + "Non-consec: UP3 = aaa")) + return XELP_E_ERR; + } + + /* 3. Empty string never stored regardless */ + { + XELP x; + XelpInit(&x,"HD3"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,dummyOut); + + feedString(&x, "aaa"); XelpParseKey(&x, XELPKEY_ENTER); + XelpParseKey(&x, XELPKEY_ENTER); /* empty */ + XelpParseKey(&x, XELPKEY_ENTER); /* empty */ + + feedKeycode(&x, XELP_KEYCODE_UP); + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "aaa"), + "Empty not stored, UP = aaa")) + return XELP_E_ERR; + } + + return XELP_S_OK; +} + +/* ==================================================================== + test_HistoryAndEcho() -- ~3 cases for history interaction with echo/output + */ +XELPRESULT test_HistoryAndEcho() { + char buf[64]; + + /* 1. Echo mask '*': recall works, output shows masked chars */ + { + XELP x; + XelpInit(&x,"HEC1"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,gDummyBufOut); + XELP_SET_ECHO(x, '*'); + + feedString(&x, "secret"); + XelpParseKey(&x, XELPKEY_ENTER); + + resetDummyBuf(); + feedKeycode(&x, XELP_KEYCODE_UP); + gDummyBufOut(0); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "secret"), + "Echo mask: buffer has real text")) + return XELP_E_ERR; + /* output should contain '*' chars, not real text */ + { + int i, stars = 0; + for (i = 0; i < XelpStrLen(gDummyBuf); i++) + if (gDummyBuf[i] == '*') stars++; + if (JB_ASSERT(stars < 6, "Echo mask: output has stars")) + return XELP_E_ERR; + } + } + + /* 2. Output disabled: history still saves and recalls */ + { + XELP x; + XelpInit(&x,"HEC2"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,gDummyBufOut); + XELP_SET_OUT_ENABLE(x, 0); + + feedString(&x, "silent"); + XelpParseKey(&x, XELPKEY_ENTER); + feedKeycode(&x, XELP_KEYCODE_UP); + + getCmdBuf(&x, buf, sizeof(buf)); + if (JB_ASSERT(XELP_S_OK != XelpStrEq(buf, XelpStrLen(buf), "silent"), + "Output disabled: recall works")) + return XELP_E_ERR; + + XELP_SET_OUT_ENABLE(x, 1); + } + + /* 3. After recall with echo off, re-enable, type: echo resumes */ + { + XELP x; + XelpInit(&x,"HEC3"); + XELP_SET_FN_CLI(x,gMyCLICommands); + XELP_SET_FN_OUT(x,gDummyBufOut); + + feedString(&x, "cmd1"); + XelpParseKey(&x, XELPKEY_ENTER); + + XELP_SET_ECHO(x, XELP_ECHO_OFF); + feedKeycode(&x, XELP_KEYCODE_UP); + XelpParseKey(&x, XELPKEY_ENTER); + + XELP_SET_ECHO(x, XELP_ECHO_NORMAL); + resetDummyBuf(); + XelpParseKey(&x, 'Q'); + gDummyBufOut(0); + if (JB_ASSERT(gDummyBuf[0] != 'Q', "Echo resumes after recall")) + return XELP_E_ERR; + } + + return XELP_S_OK; +} + +#endif /* XELP_ENABLE_LINE_EDIT && XELP_ENABLE_HISTORY */ + +/* ************************************************ + Xelp Simple Unit Test suite. +*/ +FILE *logfile; +int flogout (char x) { + if (logfile) { + fputc(x,logfile); + fflush(logfile); + } + return 0; +} +int putcharc (char x) { + return putchar(x); +} +int run_tests() { + + JumpBug_InitGlobal("Xelp", putcharc,flogout); + + JumpBug_RunUnit(test_XelpStrLen,"XelpStrLen"); + JumpBug_RunUnit(test_XelpStr2Int,"XelpStr2Int"); + JumpBug_RunUnit(test_XelpStrEq, "StrEq"); + JumpBug_RunUnit(test_XelpStrEq2, "StrEq2"); + JumpBug_RunUnit(test_XelpBufCmp,"XelpBufCmp"); + JumpBug_RunUnit(test_XelpFindTok,"XelpFindTok"); + JumpBug_RunUnit(test_XelpTokLineXB,"XelpTokLineXB"); + + JumpBug_RunUnit(test_XelpTokN,"XelpTokN"); + JumpBug_RunUnit(test_XelpNumToks,"XelpNumToks"); + JumpBug_RunUnit(test_XelpInit,"XelpInit"); + JumpBug_RunUnit(test_XelpOut_comprehensive,"XelpOut"); + JumpBug_RunUnit(test_XelpExecKC,"XelpExecKC"); + + JumpBug_RunUnit(test_XelpParseKey,"XelpParseKey"); + JumpBug_RunUnit(test_XelpParse,"XelpParse"); + JumpBug_RunUnit(test_XelpParseXB,"XelpParseXB"); + JumpBug_RunUnit(test_XelpHelp,"XelpHelp"); + JumpBug_RunUnit(test_XelpParseNum,"XelpParseNum"); + JumpBug_RunUnit(test_XelpBufMacros,"XelpBufMacros"); + JumpBug_RunUnit(test_default_handlers,"DefaultHandlers"); + JumpBug_RunUnit(test_buffer_boundaries,"BufferBoundaries"); + JumpBug_RunUnit(test_stress_malformed,"StressMalformed"); + JumpBug_RunUnit(test_XelpRegisters,"XelpRegisters"); + JumpBug_RunUnit(test_KeyAccumulator,"KeyAccumulator"); + JumpBug_RunUnit(test_MultiByteKeyDispatch,"MultiByteKeyDispatch"); +#ifdef XELP_ENABLE_LINE_EDIT + JumpBug_RunUnit(test_CLILineEdit_Insert,"LineEditInsert"); + JumpBug_RunUnit(test_CLILineEdit_Delete,"LineEditDelete"); + JumpBug_RunUnit(test_CLILineEdit_HomeEnd,"LineEditHomeEnd"); + JumpBug_RunUnit(test_CLILineEdit_Backspace,"LineEditBackspace"); + JumpBug_RunUnit(test_CLIBackspaceBS,"BackspaceBS"); + JumpBug_RunUnit(test_CLIArrowsDrop,"CLIArrowsDrop"); + JumpBug_RunUnit(test_CLILineEdit_BufferFull,"LineEditBufferFull"); + JumpBug_RunUnit(test_CLILineEdit_Right,"LineEditRight"); +#endif + JumpBug_RunUnit(test_HelpMultiByteKeys,"HelpMultiByteKeys"); + JumpBug_RunUnit(test_AccumOverflow,"AccumOverflow"); + JumpBug_RunUnit(test_CLIMalformedKeys,"CLIMalformedKeys"); + JumpBug_RunUnit(test_MultiInstance,"MultiInstance"); + JumpBug_RunUnit(test_XelpArgs,"XelpArgs"); + JumpBug_RunUnit(test_XelpArgIntStr,"XelpArgIntStr"); +#ifdef XELP_ENABLE_LINE_EDIT + JumpBug_RunUnit(test_CursorWithEcho,"CursorWithEcho"); +#endif JumpBug_RunUnit(test_OutputEnable,"OutputEnable"); JumpBug_RunUnit(test_EchoControl,"EchoControl"); +#if defined(XELP_ENABLE_LINE_EDIT) && defined(XELP_ENABLE_HISTORY) + JumpBug_RunUnit(test_HistoryBasic,"HistoryBasic"); + JumpBug_RunUnit(test_HistoryInProgressSave,"HistInProgress"); + JumpBug_RunUnit(test_HistoryFull,"HistoryFull"); + JumpBug_RunUnit(test_HistoryWithEditing,"HistoryEditing"); + JumpBug_RunUnit(test_HistoryDuplicates,"HistoryDups"); + JumpBug_RunUnit(test_HistoryAndEcho,"HistoryEcho"); +#endif JumpBug_PrintResults(); From e1a367a3d522d2b03f197c8be8da8f34ad87ce2a Mon Sep 17 00:00:00 2001 From: deftio Date: Mon, 27 Apr 2026 00:31:28 -0700 Subject: [PATCH 2/3] sync manifests, badges, and sizes for 0.3.2 --- README.md | 34 +++++++++++++++++----------------- pages/index.html | 34 +++++++++++++++++----------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 8d3ef67..3fdb9f1 100644 --- a/README.md +++ b/README.md @@ -248,25 +248,25 @@ features). Even the largest full build is under 7 KB. | CPU | Width | Compiler | KEY (bytes) | CLI (bytes) | FULL (bytes) | |-----|------:|----------|------------:|------------:|-------------:| -| AVR (ATtiny85) | 8 | avr-gcc | 1046 | 4266 | 4324 | -| AVR (ATmega328P) | 8 | avr-gcc | 1054 | 4366 | 4424 | -| Z80 | 8 | SDCC | 2121 | 7280 | 7388 | -| 6800 (HC08) | 8 | SDCC | 2471 | 8614 | 8715 | -| MSP430 | 16 | msp430-gcc | 770 | 3482 | 3528 | -| 68HC11 | 16 | m68hc11-gcc | 2369 | 6884 | 6955 | -| Xtensa LX7 (ESP32-S3) | 32 | xtensa-esp-elf-gcc | 576 | 2592 | 2624 | +| AVR (ATtiny85) | 8 | avr-gcc | 1046 | 4270 | 4328 | +| AVR (ATmega328P) | 8 | avr-gcc | 1054 | 4370 | 4428 | +| Z80 | 8 | SDCC | 2121 | 7287 | 7395 | +| 6800 (HC08) | 8 | SDCC | 2471 | 8616 | 8718 | +| MSP430 | 16 | msp430-gcc | 770 | 3486 | 3532 | +| 68HC11 | 16 | m68hc11-gcc | 2369 | 6895 | 6966 | +| Xtensa LX7 (ESP32-S3) | 32 | xtensa-esp-elf-gcc | 576 | 2600 | 2632 | | ARM Thumb | 32 | arm-none-eabi-gcc | 580 | 2598 | 2642 | -| RISC-V (rv32) | 32 | riscv64-unknown-elf-gcc | 722 | 3094 | 3132 | -| Xtensa LX106 (ESP8266) | 32 | xtensa-lx106-elf-gcc | 723 | 2955 | 2987 | -| m68k | 32 | m68k-linux-gnu-gcc | 728 | 3332 | 3380 | -| ARM32 | 32 | arm-none-eabi-gcc | 980 | 3930 | 3990 | -| x86-32 | 32 | GCC | 1081 | 4916 | 4966 | +| RISC-V (rv32) | 32 | riscv64-unknown-elf-gcc | 722 | 3100 | 3138 | +| Xtensa LX106 (ESP8266) | 32 | xtensa-lx106-elf-gcc | 723 | 2947 | 2979 | +| m68k | 32 | m68k-linux-gnu-gcc | 728 | 3336 | 3384 | +| ARM32 | 32 | arm-none-eabi-gcc | 980 | 3934 | 3994 | +| x86-32 | 32 | GCC | 1081 | 4919 | 4969 | | MIPS32 | 32 | mipsel-linux-gnu-gcc | 1296 | 5224 | 5272 | -| PowerPC | 32 | powerpc-linux-gnu-gcc | 1504 | 6058 | 6122 | -| RISC-V (rv64) | 64 | riscv64-linux-gnu-gcc | 756 | 3548 | 3582 | -| x86-64 | 64 | Clang | 1043 | 5268 | 5310 | -| x86-64 | 64 | GCC | 1063 | 5136 | 5185 | -| AArch64 (ARM64) | 64 | aarch64-linux-gnu-gcc | 1324 | 5566 | 5622 | +| PowerPC | 32 | powerpc-linux-gnu-gcc | 1504 | 6066 | 6130 | +| RISC-V (rv64) | 64 | riscv64-linux-gnu-gcc | 756 | 3554 | 3588 | +| x86-64 | 64 | Clang | 1043 | 5269 | 5311 | +| x86-64 | 64 | GCC | 1063 | 5138 | 5187 | +| AArch64 (ARM64) | 64 | aarch64-linux-gnu-gcc | 1324 | 5574 | 5630 | | MIPS64 | 64 | mips64el-linux-gnuabi64-gcc | 1360 | 5864 | 5928 | diff --git a/pages/index.html b/pages/index.html index 1d96c20..5f4c168 100644 --- a/pages/index.html +++ b/pages/index.html @@ -171,25 +171,25 @@

      Compiled sizes

      - - - - - - - + + + + + + + - - - - - + + + + + - - - - - + + + + +
      CPUWidthCompilerKEY (bytes)CLI (bytes)FULL (bytes)
      AVR (ATtiny85)8avr-gcc104642664324
      AVR (ATmega328P)8avr-gcc105443664424
      Z808SDCC212172807388
      6800 (HC08)8SDCC247186148715
      MSP43016msp430-gcc77034823528
      68HC1116m68hc11-gcc236968846955
      Xtensa LX7 (ESP32-S3)32xtensa-esp-elf-gcc57625922624
      AVR (ATtiny85)8avr-gcc104642704328
      AVR (ATmega328P)8avr-gcc105443704428
      Z808SDCC212172877395
      6800 (HC08)8SDCC247186168718
      MSP43016msp430-gcc77034863532
      68HC1116m68hc11-gcc236968956966
      Xtensa LX7 (ESP32-S3)32xtensa-esp-elf-gcc57626002632
      ARM Thumb32arm-none-eabi-gcc58025982642
      RISC-V (rv32)32riscv64-unknown-elf-gcc72230943132
      Xtensa LX106 (ESP8266)32xtensa-lx106-elf-gcc72329552987
      m68k32m68k-linux-gnu-gcc72833323380
      ARM3232arm-none-eabi-gcc98039303990
      x86-3232GCC108149164966
      RISC-V (rv32)32riscv64-unknown-elf-gcc72231003138
      Xtensa LX106 (ESP8266)32xtensa-lx106-elf-gcc72329472979
      m68k32m68k-linux-gnu-gcc72833363384
      ARM3232arm-none-eabi-gcc98039343994
      x86-3232GCC108149194969
      MIPS3232mipsel-linux-gnu-gcc129652245272
      PowerPC32powerpc-linux-gnu-gcc150460586122
      RISC-V (rv64)64riscv64-linux-gnu-gcc75635483582
      x86-6464Clang104352685310
      x86-6464GCC106351365185
      AArch64 (ARM64)64aarch64-linux-gnu-gcc132455665622
      PowerPC32powerpc-linux-gnu-gcc150460666130
      RISC-V (rv64)64riscv64-linux-gnu-gcc75635543588
      x86-6464Clang104352695311
      x86-6464GCC106351385187
      AArch64 (ARM64)64aarch64-linux-gnu-gcc132455745630
      MIPS6464mips64el-linux-gnuabi64-gcc136058645928
      From e38b326a9721d37e686c149204630f811df27c03 Mon Sep 17 00:00:00 2001 From: deftio Date: Mon, 27 Apr 2026 00:33:55 -0700 Subject: [PATCH 3/3] updated library.properties --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 1a24ace..dbdfb23 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=xelp -version=0.3.2 +version=0.3.2 author=M. A. Chatterjee maintainer=M. A. Chatterjee sentence=Tiny CLI and script interpreter for embedded systems. 2KB, zero malloc, multi-instance.