Repository navigation
Duplicate toasts on every extra monitor: the follower's watcher restarts itself ungated - #85
Merged
nixfred merged 1 commit intoSep 15, 2026
Conversation
…rts itself Omarchy builds one bar — and one of these widgets — per screen, and the leader election exists to keep only the first screen's widget polling, watching and toasting. Two defects let the followers do it anyway, so a docked machine fired one desktop notification per monitor. QsWindow.window is still null when a freshly built bar completes its widgets, so on a hotplug every screen's widget satisfied `!ownScreen` and crowned itself. A moment later the window resolved and the gate correctly demoted them — but `running: root.leader` had already started their `imsg watch`, and the demotion killed it. That exit is counted as a failure, so one backoff later watchRestart ran `watchProc.running = true`, which REPLACES the binding permanently. From then on the follower watched, refreshed and toasted forever, ungated, until the shell restarted. Restore the binding on restart instead of assigning a bare true, and let an unresolved widget wait rather than race when more than one screen exists. One screen (or none) still defaults to leader — that is what keeps a widget outside any window working. Reproduced on a three-monitor dock: three `imsg watch` ssh sessions under one shell, and two rejected `nixfred.blip` IpcHandler registrations (the handler is `enabled: root.leader`) proving all three widgets believed they led. The two extra watchers started 16 s after the bar rebuild — exactly watchRestart's 8000 * 2^1 first backoff. A sandboxed Quickshell running the same expression logs `qswindow=NULL ownScreen=NULL LEADER=true` on all three panels at Component.onCompleted, then the correct true/false/false once the windows resolve. Both tests fail when their own fix is reverted, and only that one.
Owner
|
Merged, thanks @ianswope! duplicate toasts on extra monitors are gone. Tracing it to the watcher restart overwriting the leader-only rule, and matching the 16 s backoff to the process start times, was excellent diagnosis. It's in main as of 186b536 and deployed on my machine. I merged it together with nine other PRs, and all 526 tests pass on the combined tree. |
nixfred
added a commit
that referenced
this pull request
Sep 16, 2026
WHAT: CHANGE.log is a running, newest-first log of every change since the last X post, each entry naming the person with real name, GitHub login and their X account as a https://x.com/<handle> link. X-POSTS.md is the queue of posts for @nixfred: Post 1 (CONTRIBUTORS-X-POST.md, 2026-09-11) marked posted, Post 2 drafted covering the 19 merged PRs since (#54 #71-#77 #80-#82 #84 #85 #87 #88 #92-#95) plus 4bad82d and 481e555. WHY: Fred's standing rule 2026-09-16. Handles were missed before; a thank-you without the @ never reaches the person, and a wrong @ tags a stranger. HANDLES: verified @jondkinney (GitHub profile), @zachwilke_1 (pinefall.dev), @bbishdotdev (bbish.dev + name), @mwhuss (name + Omarchy posts replying to @nixfred). @jefehoser likely (same login, Omarchy posts), flagged for Fred. Ian Swope and cjoh: none verified, recorded as checked. .gitignore: *.log swallowed CHANGE.log, so it is re-included explicitly. LR-T: blip, changelog, x-posts, contributors, handles LR-D: blip, docs, community LR-K: change-log-verified-x-handles, x-post-queue Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ainpzHJ5F7nsV4hB6HpZy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Docking a laptop to two external monitors makes every allowlisted message
fire three desktop toasts — one per screen. It stays that way until the
shell is restarted; undocking clears it.
The leader election (war room #0) is doing its job. Two things defeat it.
1. Everyone is the leader for the first few hundred milliseconds
QsWindow.windowis still null when a freshly built bar completes itswidgets, so on a hotplug every screen's widget satisfies
!ownScreenatComponent.onCompletedand crowns itself. A sandboxed Quickshell runningthe current expression verbatim, on a three-monitor setup:
By the time it settles,
running: root.leaderhas already started animsg watchon all three.2. The demotion kills the follower's watcher, and its own backoff brings it back ungated
Settling flips
leaderfalse, the binding setsrunning = false, the sshsession exits.
onExitedreads that as a failure —watchFailsgoes to 1,watchRestartarms — and one backoff later:A bare assignment replaces the
running: root.leaderbinding permanently.From that moment the follower watches, refreshes and toasts forever, with no
gate left to stop it.
Evidence on real hardware
Three monitors, one shell process, after docking:
16 s is exactly
8000 * Math.pow(2, 1)— watchRestart's first backoff. Theshell log shows two rejected
nixfred.blipIpcHandler registrations at13:59:25; that handler is
enabled: root.leader, so all three widgetsgenuinely believed they led:
The 6 s poll
Timer { running: root.leader }has no imperative write andstayed correctly single — which is what pointed at the binding clobber.
The fix
watchRestartrestores the binding (Qt.binding(...)) instead of assigninga bare
true.exists. One screen or none still defaults to leader — that is what keeps a
widget outside any window working, so the single-monitor path is unchanged.
watchLiveness'srunning = falsealso drops the binding, but the restartpath re-establishes it immediately, so it self-heals. Left alone.
Verification
bun testpass.— checked both ways, individually.
not inferred from reading.
second.
installed here and the shell has been restarted, but the machine is
currently undocked, so the three-monitor path has not been re-exercised
end to end. Worth a second pair of eyes on a multi-monitor setup.