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

commite6af44a4954f5bc5368df0303501070d3c9c2764
parente1adb17ec9
authorLucas Galante <[email protected]>
date2026-07-21 21:51
feat: close-window IPC command

close-window <app_id|id> [title-substring] asks a specific window to
close. Selection mirrors focus-window (numeric id, exact app_id beats
substring), with an optional case-insensitive title filter to
disambiguate when an app has several windows — e.g. closing KeePassXC's
orphaned "Unlock Database" prompt (keepassxc#9297) without touching its
main window, which cce-keyring-unlock does after a confirmed unlock.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/cce_ctl.rs               |  1 +
 src/server/window_manager.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index fa4fc38..014e827 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -52,6 +52,7 @@ fn usage(name: &str, to_stderr: bool) {
     print("  overlay-left");
     print("  overlay-right");
     print("  focus-window <app_id>");
+    print("  close-window <app_id|id> [title-substring]  # close a specific window");
     print("  center-window [<app_id>]   # pan focused/named window on-screen; replies x= y= w= h=");
     print("  expose");
     print("  windows [--json]           # list windows; --json emits one JSON object per line");
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 33bbec4..6bae8f9 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2778,6 +2778,50 @@ impl WindowManager {
                     "error: no seat found\n".to_string()
                 }
             }
+            "close-window" => {
+                // close-window <app_id|id> [title substring...] — ask a specific
+                // window to close. The optional title filter disambiguates when an
+                // app has several windows (e.g. KeePassXC's orphaned "Unlock
+                // Database" prompt next to its main window).
+                if parts.len() < 2 { return "error: missing app_id/id\n".to_string(); }
+                let query = parts[1].to_lowercase();
+                let title_filter = parts[2..].join(" ").to_lowercase();
+                let mut target: *mut Window = std::ptr::null_mut();
+                let mut best_score = 0;
+                for &w in self.windows.iter() {
+                    if w.is_null() || (*w).closed
+                        || !matches!((*w).state, crate::window::WindowState::Mapped)
+                    {
+                        continue;
+                    }
+                    if !title_filter.is_empty() {
+                        let title = (*w).get_title_string().unwrap_or_default().to_lowercase();
+                        if !title.contains(&title_filter) {
+                            continue;
+                        }
+                    }
+                    let id_match = query.parse::<u32>().map_or(false, |id| (*w).ref_key.index == id);
+                    let aid = (*w).get_app_id_string().unwrap_or_default().to_lowercase();
+                    let score = if id_match || aid == query {
+                        100
+                    } else if !query.is_empty() && aid.contains(&query) {
+                        50
+                    } else {
+                        0
+                    };
+                    if score > best_score {
+                        best_score = score;
+                        target = w;
+                    }
+                }
+                if target.is_null() {
+                    return "error: window not found\n".to_string();
+                }
+                let title = (*target).get_title_string().unwrap_or_default();
+                log::info!("[ipc] close-window: closing {:?}", title);
+                (*target).close();
+                format!("ok {}\n", title)
+            }
             "center-window" | "bring-window" => {
                 // Pan the desktop so the target window (given app_id/id, or the
                 // focused window if omitted) is centered in the output, then focus