Add XWayland support (#16)

* save dev state

* fix xwayland-satellite supervisor lifecycle, readiness and cleanup

* remove wayland scanner protocol generated files

* default xwayland enabled with path

* remove jargons

* address x11 review: park unconfirmed cleanup, never signal a reaped group, fix storm test and notify dir mode

* fix resize tests: never build Size values from negative dimensions

* add min-width/min-height window rules that replace client minimum sizes

* rename effective_size_constraints to effective_minimum_size and minor cleanups
This commit is contained in:
Samuel Onoja authored and GitHub committed 2026-10-06 19:40:22 +01:00
1 parent ed23949a95
commit a6f83689f8
42 files changed
+7162 -1333

No files matched your search

+52 -5
View File
@@ -3,12 +3,14 @@ mod bindings;
mod input_motion;
mod layout;
mod outputs;
mod xwayland;
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
pub(crate) use self::xwayland::{XwaylandConfig, XwaylandStartup};
use appearance::*;
pub use appearance::{BorderGradient, InactiveDimSettings, MaterialStyle, ThemeSettings};
use bindings::*;
@@ -61,6 +63,8 @@ pub struct Config {
scrolling: ScrollingConfig,
#[serde(default)]
output_profiles: Vec<OutputProfileConfig>,
#[serde(default)]
pub(crate) xwayland: XwaylandConfig,
#[serde(default, rename = "status")]
_status: ShellStatusConfig,
}
@@ -114,6 +118,7 @@ impl Config {
pub(crate) fn runtime_config(&self) -> Result<crate::RuntimeConfig, ConfigError> {
self.notifications.validate().map_err(ConfigError::Binding)?;
self.desktop_widgets.validate().map_err(ConfigError::Binding)?;
self.xwayland.validate().map_err(ConfigError::Binding)?;
for daemon in &self.autostart {
if daemon.command.first().is_none_or(|program| program.trim().is_empty()) {
return Err(ConfigError::Binding("autostart command must contain a program".into()));
@@ -143,6 +148,7 @@ impl Config {
output_profiles: self.output_profiles()?,
wallpaper: self.wallpaper_settings(),
overview_font_family: self.overview_font_family(),
xwayland: self.xwayland.clone(),
})
}
@@ -517,10 +523,13 @@ mod tests {
#[test]
fn packaged_and_custom_kdl_pass_runtime_validation() {
Config::parse_source(include_str!("../../packaging/config.kdl"))
.unwrap()
.runtime_config()
.unwrap();
let packaged = Config::parse_source(include_str!("../../packaging/config.kdl")).unwrap();
packaged.runtime_config().unwrap();
assert!(
packaged.xwayland.enabled,
"packaging/config.kdl must ship X11 support enabled, matching the default"
);
let source = "input {\n touchpad {\n swipe-threshold 96\n }\n}\nbinding keys=\"Swipe3Up\" action=\"toggle-overview\"\noutput-profile name=\"desk\" {\n output match=\"DP-1\" scale=1.5 {\n position 0 0\n }\n}\ndesktop-widgets {\n clock {\n enabled #true\n outputs \"DP-1\"\n }\n}\nautostart {\n command \"program\" \"argument with space\"\n}\n";
let config = Config::parse_source(source).unwrap();
config.runtime_config().unwrap();
@@ -557,6 +566,40 @@ mod tests {
ferese_config::from_str(source).unwrap()
}
#[test]
fn an_empty_configuration_enables_the_managed_x11_service() {
let config = parse("");
assert_eq!(config.xwayland, XwaylandConfig::default());
assert!(config.xwayland.enabled, "an empty config keeps the default");
assert_eq!(config.xwayland.startup, XwaylandStartup::OnDemand);
let runtime = config.runtime_config().expect("an empty config is valid");
assert!(runtime.xwayland.enabled);
}
#[test]
fn a_configuration_without_an_xwayland_section_keeps_the_defaults() {
let source = r#"
layout-mode "scrolling";
desktop-widgets {
clock {
time-format "%H:%M";
};
}
scroll-factor 1.0;
"#;
let config = parse(source);
assert!(
source.find("xwayland").is_none(),
"the sample must have no xwayland node"
);
assert_eq!(config.xwayland, XwaylandConfig::default());
assert!(config.xwayland.enabled);
let runtime = config.runtime_config().expect("the existing config stays valid");
assert!(runtime.xwayland.enabled);
}
#[test]
fn defaults_to_half_width_scrolling_columns() {
let config = parse("");
@@ -1084,7 +1127,7 @@ mod tests {
#[test]
fn parses_and_validates_window_rules() {
let config = parse(
"window-rule app-id=\"org.example.Editor\" workspace=3 floating=#true width=900.0 height=600.0 fullscreen=#false block-out-from-screencasts=#true\n",
"window-rule app-id=\"org.example.Editor\" workspace=3 floating=#true width=900.0 height=600.0 min-width=500 min-height=400 fullscreen=#false block-out-from-screencasts=#true\n",
);
let rules = config.window_rules().unwrap();
let result = window_rules::resolve(&rules, Some("org.example.editor.desktop"), Some("Document"), false);
@@ -1093,6 +1136,8 @@ mod tests {
assert_eq!(result.floating, Some(true));
assert_eq!(result.width, Some(900.0));
assert_eq!(result.height, Some(600.0));
assert_eq!(result.min_width, Some(500.0));
assert_eq!(result.min_height, Some(400.0));
assert_eq!(result.fullscreen, Some(false));
assert_eq!(result.block_out_from_screencasts, Some(true));
}
@@ -1101,9 +1146,11 @@ mod tests {
fn rejects_invalid_window_rule_configuration() {
let catch_all = parse("window-rule floating=#true\n");
let zero_workspace = parse("window-rule app-id=\"editor\" workspace=0\n");
let zero_minimum = parse("window-rule app-id=\"editor\" min-width=0\n");
assert!(catch_all.window_rules().is_err());
assert!(zero_workspace.window_rules().is_err());
assert!(zero_minimum.window_rules().is_err());
}
#[test]