From 6420278f7164323348f7de4e83bd06771ae48e70 Mon Sep 17 00:00:00 2001 From: "Brian J. Tarricone" Date: Thu, 5 May 2022 01:14:53 -0700 Subject: [PATCH] Add simple (if ugly) settings dialog --- Cargo.lock | 11 ++ Cargo.toml | 1 + Makefile | 9 +- settings/Cargo.toml | 11 ++ settings/bscreensaver-settings.desktop | 10 + settings/src/main.rs | 249 +++++++++++++++++++++++++ util/src/settings.rs | 68 +++++-- 7 files changed, 344 insertions(+), 15 deletions(-) create mode 100644 settings/Cargo.toml create mode 100644 settings/bscreensaver-settings.desktop create mode 100644 settings/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index ff897ba..8fc0e9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,6 +308,17 @@ dependencies = [ "shell-words", ] +[[package]] +name = "bscreensaver-settings" +version = "0.1.0" +dependencies = [ + "anyhow", + "bscreensaver-util", + "glib", + "gtk", + "log", +] + [[package]] name = "bscreensaver-systemd" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9b70a07..284f69d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "dbus-service", "dialog-gtk3", "util", + "settings", "systemd", "xcb-xembed", ] diff --git a/Makefile b/Makefile index 7ee0d96..abaabf1 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,9 @@ PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin LIBEXECDIR ?= $(PREFIX)/libexec SYSCONFDIR ?= $(PREFIX)/etc +DATADIR ?= $(PREFIX)/share +APPLICATIONS_DIR = $(DATADIR)/applications CONFIG_DIR = $(SYSCONFDIR)/xdg/bscreensaver HELPER_DIR = $(LIBEXECDIR)/bscreensaver HELPERS = \ @@ -26,16 +28,17 @@ dev: HELPER_DIR=target/debug cargo build install: release - $(INSTALL) -m 0755 -d $(BINDIR) $(HELPER_DIR) $(CONFIG_DIR) - $(INSTALL) -m 0755 target/release/bscreensaver target/release/bscreensaver-command $(BINDIR) + $(INSTALL) -m 0755 -d $(BINDIR) $(HELPER_DIR) $(CONFIG_DIR) $(APPLICATIONS_DIR) + $(INSTALL) -m 0755 target/release/bscreensaver target/release/bscreensaver-command target/release/bscreensaver-settings $(BINDIR) $(INSTALL) -m 0755 $(addprefix target/release/,$(HELPERS)) $(HELPER_DIR) $(INSTALL) -m 0644 bscreensaver.toml.example $(CONFIG_DIR) + $(INSTALL) -m 0644 settings/bscreensaver-settings.desktop $(APPLICATIONS_DIR) clean: cargo clean uninstall: - rm -f $(BINDIR)/bscreensaver $(BINDIR)/bscreensaver-command $(addprefix $(HELPER_DIR)/,$(HELPERS)) || true + rm -f $(BINDIR)/bscreensaver $(BINDIR)/bscreensaver-command $(BINDIR)/bscreensaver-settings $(addprefix $(HELPER_DIR)/,$(HELPERS)) || true rmdir -p $(BINDIR) $(HELPER_DIR) || true rmdir -p $(PREFIX) || true diff --git a/settings/Cargo.toml b/settings/Cargo.toml new file mode 100644 index 0000000..a3af124 --- /dev/null +++ b/settings/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bscreensaver-settings" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +bscreensaver-util = { path = "../util" } +glib = { version = "0.15", features = ["v2_68"] } +gtk = { version = "0.15", features = ["v3_24"] } +log = "0.4" diff --git a/settings/bscreensaver-settings.desktop b/settings/bscreensaver-settings.desktop new file mode 100644 index 0000000..b0f336b --- /dev/null +++ b/settings/bscreensaver-settings.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Name=Screensaver Settings +Comment=Customize screensaver settings +Exec=bscreensaver-settings +Icon=screensaver +Terminal=false +StartupNotify=true +Type=Application +Categories=GTK;Settings;DesktopSettings; diff --git a/settings/src/main.rs b/settings/src/main.rs new file mode 100644 index 0000000..8c2ad9e --- /dev/null +++ b/settings/src/main.rs @@ -0,0 +1,249 @@ +use gtk::{glib, prelude::*}; +use glib::clone; +use std::{env, process::exit, time::Duration}; + +use bscreensaver_util::settings::Configuration; + +#[derive(Clone)] +struct Widgets { + lock_timeout: gtk::SpinButton, + blank_before_locking: gtk::SpinButton, + new_login_command: gtk::Entry, +} + +fn main() -> anyhow::Result<()> { + let config = Configuration::load()?; + + let app = gtk::Application::builder() + .application_id("org.spurint.bscreensaver-settings") + .build(); + app.connect_activate(move |app| show_ui(&app, &config)); + + exit(app.run_with_args(&env::args().into_iter().collect::>())); +} + +fn show_ui(app: >k::Application, config: &Configuration) { + let mainwin = gtk::ApplicationWindow::builder() + .application(app) + .name("BScreensaver Settings") + .title("Screensaver Settings") + .type_(gtk::WindowType::Toplevel) + .resizable(false) + .build(); + + let label_sg = gtk::SizeGroup::builder() + .mode(gtk::SizeGroupMode::Horizontal) + .build(); + + let topvbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) + .margin(4) + .build(); + mainwin.add(&topvbox); + + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .build(); + topvbox.pack_start(&hbox, false, false, 0); + + let label = gtk::Label::builder() + .label("Lock screen after") + .xalign(0.0) + .build(); + label_sg.add_widget(&label); + hbox.pack_start(&label, false, false, 0); + + let lock_timeout_spinbutton = gtk::SpinButton::builder() + .adjustment(>k::Adjustment::builder() + .lower(1.0) + .upper(f64::MAX as u32 as f64) + .step_increment(1.0) + .page_increment(5.0) + .value((config.lock_timeout.as_secs() / 60) as f64) + .build() + ) + .tooltip_text("Minutes before the screen locks") + .activates_default(true) + .build(); + hbox.pack_start(&lock_timeout_spinbutton, false, false, 0); + + let label = gtk::Label::builder() + .label("minutes") + .xalign(0.0) + .build(); + hbox.pack_start(&label, false, false, 0); + + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .build(); + topvbox.pack_start(&hbox, false, false, 0); + + let label = gtk::Label::builder() + .label("Blank screen") + .xalign(0.0) + .build(); + label_sg.add_widget(&label); + hbox.pack_start(&label, false, false, 0); + + let blank_before_locking_spinbutton = gtk::SpinButton::builder() + .adjustment(>k::Adjustment::builder() + .lower(0.0) + .upper(f64::MAX as u32 as f64) + .step_increment(1.0) + .page_increment(5.0) + .value((config.blank_before_locking.as_secs() / 60) as f64) + .build() + ) + .tooltip_text("Minutes before screen locks to blank the screen") + .activates_default(true) + .build(); + hbox.pack_start(&blank_before_locking_spinbutton, false, false, 0); + + let label = gtk::Label::builder() + .label("minutes before locking") + .xalign(0.0) + .build(); + hbox.pack_start(&label, false, false, 0); + + let hbox = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .build(); + topvbox.pack_start(&hbox, false, false, 0); + + let label = gtk::Label::builder() + .label("New Login Command:") + .tooltip_text("Command to run when the 'New Login' button is clicked in the unlock dialog") + .xalign(0.0) + .build(); + 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) + .build(); + hbox.pack_start(&new_login_command_entry, false, false, 0); + + 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(), + }; + 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 |_| { + run_file_chooser(&mainwin, &widgets); + })); + hbox.pack_start(&button, false, false, 0); + + let button_box = gtk::ButtonBox::builder() + .spacing(8) + .layout_style(gtk::ButtonBoxStyle::End) + .build(); + topvbox.pack_end(&button_box, false, false, 0); + + let close_button = gtk::Button::builder() + .label("Close") + .build(); + close_button.connect_clicked(clone!(@strong config, @strong widgets, @strong app, @strong mainwin => move |_| { + if confirm_cancel(&config, &widgets, &mainwin) { + app.quit(); + } + })); + button_box.pack_end(&close_button, false, false, 0); + + let save_button = gtk::Button::builder() + .label("Save") + .build(); + save_button.connect_clicked(clone!(@strong config, @strong widgets, @strong app, @strong mainwin => move |_| { + if save_config(&config, &widgets, &mainwin) { + app.quit(); + } + })); + button_box.pack_end(&save_button, false, false, 0); + save_button.set_can_default(true); + save_button.set_has_default(true); + + mainwin.show_all(); +} + +fn run_file_chooser(mainwin: >k::ApplicationWindow, widgets: &Widgets) { + let file_chooser = gtk::FileChooserNative::new( + Some("New Login Command"), + Some(mainwin), + gtk::FileChooserAction::Open, + Some("Select"), + Some("Cancel"), + ); + let response = file_chooser.run(); + 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()); + } + } +} + +fn save_config(config: &Configuration, widgets: &Widgets, mainwin: >k::ApplicationWindow) -> bool{ + let (new_config, changed) = build_new_configuration(&config, &widgets); + if changed { + if let Err(err) = new_config.save() { + let error_dialog = gtk::MessageDialog::new( + Some(mainwin), + gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT, + gtk::MessageType::Error, + gtk::ButtonsType::Close, + &format!("Failed to save configuration: {}", err), + ); + error_dialog.set_title("Screensaver Settings"); + error_dialog.run(); + error_dialog.hide(); + return false; + } + } + true +} + +fn confirm_cancel(config: &Configuration, widgets: &Widgets, mainwin: >k::ApplicationWindow) -> bool { + let (_, changed) = build_new_configuration(&config, &widgets); + if changed { + let dialog = gtk::MessageDialog::new( + Some(mainwin), + gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT, + gtk::MessageType::Warning, + gtk::ButtonsType::None, + "You have unsaved changes. Are you sure you want to close?", + ); + dialog.set_title("Screensaver Settings"); + dialog.add_buttons(&[ + ("Close Window", gtk::ResponseType::Close), + ("Cancel", gtk::ResponseType::Cancel), + ]); + dialog.set_default_response(gtk::ResponseType::Cancel); + let response = dialog.run(); + dialog.hide(); + match response { + gtk::ResponseType::Close => true, + _ => false, + } + } else { + true + } +} + +fn build_new_configuration(old_config: &Configuration, widgets: &Widgets) -> (Configuration, bool) { + 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()); + let changed = old_config != &new_config; + (new_config, changed) +} diff --git a/util/src/settings.rs b/util/src/settings.rs index 2c1b869..09c812b 100644 --- a/util/src/settings.rs +++ b/util/src/settings.rs @@ -1,8 +1,15 @@ use anyhow::anyhow; -use std::{fs::File, io::Read, path::PathBuf, time::Duration}; +use std::{fmt, fs::{self, File}, io::{Read, Write}, path::PathBuf, time::Duration}; use toml::Value; -#[derive(Debug, Clone, Copy)] +const CONFIG_FILE_RELATIVE_PATH: &str = "bscreensaver/bscreensaver.toml"; + +const LOCK_TIMEOUT: &str = "lock-timeout"; +const BLANK_BEFORE_LOCKING: &str = "blank-before-locking"; +const DIALOG_BACKEND: &str = "dialog-backend"; +const NEW_LOGIN_COMMAND: &str = "new-login-command"; + +#[derive(Debug, Clone, Copy, PartialEq)] pub enum DialogBackend { Gtk3, } @@ -13,6 +20,20 @@ impl DialogBackend { Self::Gtk3 => "bscreensaver-dialog-gtk3", } } + + pub fn display_name(&self) -> &str { + match self { + Self::Gtk3 => "GTK3", + } + } +} + +impl fmt::Display for DialogBackend { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Gtk3 => write!(f, "gtk3"), + } + } } impl TryFrom<&str> for DialogBackend { @@ -25,7 +46,7 @@ impl TryFrom<&str> for DialogBackend { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Configuration { pub lock_timeout: Duration, pub blank_before_locking: Duration, @@ -38,7 +59,7 @@ impl Configuration { use humantime::parse_duration; let config_tomls = xdg::BaseDirectories::new()? - .find_config_files("bscreensaver/bscreensaver.toml") + .find_config_files(CONFIG_FILE_RELATIVE_PATH) .map(|config_path| parse_config_toml(&config_path)) .collect::, _>>()?; @@ -69,16 +90,30 @@ impl Configuration { Ok(config) } } -} -fn parse_config_toml(config_path: &PathBuf) -> anyhow::Result { - let mut f = File::open(config_path)?; - let mut config = String::new(); - f.read_to_string(&mut config)?; - drop(f); + pub fn save(&self) -> anyhow::Result<()> { + use humantime::format_duration; + use toml::map::Map; - let config_toml = config.parse::()?; - Ok(config_toml) + let mut config_map = Map::new(); + 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())); + } + + 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(); + tmp_filename.push(".new"); + let tmp_path = config_path.with_file_name(tmp_filename); + let mut f = File::create(&tmp_path)?; + f.write_all(Value::Table(config_map).to_string().as_bytes())?; + drop(f); + fs::rename(tmp_path, config_path)?; + + Ok(()) + } } impl Default for Configuration { @@ -92,3 +127,12 @@ impl Default for Configuration { } } +fn parse_config_toml(config_path: &PathBuf) -> anyhow::Result { + let mut f = File::open(config_path)?; + let mut config = String::new(); + f.read_to_string(&mut config)?; + drop(f); + + let config_toml = config.parse::()?; + Ok(config_toml) +}