Skip to content

Repository files navigation

leddy-cli

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 health

Preview without a device

leddy preview renders the message locally as ASCII art. It uses the same renderer the panel runsleddy_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.

Configuration — flags-2-env

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.

Exit codes

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

Org dependencies

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 / health

Message 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.

Layout

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.

Build

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+

About

Rust command-line client for publishing, clearing, inspecting, and automating Leddy displays.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages