From 648b2b58a4209a67e1542b700a0b2330660db87e Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Sat, 7 Feb 2026 18:36:07 +0100 Subject: [PATCH] Save config (#345) * add setters for clients and authorized keys * impl change config request * basic saving functionality * save config automatically * add TODO comment --- Cargo.lock | 1 + Cargo.toml | 1 + lan-mouse-cli/src/lib.rs | 3 ++ lan-mouse-ipc/src/lib.rs | 2 + src/client.rs | 9 +++++ src/config.rs | 83 +++++++++++++++++++++++++++++++++++++++- src/service.rs | 72 +++++++++++++++++++++++++++++----- 7 files changed, 160 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 197a5ad..b5fcf02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1872,6 +1872,7 @@ dependencies = [ "tokio", "tokio-util", "toml", + "toml_edit", "webrtc-dtls", "webrtc-util", ] diff --git a/Cargo.toml b/Cargo.toml index 5425e16..29c0097 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ shadow-rs = { version = "1.2.0", features = ["metadata"] } hickory-resolver = "0.25.2" toml = "0.8" +toml_edit = { version = "0.22", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } log = "0.4.20" env_logger = "0.11.3" diff --git a/lan-mouse-cli/src/lib.rs b/lan-mouse-cli/src/lib.rs index 26e7a05..884e928 100644 --- a/lan-mouse-cli/src/lib.rs +++ b/lan-mouse-cli/src/lib.rs @@ -71,6 +71,8 @@ enum CliSubcommand { }, /// deauthorize a public key RemoveAuthorizedKey { sha256_fingerprint: String }, + /// save configuration to file + SaveConfig, } pub async fn run(args: CliArgs) -> Result<(), CliError> { @@ -162,6 +164,7 @@ async fn execute(cmd: CliSubcommand) -> Result<(), CliError> { tx.request(FrontendRequest::RemoveAuthorizedKey(sha256_fingerprint)) .await? } + CliSubcommand::SaveConfig => tx.request(FrontendRequest::SaveConfiguration).await?, } Ok(()) } diff --git a/lan-mouse-ipc/src/lib.rs b/lan-mouse-ipc/src/lib.rs index 60c0125..a2c7112 100644 --- a/lan-mouse-ipc/src/lib.rs +++ b/lan-mouse-ipc/src/lib.rs @@ -253,6 +253,8 @@ pub enum FrontendRequest { RemoveAuthorizedKey(String), /// change the hook command UpdateEnterHook(u64, Option), + /// save config file + SaveConfiguration, } #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] diff --git a/src/client.rs b/src/client.rs index 19ec65d..b67f787 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,6 +15,15 @@ pub struct ClientManager { } impl ClientManager { + /// get all clients + pub fn clients(&self) -> Vec<(ClientConfig, ClientState)> { + self.clients + .borrow() + .iter() + .map(|(_, c)| c.clone()) + .collect::>() + } + /// add a new client to this manager pub fn add_client(&self) -> ClientHandle { self.clients.borrow_mut().insert(Default::default()) as ClientHandle diff --git a/src/config.rs b/src/config.rs index c0584cc..756c99f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,6 +11,7 @@ use std::path::{Path, PathBuf}; use std::{collections::HashSet, io}; use thiserror::Error; use toml; +use toml_edit::{self, DocumentMut}; use lan_mouse_cli::CliArgs; use lan_mouse_ipc::{DEFAULT_PORT, Position}; @@ -44,7 +45,7 @@ fn default_path() -> Result { Ok(PathBuf::from(default_path)) } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, Default)] struct ConfigToml { capture_backend: Option, emulation_backend: Option, @@ -274,6 +275,33 @@ impl From for ConfigClient { } } +impl From for TomlClient { + fn from(client: ConfigClient) -> Self { + let hostname = client.hostname; + let host_name = None; + let mut ips = client.ips.into_iter().collect::>(); + ips.sort(); + let ips = Some(ips); + let port = if client.port == DEFAULT_PORT { + None + } else { + Some(client.port) + }; + let position = Some(client.pos); + let activate_on_startup = if client.active { Some(true) } else { None }; + let enter_hook = client.enter_hook; + Self { + hostname, + host_name, + ips, + port, + position, + activate_on_startup, + enter_hook, + } + } +} + #[derive(Debug, Error)] pub enum ConfigError { #[error(transparent)] @@ -384,4 +412,57 @@ impl Config { .and_then(|c| c.release_bind.clone()) .unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned())) } + + /// set configured clients + pub fn set_clients(&mut self, clients: Vec) { + if clients.is_empty() { + return; + } + if self.config_toml.is_none() { + self.config_toml = Default::default(); + } + self.config_toml.as_mut().expect("config").clients = + Some(clients.into_iter().map(|c| c.into()).collect::>()); + } + + /// set authorized keys + pub fn set_authorized_keys(&mut self, fingerprints: HashMap) { + if fingerprints.is_empty() { + return; + } + if self.config_toml.is_none() { + self.config_toml = Default::default(); + } + self.config_toml + .as_mut() + .expect("config") + .authorized_fingerprints = Some(fingerprints); + } + + pub fn write_back(&self) -> Result<(), io::Error> { + log::info!("writing config to {:?}", &self.config_path); + /* load the current configuration file */ + let current_config = fs::read_to_string(&self.config_path)?; + let current_config = current_config.parse::().expect("fix me"); + let _current_config = + toml_edit::de::from_document::(current_config).expect("fixme"); + + /* the new config */ + let new_config = self.config_toml.clone().unwrap_or_default(); + // let new_config = toml_edit::ser::to_document::(&new_config).expect("fixme"); + let new_config = toml_edit::ser::to_string_pretty(&new_config).expect("config"); + + /* + * TODO merge documents => eventually we might want to split this up into clients configured + * via the config file and clients managed through the GUI / frontend. + * The latter should be saved to $XDG_DATA_HOME instead of $XDG_CONFIG_HOME, + * and clients configured through .config could be made permanent. + * For now we just override the config file. + */ + + /* write new config to file */ + fs::write(&self.config_path, new_config)?; + + Ok(()) + } } diff --git a/src/service.rs b/src/service.rs index f708193..ff7c22f 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,7 +1,7 @@ use crate::{ capture::{Capture, CaptureType, ICaptureEvent}, client::ClientManager, - config::Config, + config::{Config, ConfigClient}, connect::LanMouseConnection, crypto, dns::{DnsEvent, DnsResolver}, @@ -39,6 +39,8 @@ pub enum ServiceError { } pub struct Service { + /// configuration + config: Config, /// input capture capture: Capture, /// input emulation @@ -122,6 +124,7 @@ impl Service { let port = config.port(); let service = Self { + config, capture, emulation, frontend_listener, @@ -182,24 +185,73 @@ impl Service { Err(e) => return log::error!("error receiving request: {e}"), }; match request { - FrontendRequest::Activate(handle, active) => self.set_client_active(handle, active), - FrontendRequest::AuthorizeKey(desc, fp) => self.add_authorized_key(desc, fp), + FrontendRequest::Activate(handle, active) => { + self.set_client_active(handle, active); + self.save_config(); + } + FrontendRequest::AuthorizeKey(desc, fp) => { + self.add_authorized_key(desc, fp); + self.save_config(); + } FrontendRequest::ChangePort(port) => self.change_port(port), - FrontendRequest::Create => self.add_client(), - FrontendRequest::Delete(handle) => self.remove_client(handle), + FrontendRequest::Create => { + self.add_client(); + self.save_config(); + } + FrontendRequest::Delete(handle) => { + self.remove_client(handle); + self.save_config(); + } FrontendRequest::EnableCapture => self.capture.reenable(), FrontendRequest::EnableEmulation => self.emulation.reenable(), FrontendRequest::Enumerate() => self.enumerate(), - FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips), - FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host), - FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port), - FrontendRequest::UpdatePosition(handle, pos) => self.update_pos(handle, pos), + FrontendRequest::UpdateFixIps(handle, fix_ips) => { + self.update_fix_ips(handle, fix_ips); + self.save_config(); + } + FrontendRequest::UpdateHostname(handle, host) => { + self.update_hostname(handle, host); + self.save_config(); + } + FrontendRequest::UpdatePort(handle, port) => { + self.update_port(handle, port); + self.save_config(); + } + FrontendRequest::UpdatePosition(handle, pos) => { + self.update_pos(handle, pos); + self.save_config(); + } FrontendRequest::ResolveDns(handle) => self.resolve(handle), FrontendRequest::Sync => self.sync_frontend(), - FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key), + FrontendRequest::RemoveAuthorizedKey(key) => { + self.remove_authorized_key(key); + self.save_config(); + } FrontendRequest::UpdateEnterHook(handle, enter_hook) => { self.update_enter_hook(handle, enter_hook) } + FrontendRequest::SaveConfiguration => self.save_config(), + } + } + + fn save_config(&mut self) { + let clients = self.client_manager.clients(); + let clients = clients + .into_iter() + .map(|(c, s)| ConfigClient { + ips: HashSet::from_iter(c.fix_ips), + hostname: c.hostname, + port: c.port, + pos: c.pos, + active: s.active, + enter_hook: c.cmd, + }) + .collect(); + self.config.set_clients(clients); + let authorized_keys = self.authorized_keys.read().expect("lock").clone(); + self.config.set_authorized_keys(authorized_keys); + if let Err(e) = self.config.write_back() { + log::warn!("failed to write config: {e}"); } }