diff --git a/README.md b/README.md index d7ed9d9..00001db 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/dist/dev.greyson.vtabs.desktop.in b/dist/dev.greyson.vtabs.desktop.in new file mode 100644 index 0000000..5b43c4a --- /dev/null +++ b/dist/dev.greyson.vtabs.desktop.in @@ -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 diff --git a/dist/dev.greyson.vtabs.svg b/dist/dev.greyson.vtabs.svg new file mode 100644 index 0000000..0c3e30b --- /dev/null +++ b/dist/dev.greyson.vtabs.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flake.nix b/flake.nix index fc8b531..9f40d1d 100644 --- a/flake.nix +++ b/flake.nix @@ -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" ''; }; }); diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..79f8b04 --- /dev/null +++ b/mise.toml @@ -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" < "$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" +'''