git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit8b616d05bd7f4e671668b676393b52087f2170f6
parentd2ad151e2f
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-26 15:49
fix(restore): an empty title is not an exact title match

Two untitled windows of one app_id matched in the exact pass, which made
it an app_id-only match that skipped the same-program check: any
untitled steam_proton window (Wine's tray window, upc.exe's untitled
helpers) could claim another's saved entry. The exact pass now needs a
non-empty title; an untitled window falls through to the app_id-only
pass, which still restores a relaunched app at app_id time, before its
title arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/server/window_manager.rs | 28 ++++++++++++++++++++++++----
 1 file changed, 24 insertions(+), 4 deletions(-)

diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index c11a12e6..56eb9b91 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -143,6 +143,15 @@ fn saved_by_program(saved: &SavedWindowState, program: &str) -> bool {
             .map_or(false, |rest| rest.is_empty() || rest.starts_with(' '))
 }
 
+/// The state matchers' exact title pass. Two empty titles do not match: an
+/// untitled window has no identity beyond its app_id, so "" == "" was an
+/// app_id-only match that skipped `same_program` — Wine's untitled tray
+/// window and any other untitled helper of a `steam_proton` app all
+/// claimed one another's entries through it.
+fn titles_match(a: &str, b: &str) -> bool {
+    !a.is_empty() && a == b
+}
+
 /// The state matchers' fuzzy title pass: equal once a trailing `*` (an
 /// editor's unsaved marker) is stripped, or one a prefix of the other.
 /// An empty title resembles nothing — as a prefix it would resemble every
@@ -1471,8 +1480,9 @@ impl WindowManager {
         if app_id.is_empty() {
             return None;
         }
-        // First pass: Exact match (app_id AND title)
-        if let Some(pos) = self.restore_queue.iter().position(|w| w.app_id == app_id && w.title == title) {
+        // First pass: Exact match (app_id AND title). An empty title is no
+        // identity (`titles_match`); it falls through to the app_id-only pass.
+        if let Some(pos) = self.restore_queue.iter().position(|w| w.app_id == app_id && titles_match(title, &w.title)) {
             let entry = self.restore_queue.remove(pos);
             self.remove_placeholder_for(&entry);
             return Some(entry);
@@ -1503,8 +1513,8 @@ impl WindowManager {
         if app_id.is_empty() {
             return None;
         }
-        // First pass: Exact match (app_id AND title)
-        if let Some(w) = self.last_window_states.iter().find(|w| w.app_id == app_id && w.title == title) {
+        // First pass: Exact match (app_id AND title), as above
+        if let Some(w) = self.last_window_states.iter().find(|w| w.app_id == app_id && titles_match(title, &w.title)) {
             return Some(w.clone());
         }
         // Second pass: Fuzzy title match
@@ -7631,6 +7641,13 @@ mod tests {
         assert!(!saved_by_program(&proton_entry("", "steam_proton"), EXPLORER));
     }
 
+    #[test]
+    fn empty_titles_match_nothing_exactly() {
+        assert!(titles_match("Ubisoft Connect", "Ubisoft Connect"));
+        assert!(!titles_match("", ""));
+        assert!(!titles_match("Ubisoft Connect", "Ubisoft"));
+    }
+
     #[test]
     fn empty_titles_resemble_nothing() {
         assert!(titles_resemble("Doc.txt*", "Doc.txt"));
@@ -7662,6 +7679,9 @@ mod tests {
         }
         wm.last_window_states[0] = proton_entry("", &format!("{EXPLORER} /desktop"));
         unsafe {
+            // Untitled on both sides is not an exact match, so the program
+            // check applies to it too.
+            assert!(wm.match_last_window_state("steam_proton", "", Some(UPC)).is_none());
             assert!(wm.match_last_window_state("steam_proton", "Ubisoft Connect", Some(UPC)).is_none());
             assert!(wm.match_last_window_state("steam_proton", "", Some(EXPLORER)).is_some());
         }