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.
This commit is contained in:
CanadianBaconBoi 2026-06-17 09:34:37 +02:00
parent 4f2902a847
commit 66d90100df
7 changed files with 351 additions and 40 deletions

View File

@ -5,37 +5,43 @@ build = "build.rs"
edition = "2024"
[dependencies]
slint = { git = "https://github.com/slint-ui/slint", branch = "master", default-features = false, features = [
anyhow = "1.0"
chrono = "0.4"
rand = "0.10"
is_executable = "1.0"
walkdir = "2.5"
toml = { version = "1.1", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde-aux = "4.7"
tokio = { version = "1.52", features = ["macros", "rt-multi-thread", "net", "fs", "full"] }
tokio-stream = {version = "0.1", features = ["fs"]}
freedesktop_entry_parser = "2.0"
pretty_env_logger = "0.5"
log = {version = "0.4", features = ["kv"]}
slint = { git = "https://github.com/slint-ui/slint", rev = "2e97daa", default-features = false, features = [
"std",
"compat-1-2",
"renderer-femtovg",
"backend-linuxkms",
"backend-winit-wayland",
] }
anyhow = "1.0.102"
greetd_ipc = { version = "0.10.3", features = [
i-slint-core = { git = "https://github.com/slint-ui/slint", rev = "2e97daa" }
greetd_ipc = { version = "0.10", features = [
"codec",
"async-trait",
"tokio-codec",
] }
i-slint-core = { git = "https://github.com/slint-ui/slint", branch = "master" }
toml = { version = "1.1.2", features = ["serde"] }
serde = { version = "1.0.228", features = ["derive"] }
serde-aux = "4.7.0"
zbus = { version = "5.15.0", features = ["blocking"] }
tokio = { version = "1.52.2", features = ["macros", "rt-multi-thread", "net", "fs", "full"] }
tokio-stream = {version = "0.1.18", features = ["fs"]}
greetd-stub = "0.3.0"
chrono = "0.4.44"
pretty_env_logger = "0.5.0"
log = {version = "0.4.32", features = ["kv"]}
zbus = { version = "5.16", features = ["blocking"] }
reqwest = { version = "0.13", features = ["json", "default-tls"] }
pexels-api = { git = "https://github.com/houseme/pexels", rev = "b0b692a" }
reqwest = { version = "0.13.3", features = ["json", "default-tls"] }
magick_rust = "2.0"
magick_rust = "2.0.0"
rand = "0.9.4"
greetd-stub = "0.3"
[build-dependencies]
slint-build = { git = "https://github.com/slint-ui/slint", branch = "master" }

67
LICENSE.md Normal file
View File

@ -0,0 +1,67 @@
# Functional Source License, Version 1.1, MIT Future License
## Abbreviation
FSL-1.1-MIT
## Notice
Copyright 2026 CanadianBaconBoi
## Terms and Conditions
### Licensor ("We")
The party offering the Software under these Terms and Conditions.
### The Software
The "Software" is each version of the software that we make available under these Terms and Conditions, as indicated by our inclusion of these Terms and Conditions with the Software.
### License Grant
Subject to your compliance with this License Grant and the Patents, Redistribution and Trademark clauses below, we hereby grant you the right to use, copy, modify, create derivative works, publicly perform, publicly display and redistribute the Software for any Permitted Purpose identified below.
### Permitted Purpose
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use means making the Software available to others in a commercial product or service that:
1. substitutes for the Software;
2. substitutes for any other product or service we offer using the Software that exists as of the date we make the Software available; or
3. offers the same or substantially similar functionality as the Software.
Permitted Purposes specifically include using the Software:
1. for your internal use and access;
2. for non-commercial education;
3. for non-commercial research; and
4. in connection with professional services that you provide to a licensee using the Software in accordance with these Terms and Conditions.
### Patents
To the extent your use for a Permitted Purpose would necessarily infringe our patents, the license grant above includes a license under our patents. If you make a claim against any party that the Software infringes or contributes to the infringement of any patent, then your patent license to the Software ends immediately.
### Redistribution
The Terms and Conditions apply to all copies, modifications and derivatives of the Software.
If you redistribute any copies, modifications or derivatives of the Software, you must include a copy of or a link to these Terms and Conditions and not remove any copyright notices provided in or with the Software.
### Disclaimer
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
### Trademarks
Except for displaying the License Details and identifying us as the origin of the Software, you have no right under these Terms and Conditions to use our trademarks, trade names, service marks or product names.
## Grant of Future License
We hereby irrevocably grant you an additional license to use the Software under the MIT license that is effective on the second anniversary of the date we make the Software available. On or after that date, you may use the Software under the MIT license, in which case the following will apply:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# CKGreeter: Just another greeter
### A simple GreetD greeter written in Rust.
---
## Installation
### Requirements
- GreetD
- Rust/Cargo
- A Wayland compositor, **cage** is used in the example
### CKGreeter Install
- `sudo cargo install --git https://codeberg.org/CanadianBaconBoi/CKGreeter.git --bin ckgreeter --root /usr/bin`
- `sudo mkdir -p /{etc,var/cache}/ckgreeter`
- `sudo chown greetd:greetd /var/cache/ckgreeter`
### Configuring GreetD
- Add the following line to `/etc/greetd/greetd.conf`
- `command = "cage -s -d -- /usr/bin/ckgreeter"`
## Configuration
The configuration file is located at `/etc/ckgreeter/config.toml`
Example configurations can be found in [examples/config/](examples/config)
The config needs to be copied to `/etc/ckgreeter/config.toml` before the greeter can be run.
## License
- **Commits after 17.06.2026:** Licensed under the [Functional Source License, Version 1.1, MIT Future License](LICENSE.md).
- **Prior unlicensed commits (before 17.06.2026):** Copyright (c) 2026 CanadianBaconBoi. All rights reserved.

View File

@ -0,0 +1,26 @@
# Username in the username box at startup; can be left empty or removed
default_username = "canadian"
# Your Pexels API key can be found at https://www.pexels.com/api/key/; can be left empty or removed
pexels_api_key = "1234567890123456789012345678901234567890"
# Query to use for the Pexels search; can be left empty or removed
pexels_query = "slot canyon"
# How long to wait between new backgrounds; can be removed, will default to 30
background_delay = 30
# Directory to cache old background images; can be removed, will default to /var/cache/ckgreeter/
background_cache_directory = "/var/cache/ckgreeter/"
# Should this greeter find Wayland sessions via the /usr/share/wayland-sessions/ directory? can be removed, will default to true
find_wayland_sessions = true
# Should this greeter find X sessions via the /usr/share/xsessions/ directory? can be removed, will default to true
find_x_sessions = true
# If none of the configured environments below have `is_default` set, find a default from the xdg sessions by name. can be left empty or removed
# This is omitted in this config as the default has been set with the below environment
#default_by_name = "Plasma (Wayland)"
# Example environment configuration
[environments."Example (Wayland)"]
image_path = "/etc/ckgreeter/example.svg" # Images can be SVG, PNG, JPG; Can be removed, will default to an empty image
command = "/bin/true" # A greeter command, typically something like `dbus-run-session startplasma-wayland`
index = 0 # The index in the switcher; if unset or set to -1, will sort by environment name
is_default = true # Should this environment be selected by default, this can only be set in one environment; Can be removed, will default to false.

View File

@ -0,0 +1,18 @@
# Username in the username box at startup; can be left empty or removed
default_username = "canadian"
# Your Pexels API key can be found at https://www.pexels.com/api/key/; can be left empty or removed
pexels_api_key = "1234567890123456789012345678901234567890"
# Query to use for the Pexels search; can be left empty or removed
pexels_query = "slot canyon"
# How long to wait between new backgrounds; can be removed, will default to 30
background_delay = 30
# Directory to cache old background images; can be removed, will default to /var/cache/ckgreeter/
background_cache_directory = "/var/cache/ckgreeter/"
# Should this greeter find Wayland sessions via the /usr/share/wayland-sessions/ directory? can be removed, will default to true
find_wayland_sessions = true
# Should this greeter find X sessions via the /usr/share/xsessions/ directory? can be removed, will default to true
find_x_sessions = true
# If none of the configured environments below have `is_default` set, find a default from the xdg sessions by name. can be left empty or removed
default_by_name = "Plasma (Wayland)"

View File

@ -1,15 +1,16 @@
use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::{anyhow, Context};
use std::path::{Path, PathBuf};
use anyhow::anyhow;
use i_slint_core::api::{Image, ToSharedString};
use log::{debug, trace};
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: PathBuf,
pub(crate) image_path: Option<PathBuf>,
pub(crate) command: String,
#[serde(default = "default_i32::<-1>")]
pub(crate) index: i32,
@ -33,13 +34,140 @@ pub struct Config {
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::<10>", alias = "bg_delay")]
#[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(&self) -> anyhow::Result<(Vec<LoginOptionTileData>, Option<usize>)> {
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()
@ -73,16 +201,44 @@ impl Config {
debug!(target: "ckgreeter::config_get_tiles", "Setting default tile to `{}`", name);
default = Some(tiles.len());
}
tiles.push(LoginOptionTileData {
command: tile_config.command.to_shared_string(),
image: Image::load_from_path(&tile_config.image_path).with_context(|| {
format!(
"Failed to load image for login option `{}` from `{}`",
name, tile_config.image_path.display()
)
})?,
name: name.to_shared_string(),
})
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))

View File

@ -60,6 +60,7 @@ async fn main() -> anyhow::Result<()> {
pretty_env_logger::formatted_timed_builder()
.target(Target::Stdout)
.parse_filters("ckgreeter=debug")
.parse_filters(std::env::var("RUST_LOG").unwrap_or_else(|_| "".to_string()).as_str())
.init();
#[cfg(not(debug_assertions))]
pretty_env_logger::formatted_timed_builder()
@ -84,23 +85,29 @@ async fn main() -> anyhow::Result<()> {
let config_path = std::env::var("CKGREETER_CONFIG").unwrap_or_else(|_| CONFIG_PATH.to_string());
if !std::fs::exists(&config_path)? {
error!(target: "ckgreeter::init", "Config file not found at {}", config_path);
return Err(anyhow::anyhow!("Config file not found at {}", config_path));
}
info!(target: "ckgreeter::init", "Loading config from {}", config_path);
let config_data = std::fs::read(&config_path)
.with_context(|| format!("Failed to read config file at {}", config_path))?;
info!(target: "ckgreeter::init", "Config loaded");
let config: Config = toml::from_slice(&config_data)
let mut config: Config = toml::from_slice(&config_data)
.with_context(|| format!("Failed to parse config file at {}", config_path))?;
debug!(target: "ckgreeter::init", "Config parsed");
let display = GreeterDisplay::new()?;
let data = display.global::<GreeterData>();
let (tiles, default_tile) = config.get_tiles()?;
let cache_dir = &config.background_cache_directory;
info!(target: "ckgreeter::init", "Using cache directory: {}", cache_dir.display());
std::fs::create_dir_all(cache_dir)
.with_context(|| format!("Failed to create cache directory: {}", cache_dir.display()))?;
let display = GreeterDisplay::new()?;
let data = display.global::<GreeterData>();
let (tiles, default_tile) = config.get_tiles()?;
if tiles.len() == 0 {
error!(target: "ckgreeter::init", "No tiles found in config");