use crate::config::Config; use crate::input::KeyboardLockStates; use crate::{GreeterData, GreeterDisplay, LoginOptionTileData}; use anyhow::{Context, anyhow}; use greetd_ipc::codec::TokioCodec; use greetd_ipc::{AuthMessageType, ErrorType, Request, Response}; use i_slint_core::api::{ComponentHandle, Global, Image, Rgba8Pixel, SharedPixelBuffer, Weak}; use i_slint_core::model::{Model, VecModel}; use log::{debug, error, info, trace}; use magick_rust::MagickWand; use pexels_api::{PexelsClient, SearchParams}; use rand::seq::SliceRandom; use std::collections::VecDeque; use std::fmt::Display; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use tokio::fs::File; use tokio::io::{AsyncBufReadExt, BufReader, Lines}; use tokio::net::UnixStream; #[cfg(not(debug_assertions))] use slint::SharedString; use tokio_stream::StreamExt; use tokio_stream::wrappers::ReadDirStream; #[cfg(not(debug_assertions))] use zbus::{Connection, proxy}; pub fn register_login_callback( data: &GreeterData, display_weak: Weak, tiles: Rc>, ) { data.on_login({ let tiles = tiles.clone(); let display_weak = display_weak.clone(); move |username, password| { debug!(target: "ckgreeter::callbacks::login", "----- Login callback called"); let tiles = tiles.clone(); let display_weak = display_weak.clone(); debug!(target: "ckgreeter::callbacks::login", "spawning slint thread"); slint::spawn_local(async move { debug!(target: "ckgreeter::callbacks::login", "spawned slint thread"); let display = display_weak.unwrap(); let username = username.to_string(); let password = password.to_string(); debug!(target: "ckgreeter::callbacks::login", "username: {username:?}"); let index = display.get_selected_index(); let tile = &tiles.row_data(index as usize); let tile = match tile { Some(tile) => tile, None => { display.invoke_set_error("Failed to login: Invalid index for tile".into()); error!("Failed to login: Invalid index for tile: {index}"); return; } }; match login(username.as_str(), password.as_str(), tile.command.as_str()).await { Ok(successful) => { if !successful { display.invoke_set_error("Failed to login: Bad Login".into()); error!("Failed to login: Bad Login"); return; } if let Err(err) = slint::quit_event_loop() { display .invoke_set_error(format!("Failed to exit greeter: {err}").into()); error!("Failed to exit greeter: {err}"); } } Err(err) => { display.invoke_set_error(format!("Failed to login: {err}").into()); error!("Failed to login: {err}"); return; } }; }) .unwrap(); } }); async fn login(username: &str, password: &str, cmd: &str) -> anyhow::Result { if username.trim().is_empty() { return Err(anyhow::anyhow!("Username is required")); } if cmd.trim().is_empty() { return Err(anyhow::anyhow!("Command is required")); } debug!(target: "ckgreeter::callbacks::login", "connecting to greetd socket"); let socket = std::env::var("GREETD_SOCK").context("GREETD_SOCK is not set")?; let mut stream = UnixStream::connect(socket) .await .context("failed to connect to greetd socket")?; debug!(target: "ckgreeter::callbacks::login", "connected to greetd socket"); let mut next_request = Request::CreateSession { username: username.to_string(), }; trace!(target: "ckgreeter::callbacks::login", "sending request to create session: {next_request:?}"); let mut starting = false; loop { next_request.write_to(&mut stream).await?; match Response::read_from(&mut stream).await? { Response::AuthMessage { auth_message: _auth_message, auth_message_type, } => { let response = match auth_message_type { AuthMessageType::Visible => None, AuthMessageType::Secret => { if !password.is_empty() { Some(password.to_string()) } else { None } } AuthMessageType::Info => None, AuthMessageType::Error => None, }; trace!(target: "ckgreeter::callbacks::login", "sending request: {next_request:?}"); next_request = Request::PostAuthMessageResponse { response }; } Response::Success => { if starting { debug!(target: "ckgreeter::callbacks::login", "starting session"); return Ok(true); } else { starting = true; trace!(target: "ckgreeter::callbacks::login", "sending request: {next_request:?}"); next_request = Request::StartSession { env: vec![], cmd: vec![cmd.to_string()], } } } Response::Error { error_type, description, } => { debug!(target: "ckgreeter::callbacks::login", "error logging in"); Request::CancelSession.write_to(&mut stream).await?; return match error_type { ErrorType::AuthError => Ok(false), ErrorType::Error => Err(anyhow::anyhow!("login error: {description:?}")), }; } } } } } pub fn register_power_callback(_data: &GreeterData) { #[cfg(not(debug_assertions))] _data.on_loginctl(move |command: SharedString| { debug!(target: "ckgreeter::callbacks::power", "----- Power callback called"); slint::spawn_local(async move { if let Err(err) = handle_power_command(command.as_str()).await { eprintln!("Power command failed: {err}"); } }) .unwrap(); }); #[cfg(not(debug_assertions))] async fn handle_power_command(command: &str) -> anyhow::Result<()> { debug!(target: "ckgreeter::callbacks::power", "command: {command:?}"); trace!(target: "ckgreeter::callbacks::power", "connecting to dbus"); let connection = Connection::system().await?; trace!(target: "ckgreeter::callbacks::power", "connected to dbus"); trace!(target: "ckgreeter::callbacks::power", "getting power manager proxy"); let proxy = PowerManagerProxy::new(&connection).await?; trace!(target: "ckgreeter::callbacks::power", "got power manager proxy"); debug!(target: "ckgreeter::callbacks::power", "running power command: {command}"); match command { "suspend" => proxy.suspend(&false).await?, "poweroff" => proxy.power_off(&false).await?, "reboot" => proxy.reboot(&false).await?, command => return Err(anyhow::anyhow!("Unknown power command: {command}")), } Ok(()) } #[cfg(not(debug_assertions))] #[proxy( interface = "org.freedesktop.login1.Manager", default_service = "org.freedesktop.login1", default_path = "/org/freedesktop/login1" )] trait PowerManager { fn suspend(&self, interactive: &bool) -> zbus::Result<()>; fn reboot(&self, interactive: &bool) -> zbus::Result<()>; fn power_off(&self, interactive: &bool) -> zbus::Result<()>; } } pub fn register_lock_state_callback(data: &GreeterData, data_weak: Weak>) { data.on_check_lock_states({ let data_weak = data_weak.clone(); move || { let data_weak = data_weak.clone(); slint::spawn_local(async move { debug!(target: "ckgreeter::callbacks::lock_state", "----- lock state callback called"); let data = data_weak.unwrap(); if let Some(states) = KeyboardLockStates::get_locks_state() { data.set_caps_lock_state(states.caps_lock); trace!(target: "ckgreeter::callbacks::lock_state", "caps lock state: {}", states.caps_lock); data.set_num_lock_state(states.num_lock); trace!(target: "ckgreeter::callbacks::lock_state", "num lock state: {}", states.num_lock); data.set_scroll_lock_state(states.scroll_lock); trace!(target: "ckgreeter::callbacks::lock_state", "scroll lock state: {}", states.scroll_lock) } }) .unwrap(); } }); } pub fn register_wallpaper_callback( data: &GreeterData, display_weak: Weak, data_weak: Weak>, cache_dir: PathBuf, config: &Config, ) { //TODO: Seperate this out into seperate methods, cleanup!!! const MAX_RECENT_WALLPAPERS: usize = 25; async fn read_lines

