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
5 changes: 5 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ hostname = "thorium"
ips = ["192.168.178.189", "192.168.178.172"]
# optional port
port = 4242
# optional shell command run via `sh -c` when the cursor enters this client
enter_hook = "notify-send 'entered thorium'"
# optional shell command run via `sh -c` when capture for this client is
# released (release_bind chord, peer Leave, or `cli deactivate`)
leave_hook = "notify-send 'left thorium'"
7 changes: 7 additions & 0 deletions lan-mouse-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ struct Client {
ips: Option<Vec<IpAddr>>,
#[arg(long)]
enter_hook: Option<String>,
#[arg(long)]
leave_hook: Option<String>,
}

#[derive(Clone, Subcommand, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -88,6 +90,7 @@ async fn execute(cmd: CliSubcommand) -> Result<(), CliError> {
port,
ips,
enter_hook,
leave_hook,
}) => {
tx.request(FrontendRequest::Create).await?;
while let Some(e) = rx.next().await {
Expand All @@ -108,6 +111,10 @@ async fn execute(cmd: CliSubcommand) -> Result<(), CliError> {
tx.request(FrontendRequest::UpdateEnterHook(handle, Some(enter_hook)))
.await?;
}
if let Some(leave_hook) = leave_hook {
tx.request(FrontendRequest::UpdateLeaveHook(handle, Some(leave_hook)))
.await?;
}
break;
}
}
Expand Down
5 changes: 5 additions & 0 deletions lan-mouse-ipc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ pub struct ClientConfig {
pub pos: Position,
/// enter hook
pub cmd: Option<String>,
/// leave hook
pub leave_cmd: Option<String>,
}

