From 2f6a3629ad5bb6e8687f52059c7f921afd4b693a Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Sat, 15 Mar 2025 18:20:25 +0100 Subject: [PATCH] remove cli frontend in favour of cli subcommand (#278) this removes the cli frontend entirely, replacing it with a subcommand instead --- Cargo.lock | 4 +- README.md | 4 +- lan-mouse-cli/Cargo.toml | 2 + lan-mouse-cli/src/command.rs | 153 ----------- lan-mouse-cli/src/lib.rs | 417 ++++++++++------------------- lan-mouse-ipc/src/connect_async.rs | 13 +- lan-mouse-ipc/src/lib.rs | 4 + nix/hm-module.nix | 4 +- service/lan-mouse.service | 2 +- src/capture_test.rs | 6 +- src/client.rs | 7 + src/config.rs | 60 +++-- src/emulation_test.rs | 13 +- src/main.rs | 69 ++--- src/service.rs | 8 + 15 files changed, 269 insertions(+), 497 deletions(-) delete mode 100644 lan-mouse-cli/src/command.rs diff --git a/Cargo.lock b/Cargo.lock index a3194f6..c453d28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -2005,8 +2005,10 @@ dependencies = [ name = "lan-mouse-cli" version = "0.2.0" dependencies = [ + "clap", "futures", "lan-mouse-ipc", + "thiserror 2.0.0", "tokio", ] diff --git a/README.md b/README.md index d84a078..4335de6 100644 --- a/README.md +++ b/README.md @@ -288,10 +288,10 @@ $ cargo run --release -- --frontend cli Lan Mouse can be launched in daemon mode to keep it running in the background (e.g. for use in a systemd-service). -To do so, add `--daemon` to the commandline args: +To do so, use the `daemon` subcommand: ```sh -lan-mouse --daemon +lan-mouse daemon ``` In order to start lan-mouse with a graphical session automatically, diff --git a/lan-mouse-cli/Cargo.toml b/lan-mouse-cli/Cargo.toml index 66fc707..238bffc 100644 --- a/lan-mouse-cli/Cargo.toml +++ b/lan-mouse-cli/Cargo.toml @@ -9,6 +9,8 @@ repository = "https://github.com/feschber/lan-mouse" [dependencies] futures = "0.3.30" lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" } +clap = { version = "4.4.11", features = ["derive"] } +thiserror = "2.0.0" tokio = { version = "1.32.0", features = [ "io-util", "io-std", diff --git a/lan-mouse-cli/src/command.rs b/lan-mouse-cli/src/command.rs deleted file mode 100644 index 2bbb906..0000000 --- a/lan-mouse-cli/src/command.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::{ - fmt::Display, - str::{FromStr, SplitWhitespace}, -}; - -use lan_mouse_ipc::{ClientHandle, Position}; - -pub(super) enum CommandType { - NoCommand, - Help, - Connect, - Disconnect, - Activate, - Deactivate, - List, - SetHost, - SetPort, -} - -#[derive(Debug)] -pub(super) struct InvalidCommand { - cmd: String, -} - -impl Display for InvalidCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "invalid command: \"{}\"", self.cmd) - } -} - -impl FromStr for CommandType { - type Err = InvalidCommand; - - fn from_str(s: &str) -> std::prelude::v1::Result { - match s { - "connect" => Ok(Self::Connect), - "disconnect" => Ok(Self::Disconnect), - "activate" => Ok(Self::Activate), - "deactivate" => Ok(Self::Deactivate), - "list" => Ok(Self::List), - "set-host" => Ok(Self::SetHost), - "set-port" => Ok(Self::SetPort), - "help" => Ok(Self::Help), - _ => Err(InvalidCommand { cmd: s.to_string() }), - } - } -} - -#[derive(Debug)] -pub(super) enum Command { - None, - Help, - Connect(Position, String, Option), - Disconnect(ClientHandle), - Activate(ClientHandle), - Deactivate(ClientHandle), - List, - SetHost(ClientHandle, String), - SetPort(ClientHandle, Option), -} - -impl CommandType { - pub(super) fn usage(&self) -> &'static str { - match self { - CommandType::Help => "help", - CommandType::NoCommand => "", - CommandType::Connect => "connect left|right|top|bottom []", - CommandType::Disconnect => "disconnect ", - CommandType::Activate => "activate ", - CommandType::Deactivate => "deactivate ", - CommandType::List => "list", - CommandType::SetHost => "set-host ", - CommandType::SetPort => "set-port ", - } - } -} - -pub(super) enum CommandParseError { - Usage(CommandType), - Invalid(InvalidCommand), -} - -impl Display for CommandParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Usage(cmd) => write!(f, "usage: {}", cmd.usage()), - Self::Invalid(cmd) => write!(f, "{}", cmd), - } - } -} - -impl FromStr for Command { - type Err = CommandParseError; - - fn from_str(cmd: &str) -> Result { - let mut args = cmd.split_whitespace(); - let cmd_type: CommandType = match args.next() { - Some(c) => c.parse().map_err(CommandParseError::Invalid), - None => Ok(CommandType::NoCommand), - }?; - match cmd_type { - CommandType::Help => Ok(Command::Help), - CommandType::NoCommand => Ok(Command::None), - CommandType::Connect => parse_connect_cmd(args), - CommandType::Disconnect => parse_disconnect_cmd(args), - CommandType::Activate => parse_activate_cmd(args), - CommandType::Deactivate => parse_deactivate_cmd(args), - CommandType::List => Ok(Command::List), - CommandType::SetHost => parse_set_host(args), - CommandType::SetPort => parse_set_port(args), - } - } -} - -fn parse_connect_cmd(mut args: SplitWhitespace<'_>) -> Result { - const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Connect); - let pos = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?; - let host = args.next().ok_or(USAGE)?.to_string(); - let port = args.next().and_then(|p| p.parse().ok()); - Ok(Command::Connect(pos, host, port)) -} - -fn parse_disconnect_cmd(mut args: SplitWhitespace<'_>) -> Result { - const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Disconnect); - let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?; - Ok(Command::Disconnect(id)) -} - -fn parse_activate_cmd(mut args: SplitWhitespace<'_>) -> Result { - const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Activate); - let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?; - Ok(Command::Activate(id)) -} - -fn parse_deactivate_cmd(mut args: SplitWhitespace<'_>) -> Result { - const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Deactivate); - let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?; - Ok(Command::Deactivate(id)) -} - -fn parse_set_host(mut args: SplitWhitespace<'_>) -> Result { - const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetHost); - let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?; - let host = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?; - Ok(Command::SetHost(id, host)) -} - -fn parse_set_port(mut args: SplitWhitespace<'_>) -> Result { - const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetPort); - let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?; - let port = args.next().and_then(|p| p.parse().ok()); - Ok(Command::SetPort(id, port)) -} diff --git a/lan-mouse-cli/src/lib.rs b/lan-mouse-cli/src/lib.rs index 2c1db0d..b8c4a0d 100644 --- a/lan-mouse-cli/src/lib.rs +++ b/lan-mouse-cli/src/lib.rs @@ -1,298 +1,167 @@ +use clap::{Args, Parser, Subcommand}; use futures::StreamExt; -use tokio::{ - io::{AsyncBufReadExt, BufReader}, - task::LocalSet, -}; -use std::io::{self, Write}; - -use self::command::{Command, CommandType}; +use std::{net::IpAddr, time::Duration}; +use thiserror::Error; use lan_mouse_ipc::{ - AsyncFrontendEventReader, AsyncFrontendRequestWriter, ClientConfig, ClientHandle, ClientState, - FrontendEvent, FrontendRequest, IpcError, DEFAULT_PORT, + connect_async, ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError, + Position, }; -mod command; +#[derive(Debug, Error)] +pub enum CliError { + /// is the service running? + #[error("could not connect: `{0}` - is the service running?")] + ServiceNotRunning(#[from] ConnectionError), + #[error("error communicating with service: {0}")] + Ipc(#[from] IpcError), +} -pub fn run() -> Result<(), IpcError> { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build()?; - runtime.block_on(LocalSet::new().run_until(async move { - let (rx, tx) = lan_mouse_ipc::connect_async().await?; - let mut cli = Cli::new(rx, tx); - cli.run().await - }))?; +#[derive(Parser, Debug, PartialEq, Eq)] +#[command(name = "lan-mouse-cli", about = "LanMouse CLI interface")] +pub struct CliArgs { + #[command(subcommand)] + command: CliSubcommand, +} + +#[derive(Args, Clone, Debug, PartialEq, Eq)] +struct Client { + #[arg(long)] + hostname: Option, + #[arg(long)] + port: Option, + #[arg(long)] + ips: Option>, + #[arg(long)] + enter_hook: Option, +} + +#[derive(Subcommand, Debug, PartialEq, Eq)] +enum CliSubcommand { + /// add a new client + AddClient(Client), + /// remove an existing client + RemoveClient { id: ClientHandle }, + /// activate a client + Activate { id: ClientHandle }, + /// deactivate a client + Deactivate { id: ClientHandle }, + /// list configured clients + List, + /// change hostname + SetHost { + id: ClientHandle, + host: Option, + }, + /// change port + SetPort { id: ClientHandle, port: u16 }, + /// set position + SetPosition { id: ClientHandle, pos: Position }, + /// set ips + SetIps { id: ClientHandle, ips: Vec }, + /// re-enable capture + EnableCapture, + /// re-enable emulation + EnableEmulation, + /// authorize a public key + AuthorizeKey { + description: String, + sha256_fingerprint: String, + }, + /// deauthorize a public key + RemoveAuthorizedKey { sha256_fingerprint: String }, +} + +pub async fn run(args: CliArgs) -> Result<(), CliError> { + execute(args.command).await?; Ok(()) } -struct Cli { - clients: Vec<(ClientHandle, ClientConfig, ClientState)>, - rx: AsyncFrontendEventReader, - tx: AsyncFrontendRequestWriter, -} - -impl Cli { - fn new(rx: AsyncFrontendEventReader, tx: AsyncFrontendRequestWriter) -> Cli { - Self { - clients: vec![], - rx, - tx, - } - } - - async fn run(&mut self) -> Result<(), IpcError> { - let stdin = tokio::io::stdin(); - let stdin = BufReader::new(stdin); - let mut stdin = stdin.lines(); - - /* initial state sync */ - self.clients = loop { - match self.rx.next().await { - Some(Ok(e)) => { - if let FrontendEvent::Enumerate(clients) = e { - break clients; +async fn execute(cmd: CliSubcommand) -> Result<(), CliError> { + let (mut rx, mut tx) = connect_async(Some(Duration::from_millis(500))).await?; + match cmd { + CliSubcommand::AddClient(Client { + hostname, + port, + ips, + enter_hook, + }) => { + tx.request(FrontendRequest::Create).await?; + while let Some(e) = rx.next().await { + if let FrontendEvent::Created(handle, _, _) = e? { + if let Some(hostname) = hostname { + tx.request(FrontendRequest::UpdateHostname(handle, Some(hostname))) + .await?; } - } - Some(Err(e)) => return Err(e), - None => return Ok(()), - } - }; - - loop { - prompt()?; - tokio::select! { - line = stdin.next_line() => { - let Some(line) = line? else { - break Ok(()); - }; - let cmd: Command = match line.parse() { - Ok(cmd) => cmd, - Err(e) => { - eprintln!("{e}"); - continue; - } - }; - self.execute(cmd).await?; - } - event = self.rx.next() => { - if let Some(event) = event { - self.handle_event(event?); - } else { - break Ok(()); + if let Some(port) = port { + tx.request(FrontendRequest::UpdatePort(handle, port)) + .await?; } + if let Some(ips) = ips { + tx.request(FrontendRequest::UpdateFixIps(handle, ips)) + .await?; + } + if let Some(enter_hook) = enter_hook { + tx.request(FrontendRequest::UpdateEnterHook(handle, Some(enter_hook))) + .await?; + } + break; } } } - } - - async fn execute(&mut self, cmd: Command) -> Result<(), IpcError> { - match cmd { - Command::None => {} - Command::Connect(pos, host, port) => { - let request = FrontendRequest::Create; - self.tx.request(request).await?; - let handle = loop { - if let Some(Ok(event)) = self.rx.next().await { - match event { - FrontendEvent::Created(h, c, s) => { - self.clients.push((h, c, s)); - break h; - } - _ => { - self.handle_event(event); - continue; - } - } - } - }; - for request in [ - FrontendRequest::UpdateHostname(handle, Some(host.clone())), - FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)), - FrontendRequest::UpdatePosition(handle, pos), - ] { - self.tx.request(request).await?; - } - } - Command::Disconnect(id) => { - self.tx.request(FrontendRequest::Delete(id)).await?; - loop { - if let Some(Ok(event)) = self.rx.next().await { - self.handle_event(event.clone()); - if let FrontendEvent::Deleted(_) = event { - self.handle_event(event); - break; - } - } - } - } - Command::Activate(id) => { - self.tx.request(FrontendRequest::Activate(id, true)).await?; - } - Command::Deactivate(id) => { - self.tx - .request(FrontendRequest::Activate(id, false)) - .await?; - } - Command::List => { - self.tx.request(FrontendRequest::Enumerate()).await?; - while let Some(e) = self.rx.next().await { - let event = e?; - self.handle_event(event.clone()); - if let FrontendEvent::Enumerate(_) = event { - break; - } - } - } - Command::SetHost(handle, host) => { - let request = FrontendRequest::UpdateHostname(handle, Some(host.clone())); - self.tx.request(request).await?; - } - Command::SetPort(handle, port) => { - let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)); - self.tx.request(request).await?; - } - Command::Help => { - for cmd_type in [ - CommandType::List, - CommandType::Connect, - CommandType::Disconnect, - CommandType::Activate, - CommandType::Deactivate, - CommandType::SetHost, - CommandType::SetPort, - ] { - eprintln!("{}", cmd_type.usage()); - } - } + CliSubcommand::RemoveClient { id } => tx.request(FrontendRequest::Delete(id)).await?, + CliSubcommand::Activate { id } => tx.request(FrontendRequest::Activate(id, true)).await?, + CliSubcommand::Deactivate { id } => { + tx.request(FrontendRequest::Activate(id, false)).await? } - Ok(()) - } - - fn find_mut( - &mut self, - handle: ClientHandle, - ) -> Option<&mut (ClientHandle, ClientConfig, ClientState)> { - self.clients.iter_mut().find(|(h, _, _)| *h == handle) - } - - fn remove( - &mut self, - handle: ClientHandle, - ) -> Option<(ClientHandle, ClientConfig, ClientState)> { - let idx = self.clients.iter().position(|(h, _, _)| *h == handle); - idx.map(|i| self.clients.swap_remove(i)) - } - - fn handle_event(&mut self, event: FrontendEvent) { - match event { - FrontendEvent::Created(h, c, s) => { - eprint!("client added ({h}): "); - print_config(&c); - eprint!(" "); - print_state(&s); - eprintln!(); - self.clients.push((h, c, s)); - } - FrontendEvent::NoSuchClient(h) => { - eprintln!("no such client: {h}"); - } - FrontendEvent::State(h, c, s) => { - if let Some((_, config, state)) = self.find_mut(h) { - let old_host = config.hostname.clone().unwrap_or("\"\"".into()); - let new_host = c.hostname.clone().unwrap_or("\"\"".into()); - if old_host != new_host { - eprintln!( - "client {h}: hostname updated ({} -> {})", - old_host, new_host + CliSubcommand::List => { + tx.request(FrontendRequest::Enumerate()).await?; + while let Some(e) = rx.next().await { + if let FrontendEvent::Enumerate(clients) = e? { + for (handle, config, state) in clients { + let host = config.hostname.unwrap_or("unknown".to_owned()); + let port = config.port; + let pos = config.pos; + let active = state.active; + let ips = state.ips; + println!( + "id {handle}: {host}:{port} ({pos}) active: {active}, ips: {ips:?}" ); } - if config.port != c.port { - eprintln!("client {h} changed port: {} -> {}", config.port, c.port); - } - if config.fix_ips != c.fix_ips { - eprintln!("client {h} ips updated: {:?}", c.fix_ips) - } - *config = c; - if state.active ^ s.active { - eprintln!( - "client {h} {}", - if s.active { "activated" } else { "deactivated" } - ); - } - *state = s; + break; } } - FrontendEvent::Deleted(h) => { - if let Some((h, c, _)) = self.remove(h) { - eprint!("client {h} removed ("); - print_config(&c); - eprintln!(")"); - } - } - FrontendEvent::PortChanged(p, e) => { - if let Some(e) = e { - eprintln!("failed to change port: {e}"); - } else { - eprintln!("changed port to {p}"); - } - } - FrontendEvent::Enumerate(clients) => { - self.clients = clients; - self.print_clients(); - } - FrontendEvent::Error(e) => { - eprintln!("ERROR: {e}"); - } - FrontendEvent::CaptureStatus(s) => { - eprintln!("capture status: {s:?}") - } - FrontendEvent::EmulationStatus(s) => { - eprintln!("emulation status: {s:?}") - } - FrontendEvent::AuthorizedUpdated(fingerprints) => { - eprintln!("authorized keys changed:"); - for (desc, fp) in fingerprints { - eprintln!("{desc}: {fp}"); - } - } - FrontendEvent::PublicKeyFingerprint(fp) => { - eprintln!("the public key fingerprint of this device is {fp}"); - } - FrontendEvent::IncomingConnected(..) => {} - FrontendEvent::IncomingDisconnected(..) => {} + } + CliSubcommand::SetHost { id, host } => { + tx.request(FrontendRequest::UpdateHostname(id, host)) + .await? + } + CliSubcommand::SetPort { id, port } => { + tx.request(FrontendRequest::UpdatePort(id, port)).await? + } + CliSubcommand::SetPosition { id, pos } => { + tx.request(FrontendRequest::UpdatePosition(id, pos)).await? + } + CliSubcommand::SetIps { id, ips } => { + tx.request(FrontendRequest::UpdateFixIps(id, ips)).await? + } + CliSubcommand::EnableCapture => tx.request(FrontendRequest::EnableCapture).await?, + CliSubcommand::EnableEmulation => tx.request(FrontendRequest::EnableEmulation).await?, + CliSubcommand::AuthorizeKey { + description, + sha256_fingerprint, + } => { + tx.request(FrontendRequest::AuthorizeKey( + description, + sha256_fingerprint, + )) + .await? + } + CliSubcommand::RemoveAuthorizedKey { sha256_fingerprint } => { + tx.request(FrontendRequest::RemoveAuthorizedKey(sha256_fingerprint)) + .await? } } - - fn print_clients(&mut self) { - for (h, c, s) in self.clients.iter() { - eprint!("client {h}: "); - print_config(c); - eprint!(" "); - print_state(s); - eprintln!(); - } - } -} - -fn prompt() -> io::Result<()> { - eprint!("lan-mouse > "); - std::io::stderr().flush()?; Ok(()) } - -fn print_config(c: &ClientConfig) { - eprint!( - "{}:{} ({}), ips: {:?}", - c.hostname.clone().unwrap_or("(no hostname)".into()), - c.port, - c.pos, - c.fix_ips - ); -} - -fn print_state(s: &ClientState) { - eprint!("active: {}, dns: {:?}", s.active, s.ips); -} diff --git a/lan-mouse-ipc/src/connect_async.rs b/lan-mouse-ipc/src/connect_async.rs index faec49e..ea4fa9e 100644 --- a/lan-mouse-ipc/src/connect_async.rs +++ b/lan-mouse-ipc/src/connect_async.rs @@ -1,7 +1,6 @@ use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError}; use std::{ cmp::min, - io, task::{ready, Poll}, time::Duration, }; @@ -47,7 +46,7 @@ impl Stream for AsyncFrontendEventReader { } impl AsyncFrontendRequestWriter { - pub async fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> { + pub async fn request(&mut self, request: FrontendRequest) -> Result<(), IpcError> { let mut json = serde_json::to_string(&request).unwrap(); log::debug!("requesting: {json}"); json.push('\n'); @@ -57,8 +56,16 @@ impl AsyncFrontendRequestWriter { } pub async fn connect_async( + timeout: Option, ) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> { - let stream = wait_for_service().await?; + let stream = if let Some(duration) = timeout { + tokio::select! { + s = wait_for_service() => s?, + _ = tokio::time::sleep(duration) => return Err(ConnectionError::Timeout), + } + } else { + wait_for_service().await? + }; #[cfg(unix)] let (rx, tx): (ReadHalf, WriteHalf) = tokio::io::split(stream); #[cfg(windows)] diff --git a/lan-mouse-ipc/src/lib.rs b/lan-mouse-ipc/src/lib.rs index 0d0b87c..a281a52 100644 --- a/lan-mouse-ipc/src/lib.rs +++ b/lan-mouse-ipc/src/lib.rs @@ -30,6 +30,8 @@ pub enum ConnectionError { SocketPath(#[from] SocketPathError), #[error(transparent)] Io(#[from] io::Error), + #[error("connection timed out")] + Timeout, } #[derive(Debug, Error)] @@ -237,6 +239,8 @@ pub enum FrontendRequest { AuthorizeKey(String, String), /// remove fingerprint (fingerprint) RemoveAuthorizedKey(String), + /// change the hook command + UpdateEnterHook(u64, Option), } #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 9484a37..5d0f7fa 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -52,7 +52,7 @@ in { }; Service = { Type = "simple"; - ExecStart = "${cfg.package}/bin/lan-mouse --daemon"; + ExecStart = "${cfg.package}/bin/lan-mouse daemon"; }; Install.WantedBy = [ (lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target") @@ -65,7 +65,7 @@ in { config = { ProgramArguments = [ "${cfg.package}/bin/lan-mouse" - "--daemon" + "daemon" ]; KeepAlive = true; }; diff --git a/service/lan-mouse.service b/service/lan-mouse.service index 4b1e6f4..384b669 100644 --- a/service/lan-mouse.service +++ b/service/lan-mouse.service @@ -6,7 +6,7 @@ After=graphical-session.target BindsTo=graphical-session.target [Service] -ExecStart=/usr/bin/lan-mouse --daemon +ExecStart=/usr/bin/lan-mouse daemon Restart=on-failure [Install] diff --git a/src/capture_test.rs b/src/capture_test.rs index 15a3019..9654ab8 100644 --- a/src/capture_test.rs +++ b/src/capture_test.rs @@ -1,9 +1,13 @@ use crate::config::Config; +use clap::Args; use futures::StreamExt; use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position}; use input_event::{Event, KeyboardEvent}; -pub async fn run(config: Config) -> Result<(), InputCaptureError> { +#[derive(Args, Debug, Eq, PartialEq)] +pub struct TestCaptureArgs {} + +pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCaptureError> { log::info!("running input capture test"); log::info!("creating input capture"); let backend = config.capture_backend.map(|b| b.into()); diff --git a/src/client.rs b/src/client.rs index 0b36384..19ec65d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -199,6 +199,13 @@ impl ClientManager { } } + /// update the enter hook command of the client + pub(crate) fn set_enter_hook(&self, handle: ClientHandle, enter_hook: Option) { + if let Some((c, _s)) = self.clients.borrow_mut().get_mut(handle as usize) { + c.cmd = enter_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) { diff --git a/src/config.rs b/src/config.rs index 95305c7..b681e58 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,6 @@ -use clap::{Parser, ValueEnum}; +use crate::capture_test::TestCaptureArgs; +use crate::emulation_test::TestEmulationArgs; +use clap::{Parser, Subcommand, ValueEnum}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::env::{self, VarError}; @@ -10,6 +12,7 @@ use std::{collections::HashSet, io}; use thiserror::Error; use toml; +use lan_mouse_cli::CliArgs; use lan_mouse_ipc::{Position, DEFAULT_PORT}; use input_event::scancode::{ @@ -55,7 +58,7 @@ impl ConfigToml { #[derive(Parser, Debug)] #[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)] -struct CliArgs { +pub struct Args { /// the listen port for lan-mouse #[arg(short, long)] port: Option, @@ -66,31 +69,34 @@ struct CliArgs { /// non-default config file location #[arg(short, long)] - config: Option, + pub config: Option, - /// run only the service as a daemon without the frontend - #[arg(short, long)] - daemon: bool, - - /// test input capture - #[arg(long)] - test_capture: bool, - - /// test input emulation - #[arg(long)] - test_emulation: bool, + #[command(subcommand)] + pub command: Option, /// capture backend override #[arg(long)] - capture_backend: Option, + pub capture_backend: Option, /// emulation backend override #[arg(long)] - emulation_backend: Option, + pub emulation_backend: Option, /// path to non-default certificate location #[arg(long)] - cert_path: Option, + pub cert_path: Option, +} + +#[derive(Subcommand, Debug, Eq, PartialEq)] +pub enum Command { + /// test input emulation + TestEmulation(TestEmulationArgs), + /// test input capture + TestCapture(TestCaptureArgs), + /// Lan Mouse commandline interface + Cli(CliArgs), + /// run in daemon mode + Daemon, } #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)] @@ -218,8 +224,8 @@ impl Display for EmulationBackend { pub enum Frontend { #[serde(rename = "gtk")] Gtk, - #[serde(rename = "cli")] - Cli, + #[serde(rename = "none")] + None, } impl Default for Frontend { @@ -227,7 +233,7 @@ impl Default for Frontend { if cfg!(feature = "gtk") { Self::Gtk } else { - Self::Cli + Self::None } } } @@ -248,8 +254,6 @@ pub struct Config { pub port: u16, /// list of clients pub clients: Vec<(TomlClient, Position)>, - /// whether or not to run as a daemon - pub daemon: bool, /// configured release bind pub release_bind: Vec, /// test capture instead of running the app @@ -283,8 +287,7 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] = [KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt]; impl Config { - pub fn new() -> Result { - let args = CliArgs::parse(); + pub fn new(args: &Args) -> Result { const CONFIG_FILE_NAME: &str = "config.toml"; const CERT_FILE_NAME: &str = "lan-mouse.pem"; @@ -306,7 +309,7 @@ impl Config { let config_file = config_path.join(CONFIG_FILE_NAME); // --config overrules default location - let config_file = args.config.map(PathBuf::from).unwrap_or(config_file); + let config_file = args.config.clone().unwrap_or(config_file); let mut config_toml = match ConfigToml::new(&config_file) { Err(e) => { @@ -342,6 +345,7 @@ impl Config { let cert_path = args .cert_path + .clone() .or(config_toml.as_ref().and_then(|c| c.cert_path.clone())) .unwrap_or(config_path.join(CERT_FILE_NAME)); @@ -367,16 +371,14 @@ impl Config { } } - let daemon = args.daemon; - let test_capture = args.test_capture; - let test_emulation = args.test_emulation; + let test_capture = matches!(args.command, Some(Command::TestCapture(_))); + let test_emulation = matches!(args.command, Some(Command::TestEmulation(_))); Ok(Config { path: config_path, authorized_fingerprints, capture_backend, emulation_backend, - daemon, frontend, clients, port, diff --git a/src/emulation_test.rs b/src/emulation_test.rs index f439712..5b2d649 100644 --- a/src/emulation_test.rs +++ b/src/emulation_test.rs @@ -1,4 +1,5 @@ use crate::config::Config; +use clap::Args; use input_emulation::{InputEmulation, InputEmulationError}; use input_event::{Event, PointerEvent}; use std::f64::consts::PI; @@ -7,7 +8,17 @@ use std::time::{Duration, Instant}; const FREQUENCY_HZ: f64 = 1.0; const RADIUS: f64 = 100.0; -pub async fn run(config: Config) -> Result<(), InputEmulationError> { +#[derive(Args, Debug, Eq, PartialEq)] +pub struct TestEmulationArgs { + #[arg(long)] + mouse: bool, + #[arg(long)] + keyboard: bool, + #[arg(long)] + scroll: bool, +} + +pub async fn run(config: Config, _args: TestEmulationArgs) -> Result<(), InputEmulationError> { log::info!("running input emulation test"); let backend = config.emulation_backend.map(|b| b.into()); diff --git a/src/main.rs b/src/main.rs index 85b9470..33672ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ +use clap::Parser; use env_logger::Env; use input_capture::InputCaptureError; use input_emulation::InputEmulationError; use lan_mouse::{ capture_test, - config::{Config, ConfigError, Frontend}, + config::{self, Config, ConfigError, Frontend}, emulation_test, service::{Service, ServiceError}, }; +use lan_mouse_cli::CliError; use lan_mouse_ipc::{IpcError, IpcListenerCreationError}; use std::{ future::Future, @@ -30,6 +32,8 @@ enum LanMouseError { Capture(#[from] InputCaptureError), #[error(transparent)] Emulation(#[from] InputEmulationError), + #[error(transparent)] + Cli(#[from] CliError), } fn main() { @@ -45,34 +49,39 @@ fn main() { fn run() -> Result<(), LanMouseError> { // parse config file + cli args - let config = Config::new()?; - if config.test_capture { - run_async(capture_test::run(config))?; - } else if config.test_emulation { - run_async(emulation_test::run(config))?; - } else if config.daemon { - // if daemon is specified we run the service - match run_async(run_service(config)) { - Err(LanMouseError::Service(ServiceError::IpcListen( - IpcListenerCreationError::AlreadyRunning, - ))) => log::info!("service already running!"), - r => r?, - } - } else { - // otherwise start the service as a child process and - // run a frontend - let mut service = start_service()?; - run_frontend(&config)?; - #[cfg(unix)] - { - // on unix we give the service a chance to terminate gracefully - let pid = service.id() as libc::pid_t; - unsafe { - libc::kill(pid, libc::SIGINT); + let args = config::Args::parse(); + let config = config::Config::new(&args)?; + match args.command { + Some(command) => match command { + config::Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?, + config::Command::TestCapture(args) => run_async(capture_test::run(config, args))?, + config::Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?, + config::Command::Daemon => { + // if daemon is specified we run the service + match run_async(run_service(config)) { + Err(LanMouseError::Service(ServiceError::IpcListen( + IpcListenerCreationError::AlreadyRunning, + ))) => log::info!("service already running!"), + r => r?, + } } - service.wait()?; + }, + None => { + // otherwise start the service as a child process and + // run a frontend + let mut service = start_service()?; + run_frontend(&config)?; + #[cfg(unix)] + { + // on unix we give the service a chance to terminate gracefully + let pid = service.id() as libc::pid_t; + unsafe { + libc::kill(pid, libc::SIGINT); + } + service.wait()?; + } + service.kill()?; } - service.kill()?; } Ok(()) @@ -96,7 +105,7 @@ where fn start_service() -> Result { let child = Command::new(std::env::current_exe()?) .args(std::env::args().skip(1)) - .arg("--daemon") + .arg("daemon") .spawn()?; Ok(child) } @@ -118,8 +127,8 @@ fn run_frontend(config: &Config) -> Result<(), IpcError> { } #[cfg(not(feature = "gtk"))] Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"), - Frontend::Cli => { - lan_mouse_cli::run()?; + Frontend::None => { + log::warn!("no frontend available!"); } }; Ok(()) diff --git a/src/service.rs b/src/service.rs index 5ae65d8..516c571 100644 --- a/src/service.rs +++ b/src/service.rs @@ -193,6 +193,9 @@ impl Service { FrontendRequest::ResolveDns(handle) => self.resolve(handle), FrontendRequest::Sync => self.sync_frontend(), FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key), + FrontendRequest::UpdateEnterHook(handle, enter_hook) => { + self.update_enter_hook(handle, enter_hook) + } } } @@ -476,6 +479,11 @@ impl Service { self.broadcast_client(handle); } + fn update_enter_hook(&mut self, handle: ClientHandle, enter_hook: Option) { + self.client_manager.set_enter_hook(handle, enter_hook); + self.broadcast_client(handle); + } + fn broadcast_client(&mut self, handle: ClientHandle) { let event = self .client_manager