remove cli frontend in favour of cli subcommand (#278)

this removes the cli frontend entirely, replacing it with a subcommand instead
This commit is contained in:
Ferdinand Schober
2025-03-15 18:20:25 +01:00
committed by GitHub
parent 7898f2362c
commit 2f6a3629ad
15 changed files with 269 additions and 497 deletions

4
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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,

View File

@@ -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",

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,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<String>,
#[arg(long)]
port: Option<u16>,
#[arg(long)]
ips: Option<Vec<IpAddr>>,
#[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<(), 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;
}
}
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(());
}
}
}
}
}
async fn execute(&mut self, cmd: Command) -> Result<(), IpcError> {
async fn execute(cmd: CliSubcommand) -> Result<(), CliError> {
let (mut rx, mut tx) = connect_async(Some(Duration::from_millis(500))).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))
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?;
}
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?;
}
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?;
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?
}
Command::SetPort(handle, port) => {
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
self.tx.request(request).await?;
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:?}"
);
}
Command::Help => {
for cmd_type in [
CommandType::List,
CommandType::Connect,
CommandType::Disconnect,
CommandType::Activate,
CommandType::Deactivate,
CommandType::SetHost,
CommandType::SetPort,
] {
eprintln!("{}", cmd_type.usage());
break;
}
}
}
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?
}
}
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
);
}
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(())
}
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

@@ -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<Duration>,
) -> 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<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
#[cfg(windows)]

View File

@@ -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<String>),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]

View File

@@ -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;
};

View File

@@ -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]

View File

@@ -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());

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
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
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 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<u16>,
@@ -66,31 +69,34 @@ struct CliArgs {
/// non-default config file location
#[arg(short, long)]
config: Option<String>,
pub config: Option<PathBuf>,
/// 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<Command>,
/// capture backend override
#[arg(long)]
capture_backend: Option<CaptureBackend>,
pub capture_backend: Option<CaptureBackend>,
/// emulation backend override
#[arg(long)]
emulation_backend: Option<EmulationBackend>,
pub emulation_backend: Option<EmulationBackend>,
/// path to non-default certificate location
#[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)]
@@ -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<scancode::Linux>,
/// 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<Self, ConfigError> {
let args = CliArgs::parse();
pub fn new(args: &Args) -> Result<Self, ConfigError> {
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 <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) {
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,

View File

@@ -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());

View File

@@ -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,12 +49,14 @@ 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 {
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(
@@ -58,7 +64,9 @@ fn run() -> Result<(), LanMouseError> {
))) => log::info!("service already running!"),
r => r?,
}
} else {
}
},
None => {
// otherwise start the service as a child process and
// run a frontend
let mut service = start_service()?;
@@ -74,6 +82,7 @@ fn run() -> Result<(), LanMouseError> {
}
service.kill()?;
}
}
Ok(())
}
@@ -96,7 +105,7 @@ where
fn start_service() -> Result<Child, io::Error> {
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(())

View File

@@ -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<String>) {
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