CKGreeter/src/config.rs
CanadianBaconBoi 66d90100df Add License and README, and add XDG session detection
- Updated dependencies in cargo.toml
- Added wayland-sessions and xsessions support
- Added better error handling for image and config loading
- Added some example configs
- Add FSL-1.1-MIT License
Note: All unlicensed commits prior to this commit on 17.06.2026 are considered All Rights Reserved.
2026-06-17 09:34:37 +02:00

246 lines
10 KiB
Rust

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<PathBuf>,
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<String>,
#[serde(default)]
pub(crate) environments: HashMap<String, LoginOptionTileConfig>,
pub(crate) pexels_api_key: Option<String>,
pub(crate) pexels_query: Option<String>,
#[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<String>,
}
impl Config {
pub fn get_tiles(&mut self) -> anyhow::Result<(Vec<LoginOptionTileData>, Option<usize>)> {
debug!(target: "ckgreeter::config_get_tiles", "Getting tiles from config");
fn get_sessions_from_dir(dir: &Path) -> anyhow::Result<HashMap<String, LoginOptionTileConfig>> {
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::<Vec<(&String, &LoginOptionTileConfig)>>();
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<LoginOptionTileData> = 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))
}
}