embassy-shell 0.2.0

no_std async interactive shell for embassy: bash-like line editing, tab completion and history over any embedded-io-async transport (UART, USB CDC)
Documentation
# embassy-shell

[![crates.io](https://img.shields.io/crates/v/embassy-shell.svg)](https://crates.io/crates/embassy-shell)
[![docs.rs](https://img.shields.io/docsrs/embassy-shell/latest)](https://docs.rs/embassy-shell)
[![CI](https://github.com/alexxy/embassy-shell/actions/workflows/ci.yml/badge.svg)](https://github.com/alexxy/embassy-shell/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE-MIT)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE-APACHE)


A small `no_std` interactive shell in the spirit of bash, built for
[embassy](https://embassy.dev) (any async executor works) and any
transport implementing [`embedded-io-async`](https://docs.rs/embedded-io-async)
— UART, USB CDC, TCP sockets, …

Think [`ushell`](https://github.com/dotcypress/ushell), but alive, native
`async`/embassy, with a simpler command-registration API.

## Features

* **Bash-like line editing** — cursor keys, `Home`/`End`, `Delete`,
  `Backspace`, `Ctrl-U` (kill line), `Ctrl-W` (delete word), `Ctrl-L`
  (clear screen), UTF-8 input (can be disabled to save flash, see
  [Cargo features]#cargo-features).
* **Tab completion** — completes command names; per-command argument
  completion from fixed option lists or custom callbacks. Inserts the common
  prefix and lists candidates when ambiguous, like bash.
* **History** — `Up`/`Down` navigation, configurable number of entries
  (`max_history`) and per-entry length (`max_history_len`), `history`
  built-in.
* **Ctrl-C interrupts a running command** — the handler future is dropped
  (handlers should be cancel-safe).
* **Simple command registration** — closures returning a boxed future;
  commands get an `Io` output handle so they can `await` slow writes.
* **Built-ins** `help`, `history`, `clear` (can be shadowed by user
  commands).
* Only `embedded-io` + `embedded-io-async` as dependencies. Uses `alloc`
  (a global allocator is required). Not `Send`-bound, which matches
  embassy's per-core executor model.

## Cargo features

| feature | default | description |
|---------|---------|-------------|
| `unicode` | yes | Decode multi-byte UTF-8 sequences typed at the prompt. Turning it off removes the UTF-8 reader from the input path (~1 KB of flash on Cortex-M); bytes `>= 0x80` are then treated as individual Latin-1 characters, so pasted UTF-8 will be garbled on display. Command handling itself is byte-oriented and unaffected. |
| `defmt` | no | Emit trace-level logs (received keys, dispatched commands, completion counts) via [`defmt`]https://docs.rs/defmt. Zero cost when disabled. |

```toml
# Lean configuration: no UTF-8 decoding, no logging.
embassy-shell = { version = "0.2", default-features = false }
```

## Quick start

```rust
use embassy_shell::Shell;

let mut shell = Shell::new();

// A command with free-form arguments.
shell.add_command("echo", "print arguments", |args, mut io| {
    Box::pin(async move { io.println(&args.rest()).await })
});

// A command with a fixed set of first-argument options (`led on|off|blink`).
shell.add_command_with_options("led", "control the led", &["on", "off", "blink"],
    |args, mut io| Box::pin(async move {
        match args.get(0) {
            Some("on") => io.println("led on").await,
            Some("off") => io.println("led off").await,
            _ => io.println("usage: led on|off|blink").await,
        }
    }),
);

// Run over anything that is embedded-io-async Read + Write:
// embassy UART handles, embassy-usb CDC `SerialPort`, ...
shell.run(&mut reader, &mut writer).await?;
```

### With embassy (UART)

```rust
#[embassy_executor::task]
async fn shell_task(mut uart: Uart<'static, PERIPHERALS>) {
    let mut shell = Shell::new();
    // ... add_command(...) ...
    shell.run(&mut uart, &mut uart).await.ok();
}
```

The same works with embassy-usb CDC-ACM: pass the `SerialPort`'s reader and
writer halves.

## Command handlers

A handler is `Fn(Args, Io) -> BoxFuture<'_, Result<()>>`. Wrap an
`async move` block in `Box::pin` (this type-erases per-command future types
so all commands live in one table):

```rust
shell.add_command("wait", "wait n ms, printing dots", |args, mut io| {
    Box::pin(async move {
        for _ in 0..args.get(0).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0) {
            io.print(".").await?;
            Timer::after_millis(100).await;   // any await is fine
        }
        io.println(" done").await
    })
});
```

`Args` gives `get(i)`, `iter()`, `len()` and `rest()` (the arguments joined
with spaces); quotes are handled bash-style (`led "on blink"` → one token).
`Io` provides `print`, `println`, `write_all`, `flush` and implements
`embedded_io_async::Write`, so you can hand it to other libraries.

Custom argument completion:

```rust
shell.add_command_with_completer(
    "set", "set a config key",
    |args, io| Box::pin(async move { /* ... */ io.println("ok").await }),
    |index, prefix| match index {
        1 => KEYS.iter().filter(|k| k.starts_with(prefix)).map(|k| k.to_string()).collect(),
        _ => vec![],
    },
);
```

## Terminal keys

| Key        | Action                                   |
|------------|------------------------------------------|
| `Enter`    | run line                                  |
| `Tab`      | complete command / argument               |
| `Up/Down`  | history                                   |
| `←/→`      | move cursor (ins-line editing supported)  |
| `Home/End` | jump to start / end                       |
| `Backspace` / `Delete` | erase around cursor           |
| `Ctrl-C`   | clear line / **interrupt running command**|
| `Ctrl-U`   | kill line                                 |
| `Ctrl-W`   | delete previous word                      |
| `Ctrl-L`   | clear screen                              |

## Examples

Eight ready-to-flash projects live in [`examples/`](examples/). Each one is a
standalone crate (own `Cargo.toml`, `.cargo/config.toml` and runner), so
`cd` into it and use `cargo check` / `cargo run`.

Every example runs a shell with `led on|off|toggle|blink [period_ms]`,
`pwm <0-255>|off` (the ESP32-C6 examples drive a WS2812 strip instead, via
`rgb <r> <g> <b>|<color>|off`) and `status` (plus `reboot` on the STM32 ones).

| Example | Transport | Console |
|---|---|---|
| [`stm32f411-uart`]examples/stm32f411-uart | USART1 + DMA | serial, 115200 8N1 |
| [`stm32f411-usb`]examples/stm32f411-usb | USB CDC-ACM | any COM terminal |
| [`nanoch32v305-uart`]examples/nanoch32v305-uart | USART1 + DMA | serial, 115200 8N1 |
| [`nanoch32v305-usb`]examples/nanoch32v305-usb | USB CDC-ACM | any COM terminal |
| [`esp32c6-uart`]examples/esp32c6-uart | UART0 | serial, 115200 8N1 |
| [`esp32c6-usb`]examples/esp32c6-usb | USB Serial/JTAG | any COM terminal |
| [`esp32c3-uart`]examples/esp32c3-uart | UART0 | serial, 115200 8N1 |
| [`esp32c3-usb`]examples/esp32c3-usb | USB Serial/JTAG | any COM terminal |

All examples log via **defmt** and are flashed with **probe-rs**: `cargo run`
builds, flashes and streams the logs (`DEFMT_LOG=info` is preset in each
`.cargo/config.toml`; override with e.g. `DEFMT_LOG=debug cargo run`).

### STM32F411 Blackpill (STM32F411CE)

* Toolchain: stable, `thumbv7em-none-eabihf`. Flash: `cargo run` (runner is
  `probe-rs run --chip STM32F411CE`), defmt over SWD RTT.
* Console (uart): `PA9` = TX, `PA10` = RX, 115200 8N1.
* USB (usb): built-in OTG FS on the `USB` connector, `PA11` = DM, `PA12` = DP.
  Uses the 25 MHz HSE crystal → 96 MHz core, PLL1_Q = 48 MHz for USB.
* LED: `PC13` (active low). The LED is also the PWM output: `PC13` has no
  hardware timer channel on the F411, so the PWM is generated in software
  with the [`spwm`]https://crates.io/crates/spwm crate — a 100 kHz TIM3
  interrupt drives a 1 kHz software PWM (100 physical ticks per period), and
  error-feedback dithering maps the whole `0..255` brightness range onto
  10 µs steps. embassy-time runs on TIM4.

### NanoCH32V305 (CH32V305RBT6)

* Toolchain: **nightly** with `rust-src` (custom `riscv32imfc-unknown-none-elf`
  JSON target + `-Zbuild-std`). Flash: `cargo run` (runner is
  `probe-rs run --chip CH32V305RBT6 --connect-under-reset`), defmt over SWD
  RTT.
* Depends on [`ch32-hal`]https://github.com/ch32-rs/ch32-hal pinned to a
  git revision.
* Console (uart): `PA9` = TX, `PA10` = RX, 115200 8N1.
* USB (usb): OTG FS, `PA11` = DM, `PA12` = DP, core clock 144 MHz from HSI
  (required for the 48 MHz USB clock). These pins are shared with the ISP
  bootloader: if the device never enumerates, make sure the board is not
  being held in the bootloader.
* LED: `PA3` (active low). The LED is also the PWM output: `PA3` = TIM2_CH4
  (no remap), hardware PWM at 1 kHz with active-low polarity, so the user
  brightness `0..255` maps linearly onto the timer duty.

### ESP32-C6 / ESP32-C3

* Toolchain: stable, built-in target `riscv32imac-unknown-none-elf` (C6) /
  `riscv32imc-unknown-none-elf` (C3); esp-hal 1.1 + `esp-rtos` (embassy
  executor). Flash: `cargo run` (runner is `probe-rs run --chip esp32c6` /
  `--chip esp32c3`). The C3 examples share the structure of the C6 ones
  (transport, shell, defmt setup) but drive a plain LED + LEDC PWM instead of
  a WS2812 strip, and differ in chip feature, target and UART0 pins.
* Console (uart): UART0; `GPIO16` = TX, `GPIO17` = RX (C6) or `GPIO21` = TX,
  `GPIO20` = RX (C3), 115200 8N1.
* USB (usb): neither the C6 nor the C3 has **USB-OTG**; these examples use
  the built-in **USB Serial/JTAG** peripheral (the same controller as the
  board's flash port), which enumerates as a CDC device. Because debug and
  USB Serial/JTAG share the connector, the runner adds
  `--connect-under-reset`.
* defmt: on by default in the uart example (RTT over SWD); **off** by default
  in the usb example to avoid contending for the USB Serial/JTAG console —
  enable with `cargo build --features defmt`.
* LED (C6 examples): WS2812 RGB LED on `GPIO8` (`D8` of the ESP32-C6
  Supermini), driven by the RMT peripheral (GRB frames on an 80 MHz tick,
  idle-high between frames). Commands: `rgb <r> <g> <b>` (each 0–255), named
  presets (`red`, `green`, `blue`, `yellow`, `magenta`, `cyan`, `white`) and
  `rgb off`; `led on|off|toggle|blink` shows the last color (white by
  default).
* LED (C3 examples): onboard blue LED on `GPIO8` (active high). The LED is
  also the PWM output: `GPIO8` via LEDC low-speed channel 1, 1 kHz with
  10-bit duty, so the user brightness `0..255` maps linearly onto the timer
  duty.

The shell itself is transport-agnostic: the UART examples plug a small
`embedded-io-async` adapter over the HAL's DMA-backed UART, the USB CDC ones
do the same over embassy-usb endpoints (see `CdcReader`/`CdcWriter`), and the
ESP examples use the HAL's async `Rx`/`Tx` halves directly.

## Memory footprint

The core heap working set is kept small and — crucially — bounded:

* **Line length cap** (`max_line_len`, default 128 bytes): once the input
  buffer is full, further characters are rejected with a `BEL` instead of
  growing it. Without this, a single pasted 1000-character line could demand
  ~7 KB of heap.
* **History truncation** (`max_history_len`, default 64 bytes): each stored
  entry is cut to this length (at a character boundary), so the history
  queue can never blow up on long lines.
* **No `format!` in the shell core** — prompts, ANSI cursor moves, `help` /
  `history` output and error lines are emitted as several small writes into
  fixed stack buffers, so rendering a line allocates nothing.
* **Reusable token scratch** — tokenization writes into a long-lived
  `Vec<String>` whose slots are reused across lines, so parsing stops
  allocating once the largest line seen so far has been handled.
* **Allocation-free option completion** — commands registered with
  `add_command_with_options` complete their arguments without boxing a
  closure or building `String` candidates.

```rust
# use embassy_shell::Shell;
let mut shell = Shell::new();
shell.max_history(16)      // remember at most 16 lines
     .max_history_len(48)  // store at most 48 bytes of each
     .max_line_len(80);    // cap a typed line at 80 bytes
```

On an STM32F411 the release UART example lands at ~35 KB of flash and ~10.5 KB
of RAM (with an 8 KB heap). Disabling `unicode` (see
[Cargo features](#cargo-features)) saves about another 1 KB of flash.

## Notes & limitations

* `alloc` is required (command table, line buffer, history); the working set
  is bounded by `max_line_len` / `max_history` / `max_history_len`.
* Ctrl-C cancels the handler future: don't hold non-cancel-safe state
  across `.await` inside a command.
* Bytes typed *while* a command runs are not echoed; only the most recent
  one is kept for the next line.
* Completion completes the token at the end of the line.
* ANSI/VT100 terminal assumed (`picocom`, `minicom`, PuTTY, Windows
  Terminal, …); CR/LF/CRLF all accepted as Enter.

## Status / ideas

* [ ] `heapless` back-end so the core works without `alloc`
* [ ] password auth hook, command aliases
* [ ] `Send` variant of the command table for multi-core setups

PRs welcome. MIT OR Apache-2.0 licensed.