Skip to content

fix: manual window moves stick; popout opens on the clicked screen and from SUPER+CTRL+<n> - #109

Merged
nixfred merged 3 commits into
nixfred:mainfrom
joshhattan:fix/window-move-and-popout-screen
Sep 20, 2026
Merged

nixfred merged 3 commits into
nixfred:mainfrom
joshhattan:fix/window-move-and-popout-screen

Conversation

@joshhattan

@joshhattan joshhattan commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

What

Three fixes for multi-workspace / multi-monitor use, one commit each:

  1. A manual move of the Blip window now sticks. sameAddress() in BlipWindow.qml compared ownToplevel.lastIpcObject.address (0x5b5a…) with the raw movewindowv2 event address (5b5a…, no prefix) as plain strings, so it never matched. The reason: "move" path from fix: keep the app on its workspace after idle #87 was therefore never taken, and ~400ms later strayReturn read the move as a stray remap and sent the window back to its old home. Both sides are now stripped of 0x before comparing.
  2. The bar popout opens on the screen whose icon was clicked. The panel only exists on the leader widget (first screen). Clicks on another screen's bar were forwarded as a bare toggle, so the popout always anchored to the leader's bar. Followers now send toggleon <screen>; the leader finds that screen's widget with bar.moduleWidgets(), re-anchors the panel to its button and bar, then opens it (the popout takes its screen from its anchor).
  3. SUPER+CTRL+<n> opens the popout on any screen, not just the first. Omarchy routes a panel hotkey to the widget on the focused screen, but only the leader owns a panel, so on any other screen open() was a silent no-op that still reported success (no summon: warning, the hotkey just did nothing). open() now goes through openOn(screen): the leader re-anchors to the asked-for screen and opens; a follower forwards to the leader over IPC like a click does. The new verb is openon, not toggleon, because a hotkey that means "open" must not close a panel that is already up. close() on a follower forwards too.

Why

  • Dragging/moving the window to another workspace (SUPER+SHIFT+N) appeared to work, then it jumped back to the old workspace. Traced on Hyprland's raw event socket: movewindowv2 …,6,6 followed ~400ms later by movewindowv2 …,1,1. The window's saved home was never updated, so it could not be moved off its first workspace.
  • On a dual-monitor setup the popout only ever opened on the first monitor, even when clicking the icon on the second monitor's bar.
  • Fix 3 is the same gap on the keyboard path: with the second monitor focused, SUPER+CTRL+1 did nothing at all. Omarchy's panel hotkeys reach Blip: expose opened on the bar widget #38 made the hotkey reach the widget (open/close/opened); this makes it work when that widget is a follower. Fix 3 reuses the anchoring from fix 2, which is why it belongs in this PR.

The DEVLOG entry for #87 verified that a stray move is returned but not that a deliberate move is saved; QML has no unit tests and ui.test.ts only asserts that the string sameAddress exists, so neither path was covered. Fix 3 adds a ui.test.ts assertion that the follower forwards, that openOn never toggles, and that close() forwards too (it fails against the previous BarWidget.qml).

How it was verified

  • bun test is green (561 pass)
  • Any send/receive behavior was exercised against my own number only — no send/receive behavior touched
  • UI change → screenshot attached (no screenshot; verified with hyprctl layers instead, below)
  • Touches an invariant in CLAUDE.md → named below; doc not changed

Live checks on Hyprland with two monitors (DP-1, DP-2):

  • Before fix 1: dispatched a move to workspace 6, event socket showed the automatic move back to 1. After: window stayed on 6 and window.json saved "workspace":"6".
  • Fix 2: ipc call nixfred.blip toggleon DP-2 puts omarchy-keyboard-panel on DP-2; toggleon DP-1 puts it on DP-1. I did not physically click the DP-2 bar icon; that path calls the same toggleon IPC with ownScreen.name.
  • Fix 3: with DP-2 focused, the shell's togglePanelAt right 1 (what SUPER+CTRL+1 runs) previously produced no omarchy-keyboard-panel layer; after the fix it does, and the popout is visible on DP-2. Physically pressing SUPER+CTRL+1 on DP-2 opens it. The qs ipc verb openon <screen> is registered after omarchy-restart-shell.
  • No new QML errors in qs log after omarchy-restart-shell.

Invariants touched: "The app window is RECREATED on show, never re-mapped" (save workspace only from a user movewindowv2). Fix 1 makes that behaviour actually happen; the documented invariant is unchanged. Fix 3 touches none.

