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

+5 -2
View File
@@ -136,8 +136,11 @@ impl cosmic::Application for Consent {
Message::Opened(id) => {
let parent = self.prompt.parent.clone();
return cosmic::iced::window::run(id, move |window| {
let parent = crate::parent::Parent::attach(window, &parent)?.map(Arc::new);
Ok((parent, ModalMaterial::attach(window)))
let attached = crate::parent::Parent::attach(window, &parent)?;
if let Some(diagnostic) = attached.diagnostic {
eprintln!("ferese portal: {diagnostic}");
}
Ok((attached.parent.map(Arc::new), ModalMaterial::attach(window)))
})
.map(|result| cosmic::Action::App(Message::Attached(result)));
}
+150 -13
View File
@@ -13,6 +13,62 @@ use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2;
use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2::ZxdgImportedV2;
use wayland_protocols::xdg::foreign::zv2::client::zxdg_importer_v2::ZxdgImporterV2;
const MAX_PARENT_IDENTIFIER: usize = 4096;
#[derive(Debug, Default)]
pub(crate) struct ParentAttachment {
pub parent: Option<Parent>,
pub diagnostic: Option<String>,
}
fn classify_parent<'a>(parent: &'a str) -> ParentKind<'a> {
if parent.is_empty() {
return ParentKind::None;
}
if parent.len() > MAX_PARENT_IDENTIFIER {
return ParentKind::Unsupported;
}
if let Some(handle) = parent.strip_prefix("wayland:") {
return if handle.is_empty() {
ParentKind::Unsupported
} else {
ParentKind::Wayland(handle)
};
}
if let Some(handle) = parent.strip_prefix("x11:") {
return if is_x11_window_identifier(handle) {
ParentKind::X11
} else {
// A malformed XID is a protocol error, not an X11 parent to
// degrade from: accepting arbitrary text here would let any string
// be reported as an X11 window.
ParentKind::Unsupported
};
}
ParentKind::Unsupported
}
/// Whether `identifier` is a syntactically valid X11 window identifier.
///
/// XIDs are unsigned 32-bit hex, optionally with the `0x` prefix, as X11
/// clients write them (for example `0x2400003` or `1a2b3c`).
fn is_x11_window_identifier(identifier: &str) -> bool {
let digits = identifier
.strip_prefix("0x")
.or_else(|| identifier.strip_prefix("0X"))
.unwrap_or(identifier);
!digits.is_empty() && digits.len() <= 8 && digits.bytes().all(|byte| byte.is_ascii_hexdigit())
}
#[derive(Debug, PartialEq, Eq)]
enum ParentKind<'a> {
None,
Wayland(&'a str),
X11,
Unsupported,
}
#[derive(Debug)]
pub(crate) struct Parent {
connection: Connection,
@@ -21,15 +77,25 @@ pub(crate) struct Parent {
}
impl Parent {
pub(crate) fn attach(window: &dyn cosmic::iced::window::Window, parent: &str) -> Result<Option<Self>, String> {
pub(crate) fn attach(window: &dyn cosmic::iced::window::Window, parent: &str) -> Result<ParentAttachment, String> {
use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
if parent.is_empty() {
return Ok(None);
}
let handle = parent
.strip_prefix("wayland:")
.filter(|handle| !handle.is_empty() && handle.len() <= 4096)
.ok_or("Unsupported portal parent window identifier")?;
let handle = match classify_parent(parent) {
ParentKind::None => {
return Ok(ParentAttachment::default());
}
ParentKind::X11 => {
return Ok(ParentAttachment {
parent: None,
diagnostic: Some(
"showing this dialog unparented: X11 parent windows are not supported yet".to_owned(),
),
});
}
ParentKind::Unsupported => {
return Err("Unsupported portal parent window identifier".into());
}
ParentKind::Wayland(handle) => handle,
};
let display = window.display_handle().map_err(|error| error.to_string())?;
let surface = window.window_handle().map_err(|error| error.to_string())?;
let (RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(surface)) =
@@ -65,11 +131,14 @@ impl Parent {
}
imported.set_parent_of(&surface);
connection.flush().map_err(|error| error.to_string())?;
Ok(Some(Self {
connection,
imported,
importer,
}))
Ok(ParentAttachment {
parent: Some(Self {
connection,
imported,
importer,
}),
diagnostic: None,
})
}
}
@@ -114,3 +183,71 @@ impl Dispatch<ZxdgImportedV2, ()> for State {
}
delegate_noop!(State: ignore ZxdgImporterV2);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_x11_parent_degrades_instead_of_failing_the_request() {
assert!(matches!(classify_parent("x11:0x2400003"), ParentKind::X11));
assert!(matches!(classify_parent("x11:1a2b3c"), ParentKind::X11));
}
#[test]
fn an_x11_parent_is_never_reinterpreted_as_a_wayland_handle() {
match classify_parent("x11:0x2400003") {
ParentKind::Wayland(handle) => panic!("an X11 parent must not become a Wayland handle: {handle}"),
ParentKind::X11 => {}
other => panic!("unexpected classification: {other:?}"),
}
}
#[test]
fn wayland_parents_are_still_imported() {
assert!(matches!(
classify_parent("wayland:handle-1"),
ParentKind::Wayland("handle-1")
));
assert_eq!(classify_parent(""), ParentKind::None);
}
#[test]
fn malformed_x11_window_identifiers_are_errors_not_parents() {
// Anything that is not an X11 window identifier must never be reported
// as one: doing so would turn arbitrary caller text into a window.
for parent in [
"x11:",
"x11:not-a-window",
"x11:0xzzzz",
"x11:0x2400003extra",
"x11:9999999999",
"x11:-1",
] {
assert!(
matches!(classify_parent(parent), ParentKind::Unsupported),
"{parent} must stay a hard error"
);
}
// Every form an X11 client actually produces is still accepted.
for parent in ["x11:0x2400003", "x11:1a2b3c", "x11:0X1", "x11:ffffffff"] {
assert!(
matches!(classify_parent(parent), ParentKind::X11),
"{parent} is a valid window identifier"
);
}
}
#[test]
fn malformed_identifiers_remain_hard_errors() {
for parent in ["x11:", "wayland:", "dbus:", "not-a-parent", "X11:0x1"] {
assert!(
matches!(classify_parent(parent), ParentKind::Unsupported),
"{parent} must stay a hard error"
);
}
let huge = format!("wayland:{}", "a".repeat(MAX_PARENT_IDENTIFIER + 1));
assert!(matches!(classify_parent(&huge), ParentKind::Unsupported));
}
}
+8 -2
View File
@@ -177,8 +177,14 @@ impl cosmic::Application for Picker {
Message::WindowOpened(id) => {
let parent = self.prompt.parent.clone();
return cosmic::iced::window::run(id, move |window| {
let parent = crate::parent::Parent::attach(window, &parent)?.map(std::sync::Arc::new);
Ok((parent, ferese_theme_client::material::ModalMaterial::attach(window)))
let attached = crate::parent::Parent::attach(window, &parent)?;
if let Some(diagnostic) = attached.diagnostic {
eprintln!("ferese portal: {diagnostic}");
}
Ok((
attached.parent.map(std::sync::Arc::new),
ferese_theme_client::material::ModalMaterial::attach(window),
))
})
.map(|result| cosmic::Action::App(Message::Attached(result)));
}