use gtk::{glib, prelude::*}; use glib::{clone, Propagation::{Proceed, Stop}}; use log::warn; use std::{env, process::exit, time::Duration, ffi::CString}; 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_combo: gtk::ComboBoxText, custom_new_login_command_entry: gtk::Entry, handle_brightness_keys_checkbox: gtk::CheckButton, } fn main() -> anyhow::Result<()> { init_logging("BSCREENSAVER_SETTINGS"); let config = Configuration::load()?; // Can't use the rust version as it requires gtk_init() to be // called first, but the underlying C function requires that // it hasn't. let backends = CString::new("x11").unwrap(); unsafe { gtk::gdk::ffi::gdk_set_allowed_backends(backends.as_ptr()); }; 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::>()).into()); } 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 vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(8) .build(); 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 hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(8) .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 handle_brightness_keys_checkbox = gtk::CheckButton::builder() .label("Handle brightness keys") .active(config.handle_brightness_keys) .build(); hbox.pack_start(&handle_brightness_keys_checkbox, false, false, 0); let widgets = Widgets { lock_timeout: lock_timeout_spinbutton.clone(), blank_before_locking: blank_before_locking_spinbutton.clone(), new_login_command_combo: new_login_command_combo.clone(), custom_new_login_command_entry: custom_new_login_command_entry.clone(), handle_brightness_keys_checkbox: handle_brightness_keys_checkbox.clone(), }; mainwin.connect_delete_event(clone!(@strong config, @strong widgets, @strong app, @strong mainwin => move |_,_| { if !confirm_cancel(&config, &widgets, &mainwin) { Stop } else { Proceed } })); custom_new_login_command_button.connect_clicked(clone!(@strong mainwin, @strong widgets => move |_| { run_file_chooser(&mainwin, &widgets); })); 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("Custom 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.custom_new_login_command_entry.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 = 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 }, }; new_config.handle_brightness_keys = widgets.handle_brightness_keys_checkbox.is_active(); let changed = old_config != &new_config; (new_config, changed) }