joshhattan and others added 2 commits September 17, 2026 19:50
sameAddress() compared ownToplevel.lastIpcObject.address (0x-prefixed)
against the raw movewindowv2 IPC event's address (bare hex) with no
normalization, so it never matched. Every real user move fell through
to strayReturn 400ms later, which misread it as a stray idle/DPMS
remap and silently moved the window back to its old saved home. This
made manually moving the window to a new workspace never actually
stick, on any workspace.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The panel exists only on the leader widget (first screen), and a click on
another screen's bar was forwarded as a bare `toggle`, so the popout always
anchored to the leader's bar. Followers now send `toggleon <screen>`, and
the leader re-anchors the panel to that screen's widget button (found via
bar.moduleWidgets) before opening; the popout derives its screen from its
anchor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@joshhattan
joshhattan requested a review from nixfred as a code owner September 19, 2026 13:41
… first

Omarchy routes a panel hotkey to the bar widget on the FOCUSED screen, but
only the leader (first screen) owns a panel. On any other screen open() was
a silent no-op that still reported success, so the shell logged no
"summon:" warning and the hotkey simply did nothing.

open() now goes through openOn(screen): the leader re-anchors to the
asked-for screen (anchorPanel, shared with toggleOn) and opens; a follower
forwards to the leader over IPC the way a click on its icon already does.
The new IPC verb is openon, not toggleon: a hotkey that means "open" must
not close a panel that is already up. close() on a follower forwards too.

Touches no CLAUDE.md invariant; extends the nixfred#38 hotkey path (open/close/
opened on the widget) to multi-monitor bars. ui.test.ts pins the forwarding
and asserts openOn never toggles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@joshhattan joshhattan changed the title fix: manual window moves stick; popout opens on the clicked screen fix: manual window moves stick; popout opens on the clicked screen and from SUPER+CTRL+<n> Sep 19, 2026
@nixfred
nixfred merged commit 6935ad0 into nixfred:main Sep 20, 2026
nixfred added a commit that referenced this pull request Sep 20, 2026
…d to 27

