- 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.
246 lines
10 KiB
Rust
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))
|
|
}
|
|
} |