feat: simplify and change configuration (#279)

*breaking change*
this changes the configuration syntax, allowing for an unlimited amount of configured clients.
Also a first step towards enabling a "save config" feature.
This commit is contained in:
Ferdinand Schober
2025-03-15 18:45:19 +01:00
committed by GitHub
parent 2f6a3629ad
commit 92f652df2e
10 changed files with 226 additions and 245 deletions

View File

@@ -24,33 +24,50 @@ use shadow_rs::shadow;
shadow!(build);
#[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml {
pub capture_backend: Option<CaptureBackend>,
pub emulation_backend: Option<EmulationBackend>,
pub port: Option<u16>,
pub frontend: Option<Frontend>,
pub release_bind: Option<Vec<scancode::Linux>>,
pub cert_path: Option<PathBuf>,
pub left: Option<TomlClient>,
pub right: Option<TomlClient>,
pub top: Option<TomlClient>,
pub bottom: Option<TomlClient>,
pub authorized_fingerprints: Option<HashMap<String, String>>,
const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem";
fn default_path() -> Result<PathBuf, VarError> {
#[cfg(unix)]
let default_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/")
};
#[cfg(not(unix))]
let default_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\")
};
Ok(PathBuf::from(default_path))
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct TomlClient {
pub hostname: Option<String>,
pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>,
pub activate_on_startup: Option<bool>,
pub enter_hook: Option<String>,
#[derive(Serialize, Deserialize, Debug)]
struct ConfigToml {
capture_backend: Option<CaptureBackend>,
emulation_backend: Option<EmulationBackend>,
port: Option<u16>,
release_bind: Option<Vec<scancode::Linux>>,
cert_path: Option<PathBuf>,
clients: Vec<TomlClient>,
authorized_fingerprints: Option<HashMap<String, String>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct TomlClient {
hostname: Option<String>,
host_name: Option<String>,
ips: Option<Vec<IpAddr>>,
port: Option<u16>,
pos: Option<Position>,
activate_on_startup: Option<bool>,
enter_hook: Option<String>,
}
impl ConfigToml {
pub fn new(path: &Path) -> Result<ConfigToml, ConfigError> {
fn new(path: &Path) -> Result<ConfigToml, ConfigError> {
let config = fs::read_to_string(path)?;
Ok(toml::from_str::<_>(&config)?)
}
@@ -58,36 +75,33 @@ impl ConfigToml {
#[derive(Parser, Debug)]
#[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)]
pub struct Args {
struct Args {
/// the listen port for lan-mouse
#[arg(short, long)]
port: Option<u16>,
/// the frontend to use [cli | gtk]
#[arg(short, long)]
frontend: Option<Frontend>,
/// non-default config file location
#[arg(short, long)]
pub config: Option<PathBuf>,
#[command(subcommand)]
pub command: Option<Command>,
config: Option<PathBuf>,
/// capture backend override
#[arg(long)]
pub capture_backend: Option<CaptureBackend>,
capture_backend: Option<CaptureBackend>,
/// emulation backend override
#[arg(long)]
pub emulation_backend: Option<EmulationBackend>,
emulation_backend: Option<EmulationBackend>,
/// path to non-default certificate location
#[arg(long)]
pub cert_path: Option<PathBuf>,
cert_path: Option<PathBuf>,
/// subcommands
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand, Debug, Eq, PartialEq)]
#[derive(Subcommand, Clone, Debug, Eq, PartialEq)]
pub enum Command {
/// test input emulation
TestEmulation(TestEmulationArgs),
@@ -220,48 +234,16 @@ impl Display for EmulationBackend {
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, ValueEnum)]
pub enum Frontend {
#[serde(rename = "gtk")]
Gtk,
#[serde(rename = "none")]
None,
}
impl Default for Frontend {
fn default() -> Self {
if cfg!(feature = "gtk") {
Self::Gtk
} else {
Self::None
}
}
}
#[derive(Debug)]
pub struct Config {
/// the path to the configuration file used
pub path: PathBuf,
/// public key fingerprints authorized for connection
pub authorized_fingerprints: HashMap<String, String>,
/// optional input-capture backend override
pub capture_backend: Option<CaptureBackend>,
/// optional input-emulation backend override
pub emulation_backend: Option<EmulationBackend>,
/// the frontend to use
pub frontend: Frontend,
/// the port to use (initially)
pub port: u16,
/// list of clients
pub clients: Vec<(TomlClient, Position)>,
/// configured release bind
pub release_bind: Vec<scancode::Linux>,
/// test capture instead of running the app
pub test_capture: bool,
/// test emulation instead of running the app
pub test_emulation: bool,
/// path to the tls certificate to use
pub cert_path: PathBuf,
/// command line arguments
args: Args,
/// path to the certificate file used
cert_path: PathBuf,
/// path to the config file used
config_path: PathBuf,
/// the (optional) toml config and it's path
config_toml: Option<ConfigToml>,
}
pub struct ConfigClient {
@@ -273,6 +255,25 @@ pub struct ConfigClient {
pub enter_hook: Option<String>,
}
impl From<TomlClient> for ConfigClient {
fn from(toml: TomlClient) -> Self {
let active = toml.activate_on_startup.unwrap_or(false);
let enter_hook = toml.enter_hook;
let hostname = toml.hostname;
let ips = HashSet::from_iter(toml.ips.into_iter().flatten());
let port = toml.port.unwrap_or(DEFAULT_PORT);
let pos = toml.pos.unwrap_or_default();
Self {
ips,
hostname,
port,
pos,
active,
enter_hook,
}
}
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error(transparent)]
@@ -287,133 +288,99 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
impl Config {
pub fn new(args: &Args) -> Result<Self, ConfigError> {
const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem";
#[cfg(unix)]
let config_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/")
};
#[cfg(not(unix))]
let config_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\")
};
let config_path = PathBuf::from(config_path);
let config_file = config_path.join(CONFIG_FILE_NAME);
pub fn new() -> Result<Self, ConfigError> {
let args = Args::parse();
// --config <file> overrules default location
let config_file = args.config.clone().unwrap_or(config_file);
let config_path = args
.config
.clone()
.unwrap_or(default_path()?.join(CONFIG_FILE_NAME));
let mut config_toml = match ConfigToml::new(&config_file) {
let config_toml = match ConfigToml::new(&config_path) {
Err(e) => {
log::warn!("{config_file:?}: {e}");
log::warn!("{config_path:?}: {e}");
log::warn!("Continuing without config file ...");
None
}
Ok(c) => Some(c),
};
let frontend_arg = args.frontend;
let frontend_cfg = config_toml.as_ref().and_then(|c| c.frontend);
let frontend = frontend_arg.or(frontend_cfg).unwrap_or_default();
let port = args
.port
.or(config_toml.as_ref().and_then(|c| c.port))
.unwrap_or(DEFAULT_PORT);
log::debug!("{config_toml:?}");
let release_bind = config_toml
.as_ref()
.and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
let capture_backend = args
.capture_backend
.or(config_toml.as_ref().and_then(|c| c.capture_backend));
let emulation_backend = args
.emulation_backend
.or(config_toml.as_ref().and_then(|c| c.emulation_backend));
// --cert-path <file> overrules default location
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));
let authorized_fingerprints = config_toml
.as_mut()
.and_then(|c| std::mem::take(&mut c.authorized_fingerprints))
.unwrap_or_default();
let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml {
if let Some(c) = config_toml.right {
clients.push((c, Position::Right))
}
if let Some(c) = config_toml.left {
clients.push((c, Position::Left))
}
if let Some(c) = config_toml.top {
clients.push((c, Position::Top))
}
if let Some(c) = config_toml.bottom {
clients.push((c, Position::Bottom))
}
}
let test_capture = matches!(args.command, Some(Command::TestCapture(_)));
let test_emulation = matches!(args.command, Some(Command::TestEmulation(_)));
.unwrap_or(default_path()?.join(CERT_FILE_NAME));
Ok(Config {
path: config_path,
authorized_fingerprints,
capture_backend,
emulation_backend,
frontend,
clients,
port,
release_bind,
test_capture,
test_emulation,
args,
cert_path,
config_path,
config_toml,
})
}
pub fn get_clients(&self) -> Vec<ConfigClient> {
self.clients
.iter()
.map(|(c, pos)| {
let port = c.port.unwrap_or(DEFAULT_PORT);
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
HashSet::from_iter(ips.iter().cloned())
} else {
HashSet::new()
};
let hostname = match &c.hostname {
Some(h) => Some(h.clone()),
None => c.host_name.clone(),
};
let active = c.activate_on_startup.unwrap_or(false);
let enter_hook = c.enter_hook.clone();
ConfigClient {
ips,
hostname,
port,
pos: *pos,
active,
enter_hook,
}
})
/// the command to run
pub fn command(&self) -> Option<Command> {
self.args.command.clone()
}
pub fn config_path(&self) -> &Path {
&self.config_path
}
/// public key fingerprints authorized for connection
pub fn authorized_fingerprints(&self) -> HashMap<String, String> {
self.config_toml
.as_ref()
.and_then(|c| c.authorized_fingerprints.clone())
.unwrap_or_default()
}
/// path to certificate
pub fn cert_path(&self) -> &Path {
&self.cert_path
}
/// optional input-capture backend override
pub fn capture_backend(&self) -> Option<CaptureBackend> {
self.args
.capture_backend
.or(self.config_toml.as_ref().and_then(|c| c.capture_backend))
}
/// optional input-emulation backend override
pub fn emulation_backend(&self) -> Option<EmulationBackend> {
self.args
.emulation_backend
.or(self.config_toml.as_ref().and_then(|c| c.emulation_backend))
}
/// the port to use (initially)
pub fn port(&self) -> u16 {
self.args
.port
.or(self.config_toml.as_ref().and_then(|c| c.port))
.unwrap_or(DEFAULT_PORT)
}
/// list of configured clients
pub fn clients(&self) -> Vec<ConfigClient> {
self.config_toml
.as_ref()
.map(|c| c.clients.clone())
.into_iter()
.flatten()
.map(From::<TomlClient>::from)
.collect()
}
/// release bind for returning control to the host
pub fn release_bind(&self) -> Vec<scancode::Linux> {
self.config_toml
.as_ref()
.and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()))
}
}