Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat(menu): the desktop menu gets a Window Mode page for the focused window
The same Floating / Tiled / Fullscreen page cce-app-menu shows, current mode
marked, applied with `ccectl set-mode <mode> <id>` and the window refocused
after. The background right-click drops focus right after spawning the menu
(so the popup takes the keyboard and a click-away dismisses it), so the
script cannot ask which window was focused: the compositor passes it as
`-i <id> -a <app_id>`, only when its mode is one set-mode accepts, and the
page is omitted otherwise.
The script is also made executable in the repo; the installer chmods, so
only a shadow pointed at the tree's copy ever noticed.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 4 ++
scripts/cce-desktop-menu | 97 ++++++++++++++++++++++++++++++++++++++----------
src/server/cursor.rs | 25 ++++++++++++-
3 files changed, 106 insertions(+), 20 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index bf05305..701fb5b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -489,6 +489,10 @@ sat outside the edges and the ring that followed hugged them.
reached through a `target_page` button, which switches pages without
closing the popup — sets the mode with `ccectl set-mode <mode> <id>`, one
window by id. Not `ccectl mode`, which appends a persistent app_id rule.
+ `cce-desktop-menu` carries the same page for the FOCUSED window: the
+ background right-click passes it as `-i <id> -a <app_id>` (settable modes
+ only) because it drops focus right after the spawn, so the script could
+ not ask for it.
None of this is policy — `cce-window-manager` was untouched. The mode is
already in `ActionCtx`, but what a *pointer* may grab is mechanism.
diff --git a/scripts/cce-desktop-menu b/scripts/cce-desktop-menu
old mode 100644
new mode 100755
index fb3be1d..a362957
--- a/scripts/cce-desktop-menu
+++ b/scripts/cce-desktop-menu
@@ -1,5 +1,13 @@
#!/usr/bin/env bash
# cce-desktop-menu — Context menu for CCE desktop background
+#
+# A `cce-cloud --json` popup, like cce-app-menu and cce-grid's item menu. The
+# compositor (cursor.rs, the background right-click) passes the FOCUSED
+# window as `-i <id> -a <app_id>` when there is one whose mode can be set:
+# it drops focus right after the spawn so the popup can take the keyboard,
+# so the script cannot ask for it. With a window named, the menu gains a
+# "Window Mode" page — the same page cce-app-menu shows — whose picks go
+# through `ccectl set-mode <mode> <id>`.
CCE_CTL="$HOME/.local/bin/ccectl"
CLEAR_CLOUD="$HOME/.local/bin/cce-cloud"
@@ -9,6 +17,8 @@ x_arg=""
y_arg=""
x_val=""
y_val=""
+window_index=""
+app_id=""
# Open the app WHERE THE MENU WAS OPENED: the next window matching $1 covers
# the grid square under the click, keeping the size it had last time and
@@ -31,31 +41,72 @@ while [[ $# -gt 0 ]]; do
-y|--y-pos)
if [[ $# -ge 2 ]]; then y_arg="-y $2"; y_val="$2"; shift 2; else shift; fi
;;
+ -i|--window-id)
+ if [[ $# -ge 2 ]]; then window_index="$2"; shift 2; else shift; fi
+ ;;
+ -a|--app-id)
+ if [[ $# -ge 2 ]]; then app_id="$2"; shift 2; else shift; fi
+ ;;
*)
shift
;;
esac
done
-# Define the JSON layout for the desktop context menu
-json_layout='{
- "pages": [
- {
- "title": "Desktop Context Menu",
- "justify": "left",
- "widgets": [
- { "type": "button", "text": "Terminal", "id": "terminal" },
- { "type": "button", "text": "Files", "id": "files" },
- { "type": "button", "text": "Data Editor", "id": "data_editor" },
- { "type": "button", "text": "Applications", "id": "apps" },
- { "type": "button", "text": "System Settings", "id": "settings" },
- { "type": "button", "text": "Overview", "id": "overview" },
- { "type": "button", "text": "Reload Config", "id": "reload" },
- { "type": "button", "text": "Logout", "id": "exit" }
- ]
- }
- ]
-}'
+# The named window's current mode, so the submenu can mark it. One JSON
+# object per line; a window gone since the click yields "" and the page is
+# left out.
+current_mode=""
+if [[ -n "$window_index" ]]; then
+ current_mode=$("$CCE_CTL" windows --json 2>/dev/null | python3 -c '
+import sys, json
+want = sys.argv[1]
+for line in sys.stdin:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ w = json.loads(line)
+ except ValueError:
+ continue
+ if str(w.get("id")) == want:
+ print(str(w.get("mode", "")).lower())
+ break
+' "$window_index")
+fi
+
+# Build the layout in python so the app_id (client-chosen text) is escaped
+# for JSON rather than spliced into it. The "Window Mode" entry sits with
+# Overview, the other window-manager verb; a `target_page` button switches
+# pages without closing the popup.
+json_layout=$(python3 - "$app_id" "$current_mode" <<'PY'
+import sys, json
+app_id, current = sys.argv[1], sys.argv[2]
+def mode(text, ident):
+ mark = "● " if ident == current else " "
+ return {"type": "button", "text": mark + text, "id": ident}
+main = [
+ {"type": "button", "text": "Terminal", "id": "terminal"},
+ {"type": "button", "text": "Files", "id": "files"},
+ {"type": "button", "text": "Data Editor", "id": "data_editor"},
+ {"type": "button", "text": "Applications", "id": "apps"},
+ {"type": "button", "text": "System Settings", "id": "settings"},
+ {"type": "button", "text": "Overview", "id": "overview"},
+]
+pages = [{"title": "Desktop Context Menu", "justify": "left", "widgets": main}]
+if current:
+ main.append({"type": "button", "text": "Window Mode >", "id": "mode_page", "target_page": 1})
+ pages.append({"title": app_id, "justify": "left", "widgets": [
+ mode("Floating", "floating"),
+ mode("Tiled", "tiled"),
+ mode("Fullscreen", "fullscreen"),
+ {"type": "button", "text": "< Back", "id": "back", "target_page": 0},
+ ]})
+main.append({"type": "button", "text": "Reload Config", "id": "reload"})
+main.append({"type": "button", "text": "Logout", "id": "exit"})
+print(json.dumps({"pages": pages}))
+PY
+)
# Spawn cce-cloud with the json layout and coordinates
selected=$(echo "$json_layout" | $CLEAR_CLOUD --json $x_arg $y_arg 2>/dev/null)
@@ -104,6 +155,14 @@ case "$btn" in
"overview")
$CCE_CTL overview
;;
+ "floating"|"tiled"|"fullscreen")
+ # The background right-click dropped focus for the popup's sake;
+ # the window just acted on is the one to hand it back to.
+ if [[ -n "$window_index" ]]; then
+ $CCE_CTL set-mode "$btn" "$window_index"
+ $CCE_CTL focus-window "$window_index"
+ fi
+ ;;
"reload")
$CCE_CTL reload
;;
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 79b601b..250c422 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -1679,7 +1679,30 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
let x = cursor.x() as i32;
let y = cursor.y() as i32;
let home = std::env::var("HOME").unwrap_or_default();
- let cmd = format!("{}/.local/bin/cce-desktop-menu -x {} -y {}", home, x, y);
+ let mut cmd = format!("{}/.local/bin/cce-desktop-menu -x {} -y {}", home, x, y);
+ // The menu's "Window Mode" page acts on the FOCUSED window,
+ // and focus is dropped right below (so the popup takes the
+ // keyboard and a click-away dismisses it) — by the time the
+ // script asks `ccectl windows` nothing is focused. Hand it
+ // the window here instead. Only a window whose mode
+ // `set-mode` accepts is worth naming.
+ if let Focus::Window(w) = seat.focused {
+ if !w.is_null()
+ && matches!(
+ (*w).tiling_mode,
+ crate::tiling::TilingMode::Floating
+ | crate::tiling::TilingMode::Tiled
+ | crate::tiling::TilingMode::Fullscreen
+ )
+ {
+ let app_id = (*w).get_app_id_string().unwrap_or_else(|| "unknown".to_string());
+ cmd.push_str(&format!(
+ " -i {} -a '{}'",
+ (*w).ref_key.index,
+ app_id.replace('\'', "'\\''")
+ ));
+ }
+ }
(*server).wm.execute_action(&crate::config::Action::Spawn, Some(&cmd));
seat.focus(Focus::None);