rework cli frontend

This commit is contained in:
Ferdinand Schober
2025-03-15 12:31:36 +01:00
parent 7898f2362c
commit d8e2c1ef02
12 changed files with 244 additions and 493 deletions

3
Cargo.lock generated
View File

@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@@ -2005,6 +2005,7 @@ dependencies = [
name = "lan-mouse-cli" name = "lan-mouse-cli"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"clap",
"futures", "futures",
"lan-mouse-ipc", "lan-mouse-ipc",
"tokio", "tokio",

View File

@@ -9,6 +9,7 @@ repository = "https://github.com/feschber/lan-mouse"
[dependencies] [dependencies]
futures = "0.3.30" futures = "0.3.30"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" } lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
clap = { version = "4.4.11", features = ["derive"] }
tokio = { version = "1.32.0", features = [ tokio = { version = "1.32.0", features = [
"io-util", "io-util",
"io-std", "io-std",

View File

@@ -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<Self, Self::Err> {
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<u16>),
Disconnect(ClientHandle),
Activate(ClientHandle),
Deactivate(ClientHandle),
List,
SetHost(ClientHandle, String),
SetPort(ClientHandle, Option<u16>),
}
impl CommandType {
pub(super) fn usage(&self) -> &'static str {
match self {
CommandType::Help => "help",
CommandType::NoCommand => "",
CommandType::Connect => "connect left|right|top|bottom <host> [<port>]",
CommandType::Disconnect => "disconnect <id>",
CommandType::Activate => "activate <id>",
CommandType::Deactivate => "deactivate <id>",
CommandType::List => "list",
CommandType::SetHost => "set-host <id> <host>",
CommandType::SetPort => "set-port <id> <host>",
}
}
}
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<Self, Self::Err> {
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<Command, CommandParseError> {
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<Command, CommandParseError> {
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<Command, CommandParseError> {
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<Command, CommandParseError> {
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<Command, CommandParseError> {
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<Command, CommandParseError> {
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))
}

View File

@@ -1,298 +1,152 @@
use clap::{Args, Parser, Subcommand};
use futures::StreamExt; use futures::StreamExt;
use tokio::{
io::{AsyncBufReadExt, BufReader},
task::LocalSet,
};
use std::io::{self, Write}; use std::{net::IpAddr, time::Duration};
use self::command::{Command, CommandType};
use lan_mouse_ipc::{ use lan_mouse_ipc::{
AsyncFrontendEventReader, AsyncFrontendRequestWriter, ClientConfig, ClientHandle, ClientState, connect_async, ClientHandle, FrontendEvent, FrontendRequest, IpcError, Position,
FrontendEvent, FrontendRequest, IpcError, DEFAULT_PORT,
}; };
mod command; #[derive(Parser, Debug, PartialEq, Eq)]
#[command(
name = "lan-mouse-cli",
about = "LanMouse CLI interface",
flatten_help = true
)]
pub struct CliArgs {
#[command(subcommand)]
command: CliSubcommand,
}
pub fn run() -> Result<(), IpcError> { #[derive(Args, Clone, Debug, PartialEq, Eq)]
let runtime = tokio::runtime::Builder::new_current_thread() struct Client {
.enable_io() #[arg(long)]
.enable_time() hostname: Option<String>,
.build()?; #[arg(long)]
runtime.block_on(LocalSet::new().run_until(async move { port: Option<u16>,
let (rx, tx) = lan_mouse_ipc::connect_async().await?; #[arg(long)]
let mut cli = Cli::new(rx, tx); ips: Option<Vec<IpAddr>>,
cli.run().await #[arg(long)]
}))?; enter_hook: Option<String>,
}
#[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<String>,
},
/// change port
SetPort { id: ClientHandle, port: u16 },
/// set position
SetPosition { id: ClientHandle, pos: Position },
/// set ips
SetIps { id: ClientHandle, ips: Vec<IpAddr> },
/// 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<(), IpcError> {
execute(args.command).await?;
Ok(()) Ok(())
} }
struct Cli { async fn execute(cmd: CliSubcommand) -> Result<(), IpcError> {
clients: Vec<(ClientHandle, ClientConfig, ClientState)>, let (mut rx, mut tx) = connect_async(Some(Duration::from_millis(500))).await?;
rx: AsyncFrontendEventReader, match cmd {
tx: AsyncFrontendRequestWriter, CliSubcommand::AddClient(Client {
} hostname,
port,
impl Cli { ips,
fn new(rx: AsyncFrontendEventReader, tx: AsyncFrontendRequestWriter) -> Cli { enter_hook,
Self { }) => {
clients: vec![], tx.request(FrontendRequest::Create).await?;
rx, while let Some(e) = rx.next().await {
tx, if let FrontendEvent::Created(handle, _, _) = e? {
} if let Some(hostname) = hostname {
} tx.request(FrontendRequest::UpdateHostname(handle, Some(hostname)))
.await?;
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;
} }
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;
} }
Some(Err(e)) => return Err(e),
None => return Ok(()),
} }
}; }
CliSubcommand::RemoveClient { id } => tx.request(FrontendRequest::Delete(id)).await?,
loop { CliSubcommand::Activate { id } => tx.request(FrontendRequest::Activate(id, true)).await?,
prompt()?; CliSubcommand::Deactivate { id } => {
tokio::select! { tx.request(FrontendRequest::Activate(id, false)).await?
line = stdin.next_line() => { }
let Some(line) = line? else { CliSubcommand::List => {
break Ok(()); tx.request(FrontendRequest::Enumerate()).await?;
}; while let Some(e) = rx.next().await {
let cmd: Command = match line.parse() { if let FrontendEvent::Enumerate(clients) = e? {
Ok(cmd) => cmd, for client in clients {
Err(e) => { println!("{client:?}");
eprintln!("{e}");
continue;
}
};
self.execute(cmd).await?;
}
event = self.rx.next() => {
if let Some(event) = event {
self.handle_event(event?);
} else {
break Ok(());
} }
} }
} }
} }
} CliSubcommand::SetHost { id, host } => {
tx.request(FrontendRequest::UpdateHostname(id, host))
async fn execute(&mut self, cmd: Command) -> Result<(), IpcError> { .await?
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());
}
}
} }
Ok(()) CliSubcommand::SetPort { id, port } => {
} tx.request(FrontendRequest::UpdatePort(id, port)).await?
}
fn find_mut( CliSubcommand::SetPosition { id, pos } => {
&mut self, tx.request(FrontendRequest::UpdatePosition(id, pos)).await?
handle: ClientHandle, }
) -> Option<&mut (ClientHandle, ClientConfig, ClientState)> { CliSubcommand::SetIps { id, ips } => {
self.clients.iter_mut().find(|(h, _, _)| *h == handle) tx.request(FrontendRequest::UpdateFixIps(id, ips)).await?
} }
CliSubcommand::EnableCapture => tx.request(FrontendRequest::EnableCapture).await?,
fn remove( CliSubcommand::EnableEmulation => tx.request(FrontendRequest::EnableEmulation).await?,
&mut self, CliSubcommand::AuthorizeKey {
handle: ClientHandle, description,
) -> Option<(ClientHandle, ClientConfig, ClientState)> { sha256_fingerprint,
let idx = self.clients.iter().position(|(h, _, _)| *h == handle); } => {
idx.map(|i| self.clients.swap_remove(i)) tx.request(FrontendRequest::AuthorizeKey(
} description,
sha256_fingerprint,
fn handle_event(&mut self, event: FrontendEvent) { ))
match event { .await?
FrontendEvent::Created(h, c, s) => { }
eprint!("client added ({h}): "); CliSubcommand::RemoveAuthorizedKey { sha256_fingerprint } => {
print_config(&c); tx.request(FrontendRequest::RemoveAuthorizedKey(sha256_fingerprint))
eprint!(" "); .await?
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
);
}
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;
}
}
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(..) => {}
} }
} }
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(()) 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);
}

