status bar
git clone https://git.lucas.co/cce-status-interface.git
refactor: port popups to cce_ui::process::CloudPopup{,Tracker} (proposal phase 4)
The four popup sites — window picker (trigger_switcher + spawn_window_picker),
tray DBus menus (show_cce_cloud_menu), the module context menu, and the
layout-mode menu — now share cce-ui's extracted helpers: CloudPopupTracker
(field cloud_popups) replaces the hand-rolled active_cloud_pid/
active_cloud_source state and the four copies of the /proc + comm + kill
toggle dance; CloudPopup::run_json/run_dmenu replace the four spawn/stdin/
wait_with_output blocks. CloudSpawned/CloudClosed events now just feed
tracker.on_spawned/on_closed; focus-restore stays behind on_closed's
confirmation.
Deliberate unifications (previously the sites disagreed):
- the window picker now kills another source's open popup before opening
(the other three already did); before, that popup was orphaned untracked
- the picker toggle now checks /proc/<pid>/comm like the other sites,
guarding against pid reuse
- the layout/context menus track their pid via CloudSpawned instead of
synchronously, so a click during spawn cancels cleanly (on_spawned kills
the late pid) instead of racing
- popup stderr is uniformly piped and logged at debug
get_cce_cloud_cmd and the SIGTERM helper moved into cce-ui (libc dep dropped
here). Requires cce-ui 5272446.
Co-Authored-By: Claude Fable 5 <[email protected]>
CLAUDE.md | 10 +-
Cargo.toml | 1 -
src/cloud.rs | 144 +++++++---------------------
src/config.rs | 12 +--
src/main.rs | 304 +++++++++++++---------------------------------------------
5 files changed, 113 insertions(+), 358 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 9066c4f..9bf015e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -93,10 +93,12 @@ both). Keyboard alt-tab switching is delegated to the compositor
(`ccectl window-switcher`) — don't reimplement it here.
**Popups are `cce-cloud` processes**, not surfaces of this app: the window picker, tray
-context menus, and the layout-mode menu each spawn `cce-cloud`, pipe it a JSON page
-description on stdin, and track it via the `CloudSpawned`/`CloudClosed` events
-(`active_cloud_pid`/`active_cloud_source`). Clicking again toggles the popup off by
-killing the pid; closing restores focus with `ccectl focus-window`. Follow this pattern
+context menus, and the layout-mode menu each run a `cce_ui::process::CloudPopup`
+(`run_json`/`run_dmenu`) on a worker thread, and the single-popup toggle state lives in
+`cce_ui::process::CloudPopupTracker` (`StatusApp.cloud_popups`) — the thread reports
+back via the `CloudSpawned`/`CloudClosed` events, which feed
+`tracker.on_spawned`/`on_closed`. Clicking a trigger again toggles its popup off
+(`tracker.click`); closing restores focus with `ccectl focus-window`. Follow this pattern
for any new popup.
## Config
diff --git a/Cargo.toml b/Cargo.toml
index b71975e..22f1204 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,6 @@ tokio-stream = "0.1"
zbus = "4"
resvg = "0.41.0"
png = "0.17"
-libc = "0.2"
log = "0.4"
env_logger = "0.11"
kdl = "4.6"
diff --git a/src/cloud.rs b/src/cloud.rs
index 11774a5..c3b7c0a 100644
--- a/src/cloud.rs
+++ b/src/cloud.rs
@@ -2,7 +2,7 @@
//! a `cce-cloud` process fed JSON pages on stdin.
use crate::{parse_ccectl_windows, CustomEvent};
-use crate::config::{get_cce_cloud_cmd, get_ccectl_cmd};
+use crate::config::get_ccectl_cmd;
#[zbus::proxy(
interface = "com.canonical.dbusmenu",
@@ -105,18 +105,6 @@ pub(crate) fn get_currently_focused_window() -> Option<String> {
.map(|(id, _, _, _)| id)
}
-/// Send SIGTERM to `pid` directly instead of shelling out to `kill`,
-/// logging when the signal cannot be delivered.
-pub(crate) fn send_sigterm(pid: u32) {
- let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
- if ret != 0 {
- log::warn!(
- "[cloud] SIGTERM to pid {} failed: {}",
- pid,
- std::io::Error::last_os_error()
- );
- }
-}
pub(crate) async fn show_cce_cloud_menu(
conn: &zbus::Connection,
@@ -304,39 +292,20 @@ pub(crate) async fn show_cce_cloud_menu(
});
let layout_str = layout_json.to_string();
- let mut cmd_args = vec![
- "--json".to_string(),
- "-x".to_string(),
- x_pos.to_string(),
- "-y".to_string(),
- y_pos.to_string(),
- "--parent-app-id".to_string(),
- parent_app_id,
- ];
+ let mut popup = cce_ui::process::CloudPopup::at(x_pos, y_pos)
+ .parent_app_id(parent_app_id);
if align_right {
- cmd_args.push("--align-right".to_string());
- }
-
- let mut child = std::process::Command::new(get_cce_cloud_cmd())
- .args(&cmd_args)
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::inherit())
- .spawn()?;
-
- let pid = child.id();
- last_spawned_pid = pid;
- let _ = thread_sender.send(CustomEvent::CloudSpawned { pid, source: source.clone() });
-
- if let Some(mut stdin) = child.stdin.take() {
- use std::io::Write;
- stdin.write_all(layout_str.as_bytes())?;
+ popup = popup.align_right();
}
-
- let output = child.wait_with_output()?;
- if output.status.success() {
- let stdout_str = String::from_utf8_lossy(&output.stdout);
- if let Ok(parsed_json) = serde_json::from_str::<serde_json::Value>(stdout_str.trim()) {
+ // run_json blocks this thread, like the wait_with_output it replaces —
+ // fine, show_cce_cloud_menu runs on its own single-purpose runtime.
+ let output = popup.run_json(&layout_str, |pid| {
+ last_spawned_pid = pid;
+ let _ = thread_sender.send(CustomEvent::CloudSpawned { pid, source: source.clone() });
+ })?;
+
+ if let Some(stdout_str) = output {
+ if let Ok(parsed_json) = serde_json::from_str::<serde_json::Value>(&stdout_str) {
if let Some(btn_id) = parsed_json.get("button").and_then(|v| v.as_str()) {
if btn_id.starts_with("item_") {
if let Ok(item_id) = btn_id["item_".len()..].parse::<i32>() {
@@ -398,74 +367,35 @@ pub(crate) fn spawn_window_picker(
input_str.push('\n');
}
- let cmd_args = vec![
- "--dmenu".to_string(),
- "-p".to_string(),
- "Windows:".to_string(),
- "-x".to_string(),
- x_pos.to_string(),
- "-y".to_string(),
- y_pos.to_string(),
- ];
-
- let mut child = match std::process::Command::new(get_cce_cloud_cmd())
- .args(&cmd_args)
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::inherit())
- .spawn()
- {
- Ok(c) => c,
- Err(e) => {
- log::warn!("[switcher] Failed to spawn cce-cloud: {:?}", e);
- let _ = thread_sender.send(CustomEvent::CloudClosed { pid: 0, source: source.clone() });
- return;
- }
- };
-
- let pid = child.id();
- let mut stdin = child.stdin.take().unwrap();
- let mut stdout = child.stdout.take().unwrap();
-
- // Write the item list, then drop stdin so cce-cloud sees EOF.
- use std::io::Write;
- let _ = stdin.write_all(input_str.as_bytes());
- let _ = stdin.flush();
- drop(stdin);
-
- // Spawn stdout reader
- let (stdout_tx, stdout_rx) = std::sync::mpsc::channel();
- std::thread::spawn(move || {
- let mut out_str = String::new();
- use std::io::Read;
- let _ = stdout.read_to_string(&mut out_str);
- let _ = stdout_tx.send(out_str);
+ let popup = cce_ui::process::CloudPopup::at(x_pos, y_pos);
+ let mut spawned_pid = 0;
+ let result = popup.run_dmenu("Windows:", &input_str, |pid| {
+ spawned_pid = pid;
+ let _ = thread_sender.send(CustomEvent::CloudSpawned { pid, source: source.clone() });
});
- let _ = thread_sender.send(CustomEvent::CloudSpawned { pid, source: source.clone() });
-
- let _ = child.wait();
- let stdout_str = stdout_rx.recv().unwrap_or_default();
-
- let selected = stdout_str.trim().to_string();
- if !selected.is_empty() {
- // Find the matched window
- for (id, app_id, title, _) in windows {
- let display = if title.is_empty() {
- app_id.clone()
- } else {
- format!("{} ({})", title, app_id)
- };
- if display == selected {
- log::debug!("[switcher] Selecting window title: {}, app_id: {}, id: {}", title, app_id, id);
- let _ = std::process::Command::new(get_ccectl_cmd())
- .args(["focus-window", &id])
- .spawn();
- break;
+ match result {
+ Ok(Some(selected)) => {
+ // Find the matched window
+ for (id, app_id, title, _) in windows {
+ let display = if title.is_empty() {
+ app_id.clone()
+ } else {
+ format!("{} ({})", title, app_id)
+ };
+ if display == selected {
+ log::debug!("[switcher] Selecting window title: {}, app_id: {}, id: {}", title, app_id, id);
+ let _ = std::process::Command::new(get_ccectl_cmd())
+ .args(["focus-window", &id])
+ .spawn();
+ break;
+ }
}
}
+ Ok(None) => {}
+ Err(e) => log::warn!("[switcher] Failed to spawn cce-cloud: {:?}", e),
}
- let _ = thread_sender.send(CustomEvent::CloudClosed { pid, source: source.clone() });
+ let _ = thread_sender.send(CustomEvent::CloudClosed { pid: spawned_pid, source: source.clone() });
});
}
diff --git a/src/config.rs b/src/config.rs
index 957d6c5..6c492b4 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,5 +1,5 @@
//! Config access: cached KDL config lookup, color/font/dimension readers,
-//! and resolution of the cce/ccectl/cce-cloud binaries.
+//! and resolution of the ccectl binary.
//!
//! Every key has an explicit JSON-pointer location (the canonical nesting in
//! config.kdl). Reads try the pointer first and fall back to the legacy fuzzy
@@ -225,16 +225,6 @@ pub(crate) fn read_status_box_corner_radius_from_config() -> f32 {
cfg_f32("/style/status/box_corner_radius", "status_box_corner_radius").unwrap_or(4.0)
}
-pub(crate) fn get_cce_cloud_cmd() -> String {
- if let Ok(home) = std::env::var("HOME") {
- let path = format!("{}/.local/bin/cce-cloud", home);
- if std::path::Path::new(&path).exists() {
- return path;
- }
- }
- "cce-cloud".to_string()
-}
-
pub(crate) fn get_ccectl_cmd() -> String {
if let Ok(home) = std::env::var("HOME") {
let path = format!("{}/.local/bin/ccectl", home);
diff --git a/src/main.rs b/src/main.rs
index e8fa256..91ad33a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -204,8 +204,7 @@ struct StatusApp {
tray_item_bounds: Vec<TrayIconBounds>,
viewport_bounds: Vec<ViewportBounds>,
layout_bounds: Option<LayoutBounds>,
- active_cloud_pid: Option<u32>,
- active_cloud_source: Option<String>,
+ cloud_popups: cce_ui::process::CloudPopupTracker,
previously_focused_window: Option<String>,
font_system: FontSystem,
@@ -731,25 +730,10 @@ impl StatusApp {
// click-to-pick list of the current windows.
let switcher_source = "window".to_string();
- // If a picker is already open (or pending) for this source, toggle it off.
- let running = self
- .active_cloud_pid
- .map_or(false, |pid| std::path::Path::new(&format!("/proc/{}", pid)).exists());
- if self.active_cloud_source.as_ref() == Some(&switcher_source) {
- if running {
- if let Some(pid) = self.active_cloud_pid {
- log::debug!("[window-picker] Toggling off existing cce-cloud PID {}", pid);
- send_sigterm(pid);
- }
- self.active_cloud_pid = None;
- }
- self.active_cloud_source = None;
+ if self.cloud_popups.click(&switcher_source) == cce_ui::process::CloudPopupClick::ToggledOff {
return;
}
- // Set active cloud source
- self.active_cloud_source = Some(switcher_source.clone());
-
// Get placement coords: align just below Window module if we can find it
let mut target_x = 0.0;
for mb in &self.module_bounds {
@@ -1053,8 +1037,7 @@ impl cce_ui::engine::Application for StatusApp {
tray_item_bounds: Vec::new(),
viewport_bounds: Vec::new(),
layout_bounds: None,
- active_cloud_pid: None,
- active_cloud_source: None,
+ cloud_popups: cce_ui::process::CloudPopupTracker::new(),
previously_focused_window: None,
font_system,
status_bar: cce_ui::widget::StatusBar::new(),
@@ -1133,19 +1116,11 @@ impl cce_ui::engine::Application for StatusApp {
self.tray_items.remove(&id);
}
CustomEvent::CloudSpawned { pid, source } => {
- if self.active_cloud_source.as_ref() == Some(&source) {
- log::debug!("[cloud-event] CloudSpawned: pid {} for source {} matches expected, tracking", pid, source);
- self.active_cloud_pid = Some(pid);
- } else {
- log::debug!("[cloud-event] CloudSpawned: pid {} for source {} is obsolete/canceled, killing", pid, source);
- send_sigterm(pid);
- }
+ self.cloud_popups.on_spawned(pid, &source);
}
CustomEvent::CloudClosed { pid, source } => {
- if self.active_cloud_pid == Some(pid) || (pid == 0 && self.active_cloud_source.as_ref() == Some(&source)) {
+ if self.cloud_popups.on_closed(pid, &source) {
log::debug!("[cloud-event] CloudClosed: pid {} for source {} closed, clearing tracking", pid, source);
- self.active_cloud_pid = None;
- self.active_cloud_source = None;
if source == "window" {
self.previously_focused_window = None;
} else if source == "layout" || source.starts_with("context_menu:") || source.starts_with("tray:") {
@@ -1349,41 +1324,9 @@ impl cce_ui::engine::Application for StatusApp {
let id = bound.id.clone();
let tray_source = format!("tray:{}", id);
- // Check if any cce-cloud instance is already running
- let mut running_cloud_pid = None;
- if let Some(pid) = self.active_cloud_pid {
- if std::path::Path::new(&format!("/proc/{}", pid)).exists() {
- if let Ok(comm) = std::fs::read_to_string(format!("/proc/{}/comm", pid)) {
- if comm.trim() == "cce-cloud" {
- running_cloud_pid = Some(pid);
- }
- }
- }
- }
-
- if let Some(pid) = running_cloud_pid {
- // There is an active dialog open.
- // Kill it regardless of which one it is.
- log::debug!("[tray-click] cce-cloud (PID {}) is running, killing it", pid);
- send_sigterm(pid);
- self.active_cloud_pid = None;
-
- // If it was clicked for the SAME tray icon, this is a toggle-off.
- if self.active_cloud_source.as_ref() == Some(&tray_source) {
- self.active_cloud_source = None;
- return None;
- }
- } else {
- // No active dialog is running, but check if there is a pending one for the same source
- if self.active_cloud_source.as_ref() == Some(&tray_source) {
- // User clicked same icon again while it was pending. Cancel it!
- self.active_cloud_source = None;
- return None;
- }
+ if self.cloud_popups.click(&tray_source) == cce_ui::process::CloudPopupClick::ToggledOff {
+ return None;
}
-
- // Now set the active cloud source to this one
- self.active_cloud_source = Some(tray_source.clone());
if self.previously_focused_window.is_none() {
self.previously_focused_window = get_currently_focused_window();
}
@@ -1489,37 +1432,10 @@ impl cce_ui::engine::Application for StatusApp {
log::debug!("[module-right-click] Right-clicked module: {}", mb.name);
let context_source = format!("context_menu:{}", mb.name);
- // Check if any cce-cloud instance is already running
- let mut running_cloud_pid = None;
- if let Some(pid) = self.active_cloud_pid {
- if std::path::Path::new(&format!("/proc/{}", pid)).exists() {
- if let Ok(comm) = std::fs::read_to_string(format!("/proc/{}/comm", pid)) {
- if comm.trim() == "cce-cloud" {
- running_cloud_pid = Some(pid);
- }
- }
- }
- }
-
- if let Some(pid) = running_cloud_pid {
- log::debug!("[module-right-click] cce-cloud (PID {}) is running, killing it", pid);
- send_sigterm(pid);
- self.active_cloud_pid = None;
-
- // If it was clicked for the same context menu, this is a toggle-off
- if self.active_cloud_source.as_ref() == Some(&context_source) {
- self.active_cloud_source = None;
- return None;
- }
- } else {
- if self.active_cloud_source.as_ref() == Some(&context_source) {
- self.active_cloud_source = None;
- return None;
- }
+ if self.cloud_popups.click(&context_source) == cce_ui::process::CloudPopupClick::ToggledOff {
+ return None;
}
- self.active_cloud_source = Some(context_source.clone());
-
if self.previously_focused_window.is_none() {
self.previously_focused_window = get_currently_focused_window();
}
@@ -1553,54 +1469,29 @@ impl cce_ui::engine::Application for StatusApp {
};
let parent_app_id = self.get_app_id();
-
- if let Ok(mut child) = std::process::Command::new(get_cce_cloud_cmd())
- .args([
- "--json",
- "-x",
- &x_pos.to_string(),
- "-y",
- &y_pos.to_string(),
- "--parent-app-id",
- &parent_app_id,
- ])
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::piped())
- .spawn()
- {
- let pid = child.id();
- self.active_cloud_pid = Some(pid);
- log::debug!("[module-right-click] Spawned cce-cloud with PID {}", pid);
-
- let thread_sender = self.sender.clone();
- let context_source_clone = context_source.clone();
- std::thread::spawn(move || {
- if let Some(mut stdin) = child.stdin.take() {
- use std::io::Write;
- let _ = stdin.write_all(context_json.as_bytes());
- }
- if let Ok(output) = child.wait_with_output() {
- let err_str = String::from_utf8_lossy(&output.stderr);
- if !err_str.is_empty() {
- log::debug!("[cce-cloud context stderr] {}", err_str);
- }
- if output.status.success() {
- let stdout_str = String::from_utf8_lossy(&output.stdout);
- if let Ok(parsed_json) = serde_json::from_str::<serde_json::Value>(stdout_str.trim()) {
- if let Some(btn_id) = parsed_json.get("button").and_then(|v| v.as_str()) {
- if btn_id == "toggle_hide" {
- let _ = thread_sender.send(CustomEvent::ToggleHideModules);
- } else if btn_id == "toggle_adjust" {
- let _ = thread_sender.send(CustomEvent::ToggleAdjustPositionMode);
- }
- }
+ let thread_sender = self.sender.clone();
+ std::thread::spawn(move || {
+ let popup = cce_ui::process::CloudPopup::at(x_pos, y_pos)
+ .parent_app_id(parent_app_id);
+ let mut spawned_pid = 0;
+ let result = popup.run_json(&context_json, |pid| {
+ spawned_pid = pid;
+ log::debug!("[module-right-click] Spawned cce-cloud with PID {}", pid);
+ let _ = thread_sender.send(CustomEvent::CloudSpawned { pid, source: context_source.clone() });
+ });
+ if let Ok(Some(out_str)) = &result {
+ if let Ok(parsed_json) = serde_json::from_str::<serde_json::Value>(out_str) {
+ if let Some(btn_id) = parsed_json.get("button").and_then(|v| v.as_str()) {
+ if btn_id == "toggle_hide" {
+ let _ = thread_sender.send(CustomEvent::ToggleHideModules);
+ } else if btn_id == "toggle_adjust" {
+ let _ = thread_sender.send(CustomEvent::ToggleAdjustPositionMode);
}
}
}
- let _ = thread_sender.send(CustomEvent::CloudClosed { pid, source: context_source_clone });
- });
- }
+ }
+ let _ = thread_sender.send(CustomEvent::CloudClosed { pid: spawned_pid, source: context_source });
+ });
return None;
}
@@ -1622,41 +1513,10 @@ impl cce_ui::engine::Application for StatusApp {
log::debug!("[layout-click] Layout mode clicked!");
let layout_source = "layout".to_string();
- // Check if any cce-cloud instance is already running
- let mut running_cloud_pid = None;
- if let Some(pid) = self.active_cloud_pid {
- if std::path::Path::new(&format!("/proc/{}", pid)).exists() {
- if let Ok(comm) = std::fs::read_to_string(format!("/proc/{}/comm", pid)) {
- if comm.trim() == "cce-cloud" {
- running_cloud_pid = Some(pid);
- }
- }
- }
- }
-
- if let Some(pid) = running_cloud_pid {
- // There is an active dialog open.
- // Kill it regardless of which one it is.
- log::debug!("[layout-click] cce-cloud (PID {}) is running, killing it", pid);
- send_sigterm(pid);
- self.active_cloud_pid = None;
-
- // If it was clicked for the layout menu, this is a toggle-off.
- if self.active_cloud_source.as_ref() == Some(&layout_source) {
- self.active_cloud_source = None;
- return None;
- }
- } else {
- // No active dialog is running, but check if there is a pending one for the same source
- if self.active_cloud_source.as_ref() == Some(&layout_source) {
- self.active_cloud_source = None;
- return None;
- }
+ if self.cloud_popups.click(&layout_source) == cce_ui::process::CloudPopupClick::ToggledOff {
+ return None;
}
- // Now set the active cloud source to this one
- self.active_cloud_source = Some(layout_source);
-
if self.previously_focused_window.is_none() {
self.previously_focused_window = get_currently_focused_window();
}
@@ -1664,7 +1524,6 @@ impl cce_ui::engine::Application for StatusApp {
let x_pos = self.layout_bounds.as_ref().map(|b| b.x as i32).unwrap_or(0);
let y_pos = self.layout_bounds.as_ref().map(|b| b.h as i32).unwrap_or_else(|| read_status_height_from_config() as i32);
- // Spawn the child on the main thread so we can capture its PID
let layout_json = serde_json::json!({
"width": 240,
"height": 320,
@@ -1679,72 +1538,47 @@ impl cce_ui::engine::Application for StatusApp {
]
}).to_string();
- if let Ok(mut child) = std::process::Command::new(get_cce_cloud_cmd())
- .args([
- "--json",
- "-x",
- &x_pos.to_string(),
- "-y",
- &y_pos.to_string(),
- ])
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::piped())
- .spawn()
- {
- let pid = child.id();
- self.active_cloud_pid = Some(pid);
- log::debug!("[layout-click] Spawned cce-cloud with PID {}", pid);
-
- let active_viewport = get_active_viewport_from_camera(&self.viewport);
- let thread_sender = self.sender.clone();
- std::thread::spawn(move || {
- log::debug!("[layout-click] Active tag is {}", active_viewport);
- if let Some(mut stdin) = child.stdin.take() {
- use std::io::Write;
- let _ = stdin.write_all(layout_json.as_bytes());
+ let active_viewport = get_active_viewport_from_camera(&self.viewport);
+ let thread_sender = self.sender.clone();
+ std::thread::spawn(move || {
+ log::debug!("[layout-click] Active tag is {}", active_viewport);
+ let popup = cce_ui::process::CloudPopup::at(x_pos, y_pos);
+ let mut spawned_pid = 0;
+ let result = popup.run_json(&layout_json, |pid| {
+ spawned_pid = pid;
+ log::debug!("[layout-click] Spawned cce-cloud with PID {}", pid);
+ let _ = thread_sender.send(CustomEvent::CloudSpawned { pid, source: layout_source.clone() });
+ });
+ if let Ok(Some(out_str)) = &result {
+ #[derive(serde::Deserialize)]
+ struct LayoutMenuOutput {
+ button: String,
+ checkboxes: std::collections::HashMap<String, bool>,
}
- if let Ok(output) = child.wait_with_output() {
- let err_str = String::from_utf8_lossy(&output.stderr);
- if !err_str.is_empty() {
- log::debug!("[cce-cloud stderr] {}", err_str);
- }
- if output.status.success() {
- let out_str = String::from_utf8_lossy(&output.stdout);
- #[derive(serde::Deserialize)]
- struct LayoutMenuOutput {
- button: String,
- checkboxes: std::collections::HashMap<String, bool>,
- }
- if let Ok(val) = serde_json::from_str::<LayoutMenuOutput>(out_str.trim()) {
- let selected_mode = val.button.to_lowercase();
- let apply_all = val.checkboxes.get("apply_all").copied().unwrap_or(false);
- if apply_all {
- log::debug!("[layout-click] Selected mode: {}, applying to all windows sharing mode", selected_mode);
- let _ = std::process::Command::new(get_ccectl_cmd())
- .args(["apply-mode-sharing", &selected_mode])
- .spawn();
- } else {
- log::debug!("[layout-click] Selected mode: {}, setting for tag {}", selected_mode, active_viewport);
- let _ = std::process::Command::new(get_ccectl_cmd())
- .args(["viewport-layout", &active_viewport.to_string(), &selected_mode])
- .spawn();
- }
- } else {
- // Fallback
- let selected = out_str.trim().to_string();
- if !selected.is_empty() {
- let selected_lower = selected.to_lowercase();
- let _ = std::process::Command::new(get_ccectl_cmd())
- .args(["viewport-layout", &active_viewport.to_string(), &selected_lower])
- .spawn();
- }
- }
+ if let Ok(val) = serde_json::from_str::<LayoutMenuOutput>(out_str) {
+ let selected_mode = val.button.to_lowercase();
+ let apply_all = val.checkboxes.get("apply_all").copied().unwrap_or(false);
+ if apply_all {
+ log::debug!("[layout-click] Selected mode: {}, applying to all windows sharing mode", selected_mode);
+ let _ = std::process::Command::new(get_ccectl_cmd())
+ .args(["apply-mode-sharing", &selected_mode])
+ .spawn();
+ } else {
+ log::debug!("[layout-click] Selected mode: {}, setting for tag {}", selected_mode, active_viewport);
+ let _ = std::process::Command::new(get_ccectl_cmd())
+ .args(["viewport-layout", &active_viewport.to_string(), &selected_mode])
+ .spawn();
}
+ } else {
+ // Fallback
+ let selected_lower = out_str.to_lowercase();
+ let _ = std::process::Command::new(get_ccectl_cmd())
+ .args(["viewport-layout", &active_viewport.to_string(), &selected_lower])
+ .spawn();
}
- let _ = thread_sender.send(CustomEvent::CloudClosed { pid, source: "layout".to_string() });
- });
- }
+ }
+ let _ = thread_sender.send(CustomEvent::CloudClosed { pid: spawned_pid, source: layout_source });
+ });
} else {
let mut clicked_window = false;
for mb in &self.module_bounds {