use std::collections::HashMap; use std::path::{Path, PathBuf}; use anyhow::anyhow; use i_slint_core::api::{Image, ToSharedString}; use log::{debug, error, info, trace, warn}; use serde::Deserialize; use serde_aux::prelude::*; use walkdir::WalkDir; use crate::LoginOptionTileData; #[derive(Deserialize, Debug)] pub struct LoginOptionTileConfig { pub(crate) image_path: Option, pub(crate) command: String, #[serde(default = "default_i32::<-1>")] pub(crate) index: i32, #[serde(default)] pub(crate) is_default: bool } pub fn default_cache_dir() -> PathBuf { #[cfg(debug_assertions)] return "./cache/ckgreeter".into(); #[cfg(not(debug_assertions))] return "/var/cache/ckgreeter".into(); } #[derive(Deserialize)] pub struct Config { pub(crate) default_username: Option, #[serde(default)] pub(crate) environments: HashMap, pub(crate) pexels_api_key: Option, pub(crate) pexels_query: Option, #[serde(default = "default_cache_dir", alias = "bg_cache_directory")] pub(crate) background_cache_directory: PathBuf, #[serde(default = "default_i32::<30>", alias = "bg_delay")] pub(crate) background_delay: i32, #[serde(default = "bool_true")] pub(crate) find_wayland_sessions: bool, #[serde(default = "bool_true")] pub(crate) find_x_sessions: bool, pub(crate) default_by_name: Option, } impl Config { pub fn get_tiles(&mut self) -> anyhow::Result<(Vec, Option)> { debug!(target: "ckgreeter::config_get_tiles", "Getting tiles from config"); fn get_sessions_from_dir(dir: &Path) -> anyhow::Result> { let mut ret = HashMap::new(); for entry in std::fs::read_dir(dir)? { if let Err(e) = entry { error!("Failed to read directory: {}", e); continue } let entry = entry?; let file_type = entry.file_type()?; if !file_type.is_file() { continue; } let path = entry.path(); if path.extension().and_then(|s| s.to_str()) == Some("desktop") { let desktop_entry = freedesktop_entry_parser::parse_entry(path)?; let desktop_section = desktop_entry.section("Desktop Entry").expect("Desktop Entry must have a Desktop Entry section"); let session_name = desktop_section .attr("Name").first() .expect("Desktop Entry must have a Name attribute"); let session_exec = desktop_section .attr("Exec").first() .expect("Desktop Entry must have a Exec attribute"); if let Some(session_try_exec) = desktop_section.attr("TryExec").first() { let try_exec_path = Path::new(session_try_exec); if !try_exec_path.exists() || !try_exec_path.is_file() || !is_executable::is_executable(try_exec_path) { continue; } } let desktop_names = desktop_section.attr("DesktopNames"); let mut candidate_svg_paths = Vec::new(); let mut candidate_png_paths = Vec::new(); for entry in WalkDir::new("/usr/share/icons/").follow_links(true) { if let Err(e) = entry { error!("Failed to walk /usr/share/icons/: {}", e); continue } let entry = entry?; if !entry.file_type().is_file() { continue } let path = entry.path().to_owned(); let file_name = path.file_stem().and_then(|s| s.to_str()); let extension = path.extension().and_then(|s| s.to_str()); if file_name.is_none() || extension.is_none() { continue } let file_name = file_name.unwrap(); let extension = extension.unwrap(); if extension != "svg" && extension != "png" { continue } if desktop_names.iter().any(|name| name.eq_ignore_ascii_case(file_name)) { match extension { "png" => candidate_png_paths.push(path), "svg" => candidate_svg_paths.push(path), _ => {} } } } let mut image_path = None; if candidate_svg_paths.len() > 0 { image_path = Some(candidate_svg_paths[0].to_owned()); } else if candidate_png_paths.len() > 0 { image_path = Some(candidate_png_paths[0].to_owned()); } ret.insert(session_name.clone(), LoginOptionTileConfig { image_path, command: session_exec.clone(), index: -1, is_default: false }); } } Ok(ret) } let mut x_sessions = if self.find_x_sessions { info!(target: "ckgreeter::config_get_tiles", "Adding X sessions from `/usr/share/xsessions/`"); Some(get_sessions_from_dir(Path::new("/usr/share/xsessions/"))?) } else {None}; let mut wayland_sessions = if self.find_wayland_sessions { info!(target: "ckgreeter::config_get_tiles", "Adding Wayland sessions from `/usr/share/wayland-sessions/`"); Some(get_sessions_from_dir(Path::new("/usr/share/wayland-sessions/"))?) } else {None}; if let Some(x_sessions) = &mut x_sessions && let Some(wayland_sessions) = &mut wayland_sessions { let mut duplicate_sessions = Vec::new(); for session_name in x_sessions.keys() { if wayland_sessions.contains_key(session_name) { duplicate_sessions.push(session_name.clone()); } } if duplicate_sessions.len() > 0 { warn!(target: "ckgreeter::config_get_tiles", "Found duplicate sessions in Wayland and X11 sessions: {:?}", duplicate_sessions); warn!(target: "ckgreeter::config_get_tiles", "Adding X11 and Wayland name ends to fix."); for session_name in duplicate_sessions { let old_x = x_sessions.remove(&session_name).unwrap(); let old_wl = wayland_sessions.remove(&session_name).unwrap(); x_sessions.insert(format!("{} (X11)", session_name), old_x); wayland_sessions.insert(format!("{} (Wayland)", session_name), old_wl); } } } if let Some(x_sessions) = x_sessions { self.environments.extend(x_sessions); } if let Some(wayland_sessions) = wayland_sessions { self.environments.extend(wayland_sessions); } let mut ordered = self .environments .iter() .collect::>(); debug!(target: "ckgreeter::config_get_tiles", "Sorting tiles"); ordered.sort_by_key(|(name, env)| { let index = if env.index >= 0 { env.index } else { i32::MAX }; (index, name.as_str()) }); let mut tiles: Vec = Vec::new(); let mut default = None; let mut seen_indexes = std::collections::HashSet::new(); for (name, tile_config) in ordered { debug!(target: "ckgreeter::config_get_tiles", "Adding tile `{}`", name); trace!(target: "ckgreeter::config_get_tiles", "Tile config: {:#?}", tile_config); if tile_config.index >= 0 && !seen_indexes.insert(tile_config.index) { return Err(anyhow!( "Duplicate login option index `{}` found near `{}`", tile_config.index, name )); } if tile_config.is_default { if default.is_some() { return Err(anyhow!("There can only be one default login option...")); } debug!(target: "ckgreeter::config_get_tiles", "Setting default tile to `{}`", name); default = Some(tiles.len()); } if let Some(image_path) = &tile_config.image_path { let image = Image::load_from_path(image_path); if let Ok(image) = image { tiles.push(LoginOptionTileData { command: tile_config.command.to_shared_string(), image, name: name.to_shared_string(), }) } else { error!(target: "ckgreeter::config_get_tiles", "Failed to load image for login option `{}` from `{}`", name, image_path.display()); tiles.push(LoginOptionTileData { command: tile_config.command.to_shared_string(), image: Image::default(), name: name.to_shared_string(), }) } } else { tiles.push(LoginOptionTileData { command: tile_config.command.to_shared_string(), image: Image::default(), name: name.to_shared_string(), }) } } if default.is_none() && let Some(default_name) = &self.default_by_name { debug!(target: "ckgreeter::config_get_tiles", "Trying to find default tile `{}` (by name)", default_name); for (index, tile_config) in tiles.iter().enumerate() { if tile_config.name == default_name { if default.is_some() { return Err(anyhow!("There can only be one default login option...")); } debug!(target: "ckgreeter::config_get_tiles", "Setting default tile to `{}`", tile_config.name.as_str()); default = Some(index); break } } } Ok((tiles, default)) } }