Add ruixen.shelf: a standalone drop-pocket overlay with notch quick-drop - #93
Conversation
A 5th dashboard tab. Drag files in from any app (DropArea), drag them back out into other apps or terminals (Drag.Automatic, text/uri-list + text/plain). It holds absolute-path references only -- never copies, moves or deletes anything. Agent-native like Kanban: ShelfService is the single writer of ~/.local/state/ruixen/shelf.json, and shelfList/shelfAdd/shelfRemove/ shelfClear/toggleShelf are IPC functions on the ruixen.notch target. Agent-added items are tagged and shown with a badge. Existence/size come from a bounded, async, single-worker stat pass merged by path. Also tightens the notch tab rail's spacing (8 -> 6) so six 56px buttons fit the 368px of usable height. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146fvp9wxQkTMN71NeS4JRu
|
Design pivot after reviewing the current implementation and comparing it against the DnD behavior this feature actually needs: Recommendation: detach Shelf from the dashboard and make it its own
|
Design pivot (PR #93 comment): the expanded notch is a modal surface (fullscreen layer + input mask + Exclusive focus + click-away), which is wrong for cross-app drag and drop. The Shelf is now its own overlay plugin whose window is only as big as the shelf, anchored top-only under the notch, OnDemand keyboard focus, no mask, no screen space reserved. - ruixen.shelf: manifest, Shelf.qml (window, host lifecycle, own IPC target: open/close/toggle/add/addMany/remove/clear/list), plus the existing ShelfModel/ShelfService/ShelfContent moved over unchanged in behavior; state path (shelf.json) unchanged - ruixen.notch: Shelf tab removed (back to four tabs, tab-rail spacing restored); the collapsed pill gains a DropArea that highlights on a local-file drag and relays one batched addMany call to ruixen.shelf over IPC (no shared live objects). No spring-loading: unverified - ShelfContent: preventStealing on the row drag, thumbnails use ShelfModel.uriFor instead of string concatenation - Installer enables ruixen.shelf; tests updated; docs + AGENTS.md section 9 note the new surface consumer Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146fvp9wxQkTMN71NeS4JRu
|
Implemented the pivot in df3935b (PR title/description updated to match). Status against the acceptance criteria: Done and covered by tests here (
Not verifiable from the cloud container — on the PR's "check on the real machine" list: Intentionally left out: Stage 3 spring-loading. Per the comment it should only be kept if verified live, and I can't verify that an in-flight drag carries into a newly mapped layer surface, so quick-drop on the notch is the only notch activation for now ( One judgment call to flag: the Shelf surface reads the same Black/Theme state as the notch (own copy of the resolve logic, Solid only, per AGENTS.md §9) but its exact attachment to the notch (400px wide, 52px top margin, uniform corner radius) is a first guess — expect to tune Generated by Claude Code |
|
Pre-live-test implementation pass: the architecture looks good now. I would not redesign the Shelf again before testing it live. There are just two code fixes I want landed first, plus one focus behavior to treat as a live-test gate. 1. Force incoming drops to CopyActionThis is the most important pre-live-test fix. The Shelf is explicitly a reference/copy-semantics feature: it never moves or deletes the source file. Right now the incoming drop paths use That means if a source application proposes Please explicitly accept
Suggested shape: onEntered: drag => {
if (/* acceptable local file/folder */)
drag.accept(Qt.CopyAction)
}
onDropped: drop => {
...
var result = root.shelfService.addPaths(paths, "user")
if (result.added.length > 0)
drop.accept(Qt.CopyAction)
}For the notch quick-drop: onEntered: drag => {
if (shelfQuickDrop.localUrls(drag.urls).length > 0) {
drag.accept(Qt.CopyAction)
root.notchHoverEntered()
}
}
onDropped: drop => {
var urls = shelfQuickDrop.localUrls(drop.urls)
root.notchHoverExited()
if (urls.length === 0) return
root.relayToShelf(urls)
drop.accept(Qt.CopyAction)
}Also avoid accepting a drop that did not actually produce an acceptable local Shelf entry. The intended guarantee should be:
Please add a static contract check for this so 2. Fix the current CI failure before live testingThe current PR head's feature tests are green:
But the overall GitHub Actions run is red because ShellCheck flags It is the count implemented through Please clean that up with the appropriate ./tests/run-all.shand make sure the actual GitHub Actions run is green before treating the branch as ready for live validation. This looks like a test-lint-only failure, not a Shelf behavior failure. 3. Focus behavior: don't preemptively redesign, but make it an explicit gateThe standalone window correctly uses: WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemandbut Qt.callLater(function() { focusScope.forceActiveFocus() })That may be fine because Qt Quick item focus and compositor keyboard focus are not exactly the same thing, but it may also undermine the reason we chose OnDemand: opening the Shelf should not unexpectedly take typing away from the application the user is currently working in. I would not change this blindly before the first real test unless there is an obvious documented reason it is required. Instead make this the first behavioral focus test:
If opening Shelf steals keyboard focus, first remove the automatic Then let focus move into Shelf only after actual pointer interaction with it. Escape only needs to work after Shelf has focus; the explicit close button and toggle keybind remain available otherwise. If After those fixes: live test, don't add more features yetOnce CopyAction + CI are fixed, I think this branch is ready for real-machine testing. Please do not add spring-loading, undo, search, pinning, browser-image import, auto-dismiss, etc. before the first live DnD pass. The key things the live test needs to prove are:
At this point the architecture is healthy enough that the next useful information has to come from real Hyprland/Wayland behavior, not another structural pass. |
- The open Shelf and the notch quick-drop now accept/advertise Qt.CopyAction explicitly instead of acceptProposedAction(), which would echo back a MoveAction a source app proposed even though the Shelf only stores a reference - A drop is only accepted once the shelf actually added something; drag enter and drop share the service's own normalizePath gate (and plain-text absolute paths still work); the notch rejects file:/// (root) - Static contract: acceptProposedAction() can't creep back in; copy semantics asserted for drag-in (shelf + notch) and drag-out - tests/shelf-plugin.sh: replace grep|wc -l with grep -c (ShellCheck SC2126, the CI failure on this PR) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146fvp9wxQkTMN71NeS4JRu
|
Landed in 2dfb841, addressing the three points: 1. CopyAction — both incoming paths now accept with
2. CI — fixed the ShellCheck SC2126 in 3. Focus — left as-is deliberately, and made it Gate 1 at the top of the PR's live-test list with your exact procedure (cursor in a terminal, open without clicking, type immediately). The fallback ladder is in the description: first drop No other features added; PR description updated to match (copy-semantics section, 53 checks, focus gate). Generated by Claude Code |
Found live: addMany never added anything over IPC, which broke the
notch quick-drop entirely -- the feature's primary entry point.
Two bugs, one root cause (a bracketed JSON array does not survive the
IPC boundary as a single argument):
- addMany '["/a","/b"]' arrived as THREE arguments and was refused by
the host with "Too many arguments provided (2 required but 3 were
provided)"; the count tracked the array length exactly
- a one-element array arrived as a scalar, so Array.isArray() was
false, paths stayed empty, and it silently reported
{"ok":false,"added":0,"rejected":0} -- added:0 AND rejected:0 is
the tell that the paths never reached the normalizePath gate
addMany's argument is now newline-delimited. Newline rather than comma
or semicolon because ShelfModel.normalizePath already rejects any path
containing \n or \r, so the delimiter cannot collide with a real path.
A leading "[" is still parsed as JSON so an in-process caller can keep
passing an array literal.
Verified live after redeploy: 3 files -> {"ok":true,"added":3,
"rejected":0}; mixed folder/file/bad-path -> added:2, rejected:1;
paths with spaces, '#', parentheses and Unicode add fine; folders and
symlinks come back kind:"folder"/"file" with exists:true.
tests/shelf-plugin.sh pins the encoding on both sides (4 new checks)
and the signature check follows the renamed parameter. Note these are
still grep-based contract checks -- they cannot prove the host's
splitting behavior, which is why this needed a live shell to find.
Drag-into-the-open-shelf was never affected: ShelfContent calls
service.addPaths in-process, no IPC involved.
Co-Authored-By: opencode <noreply@opencode.ai>
Live testing found
|
Follow-up to f4836e3 (addMany's newline-delimited argument, found live). - ShelfModel.parsePathsArg/joinPathsArg: the addMany argument decoding moves out of the QML handler into the pure model, so it has behavioral tests (newline batch, single path, CRLF/trailing newline, blank lines, commas/semicolons are not delimiters, legacy [ JSON, invalid JSON, and a round trip through awkward filenames) instead of only grep checks. Shelf.qml now imports ShelfModel.js and calls it - tests/live-shelf-ipc.sh: drives the REAL omarchy-shell ruixen.shelf IPC boundary (addMany/add/list/remove with spaces, #, parens, %, Unicode, folders, symlinks). Needs a running shell, so it is deliberately outside run-all.sh/CI like host-contract-regression.sh; skips cleanly without one. Only touches its own temp paths, never calls clear - docs/CONTROL.md: addMany example was still the broken JSON-array form - Overlay.qml: comment typo (ruixin -> ruixen) - tests/shelf-plugin.sh: checks follow the refactor; resolves the stale 'see the real end-to-end check below' pointer Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146fvp9wxQkTMN71NeS4JRu
Two changes to how the shelf looks and how it is laid out, keeping the drop-pocket contract (cross-app drag in/out, no fullscreen surface, OnDemand focus) exactly as it was. Shape, copied from ruixin.notch's own surface rather than invented: - 420 wide, the same width as the notch's launcherOpen mode, so the shelf reads as a step of the notch's size progression instead of its own family. 420 over anything wider on purpose: Overlay.qml's own history is a history of NEW sizes breaking the notch mask non-deterministically at the larger end. - Fixed 236 height, 28 shoulders, 44 bottom radius (the notch's own expanded radius). - The notch's shadowShadowBlur arrangement: a blurred duplicate of the shape behind a separately clipped Item, flush to the top edge so it meets the notch. No shadow properties on the masked fill -- Overlay.qml:1770-1787 records that combination reproducibly destroying the silhouette. - One plain rounded Rectangle per surface, not the notch's 3-piece RoundCorner/centerMask split: that split exists for the notch's flank wings, which this window does not have. Same radii, no seam. Layout, as an inbox: - Rows are a horizontal strip of 124px cards, so the panel keeps a fixed height no matter how full the shelf is and stays out of the way while dragging. It was capped at 420px tall and then silently stopped showing new items, which is the one thing a drop pocket must not do. - Wheel scrolls the strip; the right edge fades, only when there is more to scroll to. - A search box filters by name, folder or full path, via a tested ShelfModel.filterEntries. The header shows "2 of 9" while filtering so a shrinking count does not read as deletions, and "no match" is its own state, distinct from an empty shelf. - Clear-the-text is a separate affordance from the shelf-wide Clear. Verified: run-all green (90 shelf checks), omarchy plugin validate, a real omarchy restart shell with a new PID and no shelf warnings in the journal, live layer at 420x236, and the filter checked against the live shelf.json contents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed ShapeCopied from
The one departure — worth a lookThe notch builds its shape from two Reason: that 3-piece split exists so the notch's flank wings can tuck under the shoulders. This window has no flank pieces, so the split buys nothing and costs a seam to tune. Same radii, same silhouette. If you'd rather the shelf stay a literal copy of the notch's construction for consistency, say so and I'll switch it. Related: my first attempt imported Layout — an inbox
Verification
Two things I did not verify and would like eyes on:
|
The panel was a rounded box floating 52px below the notch. It now grows out of the frame like the notch's expanded launcher shape: - left flank + square-topped center + right flank, concave wing shoulders flaring out to the frame, built the way Overlay.qml builds it (seam overlap, 28 shoulders, 44 bottom radius, 420 body) from a shelf-local ShelfRoundCorner copy -- the notch's RoundCorner is an inline component, not a shared type - flush under the frame at the notch's own resting offset (frameInset 4, was a 52px gap); content inset by the shoulders and clears the frame edge - shadow now actually shows: the clip extends OUT by a halo pad (it was inset, hiding the halo under the fill); the window is padded for it and its input region (mask) is only the visible shape, so the halo stays click-through and no fullscreen/blocking surface exists - contract tests updated from 'single rounded box' to the real construction (wings, flush attach, outward shadow clip, shape-only input region); AGENTS.md section 9 / docs describe the new placement The inbox/search/strip content from 5ef1e60 is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146fvp9wxQkTMN71NeS4JRu
The journal reported, on every shell restart: QML ListView at ShelfContent.qml[416:5]: Binding loop detected for property "contentWidth" from the `contentWidth: count > 0 ? childrenRect.width : 0` line in 5ef1e60. That binding is circular: a ListView sizes its own content item FROM contentWidth, so asking for childrenRect.width back off it is asking a loop for a number. The bound value was never needed. The view already derives contentWidth from its delegates' positions, and the window is a fixed size (the shape plus its shadow halo pad), so an unbounded strip cannot stretch it -- what keeps the list from resizing the window is its anchors, not a content width. Removed the line and pinned the absence in the contract test instead, since the old test asserted the presence of the exact line that was causing it. Verified on a real `omarchy restart shell`, new PID 430756, with the loop gone from the journal and nothing shelf-related left in it. The image-decode warnings that remain in older entries were my own `echo x > file.png` demo stubs, not real files; with real PNGs the thumbnail path is clean too. 100 shelf contract checks, run-all green, plugin validate clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
420 showed about four cards of the horizontal strip and spent the rest of its width on empty surface. 900 shows about seven, which is what makes the shelf read as an inbox you can scan rather than a strip you have to scroll to find anything on. 900 is the notch's own EXPANDED (pinned) width, not the launcher's 420 (Overlay.qml:1715), so the shelf now matches the biggest silhouette the notch itself has, rather than a step below it. This is the exact knob the notch's own history warns about, so the reasoning is recorded in the code next to the number: Overlay.qml:1687 calls 900 "untested territory for this notch (only 44 and 190 are proven safe against the masking bug below)" -- the bug being the non-deterministic flat-bottom-corner masking failure at 1770-1787. The notch then took 900 for its pinned dashboard anyway and it holds up live, so this is a working value and not a hypothetical. The contract test now pins 900 specifically so reverting it is a visible act rather than a quiet edit. Verified on a real `omarchy restart shell`, new PID 442756, live layer 442 4 1036 276 (956 shape + 40 halo per side), and no silhouette flattening or shelf warnings in the journal at the wider size -- the failure mode that history warns about would show here first. 100 shelf checks, run-all green, plugin validate clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SUPER+D goes through `shell toggle ruixen.shelf` like every other overlay's bind, and only under --with-launcher-keybind: a plugin that takes a key without asking can clobber the user's own bind, so the installer still leaves keys alone unless asked. Clear (which empties the whole shelf) moves off the header down to the right end of the search row. It stays a sibling of searchBox rather than a child, so it does not read as part of the text field, and searchBox gives that width back when Clear hides so an empty shelf has no leftover gap. Note: no click-to-dismiss. The focus-change approach was tried and removed rather than left in dead -- Hyprland.activeToplevel did not change on click-away here, so the root cause is still open.
…olling Layout: - The count moves into a "Shelf 16" chip on the left of the search row, as its own shape beside the field rather than nested inside it (a chip drawn inside the rounded box reads as part of the text input). While filtering it reads "3 of 16", because a bare number falling from 16 to 1 as you type looks like items are being deleted. - The title/count/close bar above the search row is gone; the chip already carried the same count. Escape is the dismiss. - Clear (which empties the whole shelf) sits at the right end of that row, outside the field, and gives the width back when it hides. - Chip, field and Clear are bound to one height and one corner radius. - Padding is per-edge (padTop 12, padSide/padBottom 24) instead of anchors.margins, which would have tied the top to the sides. The top stays tight on purpose: it butts up against the notch. Scrolling: left/right/Home/End now scroll the strip, from the field or from the strip itself (clicking bare strip focuses it). Both go through one scrollStrip() clamp helper so the two inputs cannot drift apart. KNOWN BROKEN: the wheel still does not scroll, and that is unresolved. The strip is ~2104px of content in an ~850px view, so there is room to move, and the arrow keys work against the same helper. It could not be reproduced headlessly -- synthesizing a wheel event needs /dev/uinput write access, which is not available -- so it is unknown whether the WheelHandler is never invoked or fires and the math is wrong. The exact one-line probe to settle it is documented above the handler, along with how to read the result. Also: qml-dup-anchor.py, because a bulk anchor edit gave `list` two topMargin lines and QML's answer to that is a warning plus a dropped type, taking the whole plugin down. Neither `omarchy plugin validate` nor any grep-based test catches it, because it is a compile-time diagnostic.
Reported live: with ~12 items nothing past the first screenful was reachable -- the wheel did nothing and the arrow keys did nothing. - Wheel: replace the WheelHandler declared inside the ListView with a wheel-only MouseArea laid over the strip as a sibling (NoButton, so it takes no presses and card clicks / double-click / drag-out still reach the delegates; it passes the event on when it cannot scroll). A pointer handler declared in a Flickable is attached to its content item, so it depended on event delivery order and never visibly fired - Keyboard: arrows only worked when the search field or the list held item focus, and clicking a card focused neither (and a full shelf has no bare strip left to click). Pressing a card now focuses the list, and the Shelf's focus scope forwards arrows/Home/End to the strip so they work after a click anywhere on the shelf - Contract tests pin the wheel overlay (sibling, NoButton, after the list, no WheelHandler), the pass-through, and both focus paths Not reproduced here (no display, no wheel injection): the root cause of the old handler not firing is unconfirmed; this routes around it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146fvp9wxQkTMN71NeS4JRu
The chip is labelled Inbox rather than Shelf, which reads better next to the cards. Its counter is now a plain total instead of "3 of 12" while filtering. That reverses the original reasoning -- a count falling from 12 to 1 as you type reads like items are being deleted -- but the chip is sized to its text, so the count changed width on every keystroke and the whole search row visibly jumped. A fixed total keeps the row still; the results underneath already show that a filter is active. The reversal and its reason are recorded on chipCount so it does not get "fixed" back. Also corrects a padding comment still claiming 12px after the move to 24, and a test comment claiming a click on the chip focuses the field. It does not: the chip has no MouseArea, so clicks fall through to the panel.
Live feedback: the highlight drawn behind the compact notch isn't wanted. Dragging a file or folder over the notch should open the Shelf so the drop lands in it and you can see it arrive; leaving without dropping should put it away again. - ruixen.notch: no highlight. A local-file drag entering the pill asks ruixen.shelf to open via one IPC call (openFromDrag). A drop that lands on the pill before the Shelf takes over the drag is still relayed (addMany) - ruixen.shelf: new openFromDrag IPC. A shelf opened this way (openedByDrag) stays open once a drop lands (Escape/toggle/close dismiss it), hides itself after a 350ms grace if the drag leaves (cancelled if it comes back), and after a 2.5s watchdog if no drag ever reaches it. A shelf opened any other way is never auto-hidden. A 3s cooldown after an auto-hide stops a drag parked on the notch flapping it open and shut - the whole panel (wings and padding included) is now a drop target, so a drag over a dead strip is not read as having left; drag lifecycle reaches Shelf.qml through explicit signals, not a Connections on containsDrag - contract tests pin all of the above (and were mutation-checked); docs describe the new behavior Unverified: whether a drag in progress carries into the freshly mapped Shelf window is compositor behavior, and Escape after a drop needs the Shelf to have keyboard focus (OnDemand only grants it on click). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146fvp9wxQkTMN71NeS4JRu
Escape only worked if you clicked the shelf first. Opening it with SUPER+D or a drag over the notch gave a window that looked focused -- open() calls focusScope.forceActiveFocus(), so Qt-level focus was set -- and swallowed Escape, which went to the app behind it. Under Wayland that is not a Qt focus bug. OnDemand means the compositor withholds the keyboard until a click, so Qt never sees the keystroke at all; forceActiveFocus cannot conjure it. Hold Exclusive while open so Escape has something to arrive at. OnDemand alone was chosen deliberately, to keep the shelf from grabbing the keyboard from whatever the user drags into, so Exclusive is released again for the one case that actually needs it: a card being dragged out. "Drag a file into a terminal, then type a command" goes back to working, and focus is retaken when the drag ends so Escape works again afterwards. Each delegate now reports its own Drag.active up to the window, which is what the decision is made from -- asked at that moment, not bound to. The shelf still holds no keyboard while shut, and the notch drag-in path is unaffected apart from also gaining a working Escape. Verified live: activewindow flips to org.quickshell on open, Escape dismisses on both the keybind and openFromDrag paths, and typing lands back in kitty after a close. 148 checks, run-all green, plugin validate clean. Supersedes the "keyboard focus is on demand, never exclusive" test and the "OnDemand only" premise behind the search-focus test; both were rewritten to state the conditional rule instead, since the old invariant is now wrong. Untested by me: the actual drag-out mid-drag handover, which needs a real pointer drag and a foreign drop target.
…oard focus 5999ff6 made the Shelf hold the keyboard exclusively while open (released during a drag-out) so Escape works without a click. Several places still said the opposite: CONTROL.md ('never takes focus on its own, so it can't steal typing', 'keyboard focus on demand only'), AGENTS.md section 9 ('no Exclusive focus'), the Shelf.qml header and a ShelfContent comment. Docs/comments only, no behavior change. CONTROL.md now states the actual trade-off: typing goes to the Shelf, not the app underneath, until it is dismissed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146fvp9wxQkTMN71NeS4JRu
What
ruixen.shelf— a drop pocket / Inbox for file references, as its own overlay plugin (not a notch dashboard tab). It is a panel that hangs from the frame at the notch's position in the notch's expanded silhouette (concave wing shoulders, rounded bottom). Drag a file or folder over the compact notch and the Shelf opens so you can drop it in and see it land; drag back out without dropping and it hides again. Drag files back out into another app or a terminal. It never copies, moves or deletes anything on disk. An agent can read what you dropped and hand you files back over IPC.What's in it
ruixen.shelf/— overlay plugin,keepLoaded.Shelf.qml: window, host lifecycle,ruixen.shelfIPC target, and the drag-open/auto-hide logic.ShelfService.qml: the only writer of~/.local/state/ruixen/shelf.json(versioned, atomic, cap 200); async bounded single-workerstatpass.ShelfModel.js(pure, unit-tested).ShelfContent.qml: Inbox chip + search + Clear above a horizontal strip of cards (thumbnails, hover copy/open/remove, agent badge, missing-file handling); wheel and Left/Right/Home/End scroll the strip.DropAreadraws no highlight; a local-file drag entering it makes one IPC call,openFromDrag, and the Shelf opens under the drag (no dwell timer). A drop that lands keeps it open; the drag leaving hides it after a 350 ms grace (cancelled if the drag returns); a drag that never reaches it is caught by a 2.5 s watchdog; a Shelf opened any other way (keybind,open) is never auto-hidden; a 3 s cooldown after an auto-hide stops a parked drag flapping it. The whole panel — wings and padding included — is a drop target. A drop that lands on the pill before the Shelf takes over is still accepted and relayed (addMany).OnDemandwhile shut), so Escape dismisses it with no click first. Under Wayland anOnDemandwindow only receives keys after a click, so Escape was going to the app behind it;forceActiveFocus()can't change that. It lets go of the keyboard while a card is being dragged out (so you can drop into a terminal and type) and takes it back when the drag ends; it holds no keyboard while closed. Trade-off: while the Shelf is open, typing goes to the Shelf, not the app underneath, until it is dismissed (Escape, the toggle keybind, orclose). This supersedes the earlier "OnDemand, never Exclusive" rule; the tests and docs state the conditional rule.Overlay.qmlbuilds the expanded notch (flanks + square-topped center, 28 shoulders, 44 bottom radius,seamOverlap2), 900 body, fixed 236 height, flush under the frame at the notch's resting offset (frameInset4).ShelfRoundCorner.qmlis a shelf-local copy of the notch's inlineRoundCorner. Shadow isnotchShadowBlur's recipe with an outward clip; the window is padded for the halo and its input region is only the visible shape.ExclusionMode.Ignore, Overlay layer; no fullscreen surface, no click-away catcher.Qt.CopyAction, neveracceptProposedAction(), and only once something was added; drag-out advertises copy only and never auto-removes.ruixen.shelfinshell.json;install.sh --with-launcher-keybindalso offersSUPER+Dwhen free (uninstall removes everyruixen.*plugin id generically).docs/KEYBINDS.mdmoves Display Settings toSUPER+SHIFT+D.docs/CONTROL.md,docs/KEYBINDS.md;AGENTS.md§9 lists the shelf as a fourth surface consumer whose shape is a copy of the notch's.tests/js/ShelfModel.test.js;tests/shelf-plugin.sh(manifest, IPC, window semantics, copy-only drops, notch wiring, silhouette, scroll/focus, drag-open/auto-hide, keyboard rule — mutation-checked);tests/qml-dup-anchor.py(a duplicateanchors.*silently kills the whole plugin; neitheromarchy plugin validatenor the greps see it);tests/live-shelf-ipc.shdrives the real IPC boundary (needs a running shell, so outsiderun-all.sh/CI).Not in this PR
Hyprland.activeToplevel) doesn't fire when you click the already-active window or empty desktop. (Escape / toggle /closedismiss instead.)Verified
CI green on the previous head;
./tests/run-all.shgreen on this one. Live (real Hyprland/Quickshell): plugin validates and loads with a clean journal, drag into the open Shelf,addMany/add/list/removeover real IPC (this found and fixed the JSON-array bug), layout and scrolling, Escape dismissing on both the keybind andopenFromDragpaths, and typing landing back in the terminal after a close.Not verified by anyone yet
Per
AGENTS.md§8, after a fullomarchy restart shell(new PID):journalctl --user -b 0foris not a type/Type … unavailable/TypeError/Property value set multiple times, then./tests/live-shelf-ipc.sh.root.relayShelfOpen()line inonEnteredinOverlay.qml(the plain drop-on-the-pill path keeps working)#,%, Unicode); a source app that proposes move must not lose its fileUnrelated, pre-existing (not from this PR):
ruixen.quickactions/ruixen.stayawakethrowTypeError: Property 'firstPartyServiceFor' … is not a functionon shell start.Please don't bump
COMPATIBILITY.mdreviewed versions for this.🤖 Generated with Claude Code
https://claude.ai/code/session_0146fvp9wxQkTMN71NeS4JRu