Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ This is a single-crate Rust binary. All source lives under `src/`:

```
src/
main.rs # Entry point, CLI args (--demo, --list), event loop, key handling
main.rs # Entry point, CLI args + subcommands, event loop, key handling, apply_action
app.rs # Application state: Tab, Focus, DeviceState, DeviceAction events
device.rs # hidapi wrapper: open ShureDevice, send/receive HID reports, model dispatch
headless.rs # JSON get/set/preset CLI for scripting and automation (no TUI)
meter.rs # cpal audio capture: real-time dBFS metering, RollingWindow, PeakWindow
presets.rs # Host-side preset storage: TOML serialisation, load/save/delete, PresetSlot
protocol.rs # Packet encoding, CRC-16/ANSI, all command constructors, apply_response()
Expand All @@ -60,6 +61,8 @@ src/

**Data flow:** key event → `handle_key()` → `DeviceAction` → `apply_action()` → `device.rs` → HID packet → `protocol.rs`

**Headless data flow:** subcommand → `headless::run()` → `device.rs` get_state/set_* → HID packet. The TUI and `headless.rs` are independent consumers of `device.rs`; neither calls the other.

**Meter data flow:** cpal audio callback → `meter_level` (AtomicI32) + `peak_window` (Mutex<PeakWindow>) → `ui.rs` reads on each render tick

**Tab structure:** Main | EQ | Dynamics | Presets | Info
Expand Down Expand Up @@ -217,6 +220,29 @@ Preset name editing is handled in `main.rs::handle_key()`, not in `toggle_focuse
When `app.editing_preset_name` is `true`, character keys append to the name and `Enter`
commits (fires `PersistPresetName`), while `Esc` cancels without saving.

### Headless CLI

`headless.rs` is the non-TUI JSON interface (`get`, `set <setting> <value>`,
`preset list|save|load|delete`). It opens the device and calls the same typed
`device.rs` methods the TUI uses, then prints one JSON object to stdout. Errors
print `{"error": ...}` and `exit(1)`.

Key patterns:
- **Reuses `PresetSlot` for output.** `get` builds its `settings` body from
`PresetSlot::from_device_state` and strips the `name` field. Do not add a parallel
state DTO — the preset mirror types are the single serialisation source.
- **`set` enum tokens come from `Ser*` deserialisation.** A setting like `compressor`
parses its value by deserialising into `SerCompressorPreset`, so the accepted
tokens are exactly what `get` emits (input/output symmetry) and the serde error
lists valid variants for free.
- **Catalog is the single source of truth.** `catalog()` lists every setting, its
accepted values, and the models it applies to. `ensure_supported()` (applicability
check) and `set help` both read it. When adding a settable field: add a `catalog()`
entry, a `dispatch_set()` arm, and extend the `every_catalog_entry_has_a_dispatch_arm`
test's name set.
- **`apply_preset_to_device()` lives in `main.rs`** and is shared by the TUI's
`LoadPreset` and headless `preset load`. It sends every model-relevant SET.

### Demo Mode

`--demo` runs with `device: None`. `send_if_connected()` silently succeeds when
Expand All @@ -233,6 +259,7 @@ This is intentional: demo mode should always be fully navigable.
- `anyhow` — all fallible functions return `anyhow::Result`
- `clap 4.5` — CLI argument parsing; `derive` feature only
- `serde 1` (with `derive` feature) — serialisation traits for preset TOML files
- `serde_json 1` — JSON output for the headless `get`/`set`/`preset` CLI
- `toml 0.8` — TOML serialisation/deserialisation for preset files
- `dirs-next 2.0` — platform config directory resolution (`~/.config/` on Linux)
- `tempfile 3` (dev-dependency) — hermetic temp directories in `presets.rs` tests
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ clap = { version = "4.5", features = ["derive"] }
cpal = "0.17"
libc = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
dirs-next = "2.0"

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,29 @@ shurectl --demo # Run without a device (explore the UI)
shurectl --list # List detected Shure devices and exit
```

### Headless / scripting (JSON)

For scripting and automation, subcommands skip the TUI and speak JSON on stdout. On
error a `{"error": ...}` object is printed and the process exits non-zero, so a
caller can branch on the exit code alone.

```bash
shurectl get # Print the full device state as JSON
shurectl set gain 24 # Apply one setting, print the resulting state
shurectl set mute on # Booleans accept on/off, true/false, 1/0
shurectl set hpf hz75 # Enums use the same tokens that `get` emits
shurectl set led-solid-rgb B2FF33 # RGB as hex RRGGBB or r,g,b
shurectl set help # List every setting, its values, and supported models
shurectl preset list # All 4 preset slots as JSON
shurectl preset save 1 # Snapshot current device state into slot 1
shurectl preset load 1 # Apply slot 1 to the device
shurectl preset delete 1 # Delete slot 1
```

`set` validates against the connected model: a setting that doesn't apply (e.g.
`phantom` on an MV6, or `led-*` on a non-MV7+) returns an error rather than a
silent no-op. `--device <path>` works with any subcommand.

### Keyboard Shortcuts

| Key | Action |
Expand Down
Loading