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

+100 -1
View File
@@ -19,6 +19,9 @@ fn main() -> Result<(), Box<dyn Error>> {
if env::args().nth(1).as_deref() == Some("theme") {
return theme_command(env::args().skip(2).collect());
}
if env::args().nth(1).as_deref() == Some("xwayland") {
return xwayland_command(env::args().skip(2).collect());
}
let (command, args) = parse_args(env::args().skip(1))?;
let request = Request {
version: VERSION,
@@ -87,6 +90,40 @@ fn theme_command(args: Vec<String>) -> Result<(), Box<dyn Error>> {
Ok(())
}
fn xwayland_command(args: Vec<String>) -> Result<(), Box<dyn Error>> {
use ferese_ipc::xwayland::{RETRY_COMMAND, STATUS_COMMAND, Status};
let command = match args.as_slice() {
[command] if command == "status" => STATUS_COMMAND,
[command] if command == "retry" => RETRY_COMMAND,
_ => return Err("usage: feresectl xwayland <status|retry>".into()),
};
let request = Request {
version: VERSION,
id: 1,
kind: "command".to_owned(),
command: command.to_owned(),
args: json!({}),
};
let mut stream = UnixStream::connect(socket_path()?)?;
write_frame(&mut stream, &request)?;
let response: Response = read_frame(&mut stream)?;
if let Some(error) = response.error {
return Err(format!("{}: {}", error.code, error.message).into());
}
let status = Status::from_value(response.result.unwrap_or(Value::Null))?;
if command == RETRY_COMMAND {
println!(
"X11 retry requested; current state: {}",
serde_json::to_string(&status.state)?
);
}
println!("{}", serde_json::to_string_pretty(&status)?);
Ok(())
}
// The compositor stages the PNG in a private directory and replies with its
// path. Open it before unlinking so a failure never destroys the only copy, and
// stream the bytes with explicit writes: println! would corrupt binary output.
@@ -233,13 +270,75 @@ fn socket_path() -> Result<PathBuf, io::Error> {
}
fn usage() -> String {
"usage: feresectl media [get|play-pause|next|previous|raise|auto|pin PLAYER|ignore PLAYER|unignore PLAYER]\n feresectl outputs\n feresectl output-profiles\n feresectl <output-confirm|output-revert>\n feresectl output-layout <internal-only|external-only|extend|mirror>\n feresectl toggle-display-mode\n feresectl output-profile <name|auto>\n feresectl output-internal <on|off>\n feresectl autostart\n feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl screenshot-window <window-id>\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl workspace-back-and-forth\n feresectl <focus-last-window|focus-mru-next|focus-mru-previous>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview|toggle-keybinding-guide>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit|request-logout>\n feresectl <get-focused-window|get-windows|get-workspaces|get-outputs|get-idle-inhibition|reload-config>".to_owned()
"usage: feresectl media [get|play-pause|next|previous|raise|auto|pin PLAYER|ignore PLAYER|unignore PLAYER]\n feresectl outputs\n feresectl output-profiles\n feresectl <output-confirm|output-revert>\n feresectl output-layout <internal-only|external-only|extend|mirror>\n feresectl toggle-display-mode\n feresectl output-profile <name|auto>\n feresectl output-internal <on|off>\n feresectl autostart\n feresectl xwayland <status|retry>\n feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl screenshot-window <window-id>\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl workspace-back-and-forth\n feresectl <focus-last-window|focus-mru-next|focus-mru-previous>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview|toggle-keybinding-guide>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit|request-logout>\n feresectl <get-focused-window|get-windows|get-workspaces|get-outputs|get-idle-inhibition|reload-config>".to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xwayland_commands_map_to_the_shared_ipc_command_names() {
assert_eq!(ferese_ipc::xwayland::STATUS_COMMAND, "xwayland-status");
assert_eq!(ferese_ipc::xwayland::RETRY_COMMAND, "xwayland-retry");
assert!(usage().contains("feresectl xwayland <status|retry>"));
}
#[test]
fn the_xwayland_status_snapshot_round_trips_over_the_ipc_framing() {
let status = ferese_ipc::xwayland::Status {
enabled: true,
effective_startup: "on-demand".to_owned(),
state: ferese_ipc::xwayland::State::Running,
display: Some(":7".to_owned()),
satellite_pid: Some(24017),
generation: Some(2),
readiness: ferese_ipc::xwayland::Readiness::Verified,
recent_failures: 1,
restart_required: false,
last_error: None,
};
let request = Request {
version: VERSION,
id: 1,
kind: "command".to_owned(),
command: ferese_ipc::xwayland::STATUS_COMMAND.to_owned(),
args: json!({}),
};
let mut bytes = Vec::new();
write_frame(&mut bytes, &request).expect("encode the request");
let decoded: Request = read_frame(&mut bytes.as_slice()).expect("decode the request");
assert_eq!(decoded.command, "xwayland-status");
let mut reply = Vec::new();
write_frame(
&mut reply,
&Response::success(1, ferese_ipc::xwayland::Status::to_value(&status)),
)
.expect("encode the response");
let response: Response = read_frame(&mut reply.as_slice()).expect("decode the response");
assert!(response.error.is_none());
let decoded = ferese_ipc::xwayland::Status::from_value(response.result.expect("a result"))
.expect("decode the status snapshot");
assert_eq!(decoded, status);
assert_eq!(decoded.state, ferese_ipc::xwayland::State::Running);
}
#[test]
fn a_failed_x11_retry_surfaces_the_compositor_error_code() {
let response = Response::error(1, "x11_unavailable", "X11 is disabled in the configuration");
let mut bytes = Vec::new();
write_frame(&mut bytes, &response).expect("encode");
let decoded: Response = read_frame(&mut bytes.as_slice()).expect("decode");
let error = decoded.error.expect("an error response");
assert_eq!(error.code, "x11_unavailable");
assert!(error.message.contains("disabled"));
}
#[test]
fn media_commands_route_transport_and_player_preferences() {
assert_eq!(parse_args(["media".into()]).unwrap().0, "media-get");