From 9f916a9ab36aa5bbcceafd16bc9402be59db0c61 Mon Sep 17 00:00:00 2001 From: Antoine POPINEAU Date: Sun, 29 Sep 2024 14:39:42 +0200 Subject: [PATCH] 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);