openlogi 0.7.1

OpenLogi command-line interface — a local-first companion for Logitech HID++ peripherals.
# Decision log

Durable "why we did it this way" records that are not obvious from the code.
Add a dated entry when a non-obvious architectural or dependency decision is
made or revisited.

## 2026-08: Shared clippy lint set

The workspace adopted the shared ten-lint set (`assertions_on_result_states`,
`cast_possible_truncation`, `cast_possible_wrap`, `cast_sign_loss`,
`error_impl_error`, `exit`, `or_fun_call`, `ptr_as_ptr`,
`tests_outside_test_module`, `undocumented_unsafe_blocks`) on top of the
existing `pedantic` + `unwrap_used`/`expect_used` table.

- One table, inherited everywhere. `openlogi-gui`, `openlogi-camera` and
  `openlogi-hook` carried hand-copied duplicates of `[workspace.lints]`, so any
  lint added to the workspace would have silently skipped them — three of the
  crates holding most of the FFI. Cargo rejects `[lints] workspace = true`
  alongside local overrides, so `openlogi-hook` moved its `unsafe_code` opt-out
  into its three platform modules. `openlogi-hidpp` stays out on purpose
  (vendored).
- `tests_outside_test_module` only recognises a literal `#[cfg(test)]`. Compound
  gates are written as stacked attributes (`#[cfg(test)]` then `#[cfg(unix)]`);
  an integration test under `tests/` carries a file-level `#![expect(…)]`
  because that file is already a test-only crate. Splitting the attribute also
  wakes `items_after_test_module`, so such a module belongs last in its file.
- `exit` gets a real `ExitCode` wherever the call site can return — `openlogi
  list` hands status 2 back to `main` — and a reasoned `#[expect]` where it
  cannot (the AppKit run loop, the watchdog threads, the update handover).
  Clippy does not look inside `define_class!`, so the menu-bar Quit body moved
  out of the macro rather than escape the lint by accident.
- Not adopted: the policy's `unexpected_cfgs` / `check-cfg = ['cfg(kani)']`
  entry. Nothing here uses Kani, and `unexpected_cfgs` already warns by default,
  so it would be dead configuration.

## 2026-08: Standalone raw-light boundary

Standalone lights such as Litra stay outside the HID++ receiver/paired-device
model and are normalized only at the shared agent and GUI device-record
boundary. This keeps the existing HID++ wire and routing semantics unchanged
while allowing future light drivers to share capability-driven controls.

- Persist brightness as a normalized percentage and temperature as Kelvin;
  native units and report encoding remain driver responsibilities.
- Use device serials for persistent raw-device keys. OS-node identifiers are
  runtime-only hints and must not silently become physical configuration keys.
- Advertise optional light controls through `LightCapabilities`; the GUI gates
  controls from those capabilities rather than from `DeviceKind::Light`.
- Serialize and coalesce per-device light writes in the agent so reconnect,
  camera automation, config reload, and manual commands cannot interleave at
  packet level.

## 2026-07: Infrastructure we keep custom instead of using a crate

A dependency audit replaced most general-purpose infrastructure code with
mature crates (`tempfile`, `which`, `plist`, `walkdir`, `xshell`, `sysinfo`,
`fs-err`, `backon`, `opener`, `etcetera`, and others — see the git history of
`FIXDRY.md` for the full list). The following stayed custom, deliberately:

- `openlogi-core::single_instance`: the `single-instance` crate uses different
  backends (for example abstract Unix sockets on Linux) and does not preserve
  OpenLogi's data-dir lock-file path, per-role names, and error classification
  closely enough to be a safe deletion.
- Agent tray Quit's `openlogi://quit` dispatch keeps
  `std::process::Command::output()` intentionally: it blocks until
  LaunchServices accepts the Apple Event, while generic opener crates only
  guarantee process spawn.
- GUI helper launch keeps `/usr/bin/open -g -n` intentionally: it needs
  LaunchServices-specific flags to start the packaged agent under its own TCC
  identity, which generic opener crates do not expose.
- Agent autostart install keeps direct `systemctl` calls because it is managing
  systemd user units, not merely opening or spawning an arbitrary program.
- Self-restart and `disclaim` launches stay custom because they are process
  identity / update lifecycle boundaries, not generic command orchestration.
- `openlogi-hook`: event suppression/rewriting and foreground-app lookup are
  OpenLogi-specific and not covered cleanly by generic input crates.
- `openlogi-inject`: platform-specific action synthesis may overlap with
  `enigo`, but current semantics are narrower and more controlled.
- `openlogi-hid` / vendored `openlogi-hidpp`: the right path is upstreaming
  OpenLogi-specific fixes, not replacing the fork blindly.