import QtQuick import QtQuick.Shapes import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland import "root:/config" import "root:/components" import "root:/services" // The dashboard: a drawer that slides down out of the underside of the bar. // // The idea is caelestia's — the drawer is not a floating card but more of the // hull, so it is painted in the hull colour and joins the bar through two // concave fillets, and the porthole's rim runs round it without a break. Read // against the wallpaper it is a panel lowered from the ceiling of the ship. // // Four columns divided by hairlines: who and when, the month, the machine, // and what is playing. The clock in the bar toggles it, as does // `qs ipc call shell dashboard`; Escape or a click anywhere else puts it away. PanelWindow { id: root required property var modelData screen: modelData readonly property bool open: Actions.dashboardOpen && Actions.dashboardScreen === modelData.name // 0 = tucked under the bar, 1 = fully down. property real reveal: root.open ? 1 : 0 Behavior on reveal { NumberAnimation { duration: Theme.durSlow easing.type: root.open ? Easing.OutCubic : Easing.InCubic } } visible: root.open || root.reveal > 0 WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "takemi-dashboard" WlrLayershell.keyboardFocus: root.open ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None exclusionMode: ExclusionMode.Ignore // One pixel up, so the drawer covers the rim the hull draws along the // underside of the bar instead of sitting below it. anchors.top: true margins.top: Theme.barHeight - 1 // The fillets are the same radius as the porthole's corners, so the drawer // meets the bar the way the opening meets the frame. readonly property real fillet: Theme.frameRadius readonly property real radius: Theme.frameRadius readonly property real pad: Theme.padL readonly property real bodyWidth: columns.implicitWidth + root.pad * 2 readonly property real bodyHeight: columns.implicitHeight + root.pad * 2 implicitWidth: root.bodyWidth + root.fillet * 2 implicitHeight: root.bodyHeight + 1 color: "transparent" mask: Region { item: body } HyprlandFocusGrab { active: root.open windows: [root] onCleared: Actions.closeDashboard() } // Who is at the console. Read once; it does not change under a session. readonly property string user: Quickshell.env("USER") ?? "" FileView { id: hostFile path: "/etc/hostname" } readonly property string host: hostFile.text().trim() SystemClock { id: clock precision: SystemClock.Seconds } // Everything below slides as one sheet, clipped to the window so it // appears to come out from behind the bar rather than fading in over it. Item { anchors.fill: parent clip: true Item { id: sheet width: parent.width height: parent.height y: -height * (1 - root.reveal) // ------------------------------------------------------ silhouette Shape { id: silhouette anchors.fill: parent preferredRendererType: Shape.CurveRenderer readonly property real w: width readonly property real h: height readonly property real f: root.fillet readonly property real r: root.radius // Down the left fillet, round the bottom, up the right fillet. readonly property string edge: `M 0 0 A ${f} ${f} 0 0 1 ${f} ${f}` + ` V ${h - r} A ${r} ${r} 0 0 0 ${f + r} ${h}` + ` H ${w - f - r} A ${r} ${r} 0 0 0 ${w - f} ${h - r}` + ` V ${f} A ${f} ${f} 0 0 1 ${w} 0` ShapePath { fillColor: Theme.hull strokeColor: "transparent" PathSvg { path: silhouette.edge + " Z" } } // The rim, carried on round the drawer from the opening. ShapePath { fillColor: "transparent" strokeColor: Theme.rim strokeWidth: 1 PathSvg { path: silhouette.edge } } } // ---------------------------------------------------------- body FocusScope { id: body x: root.fillet width: root.bodyWidth height: parent.height focus: root.open Keys.onEscapePressed: Actions.closeDashboard() Row { id: columns x: root.pad y: root.pad spacing: 0 // Who and when: the house mark, the operator, a large clock. Item { implicitWidth: 230 implicitHeight: root.columnHeight Row { id: who spacing: Theme.padM Porthole { anchors.verticalCenter: parent.verticalCenter size: 30 lit: true } Column { anchors.verticalCenter: parent.verticalCenter spacing: 1 Text { text: root.user.toUpperCase() color: Theme.text font.family: Theme.fontDisplay font.pixelSize: Theme.fsBody font.weight: Theme.weightDisplay font.letterSpacing: Theme.tracking renderType: Text.NativeRendering } Text { text: root.host !== "" ? "@ " + root.host : "" color: Theme.muted font.family: Theme.fontMono font.pixelSize: Theme.fsMicro font.letterSpacing: 1 renderType: Text.NativeRendering } } } Row { id: time anchors.top: who.bottom anchors.topMargin: Theme.padL spacing: Theme.padS SplitText { id: hhmm text: Qt.formatDateTime(clock.date, "HH:mm") pixelSize: 54 weight: Font.Light letterSpacing: 2 color: Theme.accent } Text { anchors.baseline: hhmm.bottom anchors.baselineOffset: -12 text: Qt.formatDateTime(clock.date, "ss") color: Theme.muted font.family: Theme.fontMono font.pixelSize: Theme.fsBody renderType: Text.NativeRendering } } Text { anchors.top: time.bottom anchors.topMargin: Theme.padS text: Qt.formatDateTime(clock.date, "dddd · dd MMMM").toUpperCase() color: Theme.subtext font.family: Theme.fontMono font.pixelSize: Theme.fsMicro font.letterSpacing: 2 renderType: Text.NativeRendering } Column { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right spacing: Theme.padS Fact { label: "UPTIME"; value: Sys.uptime || "—" } Fact { visible: Battery.available label: "POWER" value: Math.round(Battery.percent) + "%" + (Battery.timeLabel !== "" ? " · " + Battery.timeLabel : "") } Fact { label: "NOTICES"; value: Notifs.count > 0 ? Notifs.count + " held" : "none" } } } Rule { height: root.columnHeight } // The month, Monday first. Today is the NieR cursor: an inverted square. Item { id: month implicitWidth: 238 implicitHeight: root.columnHeight // Keyed by the day, not the clock: the clock ticks every // second, and rebuilding 42 cells that often is waste. readonly property string dayKey: Qt.formatDateTime(clock.date, "yyyy-MM-dd") readonly property var cells: { const now = new Date(month.dayKey + "T12:00:00"); const y = now.getFullYear(), m = now.getMonth(); const lead = (new Date(y, m, 1).getDay() + 6) % 7; const out = []; for (let i = 0; i < 42; i++) { const d = new Date(y, m, 1 - lead + i); out.push({ day: d.getDate(), inMonth: d.getMonth() === m, today: d.toDateString() === now.toDateString() }); } return out; } SectionRule { id: monthHead anchors.left: parent.left anchors.right: parent.right text: Qt.formatDateTime(clock.date, "MMMM yyyy") } Row { id: weekdays anchors.top: monthHead.bottom anchors.topMargin: Theme.padS Repeater { model: ["MO", "TU", "WE", "TH", "FR", "SA", "SU"] Text { required property var modelData required property int index width: month.width / 7 horizontalAlignment: Text.AlignHCenter text: modelData color: index >= 5 ? Theme.subtext : Theme.muted font.family: Theme.fontMono font.pixelSize: Theme.fsMicro renderType: Text.NativeRendering } } } Grid { anchors.top: weekdays.bottom anchors.topMargin: Theme.padS columns: 7 Repeater { model: month.cells Item { required property var modelData width: month.width / 7 height: 26 Rectangle { anchors.centerIn: parent width: 22 height: 20 visible: parent.modelData.today color: Theme.treatmentSelectedFill } Text { anchors.centerIn: parent text: parent.modelData.day color: parent.modelData.today ? Theme.treatmentSelectedText : parent.modelData.inMonth ? Theme.text : Theme.alpha(Theme.muted, 0.5) font.family: Theme.fontMono font.pixelSize: Theme.fsSmall font.weight: parent.modelData.today ? Font.Bold : Font.Normal renderType: Text.NativeRendering } } } } } Rule { height: root.columnHeight } // The machine: the same meters the bar's readout opens onto. Item { implicitWidth: 238 implicitHeight: root.columnHeight SectionRule { id: sysHead anchors.left: parent.left anchors.right: parent.right text: "System" } Column { anchors.top: sysHead.bottom anchors.topMargin: Theme.padM anchors.left: parent.left anchors.right: parent.right spacing: Theme.padM MetricRow { width: parent.width value: Sys.cpuUsage label: "CPU" sub: Math.round(Sys.cpuTemp) + "°C" } MetricRow { width: parent.width value: Sys.memPercent label: "MEM" sub: Sys.gb(Sys.memUsed) + " / " + Sys.gb(Sys.memTotal) } MetricRow { width: parent.width visible: Sys.gpuAvailable value: Sys.gpuUsage label: "GPU" sub: Math.round(Sys.gpuTemp) + "°C" } MetricRow { width: parent.width value: Sys.diskPercent label: "DISK" sub: Sys.diskFree + " free" } } } Rule { height: root.columnHeight } // What is playing. With no player it says so, in the voice NieR's // interface uses for an empty channel. Item { implicitWidth: 280 implicitHeight: root.columnHeight SectionRule { id: playHead anchors.left: parent.left anchors.right: parent.right text: Media.hasPlayer ? Media.identity : "Audio" } Text { anchors.centerIn: parent visible: !Media.hasPlayer text: "NO SIGNAL" color: Theme.muted font.family: Theme.fontDisplay font.pixelSize: Theme.fsBody font.letterSpacing: Theme.tracking * 2 renderType: Text.NativeRendering } Item { anchors.top: playHead.bottom anchors.topMargin: Theme.padM anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom visible: Media.hasPlayer Rectangle { id: artFrame width: 96 height: 96 color: Theme.surface border.width: 1 border.color: Theme.overlay Image { id: art anchors.fill: parent anchors.margins: 1 source: Media.artUrl fillMode: Image.PreserveAspectCrop asynchronous: true sourceSize.width: 192 sourceSize.height: 192 } Icon { anchors.centerIn: parent visible: art.status !== Image.Ready text: "󰝚" color: Theme.muted font.pixelSize: Theme.fsHuge } } Column { anchors.left: artFrame.right anchors.leftMargin: Theme.padM anchors.right: parent.right anchors.verticalCenter: artFrame.verticalCenter spacing: 3 Text { width: parent.width text: Media.title || "Untitled" elide: Text.ElideRight maximumLineCount: 2 wrapMode: Text.Wrap color: Theme.accent font.family: Theme.fontDisplay font.pixelSize: Theme.fsLarge font.weight: Theme.weightDisplay renderType: Text.NativeRendering } Text { width: parent.width text: Media.artist elide: Text.ElideRight color: Theme.subtext font.family: Theme.fontMono font.pixelSize: Theme.fsSmall renderType: Text.NativeRendering } } // Position: a hairline with a square riding it. Item { id: timeline anchors.top: artFrame.bottom anchors.topMargin: Theme.padM anchors.left: parent.left anchors.right: parent.right height: Theme.marker visible: Media.hasTimeline Rectangle { anchors.verticalCenter: parent.verticalCenter width: parent.width height: 1 color: Theme.overlay } Rectangle { anchors.verticalCenter: parent.verticalCenter width: parent.width * Media.progress height: 1 color: Theme.accent } Rectangle { anchors.verticalCenter: parent.verticalCenter x: parent.width * Media.progress - width / 2 width: Theme.marker height: Theme.marker color: Theme.accent } } Row { anchors.bottom: parent.bottom anchors.left: parent.left spacing: Theme.padS Transport { glyph: "󰒮" enabled: Media.canGoPrevious onClicked: Media.previous() } Transport { glyph: Media.playing ? "󰏤" : "󰐊" onClicked: Media.playPause() } Transport { glyph: "󰒭" enabled: Media.canGoNext onClicked: Media.next() } } Text { anchors.bottom: parent.bottom anchors.bottomMargin: 8 anchors.right: parent.right visible: Media.hasTimeline text: Media.timeString(Media.position) + " / " + Media.timeString(Media.length) color: Theme.muted font.family: Theme.fontMono font.pixelSize: Theme.fsMicro renderType: Text.NativeRendering } } } } } } } // ------------------------------------------------------------ columns // A vertical hairline between columns, the same rule the bar divides its // modules with, at the drawer's scale. component Rule: Item { implicitWidth: Theme.padL * 2 + 1 Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.bottom: parent.bottom anchors.topMargin: Theme.padS anchors.bottomMargin: Theme.padS width: 1 color: Theme.alpha(Theme.outline, 0.5) } } readonly property real columnHeight: 214 // A label and its reading on one line, label left, reading right. component Fact: Item { property string label: "" property string value: "" width: parent.width implicitHeight: 14 Text { anchors.left: parent.left text: parent.label color: Theme.muted font.family: Theme.fontMono font.pixelSize: Theme.fsMicro font.letterSpacing: 2 renderType: Text.NativeRendering } Text { anchors.right: parent.right text: parent.value color: Theme.text font.family: Theme.fontMono font.pixelSize: Theme.fsMicro renderType: Text.NativeRendering } } // A square hairline button. Hover lifts it; nothing here inverts, since // inversion is reserved for the keyboard's cursor. component Transport: Item { id: tb property string glyph: "" signal clicked() width: 30 height: 30 opacity: tb.enabled ? 1 : 0.35 Rectangle { anchors.fill: parent color: tbHover.hovered ? Theme.treatmentHoverFill : "transparent" border.width: 1 border.color: tbHover.hovered ? Theme.treatmentHoverMark : Theme.overlay Behavior on color { ColorAnimation { duration: Theme.durFast } } } Icon { anchors.centerIn: parent text: tb.glyph color: tbHover.hovered ? Theme.accent : Theme.text } HoverHandler { id: tbHover enabled: tb.enabled cursorShape: Qt.PointingHandCursor } TapHandler { enabled: tb.enabled onTapped: tb.clicked() } } }