// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import QtQuick import ".." import "Logic.js" as Logic // The island: a sliver at the top centre of DP-1 that the two pills slide // out of. The window is a fixed overlay strip that never resizes; the pills // move inside it and the input mask follows them, so the rest of the strip // is click-through. PanelWindow { id: win required property QtObject sound // the sound module's Service required property QtObject drawer // Drawer, for the sound page link screen: Quickshell.screens.find(s => s.name === "DP-1") ?? Quickshell.screens[0] anchors { top: true; left: true; right: true } implicitHeight: 360 color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell-island" WlrLayershell.keyboardFocus: panel !== "" ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None // "", "sound", "controls" or "calendar". property string panel: "" // --- reveal and hide ------------------------------------------------- readonly property bool hovered: sliverHit.containsMouse || clock.hovered || status.hovered property bool shown: false // A tray menu is a separate popup: moving onto it un-hovers the pills, // and the hide timer would then hide the island under the open menu. The // menu cannot report its own close through display(), so the hold ends // the next time the pointer comes back to the island. // ponytail: if the menu is dismissed elsewhere, the island may stay shown // until the next hover. property bool menuHeld: false onHoveredChanged: { if (hovered) { hideTimer.stop(); shown = true; menuHeld = false; } else if (panel === "" && !menuHeld) hideTimer.restart(); } onPanelChanged: { if (panel === "calendar") clock.focusPanel(); else if (panel !== "") keys.forceActiveFocus(); else if (!hovered) hideTimer.restart(); } // The keybind's way in: reveal, and open or close the controls panel. // Closing goes through onPanelChanged, which starts the hide timer when // the pointer is elsewhere. function toggleControls() { hideTimer.stop(); shown = true; panel = panel === "controls" ? "" : "controls"; } Timer { id: hideTimer interval: 400 onTriggered: if (!win.hovered && win.panel === "" && !win.menuHeld) win.shown = false } // 0 hidden, 1 revealed. One amount drives the split, so an interrupted // reveal turns around from wherever it is. property real reveal: shown ? 1 : 0 Behavior on reveal { NumberAnimation { duration: 280; easing.type: Easing.OutCubic } } // The clock pill also drops alone for a flick while the island is hidden. property real clockDrop: (shown || flicking) ? 1 : 0 Behavior on clockDrop { NumberAnimation { duration: 280; easing.type: Easing.OutCubic } } // --- flick ----------------------------------------------------------- // Hyprland reports the initial state as events at startup. property bool armed: false Timer { interval: 1500; running: true; onTriggered: win.armed = true } property bool flicking: false property string flickWs: "" property string flickApp: "" function flick(ws, app) { flickWs = ws; flickApp = app; flicking = true; flickTimer.restart(); } Timer { id: flickTimer; interval: 1500; onTriggered: win.flicking = false } // activeToplevel settles a moment after workspacev2 arrives. Timer { id: settle interval: 60 onTriggered: { const ws = Hyprland.focusedWorkspace; const top = Hyprland.activeToplevel; const app = top && top.workspace === ws ? (top.wayland?.appId ?? "") : ""; win.flick(ws?.name ?? "", app); } } Connections { target: Hyprland function onRawEvent(event) { if (!win.armed) return; if (event.name === "workspacev2") { settle.restart(); } else if (event.name === "openwindow") { const w = Logic.parseOpenWindow(event.data); if (!w.workspace.startsWith("special:")) win.flick(w.workspace, w.appClass); } } } // --- dismissal ------------------------------------------------------- HyprlandFocusGrab { windows: [win] active: win.panel !== "" onCleared: win.panel = "" } // Key events reach a focused item, not the window (AGENTS.md). Item { id: keys Keys.onEscapePressed: if (!(win.panel === "calendar" && clock.back())) win.panel = "" } // --- geometry -------------------------------------------------------- readonly property real gap: 10 readonly property real top: 8 readonly property real cx: width / 2 mask: Region { item: sliverHit Region { item: clock; intersection: Intersection.Combine } Region { item: status; intersection: Intersection.Combine } } // The visible sliver: neutral at rest, the capture colours while // something captures, one equal segment per active kind. Not the accent // at rest: udt snaps the accent to a palette colour, and when it lands on // green an idle sliver read exactly like a live microphone. Row { id: sliver x: win.cx - width / 2 y: 0 width: 120 height: 4 // A handle, so it gives way to the pills it reveals. Not while // something captures: then it is the only privacy indicator there is. opacity: Privacy.roles.length ? 1 : 1 - win.reveal Repeater { model: Privacy.roles.length ? Privacy.roles : ["overlay"] delegate: Rectangle { required property string modelData width: sliver.width / Math.max(1, Privacy.roles.length) height: sliver.height radius: 2 color: Theme[modelData] } } } MouseArea { id: sliverHit x: win.cx - width / 2 y: 0 width: 220 height: 10 hoverEnabled: true } // Both pills start overlapped at the sliver, above the screen edge, and // travel down and apart. At reveal 1 the clock ends gap/2 left of the // centre and the status gap/2 right of it; the status pill's left edge // stays put while its panel widens it. ClockPill { id: clock x: win.cx - width / 2 - (win.gap / 2 + width / 2) * win.reveal y: -height + (win.top + height) * win.clockDrop opacity: Math.min(1, win.clockDrop * 2) flicking: win.flicking flickWs: win.flickWs flickApp: win.flickApp panel: win.panel onOpenCalendar: win.panel = win.panel === "calendar" ? "" : "calendar" onCloseCalendar: win.panel = "" } StatusPill { id: status x: win.cx - width / 2 + (win.gap / 2 + width / 2) * win.reveal y: -height + (win.top + height) * win.reveal opacity: Math.min(1, win.reveal * 2) sound: win.sound window: win panel: win.panel onMenuShown: win.menuHeld = true onOpenSound: win.panel = win.panel === "sound" ? "" : "sound" onOpenDrawer: { win.panel = ""; win.drawer.show("sound"); } onOpenControls: win.toggleControls() onOpenPage: page => { win.panel = ""; win.drawer.show(page); } } }