Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix(restore): no login placeholder for a window that is not relaunched
A Wine/Proton window's saved command is its Windows-side exe path, which
spawn_restored_one skips since /bin/sh cannot run it -- but
create_restore_placeholders still drew its plate, so Ubisoft Connect's
stood a minute over the empty desk every login until the sweep.
Both now ask one predicate, relaunchable(). The entry stays in the
restore queue, so the app still lands on its saved spot when launched by
hand. create_restore_placeholders logs its count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 6 +++++
src/server/window_manager.rs | 62 ++++++++++++++++++++++++++++++--------------
2 files changed, 49 insertions(+), 19 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index a5e5a772..762bed0f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -683,6 +683,12 @@ the **bare name** whenever the name's first `PATH` hit is a different file
from the one running (`path_shadowed_name`), and the restore's `sh -c`
resolves it the way the launcher did. The absolute path is kept when PATH
agrees with it.
+Wine/Proton windows record their Windows-side exe path (`C:\...`) as the
+command, which `/bin/sh` cannot run, so the restore never relaunches them
+(`relaunchable`) — and draws no login placeholder for them either: until
+2026-09-26 Ubisoft Connect's plate stood a minute over the empty desk,
+waiting for a window nothing had started. The entry stays queued, so the app
+still lands on its saved spot when the user launches it.
A restored **floating** window is recalled into the current view
(`policy::camera::recalled_origin`, applied at the end of `try_restore`)
when its remembered position would show less than a quarter of it: the
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index af9d88ae..afdd418b 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -157,6 +157,19 @@ fn borrowable(saved: &SavedWindowState, program: Option<&str>) -> bool {
!saved.title.is_empty() && same_program(saved, program)
}
+/// Whether the session restore can relaunch a window from this saved
+/// command. A Wine/Proton window records its WINDOWS-side exe path
+/// (`C:\...` or `C:/...`) — /bin/sh can never run it — and an empty
+/// command has nothing to run. `spawn_restored_one` skips such entries and
+/// `create_restore_placeholders` draws no plate for them: Ubisoft Connect's
+/// stood a minute over the empty desk every login (2026-09-26), waiting for
+/// a window nothing had started.
+fn relaunchable(cmdline: &str) -> bool {
+ let b = cmdline.trim().as_bytes();
+ let windows_path = b.len() > 2 && b[0].is_ascii_alphabetic() && b[1] == b':' && (b[2] == b'/' || b[2] == b'\\');
+ !b.is_empty() && !windows_path
+}
+
/// Whether a window mapping as (`app_id`, `program`) is the reconnect of
/// one that vanished as (`gone_app_id`, `gone_program`): the same app_id
/// AND the same program. An app_id alone is too coarse — every Proton
@@ -1000,6 +1013,13 @@ impl WindowManager {
if entry.width == 0 || entry.height == 0 {
continue;
}
+ // Nothing is launched for this entry, so no window is coming to
+ // replace the plate: it would stand there until the sweep, a
+ // minute of frame over empty desk. The entry stays queued, so
+ // the app still lands on its saved spot if the user starts it.
+ if !relaunchable(&entry.cmdline) {
+ continue;
+ }
let mut color = if entry.focused {
self.layout.border_color_focused
} else {
@@ -1023,6 +1043,11 @@ impl WindowManager {
h: entry.height,
});
}
+ log::info!(
+ "[Restore] {} placeholder(s) for {} queued window(s)",
+ self.restore_placeholders.len(),
+ self.restore_queue.len()
+ );
if self.restore_placeholders.is_empty() {
return;
}
@@ -1702,25 +1727,14 @@ impl WindowManager {
}
fn spawn_restored_one(w: &SavedWindowState, spawned_any: &mut bool) {
- // A wine/Proton window records its WINDOWS-side exe path
- // (C:\... or C:/...) as the command — /bin/sh can never run
- // it, so each one burns a silent no-op fork per login. Skip
- // them outright.
- let cmd_trimmed = w.cmdline.trim();
- let bytes = cmd_trimmed.as_bytes();
- let is_windows_path = bytes.len() > 2
- && bytes[0].is_ascii_alphabetic()
- && bytes[1] == b':'
- && (bytes[2] == b'/' || bytes[2] == b'\\');
- if is_windows_path {
- log::info!(
- "Skipping unrestorable Windows-path command for {:?}: {}",
- w.app_id,
- cmd_trimmed
- );
- return;
- }
- if w.cmdline.is_empty() {
+ if !relaunchable(&w.cmdline) {
+ if !w.cmdline.trim().is_empty() {
+ log::info!(
+ "Skipping unrestorable Windows-path command for {:?}: {}",
+ w.app_id,
+ w.cmdline.trim()
+ );
+ }
return;
}
// Small stagger so N clients don't all hit Vulkan device
@@ -7687,6 +7701,16 @@ mod tests {
assert!(!saved_by_program(&proton_entry("", "steam_proton"), EXPLORER));
}
+ #[test]
+ fn windows_paths_and_empty_commands_are_not_relaunched() {
+ assert!(relaunchable("/usr/bin/cce-files"));
+ assert!(relaunchable("cce-terminal --working-directory='/tmp'"));
+ assert!(!relaunchable(&format!("{UPC} -upc_desktop_mode")));
+ assert!(!relaunchable("D:/Games/thing.exe"));
+ assert!(!relaunchable(" "));
+ assert!(!relaunchable(""));
+ }
+
#[test]
fn a_reconnect_is_the_same_program_under_the_same_app_id() {
let upc = Some(UPC);