WHAT: the merge-day entry for #109 (joshhattan) in both contributor-facing logs,
plus the W38 header corrected from 25 to 27 merged PRs (it was already one short
before #109 landed; counted the distinct PR numbers in the section).

WHY: standing rule, Fred 2026-09-18 — every merged PR gets a line THE DAY IT
MERGES, with the contributor's name and X handle, never reconstructed on Sunday.

The notes record what was actually verified here and what was not, including the
part his DP-1/DP-2 testing carries, and the open follow-up on his handle: still
nothing published, already asked on the PR.

LR-T: blip, changelog, contributors, weekly-notes
LR-D: blip
LR-R: #109

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nixfred added a commit that referenced this pull request Sep 20, 2026
WHAT: cuts 2.6.0. manifest.json, the README badge and the CHANGELOG heading move
together (ui.test.ts pins all three to each other); the Unreleased section
becomes the release section, as 2.5.0 did.

Headline changes since 2.5.0, from eight contributors across 27 merged PRs this
week alone: security codes offer to fill themselves beside the field (and now
work in Zen and Firefox, where Gecko lies about writing), an unknown sender can
be saved as a real contact on the Mac, right-click quotes a message as a reply,
reading a group in Blip clears it on the phone, Send Later shows as Scheduled
instead of sent, GIFs arrive as GIFs, a message carries several files, stamps
cross the bridge as UTC, and a never-opened unread stopped slowing every poll.

Also added here: the two CHANGELOG entries #109 shipped without. That PR carried
three fixes and documented only the third, so the window-move fix and the
clicked-screen popout had no user-facing line.

VERIFIED before the bump: bun test 622 pass, 0 fail. Deployed to
~/.config/omarchy/plugins/nixfred.blip and the shell restarted; qs log clean for
nixfred.blip; every popout verb driven live (open, close, toggle, toggleon,
openon) and the panel screenshotted rendering under its bar icon.

REAL multi-monitor check, which the merge commit said was NOT possible: a second
output appeared mid-session (BEAM-IPAD, the iPad stream, at 5120,0). It is
Quickshell.screens[0], so it holds the panel and DVI-I-1's widget is the
follower. `openon DVI-I-1` re-anchored the panel onto DVI-I-1 (layer 0 0 5120
1440) while a bare `open` left it on BEAM-IPAD (5120 0 960 540). #109's
re-anchoring is therefore confirmed on real hardware, not just through IPC.

KNOWN, PRE-EXISTING, NOT FIXED HERE: the follower-to-leader forward hardcodes
`qs -p /usr/share/omarchy/shell`. gus runs its shell from the dev link at
~/Projects/video.on.desktop.omarchy/omarchy/shell, so every forward exits 255
with "No running instances" and the summon silently reports ok. Four such calls
predate #109 and it added two more. Raised with Fred separately.

LR-T: blip, release, 2.6.0, versioning, multi-monitor
LR-D: blip
LR-K: version-lives-in-three-pinned-files, beam-ipad-is-a-real-second-output
LR-R: #109, #110

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nixfred

nixfred commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Merged, and shipped in 2.6.0. Thank you, this is a good catch.

The address fix is the one I want to call out. reason: "move" has been dead code since #87 shipped it, because the two sides were never the same string, and nothing covered it: the #87 devlog proved a stray move gets returned but never that a deliberate move gets saved, and ui.test.ts only asserted the literal sameAddress existed. Confirmed both formats here on gus before merging:

socket2:                activewindowv2>>5b160312f310
hyprctl clients -j:     "0x5b1604430290"

How it was integrated: your branch was based on b5cafc2, three commits behind, so it was cherry-picked onto current main first. Only CHANGELOG.md conflicted. bun test 622 pass, 0 fail.

Verified here on the live shell, not just read: deployed, restarted, qs log clean for nixfred.blip, and every popout verb driven (open, close, toggle, toggleon, openon), plus a screenshot of the panel rendering under its bar icon.

And the multi-monitor half got verified after all. I said in the merge commit that I could not check it, because this machine had one output attached. A second one then appeared mid-session (an iPad stream at 5120,0), and it turned out to be Quickshell.screens[0], so it held the panel and the 5120x1440 desktop monitor was the follower. That is your exact case:

open              -> panel on the leader        (5120 0 960 540)
openon DVI-I-1    -> panel re-anchored there    (0 0 5120 1440)

Your separate open-only verb was the right call, and it meshes with Omarchy's own routing better than I expected: BarModel.pickPanelSlot prefers an already-open copy, so the next press still reaches the leader and closes it. The open-only verb cannot strand a panel.

One thing you could not have known about, and it is not yours to fix. The follower-to-leader forward hardcodes qs -p /usr/share/omarchy/shell. That is correct on a stock install and it predates your PR (four such calls already, yours added two more in the same style, which is exactly right per house rule "match existing style"). On this machine the shell runs from a dev link, so every forward exits 255 with No running instances and the summon still reports ok. Your fix is inert here for that reason alone, and so is the pre-existing click forward. I am raising it separately as its own change.

Still open from #110: no X handle found for you anywhere public. If you want credit in the weekly post under a handle, say the word and I will use it; otherwise you are credited as joshhattan.

nixfred added a commit that referenced this pull request Sep 20, 2026
…nning

WHAT: BarWidget's six follower-to-leader forwards stop hardcoding
`qs -p /usr/share/omarchy/shell` and use `Quickshell.shellDir`, the directory
the running shell was loaded from. New `root.shellRoot` holds it; ui.test.ts
gains a guard that fails on any re-hardcoded path.

WHY: `qs -p` matches a running instance BY ITS CONFIG PATH. The literal is
correct on a stock install and wrong the moment the shell runs from a checkout,
which is what `omarchy dev link` does and what gus has been running all along.
Every forward then exits 255 with "No running instances" — and the shell's own
summon still returns ok, because Bar.summonBarWidget only checks that the widget
exposes open(), not that anything happened. So on a second monitor the bar icon
and SUPER+CTRL+<n> did nothing, silently, with no warning anywhere.

Found while verifying #109, which is the sharp part: #109 is CORRECT, its new
test passes, and its feature was still completely inert on this machine. Four of
the six calls predate it; joshhattan's two copied the surrounding style and were
right to. The bug was in the style.

VERIFIED on real hardware, which earlier commits could not do. A second output
appeared mid-session (BEAM-IPAD, the iPad stream, 5120x1080 at 5120,0) and it is
Quickshell.screens[0], so IT owns the panel and the 5120x1440 desktop monitor is
the follower. With DVI-I-1 focused:

  before  qs -p /usr/share/omarchy/shell ... -> exit 255, no panel, summon "ok"
  after   shell summon nixfred.blip        -> panel on DVI-I-1 (0 0 5120 1440)

and a bare `open` still anchors to the leader (5120 0 960 540), so the
re-anchoring from #109 is confirmed in both directions on two real bars.

  * bun test 623 pass, 0 fail.
  * The guard was run against the PRE-FIX BarWidget.qml and FAILS there, then
    passes on the fix. A guard that cannot fail is not a guard.
  * Deployed and the shell restarted; bridge back to online=true, 298 threads,
    healthy=true; qs log clean for nixfred.blip.

NOT verified: moving Hyprland focus ONTO BEAM-IPAD did not work from the CLI, so
the leader-focused summon was exercised through the IPC verbs rather than by
really focusing that output.

SCOPE: Fred chose this over filing an issue (AskUserQuestion, 2026-09-20), as a
patch release rather than amending 2.6.0, so nothing already pushed is rewritten.

LR-T: blip, quickshell, ipc, multi-monitor, omarchy-dev-link, release
LR-D: blip, desktop
LR-K: qs-p-matches-by-config-path, summon-ok-does-not-mean-it-happened
LR-R: #109, 2.6.1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nixfred added a commit that referenced this pull request Sep 24, 2026
…lay-off

WHAT: the leader rule leaves the BarWidget.qml binding for screen-leader.ts
(built to ScreenLeader.mjs like SendState.mjs and BinDir.mjs), from Brad
Larson. A widget leads only on the FIRST REAL screen; an unresolved widget
(QsWindow.window still null) leads only when exactly one real screen exists;
with no real screen nobody leads, so nothing maps the app window.

WHY: when the last output disappears (monitor powered off, or a DisplayPort
monitor dropping off in its sleep, a Samsung C49HG9x does this) Qt substitutes
QPlatformPlaceholderScreen: name "", geometry 0x0. Omarchy's bar is a Variants
over Quickshell.screens, so it builds a bar on the placeholder; Blip's widget
there satisfied `Quickshell.screens.length <= 1`, crowned itself, and restored
the app window from window.json (visible:true). Hyprland 0.56.2 segfaulted in
CDefaultFloatingAlgorithm::newTarget <- CWindow::mapWindow mapping a floating
window with no output. Four separate nights on Brad's machine. gus logged the
same "There are no outputs - creating placeholder screen" twice on 2026-09-20.

VERIFIED in Test Drive (Omarchy 4.0.2, Hyprland 0.56.2, Quickshell 0.3.1, the
crash stack exactly), not by reading the diff:
  * bun test 629 pass, 0 fail; ScreenLeader.mjs is byte-identical to a fresh
    `bun build` of screen-leader.ts; quickshell-core.qmltypes exposes name,
    width and height on ShellScreen.
  * The output was removed with `hyprctl eval 'hl.monitor({ output =
    "Virtual-1", disabled = true })'` (`hyprctl keyword` is refused by the
    Lua parser). Pre-fix build with the window open and floating: the
    placeholder bar's Blip registered a second IpcHandler for nixfred.blip
    (the "another handler is registered" warning) and a Blip window was
    mapped, i.e. it crowned itself exactly as described.
  * An instrumented copy of the PR build logged Quickshell's screen list at
    every change. The placeholder presents as ["",0,0]. In the crash state,
    screens=[["",0,0]] only, the new rule gave leader=false and the window
    Loader off for both an unresolved widget and one resolved to the
    placeholder. When Virtual-1 returned, leader=true on it and the window
    came back on monitor 0.
  * NOT reproduced: the segfault itself. Hyprland stands up a FALLBACK
    1920x1080 output in the same event as the removal, and the window landed
    there. FALLBACK is a real screen (name and size), so Blip leads on it;
    that is correct, it is an output. The DRM-sleep race that leaves Hyprland
    with no output while a client maps is what Brad hit and this VM cannot
    stage.
  * Observation, not a defect: in the transitional [FALLBACK, placeholder]
    and [placeholder, Virtual-1] states an UNRESOLVED widget briefly leads
    under the unchanged one-real-screen rule, then drops it once its own
    screen resolves to the placeholder. A real output exists in both states.

LR-T: blip, leader-election, placeholder-screen, hyprland-crash, display-off, quickshell, test-drive
LR-D: blip
LR-K: qt-placeholder-screen-is-name-empty-0x0, hyprland-fallback-output-is-a-real-screen, hyprctl-keyword-refused-use-eval-hl-monitor
LR-R: #113, #109

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETCwBtUznDoR5qMkAEfzbd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants