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

commit754d0bab801a3710f6496e98cbe4dc0eda7810f6
parent9d454c4c37
authorLucas Galante <[email protected]>
date2026-08-16 10:55
feat: ccebuild installs the .desktop entries crates ship

Desktop entries were the last piece of the DE installed by hand. All ten lived only in ~/.local/share/applications, outside version control entirely — the workspace root is not a git repo — and they had drifted the way unversioned files do: nine hardcoded Exec=/home/lsgalante/.local/bin/<bin>, and cce-test-interface pointed Icon at a path that had stopped existing. This is the same hand-maintained-list failure ccebuild already exists to avoid for binaries and units.

Crates now ship <name>.desktop at their own root, beside Cargo.toml and any unit file, and install picks them up filtered per package exactly like units, then refreshes the MIME cache. Discovery is -maxdepth 2 rather than the units' 3 because desktop entries belong at the crate root; the crate-directory logic the two loops share is factored out as file_crate_dir, which is what unwraps cce-compositor/scripts/ for the deeper unit case.

This is also what unblocks the settings app's Default Apps page: a cce app is only offered as a handler if its entry declares MimeType, so shipping these in-repo is the prerequisite for adding those declarations.

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

 CLAUDE.md        |  9 +++++++++
 WORKSPACE.md     | 23 +++++++++++++++++++++++
 scripts/ccebuild | 48 +++++++++++++++++++++++++++++++++++++++++++-----
 3 files changed, 75 insertions(+), 5 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 5e224ed..0c4c135 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -62,6 +62,15 @@ around it. When touching it, keep two invariants:
   Matching is anchored: a basename must equal a dead crate name exactly, or that name
   plus a hex hash.
 
+It also installs the **`.desktop` entries** crates ship at their own root into
+`$XDG_DATA_HOME/applications` (then `update-desktop-database`), discovered by
+`desktop_entries()` and filtered per package exactly like units. Discovery is
+`-maxdepth 2` — crate root only — so keep the file next to `Cargo.toml`; units get
+`-maxdepth 3` because `cce-compositor/scripts/` holds one, which is what the shared
+`file_crate_dir()` helper unwraps. These entries were unversioned hand-written files
+in `~/.local/share/applications` until 2026-08-16; see `./WORKSPACE.md` for the
+`Exec=`/`MimeType=` rules that go with them.
+
 `ccebuild restart` deliberately cannot reach the compositor: `cce-fx` is not a user
 unit (startcce launches it), and restarting it would tear down the session.
 
diff --git a/WORKSPACE.md b/WORKSPACE.md
index 6bc294c..04807f7 100644
--- a/WORKSPACE.md
+++ b/WORKSPACE.md
@@ -91,6 +91,29 @@ three `cce-keyring-unlock*` helpers). Each crate's `make install` is now a thin
 wrapper around `ccebuild install --no-build <pkg>`; `make build/run/clean` are
 unchanged. **Never add a binary name to a Makefile** — cargo already knows it.
 
+### Desktop entries
+
+A crate that should appear in the launcher — or be selectable as an XDG default —
+ships **`<crate>/<name>.desktop` at its own root**, next to `Cargo.toml` and beside
+any `*.service` it ships. `ccebuild install` copies those into
+`$XDG_DATA_HOME/applications` and runs `update-desktop-database`, filtered by
+package the same way units are.
+
+Two rules, both learned the hard way when these files lived only in
+`~/.local/share/applications` and were hand-edited there:
+
+- **`Exec=` is a bare binary name**, never an absolute path. `~/.local/bin` is the
+  first entry on the session PATH, and the launcher (`cce-cloud`) spawns through
+  `sh -c`, so the name resolves. Nine of the ten imported entries had baked in
+  `/home/lsgalante/.local/bin/…`.
+- **An app is only reachable as a default handler if it declares `MimeType=`.** The
+  settings app's Default Apps page builds each dropdown by scanning installed
+  entries for the ones claiming that category's MIME types, so an app with no
+  `MimeType=` line simply never appears as a candidate — which is why `cce-files`
+  could not be chosen as the file manager despite having an entry. Declaring a type
+  also means honoring it: the app has to accept the path or URL argv the field code
+  (`%f`/`%u`) passes it.
+
 `ccebuild status` is the tool for "is what's running actually the code I built?".
 Because `install` unlinks before writing, a process still on the old inode reports its
 exe as `(deleted)`, which is how both `status` and `restart` detect drift. It also