(filename: P) -> std::io::Result>> where P: AsRef + std::fmt::Debug, { debug!(target: "ckgreeter::callbacks::wallpaper::read_lines", "reading lines from file: {:?}", filename); let file = File::open(filename).await?; Ok(BufReader::new(file).lines()) } async fn write_lines

(filename: &P, lines: VecDeque) -> std::io::Result<()> where P: AsRef + std::fmt::Debug, { let mut contents = String::new(); for line in lines { contents.push_str(&line); contents.push('\n'); } debug!(target: "ckgreeter::callbacks::wallpaper::write_lines", "writing lines to file: {:?}", filename); std::fs::write(filename, contents) } fn set_background_image( display: &GreeterDisplay, data: &GreeterData, pixels: &[u8], w: u32, h: u32, ) { debug!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "setting background image"); if !display.get_background_toggle() { trace!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "setting background image 1"); let old = data.get_wallpaper_image(); data.set_wallpaper_image(Image::from_rgba8( SharedPixelBuffer::::clone_from_slice(pixels, w, h), )); trace!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "set background image 1"); drop(old); } else { trace!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "setting background image 2"); let old = data.get_wallpaper_image_1(); data.set_wallpaper_image_1(Image::from_rgba8( SharedPixelBuffer::::clone_from_slice(pixels, w, h), )); trace!(target: "ckgreeter::callbacks::wallpaper::set_background_image", "set background image 2"); drop(old); } } fn export_wand_rgba(wand: &MagickWand) -> anyhow::Result<(u32, u32, Vec)> { trace!(target: "ckgreeter::callbacks::wallpaper::export_wand_rgba", "exporting wand"); let pixels = wand .export_image_pixels( 0, 0, wand.get_image_width(), wand.get_image_height(), "RGBA", ) .ok_or_else(|| anyhow!("Failed to export image pixels"))?; trace!(target: "ckgreeter::callbacks::wallpaper::export_wand_rgba", "exported wand"); Ok(( wand.get_image_width() as u32, wand.get_image_height() as u32, pixels, )) } let reqwest_client = match reqwest::ClientBuilder::new() .tls_backend_rustls() .http2_prior_knowledge() .http2_keep_alive_timeout(std::time::Duration::from_secs(10)) .build() { Ok(client) => client, Err(err) => { eprintln!("Failed to create reqwest client: {err}"); return; } }; debug!(target: "ckgreeter::callbacks::wallpaper", "reqwest client created"); if let Some(pexels_api_key) = config.pexels_api_key.clone() { info!(target: "ckgreeter::callbacks::wallpaper", "Using pexels api"); debug!(target: "ckgreeter::callbacks::wallpaper", "pexels api key found"); let pexels_client = Arc::new(PexelsClient::new(pexels_api_key)); let pexels_query = config .pexels_query .clone() .unwrap_or_else(|| String::from("nature")); debug!(target: "ckgreeter::callbacks::wallpaper", "pexels query: {pexels_query}"); info!(target: "ckgreeter::callbacks::wallpaper", "Registering wallpaper callback"); data.on_get_new_wallpaper({ let data_weak = data_weak.clone(); let display_weak = display_weak.clone(); move || { debug!(target: "ckgreeter::callbacks::wallpaper", "----- wallpaper callback called"); let pexels_client = pexels_client.clone(); let reqwest_client = reqwest_client.clone(); let cache_dir = cache_dir.clone(); let pexels_query = pexels_query.clone(); let display_weak = display_weak.clone(); let data_weak = data_weak.clone(); slint::spawn_local(async move { trait ResultExt { fn wpaper_err(self, data: &GreeterData) -> Result; } impl ResultExt for Result { fn wpaper_err(self, data: &GreeterData) -> Result { self.inspect_err(|e| { error!(target: "ckgreeter::callbacks::wallpaper", "Failed to get wallpaper: {e}"); data.set_wallpaper_author("Error".into()); data.set_wallpaper_alt(e.to_string().into()); data.set_wallpaper_url("Failed to get wallpaper".into()); }) } } let data = data_weak.unwrap(); let display = display_weak.unwrap(); let last_used_log = cache_dir.join("last_used_log.txt"); debug!(target: "ckgreeter::callbacks::wallpaper", "last used log: {:?}", last_used_log); if !tokio::fs::try_exists(&last_used_log).await.wpaper_err(&data)? { File::create(&last_used_log).await.wpaper_err(&data)?; debug!(target: "ckgreeter::callbacks::wallpaper", "created last used log"); } let mut file_lines = tokio::time::timeout( std::time::Duration::from_secs(5), read_lines(&last_used_log), ) .await.wpaper_err(&data)?.wpaper_err(&data)?; debug!(target: "ckgreeter::callbacks::wallpaper", "read last used log"); let mut idx = 0; let mut last_used_lines = VecDeque::with_capacity(MAX_RECENT_WALLPAPERS); while let Some(line) = file_lines.next_line().await.wpaper_err(&data)? { last_used_lines.push_back(line); idx += 1; if idx > MAX_RECENT_WALLPAPERS { last_used_lines.pop_front(); } } drop(file_lines); let mut page = 0; loop { debug!(target: "ckgreeter::callbacks::wallpaper", "fetched photos list from pexels api"); let mut is_photo_set: bool = false; let mut found_photo: bool = false; match pexels_client .search_photos( pexels_query.as_str(), &SearchParams::new() .page(page) .per_page(10) .size(pexels_api::Size::Medium) .orientation(pexels_api::Orientation::Landscape), ) .await { Ok(photos) => { if photos.total_results > 0 { found_photo = true; debug!(target: "ckgreeter::callbacks::wallpaper", "got photos from pexels api"); for photo in photos.photos { let string_id = photo.id.to_string(); trace!(target: "ckgreeter::callbacks::wallpaper", "pexels photo id: {}", string_id); if last_used_lines.contains(&string_id) { trace!(target: "ckgreeter::callbacks::wallpaper", "pexels photo id already used recently"); continue; } is_photo_set = true; last_used_lines.push_back(string_id); if last_used_lines.len() > MAX_RECENT_WALLPAPERS { last_used_lines.pop_front(); } let image_path = cache_dir.join(format!("{}.png", photo.id)); trace!(target: "ckgreeter::callbacks::wallpaper", "pexels photo image path: {:?}", image_path); let _ = if !tokio::fs::try_exists(&image_path).await.wpaper_err(&data)? { debug!(target: "ckgreeter::callbacks::wallpaper", "pexels photo image not found, downloading"); let response = reqwest_client .get(photo.src.large2x.clone()) // TODO: determine which photo src size is best, maybe determine based on the monitor size to reduce memory pressure .send() .await.wpaper_err(&data)?; let image_bytes = response.bytes().await.wpaper_err(&data)?; debug!(target: "ckgreeter::callbacks::wallpaper", "pexels photo image downloaded"); let size = display.window().size(); debug!(target: "ckgreeter::callbacks::wallpaper", "resizing image with ImageMagick"); let (w, h, pixels) = tokio::task::spawn_blocking(move || { let wand = MagickWand::new(); wand.read_image_blob(&image_bytes)?; drop(image_bytes); if size.width >= size.height { wand.fit( size.width as usize, size.width as usize, ); } else { wand.fit( size.height as usize, size.height as usize, ); } trace!(target: "ckgreeter::callbacks::wallpaper", "exporting png data"); let png_data = wand.write_image_blob("png")?; std::fs::write(image_path, png_data)?; trace!(target: "ckgreeter::callbacks::wallpaper", "exported png data to disk"); export_wand_rgba(&wand) }) .await.wpaper_err(&data)?.wpaper_err(&data)?; debug!(target: "ckgreeter::callbacks::wallpaper", "image resized with ImageMagick"); set_background_image( &display, &data, pixels.as_slice(), w, h, ); debug!(target: "ckgreeter::callbacks::wallpaper", "set background image"); drop(pixels); } else { debug!(target: "ckgreeter::callbacks::wallpaper", "pexels photo image found, using cached"); let contents = tokio::fs::read(image_path).await.wpaper_err(&data)?; debug!(target: "ckgreeter::callbacks::wallpaper", "resizing image with ImageMagick"); let (w, h, pixels) = tokio::task::spawn_blocking(move || { let wand = MagickWand::new(); wand.read_image_blob(contents.as_slice())?; export_wand_rgba(&wand) }) .await.wpaper_err(&data)?.wpaper_err(&data)?; debug!(target: "ckgreeter::callbacks::wallpaper", "image resized with ImageMagick"); set_background_image( &display, &data, pixels.as_slice(), w, h, ); debug!(target: "ckgreeter::callbacks::wallpaper", "set background image"); drop(pixels); }; debug!(target: "ckgreeter::callbacks::wallpaper", "set pexels photo author: {}", photo.photographer); data.set_wallpaper_author(photo.photographer.into()); let photo_alt = photo.alt.unwrap_or("".into()); debug!(target: "ckgreeter::callbacks::wallpaper", "set pexels photo alt: {}", photo_alt); data.set_wallpaper_alt( photo_alt.into(), ); debug!(target: "ckgreeter::callbacks::wallpaper", "set pexels photo url: {}", photo.url); data.set_wallpaper_url(photo.url.into()); debug!(target: "ckgreeter::callbacks::wallpaper", "switching background to foreground"); display.invoke_switch_background(); break; } } } Err(err) => { error!("Failed to fetch online images, using cached. {err}"); debug!(target: "ckgreeter::callbacks::wallpaper", "getting cached wallpaper images from disk"); let mut photos = ReadDirStream::new(tokio::fs::read_dir(&cache_dir).await.wpaper_err(&data)?) .filter(|de| match de { Ok(de) => { de.path().is_file() && de.path().extension() == Some(std::ffi::OsStr::new("png")) } Err(_) => false, }) .collect::>() .await; debug!(target: "ckgreeter::callbacks::wallpaper", "got cached wallpaper images from disk"); photos.shuffle(&mut rand::rng()); debug!(target: "ckgreeter::callbacks::wallpaper", "shuffled cached wallpaper images"); let mut photos = tokio_stream::iter(photos); while let Some(photo) = photos.next().await { match photo { Ok(photo) => { let photo_path = photo.path(); let string_id = photo_path .file_prefix() .context("failed to get filename").wpaper_err(&data)? .to_string_lossy() .to_string(); if last_used_lines .front() .is_some_and(|first| first == &string_id) { continue; } last_used_lines.push_back(string_id); if last_used_lines.len() > MAX_RECENT_WALLPAPERS { last_used_lines.pop_front(); } debug!(target: "ckgreeter::callbacks::wallpaper", "using cached wallpaper image: {:?}", photo_path); debug!(target: "ckgreeter::callbacks::wallpaper", "resizing image with ImageMagick"); let (w, h, pixels) = tokio::task::spawn_blocking(move || { let wand = MagickWand::new(); wand.read_image(photo_path.to_str().context( "failed to convert image path to UTF-8", )?)?; export_wand_rgba(&wand) }) .await.wpaper_err(&data)?.wpaper_err(&data)?; debug!(target: "ckgreeter::callbacks::wallpaper", "image resized with ImageMagick"); set_background_image( &display, &data, pixels.as_slice(), w, h, ); debug!(target: "ckgreeter::callbacks::wallpaper", "set background image"); drop(pixels); data.set_wallpaper_author("Unknown".into()); data.set_wallpaper_alt("".into()); data.set_wallpaper_url("Cached wallpaper".into()); debug!(target: "ckgreeter::callbacks::wallpaper", "switching background to foreground"); display.invoke_switch_background(); break; } Err(_err) => { continue; } } } } } if is_photo_set || !found_photo { break; } page += 1; } tokio::time::timeout( std::time::Duration::from_secs(5), write_lines(&last_used_log, last_used_lines), ) .await .context("timed out while writing wallpaper log").wpaper_err(&data)?.wpaper_err(&data)?; return anyhow::Ok(()); }).unwrap(); } }); } } pub fn register_get_default_font_size(data: &GreeterData, display_weak: Weak) { data.on_get_default_font_size(move || { debug!(target: "ckgreeter::callbacks::font_size", "----- get default font size callback called"); let display = display_weak.unwrap(); let size = display.window().size(); trace!(target: "ckgreeter::callbacks::font_size", "window size: {:?}", size); let font_size = ((size.width as f32).min(size.height as f32)) * 0.01; trace!(target: "ckgreeter::callbacks::font_size", "font size: {}", font_size); font_size }); } pub fn register_callbacks( display: &GreeterDisplay, data: &GreeterData, tiles: Rc>, cache_dir: PathBuf, config: &Config, ) { let display_weak = display.as_weak(); let data_weak = data.as_weak(); debug!(target: "ckgreeter::callbacks", "Registering callbacks"); register_login_callback(data, display_weak.clone(), tiles); debug!(target: "ckgreeter::callbacks", "Registered login callback"); register_power_callback(data); debug!(target: "ckgreeter::callbacks", "Registered power callback"); register_lock_state_callback(data, data_weak.clone()); debug!(target: "ckgreeter::callbacks", "Registered lock state callback"); register_wallpaper_callback( data, display_weak.clone(), data_weak.clone(), cache_dir, config, ); debug!(target: "ckgreeter::callbacks", "Registered clean inactive wallpaper callback"); register_get_default_font_size(data, display_weak.clone()); debug!(target: "ckgreeter::callbacks", "Registered get default font size callback"); }