impl Default for ClientConfig {
Expand All @@ -150,6 +152,7 @@ impl Default for ClientConfig {
fix_ips: Default::default(),
pos: Default::default(),
cmd: None,
leave_cmd: None,
}
}
}
Expand Down Expand Up @@ -259,6 +262,8 @@ pub enum FrontendRequest {
RemoveAuthorizedKey(String),
/// change the hook command
UpdateEnterHook(u64, Option<String>),
/// change the leave hook command
UpdateLeaveHook(u64, Option<String>),
/// save config file
SaveConfiguration,
}
Expand Down
30 changes: 29 additions & 1 deletion src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ pub(crate) enum ICaptureEvent {
/// either the remote client leaving its device region,
/// a new device entering the screen or the release bind.
ClientEntered(u64),
/// The previously active client was left, i.e. capture
/// was released for the given handle. Mirrors
/// [`ICaptureEvent::ClientEntered`] for the leave side
/// and fires on every release path (release-bind chord,
/// remote `Leave`, explicit `Release` request, send
/// failure, or destroy of the active capture).
ClientLeft(u64),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -301,6 +308,16 @@ impl CaptureTask {
capture.create(h, p).await?;
}
CaptureRequest::Destroy(h) => {
// If the capture we're tearing down is the
// currently-active one, treat this as a
// release for hook purposes. The release_capture
// path also clears active_client and flushes
// pressed-key state to the peer; without this,
// `cli deactivate` (or a hostname change
// re-creating the client) would skip leave_hook.
if self.active_client == Some(h) {
self.release_capture(capture).await?;
}
self.remove_capture(h);
capture.destroy(h).await?;
}
Expand Down Expand Up @@ -368,14 +385,25 @@ impl CaptureTask {
if let Err(e) = self.conn.send(event, handle).await {
const DUR: Duration = Duration::from_millis(500);
debounce!(PREV_LOG, DUR, log::warn!("releasing capture: {e}"));
capture.release().await?;
// Funnel through release_capture so the leave_hook
// 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?;
Comment thread
tyvsmith marked this conversation as resolved.
}
Ok(())
}

async fn release_capture(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> {
// If we have an active client, notify them we're leaving
if let Some(handle) = self.active_client.take() {
// Surface the leave to the service layer so it can fire
// the per-client leave_hook. Sent before the network
// teardown below so we never race against the peer
// disappearing.
self.event_tx
.send(ICaptureEvent::ClientLeft(handle))
.expect("channel closed");
Comment thread
tyvsmith marked this conversation as resolved.
// Synthesize key-up events for every key still held in the
// capture's pressed_keys set BEFORE sending Leave. Without
// this, pressing the release-bind chord (typically all four
Expand Down
16 changes: 16 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ impl ClientManager {
port: config_client.port,
pos: config_client.pos,
cmd: config_client.enter_hook,
leave_cmd: config_client.leave_hook,
};
let state = ClientState {
active: config_client.active,
Expand Down Expand Up @@ -236,6 +237,13 @@ impl ClientManager {
}
}

/// update the leave hook command of the client
pub(crate) fn set_leave_hook(&self, handle: ClientHandle, leave_hook: Option<String>) {
if let Some((c, _s)) = self.clients.borrow_mut().get_mut(handle as usize) {
c.leave_cmd = leave_hook;
}
}

/// set resolving status of the client
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
Expand All @@ -251,6 +259,14 @@ impl ClientManager {
.and_then(|(c, _)| c.cmd.clone())
}

/// get the leave hook command
pub(crate) fn get_leave_cmd(&self, handle: ClientHandle) -> Option<String> {
self.clients
.borrow()
.get(handle as usize)
.and_then(|(c, _)| c.leave_cmd.clone())
}

/// returns all clients that are currently registered
pub(crate) fn registered_clients(&self) -> Vec<ClientHandle> {
self.clients
Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ struct TomlClient {
position: Option<Position>,
activate_on_startup: Option<bool>,
enter_hook: Option<String>,
leave_hook: Option<String>,
}

impl ConfigToml {
Expand Down Expand Up @@ -275,12 +276,14 @@ pub struct ConfigClient {
pub pos: Position,
pub active: bool,
pub enter_hook: Option<String>,
pub leave_hook: Option<String>,
}

impl From<TomlClient> for ConfigClient {
fn from(toml: TomlClient) -> Self {
let active = toml.activate_on_startup.unwrap_or(false);
let enter_hook = toml.enter_hook;
let leave_hook = toml.leave_hook;
let hostname = toml.hostname;
let ips = HashSet::from_iter(toml.ips.into_iter().flatten());
let port = toml.port.unwrap_or(DEFAULT_PORT);
Expand All @@ -292,6 +295,7 @@ impl From<TomlClient> for ConfigClient {
pos,
active,
enter_hook,
leave_hook,
}
}
}
Expand All @@ -311,6 +315,7 @@ impl From<ConfigClient> for TomlClient {
let position = Some(client.pos);
let activate_on_startup = if client.active { Some(true) } else { None };
let enter_hook = client.enter_hook;
let leave_hook = client.leave_hook;
Self {
hostname,
host_name,
Expand All @@ -319,6 +324,7 @@ impl From<ConfigClient> for TomlClient {
position,
activate_on_startup,
enter_hook,
leave_hook,
}
}
}
Expand Down
48 changes: 39 additions & 9 deletions src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ impl Service {
FrontendRequest::UpdateEnterHook(handle, enter_hook) => {
self.update_enter_hook(handle, enter_hook)
}
FrontendRequest::UpdateLeaveHook(handle, leave_hook) => {
self.update_leave_hook(handle, leave_hook)
}
FrontendRequest::SaveConfiguration => self.save_config(),
}
}
Expand All @@ -229,6 +232,7 @@ impl Service {
pos: c.pos,
active: s.active,
enter_hook: c.cmd,
leave_hook: c.leave_cmd,
})
.collect();
self.config.set_clients(clients);
Expand Down Expand Up @@ -349,7 +353,11 @@ impl Service {
}
ICaptureEvent::ClientEntered(handle) => {
log::info!("entering client {handle} ...");
self.spawn_hook_command(handle);
self.spawn_hook_command(handle, HookKind::Enter);
}
ICaptureEvent::ClientLeft(handle) => {
log::info!("leaving client {handle} ...");
self.spawn_hook_command(handle, HookKind::Leave);
}
}
}
Expand Down Expand Up @@ -573,6 +581,11 @@ impl Service {
self.broadcast_client(handle);
}

fn update_leave_hook(&mut self, handle: ClientHandle, leave_hook: Option<String>) {
self.client_manager.set_leave_hook(handle, leave_hook);
self.broadcast_client(handle);
}

fn broadcast_client(&mut self, handle: ClientHandle) {
let event = self
.client_manager
Expand All @@ -582,29 +595,46 @@ impl Service {
self.notify_frontend(event);
}

fn spawn_hook_command(&self, handle: ClientHandle) {
let Some(cmd) = self.client_manager.get_enter_cmd(handle) else {
return;
fn spawn_hook_command(&self, handle: ClientHandle, kind: HookKind) {
let cmd = match kind {
HookKind::Enter => self.client_manager.get_enter_cmd(handle),
HookKind::Leave => self.client_manager.get_leave_cmd(handle),
};
let Some(cmd) = cmd else { return };
tokio::task::spawn_local(async move {
log::info!("spawning command!");
log::info!("spawning {kind} hook for client {handle}");
let mut child = match Command::new("sh").arg("-c").arg(cmd.as_str()).spawn() {
Ok(c) => c,
Err(e) => {
log::warn!("could not execute cmd: {e}");
log::warn!("could not execute {kind} hook for client {handle}: {e}");
return;
}
};
match child.wait().await {
Ok(s) => {
if s.success() {
log::info!("{cmd} exited successfully");
log::info!("{kind} hook for client {handle} ({cmd}) exited successfully");
} else {
log::warn!("{cmd} exited with {s}");
log::warn!("{kind} hook for client {handle} ({cmd}) exited with {s}");
}
}
Err(e) => log::warn!("{cmd}: {e}"),
Err(e) => log::warn!("{kind} hook for client {handle} ({cmd}): {e}"),
}
});
}
}

#[derive(Clone, Copy, Debug)]
enum HookKind {
Enter,
Leave,
}

impl std::fmt::Display for HookKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HookKind::Enter => f.write_str("enter"),
HookKind::Leave => f.write_str("leave"),
}
}
}
Loading