GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor(process): remove the cce-cloud popup API
CloudPopup, CloudPopupTracker, CloudPopupClick and get_cce_cloud_cmd had
no caller left anywhere in the workspace: every app that once spawned a
cce-cloud popup opens an in-process widget now (the designer's palette was
the last, on 2026-09-19). The detached/tracked spawn helpers stay.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md | 8 +-
src/process.rs | 298 ---------------------------------------------------------
2 files changed, 4 insertions(+), 302 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index f6fa371..9c7a45a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -619,10 +619,10 @@ cce-system-interface) to confirm behavior, not just the test suite.
KDL/JSON-driven `json_layout.rs` is dissolved; `scene/layout.rs` is the box model.)
- `protocol.rs` — inline-generated Wayland protocol bindings.
- `ipc.rs` — the `/tmp/<prefix>-<WAYLAND_DISPLAY>.sock` helpers (`socket_path`, `send_command`).
-- `process.rs` — detached/tracked child spawning, plus the `cce-cloud` popup pattern:
- `CloudPopup` (one blocking `run_json`/`run_dmenu` invocation) and `CloudPopupTracker`
- (an app's single-active-popup toggle state; see cce-status-interface for the
- canonical usage).
+- `process.rs` — detached/tracked child spawning. (The `cce-cloud` popup pattern
+ that lived beside it — `CloudPopup`, `CloudPopupTracker` — was removed on
+ 2026-09-28: every app that spawned a popup has an in-process widget for it now,
+ and no crate in the workspace called it.)
- `icon.rs` — XDG icon-theme lookup: a `.desktop` `Icon=` key (or an SNI tray icon
name) → a file on disk, plus `upload_themed` to rasterize/decode and upload it.
**Not** `lib.rs`'s `upload_icon`, which loads a *bundled* cce-icons glyph by its
diff --git a/src/process.rs b/src/process.rs
index e211cce..9851e38 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -1,4 +1,3 @@
-use std::io::Write;
use std::process::Command;
use std::sync::Mutex;
@@ -42,239 +41,6 @@ pub fn cleanup_spawned_processes() {
}
}
-/// Resolve the `cce-cloud` binary, preferring `~/.local/bin`.
-pub 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()
-}
-
-fn send_sigterm(pid: u32) {
- let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
- if ret != 0 {
- log::warn!(
- "[cloud-popup] SIGTERM to pid {} failed: {}",
- pid,
- std::io::Error::last_os_error()
- );
- }
-}
-
-/// What a click on a popup trigger should do next — the result of
-/// [`CloudPopupTracker::click`].
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CloudPopupClick {
- /// The click closed (or canceled) this source's own popup; don't spawn a
- /// new one.
- ToggledOff,
- /// Spawn this source's popup now; any other source's popup has been
- /// killed and the tracker is pending on this source.
- Open,
-}
-
-/// Tracks an application's single active `cce-cloud` popup.
-///
-/// The pattern: a trigger (button, tray icon, …) identified by a `source`
-/// string spawns a `cce-cloud` process on a worker thread ([`CloudPopup`]);
-/// the thread reports the pid back through the app's event loop
-/// ([`on_spawned`](Self::on_spawned)) and reports exit when the popup closes
-/// ([`on_closed`](Self::on_closed)). Clicking the trigger again while its
-/// popup is open (or still spawning) toggles it off. Only one popup exists at
-/// a time: opening one source's popup kills any other's.
-#[derive(Debug, Default)]
-pub struct CloudPopupTracker {
- active_pid: Option<u32>,
- active_source: Option<String>,
-}
-
-/// Basename of a `/proc/<pid>/exe` link, with the `" (deleted)"` marker the
-/// kernel appends once the binary has been unlinked stripped off.
-fn exe_basename(exe: &std::path::Path) -> Option<&str> {
- let name = exe.file_name()?.to_str()?;
- Some(name.strip_suffix(" (deleted)").unwrap_or(name))
-}
-
-impl CloudPopupTracker {
- pub fn new() -> Self {
- Self::default()
- }
-
- /// The tracked popup's pid, if that process is still alive and still a
- /// `cce-cloud` (guards against pid reuse).
- ///
- /// Identity comes from the exe link, not `/proc/<pid>/comm`: comm is capped
- /// at 15 characters, so it silently truncates for over half the cce
- /// binaries (all three `cce-keyring-unlock*` collapse onto one string).
- /// `cce-cloud` fits today, which is the only reason a comm comparison
- /// worked — a rename to anything longer would have quietly made this always
- /// return `None`, leaving the tracker convinced no popup is ever open.
- ///
- /// The exe link needs the `" (deleted)"` suffix stripped: `ccebuild install`
- /// unlinks before writing, so every popup running across a reinstall reads
- /// as `cce-cloud (deleted)` — the one case where comm was the more forgiving
- /// of the two.
- fn running_pid(&self) -> Option<u32> {
- let pid = self.active_pid?;
- let exe = std::fs::read_link(format!("/proc/{}/exe", pid)).ok()?;
- (exe_basename(&exe) == Some("cce-cloud")).then_some(pid)
- }
-
- /// Whether the tracked popup is currently running.
- pub fn is_open(&self) -> bool {
- self.running_pid().is_some()
- }
-
- /// Handle a click on `source`'s trigger. Kills whatever popup is open;
- /// returns [`CloudPopupClick::ToggledOff`] when the click closed or
- /// canceled `source`'s own popup, [`CloudPopupClick::Open`] when the
- /// caller should now spawn `source`'s popup.
- pub fn click(&mut self, source: &str) -> CloudPopupClick {
- if let Some(pid) = self.running_pid() {
- log::debug!("[cloud-popup] killing open popup pid {} on click for '{}'", pid, source);
- send_sigterm(pid);
- self.active_pid = None;
- if self.active_source.as_deref() == Some(source) {
- self.active_source = None;
- return CloudPopupClick::ToggledOff;
- }
- } else if self.active_source.as_deref() == Some(source) {
- // Same source clicked again while its popup was still spawning:
- // cancel it (on_spawned will kill the late-arriving pid).
- self.active_source = None;
- return CloudPopupClick::ToggledOff;
- }
- self.active_source = Some(source.to_string());
- CloudPopupClick::Open
- }
-
- /// A spawner thread announced its popup's pid. Adopts the pid if `source`
- /// is still the active one; kills the process if the popup was canceled
- /// or superseded while spawning.
- pub fn on_spawned(&mut self, pid: u32, source: &str) {
- if self.active_source.as_deref() == Some(source) {
- self.active_pid = Some(pid);
- } else {
- log::debug!("[cloud-popup] pid {} for source '{}' is obsolete/canceled, killing", pid, source);
- send_sigterm(pid);
- }
- }
-
- /// A spawner thread reported its popup closed (`pid` 0 when it never
- /// spawned). Returns `true` — with the tracker cleared — when it was the
- /// active popup, so the caller can run its close action (e.g. restore
- /// focus). `false` for stale reports from superseded popups.
- pub fn on_closed(&mut self, pid: u32, source: &str) -> bool {
- if self.active_pid == Some(pid)
- || (pid == 0 && self.active_source.as_deref() == Some(source))
- {
- self.active_pid = None;
- self.active_source = None;
- true
- } else {
- false
- }
- }
-}
-
-/// One `cce-cloud` popup invocation: where it opens and how it's parented.
-///
-/// [`run_json`](Self::run_json) / [`run_dmenu`](Self::run_dmenu) block until
-/// the popup closes — call them from a worker thread, report the pid from
-/// `on_spawn` back to the event loop for [`CloudPopupTracker::on_spawned`],
-/// and report [`CloudPopupTracker::on_closed`] when they return.
-#[derive(Debug, Clone, Default)]
-pub struct CloudPopup {
- x: i32,
- y: i32,
- parent_app_id: Option<String>,
- align_right: bool,
-}
-
-impl CloudPopup {
- pub fn at(x: i32, y: i32) -> Self {
- Self { x, y, ..Self::default() }
- }
-
- /// Parent the popup to a surface by app id (compositor-side placement).
- pub fn parent_app_id(mut self, app_id: impl Into<String>) -> Self {
- self.parent_app_id = Some(app_id.into());
- self
- }
-
- /// Grow the popup leftward from `x` instead of rightward.
- pub fn align_right(mut self) -> Self {
- self.align_right = true;
- self
- }
-
- /// `--json` mode: pipe a JSON page description, block until the popup
- /// closes, and return its trimmed stdout (the selection JSON). `None`
- /// when the popup was dismissed/killed without a selection.
- pub fn run_json(
- &self,
- layout_json: &str,
- on_spawn: impl FnOnce(u32),
- ) -> std::io::Result<Option<String>> {
- self.run(&["--json".to_string()], layout_json, on_spawn)
- }
-
- /// `--dmenu` mode: pipe newline-separated items, block until the popup
- /// closes, and return the selected line. `None` when nothing was picked.
- pub fn run_dmenu(
- &self,
- prompt: &str,
- items: &str,
- on_spawn: impl FnOnce(u32),
- ) -> std::io::Result<Option<String>> {
- self.run(&["--dmenu".to_string(), "-p".to_string(), prompt.to_string()], items, on_spawn)
- }
-
- fn run(
- &self,
- mode_args: &[String],
- stdin_payload: &str,
- on_spawn: impl FnOnce(u32),
- ) -> std::io::Result<Option<String>> {
- let mut args = mode_args.to_vec();
- args.extend(["-x".to_string(), self.x.to_string(), "-y".to_string(), self.y.to_string()]);
- if let Some(ref parent) = self.parent_app_id {
- args.extend(["--parent-app-id".to_string(), parent.clone()]);
- }
- if self.align_right {
- args.push("--align-right".to_string());
- }
-
- let mut child = Command::new(get_cce_cloud_cmd())
- .args(&args)
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::piped())
- .spawn()?;
-
- on_spawn(child.id());
-
- if let Some(mut stdin) = child.stdin.take() {
- let _ = stdin.write_all(stdin_payload.as_bytes());
- // dropped here: cce-cloud sees EOF
- }
-
- let 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() {
- return Ok(None);
- }
- let out = String::from_utf8_lossy(&output.stdout).trim().to_string();
- Ok((!out.is_empty()).then_some(out))
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -291,68 +57,4 @@ mod tests {
assert!(spawn_tracked(cmd).is_ok());
cleanup_spawned_processes();
}
-
- #[test]
- fn exe_basename_strips_the_deleted_marker() {
- use std::path::Path;
- assert_eq!(exe_basename(Path::new("/home/u/.local/bin/cce-cloud")), Some("cce-cloud"));
- // `ccebuild install` unlinks before writing, so a popup that outlives a
- // reinstall reads like this — still a cce-cloud, and the tracker must
- // keep recognizing it or it loses the ability to close its own popup.
- assert_eq!(
- exe_basename(Path::new("/home/u/.local/bin/cce-cloud (deleted)")),
- Some("cce-cloud")
- );
- assert_eq!(exe_basename(Path::new("/usr/bin/foot")), Some("foot"));
- }
-
- #[test]
- fn running_pid_rejects_a_live_process_that_is_not_cce_cloud() {
- // The state-machine tests below use pids that are almost certainly
- // dead, so `running_pid` returns None for want of a process at all.
- // This one adopts a pid that definitely IS alive — the test runner —
- // to show the gate is identity, not mere liveness. (Only `is_open` is
- // called here: it never signals, so the runner is in no danger.)
- let mut t = CloudPopupTracker::new();
- assert_eq!(t.click("layout"), CloudPopupClick::Open);
- t.on_spawned(std::process::id(), "layout");
- assert!(!t.is_open(), "a live non-cce-cloud pid must not count as an open popup");
- }
-
- // --- CloudPopupTracker state machine ---
- // (pids here are never live cce-cloud processes, so running_pid() is
- // always None — these tests cover the pending/source transitions; the
- // kill-the-open-popup paths need a live popup and are covered by the
- // manual smoke test.)
-
- #[test]
- fn click_open_then_second_click_cancels_pending() {
- let mut t = CloudPopupTracker::new();
- assert_eq!(t.click("layout"), CloudPopupClick::Open);
- assert_eq!(t.click("layout"), CloudPopupClick::ToggledOff);
- // canceled: a late pid announcement must not be adopted
- t.on_spawned(4_000_000, "layout");
- assert!(!t.is_open());
- assert!(!t.on_closed(4_000_000, "layout"));
- }
-
- #[test]
- fn different_source_supersedes_pending() {
- let mut t = CloudPopupTracker::new();
- assert_eq!(t.click("tray:a"), CloudPopupClick::Open);
- assert_eq!(t.click("tray:b"), CloudPopupClick::Open);
- // a's late spawn is obsolete; b's is adopted
- t.on_spawned(4_000_001, "tray:a");
- t.on_spawned(4_000_002, "tray:b");
- assert!(t.on_closed(4_000_002, "tray:b"));
- }
-
- #[test]
- fn closed_with_pid_zero_matches_pending_source_only() {
- let mut t = CloudPopupTracker::new();
- assert_eq!(t.click("window"), CloudPopupClick::Open);
- assert!(!t.on_closed(0, "layout"));
- assert!(t.on_closed(0, "window"));
- assert_eq!(t.click("window"), CloudPopupClick::Open);
- }
}