The leddy command-line client for Leddy LED
matrix displays.
leddy publish --text "DEPLOY OK" --speed 30 --repeat 3
leddy preview --text "DEPLOY OK" --width 128 --height 8 --at 2000
leddy clear
leddy healthleddy preview renders the message locally as ASCII art. It uses the same
renderer the panel runs — leddy_lib::render_message_frame — so what you see
is what the display would show at that moment, not a lookalike:
$ leddy preview --text "HI LEDDY" --width 48 --height 8 --at 2000
cli-1786061928883 48x8 at 2000 ms (47 px wide, 3959 ms/cycle)
#...#..###........#.....#####.###...###...#...#.
#...#...#.........#.....#.....#..#..#..#..#...#.
#...#...#.........#.....#.....#...#.#...#.#...#.
#####...#.........#.....####..#...#.#...#..#.#..
#...#...#.........#.....#.....#...#.#...#...#...
#...#...#.........#.....#.....#..#..#..#....#...
#...#..###........#####.#####.###...###.....#...
................................................
It needs no device and no network, which makes it the fastest way to check
whether a message fits before publishing it. A --repeat once message that has
finished by --at reports that the display would be blank rather than failing.
Flags are declared once in .cli-flags.toml, the
flags-2-env config format. Each
flag maps to an environment variable, and precedence is
CLI flags > environment > TOML defaults:
export LEDDY_API_URL=http://leddy.local:8080 # = --url / -u
export LEDDY_SCROLL_SPEED=18 # = --speed
export LEDDY_JSON=1 # = --json / -j
leddy publish --text hi # uses the environment
leddy publish --text hi --speed 40 # the flag still wins--help is rendered by the flags-2-env core from that file at runtime — there
is no usage string in the Rust source to drift — and it is subcommand-aware:
leddy --help # global flags + the command table
leddy preview --help # preview's own flags, plus inherited global ones--width, --height, and --at are command-scoped to preview; outside it
they are rejected unknown options rather than silently ignored ones.
A device API token, where a deployment uses one, is a credential: a flag value is
visible in ps output and shell history, so LEDDY_API_TOKEN is an
[env] ignore entry read from the environment and is never a flag.
Shell completions come from the same contract and are static — no TOML read and no process spawn while you are pressing Tab:
leddy completion --shell bash > "${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions/leddy"
leddy completion --shell zsh > "${ZDOTDIR:-$HOME}/.zfunc/_leddy"src/cli_config.rs is generated from the contract and CI
diffs it against fresh generator output, so the typed struct cannot drift from
the flags.
| Code | Meaning |
|---|---|
0 |
success |
1 |
the invocation was valid but the work failed (device unreachable, non-2xx) |
2 |
bad invocation: unknown flag or command, bad --direction/--repeat, message the protocol rejects |
3 |
.cli-flags.toml could not be found, read, or audited |
All three Leddy layers are real dependencies here, not decoration. The same
edges appear in Cargo.toml and in .zpkg.toml as
the zed dependency graph:
[dependencies]
"led-dynamo/leddy-interfaces" = "^0.1.0" # shapes: MessageEnvelope, DisplayConfig + validators
"led-dynamo/leddy-lib" = "^0.1.0" # behaviour: framebuffer + scrolling renderer
"led-dynamo/leddy-clients" = "^0.1.0" # transport: publish / clear / healthMessage and geometry validation is leddy-interfaces' own validate(), called
from src/message.rs — the protocol limits are not restated
here, so they cannot quietly diverge. Cycle length comes from leddy-lib, the
same function the renderer uses.
The Cargo entries deliberately carry no rev: leddy-lib and
leddy-clients both depend on leddy-interfaces by plain git URL, and cargo
only unifies git sources whose specs match. Pinning a rev here would fork
leddy-interfaces into two crates and the shared types would stop being the same
type. Cargo.lock still pins the exact commits.
scripts/check-zed-dependencies.py fails CI
if the two manifests disagree, or if a declared zed edge is not also a real Cargo
dependency.
No module does two jobs, and main.rs does almost nothing:
| File | Responsibility |
|---|---|
src/main.rs |
argv in, exit code out |
src/lib.rs |
module wiring + top-level run |
src/flags.rs |
contract audit, parse, precedence, coercion, range checks |
src/cli_config.rs |
generated typed representation of .cli-flags.toml |
src/help.rs |
help tables + completion scripts from the native core |
src/message.rs |
flags → a validated MessageEnvelope/DisplayConfig |
src/commands/ |
one module per subcommand |
src/output.rs / src/error.rs |
human-vs-JSON, and CliError |
preview and completion never start a runtime or an HTTP client — being
offline is part of what they are, so Command::needs_network decides that once,
in the dispatcher.
unsafe_code is denied crate-wide; src/help.rs is the single module that opts
itself out, because binding to the flags-2-env C core needs it.
cargo build --locked --release # target/release/leddy
cargo test --all-targets --locked
cargo clippy --all-targets --locked -- -D warnings
python3 scripts/check-zed-dependencies.py # needs Python 3.11+