use chrono::prelude::*; use gdkx11::X11Window; use gethostname::gethostname; use glib::GString; use gtk::{prelude::*, Button, Entry, Label, Plug, Window}; use log::{debug, error}; use std::{io::{self, Write}, process::exit, rc::Rc, thread, time::Duration}; use bscreensaver_util::init_logging; const DIALOG_UPDATE_INTERVAL: Duration = Duration::from_millis(100); const DIALOG_TIMEOUT: Duration = Duration::from_secs(60); fn main() -> anyhow::Result<()> { init_logging("BSCREENSAVER_DIALOG_GTK3_LOG"); let standalone = std::env::var("BSCREENSAVER_DIALOG_STANDALONE").is_ok(); unsafe { glib::log_writer_default_set_use_stderr(true) }; gtk::init()?; let top_sg = gtk::SizeGroup::builder() .mode(gtk::SizeGroupMode::Horizontal) .build(); let label_sg = gtk::SizeGroup::builder() .mode(gtk::SizeGroupMode::Horizontal) .build(); let entry_sg = gtk::SizeGroup::builder() .mode(gtk::SizeGroupMode::Horizontal) .build(); let header = gtk::HeaderBar::builder() .title("Unlock Screen") .show_close_button(true) .build(); let top_vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(0) .build(); let dialog = if standalone { let win = Window::builder() .type_(gtk::WindowType::Toplevel) .title("Unlock Screen") .modal(true) .build(); win.set_titlebar(Some(&header)); win.upcast::() } else { let plug = Plug::builder() .type_(gtk::WindowType::Toplevel) .title("Unlock Screen") .modal(true) .build(); plug.connect_embedded(|_| debug!("DIALOG EMBEDDED")); plug.connect_embedded_notify(|_| debug!("DIALOG EMBEDDED (notify)")); plug.connect_realize(|plug| { let plug_window = match plug.window().unwrap().downcast::() { Err(err) => { error!("Failed to find XID of unlock dialog window: {}", err); exit(2); }, Ok(w) => w, }; let xid = plug_window.xid() as u32; let xid_buf: [u8; 4] = [ ((xid >> 24) & 0xff) as u8, ((xid >> 16) & 0xff) as u8, ((xid >> 8) & 0xff) as u8, (xid & 0xff) as u8, ]; let out = io::stdout(); let mut out_locked = out.lock(); if let Err(err) = out_locked.write_all(&xid_buf).and_then(|_| out_locked.flush()) { error!("Failed to write XID to stdout: {}", err); exit(2); }; }); // Walking the header's widget tree, finding the close button, and connecting // to the 'clicked' signal strangely doesn't work either, so let's just // disable it for now. header.set_show_close_button(false); top_vbox.pack_start(&header, true, false, 0); plug.upcast::() }; // I don't know why, but this doesn't work when we're a GktPlug, despite // an examination of the gtk source suggesting that the header should send // a delete-event to the toplevel (which should be the plug) when the close // button is clicked. For some reason, though, we never get the delete-event. dialog.connect_delete_event(|_, _| exit(1)); dialog.connect_realize(|_| debug!("DIALOG REALIZED")); dialog.connect_map(|_| debug!("DIALOG MAPPED")); dialog.connect_unmap(|_| debug!("DIALOG UNMAPPED")); dialog.connect_unrealize(|_| debug!("DIALOG_UNREALIZED")); dialog.add(&top_vbox); let top_hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .border_width(48) .spacing(8) .build(); top_vbox.pack_start(&top_hbox, true, true, 0); let vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) .build(); top_sg.add_widget(&vbox); top_hbox.pack_start(&vbox, true, true, 0); let attrs = gtk::pango::AttrList::new(); attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_XX_LARGE)); let mut bold_desc = gtk::pango::FontDescription::new(); bold_desc.set_weight(gtk::pango::Weight::Bold); attrs.insert(gtk::pango::AttrFontDesc::new(&bold_desc)); let label = gtk::Label::builder() .label(gethostname().to_str().unwrap_or("(unknown hostname)")) .xalign(0.5) .yalign(0.5) .attributes(&attrs) .build(); vbox.pack_start(&label, false, false, 0); let attrs = gtk::pango::AttrList::new(); attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_LARGE)); let label = gtk::Label::builder() .xalign(0.5) .yalign(0.5) .attributes(&attrs) .build(); set_time_label(&label); vbox.pack_start(&label, false, false, 0); glib::timeout_add_seconds_local(1, move || { set_time_label(&label); glib::source::Continue(true) }); let sep = gtk::Separator::builder() .orientation(gtk::Orientation::Vertical) .build(); top_hbox.pack_start(&sep, true, false, 0); let vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) .build(); top_sg.add_widget(&vbox); top_hbox.pack_start(&vbox, true, true, 0); let hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(8) .build(); vbox.pack_start(&hbox, true, true, 2); let label = Label::builder() .label("Username:") .xalign(0.0) .build(); label_sg.add_widget(&label); hbox.pack_start(&label, false, true, 8); let username = bscreensaver_util::get_username()?; let username_box = Entry::builder() .text(&username) .sensitive(false) .build(); entry_sg.add_widget(&username_box); hbox.pack_start(&username_box, true, true, 8); let hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(8) .build(); vbox.pack_start(&hbox, true, true, 0); let label = Label::builder() .label("Password:") .xalign(0.0) .build(); label_sg.add_widget(&label); hbox.pack_start(&label, false, true, 8); let password_box = Rc::new(Entry::builder() .visibility(false) .input_purpose(gtk::InputPurpose::Password) .activates_default(true) .width_chars(25) .build()); entry_sg.add_widget(&*password_box); hbox.pack_start(&*password_box, true, true, 8); password_box.connect_key_press_event(|_, ev| { if ev.keyval().name() == Some(GString::from("Escape")) { exit(1); } gtk::Inhibit(false) }); password_box.grab_focus(); let hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(8) .build(); vbox.pack_start(&hbox, true, true, 2); let button = Button::builder() .label("Unlock") .build(); { let password_box = Rc::clone(&password_box); button.connect_clicked(move |button| { button.set_sensitive(false); password_box.set_sensitive(false); let username = username.clone(); let password = password_box.text().to_string(); thread::spawn(move || { if authenticate(&username, &password) { exit(0); } else { exit(-1); } }); }); } hbox.pack_end(&button, false, true, 8); button.set_can_default(true); button.set_has_default(true); let timer = Rc::new(gtk::ProgressBar::builder() .orientation(gtk::Orientation::Horizontal) .fraction(1.0) .show_text(false) .can_focus(false) .margin(2) .build()); top_vbox.pack_end(&*timer, false, false, 0); { let timer = Rc::clone(&timer); let delta = (DIALOG_UPDATE_INTERVAL.as_millis() as f64) / (DIALOG_TIMEOUT.as_millis() as f64); gtk::glib::source::timeout_add_local(DIALOG_UPDATE_INTERVAL, move || { let new_fraction = timer.fraction() - delta; if new_fraction <= 0.0 { exit(1); } timer.set_fraction(new_fraction); Continue(true) }); } { let timer = Rc::clone(&timer); password_box.connect_key_press_event(move |_, _| { let new_fraction = timer.fraction() + 0.05; timer.set_fraction(if new_fraction >= 1.0 { 1.0 } else { new_fraction }); Inhibit(false) }); } dialog.show_all(); gtk::main(); Ok(()) } fn set_time_label(label: >k::Label) { let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); label.set_label(&now); } fn authenticate(username: &String, password: &String) -> bool { let mut authenticator = match pam::Authenticator::with_password("xscreensaver") { Err(err) => { error!("[PAM] {}", err); return false; }, Ok(authenticator) => authenticator, }; authenticator.get_handler().set_credentials(username, password); if let Err(err) = authenticator.authenticate() { error!("[PAM] {}", err); return false; } if let Err(err) = authenticator.open_session() { error!("[PAM] {}", err); false } else { true } }