From 9f916a9ab36aa5bbcceafd16bc9402be59db0c61 Mon Sep 17 00:00:00 2001 From: Antoine POPINEAU Date: Sun, 29 Sep 2024 14:39:42 +0200 Subject: [PATCH 1/5] Allow configuration through a TOML file (#156). --- Cargo.lock | 10 +- Cargo.toml | 2 + src/config/file.rs | 78 ++++++++ src/config/mod.rs | 2 + src/config/parser.rs | 333 ++++++++++++++++++++++++++++++++++ src/greeter.rs | 269 ++++----------------------- src/integration/common/mod.rs | 4 +- src/main.rs | 1 + src/ui/sessions.rs | 2 +- src/ui/util.rs | 22 +-- 10 files changed, 476 insertions(+), 247 deletions(-) create mode 100644 src/config/file.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/parser.rs diff --git a/Cargo.lock b/Cargo.lock index 1bdcb44..c8ade4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1279,18 +1279,18 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -1732,9 +1732,11 @@ dependencies = [ "ratatui", "rust-embed", "rust-ini", + "serde", "smart-default", "tempfile", "tokio", + "toml 0.8.19", "tracing", "tracing-appender", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index daaa08c..dbcc69f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,8 @@ tracing-appender = "0.2.3" tracing-subscriber = "0.3.18" tracing = "0.1.40" utmp-rs = "0.3.0" +serde = { version = "1.0.210", features = ["serde_derive"] } +toml = "0.8.19" [profile.release] lto = true diff --git a/src/config/file.rs b/src/config/file.rs new file mode 100644 index 0000000..a5f3a78 --- /dev/null +++ b/src/config/file.rs @@ -0,0 +1,78 @@ +use std::{error::Error, path::PathBuf}; + +use serde::Deserialize; + +use crate::Greeter; + +#[derive(Debug, Default, Deserialize)] +pub struct FileConfig { + #[serde(default)] + pub defaults: Defaults, + #[serde(default)] + pub sessions: Sessions, + #[serde(default)] + pub remember: Remember, + #[serde(default)] + pub ui: Ui, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Defaults { + pub debug: Option, + pub command: Option, + pub env: Option>, + pub user_min_uid: Option, + pub user_max_uid: Option, + #[serde(default)] + pub power_no_setsid: bool, + pub shutdown_command: Option, + pub reboot_command: Option, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Sessions { + pub wayland_paths: Option>, + pub wayland_wrapper: Option, + pub x11_paths: Option>, + pub x11_wrapper: Option, + #[serde(default)] + pub x11_wrapper_disabled: bool, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Remember { + #[serde(default)] + pub last_user: bool, + #[serde(default)] + pub last_session: bool, + #[serde(default)] + pub last_user_session: bool, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Ui { + pub greeting: Option, + #[serde(default)] + pub use_issue: bool, + #[serde(default)] + pub show_time: bool, + pub time_format: Option, + #[serde(default)] + pub show_user_menu: bool, + #[serde(default)] + pub show_asterisks: bool, + pub asterisks_char: Option, + pub width: Option, + pub window_padding: Option, + pub container_padding: Option, + pub prompt_padding: Option, + pub command_f_key: Option, + pub sessions_f_key: Option, + pub power_f_key: Option, +} + +impl Greeter { + pub fn parse_config_file(&mut self, file: &str) -> Result> { + Ok(toml::from_str::(file)?) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..d6a2e9b --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,2 @@ +pub mod file; +pub mod parser; diff --git a/src/config/parser.rs b/src/config/parser.rs new file mode 100644 index 0000000..cb6316c --- /dev/null +++ b/src/config/parser.rs @@ -0,0 +1,333 @@ +use std::{env, error::Error}; + +use chrono::format::{Item, StrftimeItems}; + +use crate::{ + info::{get_issue, get_min_max_uids, get_users}, + power::PowerOption, + ui::{ + common::menu::Menu, + power::Power, + sessions::{SessionSource, SessionType}, + }, + Greeter, SecretDisplay, +}; + +const DEFAULT_LOG_FILE: &str = "/tmp/tuigreet.log"; +const DEFAULT_ASTERISKS_CHARS: &str = "*"; +// `startx` wants an absolute path to the executable as a first argument. +// We don't want to resolve the session command in the greeter though, so it should be additionally wrapped with a known noop command (like `/usr/bin/env`). +pub const DEFAULT_XSESSION_WRAPPER: &str = "startx /usr/bin/env"; + +impl Greeter { + pub fn parse_debug(&mut self) { + let debug = self.config.defaults.debug.clone(); + + if self.config().opt_present("debug") || debug.is_some() { + self.debug = true; + + self.logfile = match self.config().opt_str("debug").or(debug) { + Some(file) => file.to_string(), + None => DEFAULT_LOG_FILE.to_string(), + }; + } + } + + pub fn parse_greeting(&mut self) -> Result<(), Box> { + let has_greeting = self.config().opt_present("greeting") || self.config.ui.greeting.is_some(); + let has_issue = self.config().opt_present("issue") || self.config.ui.use_issue; + + if has_greeting && has_issue { + return Err("Only one of --issue and --greeting may be used at the same time".into()); + } + + self.greeting = self.option("greeting").or_else(|| self.config.ui.greeting.clone()); + + if has_issue { + self.greeting = get_issue(); + } + + Ok(()) + } + + pub fn parse_asterisks(&mut self) -> Result<(), Box> { + let has_asterisks = self.config().opt_present("asterisks") || self.config.ui.show_asterisks; + + if has_asterisks { + let asterisk = if let Some(value) = self.config().opt_str("asterisks-char").or_else(|| self.config.ui.asterisks_char.map(|c| c.to_string())) { + if value.chars().count() < 1 { + return Err("--asterisks-char must have at least one character as its value".into()); + } + + value + } else { + DEFAULT_ASTERISKS_CHARS.to_string() + }; + + self.secret_display = SecretDisplay::Character(asterisk); + } + + Ok(()) + } + + pub fn parse_default_command(&mut self) -> Result<(), Box> { + // If the `--cmd` argument is provided, it will override the selected session. + if let Some(command) = self.option("cmd").or_else(|| self.config.defaults.command.clone()) { + let envs = self.options_multi("env").or_else(|| self.config.defaults.env.clone()); + + if let Some(envs) = &envs { + for env in envs { + if !env.contains('=') { + return Err(format!("malformed environment variable definition for '{env}'").into()); + } + } + } + + self.session_source = SessionSource::DefaultCommand(command, envs); + } + + Ok(()) + } + + pub fn parse_sessions(&mut self) { + if let Some(dirs) = self.option("sessions") { + self.session_paths.extend(env::split_paths(&dirs).map(|dir| (dir, SessionType::Wayland))); + } else if let Some(dirs) = self.config.sessions.wayland_paths.clone() { + self.session_paths.extend(dirs.into_iter().map(|dir| (dir, SessionType::Wayland))); + } + + if let Some(dirs) = self.option("xsessions") { + self.session_paths.extend(env::split_paths(&dirs).map(|dir| (dir, SessionType::X11))); + } else if let Some(dirs) = self.config.sessions.x11_paths.clone() { + self.session_paths.extend(dirs.into_iter().map(|dir| (dir, SessionType::X11))); + } + + if self.option("session-wrapper").is_some() || self.config.sessions.wayland_wrapper.is_some() { + self.session_wrapper = self.option("session-wrapper").or_else(|| self.config.sessions.wayland_wrapper.clone()); + } + + if !self.config().opt_present("no-xsession-wrapper") && self.config.sessions.x11_wrapper_disabled { + self.xsession_wrapper = self + .option("xsession-wrapper") + .or_else(|| self.config.sessions.x11_wrapper.clone()) + .or_else(|| Some(DEFAULT_XSESSION_WRAPPER.to_string())); + } + } + + pub fn parse_time(&mut self) -> Result<(), Box> { + self.time = self.config().opt_present("time") || self.config.ui.show_time; + + if let Some(format) = self.config().opt_str("time-format").or_else(|| self.config.ui.time_format.clone()) { + if StrftimeItems::new(&format).any(|item| item == Item::Error) { + return Err("Invalid strftime format provided in --time-format".into()); + } + + self.time_format = Some(format); + } + + Ok(()) + } + + pub fn parse_menus(&mut self) -> Result<(), Box> { + if self.config().opt_present("user-menu") || self.config.ui.show_user_menu { + self.user_menu = true; + + let min_uid = self.config().opt_str("user-menu-min-uid").and_then(|uid| uid.parse::().ok()).or(self.config.defaults.user_min_uid); + let max_uid = self.config().opt_str("user-menu-max-uid").and_then(|uid| uid.parse::().ok()).or(self.config.defaults.user_max_uid); + let (min_uid, max_uid) = get_min_max_uids(min_uid, max_uid); + + tracing::info!("min/max UIDs are {}/{}", min_uid, max_uid); + + if min_uid >= max_uid { + return Err("Minimum UID ({min_uid}) must be less than maximum UID ({max_uid})".into()); + } + + self.users = Menu { + title: fl!("title_users"), + options: get_users(min_uid, max_uid), + selected: 0, + }; + + tracing::info!("found {} users", self.users.options.len()); + } + + Ok(()) + } + + pub fn parse_remembers(&mut self) -> Result<(), Box> { + let has_remember = self.config().opt_present("remember") || self.config.remember.last_user; + let has_remember_session = self.config().opt_present("remember-session") || self.config.remember.last_session; + let has_remember_user_session = self.config().opt_present("remember-user-session") || self.config.remember.last_user_session; + + if has_remember_session && has_remember_user_session { + return Err("Only one of --remember-session and --remember-user-session may be used at the same time".into()); + } + if has_remember_user_session && !has_remember { + return Err("--remember-session must be used with --remember".into()); + } + + self.remember = has_remember; + self.remember_session = has_remember_session; + self.remember_user_session = has_remember_user_session; + + Ok(()) + } + + pub fn parse_power(&mut self) { + self.powers.options.push(Power { + action: PowerOption::Shutdown, + label: fl!("shutdown"), + command: self.config().opt_str("power-shutdown").or_else(|| self.config.defaults.shutdown_command.clone()), + }); + + self.powers.options.push(Power { + action: PowerOption::Reboot, + label: fl!("reboot"), + command: self.config().opt_str("power-reboot").or_else(|| self.config.defaults.reboot_command.clone()), + }); + + self.power_setsid = !(self.config().opt_present("power-no-setsid") || self.config.defaults.power_no_setsid); + } + + pub fn parse_keybinds(&mut self) -> Result<(), Box> { + self.kb_command = self.config().opt_str("kb-command").and_then(|i| i.parse::().ok()).or(self.config.ui.command_f_key).unwrap_or(2); + self.kb_sessions = self.config().opt_str("kb-sessions").and_then(|i| i.parse::().ok()).or(self.config.ui.sessions_f_key).unwrap_or(3); + self.kb_power = self.config().opt_str("kb-power").and_then(|i| i.parse::().ok()).or(self.config.ui.power_f_key).unwrap_or(12); + + if self.kb_command == self.kb_sessions || self.kb_sessions == self.kb_power || self.kb_power == self.kb_command { + return Err("keybindings must all be distinct".into()); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{config::file::FileConfig, ui::sessions::SessionSource, Greeter, SecretDisplay}; + + #[tokio::test] + async fn test_command_line_arguments() { + let table: &[(&[&str], _, Option)] = &[ + // No arguments + (&[], true, None), + // Valid combinations + (&["--cmd", "hello"], true, None), + ( + &[ + "--cmd", + "uname", + "--env", + "A=B", + "--env", + "C=D=E", + "--asterisks", + "--asterisks-char", + ".", + "--issue", + "--time", + "--prompt-padding", + "0", + "--window-padding", + "1", + "--container-padding", + "12", + "--user-menu", + ], + true, + Some(|greeter| { + assert!(matches!(&greeter.session_source, SessionSource::DefaultCommand(cmd, Some(env)) if cmd == "uname" && env.len() == 2)); + + if let SessionSource::DefaultCommand(_, Some(env)) = &greeter.session_source { + assert_eq!(env[0], "A=B"); + assert_eq!(env[1], "C=D=E"); + } + + assert!(matches!(&greeter.secret_display, SecretDisplay::Character(c) if c == ".")); + assert_eq!(greeter.prompt_padding(), 0); + assert_eq!(greeter.window_padding(), 1); + assert_eq!(greeter.container_padding(), 13); + assert_eq!(greeter.user_menu, true); + assert!(matches!(greeter.xsession_wrapper.as_deref(), Some("startx /usr/bin/env"))); + }), + ), + ( + &["--xsession-wrapper", "mywrapper.sh"], + true, + Some(|greeter| { + assert!(matches!(greeter.xsession_wrapper.as_deref(), Some("mywrapper.sh"))); + }), + ), + ( + &["--no-xsession-wrapper"], + true, + Some(|greeter| { + assert!(matches!(greeter.xsession_wrapper, None)); + }), + ), + // Invalid combinations + (&["--remember-session", "--remember-user-session"], false, None), + (&["--asterisk-char", ""], false, None), + (&["--remember-user-session"], false, None), + (&["--min-uid", "10000", "--max-uid", "5000"], false, None), + (&["--issue", "--greeting", "Hello, world!"], false, None), + (&["--kb-command", "2", "--kb-sessions", "2"], false, None), + (&["--time-format", "%i %"], false, None), + (&["--cmd", "cmd", "--env"], false, None), + (&["--cmd", "cmd", "--env", "A"], false, None), + ]; + + for (opts, valid, check) in table { + let mut greeter = Greeter::default(); + + match valid { + true => { + assert!(matches!(greeter.parse_options(*opts).await, Ok(())), "{:?} cannot be parsed", opts); + + if let Some(check) = check { + check(&greeter); + } + } + + false => assert!(matches!(greeter.parse_options(*opts).await, Err(_))), + } + } + } + + #[tokio::test] + async fn command_and_env() { + let table: &[(&[&str], fn(&mut FileConfig), fn(&Greeter))] = &[ + ( + &["--cmd", "mycommand", "--env", "A=b", "--env", "C=d"], + |file: &mut FileConfig| { + file.defaults.command = Some("secondcommand".to_string()); + file.defaults.env = Some(vec!["X=y".to_string()]); + }, + |greeter| { + assert!(matches!(&greeter.session_source, SessionSource::DefaultCommand(cmd, Some(env)) if cmd == "mycommand" && env.len() == 2)); + }, + ), + ( + &[], + |file: &mut FileConfig| { + file.defaults.command = Some("secondcommand".to_string()); + file.defaults.env = Some(vec!["X=y".to_string()]); + }, + |greeter| { + assert!(matches!(&greeter.session_source, SessionSource::DefaultCommand(cmd, Some(env)) if cmd == "secondcommand" && env.len() == 1 && env.first().unwrap() == "X=y")); + }, + ), + ]; + + for (opts, file, check) in table { + let mut greeter = Greeter::default(); + greeter.config = FileConfig::default(); + + file(&mut greeter.config); + + assert!(matches!(greeter.parse_options(*opts).await, Ok(())), "{:?} cannot be parsed", opts); + + check(&greeter); + } + } +} diff --git a/src/greeter.rs b/src/greeter.rs index 9b74ce4..73616d7 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -4,15 +4,13 @@ use std::{ error::Error, ffi::OsStr, fmt::{self, Display}, + fs, path::PathBuf, process, sync::Arc, }; -use chrono::{ - format::{Item, StrftimeItems}, - Locale, -}; +use chrono::Locale; use getopts::{Matches, Options}; use i18n_embed::DesktopLanguageRequester; use tokio::{ @@ -22,9 +20,9 @@ use tokio::{ use zeroize::Zeroize; use crate::{ + config::{file::FileConfig, parser::DEFAULT_XSESSION_WRAPPER}, event::Event, - info::{get_issue, get_last_command, get_last_session_path, get_last_user_command, get_last_user_name, get_last_user_session, get_last_user_username, get_min_max_uids, get_sessions, get_users}, - power::PowerOption, + info::{get_last_command, get_last_session_path, get_last_user_command, get_last_user_name, get_last_user_session, get_last_user_username, get_sessions}, ui::{ common::{masked::MaskedString, menu::Menu, style::Theme}, power::Power, @@ -33,12 +31,7 @@ use crate::{ }, }; -const DEFAULT_LOG_FILE: &str = "/tmp/tuigreet.log"; const DEFAULT_LOCALE: Locale = Locale::en_US; -const DEFAULT_ASTERISKS_CHARS: &str = "*"; -// `startx` wants an absolute path to the executable as a first argument. -// We don't want to resolve the session command in the greeter though, so it should be additionally wrapped with a known noop command (like `/usr/bin/env`). -const DEFAULT_XSESSION_WRAPPER: &str = "startx /usr/bin/env"; #[derive(Debug, Copy, Clone)] pub enum AuthStatus { @@ -100,12 +93,14 @@ pub enum GreetAlign { #[derive(SmartDefault)] pub struct Greeter { + pub opts: Option, + pub config: FileConfig, + pub debug: bool, pub logfile: String, #[default(DEFAULT_LOCALE)] pub locale: Locale, - pub config: Option, pub socket: String, pub stream: Option>>, pub events: Option>, @@ -224,6 +219,25 @@ impl Greeter { process::exit(1); } + greeter.config = if let Some(config_file) = greeter.config().opt_str("config") { + if let Ok(config) = fs::read_to_string(config_file) { + match greeter.parse_config_file(&config) { + Ok(config) => config, + Err(err) => { + eprintln!("{err}"); + print_usage(Greeter::options()); + + process::exit(1); + } + } + } else { + // TODO: add error here. + FileConfig::default() + } + } else { + FileConfig::default() + }; + greeter.connect().await; } @@ -328,7 +342,7 @@ impl Greeter { } pub fn config(&self) -> &Matches { - self.config.as_ref().unwrap() + self.opts.as_ref().unwrap() } pub async fn stream(&self) -> RwLockWriteGuard<'_, UnixStream> { @@ -424,6 +438,7 @@ impl Greeter { opts.optflag("h", "help", "show this usage information"); opts.optflag("v", "version", "print version information"); + opts.optopt("", "config", "Path to tuigreet's configuration file", "FILE"); opts.optflagopt("d", "debug", "enable debug logging to the provided file, or to /tmp/tuigreet.log", "FILE"); opts.optopt("c", "cmd", "command to run", "COMMAND"); opts.optmulti("", "env", "environment variables to run the default session with (can appear more than once)", "KEY=VALUE"); @@ -474,7 +489,7 @@ impl Greeter { { let opts = Greeter::options(); - self.config = match opts.parse(args) { + self.opts = match opts.parse(args) { Ok(matches) => Some(matches), Err(err) => return Err(err.into()), }; @@ -488,18 +503,16 @@ impl Greeter { process::exit(0); } - if self.config().opt_present("debug") { - self.debug = true; - - self.logfile = match self.config().opt_str("debug") { - Some(file) => file.to_string(), - None => DEFAULT_LOG_FILE.to_string(), - } - } - - if self.config().opt_present("issue") && self.config().opt_present("greeting") { - return Err("Only one of --issue and --greeting may be used at the same time".into()); - } + self.parse_debug(); + self.parse_greeting()?; + self.parse_asterisks()?; + self.parse_default_command()?; + self.parse_sessions(); + self.parse_time()?; + self.parse_menus()?; + self.parse_remembers()?; + self.parse_power(); + self.parse_keybinds()?; if self.config().opt_present("theme") { if let Some(spec) = self.config().opt_str("theme") { @@ -507,121 +520,6 @@ impl Greeter { } } - if self.config().opt_present("asterisks") { - let asterisk = if let Some(value) = self.config().opt_str("asterisks-char") { - if value.chars().count() < 1 { - return Err("--asterisks-char must have at least one character as its value".into()); - } - - value - } else { - DEFAULT_ASTERISKS_CHARS.to_string() - }; - - self.secret_display = SecretDisplay::Character(asterisk); - } - - self.time = self.config().opt_present("time"); - - if let Some(format) = self.config().opt_str("time-format") { - if StrftimeItems::new(&format).any(|item| item == Item::Error) { - return Err("Invalid strftime format provided in --time-format".into()); - } - - self.time_format = Some(format); - } - - if self.config().opt_present("user-menu") { - self.user_menu = true; - - let min_uid = self.config().opt_str("user-menu-min-uid").and_then(|uid| uid.parse::().ok()); - let max_uid = self.config().opt_str("user-menu-max-uid").and_then(|uid| uid.parse::().ok()); - let (min_uid, max_uid) = get_min_max_uids(min_uid, max_uid); - - tracing::info!("min/max UIDs are {}/{}", min_uid, max_uid); - - if min_uid >= max_uid { - return Err("Minimum UID ({min_uid}) must be less than maximum UID ({max_uid})".into()); - } - - self.users = Menu { - title: fl!("title_users"), - options: get_users(min_uid, max_uid), - selected: 0, - }; - - tracing::info!("found {} users", self.users.options.len()); - } - - if self.config().opt_present("remember-session") && self.config().opt_present("remember-user-session") { - return Err("Only one of --remember-session and --remember-user-session may be used at the same time".into()); - } - if self.config().opt_present("remember-user-session") && !self.config().opt_present("remember") { - return Err("--remember-session must be used with --remember".into()); - } - - self.remember = self.config().opt_present("remember"); - self.remember_session = self.config().opt_present("remember-session"); - self.remember_user_session = self.config().opt_present("remember-user-session"); - self.greeting = self.option("greeting"); - - // If the `--cmd` argument is provided, it will override the selected session. - if let Some(command) = self.option("cmd") { - let envs = self.options_multi("env"); - - if let Some(envs) = envs { - for env in envs { - if !env.contains('=') { - return Err(format!("malformed environment variable definition for '{env}'").into()); - } - } - } - - self.session_source = SessionSource::DefaultCommand(command, self.options_multi("env")); - } - - if let Some(dirs) = self.option("sessions") { - self.session_paths.extend(env::split_paths(&dirs).map(|dir| (dir, SessionType::Wayland))); - } - - if let Some(dirs) = self.option("xsessions") { - self.session_paths.extend(env::split_paths(&dirs).map(|dir| (dir, SessionType::X11))); - } - - if self.option("session-wrapper").is_some() { - self.session_wrapper = self.option("session-wrapper"); - } - - if !self.config().opt_present("no-xsession-wrapper") { - self.xsession_wrapper = self.option("xsession-wrapper").or_else(|| Some(DEFAULT_XSESSION_WRAPPER.to_string())); - } - - if self.config().opt_present("issue") { - self.greeting = get_issue(); - } - - self.powers.options.push(Power { - action: PowerOption::Shutdown, - label: fl!("shutdown"), - command: self.config().opt_str("power-shutdown"), - }); - - self.powers.options.push(Power { - action: PowerOption::Reboot, - label: fl!("reboot"), - command: self.config().opt_str("power-reboot"), - }); - - self.power_setsid = !self.config().opt_present("power-no-setsid"); - - self.kb_command = self.config().opt_str("kb-command").map(|i| i.parse::().unwrap_or_default()).unwrap_or(2); - self.kb_sessions = self.config().opt_str("kb-sessions").map(|i| i.parse::().unwrap_or_default()).unwrap_or(3); - self.kb_power = self.config().opt_str("kb-power").map(|i| i.parse::().unwrap_or_default()).unwrap_or(12); - - if self.kb_command == self.kb_sessions || self.kb_sessions == self.kb_power || self.kb_power == self.kb_command { - return Err("keybindings must all be distinct".into()); - } - Ok(()) } @@ -657,7 +555,7 @@ fn print_version() { #[cfg(test)] mod test { - use crate::{ui::sessions::SessionSource, Greeter, SecretDisplay}; + use crate::Greeter; #[test] fn test_prompt_width() { @@ -687,91 +585,4 @@ mod test { assert_eq!(greeter.prompt, None); } - - #[tokio::test] - async fn test_command_line_arguments() { - let table: &[(&[&str], _, Option)] = &[ - // No arguments - (&[], true, None), - // Valid combinations - (&["--cmd", "hello"], true, None), - ( - &[ - "--cmd", - "uname", - "--env", - "A=B", - "--env", - "C=D=E", - "--asterisks", - "--asterisks-char", - ".", - "--issue", - "--time", - "--prompt-padding", - "0", - "--window-padding", - "1", - "--container-padding", - "12", - "--user-menu", - ], - true, - Some(|greeter| { - assert!(matches!(&greeter.session_source, SessionSource::DefaultCommand(cmd, Some(env)) if cmd == "uname" && env.len() == 2)); - - if let SessionSource::DefaultCommand(_, Some(env)) = &greeter.session_source { - assert_eq!(env[0], "A=B"); - assert_eq!(env[1], "C=D=E"); - } - - assert!(matches!(&greeter.secret_display, SecretDisplay::Character(c) if c == ".")); - assert_eq!(greeter.prompt_padding(), 0); - assert_eq!(greeter.window_padding(), 1); - assert_eq!(greeter.container_padding(), 13); - assert_eq!(greeter.user_menu, true); - assert!(matches!(greeter.xsession_wrapper.as_deref(), Some("startx /usr/bin/env"))); - }), - ), - ( - &["--xsession-wrapper", "mywrapper.sh"], - true, - Some(|greeter| { - assert!(matches!(greeter.xsession_wrapper.as_deref(), Some("mywrapper.sh"))); - }), - ), - ( - &["--no-xsession-wrapper"], - true, - Some(|greeter| { - assert!(matches!(greeter.xsession_wrapper, None)); - }), - ), - // Invalid combinations - (&["--remember-session", "--remember-user-session"], false, None), - (&["--asterisk-char", ""], false, None), - (&["--remember-user-session"], false, None), - (&["--min-uid", "10000", "--max-uid", "5000"], false, None), - (&["--issue", "--greeting", "Hello, world!"], false, None), - (&["--kb-command", "F2", "--kb-sessions", "F2"], false, None), - (&["--time-format", "%i %"], false, None), - (&["--cmd", "cmd", "--env"], false, None), - (&["--cmd", "cmd", "--env", "A"], false, None), - ]; - - for (opts, valid, check) in table { - let mut greeter = Greeter::default(); - - match valid { - true => { - assert!(matches!(greeter.parse_options(*opts).await, Ok(())), "{:?} cannot be parsed", opts); - - if let Some(check) = check { - check(&greeter); - } - } - false => assert!(matches!(greeter.parse_options(*opts).await, Err(_))), - } - } - } } diff --git a/src/integration/common/mod.rs b/src/integration/common/mod.rs index f14b545..3740925 100644 --- a/src/integration/common/mod.rs +++ b/src/integration/common/mod.rs @@ -75,8 +75,8 @@ impl IntegrationRunner { builder(&mut greeter); } - if greeter.config.is_none() { - greeter.config = Greeter::options().parse(&[""]).ok(); + if greeter.opts.is_none() { + greeter.opts = Greeter::options().parse(&[""]).ok(); } greeter.logfile = "/tmp/tuigreet.log".to_string(); diff --git a/src/main.rs b/src/main.rs index a12a804..8d9218c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ extern crate smart_default; #[macro_use] mod macros; +mod config; mod event; mod greeter; mod info; diff --git a/src/ui/sessions.rs b/src/ui/sessions.rs index 65f6ba7..75b5ec7 100644 --- a/src/ui/sessions.rs +++ b/src/ui/sessions.rs @@ -13,7 +13,7 @@ use super::common::menu::MenuItem; // file. Each variant contains a reference to the data required to create a // session, either the String of the command or the index of the session in the // session list. -#[derive(SmartDefault)] +#[derive(Debug, SmartDefault)] pub enum SessionSource { #[default] None, diff --git a/src/ui/util.rs b/src/ui/util.rs index 0bf5dc9..93c8826 100644 --- a/src/ui/util.rs +++ b/src/ui/util.rs @@ -153,7 +153,7 @@ mod test { #[test] fn test_container_height_username_padding_zero() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--container-padding", "0"]).ok(); + greeter.opts = Greeter::options().parse(&["--container-padding", "0"]).ok(); greeter.mode = Mode::Username; assert_eq!(get_height(&greeter), 3); @@ -167,7 +167,7 @@ mod test { #[test] fn test_container_height_username_padding_one() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--container-padding", "1"]).ok(); + greeter.opts = Greeter::options().parse(&["--container-padding", "1"]).ok(); greeter.mode = Mode::Username; assert_eq!(get_height(&greeter), 5); @@ -183,7 +183,7 @@ mod test { #[test] fn test_container_height_username_greeting_padding_one() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--container-padding", "1"]).ok(); + greeter.opts = Greeter::options().parse(&["--container-padding", "1"]).ok(); greeter.greeting = Some("Hello".into()); greeter.mode = Mode::Username; @@ -202,7 +202,7 @@ mod test { #[test] fn test_container_height_password_greeting_padding_one_prompt_padding_1() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--container-padding", "1"]).ok(); + greeter.opts = Greeter::options().parse(&["--container-padding", "1"]).ok(); greeter.greeting = Some("Hello".into()); greeter.mode = Mode::Password; greeter.prompt = Some("Password:".into()); @@ -221,7 +221,7 @@ mod test { #[test] fn test_container_height_password_greeting_padding_one_prompt_padding_0() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--container-padding", "1", "--prompt-padding", "0"]).ok(); + greeter.opts = Greeter::options().parse(&["--container-padding", "1", "--prompt-padding", "0"]).ok(); greeter.greeting = Some("Hello".into()); greeter.mode = Mode::Password; greeter.prompt = Some("Password:".into()); @@ -232,7 +232,7 @@ mod test { #[test] fn test_rect_bounds() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--width", "50"]).ok(); + greeter.opts = Greeter::options().parse(&["--width", "50"]).ok(); let (x, y, width, height) = get_rect_bounds(&greeter, Rect::new(0, 0, 100, 100), 1); @@ -249,7 +249,7 @@ mod test { #[test] fn input_width() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--width", "40", "--container-padding", "1"]).ok(); + greeter.opts = Greeter::options().parse(&["--width", "40", "--container-padding", "1"]).ok(); let input_width = get_input_width(&greeter, 40, &Some("Username:".into())); @@ -259,7 +259,7 @@ mod test { #[test] fn greeting_height_one_line() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--width", "15", "--container-padding", "1"]).ok(); + greeter.opts = Greeter::options().parse(&["--width", "15", "--container-padding", "1"]).ok(); greeter.greeting = Some("Hello World".into()); let (_, height) = get_greeting_height(&greeter, 1, 0); @@ -270,7 +270,7 @@ mod test { #[test] fn greeting_height_two_lines() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--width", "8", "--container-padding", "1"]).ok(); + greeter.opts = Greeter::options().parse(&["--width", "8", "--container-padding", "1"]).ok(); greeter.greeting = Some("Hello World".into()); let (_, height) = get_greeting_height(&greeter, 1, 0); @@ -281,7 +281,7 @@ mod test { #[test] fn ansi_greeting_height_one_line() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--width", "15", "--container-padding", "1"]).ok(); + greeter.opts = Greeter::options().parse(&["--width", "15", "--container-padding", "1"]).ok(); greeter.greeting = Some("\x1b[31mHello\x1b[0m World".into()); let (text, height) = get_greeting_height(&greeter, 1, 0); @@ -299,7 +299,7 @@ mod test { #[test] fn ansi_greeting_height_two_lines() { let mut greeter = Greeter::default(); - greeter.config = Greeter::options().parse(&["--width", "8", "--container-padding", "1"]).ok(); + greeter.opts = Greeter::options().parse(&["--width", "8", "--container-padding", "1"]).ok(); greeter.greeting = Some("\x1b[31mHello\x1b[0m World".into()); let (text, height) = get_greeting_height(&greeter, 1, 0); From 777e05474fd3aab60aa3410175e01dce7324e9a7 Mon Sep 17 00:00:00 2001 From: Antoine POPINEAU Date: Sun, 29 Sep 2024 16:12:05 +0200 Subject: [PATCH 2/5] Added error handling and fixed tests. --- src/config/file.rs | 15 +++++------ src/config/parser.rs | 22 ++++++++++++---- src/greeter.rs | 61 ++++++++++++++++++++++++-------------------- 3 files changed, 57 insertions(+), 41 deletions(-) diff --git a/src/config/file.rs b/src/config/file.rs index a5f3a78..f3d0354 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -1,10 +1,9 @@ -use std::{error::Error, path::PathBuf}; +use std::path::PathBuf; use serde::Deserialize; -use crate::Greeter; - #[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct FileConfig { #[serde(default)] pub defaults: Defaults, @@ -17,6 +16,7 @@ pub struct FileConfig { } #[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Defaults { pub debug: Option, pub command: Option, @@ -30,6 +30,7 @@ pub struct Defaults { } #[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Sessions { pub wayland_paths: Option>, pub wayland_wrapper: Option, @@ -40,6 +41,7 @@ pub struct Sessions { } #[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Remember { #[serde(default)] pub last_user: bool, @@ -50,6 +52,7 @@ pub struct Remember { } #[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Ui { pub greeting: Option, #[serde(default)] @@ -70,9 +73,3 @@ pub struct Ui { pub sessions_f_key: Option, pub power_f_key: Option, } - -impl Greeter { - pub fn parse_config_file(&mut self, file: &str) -> Result> { - Ok(toml::from_str::(file)?) - } -} diff --git a/src/config/parser.rs b/src/config/parser.rs index cb6316c..285284b 100644 --- a/src/config/parser.rs +++ b/src/config/parser.rs @@ -106,7 +106,7 @@ impl Greeter { self.session_wrapper = self.option("session-wrapper").or_else(|| self.config.sessions.wayland_wrapper.clone()); } - if !self.config().opt_present("no-xsession-wrapper") && self.config.sessions.x11_wrapper_disabled { + if !self.config().opt_present("no-xsession-wrapper") && !self.config.sessions.x11_wrapper_disabled { self.xsession_wrapper = self .option("xsession-wrapper") .or_else(|| self.config.sessions.x11_wrapper.clone()) @@ -277,19 +277,30 @@ mod tests { (&["--cmd", "cmd", "--env", "A"], false, None), ]; - for (opts, valid, check) in table { + for (args, valid, check) in table { let mut greeter = Greeter::default(); + let opts = greeter.parse_opts(*args); + + let result = match opts { + Ok(opts) => { + greeter.opts = opts; + greeter.parse_config().await.ok() + } + + Err(_) => None, + }; match valid { true => { - assert!(matches!(greeter.parse_options(*opts).await, Ok(())), "{:?} cannot be parsed", opts); + assert!(result.is_some(), "{:?} cannot be parsed", args); + assert!(matches!(greeter.parse_config().await, Ok(())), "{:?} cannot be parsed", greeter.opts); if let Some(check) = check { check(&greeter); } } - false => assert!(matches!(greeter.parse_options(*opts).await, Err(_))), + false => assert!(result.is_none(), "{:?} should not have been parsed", args), } } } @@ -321,11 +332,12 @@ mod tests { for (opts, file, check) in table { let mut greeter = Greeter::default(); + greeter.opts = greeter.parse_opts(*opts).unwrap(); greeter.config = FileConfig::default(); file(&mut greeter.config); - assert!(matches!(greeter.parse_options(*opts).await, Ok(())), "{:?} cannot be parsed", opts); + assert!(matches!(greeter.parse_config().await, Ok(())), "{:?} cannot be parsed", opts); check(&greeter); } diff --git a/src/greeter.rs b/src/greeter.rs index 73616d7..32c5b6e 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -212,32 +212,42 @@ impl Greeter { let args = env::args().collect::>(); - if let Err(err) = greeter.parse_options(&args).await { - eprintln!("{err}"); - print_usage(Greeter::options()); + match greeter.parse_opts(&args) { + Ok(opts) => greeter.opts = opts, + Err(err) => { + eprintln!("{err}"); + print_usage(Greeter::options()); - process::exit(1); + process::exit(1); + } } greeter.config = if let Some(config_file) = greeter.config().opt_str("config") { - if let Ok(config) = fs::read_to_string(config_file) { - match greeter.parse_config_file(&config) { + match fs::read_to_string(config_file) { + Ok(config) => match toml::from_str::(&config) { Ok(config) => config, Err(err) => { - eprintln!("{err}"); - print_usage(Greeter::options()); - + eprintln!("ERROR: could not parse configuration file: {err}"); process::exit(1); } + }, + + Err(err) => { + eprintln!("ERROR: could not open configuration file: {err}"); + process::exit(1); } - } else { - // TODO: add error here. - FileConfig::default() } } else { FileConfig::default() }; + if let Err(err) = greeter.parse_config().await { + eprintln!("{err}"); + print_usage(Greeter::options()); + + process::exit(1); + } + greeter.connect().await; } @@ -482,20 +492,17 @@ impl Greeter { opts } - // Parses command line arguments to configured the software accordingly. - pub async fn parse_options(&mut self, args: &[S]) -> Result<(), Box> + pub fn parse_opts(&mut self, args: &[S]) -> Result, Box> where S: AsRef, { - let opts = Greeter::options(); - - self.opts = match opts.parse(args) { - Ok(matches) => Some(matches), - Err(err) => return Err(err.into()), - }; + Ok(Some(Greeter::options().parse(args)?)) + } + // Parses command line arguments to configured the software accordingly. + pub async fn parse_config(&mut self) -> Result<(), Box> { if self.config().opt_present("help") { - print_usage(opts); + print_usage(Greeter::options()); process::exit(0); } if self.config().opt_present("version") { @@ -503,6 +510,12 @@ impl Greeter { process::exit(0); } + if self.config().opt_present("theme") { + if let Some(spec) = self.config().opt_str("theme") { + self.theme = Theme::parse(spec.as_str()); + } + } + self.parse_debug(); self.parse_greeting()?; self.parse_asterisks()?; @@ -514,12 +527,6 @@ impl Greeter { self.parse_power(); self.parse_keybinds()?; - if self.config().opt_present("theme") { - if let Some(spec) = self.config().opt_str("theme") { - self.theme = Theme::parse(spec.as_str()); - } - } - Ok(()) } From aa2f2649f748c9259e80a1b391bbcb54ae235846 Mon Sep 17 00:00:00 2001 From: Antoine POPINEAU Date: Tue, 1 Oct 2024 13:37:34 +0200 Subject: [PATCH 3/5] Support more options in configuration file. --- src/config/file.rs | 10 ++++++---- src/config/parser.rs | 10 +++++++++- src/greeter.rs | 27 ++++++++++----------------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/config/file.rs b/src/config/file.rs index f3d0354..58b3554 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -54,6 +54,7 @@ pub struct Remember { #[derive(Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] pub struct Ui { + pub theme: Option, pub greeting: Option, #[serde(default)] pub use_issue: bool, @@ -65,10 +66,11 @@ pub struct Ui { #[serde(default)] pub show_asterisks: bool, pub asterisks_char: Option, - pub width: Option, - pub window_padding: Option, - pub container_padding: Option, - pub prompt_padding: Option, + pub width: Option, + pub window_padding: Option, + pub container_padding: Option, + pub prompt_padding: Option, + pub greet_align: Option, pub command_f_key: Option, pub sessions_f_key: Option, pub power_f_key: Option, diff --git a/src/config/parser.rs b/src/config/parser.rs index 285284b..37b3d68 100644 --- a/src/config/parser.rs +++ b/src/config/parser.rs @@ -6,7 +6,7 @@ use crate::{ info::{get_issue, get_min_max_uids, get_users}, power::PowerOption, ui::{ - common::menu::Menu, + common::{menu::Menu, style::Theme}, power::Power, sessions::{SessionSource, SessionType}, }, @@ -33,6 +33,14 @@ impl Greeter { } } + pub fn parse_theme(&mut self) -> Result<(), Box> { + if let Some(spec) = self.config().opt_str("theme").or_else(|| self.config.ui.theme.clone()) { + self.theme = Theme::parse(spec.as_str()); + } + + Ok(()) + } + pub fn parse_greeting(&mut self) -> Result<(), Box> { let has_greeting = self.config().opt_present("greeting") || self.config.ui.greeting.is_some(); let has_issue = self.config().opt_present("issue") || self.config.ui.use_issue; diff --git a/src/greeter.rs b/src/greeter.rs index 32c5b6e..3b946e1 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -373,10 +373,8 @@ impl Greeter { // Returns the width of the main window where content is displayed from the // provided arguments. pub fn width(&self) -> u16 { - if let Some(value) = self.option("width") { - if let Ok(width) = value.parse::() { - return width; - } + if let Some(width) = self.option("width").and_then(|value| value.parse::().ok()).or(self.config.ui.width) { + return width; } 80 @@ -384,10 +382,8 @@ impl Greeter { // Returns the padding of the screen from the provided arguments. pub fn window_padding(&self) -> u16 { - if let Some(value) = self.option("window-padding") { - if let Ok(padding) = value.parse::() { - return padding; - } + if let Some(padding) = self.option("window-padding").and_then(|value| value.parse::().ok()).or(self.config.ui.window_padding) { + return padding; } 0 @@ -396,10 +392,8 @@ impl Greeter { // Returns the padding of the main window where content is displayed from the // provided arguments. pub fn container_padding(&self) -> u16 { - if let Some(value) = self.option("container-padding") { - if let Ok(padding) = value.parse::() { - return padding + 1; - } + if let Some(padding) = self.option("container-padding").and_then(|value| value.parse::().ok()).or(self.config.ui.container_padding) { + return padding + 1; } 2 @@ -407,17 +401,15 @@ impl Greeter { // Returns the spacing between each prompt from the provided arguments. pub fn prompt_padding(&self) -> u16 { - if let Some(value) = self.option("prompt-padding") { - if let Ok(padding) = value.parse::() { - return padding; - } + if let Some(padding) = self.option("prompt-padding").and_then(|value| value.parse::().ok()).or(self.config.ui.prompt_padding) { + return padding; } 1 } pub fn greet_align(&self) -> GreetAlign { - if let Some(value) = self.option("greet-align") { + if let Some(value) = self.option("greet-align").or_else(|| self.config.ui.greet_align.clone()) { match value.as_str() { "left" => GreetAlign::Left, "right" => GreetAlign::Right, @@ -526,6 +518,7 @@ impl Greeter { self.parse_remembers()?; self.parse_power(); self.parse_keybinds()?; + self.parse_theme()?; Ok(()) } From 683d60bfb7d0b64e0702e65cd9ad2a217377264f Mon Sep 17 00:00:00 2001 From: Antoine POPINEAU Date: Tue, 8 Oct 2024 19:05:10 +0200 Subject: [PATCH 4/5] Allow --version and --help even if GREETD_SOCK is not defined. --- contrib/tuigreet.toml | 39 +++++++++++++++++++++++++++++++++++++++ src/greeter.rs | 34 +++++++++++++++++----------------- 2 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 contrib/tuigreet.toml diff --git a/contrib/tuigreet.toml b/contrib/tuigreet.toml new file mode 100644 index 0000000..9c67ff9 --- /dev/null +++ b/contrib/tuigreet.toml @@ -0,0 +1,39 @@ +[defaults] +debug = false +command = "/bin/bash" +env = ["VAR=value"] +user_min_uid = 1000 +user_max_uid = 3000 +power_no_setsid = false +shutdown_command = "shutdown -r now" +reboot_command = "reboot" + +[sessions] +wayland_paths = ["/usr/share/wayland-sessions"] +wayland_wrapper = "/usr/local/bin/wlwrapper" +x11_paths = ["/usr/share/xsessions"] +x11_wrapper = "/usr/local/bin/x11wraper" +x11_wrapper_disabled = false + +[remember] +last_user = false +last_session = false +last_user_session = false + +[ui] +theme = "border=red" +greeting = "Well, hello there!" +use_issue = false +show_time = false +time_format = "%d %B %Y" +show_user_menu = false +show_asterisks = false +asterisks_char = "*" +width = 220 +window_padding = 2 +container_padding = 2 +prompt_padding = 1 +greet_align = "center" +command_f_key = 1 +sessions_f_key = 2 +power_f_key = 3 diff --git a/src/greeter.rs b/src/greeter.rs index 3b946e1..d629db1 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -202,14 +202,6 @@ impl Greeter { #[cfg(not(test))] { - match env::var("GREETD_SOCK") { - Ok(socket) => greeter.socket = socket, - Err(_) => { - eprintln!("GREETD_SOCK must be defined"); - process::exit(1); - } - } - let args = env::args().collect::>(); match greeter.parse_opts(&args) { @@ -222,6 +214,23 @@ impl Greeter { } } + if greeter.config().opt_present("help") { + print_usage(Greeter::options()); + process::exit(0); + } + if greeter.config().opt_present("version") { + print_version(); + process::exit(0); + } + + match env::var("GREETD_SOCK") { + Ok(socket) => greeter.socket = socket, + Err(_) => { + eprintln!("GREETD_SOCK must be defined"); + process::exit(1); + } + } + greeter.config = if let Some(config_file) = greeter.config().opt_str("config") { match fs::read_to_string(config_file) { Ok(config) => match toml::from_str::(&config) { @@ -493,15 +502,6 @@ impl Greeter { // Parses command line arguments to configured the software accordingly. pub async fn parse_config(&mut self) -> Result<(), Box> { - if self.config().opt_present("help") { - print_usage(Greeter::options()); - process::exit(0); - } - if self.config().opt_present("version") { - print_version(); - process::exit(0); - } - if self.config().opt_present("theme") { if let Some(spec) = self.config().opt_str("theme") { self.theme = Theme::parse(spec.as_str()); From aa0386e2afbd0ec3b017763cc51f923ed8fe01a7 Mon Sep 17 00:00:00 2001 From: Antoine POPINEAU Date: Wed, 9 Oct 2024 10:25:40 +0200 Subject: [PATCH 5/5] Refactor config file tests and write some more. --- src/config/file.rs | 14 +++++---- src/config/parser.rs | 72 ++++++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/src/config/file.rs b/src/config/file.rs index 58b3554..b4c3777 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use serde::Deserialize; -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct FileConfig { #[serde(default)] @@ -15,10 +15,12 @@ pub struct FileConfig { pub ui: Ui, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Defaults { - pub debug: Option, + #[serde(default)] + pub debug: bool, + pub log_file: Option, pub command: Option, pub env: Option>, pub user_min_uid: Option, @@ -29,7 +31,7 @@ pub struct Defaults { pub reboot_command: Option, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Sessions { pub wayland_paths: Option>, @@ -40,7 +42,7 @@ pub struct Sessions { pub x11_wrapper_disabled: bool, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Remember { #[serde(default)] @@ -51,7 +53,7 @@ pub struct Remember { pub last_user_session: bool, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Ui { pub theme: Option, diff --git a/src/config/parser.rs b/src/config/parser.rs index 37b3d68..21f56b8 100644 --- a/src/config/parser.rs +++ b/src/config/parser.rs @@ -21,12 +21,10 @@ pub const DEFAULT_XSESSION_WRAPPER: &str = "startx /usr/bin/env"; impl Greeter { pub fn parse_debug(&mut self) { - let debug = self.config.defaults.debug.clone(); - - if self.config().opt_present("debug") || debug.is_some() { + if self.config().opt_present("debug") || self.config.defaults.debug { self.debug = true; - self.logfile = match self.config().opt_str("debug").or(debug) { + self.logfile = match self.config().opt_str("debug").or_else(|| self.config.defaults.log_file.clone()) { Some(file) => file.to_string(), None => DEFAULT_LOG_FILE.to_string(), }; @@ -212,7 +210,7 @@ impl Greeter { #[cfg(test)] mod tests { - use crate::{config::file::FileConfig, ui::sessions::SessionSource, Greeter, SecretDisplay}; + use crate::{ui::sessions::SessionSource, Greeter, SecretDisplay}; #[tokio::test] async fn test_command_line_arguments() { @@ -315,39 +313,73 @@ mod tests { #[tokio::test] async fn command_and_env() { - let table: &[(&[&str], fn(&mut FileConfig), fn(&Greeter))] = &[ + use crate::config::file::*; + + let file = FileConfig { + defaults: Defaults { + debug: true, + log_file: Some("/tmp/filedebug.log".to_string()), + command: Some("filecmd".to_string()), + env: Some(vec!["FILEENV=value".to_string()]), + power_no_setsid: true, + ..Defaults::default() + }, + sessions: Sessions { ..Default::default() }, + remember: Remember { ..Default::default() }, + ui: Ui { ..Default::default() }, + }; + + let table: &[(&[&str], fn(&Greeter), fn(&Greeter))] = &[ + ( + &["--debug=/tmp/cmdfile.log"], + |greeter| { + assert_eq!(greeter.debug, true); + assert_eq!(&greeter.logfile, "/tmp/filedebug.log"); + }, + |greeter| { + assert_eq!(greeter.debug, true); + assert_eq!(&greeter.logfile, "/tmp/cmdfile.log"); + }, + ), ( &["--cmd", "mycommand", "--env", "A=b", "--env", "C=d"], - |file: &mut FileConfig| { - file.defaults.command = Some("secondcommand".to_string()); - file.defaults.env = Some(vec!["X=y".to_string()]); + |greeter| { + assert!(matches!(&greeter.session_source, SessionSource::DefaultCommand(cmd, Some(env)) if cmd == "filecmd" && env.len() == 1 && env.first().unwrap() == "FILEENV=value")); }, |greeter| { assert!(matches!(&greeter.session_source, SessionSource::DefaultCommand(cmd, Some(env)) if cmd == "mycommand" && env.len() == 2)); }, ), ( - &[], - |file: &mut FileConfig| { - file.defaults.command = Some("secondcommand".to_string()); - file.defaults.env = Some(vec!["X=y".to_string()]); + &["--power-no-setsid"], + |greeter| { + assert_eq!(greeter.power_setsid, false); }, |greeter| { - assert!(matches!(&greeter.session_source, SessionSource::DefaultCommand(cmd, Some(env)) if cmd == "secondcommand" && env.len() == 1 && env.first().unwrap() == "X=y")); + assert_eq!(greeter.power_setsid, false); }, ), ]; - for (opts, file, check) in table { + for (opts, without_opts, with_opts) in table { let mut greeter = Greeter::default(); - greeter.opts = greeter.parse_opts(*opts).unwrap(); - greeter.config = FileConfig::default(); + greeter.config = file.clone(); - file(&mut greeter.config); + { + greeter.opts = greeter.parse_opts::<&str>(&[]).unwrap(); - assert!(matches!(greeter.parse_config().await, Ok(())), "{:?} cannot be parsed", opts); + assert!(matches!(greeter.parse_config().await, Ok(())), "{:?} cannot be parsed", opts); + + without_opts(&greeter); + } - check(&greeter); + { + greeter.opts = greeter.parse_opts(*opts).unwrap(); + + assert!(matches!(greeter.parse_config().await, Ok(())), "{:?} cannot be parsed", opts); + + with_opts(&greeter); + } } } }