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

commitf3c35055d8f2e7fb6046441e9846c5d0fdc38ba3
parent10090ee19e
authorLucas Galante <[email protected]>
date2026-08-16 11:27
ccebuild: install app icons from any crate's hicolor/ tree

app_icons() mirrors a crate's hicolor/ tree verbatim into
$XDG_DATA_HOME/icons/hicolor/ and refreshes the GTK icon cache, so an
icon's size/context is its directory, not a rule in this script. Two
things are load-bearing and commented: -type l in the find (every file
in cce-icons/hicolor is a symlink into its svg/, and -type f alone
matches none of them — a clean install of nothing), and the deliberate
absence of package filtering (cce-icons has no Cargo.toml, so
crate_selected() could never select it).

Also documents the desktop entries' new Icon= convention (Icon=<binary
name>, backed by the theme) in WORKSPACE.md — before this, entries
borrowed generic freedesktop names that resolved only if another theme
happened to provide them, and cce-preview 'worked' only via five PNGs
hand-copied into ~/.local/share/icons, unversioned and gone on a fresh
clone.

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

 CLAUDE.md        | 10 ++++++++++
 WORKSPACE.md     | 19 ++++++++++++++++++
 scripts/ccebuild | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 89 insertions(+)

diff --git a/CLAUDE.md b/CLAUDE.md
index 53c2c77..e0b1820 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -71,6 +71,16 @@ It also installs the **`.desktop` entries** crates ship at their own root into
 in `~/.local/share/applications` until 2026-08-16; see `./WORKSPACE.md` for the
 `Exec=`/`MimeType=` rules that go with them.
 
+**App icons** install from any crate's `hicolor/` tree (`app_icons()`), mirrored
+verbatim into `$XDG_DATA_HOME/icons/hicolor/` — so an icon's size and context are
+its directory, not a rule in the script, and `48x48/apps` would need no edit here.
+In practice the tree is `cce-icons/hicolor/`, whose files are all **symlinks** into
+its own `svg/`; that makes `-type l` load-bearing in the `find`, because `-type f`
+alone matches none of them and would report a clean install of nothing. The install
+is deliberately *not* package-filtered: `cce-icons` has no `Cargo.toml`, so
+`crate_selected()` can never match it. See `cce-icons/hicolor/README.md` for why the
+target is `hicolor` rather than the `cce` theme, and why it ships no `index.theme`.
+
 **Helper scripts** are installed from **any** crate's `scripts/` dir, not just
 this one's (`crate_scripts()`, same per-package filtering). A script belongs in
 the repo whose code it is about — `cce-keyring-selftest` reports on the keyring
diff --git a/WORKSPACE.md b/WORKSPACE.md
index 04807f7..dfd8309 100644
--- a/WORKSPACE.md
+++ b/WORKSPACE.md
@@ -114,6 +114,25 @@ Two rules, both learned the hard way when these files lived only in
   also means honoring it: the app has to accept the path or URL argv the field code
   (`%f`/`%u`) passes it.
 
+### App icons
+
+An entry's `Icon=` should be the app's own name (`Icon=cce-files`), backed by
+`cce-icons/hicolor/scalable/apps/cce-files.svg`. `ccebuild install` mirrors any
+crate's `hicolor/` tree into `$XDG_DATA_HOME/icons/hicolor/` and refreshes the GTK
+icon cache; `cce-icons/hicolor/README.md` documents the naming and the symlink
+convention that keeps `svg/` the sole source of the artwork.
+
+Before 2026-08-16 the entries borrowed generic freedesktop names
+(`preferences-system`, `system-file-manager`), which resolved only if some other
+installed theme happened to provide them, and `cce-preview` "worked" only because
+five PNGs had been hand-copied into `~/.local/share/icons` — unversioned, and gone
+on a fresh clone. The same failure as the `.desktop` files themselves.
+
+Note that **nothing displayed an `Icon=` key at all** until the launcher was taught
+to: `cce-cloud`'s `AppInfo` had no icon field. `cce_ui::icon` is the shared
+resolver (theme name or absolute path → file); it is distinct from
+`cce_ui::upload_icon`, which loads a *bundled* cce-icons glyph for in-widget use.
+
 `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 1ac156a..bec344b 100755
--- a/scripts/ccebuild
+++ b/scripts/ccebuild
@@ -30,6 +30,7 @@ PREFIX="${CCE_PREFIX:-$HOME/.local}"
 BINDIR="$PREFIX/bin"
 UNITDIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
 DESKTOPDIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
+ICONDIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons"
 DBUSDIR="${XDG_DATA_HOME:-$HOME/.local/share}/dbus-1/services"
 
 die() { printf 'ccebuild: %s\n' "$*" >&2; exit 1; }
@@ -134,6 +135,38 @@ desktop_entries() {
     find "$WS" -maxdepth 2 -name '*.desktop' -not -path "$WS/target/*" 2>/dev/null
 }
 
+# App icons, as a whole XDG icon theme tree a crate ships under hicolor/ —
+# cce-icons/hicolor/scalable/apps/cce-files.svg installs to
+# $XDG_DATA_HOME/icons/hicolor/scalable/apps/cce-files.svg. The path IS the
+# install path, so an icon's size and context are its directory rather than a
+# rule in this script: adding 48x48/apps or scalable/mimetypes later needs no
+# edit here. The icon's basename must equal the Icon= key in the crate's
+# .desktop entry, which is what makes them resolve at all.
+#
+# Two things are load-bearing:
+#
+# - **`-type l` as well as `-type f`.** Every file in cce-icons/hicolor is a
+#   symlink into ../../../svg (svg/ is the sole source; an entry there is a name
+#   for a glyph, not a second copy of it). `-type f` alone does not match a
+#   symlink, so the plain `-type f` used by crate_scripts/dbus_services would
+#   find nothing at all here and report success. `install` dereferences, so the
+#   destination is a real file either way.
+# - **hicolor, and no index.theme.** hicolor is the spec's fallback theme,
+#   searched whatever the user's icon theme is; the sibling cce cursor theme
+#   installs to icons/cce and nothing anywhere selects it. The system
+#   hicolor-icon-theme package owns the index.theme listing every size/context
+#   dir, and a theme is the union of its trees across base dirs — shipping a
+#   second index here that named only scalable/apps would be read first and hide
+#   every other hicolor directory.
+#
+# The extension filter is the icon formats the icon-theme spec defines, not a
+# per-crate list: it is what keeps the tree's own README.md out of the install.
+app_icons() {
+    find "$WS" -mindepth 3 -path '*/hicolor/*' \( -type f -o -type l \) \
+        \( -name '*.svg' -o -name '*.png' -o -name '*.xpm' \) \
+        -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() {
@@ -245,6 +278,33 @@ cmd_install() {
     # Absent on a minimal install and non-fatal there, so failure is ignored.
     [ "$desktops" -gt 0 ] && { update-desktop-database "$DESKTOPDIR" 2>/dev/null || true; }
 
+    # Deliberately NOT filtered by package. The icon theme is one shared tree
+    # owned by cce-icons, which has no Cargo.toml — it is an asset repo, not a
+    # crate — so crate_selected() can never match it and a filtered install
+    # would silently install no icons at all. file_crate_dir() is no help
+    # either: the dir above an icon is `apps`, not the crate. Over-installing a
+    # dozen small files on `ccebuild install cce-preview` is the cheaper wrong
+    # answer than an app whose entry points at an icon that was never shipped.
+    local i icons=0 rel
+    while read -r i; do
+        [ -n "$i" ] || continue
+        # Everything after the hicolor/ component, so the source tree's layout
+        # is reproduced verbatim under $ICONDIR.
+        rel="hicolor/${i#*/hicolor/}"
+        [ "$icons" -eq 0 ] && printf '==> installing app icons -> %s\n' "$ICONDIR"
+        install -D -m 644 "$i" "$ICONDIR/$rel"
+        icons=$((icons + 1))
+        n=$((n + 1))
+    done < <(app_icons)
+    # GTK reads the mmapped cache in preference to the directory when it is
+    # newer than the directory, so a new icon can be invisible until it is
+    # rebuilt. --ignore-theme-index because this tree deliberately ships no
+    # index.theme (see app_icons above); absent on a minimal install, so
+    # failure is ignored.
+    [ "$icons" -gt 0 ] && {
+        gtk-update-icon-cache --ignore-theme-index -q -f "$ICONDIR/hicolor" 2>/dev/null || true
+    }
+
     if [ ${#missing[@]} -gt 0 ]; then
         printf 'ccebuild: NOT BUILT, skipped: %s\n' "${missing[*]}" >&2
     fi