diff --git a/Cargo.lock b/Cargo.lock index 3eec34a..108ea3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,6 +318,7 @@ dependencies = [ "bscreensaver-util", "glib", "gtk", + "log", ] [[package]] @@ -347,9 +348,11 @@ dependencies = [ "humantime", "lazy_static", "libc", + "shell-words", "toml", "xcb", "xdg", + "zbus", ] [[package]] diff --git a/dialog-gtk3/src/main.rs b/dialog-gtk3/src/main.rs index 38f38fb..bd2a7b6 100644 --- a/dialog-gtk3/src/main.rs +++ b/dialog-gtk3/src/main.rs @@ -4,9 +4,9 @@ use gethostname::gethostname; use glib::GString; use gtk::{prelude::*, Button, Entry, Label, Plug, Window}; use log::{debug, error, warn}; -use std::{io::{self, Write}, process::{exit, Command}, rc::Rc, thread, time::Duration}; +use std::{io::{self, Write}, process::exit, rc::Rc, thread, time::Duration}; -use bscreensaver_util::{init_logging, settings::Configuration}; +use bscreensaver_util::{init_logging, settings::Configuration, desktop::NewLoginCommand}; const DIALOG_UPDATE_INTERVAL: Duration = Duration::from_millis(100); const DIALOG_TIMEOUT: Duration = Duration::from_secs(60); @@ -15,11 +15,13 @@ fn main() -> anyhow::Result<()> { init_logging("BSCREENSAVER_DIALOG_GTK3_LOG"); glib::log_set_default_handler(glib::rust_log_handler); - let mut config = Configuration::load()?; - let new_login_command = config.new_login_command - .take() - .map(|nlc| shell_words::split(&nlc)) - .transpose()?; + let config = Configuration::load()?; + let new_login_command = + if config.new_login_command == NewLoginCommand::Disabled { + None + } else { + Some(config.new_login_command.clone()) + }; let standalone = std::env::var("BSCREENSAVER_DIALOG_STANDALONE").is_ok(); @@ -245,14 +247,14 @@ fn main() -> anyhow::Result<()> { .label("New Login") .build(); new_login_button.connect_clicked(move |_| { - let cmd = &new_login_command[0]; - let empty = Vec::::new(); - let args = if new_login_command.len() > 1 { &new_login_command[1..] } else { &empty }; - if let Err(err) = Command::new(cmd).args(args).spawn() { - warn!("Failed to run new login command: {}", err); - } else { - exit(1); - } + let new_login_command = new_login_command.clone(); + thread::spawn(move || { + if let Err(err) = new_login_command.run() { + warn!("Failed to run new login command: {}", err); + } else { + exit(1); + } + }); }); hbox.pack_start(&new_login_button, false, true, 8); } diff --git a/settings/Cargo.toml b/settings/Cargo.toml index dadbca1..9434ea0 100644 --- a/settings/Cargo.toml +++ b/settings/Cargo.toml @@ -8,3 +8,4 @@ anyhow = "1" bscreensaver-util = { path = "../util" } glib = "0.15" gtk = { version = "0.15", features = ["v3_20"] } +log = "0.4" diff --git a/settings/src/main.rs b/settings/src/main.rs index 8c2ad9e..641a2ba 100644 --- a/settings/src/main.rs +++ b/settings/src/main.rs @@ -1,17 +1,20 @@ use gtk::{glib, prelude::*}; use glib::clone; +use log::warn; use std::{env, process::exit, time::Duration}; -use bscreensaver_util::settings::Configuration; +use bscreensaver_util::{init_logging, settings::Configuration, desktop::NewLoginCommand}; #[derive(Clone)] struct Widgets { lock_timeout: gtk::SpinButton, blank_before_locking: gtk::SpinButton, - new_login_command: gtk::Entry, + new_login_command_combo: gtk::ComboBoxText, + custom_new_login_command_entry: gtk::Entry, } fn main() -> anyhow::Result<()> { + init_logging("BSCREENSAVER_SETTINGS"); let config = Configuration::load()?; let app = gtk::Application::builder() @@ -122,27 +125,83 @@ fn show_ui(app: >k::Application, config: &Configuration) { label_sg.add_widget(&label); hbox.pack_start(&label, false, false, 0); - let new_login_command_entry = gtk::Entry::builder() - .text(config.new_login_command.as_ref().unwrap_or(&"".to_string())) - .width_chars(30) - .activates_default(true) + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) .build(); - hbox.pack_start(&new_login_command_entry, false, false, 0); + hbox.pack_start(&vbox, true, true, 0); + + let new_login_command_combo = gtk::ComboBoxText::builder() + .build(); + new_login_command_combo.append_text(NewLoginCommand::Auto.as_str()); + new_login_command_combo.append_text(NewLoginCommand::DisplayManagerDBus.as_str()); + new_login_command_combo.append_text(NewLoginCommand::Gdm.as_str()); + new_login_command_combo.append_text(NewLoginCommand::Kdm.as_str()); + new_login_command_combo.append_text(NewLoginCommand::LightDm.as_str()); + new_login_command_combo.append_text(NewLoginCommand::Lxdm.as_str()); + new_login_command_combo.append_text(NewLoginCommand::Custom("".to_string()).as_str()); + new_login_command_combo.append_text(NewLoginCommand::Disabled.as_str()); + vbox.pack_start(&new_login_command_combo, false, false, 0); + + let (custom_new_login_command_hbox, custom_new_login_command_entry, custom_new_login_command_button) = { + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .sensitive(false) + .build(); + topvbox.pack_start(&hbox, false, false, 0); + + let spacer = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(0) + .build(); + label_sg.add_widget(&spacer); + hbox.pack_start(&spacer, false, false, 0); + + let label = gtk::Label::builder() + .label("Custom Command:") + .tooltip_text("Custom command to run when the 'New Login' button is clicked in the unlock dialog") + .xalign(0.0) + .build(); + hbox.pack_start(&label, false, false, 0); + + let entry = gtk::Entry::builder() + .width_chars(30) + .activates_default(true) + .build(); + hbox.pack_start(&entry, false, false, 0); + + let button = gtk::Button::from_icon_name(Some("folder-open"), gtk::IconSize::Button); + hbox.pack_start(&button, false, false, 0); + + (hbox, entry, button) + }; + + match &config.new_login_command { + NewLoginCommand::Custom(cmd) => { + custom_new_login_command_entry.set_text(cmd.as_str()); + custom_new_login_command_hbox.set_sensitive(true); + }, + _ => (), + }; + new_login_command_combo.set_active(Some(config.new_login_command.ord())); + new_login_command_combo.connect_changed(clone!(@strong custom_new_login_command_hbox => move |combo| { + let sensitive = combo.active().unwrap_or(0) == NewLoginCommand::Custom("".to_string()).ord(); + custom_new_login_command_hbox.set_sensitive(sensitive); + })); let widgets = Widgets { lock_timeout: lock_timeout_spinbutton.clone(), blank_before_locking: blank_before_locking_spinbutton.clone(), - new_login_command: new_login_command_entry.clone(), + new_login_command_combo: new_login_command_combo.clone(), + custom_new_login_command_entry: custom_new_login_command_entry.clone(), }; mainwin.connect_delete_event(clone!(@strong config, @strong widgets, @strong app, @strong mainwin => move |_,_| { Inhibit(!confirm_cancel(&config, &widgets, &mainwin)) })); - - let button = gtk::Button::from_icon_name(Some("folder-open"), gtk::IconSize::Button); - button.connect_clicked(clone!(@strong mainwin, @strong widgets => move |_| { + custom_new_login_command_button.connect_clicked(clone!(@strong mainwin, @strong widgets => move |_| { run_file_chooser(&mainwin, &widgets); })); - hbox.pack_start(&button, false, false, 0); let button_box = gtk::ButtonBox::builder() .spacing(8) @@ -177,7 +236,7 @@ fn show_ui(app: >k::Application, config: &Configuration) { fn run_file_chooser(mainwin: >k::ApplicationWindow, widgets: &Widgets) { let file_chooser = gtk::FileChooserNative::new( - Some("New Login Command"), + Some("Custom New Login Command"), Some(mainwin), gtk::FileChooserAction::Open, Some("Select"), @@ -187,7 +246,7 @@ fn run_file_chooser(mainwin: >k::ApplicationWindow, widgets: &Widgets) { file_chooser.hide(); if response == gtk::ResponseType::Accept { if let Some(filename) = file_chooser.filename() { - widgets.new_login_command.set_text(&filename.to_string_lossy()); + widgets.custom_new_login_command_entry.set_text(&filename.to_string_lossy()); } } } @@ -243,7 +302,19 @@ fn build_new_configuration(old_config: &Configuration, widgets: &Widgets) -> (Co let mut new_config = old_config.clone(); new_config.lock_timeout = Duration::from_secs(widgets.lock_timeout.adjustment().value() as u64 * 60); new_config.blank_before_locking = Duration::from_secs(widgets.blank_before_locking.adjustment().value() as u64 * 60); - new_config.new_login_command = Some(widgets.new_login_command.text()).filter(|s| !s.is_empty()).map(|s| s.to_string()); + + new_config.new_login_command = match NewLoginCommand::try_from(widgets.new_login_command_combo.active().unwrap_or(0) as u32) { + Ok(NewLoginCommand::Custom(_)) => Some(widgets.custom_new_login_command_entry.text()) + .filter(|s| !s.is_empty()) + .map(|s| NewLoginCommand::Custom(s.to_string())) + .unwrap_or(NewLoginCommand::Auto), + Ok(nlc) => nlc, + Err(_) => { + warn!("BUG: couldn't figure out new login command type from combo box"); + NewLoginCommand::Auto + }, + }; + let changed = old_config != &new_config; (new_config, changed) } diff --git a/util/Cargo.toml b/util/Cargo.toml index 3b5af4f..dfc89ef 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -12,6 +12,8 @@ env_logger = "0.9" humantime = "2" lazy_static = "1" libc = "0.2" +shell-words = "1" toml = "0.5" xcb = { git = "https://github.com/rust-x-bindings/rust-xcb", rev = "d09b5f91bc07d56673f1bc0d6c7ecd72b5ff7b3e", features = ["randr", "screensaver", "xfixes"] } xdg = "2" +zbus = "2" diff --git a/util/src/desktop.rs b/util/src/desktop.rs new file mode 100644 index 0000000..7ab1497 --- /dev/null +++ b/util/src/desktop.rs @@ -0,0 +1,171 @@ +use std::{env, fmt, process::Command}; + +const GDM_CMD: &[&str] = &[ "gdmflexiserver", "-ls" ]; +const KDM_CMD: &[&str] = &[ "kdmctl", "reserve" ]; +const LIGHTDM_CMD: &[&str] = &[ "dm-tool", "switch-to-greeter" ]; +const LXDM_CMD: &[&str] = &[ "lxdm", "-c", "USER_SWITCH" ]; + +#[derive(Debug, Clone, PartialEq)] +pub enum NewLoginCommand { + Auto, + DisplayManagerDBus, + Gdm, + Kdm, + LightDm, + Lxdm, + Custom(String), + Disabled, +} + +impl NewLoginCommand { + pub fn run(&self) -> anyhow::Result<()> { + match self { + Self::Auto => auto_run(), + Self::DisplayManagerDBus => call_dbus(), + Self::Gdm => run(GDM_CMD), + Self::Kdm => run(KDM_CMD), + Self::LightDm => run(LIGHTDM_CMD), + Self::Lxdm => run(LXDM_CMD), + Self::Custom(cmd) => shell_words::split(cmd.as_str()) + .map_err(|err| err.into()) + .and_then(|argvec| { + let argv: Vec<&str> = argvec.iter().map(|s| s.as_str()).collect(); + run(&argv).map_err(|err| err.into()) + }), + Self::Disabled => Ok(()), + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Auto => "Auto", + Self::DisplayManagerDBus => "DBus (desktop-agnostic)", + Self::Gdm => "GDM (GNOME)", + Self::Kdm => "KDM (KDE)", + Self::LightDm => "LightDM", + Self::Lxdm => "LXDM (LXDE)", + Self::Custom(_) => "Custom", + Self::Disabled => "Disabled", + } + } + + pub fn ord(&self) -> u32 { + match self { + Self::Auto => 0, + Self::DisplayManagerDBus => 1, + Self::Gdm => 2, + Self::Kdm => 3, + Self::LightDm => 4, + Self::Lxdm => 5, + Self::Custom(_) => 6, + Self::Disabled => 7, + } + } +} + +impl fmt::Display for NewLoginCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl TryFrom for NewLoginCommand { + type Error = anyhow::Error; + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Auto), + 1 => Ok(Self::DisplayManagerDBus), + 2 => Ok(Self::Gdm), + 3 => Ok(Self::Kdm), + 4 => Ok(Self::LightDm), + 5 => Ok(Self::Lxdm), + 6 => Ok(Self::Custom("".to_string())), + 7 => Ok(Self::Disabled), + other => Err(anyhow::anyhow!("Unknown ordinal for NewLoginCommand: {}", other)), + } + } +} + +impl TryFrom<&toml::Value> for NewLoginCommand { + type Error = anyhow::Error; + fn try_from(value: &toml::Value) -> Result { + if let Some(s) = value.as_str() { + return match s { + "auto" => Ok(Self::Auto), + "dbus" => Ok(Self::DisplayManagerDBus), + "gdm" => Ok(Self::Gdm), + "kdm" => Ok(Self::Kdm), + "lightdm" => Ok(Self::LightDm), + "lxdm" => Ok(Self::Lxdm), + "disabled" => Ok(Self::Disabled), + other => Err(anyhow::anyhow!("Unknown value '{}' for new login command", other)), + }; + } + + if let Some(arr) = value.as_array() { + let argv = arr.into_iter() + .map(|v| v.as_str().map(|s| s.to_string()).ok_or_else(|| anyhow::anyhow!("Custom command must be made of strings"))) + .collect::>>()?; + return Ok(Self::Custom(shell_words::join(argv))) + } + + Err(anyhow::anyhow!("Unknown value for new login command")) + } +} + +impl TryInto for NewLoginCommand { + type Error = anyhow::Error; + fn try_into(self) -> Result { + match self { + Self::Auto => Ok(toml::Value::String("auto".to_string())), + Self::DisplayManagerDBus => Ok(toml::Value::String("dbus".to_string())), + Self::Gdm => Ok(toml::Value::String("gdm".to_string())), + Self::Kdm => Ok(toml::Value::String("kdm".to_string())), + Self::LightDm => Ok(toml::Value::String("lightdm".to_string())), + Self::Lxdm => Ok(toml::Value::String("lxdm".to_string())), + Self::Custom(cmd) => shell_words::split(cmd.as_str()) + .map_err(|err| err.into()) + .map(|words| toml::Value::Array(words.into_iter().map(|s| toml::Value::String(s)).collect())), + Self::Disabled => Ok(toml::Value::String("disabled".to_string())), + } + } +} + +fn auto_run() -> anyhow::Result<()> { + if let Ok(_) = call_dbus() { + return Ok(()) + } + + if let Some(session_desktop) = env::var("XDG_SESSION_DESKTOP").map(|s| s.to_lowercase()).ok() { + match session_desktop.as_str() { + "gnome" | "gdm" => run(GDM_CMD), + "kde" | "kdm" => run(KDM_CMD), + "lightdm-session" => run(LIGHTDM_CMD), + "lxde" | "lxdm" => run(LXDM_CMD), + other => Err(anyhow::anyhow!("Couldn't determine how to start a new login session for session type {}", other)), + } + } else { + Err(anyhow::anyhow!("Couldn't determine how to start a new login session")) + } +} + +fn call_dbus() -> anyhow::Result<()> { + let seat_path = env::var("XDG_SEAT_PATH") + .unwrap_or_else(|_| "/org/freedesktop/DisplayManager/Seat0".to_string()); + let bus = zbus::blocking::Connection::system()?; + let proxy = zbus::blocking::Proxy::new( + &bus, + "org.freedesktop.DisplayManager", + seat_path, + "org.freedesktop.DisplayManager.Seat" + )?; + proxy.call("SwitchToGreeter", &())?; + Ok(()) +} + +fn run(argv: &[&str]) -> anyhow::Result<()> { + Command::new(argv[0]) + .args(if argv.len() > 1 { &argv[1..argv.len()-1] } else { &[] }) + .spawn()?; + Ok(()) +} diff --git a/util/src/lib.rs b/util/src/lib.rs index 30ec6af..ed4cd4b 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -1,6 +1,7 @@ use std::{ffi::CStr, io}; use xcb::x; +pub mod desktop; pub mod settings; pub const BSCREENSAVER_WM_CLASS: &[u8] = b"bscreensaver\0Bscreensaver\0"; diff --git a/util/src/settings.rs b/util/src/settings.rs index 09c812b..7d2b442 100644 --- a/util/src/settings.rs +++ b/util/src/settings.rs @@ -2,6 +2,8 @@ use anyhow::anyhow; use std::{fmt, fs::{self, File}, io::{Read, Write}, path::PathBuf, time::Duration}; use toml::Value; +use crate::desktop::NewLoginCommand; + const CONFIG_FILE_RELATIVE_PATH: &str = "bscreensaver/bscreensaver.toml"; const LOCK_TIMEOUT: &str = "lock-timeout"; @@ -51,7 +53,7 @@ pub struct Configuration { pub lock_timeout: Duration, pub blank_before_locking: Duration, pub dialog_backend: DialogBackend, - pub new_login_command: Option, + pub new_login_command: NewLoginCommand, } impl Configuration { @@ -80,7 +82,7 @@ impl Configuration { }; config.new_login_command = match config_toml.get("new-login-command") { None => config.new_login_command, - Some(val) => val.as_str().map(|s| s.to_string()), + Some(val) => val.try_into().unwrap_or(NewLoginCommand::Auto), }; } @@ -99,9 +101,7 @@ impl Configuration { config_map.insert(LOCK_TIMEOUT.to_string(), Value::String(format_duration(self.lock_timeout).to_string())); config_map.insert(BLANK_BEFORE_LOCKING.to_string(), Value::String(format_duration(self.blank_before_locking).to_string())); config_map.insert(DIALOG_BACKEND.to_string(), Value::String(self.dialog_backend.to_string())); - if let Some(new_login_command) = &self.new_login_command { - config_map.insert(NEW_LOGIN_COMMAND.to_string(), Value::String(new_login_command.clone())); - } + config_map.insert(NEW_LOGIN_COMMAND.to_string(), self.new_login_command.clone().try_into()?); let config_path = xdg::BaseDirectories::new()?.place_config_file(CONFIG_FILE_RELATIVE_PATH)?; let mut tmp_filename = config_path.file_name().unwrap().to_os_string(); @@ -122,7 +122,7 @@ impl Default for Configuration { lock_timeout: Duration::from_secs(60 * 10), blank_before_locking: Duration::ZERO, dialog_backend: DialogBackend::Gtk3, - new_login_command: None, + new_login_command: NewLoginCommand::Auto, } } }