feat(devshell): stream quickshell warnings and errors in nix develop

qs-log follows the working-tree instance's log filtered to WARN|ERROR (-a for
everything), and the shellHook starts it in the background once qs-dev is up,
taking it down again in the exit trap alongside qs-prod. A binding loop or a
failed binding is a WARN, and easy to miss when it scrolls past unwatched.

It starts at the end of the log rather than replaying the backlog, and
re-attaches in a loop: `qs log -f` ends when the instance it attached to exits,
and the dev shell outlives individual instances.

Also corrects the hot-reload note added in 7268221, which was wrong on both
counts. Tested against the running shell: `touch` never reloads (mtime is not a
content change) and inode-replacing edits like `sed -i` are picked up fine. The
reliable check is whether `qs log` shows a "Reloading configuration..." line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
This commit is contained in:
2026-08-29 03:25:26 +02:00
co-authored by Claude Opus 5
parent 7eb7b948e8
commit 15698d3a78
2 changed files with 37 additions and 3 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ qs -p . # run this directory explicitly regardless of symlink
qs -n # exit immediately if another instance is already running (use to avoid duplicate shells while iterating) qs -n # exit immediately if another instance is already running (use to avoid duplicate shells while iterating)
``` ```
Quickshell hot-reloads QML on file save when already running, so for most edits just save and check the running instance rather than restarting `qs`. The watcher follows the file's inode, so an edit that REPLACES the file (`perl -i`, `sed -i`, `mv`) silently detaches it — the shell keeps rendering the previous config and `qs log` still says "Configuration Loaded" for the last real reload. Edit in place, or `touch` a still-watched file to force a full reload. There is no separate build/lint/test tooling in this repo — verification is visual/behavioral via the running shell. `qmlls` (QML language server) is configured via `.qmlls.ini` for editor diagnostics. Quickshell hot-reloads QML on file save when already running, so for most edits just save and check the running instance rather than restarting `qs`. It watches file CONTENT: `touch` alone never reloads (mtime is not a change), while any real edit does, including inode-replacing ones (`sed -i`, `perl -i`). A save that is not picked up leaves the shell rendering the previous config with no error — `qs log` shows a `Reloading configuration...` line for every save it saw, so that is the check. There is no separate build/lint/test tooling in this repo — verification is visual/behavioral via the running shell. `qmlls` (QML language server) is configured via `.qmlls.ini` for editor diagnostics.
## Architecture ## Architecture
+36 -2
View File
@@ -583,6 +583,7 @@
# instance is the identical build to the packaged one. # instance is the identical build to the packaged one.
qs = "${nixpkgs.legacyPackages.${system}.quickshell}/bin/qs"; qs = "${nixpkgs.legacyPackages.${system}.quickshell}/bin/qs";
git = "${nixpkgs.legacyPackages.${system}.git}/bin/git"; git = "${nixpkgs.legacyPackages.${system}.git}/bin/git";
grep = "${nixpkgs.legacyPackages.${system}.gnugrep}/bin/grep";
# Resolved at RUN time, not build time: the entire point is to run the # Resolved at RUN time, not build time: the entire point is to run the
# working tree, and `self` here is only a store snapshot of it. # working tree, and `self` here is only a store snapshot of it.
@@ -631,6 +632,33 @@
echo "qs-dev: live on $cfg edits there now hot-reload" echo "qs-dev: live on $cfg edits there now hot-reload"
''; '';
# qs log -f prints everything the instance logs; WARN and ERROR are the
# two that mean something is wrong with the QML in front of you. A
# binding loop or a failed binding is a WARN and easy to miss when it
# scrolls past inside a reload's worth of chatter.
qs-log = pkgs.writeShellScriptBin "qs-log" ''
set -uo pipefail
${preamble}
filter='WARN|ERROR'
case "''${1:-}" in
-a|--all) filter='.' ;;
esac
# -t 1: `qs log -f` replays the whole backlog first, which would dump
# every historical warning into the terminal on shell entry.
#
# `qs log -f` ends when the instance it attached to exits, and the dev
# shell outlives individual instances a QML error kills one, `qs-dev`
# starts another. Re-attach instead of going quiet for the session.
while :; do
if running "$cfg"; then
${qs} log -p "$cfg" -t 1 -f 2>/dev/null | ${grep} --line-buffered -E "$filter" >&2
fi
sleep 1
done
'';
qs-prod = pkgs.writeShellScriptBin "qs-prod" '' qs-prod = pkgs.writeShellScriptBin "qs-prod" ''
set -uo pipefail set -uo pipefail
${preamble} ${preamble}
@@ -641,7 +669,7 @@
''; '';
in in
pkgs.mkShell { pkgs.mkShell {
packages = [ pkgs.quickshell qs-dev qs-prod ]; packages = [ pkgs.quickshell qs-dev qs-prod qs-log ];
# Swap on entry, swap back on exit. Three guards: # Swap on entry, swap back on exit. Three guards:
# - interactive only ($- has i). `nix develop --command X` EXECs X, # - interactive only ($- has i). `nix develop --command X` EXECs X,
@@ -655,9 +683,15 @@
shellHook = '' shellHook = ''
if [[ $- == *i* ]] && [ -n "''${WAYLAND_DISPLAY:-}" ] && [ -z "''${HOMELAB_QS_DEV:-}" ]; then if [[ $- == *i* ]] && [ -n "''${WAYLAND_DISPLAY:-}" ] && [ -z "''${HOMELAB_QS_DEV:-}" ]; then
export HOMELAB_QS_DEV=1 export HOMELAB_QS_DEV=1
qs-dev && trap qs-prod EXIT if qs-dev; then
# Stream the dev instance's warnings and errors into this
# terminal, and take the follower down with the shell.
qs-log & HOMELAB_QS_LOG=$!
trap 'kill "$HOMELAB_QS_LOG" 2>/dev/null; qs-prod' EXIT
fi
fi fi
echo "homelab devshell qs-dev (working tree) / qs-prod (packaged); exit restores" echo "homelab devshell qs-dev (working tree) / qs-prod (packaged); exit restores"
echo "homelab devshell quickshell WARN/ERROR stream here; qs-log -a for everything"
''; '';
}; };
}; };