Skip to content

feat: add leave_hook symmetric to enter_hook#448

Open
tyvsmith wants to merge 4 commits into
feschber:mainfrom
tyvsmith:feat/leave-hook
Open

feat: add leave_hook symmetric to enter_hook#448
tyvsmith wants to merge 4 commits into
feschber:mainfrom
tyvsmith:feat/leave-hook

Conversation

@tyvsmith
Copy link
Copy Markdown
Contributor

@tyvsmith tyvsmith commented May 26, 2026

Summary

Mirrors the existing enter_hook mechanism with a leave_hook that fires when capture for a client is released.

Motivation: external scripts that change host state on cross (e.g. swapping keyboard layout, toggling natural scroll, repositioning windows) currently have no symmetric "you're back" signal — enter_hook fires deterministically but the only way to detect release today is to poll hyprctl cursorpos / similar, which is fragile because the local cursor isn't actually pinned during capture on Wayland.

Design

A single new IPC capture event ICaptureEvent::ClientLeft(handle) emitted from release_capture() in src/capture.rs. Every release path funnels through that function:

  • release_bind chord
  • peer Leave event over the wire
  • CaptureRequest::Destroy when the active client is being torn down (with an explicit note about hostname-change client recreation)
  • CaptureRequest::Release

Wiring otherwise exactly mirrors enter_hook:

Layer enter_hook leave_hook
src/config.rs enter_hook: Option<String> + leave_hook: Option<String>
lan-mouse-ipc/src/lib.rs enter_cmd on ClientConfig, UpdateEnterHook request + leave_cmd, UpdateLeaveHook
src/client.rs set_enter_hook / get_enter_cmd + set_leave_hook / get_leave_cmd
src/service.rs update_enter_hook, spawn_hook_command, dispatch on ClientEntered + update_leave_hook, spawn_leave_hook_command, dispatch on ClientLeft
lan-mouse-cli/src/lib.rs --enter-hook flag on add-client + --leave-hook flag

Docs

The existing enter_hook has been undocumented since #130. Second commit backfills both in the example config.toml so the per-client hook surface is discoverable.

Out of scope

  • GUI surface in lan-mouse-gtk for the new field (text-config only for now)
  • Unit tests (no test infrastructure exists in the project today; manual smoke-tested with notify-send hooks across crossing + release_bind release on Wayland/Hyprland)

Test plan

  • Builds clean: cargo build --release (1m 20s, no warnings)
  • cargo clippy --all-targets --release -- -D warnings clean
  • Manual smoke: enter_hook and leave_hook set to notify-send strings, daemon restarted from built binary, cross to client and release via chord — both notifications fire as expected
  • Verify behavior with peer-initiated Leave (cross-back from the remote side)
  • Verify behavior on cli deactivate of the active client

tyvsmith and others added 2 commits May 26, 2026 15:09
Per-client leave_hook fires sh -c <cmd> when capture for that client
is released. Wired through a single release_capture() chokepoint
covering release_bind, network send failure, Destroy-of-active-client,
and Release request paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The existing enter_hook field has been undocumented since it was added
in feschber#130. Add it alongside the new leave_hook to the example config so
both per-client hooks are discoverable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tyvsmith tyvsmith marked this pull request as ready for review May 26, 2026 23:38
Copilot AI review requested due to automatic review settings May 26, 2026 23:38
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for per-client “leave hooks” that run when capture is released for a client, wiring the new setting through config, IPC, CLI, and capture/service event handling.

Changes:

  • Introduce leave_hook / leave_cmd in config and IPC models, plus CLI flag support.
  • Emit a new ICaptureEvent::ClientLeft on release paths and handle it in the service.
  • Execute configured leave commands via sh -c when a client is left.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/service.rs Handle UpdateLeaveHook, persist leave_hook, and spawn leave-hook commands on ClientLeft.