View File

@@ -57,8 +57,16 @@ impl AsyncFrontendRequestWriter {
} }
pub async fn connect_async( pub async fn connect_async(
timeout: Option<Duration>,
) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> { ) -> 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)] #[cfg(unix)]
let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream); let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
#[cfg(windows)] #[cfg(windows)]

View File

@@ -30,6 +30,8 @@ pub enum ConnectionError {
SocketPath(#[from] SocketPathError), SocketPath(#[from] SocketPathError),
#[error(transparent)] #[error(transparent)]
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("connection timed out")]
Timeout,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -237,6 +239,8 @@ pub enum FrontendRequest {
AuthorizeKey(String, String), AuthorizeKey(String, String),
/// remove fingerprint (fingerprint) /// remove fingerprint (fingerprint)
RemoveAuthorizedKey(String), RemoveAuthorizedKey(String),
/// change the hook command
UpdateEnterHook(u64, Option<String>),
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]

View File

@@ -1,9 +1,13 @@
use crate::config::Config; use crate::config::Config;
use clap::Args;
use futures::StreamExt; use futures::StreamExt;
use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position}; use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
use input_event::{Event, KeyboardEvent}; 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!("running input capture test");
log::info!("creating input capture"); log::info!("creating input capture");
let backend = config.capture_backend.map(|b| b.into()); let backend = config.capture_backend.map(|b| b.into());

