Previously the Hello handler in `ListenTask` echoed our local commit
back but deliberately threw away the peer's, on the assumption that
the outgoing connect-side path (`connect.rs:278-279` →
`set_peer_commit`) would always populate the visible state for any
bidirectionally-configured peer.
That assumption breaks any time the *outgoing* TCP/DTLS direction is
broken even though the inbound direction is fine — happened just now
when the peer Mac's daemon stopped listening on 4242 (DHCP-renewed
IP, daemon crashed, asymmetric NAT, …). Mac was still happily
connecting in the other direction and sending events, including the
initial Hello, but Linux silently displayed "peer version unknown"
because the listen side dropped Mac's commit on the floor.
Add a `PeerHello { addr, commit }` EmulationEvent variant fired from
the listen-side Hello handler. The service maps `addr → ClientHandle`
via `client_manager.get_client(addr)` and calls `set_peer_commit` +
`broadcast_client` exactly like the connect path does. The connect
path remains the primary source for symmetric setups; this is the
defensive fallback so version visibility doesn't depend on outbound
reachability.
Skips silently when no outgoing client is configured for the peer's
addr (incoming-only setup) — there's no UI row to update in that
case anyway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a one-shot Hello message to the lan-mouse wire protocol so each
peer can display the other end's build commit hash and warn on
version mismatch. Soft-warn only — mismatched versions never refuse
traffic.
Wire change (lan-mouse-proto)
* `ProtoEvent::Hello { commit: [u8; 8] }` carries the 8-byte ASCII
short commit from shadow_rs's `SHORT_COMMIT`. Encoded/decoded
alongside the existing event variants.
* `EventType::Hello` is appended to the enum so existing IDs are
untouched. Old peers receive the event, hit `InvalidEventId`, and
silently skip it via the forward-compat handler in
`connect.rs::receive_loop` — the connection is unaffected.
Daemon
* Connect side sends one Hello immediately after the DTLS handshake
authenticates and before the ping_pong loop starts. Best-effort,
fire-and-forget — `log::debug!` on send error.
* Listen side mirrors the peer's Hello with its own (same shape as
the existing Ping → Pong reply), so the peer's connect-side
receive_loop populates `ClientState::peer_commit` for that
handle.
* The disconnect path clears `peer_commit` so a stale hash isn't
shown after the connection drops.
IPC
* `ClientState::peer_commit: Option<[u8; 8]>`. `None` means the
peer hasn't sent Hello yet — either fresh connection or older
build that predates the event.
GTK
* `ClientObject` exposes `peer-commit` as an `Option<String>`
property; `peer_commit_to_string` converts the wire `[u8; 8]` to
the displayable hex.
* `lan_mouse_gtk::run` now takes the local commit and stashes it in
a `OnceLock` so per-row UI can compare against each peer's hash.
* `ClientRow::refresh_version_status` re-renders the collapsed
subtitle with Pango markup whenever the property changes:
- matched → green "peer version: <hex> · matched"
- mismatch → orange "peer version: <hex> · ours: <hex>"
- unknown → orange "peer version: unknown · ours: <hex>"
* Window invokes `refresh_version_status` from
`update_client_state` after writing the new property, and
`bind` calls it once on row construction so the initial
subtitle isn't blank.
Known limitation: state-change broadcasts from the network side
(set_alive / set_active_addr / set_peer_commit) don't currently
trigger a `FrontendEvent::State` directly; the UI picks up the
latest values on the next user-driven broadcast. Same pre-existing
behavior as the alive/active_addr fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DTLS recv loops in src/listen.rs and src/connect.rs each read
one full datagram per call. A failed `try_into::<ProtoEvent>()`
means the datagram's leading EventType byte didn't match any
known variant — a misalignment is impossible because DTLS is
message-framed, not stream-framed.
Previously, src/listen.rs would `break` out of the loop on parse
failure (tearing down the connection) and src/connect.rs would
silently swallow the error with no log. Both are wrong as
forward-compat behavior: any future protocol addition (e.g. a
new event variant) would force every existing peer to disconnect
rather than gracefully ignoring the unknown event.
Skip-and-continue on both sides, with a debug-level log so the
behavior is observable. Pre-requisite for any future ProtoEvent
variant to land without forcing a coordinated upgrade across
every peer in a deployment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user pressed the release-bind chord (typically all four
modifiers) the down events for the chord were forwarded to the peer
while capture was active, but the matching up events arrived after
the local tap flipped to passthrough and were never forwarded. The
peer was left with phantom held modifiers until either its watchdog
ran (1+ s) or the Leave message was processed — and Leave is sent
over UDP/DTLS and can be lost.
Drain the capture's pressed_keys set in release_capture and emit a
KeyboardEvent::Key{state: 0} for every still-held key, plus a
zeroed KeyboardEvent::Modifiers, before sending Leave. The receiver
already maintains pressed_keys per handle and processes these
key-up events through its normal path, so no protocol change is
required and an unmodified peer picks up the fix automatically.
The receiver-side release_keys safety net stays in place for the
genuine packet-loss / disconnect-without-Leave cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix created the config directory but left config.toml
absent on a first launch, which surfaced as two "No such file or
directory (os error 2)" warnings (one from the main process, one
from the spawned daemon child) and left the app starting up with
config_toml=None until the GUI persisted something.
Write ConfigToml::default() to the path if it doesn't exist, so
every entry point — GUI main, spawned daemon, CLI, test commands
— gets a concrete file to read, and first-launch logs stay clean.
Also reorders Config::new() so both the directory creation and the
file bootstrap run before the first read attempt, eliminating the
warning at the source rather than hiding it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
notify::Watcher fails on macOS if the config directory doesn't
exist. Create it with create_dir_all before calling config.watch().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix parsing toml key position and values
* Using rename_all instead rename over each enum
* rename struct field directly
---------
Co-authored-by: Ferdinand Schober <ferdinand.schober@fau.de>
*breaking change*
this changes the configuration syntax, allowing for an unlimited amount of configured clients.
Also a first step towards enabling a "save config" feature.
client configuration now applies immediately instead of after enabling / disabling clients.
Also fixes a potential feedback loop when changing settings.
* reference count capture
Multiple captures can now be created at the same position.
Captures at the same position are reference counted.
* update testcase
will be required by #200 / #164
* Input capture and emulation can now be disabled and will prompt the user to enable again.
* Improved error handling to deliver more useful error messages