Add simple (if ugly) settings dialog
This commit is contained in:
		
							
								
								
									
										11
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -6,6 +6,7 @@ members = [ | ||||
|     "dbus-service", | ||||
|     "dialog-gtk3", | ||||
|     "util", | ||||
|     "settings", | ||||
|     "systemd", | ||||
|     "xcb-xembed", | ||||
| ] | ||||
|   | ||||
							
								
								
									
										9
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								settings/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								settings/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||
							
								
								
									
										10
									
								
								settings/bscreensaver-settings.desktop
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								settings/bscreensaver-settings.desktop
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
							
								
								
									
										249
									
								
								settings/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								settings/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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::<Vec<String>>())); | ||||
| } | ||||
|  | ||||
| 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) | ||||
| } | ||||
| @@ -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::<Result<Vec<Value>, _>>()?; | ||||
|  | ||||
| @@ -69,16 +90,30 @@ impl Configuration { | ||||
|             Ok(config) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn parse_config_toml(config_path: &PathBuf) -> anyhow::Result<Value> { | ||||
|     let mut f = File::open(config_path)?; | ||||
|     let mut config = String::new(); | ||||
|     f.read_to_string(&mut config)?; | ||||
|     pub fn save(&self) -> anyhow::Result<()> { | ||||
|         use humantime::format_duration; | ||||
|         use toml::map::Map; | ||||
|  | ||||
|         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)?; | ||||
|  | ||||
|     let config_toml = config.parse::<Value>()?; | ||||
|     Ok(config_toml) | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for Configuration { | ||||
| @@ -92,3 +127,12 @@ impl Default for Configuration { | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn parse_config_toml(config_path: &PathBuf) -> anyhow::Result<Value> { | ||||
|     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::<Value>()?; | ||||
|     Ok(config_toml) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user