// ABOUTME: Entry point for Moonlock — secure Wayland lockscreen. // ABOUTME: Sets up GTK Application, ext-session-lock-v1, Panic-Hook, and multi-monitor windows. mod auth; mod config; mod fingerprint; mod i18n; mod lockscreen; mod power; mod users; use gdk4 as gdk; use gtk4::prelude::*; use gtk4::{self as gtk, gio}; use gtk4_session_lock; use std::cell::{Cell, RefCell}; use std::rc::Rc; use crate::fingerprint::FingerprintListener; fn load_css(display: &gdk::Display) { let css_provider = gtk::CssProvider::new(); css_provider.load_from_resource("/dev/moonarch/moonlock/style.css"); gtk::style_context_add_provider_for_display( display, &css_provider, gtk::STYLE_PROVIDER_PRIORITY_USER, ); } fn activate(app: >k::Application) { let display = match gdk::Display::default() { Some(d) => d, None => { log::error!("No display available — cannot start lockscreen UI"); return; } }; load_css(&display); let config = config::load_config(None); if gtk4_session_lock::is_supported() { activate_with_session_lock(app, &display, &config); } else { #[cfg(debug_assertions)] { log::warn!("ext-session-lock-v1 not supported — running in development mode"); activate_without_lock(app, &config); } #[cfg(not(debug_assertions))] { log::error!("ext-session-lock-v1 not supported — refusing to run without session lock"); std::process::exit(1); } } } fn activate_with_session_lock( app: >k::Application, display: &gdk::Display, config: &config::Config, ) { let lock = gtk4_session_lock::Instance::new(); lock.lock(); // Load wallpaper AFTER lock — disk I/O must not delay the lock acquisition let bg_texture = config::resolve_background_path(config) .and_then(|path| lockscreen::load_background_texture(&path)); let monitors = display.monitors(); // Shared unlock callback — unlocks session and quits. // Guard prevents double-unlock if PAM and fingerprint succeed simultaneously. let lock_clone = lock.clone(); let app_clone = app.clone(); let already_unlocked = Rc::new(Cell::new(false)); let au = already_unlocked.clone(); let unlock_callback: Rc = Rc::new(move || { if au.get() { log::debug!("Unlock already triggered, ignoring duplicate"); return; } au.set(true); lock_clone.unlock(); app_clone.quit(); }); // Shared caches for multi-monitor — first monitor renders, rest reuse let blur_cache: Rc>> = Rc::new(RefCell::new(None)); let avatar_cache: Rc>> = Rc::new(RefCell::new(None)); // Create all monitor windows immediately — no D-Bus calls here let mut all_handles = Vec::new(); let mut created_any = false; for i in 0..monitors.n_items() { if let Some(monitor) = monitors .item(i) .and_then(|obj| obj.downcast::().ok()) { let handles = lockscreen::create_lockscreen_window( bg_texture.as_ref(), config, app, unlock_callback.clone(), &blur_cache, &avatar_cache, ); lock.assign_window_to_monitor(&handles.window, &monitor); handles.window.present(); all_handles.push(handles); created_any = true; } } if !created_any { log::error!("No lockscreen windows created — screen stays locked (compositor policy)"); std::process::exit(1); } // Async fprintd initialization — runs after windows are visible if config.fingerprint_enabled { init_fingerprint_async(all_handles); } } /// Initialize fprintd asynchronously after windows are visible. /// Uses a single FingerprintListener shared across all monitors — /// only the first monitor's handles get the fingerprint UI wired up. fn init_fingerprint_async(all_handles: Vec) { glib::spawn_future_local(async move { let mut listener = FingerprintListener::new(); listener.init_async().await; // Use the first monitor's username to check enrollment let username = &all_handles[0].username; if username.is_empty() { return; } if !listener.is_available_async(username).await { log::debug!("fprintd not available or no enrolled fingers"); return; } let fp_rc = Rc::new(RefCell::new(listener)); // Show fingerprint label on all monitors for handles in &all_handles { lockscreen::show_fingerprint_label(handles, &fp_rc); } // Start verification listener on the first monitor only lockscreen::start_fingerprint(&all_handles[0], &fp_rc); }); } #[cfg(debug_assertions)] fn activate_without_lock( app: >k::Application, config: &config::Config, ) { let bg_texture = config::resolve_background_path(config) .and_then(|path| lockscreen::load_background_texture(&path)); let app_clone = app.clone(); let unlock_callback: Rc = Rc::new(move || { app_clone.quit(); }); let blur_cache = Rc::new(RefCell::new(None)); let avatar_cache = Rc::new(RefCell::new(None)); let handles = lockscreen::create_lockscreen_window( bg_texture.as_ref(), config, app, unlock_callback, &blur_cache, &avatar_cache, ); handles.window.set_default_size(800, 600); handles.window.present(); // Async fprintd initialization for development mode if config.fingerprint_enabled { init_fingerprint_async(vec![handles]); } } fn setup_logging() { match systemd_journal_logger::JournalLog::new() { Ok(logger) => { if let Err(e) = logger.install() { eprintln!("Failed to install journal logger: {e}"); } } Err(e) => { eprintln!("Failed to create journal logger: {e}"); } } let level = if std::env::var("MOONLOCK_DEBUG").is_ok() { log::LevelFilter::Debug } else { log::LevelFilter::Info }; log::set_max_level(level); } fn install_panic_hook() { // Install a panic hook BEFORE starting the app. // On panic, we log but NEVER unlock. The compositor's ext-session-lock-v1 // policy keeps the screen locked when the client crashes. let default_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { log::error!("PANIC — screen stays locked (compositor policy): {info}"); default_hook(info); })); } fn main() { install_panic_hook(); setup_logging(); // Root check — moonlock should not run as root if nix::unistd::getuid().is_root() { log::error!("Moonlock should not run as root"); std::process::exit(1); } log::info!("Moonlock starting"); // Register compiled GResources gio::resources_register_include!("moonlock.gresource").expect("Failed to register resources"); let app = gtk::Application::builder() .application_id("dev.moonarch.moonlock") .build(); app.connect_activate(activate); app.run(); }