From 39f925cb5846e38cbcadfa8eae7ab207520b6ced Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 11 Aug 2026 16:34:09 +0200 Subject: [PATCH] updated design --- .gitignore | 2 + DESIGN-IS-2026-08-11/00-scope.md | 36 ++ DESIGN-IS-2026-08-11/01-evidence.md | 114 +++++ DESIGN-IS-2026-08-11/02-scorecard.md | 43 ++ DESIGN-IS-2026-08-11/03-verdict.md | 30 ++ DESIGN-IS-2026-08-11/04-handoff-prompt.md | 41 ++ hypr/lid_state.conf | 2 +- hypr/modules/keybinds.conf | 2 +- plans/01-takemi-design-hierarchy.md | 503 ++++++++++++++++++++++ quickshell/README.md | 8 +- quickshell/components/P5Button.qml | 3 +- quickshell/components/Popout.qml | 2 + quickshell/config/PopupCoordinator.qml | 27 ++ quickshell/lock/LockFace.qml | 16 +- quickshell/overlays/PowerMenu.qml | 40 +- quickshell/popouts/TrayMenu.qml | 7 + quickshell/popouts/TrayMenuList.qml | 2 +- 17 files changed, 859 insertions(+), 19 deletions(-) create mode 100644 DESIGN-IS-2026-08-11/00-scope.md create mode 100644 DESIGN-IS-2026-08-11/01-evidence.md create mode 100644 DESIGN-IS-2026-08-11/02-scorecard.md create mode 100644 DESIGN-IS-2026-08-11/03-verdict.md create mode 100644 DESIGN-IS-2026-08-11/04-handoff-prompt.md create mode 100644 plans/01-takemi-design-hierarchy.md create mode 100644 quickshell/config/PopupCoordinator.qml diff --git a/.gitignore b/.gitignore index 9c88b01..6c7c6c5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ !brave-flags.conf !chromium-flags.conf !README.md +!plans/ +!DESIGN-IS-2026-08-11/ !rice.png !rice-2.png !rice-3.png diff --git a/DESIGN-IS-2026-08-11/00-scope.md b/DESIGN-IS-2026-08-11/00-scope.md new file mode 100644 index 0000000..c53c1b7 --- /dev/null +++ b/DESIGN-IS-2026-08-11/00-scope.md @@ -0,0 +1,36 @@ +# Takemi shell design audit — scope + +## Audited artifact + +- Running Persona 5-inspired desktop shell shown in the two supplied 1920×1080 screenshots. +- Quickshell implementation under `quickshell/`, including the top bar, right rail, + pop-outs, tray menus, power overlay, notifications/OSD, and lock screen. +- Shared design tokens and primitives in `quickshell/config/` and + `quickshell/components/`. + +## User and task + +- Primary user: the workstation owner using the shell throughout the day. +- Primary task: identify system state and reach common desktop controls quickly, + without the theme obscuring application content. + +## Constraints + +- Preserve the Persona 5 / Tae Takemi identity. +- Preserve the crimson, black, and white palette and screen-print character. +- Stack remains Quickshell, Hyprland, and Fluxo. +- The result must work at desktop scale, with keyboard and pointer input, and + remain legible during long sessions. + +## References + +- Persona 5 interface language: asymmetric editorial composition, hard ink, + decisive silhouettes, halftone texture, and rapid card-like motion. +- Current implementation and supplied screenshots are the concrete baseline; + fidelity to the game is subordinate to daily desktop usefulness. + +## Out of scope + +- Application-level themes outside the shell. +- Replacing the wallpaper or character artwork. +- Changing the desktop stack. diff --git a/DESIGN-IS-2026-08-11/01-evidence.md b/DESIGN-IS-2026-08-11/01-evidence.md new file mode 100644 index 0000000..e517f8e --- /dev/null +++ b/DESIGN-IS-2026-08-11/01-evidence.md @@ -0,0 +1,114 @@ +# Evidence + +## 1. Innovative + +- The shell applies one custom QML geometry and print system across panels, + backdrops, motion, and textures (`quickshell/config/Theme.qml:118-153`, + `quickshell/components/P5Panel.qml:7-56`). +- This refreshes standard desktop bars/popovers, but the interaction model is + still conventional bar + rail + popup + modal. + +## 2. Useful + +- The permanent bar exposes workspaces, active window, media, resources, and + time in one slab (`quickshell/bar/Bar.qml:67-122`). +- Power and lock are keyboard-operable; bar, rail, tray, and most pop-out + actions remain pointer-only (`quickshell/overlays/PowerMenu.qml:242-261`, + `quickshell/lock/LockFace.qml:290-311`, + `quickshell/bar/RailIcon.qml:104-124`). +- The primary surfaces contain at least 41 interactive targets; the live total + grows with workspaces, devices, streams, notifications, and tray entries. + +## 3. Aesthetic + +- A clear system exists: spacing `[6,12,20,32]`, type + `[9,11,12,15,20,34,92]`, 12 literal palette colors, one nominal 14° angle, + and shared 10px cuts (`quickshell/config/Theme.qml:19-49,92-148`). +- Runtime type exceptions include 8, 30, 42, and 156.4px; local geometry also + introduces several non-token cuts and leans. +- White on crimson is 3.68:1 and fails normal-text AA where selected destructive + actions or unread badges use that pairing + (`quickshell/components/P5Button.qml:32-73`, + `quickshell/bar/NotifButton.qml:38-58`). +- Screenshot #1: permanent chrome is visually continuous and disciplined, but + competes with an already high-contrast Persona wallpaper/frame. +- Screenshot #2: the left headline/right action-stack composition is strong; + the repeated stripe field and still-legible desktop behind it dilute focus. + +## 4. Understandable + +- The power screen labels all actions and prints its keyboard map + (`quickshell/overlays/PowerMenu.qml:31-38,216-261`). +- Rail icons expose different left/right/middle/scroll behaviors without visible + instruction (`quickshell/bar/Rail.qml:76-132`). +- Several state labels behave as inverse actions: “Wi-Fi On,” Bluetooth “On,” + “Scanning,” and “Silenced” toggle those states off. +- “WHO GOES THERE,” “WRONG,” “HELD,” “PROCS,” “VRAM,” and unlabeled load values + favor voice or jargon over immediate comprehension. + +## 5. Unobtrusive + +- Normal chrome consumes only a 40px top bar and 48px right rail + (`quickshell/config/Theme.qml:109-116`). +- The full-screen modes add stripes, a wedge, oversized split text, attribution, + six slabs, and a hint simultaneously + (`quickshell/overlays/PowerMenu.qml:50-228`). +- Twelve action identities are repeated between permanent chrome and detail + panels; repetition improves reach but adds competing affordances. + +## 6. Honest + +- No marketing inflation or dark patterns were found. Power labels map directly + to their system action (`quickshell/overlays/PowerMenu.qml:31-38,230-239`). +- “n HELD” counts all history rather than suppressed items + (`quickshell/popouts/NotifPopout.qml:27`, + `quickshell/services/Notifs.qml:36-44`). +- Muted OSDs display 0% even when stored volume remains nonzero + (`quickshell/overlays/Osd.qml:51-64,172-185`). +- A visually disabled slider can still emit changes + (`quickshell/components/P5Slider.qml:9-12,48-84`). + +## 7. Long-lasting + +- The system is based on reusable primitives rather than per-screen decoration. +- Literal `P5` branding, the quote attribution, and the esports-like continuous + frame tie the result closely to one entertainment reference and visual era + (`quickshell/bar/Sigil.qml:31`, + `quickshell/overlays/PowerMenu.qml:88-118`). + +## 8. Thorough + +- Empty, loading, failure, focus, and disabled states exist. +- A rendered success state is absent; generic buttons lack a disabled state; + focus treatment is concentrated in power and lock; explicit accessible names + and roles total zero (`quickshell/components/P5Button.qml:6-107`). +- The lock’s disabled state is clear, but `P5Slider` can look disabled while + remaining interactive. + +## 9. Environmentally friendly + +- The shell has 0 JavaScript bytes, about 259KB of QML, 1.4KB of local texture + assets, no declared HTTP request, and a measured load proxy below two seconds. +- Seven Fluxo reads recur in polling batches; playing media creates 3–6 idle + animation loops (`quickshell/services/Sys.qml:78-102`, + `quickshell/bar/MediaPill.qml:125-153`). +- No reduced-motion branch exists (`quickshell/config/Theme.qml:139-148`). + +## 10. As little design as possible + +- The one-slab bar avoids a row of redundant cards + (`quickshell/bar/Bar.qml:8-14,40-63`). +- The audited tree reaches seven declarative levels, contains twelve repeated + action identities, and includes removable atmospheric elements in modal and + lock compositions. +- The permanent surface, transient surface, and cinematic surface currently + share too much of the same visual intensity instead of forming a hierarchy. + +## Known gaps + +- Dynamic application strings, device counts, and menu depth cannot be fixed + from static source. +- Exact compositor frame time, GPU cost, and live accessibility-tree output + were not available. +- Screenshot evidence covers the normal desktop and power overlay; lock-screen + judgments also use its implementation source. diff --git a/DESIGN-IS-2026-08-11/02-scorecard.md b/DESIGN-IS-2026-08-11/02-scorecard.md new file mode 100644 index 0000000..ac53e50 --- /dev/null +++ b/DESIGN-IS-2026-08-11/02-scorecard.md @@ -0,0 +1,43 @@ +# Rams scorecard + +1. **Good design is innovative — 2/3** + Evidence: one custom print/geometry system refreshes conventional shell surfaces (`01-evidence.md#1-innovative`). + Justification: the expression is distinctive, but it does not introduce a new desktop interaction pattern. + +2. **Good design makes a product useful — 2/3** + Evidence: core state and actions are direct, while most shell controls require a pointer (`01-evidence.md#2-useful`). + Justification: the primary tasks work quickly, but keyboard and discoverability gaps add friction. + +3. **Good design is aesthetic — 2/3** + Evidence: the token system is strong, with several type/geometry exceptions and a 3.68:1 text pairing (`01-evidence.md#3-aesthetic`). + Justification: the visual system is unmistakable, but contrast and intensity inconsistencies prevent a fully resolved score. + +4. **Good design makes a product understandable — 1/3** + Evidence: multiple rail gestures are undisclosed and several state labels read like actions (`01-evidence.md#4-understandable`). + Justification: more than three controls require prior knowledge or experimentation. + +5. **Good design is unobtrusive — 1/3** + Evidence: compact permanent chrome expands into visually competing full-screen decoration (`01-evidence.md#5-unobtrusive`). + Justification: the theme sometimes becomes the figure when the user’s task should remain the figure. + +6. **Good design is honest — 2/3** + Evidence: no dark patterns exist, but held-count, muted-volume, and disabled-slider presentation diverge from behavior (`01-evidence.md#6-honest`). + Justification: the mismatches are minor rather than deceptive, but prevent a one-to-one mapping everywhere. + +7. **Good design is long-lasting — 2/3** + Evidence: reusable primitives coexist with literal franchise and gaming-HUD markers (`01-evidence.md#7-long-lasting`). + Justification: the craft will last, while the most literal references may date the surface. + +8. **Good design is thorough — 1/3** + Evidence: success, generic disabled, broad focus, and accessibility metadata are missing or rough (`01-evidence.md#8-thorough`). + Justification: three or more important state families remain incomplete. + +9. **Good design is environmentally friendly — 1/3** + Evidence: source weight is small, but recurring polling and idle motion have no reduced-motion gate (`01-evidence.md#9-environmentally-friendly`). + Justification: the runtime is lean yet motion is always on when its conditions are met. + +10. **Good design is as little design as possible — 1/3** + Evidence: restrained single slabs coexist with repeated affordances and multi-layer modal atmosphere (`01-evidence.md#10-as-little-design-as-possible`). + Justification: at least three decorative or duplicated elements could be removed without breaking a task. + +**Total: 15/30** diff --git a/DESIGN-IS-2026-08-11/03-verdict.md b/DESIGN-IS-2026-08-11/03-verdict.md new file mode 100644 index 0000000..0433761 --- /dev/null +++ b/DESIGN-IS-2026-08-11/03-verdict.md @@ -0,0 +1,30 @@ +# Verdict — REDESIGN + +The strict Rams verdict is **REDESIGN (15/30)**: preserve the excellent brand +tokens and working shell primitives, but redesign the hierarchy so permanent, +transient, and cinematic surfaces no longer compete at the same intensity. + +## Highest-leverage moves + +1. **Principles #4 and #8 — make behavior legible:** give every icon one primary + click action, add concise Persona-styled hover labels, expose secondary actions + inside its pop-out, and add real focus/disabled/accessibility states. Evidence: + `01-evidence.md#4-understandable` and `#8-thorough`. +2. **Principles #5 and #10 — introduce three intensity tiers:** quiet permanent + chrome, expressive transient panels, theatrical full-screen scenes; reserve + stripes, large split type, and strong halftone for the latter two. Evidence: + `01-evidence.md#5-unobtrusive` and `#10-as-little-design-as-possible`. +3. **Principles #3 and #6 — assign semantics by treatment, not red alone:** use + white fill for selection, a red rule/slash for active state, solid red only + for destructive/error, and pulse only for urgency; fix white-on-red small text. + Evidence: `01-evidence.md#3-aesthetic` and `#6-honest`. +4. **Principles #3 and #7 — move the chaos to macro-composition:** keep clean + module geometry, but vary panel silhouette, title placement, and scale by + context instead of repeating the same framed HUD grammar everywhere. Evidence: + `01-evidence.md#3-aesthetic` and `#7-long-lasting`. +5. **Principles #2 and #9 — add a calm mode:** stop idle equalizer/pulse/marquee + motion when reduced motion is requested and reduce polling when a detail panel + is closed. Evidence: `01-evidence.md#2-useful` and `#9-environmentally-friendly`. + +This is a hierarchy redesign, not an instruction to discard the palette, +textures, panel engine, services, or current functionality. diff --git a/DESIGN-IS-2026-08-11/04-handoff-prompt.md b/DESIGN-IS-2026-08-11/04-handoff-prompt.md new file mode 100644 index 0000000..cb50f83 --- /dev/null +++ b/DESIGN-IS-2026-08-11/04-handoff-prompt.md @@ -0,0 +1,41 @@ +# Planning handoff + +```text +/make-plan Redesign the Takemi Persona 5 desktop shell. Current design failed audit at 15/30 with critical gaps in principles #4 understandable, #5 unobtrusive, #8 thorough, #9 environmentally friendly, and #10 as little design as possible. + +Verdict paragraph: +> The strict Rams verdict is REDESIGN (15/30): preserve the excellent brand tokens and working shell primitives, but redesign the hierarchy so permanent, transient, and cinematic surfaces no longer compete at the same intensity. + +Why redesign and not refine: the total is below the 20-point refine threshold because the same high-intensity visual and interaction grammar spans permanent chrome, transient controls, and full-screen scenes. + +Preserve from current design: +- Crimson/black/white palette, type scale foundation, and motion timings in quickshell/config/Theme.qml:19-148. +- Single-slab bar/rail architecture in quickshell/bar/Bar.qml:8-63 and quickshell/bar/Rail.qml:9-64. +- Reusable P5Panel geometry/texture engine in quickshell/components/P5Panel.qml:7-248. +- Existing services and feature coverage; this is an information/design hierarchy change, not a functionality cut. + +Discard: +- One-intensity-fits-all use of stripes, halftone, split type, slashes, and repeated framed geometry. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#5-unobtrusive. Caused failure on principles #5 and #10. +- Undisclosed multi-button/multi-gesture behavior on icon-only permanent controls. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#4-understandable. Caused failure on principles #4 and #8. + +Top moves: +1. Principles #4 and #8 — make behavior legible: give every icon one primary click action, add concise Persona-styled hover labels, expose secondary actions inside its pop-out, and add real focus/disabled/accessibility states. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#4-understandable and #8-thorough. +2. Principles #5 and #10 — introduce three intensity tiers: quiet permanent chrome, expressive transient panels, theatrical full-screen scenes; reserve stripes, large split type, and strong halftone for the latter two. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#5-unobtrusive and #10-as-little-design-as-possible. +3. Principles #3 and #6 — assign semantics by treatment, not red alone: use white fill for selection, a red rule/slash for active state, solid red only for destructive/error, and pulse only for urgency; fix white-on-red small text. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#3-aesthetic and #6-honest. +4. Principles #3 and #7 — move the chaos to macro-composition: keep clean module geometry, but vary panel silhouette, title placement, and scale by context instead of repeating the same framed HUD grammar everywhere. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#3-aesthetic and #7-long-lasting. +5. Principles #2 and #9 — add a calm mode: stop idle equalizer/pulse/marquee motion when reduced motion is requested and reduce polling when a detail panel is closed. Evidence: DESIGN-IS-2026-08-11/01-evidence.md#2-useful and #9-environmentally-friendly. + +Redesign priorities: +1. Principle #4 — every visible control communicates its primary action without memorized mouse-button lore. +2. Principles #5/#10 — visual intensity increases deliberately from chrome to pop-out to modal. +3. Principles #3/#6 — color, fill, outline, pattern, and motion each have one stable semantic job. + +Deliverables: +- New information architecture, not derived mechanically from the old layout. +- New labeled primary flow compared side-by-side with the current shell. +- Empty/loading/error/success/focus/disabled state matrix. +- Migration path that preserves existing service behavior and keybinds. +- Cutover criteria based on discoverability, contrast, reduced motion, and visual hierarchy. + +Guard against porting the old structure under new styling, running both hierarchies indefinitely, following generic gaming-HUD trends, or discarding the preserved brand tokens and primitives. +``` diff --git a/hypr/lid_state.conf b/hypr/lid_state.conf index e8e4907..8b13789 100644 --- a/hypr/lid_state.conf +++ b/hypr/lid_state.conf @@ -1 +1 @@ -monitor=eDP-1, disable + diff --git a/hypr/modules/keybinds.conf b/hypr/modules/keybinds.conf index cecd5fb..578ce7e 100644 --- a/hypr/modules/keybinds.conf +++ b/hypr/modules/keybinds.conf @@ -14,7 +14,7 @@ bindr = ALT, z, exec, ~/.config/hypr/scripts/replay-ctrl.sh toggle bindr = ALT SHIFT, c, exec, ~/.config/hypr/scripts/droidcam-ctrl.sh toggle # general binds -bind = $mainMod, M, exec, pdfs-prompt +bind = $mainMod, M, exec, pdfs-prompt --dmenu --menu 'fuzzel --dmenu --width 60' bind = $mainMod, B, exec, fluxo bt menu bind = $mainMod, RETURN, exec, $terminal bind = $mainMod SHIFT, Q, killactive, diff --git a/plans/01-takemi-design-hierarchy.md b/plans/01-takemi-design-hierarchy.md new file mode 100644 index 0000000..edffa5f --- /dev/null +++ b/plans/01-takemi-design-hierarchy.md @@ -0,0 +1,503 @@ +# Takemi design hierarchy redesign + +## Outcome + +Turn the existing Persona 5 shell into a three-level visual system: + +1. **Chrome** — quiet, glanceable, and subordinate to applications. +2. **Transient** — expressive pop-outs with distinct compositions and obvious controls. +3. **Cinematic** — theatrical power and lock scenes with focused hierarchy. + +Preserve the crimson/black/white palette, services, keybinds, one-slab bar/rail, +`P5Panel`, session-lock security, and current functional coverage. This plan changes +visual hierarchy and interaction clarity, not the desktop stack. + +## Non-goals + +- Do not replace Quickshell, Hyprland, Fluxo, PAM, or the service layer. +- Do not add new accent hues or generic glass/blur cards. +- Do not reproduce Persona 5 screens literally; keep this a usable desktop shell. +- Do not build a second “control center” that duplicates every existing pop-out. +- Do not rewrite tray-menu recursion or the popup coordinator unless verification + exposes a concrete defect. + +## Definition of done + +- Permanent chrome remains readable but no longer competes with application content. +- Each primary rail icon has one obvious click action and a concise hover label. +- Selected, active, destructive, error, and urgent states are distinguishable by + treatment as well as color. +- All small text meets WCAG AA; black-on-crimson replaces white-on-crimson where needed. +- Pinned pop-outs and modal controls are keyboard reachable and visibly focused. +- Power actions that end the session require a deliberate confirmation step. +- Lock has idle, editing, busy, failure, and visible success compositions. +- Reduced motion survives process restarts and stops all nonessential loops. +- `qmllint`, live reload, interaction checks, and 1920×1080 plus 1366×768 visual + captures pass without new warnings. + +--- + +## Phase 0 — API and pattern lock + +### Allowed APIs + +- **Quickshell 0.3.0 `PopupWindow`:** `visible`, `anchor`, `grabFocus`, `screen`. + Configure placement through `anchor.item/window`, `edges`, `gravity`, `margins`, + `adjustment`; call `anchor.updateAnchor()` when a visible trigger moves. + [Official PopupWindow docs](https://quickshell.org/docs/v0.3.0/types/Quickshell/PopupWindow/) + and [PopupAnchor docs](https://quickshell.org/docs/v0.3.0/types/Quickshell/PopupAnchor/). +- **Popup bounds:** combine `PopupAdjustment.Flip | Slide | Resize` where content + can genuinely resize. Use `QsWindow.mask: Region { item: ... }` for transparent + overshoot bleed only after testing transformed entry animation. + [PopupAdjustment](https://quickshell.org/docs/v0.3.0/types/Quickshell/PopupAdjustment/), + [Region](https://quickshell.org/docs/v0.3.0/types/Quickshell/Region/). +- **Pinned dismissal:** current `HyprlandFocusGrab.active/windows/onCleared` is valid + only after a visible window exists (`components/Popout.qml:83-97`). Do not combine + it casually with `PopupWindow.grabFocus`; choose one owner per opening mode. + [HyprlandFocusGrab](https://quickshell.org/docs/v0.3.0/types/Quickshell.Hyprland/HyprlandFocusGrab/). +- **Durable preferences:** `FileView` + `JsonAdapter`, using + `Quickshell.statePath("design-preferences.json")`, `watchChanges`, `reload()`, and + `writeAdapter()`. `PersistentProperties` survives reload only, not restart. + [FileView](https://quickshell.org/docs/v0.3.0/types/Quickshell.Io/FileView/), + [JsonAdapter](https://quickshell.org/docs/v0.3.0/types/Quickshell.Io/JsonAdapter/). +- **Accessibility:** `Accessible.role/name/description/focusable/focused/pressed`, + `Accessible.onPressAction`, `onIncreaseAction`, and `onDecreaseAction`; decorative + duplicates use `Accessible.ignored: true`. + [Qt Accessible](https://doc.qt.io/qt-6/qml-qtquick-accessible.html). +- **Focus:** `FocusScope`, `focusPolicy: Qt.StrongFocus`, `activeFocusOnTab`, + `KeyNavigation`, `Keys`, and `forceActiveFocus(Qt.PopupFocusReason)`. + [Qt focus model](https://doc.qt.io/qt-6/qtquick-input-focus.html). +- **Disabled controls:** disable the root `Item`; this suppresses pointer and keyboard + input and propagates to children. Explicitly bind `HoverHandler.enabled` when a + disabled control must not react visually. + [Qt Item](https://doc.qt.io/qt-6/qml-qtquick-item.html). +- **System contrast:** + `Application.styleHints.accessibility.contrastPreference === Qt.HighContrast`. + Qt 6.11 has no documented reduced-motion preference, so motion policy is owned by + `Preferences`. +- **Secure lock:** retain `WlSessionLock`/`WlSessionLockSurface` in + `lock/Lock.qml:12-28`; `Overlay + Exclusive` is not a secure replacement. + [WlSessionLock](https://quickshell.org/docs/v0.3.0/types/Quickshell.Wayland/WlSessionLock/). + +### Repository patterns to copy + +- Copy singleton structure from `config/Theme.qml:1-16` and `services/Actions.qml:1-10`. +- Copy popup show/hide ownership from `components/Popout.qml:58-116,150-190`. +- Copy keyboard roving selection from `overlays/PowerMenu.qml:242-261`. +- Copy development-safe lock preview boundary from `lock/Lock.qml:7-27` and + `lockpreview.qml`; never style by repeatedly locking the real session. +- Copy one-slab chrome composition from `bar/Bar.qml:40-63` and `bar/Rail.qml:43-64`. + +### Verification + +- Record `qs --version` and Qt version in the implementation PR: expected Quickshell + 0.3.0 and Qt 6.11.1. +- Run `qmllint` on each copied API in a minimal changed component before broad rollout. +- Use `qs ipc show` to confirm existing IPC surface before adding typed functions. + +### Guards + +- No deprecated `PopupWindow.parentWindow`, `relativeX`, or `relativeY`. +- No direct `QsWindow.width/height`; use implicit sizes. +- No `WlrKeyboardFocus.Exclusive` as a lock substitute. +- No stock `QsMenuAnchor` or default Qt tooltip visuals inside the custom theme. +- No claimed system reduced-motion detection; it does not exist in the documented API. + +--- + +## Phase 1 — Establish the semantic design foundation + +### 1.1 Durable preferences + +Create `quickshell/config/Preferences.qml` using the Phase 0 `FileView`/`JsonAdapter` +pattern. Persist: + +- `reducedMotion: false` +- `tooltips: true` +- `highIntensityChrome: false` only as a migration escape hatch; default remains calm + +Expose typed setters through the existing `IpcHandler` in +`services/Actions.qml:60-68`, such as `setReducedMotion(enabled: bool): void`. Do not +build a settings UI in this phase. + +### 1.2 Three explicit surface presets + +- Add `components/ChromeSurface.qml`: a thin `P5Panel` preset with opaque flat fill, + one keyline, no halftone, no drop, and an active slash only when requested. +- Refine `popouts/PopoutSurface.qml:7-23` as the transient preset: halftone becomes + opt-in by composition rather than present on every panel; retain the hard offset + and stronger border. +- Keep `components/P5Backdrop.qml` cinematic-only. Bar, rail, tooltips, cards, and OSD + must not instantiate it. + +Do not add a generic numeric “intensity” prop to `P5Panel`; explicit presets make +the hierarchy visible in code and prevent arbitrary intermediate tiers. + +### 1.3 Semantic treatments + +Document and encode in `config/Theme.qml`: + +| State | Fill | Mark | Text | Motion | +|---|---|---|---|---| +| Rest | surface | neutral keyline | white | none | +| Hover | surface | short red wipe | white | one short lunge | +| Focus/selected | white | black keyline | black | snap once | +| Active | surface | persistent red slash | white | none | +| Destructive/error | solid red | black keyline | black | shake only on error | +| Urgent | surface | red pulse mark | white | bounded pulse | +| Disabled | surface | muted outline | muted | none | + +Replace small white-on-crimson labels with black-on-crimson; measured contrast improves +from 3.68:1 to 5.70:1. Do not add a new status color. + +### 1.4 Base-control contract + +- Convert `components/P5Button.qml` to a `FocusScope` or focusable root with one + activation path shared by `TapHandler`, Enter, Space, and + `Accessible.onPressAction`. Add `description`, visible focus state, and a real + disabled state. +- Replace `P5Slider.enabledControl` with root `enabled` and update its call sites. + Gate tap, drag, hover, accessible increase/decrease, and left/right keys through + the same enabled root. +- Add semantic names and roles to `RailIcon`, `StatusIcon`, notification actions, + media transport, calendar navigation, and tray-menu rows. Mark split-text ghosts, + masks, texture layers, and duplicate glyphs ignored. + +### Verification + +- Focus every base control with Tab and confirm a visible non-color-only state. +- Trigger each through pointer, Enter/Space, and its accessible action; assert one + signal per activation. +- Disable each control and assert no `clicked`/`moved` signal and no hover animation. +- Recompute all foreground/background contrast pairs; normal text must be ≥4.5:1. +- Restart Quickshell and confirm preferences persist. + +### Guards + +- Do not treat `Accessible` metadata as keyboard support; implement both. +- Do not attach semantics to every decorative child. +- Do not use `MouseArea.enabled` as the component-wide disabled gate. +- Do not spread raw state colors through callers; state treatment belongs in base controls. + +--- + +## Phase 2 — Quiet and clarify permanent chrome + +### 2.1 Bar + +Update `bar/Bar.qml` and `bar/BarPill.qml` to use `ChromeSurface` and remove all +always-on texture. Keep the single slab and hairline dividers. + +- `bar/Clock.qml`: remove permanent seconds; reveal them only inside Calendar. +- `bar/Resources.qml`: render tiny `CPU`, `MEM`, and `GPU` labels beside values so + `2.6G` is not contextless; remove the currently dead `Metric.label` path only after + its visible replacement exists. +- `bar/MediaPill.qml`: keep title/artist but replace the three continuously animated + equalizer bars with one static play-state mark in normal mode. Allow the loop only + when high-intensity chrome is explicitly enabled and reduced motion is off. +- Preserve active-window elision and current collision calculation in + `bar/Bar.qml:90-96`. + +### 2.2 Rail interaction grammar + +Make left click mean **open/pin details** for network, Bluetooth, audio, battery, +notifications, and power. Remove undisclosed right/middle-click state changes from +`bar/Rail.qml:76-132,151-171`; move those actions into their corresponding pop-outs. + +Retain scrolling only for volume, microphone, and brightness because it directly +maps to a continuous readout. Mention that gesture in the tooltip. + +### 2.3 Persona tooltip + +Add `components/P5Tooltip.qml` as a small anchored `PopupWindow`, not a stock Qt +tooltip. Copy anchor/delay/hide behavior from `components/Popout.qml`, but use: + +- calm `ChromeSurface` +- 350–450ms delay +- one-line name + current state +- optional second micro-line such as `SCROLL TO ADJUST` +- no focus grab, no halftone, no scale loop +- `PopupAdjustment.Flip | Slide` + +Add `label` and `hint` to `RailIcon`; tooltips honor `Preferences.tooltips` and never +become the only source of an accessible name. + +### 2.4 Pinned keyboard opening + +Add explicit `openHover()` and `openPinned()` paths to `components/Popout.qml`. +`openPinned()` must set its focus mode before mapping; if a hover popup is already +visible, close and remap rather than assuming a live `grabFocus` change works. Start +with Audio as the vertical slice. Choose either `PopupWindow.grabFocus` or the current +`HyprlandFocusGrab` for pinned dismissal after runtime comparison; never leave both +owning the same close event. + +### Verification + +- Screenshot clean desktop against both a quiet and high-contrast wallpaper. +- First-time test: identify every rail icon and predict left click from tooltip. +- Verify no secondary pointer action remains unless it is printed in the tooltip. +- Open Audio by hover and click: hover must remain passive; pinned mode must accept + keyboard focus, Esc-close, and outside-click close exactly once. +- Move/rescale the rail while a popup is open and call `anchor.updateAnchor()` from + the actual geometry-change path if it drifts. + +### Guards + +- Do not reintroduce separate floating cards into bar or rail. +- Do not increase bar/rail footprint. +- Do not put stripes, halftone, or large display text in permanent chrome. +- Do not make a tooltip interactive. + +--- + +## Phase 3 — Give transient surfaces distinct macro-compositions + +Retain shared popup lifecycle, width discipline, coordinator, header baseline, and +body controls. Redesign one family at a time; capture and approve each family before +moving on so a bad motif is not copied eight times. + +### 3.1 Control pop-outs + +- **Audio (`AudioPopout.qml`)** — make current volume the headline number; output and + microphone form two clear bands; applications become a restrained mixer list. + Put Mute and `Open audio mixer` inside the panel. +- **Network (`NetPopout.qml`)** — dossier composition: current connection/IP at top, + download/upload fully named, network list as clipped paper rows. Use action labels + `Turn Wi-Fi on/off` and `Network settings`. +- **Bluetooth (`BtPopout.qml`)** — share list-row mechanics with Network but not its + exact silhouette. Use action copy `Turn on/off`, `Start/stop scanning`; pair/connect + status must not rely on color alone. +- **Battery (`BatteryPopout.qml`)** — oversized percentage crossing one edge, with + `Plugged in`/`On battery`, remaining time, draw, and health ordered underneath. + Keep Suspend as the one explicit action. + +### 3.2 Information pop-outs + +- **Media (`MediaPopout.qml`)** — let album art define one side of the silhouette; + keep metadata in a solid legibility zone; label transport actions accessibly. +- **Calendar (`CalendarPopout.qml`)** — large selected day/month as the composition, + grid subordinate; move seconds here from the permanent bar; label previous/next. +- **Notifications (`NotifPopout.qml`, `NotifCard.qml`)** — overlapping-paper rhythm + only at the list level, not on every inner action. Rename `n HELD` to `n IN HISTORY`; + make DND copy an action (`Silence notifications` / `Resume notifications`). +- **System (`SysPopout.qml`)** — treat it as a diagnostic poster: four primary metrics + first, hardware detail second. Expand `PROCS`, `MEM`, `VRAM`, `Root`, and label load + averages `1 / 5 / 15 min`. + +### 3.3 Shared transient rules + +- `PopoutHeader` supplies alignment and focus traversal, not the full silhouette. +- Only one dominant numeral/image/headline per panel. +- Maximum one halftone region and one red slash per panel. +- Lists use consistent 32–40px targets and explicit selected/focused/disabled states. +- Empty and loading layouts preserve the panel silhouette instead of collapsing. +- Fix muted OSD honesty: show stored percentage beside `MUTED`, or omit percentage; + never display a fabricated 0%. + +### Verification + +- Capture all eight pop-outs at rest plus empty/loading/error/disabled states where applicable. +- Test with longest realistic SSID, device, track, app, and notification strings. +- Verify list focus order, Enter/Space activation, Esc close, and disabled-row skipping. +- Ensure only one transient window stays open through a rapid rail sweep. +- Test popup bounds at every screen edge and both target resolutions. + +### Guards + +- Do not create eight copies of the same card grid. +- Do not vary edge angle randomly inside one surface. +- Do not use display italic for body copy or dense device lists. +- Do not put action wording in state form (`Wi-Fi On`) when clicking performs the inverse. + +--- + +## Phase 4 — Focus the cinematic scenes + +### 4.1 Power menu + +In `overlays/PowerMenu.qml`: + +- Keep the left headline/right action-stack asymmetry. +- Increase backdrop opacity enough that application text no longer competes. +- Use one stripe band or wedge, not a full-screen stripe field plus every other motif. +- Separate Lock/Suspend/Hibernate from Log Out/Reboot/Shut Down with a labeled break + and greater spacing. +- Keep white fill/black text for normal selection. Use solid crimson/black text for + destructive selection. +- Add `pendingAction`: the first activation of Log Out/Reboot/Shut Down replaces the + action region with `CONFIRM LOG OUT`, `CONFIRM REBOOT`, or `CONFIRM SHUT DOWN` + plus Cancel; second activation executes. + Escape clears confirmation first and closes the menu only when no action is pending. +- Keep roving Up/Down/J/K navigation and update focus order for confirmation. + +### 4.2 Lock screen + +Refactor `lock/LockFace.qml` into explicit visual states while retaining the single +PAM context and `WlSessionLock` shell: + +- **Idle:** clock is dominant; status is quiet; auth field is present but subordinate. +- **Editing:** first user edit shifts emphasis to auth and dims/moves clock/status. +- **Busy:** `CHECKING…`, disabled input, bounded sweep. +- **Failure:** `PASSWORD INCORRECT` or PAM message, solid error treatment, one shake. +- **Success:** paint a short `ACCESS GRANTED` state before unlocking; skip delay under + reduced motion. + +Replace `WHO GOES THERE`, `WRONG`, and `PAM ERROR` with explicit copy. Resolve the +username safely from `USER` then `LOGNAME`; if both are empty, do not call PAM with a +literal placeholder—show `AUTHENTICATION USER UNAVAILABLE`. + +Keep responsive scaling, per-monitor wallpaper, and `lockpreview.qml`. Ensure the +password input exposes `Accessible.EditableText`, name `Password`, `editable: true`, +and `passwordEdit: true`. + +### Verification + +- Power: pointer and keyboard through safe action, destructive confirmation, cancel, + outside click, and repeated open/close. +- Lock preview: capture all five states at 1920×1080 and 1366×768. +- Real lock test only after preview and PAM-path review; test wrong password, correct + password, empty username environment simulation, and Quickshell reload behavior. +- Confirm `WlSessionLock.secure` path remains unchanged. + +### Guards + +- Never execute destructive action on the first activation. +- Never fake authentication success in preview or bypass PAM. +- Never replace session lock with a focus-exclusive overlay. +- Do not let theatrical copy obscure what the user must do. + +--- + +## Phase 5 — Motion, contrast, and runtime calm + +### 5.1 Motion policy + +In `Theme.qml`, derive `motionScale` from `Preferences.reducedMotion`. Apply it to: + +- every `Behavior` +- popup enter/exit sequences +- media equalizer and marquee loops +- icon pulses +- toast countdown/entry +- OSD, power, and lock scene transitions + +When reduced motion is on, set final state directly or disable `Behavior`; do not merely +shorten loops to 1ms. Essential progress indicators may remain static with clear text. + +### 5.2 High contrast + +Read `Application.styleHints.accessibility.contrastPreference`. In high contrast: + +- increase keyline/border separation +- disable low-opacity texture behind text +- retain shape/icon differences among selection, active, danger, and urgency +- do not change the three-color identity + +### 5.3 Polling cleanup + +After visual behavior is stable, move `sysReader` from the 2-second batch to the +20-second batch in `services/Sys.qml:78-102`; uptime/load/process count do not drive +permanent animation. Keep CPU/MEM/GPU at 2 seconds because the bar renders them. +Measure before making further demand-driven polling changes; do not introduce a +consumer-count abstraction without evidence that it materially reduces work. + +### Verification + +- Restart with reduced motion and verify zero infinite animations via source grep and + live observation; all final states must still appear. +- Toggle high contrast and re-run contrast checks. +- Compare Fluxo process-launch frequency before/after for 60 seconds. +- Confirm notification timing, popup dismissal, and lock success still complete when + animation is disabled. + +### Guards + +- Do not claim to follow an OS reduced-motion setting. +- Do not gate functional timers such as notification lifetime or polling behind motion. +- Do not add blur, gradients, or extra texture to compensate for removed motion. + +--- + +## Phase 6 — Integrated verification and cutover + +### Automated checks + +1. `qmllint -I quickshell` on every changed QML file individually; the current tool can + exit 255 without diagnostics on some recursive/singleton batches, so record files + and results rather than treating one giant invocation as authoritative. +2. `git diff --check`. +3. Grep for: + - white small text on solid accent + - `enabledControl` + - pointer handlers without an accessible name on shared controls + - infinite animations not gated by reduced motion + - deprecated popup properties + - old copy: `WHO GOES THERE|PAM error|HELD|PROCS|VRAM` +4. Reload through `qs ipc call shell reloadConfig` and inspect `qs log`. + +### Interaction matrix + +Test each bar/rail/pop-out action with pointer, keyboard where focusable, outside-click, +Esc, disabled state, and service unavailable. Specifically test tray menus with nested +submenus, Wi-Fi off, no Bluetooth adapter, no battery, no player, no notifications, +and missing album art. + +### Visual matrix + +Capture: + +- 1920×1080 and 1366×768 +- calm and high-contrast wallpaper +- default, reduced-motion, and high-contrast preferences +- desktop, every pop-out, tray menu, OSD, toast, power normal/confirm, and all lock states + +Compare against these invariants: + +- Chrome has no texture and never dominates content. +- Transient panels each have one dominant composition. +- Cinematic scenes retain the strongest typography and atmosphere. +- Nothing important is encoded by color alone. +- No text clips, crosses a cut edge, or falls below contrast requirements. + +### Cutover sequence + +Implement in six reviewable commits matching Phases 1–6. Do not delete the old visual +path until its phase passes screenshots and interaction tests. Remove migration aliases +(`enabledControl`, high-intensity escape hatch) only in the final cleanup commit after +all call sites and preferences have migrated. + +## Expected file map + +### New + +- `quickshell/config/Preferences.qml` +- `quickshell/components/ChromeSurface.qml` +- `quickshell/components/P5Tooltip.qml` + +### Foundation edits + +- `quickshell/config/Theme.qml` +- `quickshell/components/P5Panel.qml` +- `quickshell/components/P5Button.qml` +- `quickshell/components/P5Slider.qml` +- `quickshell/components/Popout.qml` +- `quickshell/components/Icon.qml` +- `quickshell/components/Marquee.qml` +- `quickshell/services/Actions.qml` + +### Surface edits + +- `quickshell/bar/{Bar,BarPill,Rail,RailIcon,StatusIcon,Clock,Resources,MediaPill,Tray}.qml` +- `quickshell/popouts/{PopoutSurface,PopoutHeader,AudioPopout,NetPopout,BtPopout,BatteryPopout,MediaPopout,CalendarPopout,NotifPopout,SysPopout,TrayMenuList}.qml` +- `quickshell/components/NotifCard.qml` +- `quickshell/overlays/{Osd,Toasts,PowerMenu}.qml` +- `quickshell/lock/LockFace.qml` +- `quickshell/services/Sys.qml` + +## Recommended execution order + +Execute Phase 1 first and review its state matrix before any visual restyling. Then use +Audio as the full vertical slice through Phases 2–3. Once its tooltip, pinned focus, +semantic states, reduced motion, and transient composition pass, repeat that proven +pattern across the remaining surfaces. Leave real session-lock testing until the lock +preview and power confirmation work are complete. diff --git a/quickshell/README.md b/quickshell/README.md index 9261fa0..a9512ab 100644 --- a/quickshell/README.md +++ b/quickshell/README.md @@ -80,7 +80,13 @@ contentPad * 2`), which keeps the shape maths out of binding loops. | CPU/MEM/GPU | bar | btop | | | | resource dashboard | Popouts open on a short dwell and stay while the pointer is over the trigger or -the panel. Clicking pins one open. +the panel. Clicking pins one open. A shell-wide coordinator keeps only one +transient surface mapped at a time, including tray context menus, so sweeping +across the rail cannot stack several animated panels over one another. + +The power menu is fully keyboard-driven as well as clickable: `Up`/`Down` or +`J`/`K` moves the active slab, `Enter`/`Space` confirms it, and `Escape` +dismisses the screen. ## Keybinds diff --git a/quickshell/components/P5Button.qml b/quickshell/components/P5Button.qml index 8890547..1aff322 100644 --- a/quickshell/components/P5Button.qml +++ b/quickshell/components/P5Button.qml @@ -11,13 +11,14 @@ Item { property color accent: Theme.primary property color hoverFill: Theme.accent property bool destructive: false + property bool selected: false property real lean: Theme.skew property real lunge: 6 property bool wide: false signal clicked() - readonly property bool hovered: hover.hovered + readonly property bool hovered: hover.hovered || root.selected readonly property bool pressed: tap.pressed implicitWidth: wide ? 260 : (row.implicitWidth + panel.contentPad * 2) diff --git a/quickshell/components/Popout.qml b/quickshell/components/Popout.qml index 1dd9140..b112fc7 100644 --- a/quickshell/components/Popout.qml +++ b/quickshell/components/Popout.qml @@ -103,6 +103,7 @@ PopupWindow { } function open() { + PopupCoordinator.claim(root); exitAnim.stop(); shown = true; enterAnim.restart(); @@ -183,6 +184,7 @@ PopupWindow { root.pinned = false; contentHolder.x = root.bleed; contentHolder.y = root.bleed; + PopupCoordinator.release(root); root.dismissedFully(); } } diff --git a/quickshell/config/PopupCoordinator.qml b/quickshell/config/PopupCoordinator.qml new file mode 100644 index 0000000..1ea22e6 --- /dev/null +++ b/quickshell/config/PopupCoordinator.qml @@ -0,0 +1,27 @@ +pragma Singleton + +import QtQuick +import Quickshell + +// One transient surface at a time. Without a shared owner, a quick sweep over +// the bar/rail can leave several independently animated windows mapped on top +// of each other. Every coordinated surface exposes dismiss(). +Singleton { + property var active: null + + function claim(surface: var) { + if (active && active !== surface) + active.dismiss(); + active = surface; + } + + function release(surface: var) { + if (active === surface) + active = null; + } + + function dismissAll() { + if (active) + active.dismiss(); + } +} diff --git a/quickshell/lock/LockFace.qml b/quickshell/lock/LockFace.qml index bda8486..5e964a5 100644 --- a/quickshell/lock/LockFace.qml +++ b/quickshell/lock/LockFace.qml @@ -20,6 +20,8 @@ Item { property string status: "" property bool failed: false property bool busy: pam.active + readonly property real uiScale: Math.max(0.72, Math.min(1.15, + Math.min(width / 1920, height / 1080))) Rectangle { anchors.fill: parent @@ -137,7 +139,9 @@ Item { anchors.top: parent.top anchors.topMargin: parent.height * 0.2 spacing: Theme.padM - width: 300 + width: Math.min(300, parent.width * 0.28) + scale: surface.uiScale + transformOrigin: Item.TopRight RailRow { width: parent.width @@ -181,8 +185,10 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: parent.height * 0.14 - width: 460 + width: Math.min(460, parent.width - Theme.padXL * 2) height: 150 + scale: surface.uiScale + transformOrigin: Item.Bottom transform: Translate { id: shakeShift } @@ -206,7 +212,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: prompt.bottom anchors.topMargin: Theme.padL - width: 380 + width: Math.min(380, authBlock.width - Theme.padL * 2) height: 56 lean: 0.12 chop: 16 @@ -320,7 +326,7 @@ Item { Text { anchors.verticalCenter: parent.verticalCenter - text: Quickshell.env("USER") || "narl" + text: Quickshell.env("USER") || "USER" color: Theme.muted font.family: Theme.fontMono font.pixelSize: Theme.fsSmall @@ -349,7 +355,7 @@ Item { id: pam config: "hyprlock" - user: Quickshell.env("USER") || "narl" + user: Quickshell.env("USER") || "USER" onPamMessage: { if (pam.responseRequired) pam.respond(surface.entry); diff --git a/quickshell/overlays/PowerMenu.qml b/quickshell/overlays/PowerMenu.qml index 77c6e04..a641dab 100644 --- a/quickshell/overlays/PowerMenu.qml +++ b/quickshell/overlays/PowerMenu.qml @@ -26,9 +26,21 @@ PanelWindow { exclusionMode: ExclusionMode.Ignore readonly property bool open: Actions.powerMenuOpen + property int selectedAction: 0 + + readonly property var actionModel: [ + { label: "Lock", icon: "󰌾", danger: false, act: "lock" }, + { label: "Suspend", icon: "󰤄", danger: false, act: "suspend" }, + { label: "Hibernate", icon: "󰒲", danger: false, act: "hibernate" }, + { label: "Log Out", icon: "󰗽", danger: true, act: "logout" }, + { label: "Reboot", icon: "󰜉", danger: true, act: "reboot" }, + { label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" } + ] onOpenChanged: { if (open) { + PopupCoordinator.dismissAll(); + selectedAction = 0; openAnim.restart(); } else { closeAnim.restart(); @@ -122,14 +134,7 @@ PanelWindow { Repeater { id: buttonRepeater - model: [ - { label: "Lock", icon: "󰌾", danger: false, act: "lock" }, - { label: "Suspend", icon: "󰤄", danger: false, act: "suspend" }, - { label: "Hibernate", icon: "󰒲", danger: false, act: "hibernate" }, - { label: "Log Out", icon: "󰗽", danger: true, act: "logout" }, - { label: "Reboot", icon: "󰜉", danger: true, act: "reboot" }, - { label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" } - ] + model: root.actionModel // A carrier item owns the staggered slide so the button keeps its // own hover transform. @@ -159,10 +164,12 @@ PanelWindow { text: entry.modelData.label icon: entry.modelData.icon destructive: entry.modelData.danger + selected: root.selectedAction === entry.index accent: entry.modelData.danger ? Theme.accent : Theme.primary lean: Theme.skew lunge: -10 onClicked: root.invoke(entry.modelData.act) + onHoveredChanged: if (hovered) root.selectedAction = entry.index } // Staggered entry, one button after another. @@ -211,7 +218,7 @@ PanelWindow { anchors.bottom: parent.bottom anchors.bottomMargin: 40 anchors.horizontalCenter: parent.horizontalCenter - text: "ESC TO CANCEL" + text: "↑ ↓ SELECT ENTER CONFIRM ESC CANCEL" color: Theme.muted font.family: Theme.fontMono font.pixelSize: Theme.fsMicro @@ -237,6 +244,21 @@ PanelWindow { anchors.fill: parent focus: root.open Keys.onEscapePressed: Actions.closePowerMenu() + Keys.onPressed: event => { + if (!root.open) return; + if (event.key === Qt.Key_Up || event.key === Qt.Key_K) { + root.selectedAction = (root.selectedAction + root.actionModel.length - 1) + % root.actionModel.length; + event.accepted = true; + } else if (event.key === Qt.Key_Down || event.key === Qt.Key_J) { + root.selectedAction = (root.selectedAction + 1) % root.actionModel.length; + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter + || event.key === Qt.Key_Space) { + root.invoke(root.actionModel[root.selectedAction].act); + event.accepted = true; + } + } } ParallelAnimation { diff --git a/quickshell/popouts/TrayMenu.qml b/quickshell/popouts/TrayMenu.qml index a3cf399..7e42b65 100644 --- a/quickshell/popouts/TrayMenu.qml +++ b/quickshell/popouts/TrayMenu.qml @@ -53,6 +53,7 @@ PopupWindow { function open() { if (!menuHandle) return; + PopupCoordinator.claim(root); closeAnim.stop(); shown = true; openAnim.restart(); @@ -68,6 +69,11 @@ PopupWindow { if (shown) close(); else open(); } + // Shared name used by PopupCoordinator for every transient surface. + function dismiss() { + close(); + } + // The grab can only take hold once the popup is actually mapped. Asked for // in the same tick as `shown`, Hyprland has no surface to hand it and // clears it immediately — which closed the menu the instant it opened. @@ -145,6 +151,7 @@ PopupWindow { onFinished: { root.shown = false; holder.x = root.bleed; + PopupCoordinator.release(root); } } } diff --git a/quickshell/popouts/TrayMenuList.qml b/quickshell/popouts/TrayMenuList.qml index 83f73a3..4eab649 100644 --- a/quickshell/popouts/TrayMenuList.qml +++ b/quickshell/popouts/TrayMenuList.qml @@ -188,7 +188,7 @@ Column { anchors.verticalCenter: parent.verticalCenter width: row.entry.hasChildren ? 18 : 0 visible: row.entry.hasChildren - text: row.expanded ? "" : "" + text: row.expanded ? "󰅀" : "󰅂" color: row.hovered ? Theme.ink : Theme.subtext font.family: Theme.fontIcon font.pixelSize: Theme.fsMicro