git.lucas.co / cce-cloud
cloud storage client
git clone https://git.lucas.co/cce-cloud.git

commitacbf7eab94b4e589e2560638eea2dee3dcaef64f
parentc0425e62fb
authorLucas Galante <[email protected]>
date2026-08-27 08:12
feat: launcher passes its invocation point to what it launches

An app picked from the launcher lands on the grid square the launcher
was invoked at — but only when there IS one. The desktop menu now passes
its click through to the app list, so picking an app there opens it on
that square; a launcher summoned by keyboard has no "here" to mean, and
its apps keep their remembered place.

de_bin() resolves ccectl beside this binary rather than by bare name:
the launcher runs as a systemd user service whose PATH omits
~/.local/bin, the same trap that made cce-grid's context menu fail
silently on the real desktop while working in every test.

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

 src/main.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 51 insertions(+)

diff --git a/src/main.rs b/src/main.rs
index 75a69ac..dc75284 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -475,6 +475,44 @@ fn spawn_command(cmd: &str) {
     spawn_detached("sh", &["-c", cmd]);
 }
 
+/// Resolve a cce binary installed beside this one.
+///
+/// The launcher runs as a systemd user service, whose PATH is
+/// `/usr/local/bin:/usr/bin` — `~/.local/bin`, where every cce binary lives,
+/// is not on it, so spawning one by bare name fails with ENOENT under systemd
+/// while working fine from a shell.
+fn de_bin(name: &str) -> std::path::PathBuf {
+    if let Ok(exe) = std::env::current_exe() {
+        if let Some(dir) = exe.parent() {
+            let beside = dir.join(name);
+            if beside.exists() {
+                return beside;
+            }
+        }
+    }
+    std::path::PathBuf::from(name)
+}
+
+/// Open the launched window at the grid square this launcher was invoked at,
+/// keeping its remembered size and growing away from its neighbours.
+///
+/// Only when there IS an invocation point: the desktop menu passes one
+/// through, a launcher summoned by keyboard does not, and in that case the app
+/// keeps its remembered place — there is no "here" to mean.
+fn place_next_at(exec: &str, invoked_at: Option<(i32, i32)>) {
+    let Some((x, y)) = invoked_at else { return };
+    let first = exec.split_whitespace().next().unwrap_or("");
+    let prog = first.rsplit('/').next().unwrap_or(first);
+    if prog.is_empty() {
+        return;
+    }
+    let _ = std::process::Command::new(de_bin("ccectl"))
+        .args(["place-next-cell", prog, &x.to_string(), &y.to_string()])
+        .stdout(std::process::Stdio::null())
+        .stderr(std::process::Stdio::null())
+        .status();
+}
+
 /// Launch an app entry; `Terminal=true` entries are hosted in a terminal
 /// emulator (entries are dropped at scan time when none is installed).
 fn spawn_app(app: &AppInfo) {
@@ -927,6 +965,12 @@ struct State {
     frame_images: Vec<ImageQuad>,
     plate_features: Vec<[f32; 12]>,
 
+    /// Where this popup was invoked, in layout px, when it was given a
+    /// position at all (the desktop menu passes its click through; a
+    /// keyboard-summoned launcher has no "here" to mean). Used to place the
+    /// launched window on that grid square.
+    invoked_at: Option<(i32, i32)>,
+
     fuzzel: cce_ui::widget::Adapted<FuzzelWidget>,
     json_layout: Option<cce_ui::widget::Adapted<JsonLayoutWidget>>,
     font_system: FontSystem,
@@ -1253,7 +1297,12 @@ impl State {
         let bg_color = cce_ui::color::page_low_color();
         let window_rect = (0.0, 0.0, lw, lh);
 
+        let invoked_at = match (x_pos, y_pos) {
+            (Some(x), Some(y)) => Some((x, y)),
+            _ => None,
+        };
         let mut state = Self {
+            invoked_at,
             window: Some(window),
             wl_surface,
             renderer: Some(renderer),
@@ -1679,10 +1728,12 @@ impl AppState {
                         LauncherMode::Apps => {
                             if let Some(app) = st.apps.iter().find(|app| &app.name == item) {
                                 record_app_launch(&app.name);
+                                place_next_at(&app.exec, st.invoked_at);
                                 spawn_app(app);
                             }
                         }
                         LauncherMode::Path => {
+                            place_next_at(item, st.invoked_at);
                             spawn_command(item);
                         }
                         LauncherMode::Dmenu => {}