aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-03 09:53:10 +0200
committerDanilo M. <danix@danix.xyz>2026-10-03 09:53:10 +0200
commit8af37014d58918da714c741954de92881dcb2554 (patch)
tree1fb46faca6db00c1b6034303864b21da07c14e82
parentb5aa795969dcb4115dbdc7b304fdc5dad9a3de59 (diff)
downloadquickshell-8af37014d58918da714c741954de92881dcb2554.tar.gz
quickshell-8af37014d58918da714c741954de92881dcb2554.zip
fix(status): watch for create, so a mode's first write is seenHEADmaster
Qt's atomic write builds the file as an unnamed O_TMPFILE. When the target does not exist yet it links that straight into place, and the only event naming the file is IN_CREATE: no close_write, no moved_to. statusctl watch did not listen for create, so the first write of each mode in a session went unseen. On a fresh session that is the write that matters: a Teams call started an hour in left the waybar pill reading off until some later rename happened to redraw it. The test reproduces the event with a hard link, which is what the linkat from Qt's temp file looks like to inotify. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rwxr-xr-xdesktop/modules/status/statusctl11
-rwxr-xr-xdesktop/modules/status/test-statusctl.sh16
-rw-r--r--shared/Status.qml7
3 files changed, 29 insertions, 5 deletions
diff --git a/desktop/modules/status/statusctl b/desktop/modules/status/statusctl
index 839377e..87c2cac 100755
--- a/desktop/modules/status/statusctl
+++ b/desktop/modules/status/statusctl
@@ -59,7 +59,8 @@ read_mode() {
# Write through a temporary file and rename, so no reader ever sees a
# half-written value. This is also what FileView does on the QML side, and it
-# is why a watcher has to listen for moved_to as well as close_write.
+# is why a watcher has to listen for moved_to as well as close_write, and
+# create for FileView's first write (see watch below).
write_mode() {
local want="$1" tmp
tmp="$(mktemp "$DIR/.status.$mode.XXXXXX")" || exit 1
@@ -142,13 +143,19 @@ case "$action" in
# the file, so a watch held on the old inode dies with it. This is the
# same trap the mail watcher hit with Xapian.
#
+ # create is needed as well: Qt's atomic write builds the file as an
+ # unnamed O_TMPFILE, and when the target does not exist yet it links
+ # that straight into place. The only event naming the file is then
+ # IN_CREATE, so without it the first write of every session, the one
+ # that matters most, goes unseen until some later write.
+ #
# inotifywait must die with us. Piped straight into the while loop it
# would be a pipeline sibling, not a child, so a plain kill on this
# process (which is exactly how waybar stops and respawns its exec
# children on every reload) leaves it running, watching a directory
# nobody reads anymore. Process substitution makes it a real child
# whose PID we can hold and kill from a trap.
- exec 3< <(inotifywait -q -m -e close_write,moved_to,delete --format '%f' "$DIR" 2>/dev/null)
+ exec 3< <(inotifywait -q -m -e create,close_write,moved_to,delete --format '%f' "$DIR" 2>/dev/null)
watcher=$!
trap 'kill "$watcher" 2>/dev/null' EXIT TERM INT
# Presentation also follows status.gaming, so a game starting or
diff --git a/desktop/modules/status/test-statusctl.sh b/desktop/modules/status/test-statusctl.sh
index 38da27c..afe56d4 100755
--- a/desktop/modules/status/test-statusctl.sh
+++ b/desktop/modules/status/test-statusctl.sh
@@ -133,5 +133,21 @@ kill "$watcher4" 2>/dev/null
wait "$watcher4" 2>/dev/null
check "game ending redraws" "1" "$(grep -c '"class": *"deactivated"' "$out4")"
+# The first write of a session creates the file, and Qt's atomic write does
+# that by linking an unnamed temp file into place: the only event naming the
+# mode file is IN_CREATE, no close_write and no moved_to. A hard link from
+# another name reproduces exactly that event.
+rm -f "$tmp/status.gaming" "$tmp/status.presentation"
+out5="$tmp/watch5.out"
+"$ctl" presentation watch > "$out5" 2>/dev/null &
+watcher5=$!
+sleep 0.3
+printf '1\n' > "$tmp/unnamed"
+ln "$tmp/unnamed" "$tmp/status.gaming"
+sleep 0.5
+kill "$watcher5" 2>/dev/null
+wait "$watcher5" 2>/dev/null
+check "first write, created by link, redraws" "1" "$(grep -c '"class": *"gaming"' "$out5")"
+
printf '\n%d passed, %d failed\n' "$pass" "$fail"
[[ "$fail" -eq 0 ]]
diff --git a/shared/Status.qml b/shared/Status.qml
index 0548f53..128256c 100644
--- a/shared/Status.qml
+++ b/shared/Status.qml
@@ -245,9 +245,10 @@ Singleton {
}
// Both are the documented defaults in 0.3.1, set explicitly because
- // the CLI depends on them: statusctl watches close_write,moved_to
- // precisely because an atomic write lands as a rename, so a future
- // release flipping this default would break the watcher silently.
+ // the CLI depends on them: statusctl watches create,moved_to
+ // precisely because an atomic write lands as a link (first write,
+ // no file yet) or a rename, so a future release flipping this
+ // default would break the watcher silently.
atomicWrites: true
watchChanges: true
printErrors: false