moonlock/src/main.rs
nevaforget 484e990c68 fix: elevate CSS priority to override GTK4 user theme (v0.6.4)
Colloid-Catppuccin theme loaded via ~/.config/gtk-4.0/gtk.css at
PRIORITY_USER (800) was overriding moonlock's PRIORITY_APPLICATION (600),
causing avatar to lose its circular border-radius.

- Use STYLE_PROVIDER_PRIORITY_USER for app CSS provider
- Replace border-radius: 50% with 9999px (GTK4 CSS percentage quirk)
2026-03-29 14:24:26 +02:00

239 lines
7.2 KiB
Rust

// 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: &gtk::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);
let bg_texture = config::resolve_background_path(&config)
.and_then(|path| lockscreen::load_background_texture(&path));
if gtk4_session_lock::is_supported() {
activate_with_session_lock(app, &display, bg_texture.as_ref(), &config);
} else {
#[cfg(debug_assertions)]
{
log::warn!("ext-session-lock-v1 not supported — running in development mode");
activate_without_lock(app, bg_texture.as_ref(), &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: &gtk::Application,
display: &gdk::Display,
bg_texture: Option<&gdk::Texture>,
config: &config::Config,
) {
let lock = gtk4_session_lock::Instance::new();
lock.lock();
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<dyn Fn()> = 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<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
let avatar_cache: Rc<RefCell<Option<gdk::Texture>>> = 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::<gdk::Monitor>().ok())
{
let handles = lockscreen::create_lockscreen_window(
bg_texture,
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)");
return;
}
// 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<lockscreen::LockscreenHandles>) {
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: &gtk::Application,
bg_texture: Option<&gdk::Texture>,
config: &config::Config,
) {
let app_clone = app.clone();
let unlock_callback: Rc<dyn Fn()> = 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,
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();
}