src/config.rs Add leave_hook to TOML and runtime config conversion paths.
src/client.rs Store and expose per-client leave_cmd in ClientManager.
src/capture.rs Add ClientLeft event and ensure release paths trigger it.
lan-mouse-ipc/src/lib.rs Extend IPC structs/enums with leave-hook fields and requests.
lan-mouse-cli/src/lib.rs Add --leave-hook flag and send UpdateLeaveHook on create.
config.toml Document and example-configure leave_hook.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/capture.rs
Comment on lines +404 to +406
self.event_tx
.send(ICaptureEvent::ClientLeft(handle))
.expect("channel closed");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as-is to stay consistent with the surrounding pattern: the two pre-existing event_tx.send(...).expect("channel closed") sites (CaptureBegin at L350 and ClientEntered at L371) use the same idiom, and request_rx.recv().expect("channel closed") at L303 follows the same convention. The event_tx receiver lives in the service task that owns the capture task — if it's gone the daemon is already tearing down. Changing only the new site would create a one-of-four inconsistency; if we want to soften this it should be a separate refactor across all four call sites.

Comment thread src/capture.rs
// fires and active_client is cleared (without this the
// active_client field would stay stale until the next
// Begin from a different handle).
self.release_capture(capture).await?;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error-propagation surface doesn't actually change here. Inside release_capture() everything is either infallible-or-logged (the event_tx.send panics on closed channel like its siblings; the conn.send calls for key-up/modifier-reset/Leave all use if let Err(e) = ... { log::warn!(...) } — they never propagate). The only ?-propagated error is the final capture.release().await at the end, which is exactly what the old capture.release().await? returned. So this site propagates the same CaptureError as before — it just additionally fires the leave_hook and clears active_client along the way, both of which we want in this code path.

Comment thread src/service.rs Outdated
Comment on lines +616 to +640
fn spawn_leave_hook_command(&self, handle: ClientHandle) {
let Some(cmd) = self.client_manager.get_leave_cmd(handle) else {
return;
};
tokio::task::spawn_local(async move {
log::info!("spawning leave command!");
let mut child = match Command::new("sh").arg("-c").arg(cmd.as_str()).spawn() {
Ok(c) => c,
Err(e) => {
log::warn!("could not execute leave cmd: {e}");
return;
}
};
match child.wait().await {
Ok(s) => {
if s.success() {
log::info!("{cmd} exited successfully");
} else {
log::warn!("{cmd} exited with {s}");
}
}
Err(e) => log::warn!("{cmd}: {e}"),
}
});
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3313b2e — collapsed both into a single spawn_hook_command(handle, HookKind) with the kind carrying the human label into log messages (which now also include the client handle for both enter and leave).

The Swatinem/rust-cache action was restored before brew installed the
native dependencies. When Homebrew updated glib (2.88.0 → newer) the
cached Rust build artifacts still referenced the old versioned Cellar
path (/opt/homebrew/Cellar/glib/2.88.0/lib), causing a linker failure:

    ld: library 'gio-2.0' not found

Fix by:
1. Moving `brew install` before the rust-cache step so libs are current
   before the cache is consulted.
2. Capturing `brew list --versions glib gtk4 libadwaita` into a
   prefix-key so the cache key changes whenever those packages update,
   forcing a clean Rust build with fresh pkg-config paths.

Linux and Windows cache behaviour is unchanged (MACOS_LIB_VER unset →
empty prefix-key → same default v0- prefix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tyvsmith
Copy link
Copy Markdown
Contributor Author

Reference implementation for hook driven input changes

Add to ~/.config/lan-mouse/config.toml

enter_hook = "lan-mouse-mode mac"
leave_hook = "lan-mouse-mode default"

Script that swaps super/alt & inverts scrolling for Hyprland host and MacOS client

The two spawn_*_hook_command functions were ~35 lines of near-byte-
identical glue (only the get_*_cmd call and log prefixes differed).
Replace with a single spawn_hook_command(handle, HookKind) and let
the kind carry the human label into log lines (which now also include
the client handle).

No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants