Add mise tasks, desktop entry, and app icon
mise run install builds a release binary and installs it into ~/.local with a desktop entry and a hicolor icon. Two wrinkles specific to a Nix-built GTK app installed outside the dev shell: the launcher is a generated script rather than a symlink, because a desktop launcher provides a minimal environment and GTK still needs to be pointed at its GSettings schemas and icon themes; and install registers Nix GC roots for every store path in the binary's RPATH, so nix-collect-garbage cannot delete GTK out from under the installed app. The runtime paths are exported from the flake's shellHook and read back at install time, so they cannot drift from what the binary was built against.
This commit is contained in:
@@ -16,6 +16,31 @@ zig build run
|
||||
|
||||
Everything is pinned by `flake.nix`; nothing needs to be installed on the host.
|
||||
|
||||
## Installing
|
||||
|
||||
```sh
|
||||
mise run install # builds release, installs into ~/.local
|
||||
mise run uninstall
|
||||
```
|
||||
|
||||
That puts the binary in `~/.local/libexec/vtabs`, a launcher on `~/.local/bin`,
|
||||
a desktop entry in `~/.local/share/applications`, and the icon in the hicolor
|
||||
theme, then refreshes the desktop and icon caches. `mise tasks` lists the rest
|
||||
(`build`, `run`, `fmt`, `screenshot`).
|
||||
|
||||
Two things the installer has to handle that a normal `install` wouldn't:
|
||||
|
||||
- **A launcher, not a symlink.** The binary links against GTK in the Nix store
|
||||
and has an RPATH, so it runs anywhere — but it still needs to be pointed at
|
||||
GTK's GSettings schemas and icon themes, or it aborts on a missing schema.
|
||||
A desktop launcher starts the app with a minimal environment, so those paths
|
||||
are baked into the launcher script. They are read out of the dev shell at
|
||||
install time rather than hardcoded, so they can't drift from the flake.
|
||||
- **Nix GC roots.** The installed app depends on store paths that
|
||||
`nix-collect-garbage` would otherwise be free to delete, which would break it
|
||||
later with no obvious connection to the command that did it. Install pins
|
||||
every store path in the binary's RPATH; uninstall releases them.
|
||||
|
||||
## Which "libghostty" this uses, and why
|
||||
|
||||
Ghostty ships two different C APIs, and only one of them is usable here:
|
||||
@@ -116,8 +141,8 @@ This is a proof of concept, and the following are deliberately absent:
|
||||
`./b.sh` wraps `nix develop --command zig build` and strips the enormous
|
||||
command line Zig prints on failure.
|
||||
|
||||
`./shot.sh out.png "text to type"` runs the app inside a throwaway **headless
|
||||
Sway** and screenshots it. This keeps UI checks entirely out of your real
|
||||
`mise run screenshot out.png "text to type"` (or `./shot.sh` directly) runs the
|
||||
app inside a throwaway **headless Sway** and screenshots it. This keeps UI checks entirely out of your real
|
||||
Wayland session — nothing appears on screen, and it works while the session is
|
||||
locked.
|
||||
|
||||
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=vtabs
|
||||
GenericName=Terminal
|
||||
Comment=Terminal emulator with vertical tabs
|
||||
# Substituted with an absolute path by `mise run install`, because desktop
|
||||
# launchers do not reliably have ~/.local/bin on PATH.
|
||||
Exec=@EXEC@
|
||||
Icon=dev.greyson.vtabs
|
||||
Terminal=false
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=shell;prompt;command;commandline;cmd;terminal;tabs;
|
||||
StartupNotify=true
|
||||
# Lets the compositor match the running window to this entry, which is what
|
||||
# gives the window its icon. Must equal the GApplication id.
|
||||
StartupWMClass=dev.greyson.vtabs
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
vtabs app icon: a terminal pane with the vertical tab strip down its left
|
||||
edge. Shapes are kept large and high-contrast so the sidebar is still
|
||||
readable when the icon is scaled down to 32px in a launcher.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||
<defs>
|
||||
<linearGradient id="pane" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#241f31"/>
|
||||
<stop offset="1" stop-color="#14111b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#c7b3ff"/>
|
||||
<stop offset="1" stop-color="#9b7ff0"/>
|
||||
</linearGradient>
|
||||
<!-- Everything is clipped to the rounded body so the sidebar picks up
|
||||
the same corner radius without repeating the path. -->
|
||||
<clipPath id="body">
|
||||
<rect x="8" y="12" width="112" height="104" rx="24"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect x="8" y="12" width="112" height="104" rx="24" fill="url(#pane)"/>
|
||||
|
||||
<g clip-path="url(#body)">
|
||||
<rect x="8" y="12" width="42" height="104" fill="#2f2743"/>
|
||||
<rect x="49" y="12" width="2" height="104" fill="#3d3357"/>
|
||||
|
||||
<!-- Tab rows: the active one carries the accent, the rest are muted. -->
|
||||
<rect x="16" y="30" width="26" height="12" rx="6" fill="url(#accent)"/>
|
||||
<rect x="16" y="50" width="26" height="12" rx="6" fill="#5d5280"/>
|
||||
<rect x="16" y="70" width="26" height="12" rx="6" fill="#493f66"/>
|
||||
</g>
|
||||
|
||||
<!-- Prompt chevron and cursor, the universal shorthand for a terminal. -->
|
||||
<path d="M66 51 L79 64 L66 77"
|
||||
fill="none" stroke="#e8e2f7" stroke-width="9"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="86" y="70" width="22" height="8" rx="4" fill="url(#accent)"/>
|
||||
|
||||
<rect x="8" y="12" width="112" height="104" rx="24"
|
||||
fill="none" stroke="#000000" stroke-opacity="0.28" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -84,10 +84,20 @@
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
# GTK needs to find icon themes and schemas at runtime when the app
|
||||
# is launched straight out of the dev shell rather than installed.
|
||||
export XDG_DATA_DIRS="${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}:${pkgs.adwaita-icon-theme}/share:${pkgs.hicolor-icon-theme}/share:$XDG_DATA_DIRS"
|
||||
export GSETTINGS_SCHEMA_DIR="${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}/glib-2.0/schemas"
|
||||
# The runtime data GTK needs to find at startup: its own GSettings
|
||||
# schemas and the icon themes our widgets pull symbolic icons from.
|
||||
#
|
||||
# These are exported under their own names, rather than only folded
|
||||
# into XDG_DATA_DIRS, because `mise run install` reads them back out
|
||||
# to bake into the installed launcher. The built binary already has
|
||||
# an RPATH into the Nix store and runs fine outside this shell, but
|
||||
# it still has to be pointed at this data or GTK aborts on a missing
|
||||
# schema and falls back to broken-image icons.
|
||||
export VTABS_GSETTINGS_SCHEMA_DIR="${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}/glib-2.0/schemas"
|
||||
export VTABS_RUNTIME_DATA_DIRS="${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}:${pkgs.adwaita-icon-theme}/share:${pkgs.hicolor-icon-theme}/share"
|
||||
|
||||
export GSETTINGS_SCHEMA_DIR="$VTABS_GSETTINGS_SCHEMA_DIR"
|
||||
export XDG_DATA_DIRS="$VTABS_RUNTIME_DATA_DIRS:$XDG_DATA_DIRS"
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Nix supplies the whole toolchain (Zig, GTK, libadwaita), so there is
|
||||
# nothing for mise to install; these tasks just drive it.
|
||||
|
||||
[tasks.build]
|
||||
description = "Build a release binary into zig-out/bin"
|
||||
run = "nix develop --command zig build -Doptimize=ReleaseFast"
|
||||
|
||||
[tasks.build-debug]
|
||||
description = "Build a debug binary (leak checking, safety checks)"
|
||||
run = "nix develop --command zig build"
|
||||
|
||||
[tasks.run]
|
||||
description = "Build and run vtabs"
|
||||
run = "nix develop --command zig build run"
|
||||
|
||||
[tasks.fmt]
|
||||
description = "Format all Zig source"
|
||||
run = "nix develop --command zig fmt build.zig src"
|
||||
|
||||
[tasks.screenshot]
|
||||
description = "Screenshot vtabs in a throwaway headless compositor"
|
||||
depends = ["build-debug"]
|
||||
run = "nix develop --command ./shot.sh"
|
||||
|
||||
[tasks.install]
|
||||
description = "Install vtabs into ~/.local with a desktop entry and icon"
|
||||
depends = ["build"]
|
||||
run = '''
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
data="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
bindir="$HOME/.local/bin"
|
||||
libexec="$HOME/.local/libexec/vtabs"
|
||||
apps="$data/applications"
|
||||
icons="$data/icons/hicolor/scalable/apps"
|
||||
gcroots="$data/vtabs/gcroots"
|
||||
|
||||
# The binary carries an RPATH into the Nix store, so it runs anywhere, but it
|
||||
# still has to be told where GTK's GSettings schemas and icon themes live or
|
||||
# it aborts on a missing schema. Read those paths back out of the dev shell
|
||||
# that built it rather than hardcoding store hashes.
|
||||
schemas="$(nix develop --command printenv VTABS_GSETTINGS_SCHEMA_DIR)"
|
||||
datadirs="$(nix develop --command printenv VTABS_RUNTIME_DATA_DIRS)"
|
||||
|
||||
mkdir -p "$bindir" "$libexec" "$apps" "$icons" "$gcroots"
|
||||
|
||||
install -m755 zig-out/bin/vtabs "$libexec/vtabs"
|
||||
|
||||
# A launcher script rather than a symlink: a desktop launcher starts the app
|
||||
# with a minimal environment, so the GTK runtime paths must be set here.
|
||||
# Existing values are preserved so this composes with the rest of the session.
|
||||
cat > "$bindir/vtabs" <<EOF
|
||||
#!/bin/sh
|
||||
# Generated by \`mise run install\` — re-run that instead of editing.
|
||||
export GSETTINGS_SCHEMA_DIR="$schemas\${GSETTINGS_SCHEMA_DIR:+:\$GSETTINGS_SCHEMA_DIR}"
|
||||
export XDG_DATA_DIRS="$datadirs:\${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
||||
exec "$libexec/vtabs" "\$@"
|
||||
EOF
|
||||
chmod 755 "$bindir/vtabs"
|
||||
|
||||
sed "s|@EXEC@|$bindir/vtabs|" dist/dev.greyson.vtabs.desktop.in \
|
||||
> "$apps/dev.greyson.vtabs.desktop"
|
||||
install -m644 dist/dev.greyson.vtabs.svg "$icons/dev.greyson.vtabs.svg"
|
||||
|
||||
# Pin the store paths the installed binary links against. Without this a
|
||||
# later `nix-collect-garbage` deletes GTK and the installed app stops
|
||||
# starting, with no obvious connection to the command that broke it.
|
||||
# Nix roots are transitive, so rooting each RPATH entry covers its closure.
|
||||
rm -f "$gcroots"/*
|
||||
if command -v readelf >/dev/null; then
|
||||
readelf -d "$libexec/vtabs" \
|
||||
| sed -n 's/.*R\(UN\)\?PATH.*\[\(.*\)\]/\2/p' \
|
||||
| tr ':' '\n' \
|
||||
| sed -n 's|^\(/nix/store/[^/]*\).*|\1|p' \
|
||||
| sort -u \
|
||||
| while read -r path; do
|
||||
nix-store --realise --add-root "$gcroots/$(basename "$path")" \
|
||||
--indirect "$path" >/dev/null 2>&1 || true
|
||||
done
|
||||
echo "pinned $(find "$gcroots" -maxdepth 1 -type l | wc -l) store paths against garbage collection"
|
||||
else
|
||||
echo "warning: readelf not found; store paths left unpinned (nix-collect-garbage may break the install)" >&2
|
||||
fi
|
||||
|
||||
# Let the desktop notice the new entry and icon right away.
|
||||
command -v update-desktop-database >/dev/null && update-desktop-database -q "$apps" || true
|
||||
command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -qtf "$data/icons/hicolor" 2>/dev/null || true
|
||||
|
||||
echo "installed $bindir/vtabs"
|
||||
case ":$PATH:" in
|
||||
*":$bindir:"*) ;;
|
||||
*) echo "note: $bindir is not on your PATH; the desktop entry still works" >&2 ;;
|
||||
esac
|
||||
'''
|
||||
|
||||
[tasks.uninstall]
|
||||
description = "Remove everything `install` put into ~/.local"
|
||||
run = '''
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
data="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
apps="$data/applications"
|
||||
|
||||
rm -f "$HOME/.local/bin/vtabs"
|
||||
rm -rf "$HOME/.local/libexec/vtabs"
|
||||
rm -f "$apps/dev.greyson.vtabs.desktop"
|
||||
rm -f "$data/icons/hicolor/scalable/apps/dev.greyson.vtabs.svg"
|
||||
# Dropping the roots lets Nix reclaim the runtime closure on its next GC.
|
||||
rm -rf "$data/vtabs"
|
||||
|
||||
command -v update-desktop-database >/dev/null && update-desktop-database -q "$apps" || true
|
||||
command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -qtf "$data/icons/hicolor" 2>/dev/null || true
|
||||
|
||||
echo "uninstalled vtabs"
|
||||
'''
|
||||
Reference in New Issue
Block a user