View File

@@ -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<String>) {
if let Some((c, _s)) = self.clients.borrow_mut().get_mut(handle as usize) {
c.cmd = enter_hook;
}
}
/// set resolving status of the client /// set resolving status of the client
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) { pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) { if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {

View File

@@ -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 serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::env::{self, VarError}; use std::env::{self, VarError};
@@ -10,6 +12,7 @@ use std::{collections::HashSet, io};
use thiserror::Error; use thiserror::Error;
use toml; use toml;
use lan_mouse_cli::CliArgs;
use lan_mouse_ipc::{Position, DEFAULT_PORT}; use lan_mouse_ipc::{Position, DEFAULT_PORT};
use input_event::scancode::{ use input_event::scancode::{
@@ -55,7 +58,7 @@ impl ConfigToml {
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)] #[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)]
struct CliArgs { pub struct Args {
/// the listen port for lan-mouse /// the listen port for lan-mouse
#[arg(short, long)] #[arg(short, long)]
port: Option<u16>, port: Option<u16>,
@@ -66,31 +69,34 @@ struct CliArgs {
/// non-default config file location /// non-default config file location
#[arg(short, long)] #[arg(short, long)]
config: Option<String>, pub config: Option<PathBuf>,
/// run only the service as a daemon without the frontend #[command(subcommand)]
#[arg(short, long)] pub command: Option<Command>,
daemon: bool,
/// test input capture
#[arg(long)]
test_capture: bool,
/// test input emulation
#[arg(long)]
test_emulation: bool,
/// capture backend override /// capture backend override
#[arg(long)] #[arg(long)]
capture_backend: Option<CaptureBackend>, pub capture_backend: Option<CaptureBackend>,
/// emulation backend override /// emulation backend override
#[arg(long)] #[arg(long)]
emulation_backend: Option<EmulationBackend>, pub emulation_backend: Option<EmulationBackend>,
/// path to non-default certificate location /// path to non-default certificate location
#[arg(long)] #[arg(long)]
cert_path: Option<PathBuf>, pub cert_path: Option<PathBuf>,
}
#[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)] #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
@@ -218,8 +224,8 @@ impl Display for EmulationBackend {
pub enum Frontend { pub enum Frontend {
#[serde(rename = "gtk")] #[serde(rename = "gtk")]
Gtk, Gtk,
#[serde(rename = "cli")] #[serde(rename = "none")]
Cli, None,
} }
impl Default for Frontend { impl Default for Frontend {
@@ -227,7 +233,7 @@ impl Default for Frontend {
if cfg!(feature = "gtk") { if cfg!(feature = "gtk") {
Self::Gtk Self::Gtk
} else { } else {
Self::Cli Self::None
} }
} }
} }
@@ -248,8 +254,6 @@ pub struct Config {
pub port: u16, pub port: u16,
/// list of clients /// list of clients
pub clients: Vec<(TomlClient, Position)>, pub clients: Vec<(TomlClient, Position)>,
/// whether or not to run as a daemon
pub daemon: bool,
/// configured release bind /// configured release bind
pub release_bind: Vec<scancode::Linux>, pub release_bind: Vec<scancode::Linux>,
/// test capture instead of running the app /// test capture instead of running the app
@@ -283,8 +287,7 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt]; [KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
impl Config { impl Config {
pub fn new() -> Result<Self, ConfigError> { pub fn new(args: &Args) -> Result<Self, ConfigError> {
let args = CliArgs::parse();
const CONFIG_FILE_NAME: &str = "config.toml"; const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem"; const CERT_FILE_NAME: &str = "lan-mouse.pem";
@@ -306,7 +309,7 @@ impl Config {
let config_file = config_path.join(CONFIG_FILE_NAME); let config_file = config_path.join(CONFIG_FILE_NAME);
// --config <file> overrules default location // --config <file> 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) { let mut config_toml = match ConfigToml::new(&config_file) {
Err(e) => { Err(e) => {
@@ -342,6 +345,7 @@ impl Config {
let cert_path = args let cert_path = args
.cert_path .cert_path
.clone()
.or(config_toml.as_ref().and_then(|c| c.cert_path.clone())) .or(config_toml.as_ref().and_then(|c| c.cert_path.clone()))
.unwrap_or(config_path.join(CERT_FILE_NAME)); .unwrap_or(config_path.join(CERT_FILE_NAME));
@@ -367,16 +371,14 @@ impl Config {
} }
} }
let daemon = args.daemon; let test_capture = matches!(args.command, Some(Command::TestCapture(_)));
let test_capture = args.test_capture; let test_emulation = matches!(args.command, Some(Command::TestEmulation(_)));
let test_emulation = args.test_emulation;
Ok(Config { Ok(Config {
path: config_path, path: config_path,
authorized_fingerprints, authorized_fingerprints,
capture_backend, capture_backend,
emulation_backend, emulation_backend,
daemon,
frontend, frontend,
clients, clients,
port, port,

View File

@@ -1,4 +1,5 @@
use crate::config::Config; use crate::config::Config;
use clap::Args;
use input_emulation::{InputEmulation, InputEmulationError}; use input_emulation::{InputEmulation, InputEmulationError};
use input_event::{Event, PointerEvent}; use input_event::{Event, PointerEvent};
use std::f64::consts::PI; use std::f64::consts::PI;
@@ -7,7 +8,17 @@ use std::time::{Duration, Instant};
const FREQUENCY_HZ: f64 = 1.0; const FREQUENCY_HZ: f64 = 1.0;
const RADIUS: f64 = 100.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"); log::info!("running input emulation test");
let backend = config.emulation_backend.map(|b| b.into()); let backend = config.emulation_backend.map(|b| b.into());

View File

@@ -1,9 +1,10 @@
use clap::Parser;
use env_logger::Env; use env_logger::Env;
use input_capture::InputCaptureError; use input_capture::InputCaptureError;
use input_emulation::InputEmulationError; use input_emulation::InputEmulationError;
use lan_mouse::{ use lan_mouse::{
capture_test, capture_test,
config::{Config, ConfigError, Frontend}, config::{self, Config, ConfigError, Frontend},
emulation_test, emulation_test,
service::{Service, ServiceError}, service::{Service, ServiceError},
}; };
@@ -45,34 +46,39 @@ fn main() {
fn run() -> Result<(), LanMouseError> { fn run() -> Result<(), LanMouseError> {
// parse config file + cli args // parse config file + cli args
let config = Config::new()?; let args = config::Args::parse();
if config.test_capture { let config = config::Config::new(&args)?;
run_async(capture_test::run(config))?; match args.command {
} else if config.test_emulation { Some(command) => match command {
run_async(emulation_test::run(config))?; config::Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?,
} else if config.daemon { config::Command::TestCapture(args) => run_async(capture_test::run(config, args))?,
// if daemon is specified we run the service config::Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?,
match run_async(run_service(config)) { config::Command::Daemon => {
Err(LanMouseError::Service(ServiceError::IpcListen( // if daemon is specified we run the service
IpcListenerCreationError::AlreadyRunning, match run_async(run_service(config)) {
))) => log::info!("service already running!"), Err(LanMouseError::Service(ServiceError::IpcListen(
r => r?, IpcListenerCreationError::AlreadyRunning,
} ))) => log::info!("service already running!"),
} else { r => r?,
// 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()?; },
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(()) Ok(())
@@ -96,7 +102,7 @@ where
fn start_service() -> Result<Child, io::Error> { fn start_service() -> Result<Child, io::Error> {
let child = Command::new(std::env::current_exe()?) let child = Command::new(std::env::current_exe()?)
.args(std::env::args().skip(1)) .args(std::env::args().skip(1))
.arg("--daemon") .arg("daemon")
.spawn()?; .spawn()?;
Ok(child) Ok(child)
} }
@@ -118,9 +124,7 @@ fn run_frontend(config: &Config) -> Result<(), IpcError> {
} }
#[cfg(not(feature = "gtk"))] #[cfg(not(feature = "gtk"))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"), Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::Cli => { Frontend::None => {}
lan_mouse_cli::run()?;
}
}; };
Ok(()) Ok(())
} }

View File

@@ -193,6 +193,9 @@ impl Service {
FrontendRequest::ResolveDns(handle) => self.resolve(handle), FrontendRequest::ResolveDns(handle) => self.resolve(handle),
FrontendRequest::Sync => self.sync_frontend(), FrontendRequest::Sync => self.sync_frontend(),
FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key), 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); self.broadcast_client(handle);
} }
fn update_enter_hook(&mut self, handle: ClientHandle, enter_hook: Option<String>) {
self.client_manager.set_enter_hook(handle, enter_hook);
self.broadcast_client(handle);
}
fn broadcast_client(&mut self, handle: ClientHandle) { fn broadcast_client(&mut self, handle: ClientHandle) {
let event = self let event = self
.client_manager .client_manager