diff --git a/scripts/ccebuild b/scripts/ccebuild
index 072d127..a2bd595 100755
--- a/scripts/ccebuild
+++ b/scripts/ccebuild
@@ -29,6 +29,7 @@ USER_UNIT_TARGETS=(default.target graphical-session.target cce-session.target)
 PREFIX="${CCE_PREFIX:-$HOME/.local}"
 BINDIR="$PREFIX/bin"
 UNITDIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
+DESKTOPDIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
 
 die() { printf 'ccebuild: %s\n' "$*" >&2; exit 1; }
 
@@ -94,6 +95,31 @@ user_units() {
           done
 }
 
+# XDG .desktop entries shipped by crates, at the crate root next to Cargo.toml.
+#
+# These used to be hand-written straight into ~/.local/share/applications, which
+# put them outside version control entirely: the workspace root is not a git
+# repo, so nothing there survives a fresh clone. They also went stale silently —
+# every one hardcoded Exec=/home/lsgalante/.local/bin/<bin>, and one pointed its
+# Icon at a path that no longer existed. A crate that ships its own entry is the
+# same rule as the units above: the file lives in the repo that owns it.
+#
+# An app is only reachable as an XDG default (mailto:, inode/directory, …) if it
+# declares a MimeType here, so this is also what makes cce apps selectable on the
+# settings app's Default Apps page.
+desktop_entries() {
+    find "$WS" -maxdepth 2 -name '*.desktop' -not -path "$WS/target/*" 2>/dev/null
+}
+
+# The crate directory a shipped data file belongs to. Units may sit one level
+# deeper (cce-compositor/scripts/gpu-watcher.service), so that case unwraps.
+file_crate_dir() {
+    local dir
+    dir=$(basename "$(dirname "$1")")
+    [ "$dir" = scripts ] && dir=$(basename "$(dirname "$(dirname "$1")")")
+    printf '%s\n' "$dir"
+}
+
 cmd_build() {
     printf '==> building workspace (release)\n'
     # One invocation for the whole workspace. Building per-crate with -p would
@@ -120,7 +146,7 @@ cmd_install() {
         fi
     fi
 
-    mkdir -p "$BINDIR" "$UNITDIR"
+    mkdir -p "$BINDIR" "$UNITDIR" "$DESKTOPDIR"
     local n=0 missing=()
 
     printf '==> installing binaries -> %s\n' "$BINDIR"
@@ -158,10 +184,7 @@ cmd_install() {
         [ -n "$u" ] || continue
         # Filtered install: only units belonging to a selected package's crate.
         if [ ${#pkgs[@]} -gt 0 ]; then
-            local dir
-            dir=$(basename "$(dirname "$u")")
-            [ "$dir" = scripts ] && dir=$(basename "$(dirname "$(dirname "$u")")")
-            crate_selected "$dir" "${pkgs[@]}" || continue
+            crate_selected "$(file_crate_dir "$u")" "${pkgs[@]}" || continue
         fi
         [ "$units" -eq 0 ] && printf '==> installing user units -> %s\n' "$UNITDIR"
         install -m 644 "$u" "$UNITDIR/$(basename "$u")"
@@ -169,6 +192,21 @@ cmd_install() {
     done < <(user_units)
     [ "$units" -gt 0 ] && { systemctl --user daemon-reload 2>/dev/null || true; }
 
+    local d desktops=0
+    while read -r d; do
+        [ -n "$d" ] || continue
+        # Same filtered install as the units above.
+        if [ ${#pkgs[@]} -gt 0 ]; then
+            crate_selected "$(file_crate_dir "$d")" "${pkgs[@]}" || continue
+        fi
+        [ "$desktops" -eq 0 ] && printf '==> installing desktop entries -> %s\n' "$DESKTOPDIR"
+        install -m 644 "$d" "$DESKTOPDIR/$(basename "$d")"
+        desktops=$((desktops + 1))
+    done < <(desktop_entries)
+    # Refreshes the MIME cache that makes a newly-declared MimeType resolvable.
+    # Absent on a minimal install and non-fatal there, so failure is ignored.
+    [ "$desktops" -gt 0 ] && { update-desktop-database "$DESKTOPDIR" 2>/dev/null || true; }
+
     if [ ${#missing[@]} -gt 0 ]; then
         printf 'ccebuild: NOT BUILT, skipped: %s\n' "${missing[*]}" >&2
     fi