diff --git a/Configs/quickshell/aphotic/components/SignalTile.qml b/Configs/quickshell/aphotic/components/SignalTile.qml new file mode 100644 index 00000000..80ca77be --- /dev/null +++ b/Configs/quickshell/aphotic/components/SignalTile.qml @@ -0,0 +1,129 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.config +import qs.services + +// The dashboard's one tile: dark glass, a hairline that warms toward the +// accent under the pointer, light along the top edge, and a header with an +// icon, a label and an optional figure. `live` lights a small dot beside +// the label while the tile's subject is doing something. Content goes in +// the body and is given all the room left under the header. +Item { + id: root + + property string icon: "" + property string label: "" + property string figure: "" + property bool live: false + property bool accent: false + property real padding: Tokens.padding.large + + default property alias body: content.data + readonly property bool hovered: hover.hovered + readonly property real headerHeight: header.visible ? header.implicitHeight + Tokens.spacing.small : 0 + + Rectangle { + id: glass + + anchors.fill: parent + radius: Tokens.rounding.large + color: root.hovered ? Colours.signalStyle.raised : Qt.alpha(Colours.signalStyle.surface, 0.72) + border.width: 1 + border.color: root.accent ? Qt.alpha(Colours.signal, 0.65) : (root.hovered ? Qt.alpha(Colours.signal, 0.42) : Colours.signalStyle.hairline) + + Behavior on color { + CAnim {} + } + + Behavior on border.color { + CAnim {} + } + + Rectangle { + x: glass.radius + width: parent.width - glass.radius * 2 + height: 1 + color: Colours.signalStyle.edgeLight + } + } + + RowLayout { + id: header + + visible: root.label !== "" || root.icon !== "" + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: root.padding + spacing: Tokens.spacing.small + + MaterialIcon { + visible: root.icon !== "" + text: root.icon + color: root.live || root.accent ? Colours.signal : Colours.inkMuted + fontStyle: Tokens.font.icon.small + } + + StyledText { + text: root.label.toUpperCase() + color: Colours.inkMuted + font: Tokens.font.label.builders.small.weight(Font.Medium).letterSpacing(1.2).build() + } + + Rectangle { + visible: root.live + implicitWidth: 6 + implicitHeight: 6 + radius: Math.min(width, height) / 2 + color: Colours.signal + + SequentialAnimation on opacity { + running: root.live && RenderGate.decorative && root.visible + loops: Animation.Infinite + alwaysRunToEnd: true + + NumberAnimation { + to: 0.25 + duration: 900 + easing.type: Easing.InOutSine + } + + NumberAnimation { + to: 1 + duration: 900 + easing.type: Easing.InOutSine + } + } + } + + Item { + Layout.fillWidth: true + } + + StyledText { + visible: root.figure !== "" + text: root.figure + color: Colours.ink + font: Tokens.font.mono.small + } + } + + Item { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.top: header.visible ? header.bottom : parent.top + anchors.leftMargin: root.padding + anchors.rightMargin: root.padding + anchors.bottomMargin: root.padding + anchors.topMargin: header.visible ? Tokens.spacing.small : root.padding + } + + HoverHandler { + id: hover + } +} diff --git a/Configs/quickshell/aphotic/components/qmldir b/Configs/quickshell/aphotic/components/qmldir index a4db3973..978021b4 100644 --- a/Configs/quickshell/aphotic/components/qmldir +++ b/Configs/quickshell/aphotic/components/qmldir @@ -39,3 +39,4 @@ SignalSurface 1.0 SignalSurface.qml SectionLabel 1.0 SectionLabel.qml SignalCard 1.0 SignalCard.qml Readout 1.0 Readout.qml +SignalTile 1.0 SignalTile.qml diff --git a/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml b/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml index 5345712d..9d8431fd 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/DashboardContent.qml @@ -5,7 +5,7 @@ import qs.components import qs.services import qs.modules.flow import QtQuick.Window -import "spine" +import "bento" ColumnLayout { id: root @@ -148,13 +148,13 @@ ColumnLayout { return pluginTabRepeater.itemAt(i) ?? tabLoader; } - readonly property real tabWidth: tabFrame.activeLoader.implicitWidth - readonly property real tabHeight: tabFrame.activeLoader.implicitHeight + readonly property real tabWidth: tabFrame.activeLoader.item?.implicitWidth ?? 0 + readonly property real tabHeight: tabFrame.activeLoader.item?.implicitHeight ?? 0 // One stage for every tab, so switching never resizes the frame. // A tab larger than the stage still gets room, it just grows it. - readonly property real stageWidth: Math.min(1280, (Window.width || 1280) - Tokens.padding.extraLarge * 4) - readonly property real stageHeight: Math.min(760, (Window.height || 760) - Tokens.padding.extraLarge * 8) + readonly property real stageWidth: Math.min(1680, (Window.width || 1280) - Tokens.padding.extraLarge * 4) + readonly property real stageHeight: Math.min(940, (Window.height || 760) - Tokens.padding.extraLarge * 8) Layout.preferredWidth: Math.max(tabFrame.stageWidth, tabFrame.tabWidth) + Tokens.padding.extraLarge * 2 Layout.preferredHeight: Math.max(tabFrame.stageHeight, tabFrame.tabHeight) + Tokens.padding.extraLarge * 2 @@ -185,7 +185,9 @@ ColumnLayout { Loader { id: tabLoader - anchors.centerIn: parent + // Every tab gets the whole stage, edge to edge. + anchors.fill: parent + anchors.margins: Tokens.padding.extraLarge opacity: 1 active: true @@ -242,7 +244,8 @@ ColumnLayout { required property var modelData - anchors.centerIn: parent + anchors.fill: parent + anchors.margins: Tokens.padding.extraLarge active: root._openedTabIds.includes(pluginTabLoader.modelData.id) asynchronous: true visible: pluginTabLoader.opacity > 0 @@ -263,15 +266,7 @@ ColumnLayout { Component { id: spineComp - Item { - implicitWidth: tabFrame.stageWidth - implicitHeight: tabFrame.stageHeight - - SpineSurface { - anchors.fill: parent - embedded: true - } - } + BentoDashboard {} } Component { id: flowComp diff --git a/Configs/quickshell/aphotic/modules/dashboard/WorkspacesTab.qml b/Configs/quickshell/aphotic/modules/dashboard/WorkspacesTab.qml index dc2a28b0..fc5f29f7 100644 --- a/Configs/quickshell/aphotic/modules/dashboard/WorkspacesTab.qml +++ b/Configs/quickshell/aphotic/modules/dashboard/WorkspacesTab.qml @@ -7,28 +7,51 @@ import qs.config import qs.components import qs.services -StyledRect { +// Every workspace as a live map of its windows, laid out to fill the stage. +// Each window is drawn where it actually sits on its monitor, so the tab +// reads like looking down at the desktop rather than a list of icons. +Item { id: root required property ScreenState screenState - readonly property var sortedWorkspaces: Hypr.workspaces.values.slice().sort((a, b) => a.id - b.id) - readonly property int columns: Math.min(4, Math.max(1, root.sortedWorkspaces.length)) - - implicitWidth: grid.implicitWidth + Tokens.padding.large * 2 - implicitHeight: grid.implicitHeight + Tokens.padding.large * 2 - color: "transparent" + readonly property var sortedWorkspaces: Hypr.workspaces.values.filter(w => w.id > 0).slice().sort((a, b) => a.id - b.id) + readonly property int count: Math.max(1, root.sortedWorkspaces.length) + + // The column count whose cells come closest to the monitor's shape. + readonly property int columns: { + let best = 1; + let bestScore = Infinity; + for (let c = 1; c <= root.count; c++) { + const rows = Math.ceil(root.count / c); + const cellAspect = (root.width / c) / Math.max(1, root.height / rows); + const score = Math.abs(Math.log(cellAspect / 1.7)); + if (score < bestScore) { + bestScore = score; + best = c; + } + } + return best; + } + readonly property int rows: Math.ceil(root.count / root.columns) - GridLayout { - id: grid + function monitorOf(ws: var): var { + const name = ws?.lastIpcObject?.monitor ?? ""; + return (Hypr.monitors?.values ?? []).find(m => m.name === name) ?? null; + } - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: Tokens.padding.large + function focusWindow(win: var): void { + const address = win?.address ?? ""; + if (address) + WindowList.focus(address.startsWith("0x") ? address : `0x${address}`); + root.screenState.dashboard = false; + } + GridLayout { + anchors.fill: parent columns: root.columns - columnSpacing: Tokens.spacing.medium - rowSpacing: Tokens.spacing.medium + columnSpacing: Tokens.spacing.large + rowSpacing: Tokens.spacing.large Repeater { model: ScriptModel { @@ -41,82 +64,123 @@ StyledRect { required property var modelData readonly property bool active: wsCard.modelData.id === Hypr.activeWsId readonly property var windows: Hypr.toplevels.values.filter(c => c.workspace?.id === wsCard.modelData.id) + readonly property var monitor: root.monitorOf(wsCard.modelData) - title: qsTr("Workspace %1").arg(wsCard.modelData.id) + title: wsCard.windows.length > 0 ? qsTr("Workspace %1 · %n window(s)", "", wsCard.windows.length).arg(wsCard.modelData.id) : qsTr("Workspace %1").arg(wsCard.modelData.id) tintIndex: wsCard.modelData.id selected: wsCard.active - Layout.preferredWidth: 140 - Layout.preferredHeight: 100 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 - ColumnLayout { + Interact { anchors.fill: parent - anchors.leftMargin: Tokens.padding.medium - anchors.rightMargin: Tokens.padding.medium - anchors.bottomMargin: Tokens.padding.medium - anchors.topMargin: wsCard.headerHeight - spacing: Tokens.spacing.small - - StyledText { - visible: false - text: wsCard.modelData.id - color: wsCard.active ? Colours.legibleAccent(Colours.signal, Colours.signalStyle.surface) : Colours.ink - font: Tokens.font.title.builders.medium.weight(Font.Medium).build() + radius: parent.radius + onClicked: { + Hypr.dispatch(Hypr.usingLua ? `hl.dsp.focus({ workspace = ${wsCard.modelData.id} })` : `workspace ${wsCard.modelData.id}`); + root.screenState.dashboard = false; } + } - Flow { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: Tokens.spacing.small - - Repeater { - model: wsCard.windows.slice(0, 9) - - Item { - id: tile - - required property var modelData + // The monitor, scaled into the card at its own aspect. + Item { + id: screenArea + + readonly property real monW: wsCard.monitor?.width ?? 1920 + readonly property real monH: wsCard.monitor?.height ?? 1080 + readonly property real monX: wsCard.monitor?.x ?? 0 + readonly property real monY: wsCard.monitor?.y ?? 0 + readonly property real availW: parent.width - Tokens.padding.large * 2 + readonly property real availH: parent.height - wsCard.headerHeight - Tokens.padding.large + readonly property real scale: Math.max(0.01, Math.min(screenArea.availW / screenArea.monW, screenArea.availH / screenArea.monH)) + + x: (parent.width - width) / 2 + y: wsCard.headerHeight + (screenArea.availH - height) / 2 + width: screenArea.monW * screenArea.scale + height: screenArea.monH * screenArea.scale + + Rectangle { + anchors.fill: parent + radius: Tokens.rounding.medium + color: Qt.alpha(Colours.signalStyle.base, 0.5) + border.width: 1 + border.color: wsCard.active ? Qt.alpha(Colours.signal, 0.5) : Colours.signalStyle.hairline + } - implicitWidth: icon.implicitWidth + Tokens.padding.small - implicitHeight: icon.implicitHeight + Tokens.padding.small + StyledText { + anchors.centerIn: parent + visible: wsCard.windows.length === 0 + text: qsTr("Empty") + color: Colours.inkMuted + font: Tokens.font.label.medium + } - Rectangle { - anchors.fill: parent - radius: Tokens.rounding.small - color: Colours.signalStyle.raised - border.width: 1 - border.color: Colours.signalStyle.hairline + Repeater { + model: wsCard.windows + + Item { + id: win + + required property var modelData + readonly property var info: win.modelData.lastIpcObject ?? ({}) + readonly property bool focused: win.modelData === Hypr.activeToplevel + readonly property var at: win.info.at ?? [0, 0] + readonly property var size: win.info.size ?? [screenArea.monW, screenArea.monH] + + x: Math.max(0, (win.at[0] - screenArea.monX) * screenArea.scale) + y: Math.max(0, (win.at[1] - screenArea.monY) * screenArea.scale) + width: Math.max(18, Math.min(screenArea.width - x, win.size[0] * screenArea.scale)) + height: Math.max(14, Math.min(screenArea.height - y, win.size[1] * screenArea.scale)) + + Rectangle { + anchors.fill: parent + anchors.margins: 1 + radius: Math.min(Tokens.rounding.small, height / 3) + color: win.focused ? Qt.alpha(Colours.signal, 0.14) : (winArea.containsMouse ? Colours.signalStyle.raisedHi : Colours.signalStyle.raised) + border.width: 1 + border.color: win.focused ? Colours.signal : Colours.signalStyle.hairline + + Behavior on color { + CAnim {} } + } - AppIcon { - id: icon + Column { + anchors.centerIn: parent + spacing: 4 + visible: win.height > 28 - anchors.centerIn: parent - appClass: modelData.lastIpcObject?.class ?? "" + AppIcon { + anchors.horizontalCenter: parent.horizontalCenter + appClass: win.info.class ?? "" fallbackGlyph: "desktop_windows" - colour: Colours.inkMuted - fontStyle: Tokens.font.icon.small + colour: win.focused ? Colours.signal : Colours.inkMuted + size: Math.min(48, Math.max(16, Math.min(win.width, win.height) * 0.32)) + fontStyle: Tokens.font.icon.large + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + visible: win.height > 70 && win.width > 90 + width: Math.min(implicitWidth, win.width - 12) + text: win.info.title || win.info.class || "" + elide: Text.ElideRight + color: win.focused ? Colours.ink : Colours.inkMuted + font: Tokens.font.label.small } } - } - } - StyledText { - visible: wsCard.windows.length === 0 - Layout.fillHeight: true - verticalAlignment: Text.AlignVCenter - text: qsTr("Empty") - color: Colours.inkMuted - font: Tokens.font.label.small - } - } + MouseArea { + id: winArea - Interact { - anchors.fill: parent - radius: parent.radius - onClicked: { - Hypr.dispatch(Hypr.usingLua ? `hl.dsp.focus({ workspace = ${wsCard.modelData.id} })` : `workspace ${wsCard.modelData.id}`); - root.screenState.dashboard = false; + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.focusWindow(win.modelData) + } + } } } } diff --git a/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml b/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml new file mode 100644 index 00000000..bec50ca7 --- /dev/null +++ b/Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml @@ -0,0 +1,677 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Mpris +import qs.config +import qs.components +import qs.services +import qs.services.ai +import qs.services.profile +import ".." + +// The Dashboard tab: a grid of live tiles that fills the stage edge to +// edge. Twelve columns by six rows; every tile spans whole cells, so the +// grid stays even at any size. Each tile carries one moving part (a curve, +// a ring, a dot) that moves only when its value does. +Item { + id: root + + readonly property real gap: Tokens.spacing.large + readonly property real cellW: (root.width - root.gap * 11) / 12 + readonly property real cellH: (root.height - root.gap * 5) / 6 + + // Tiles are placed by cell, not by a layout, so a tile's size never + // depends on what is inside it. + component Cell: SignalTile { + property int col: 0 + property int row: 0 + property int cols: 1 + property int rows: 1 + + x: col * (root.cellW + root.gap) + y: row * (root.cellH + root.gap) + width: cols * root.cellW + (cols - 1) * root.gap + height: rows * root.cellH + (rows - 1) * root.gap + } + + SystemUsageWatch { + detailed: true + } + + NetworkUsageWatch {} + + Component.onCompleted: AgentEvents.hold("dashboard-bento", true) + Component.onDestruction: AgentEvents.hold("dashboard-bento", false) + + property var cpuTrail: [] + property var downTrail: [] + property var upTrail: [] + + function push(list: var, value: real): var { + const next = list.concat([value]); + return next.length > 40 ? next.slice(next.length - 40) : next; + } + + Connections { + target: SystemUsage + + function onCpuPercChanged(): void { + root.cpuTrail = root.push(root.cpuTrail, SystemUsage.cpuPerc); + } + } + + Connections { + target: NetworkUsage + + function onDownloadSpeedChanged(): void { + root.downTrail = root.push(root.downTrail, NetworkUsage.downloadSpeed); + root.upTrail = root.push(root.upTrail, NetworkUsage.uploadSpeed); + } + } + + function rate(bytes: real): string { + const f = NetworkUsage.formatBytes(bytes); + return `${f.value < 10 ? f.value.toFixed(1) : Math.round(f.value)} ${f.unit}`; + } + + function gib(kib: real): string { + return (kib / 1048576).toFixed(1); + } + + // ── Row 1-2 ───────────────────────────────────────────────────────── + + Cell { + col: 0 + row: 0 + cols: 4 + rows: 2 + icon: "schedule" + label: Time.format("dddd") + accent: true + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillHeight: true + } + + Text { + text: { + Time.minuteStr; + return Time.format(Settings.twelveHourClock ? "h:mm" : "HH:mm"); + } + color: Colours.ink + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.min(parent.height * 0.62, parent.width * 0.3) + font.weight: Font.DemiBold + font.letterSpacing: -2 + } + + RowLayout { + spacing: Tokens.spacing.small + + Rectangle { + implicitWidth: 28 + implicitHeight: 3 + radius: Math.min(width, height) / 2 + color: Colours.signal + } + + StyledText { + text: Time.format("MMMM d, yyyy") + color: Colours.inkMuted + font: Tokens.font.title.small + } + + StyledText { + visible: Settings.twelveHourClock + text: Time.amPmStr + color: Colours.signal + font: Tokens.font.title.small + } + } + + Item { + Layout.fillHeight: true + } + } + } + + Cell { + col: 4 + row: 0 + cols: 4 + rows: 2 + icon: "memory" + label: qsTr("Processor") + figure: SystemUsage.cpuTemp > 0 ? `${Math.round(SystemUsage.cpuTemp)}°C` : "" + live: SystemUsage.cpuPerc > 0.5 + + StyledText { + id: cpuFigure + + text: `${Math.round(SystemUsage.cpuPerc * 100)}%` + color: Colours.ink + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.max(24, parent.height * 0.26) + font.weight: Font.Light + } + + StyledText { + anchors.left: cpuFigure.right + anchors.leftMargin: Tokens.spacing.small + anchors.baseline: cpuFigure.baseline + text: SystemUsage.cpuName + color: Colours.inkMuted + elide: Text.ElideRight + width: parent.width - cpuFigure.width - Tokens.spacing.small + font: Tokens.font.label.small + } + + Spline { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height - cpuFigure.height - Tokens.spacing.small + values: root.cpuTrail + peak: 1 + } + } + + Cell { + id: media + + readonly property MprisPlayer player: Players.active + + col: 8 + row: 0 + cols: 4 + rows: 2 + icon: "music_note" + label: media.player?.identity ?? qsTr("Media") + live: media.player?.isPlaying ?? false + + RowLayout { + anchors.fill: parent + spacing: Tokens.spacing.large + + StyledClippingRect { + Layout.preferredWidth: Math.min(parent.height, parent.width * 0.42) + Layout.preferredHeight: Layout.preferredWidth + radius: Tokens.rounding.medium + color: Colours.signalStyle.raised + + Image { + id: art + + anchors.fill: parent + source: media.player?.trackArtUrl ?? "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + sourceSize.width: 384 + sourceSize.height: 384 + } + + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 2 + + Item { + Layout.fillHeight: true + } + + StyledText { + Layout.fillWidth: true + text: media.player?.trackTitle || qsTr("Nothing playing") + color: Colours.ink + elide: Text.ElideRight + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() + } + + StyledText { + Layout.fillWidth: true + text: media.player?.trackArtist ?? "" + color: Colours.inkMuted + elide: Text.ElideRight + font: Tokens.font.body.small + } + + RowLayout { + Layout.topMargin: Tokens.spacing.medium + spacing: Tokens.spacing.medium + + Repeater { + model: [ + { icon: "skip_previous", act: () => media.player?.previous(), main: false }, + { icon: media.player?.isPlaying ? "pause" : "play_arrow", act: () => media.player?.togglePlaying(), main: true }, + { icon: "skip_next", act: () => media.player?.next(), main: false } + ] + + StyledRect { + id: control + + required property var modelData + + implicitWidth: control.modelData.main ? 44 : 34 + implicitHeight: implicitWidth + radius: Tokens.rounding.full + color: control.modelData.main ? Qt.alpha(Colours.signal, 0.18) : "transparent" + border.width: 1 + border.color: control.modelData.main ? Colours.signal : Colours.signalStyle.hairline + opacity: media.player ? 1 : 0.4 + + MaterialIcon { + anchors.centerIn: parent + text: control.modelData.icon + color: control.modelData.main ? Colours.signal : Colours.ink + fontStyle: control.modelData.main ? Tokens.font.icon.large : Tokens.font.icon.medium + } + + Interact { + anchors.fill: parent + radius: parent.radius + onClicked: control.modelData.act() + } + } + } + } + + Item { + Layout.fillHeight: true + } + } + } + } + + // ── Row 3-4 ───────────────────────────────────────────────────────── + + Cell { + col: 0 + row: 2 + cols: 2 + rows: 2 + icon: "memory_alt" + label: qsTr("Memory") + + Ring { + anchors.fill: parent + value: SystemUsage.memPerc + figure: `${Math.round(SystemUsage.memPerc * 100)}%` + caption: SystemUsage.memTotal > 0 ? `${root.gib(SystemUsage.memUsed)} / ${Math.round(SystemUsage.memTotal / 1048576)} GiB` : "" + } + } + + Cell { + col: 2 + row: 2 + cols: 2 + rows: 2 + icon: "developer_board" + label: qsTr("Graphics") + figure: SystemUsage.gpuTemp > 0 ? `${Math.round(SystemUsage.gpuTemp)}°C` : "" + + Ring { + anchors.fill: parent + value: SystemUsage.gpuStatsAvailable ? SystemUsage.gpuPerc : 0 + figure: SystemUsage.gpuStatsAvailable ? `${Math.round(SystemUsage.gpuPerc * 100)}%` : "--" + caption: SystemUsage.gpuDetected ? "" : qsTr("none found") + colour: Colours.signalTertiary + } + } + + Cell { + col: 4 + row: 2 + cols: 4 + rows: 2 + icon: "network_check" + label: qsTr("Network") + live: NetworkUsage.downloadSpeed > 512 * 1024 + + RowLayout { + id: netFigures + + spacing: Tokens.spacing.extraLarge + + Repeater { + model: [ + { icon: "\u2193", value: NetworkUsage.downloadSpeed, colour: Colours.signal }, + { icon: "\u2191", value: NetworkUsage.uploadSpeed, colour: Colours.signalSecondary } + ] + + RowLayout { + id: netFigure + + required property var modelData + + spacing: Tokens.spacing.extraSmall + + StyledText { + text: netFigure.modelData.icon + color: netFigure.modelData.colour + font: Tokens.font.title.medium + } + + StyledText { + text: root.rate(netFigure.modelData.value) + color: Colours.ink + font: Tokens.font.title.builders.medium.weight(Font.Light).build() + } + } + } + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height - netFigures.height - Tokens.spacing.small + + Spline { + anchors.fill: parent + values: root.downTrail + peak: 0 + } + + Spline { + anchors.fill: parent + values: root.upTrail + peak: Math.max(1, root.downTrail.reduce((m, v) => Math.max(m, v), 0)) + colour: Colours.signalSecondary + fill: false + lineWidth: 1.5 + } + } + } + + Cell { + id: agents + + readonly property var sessions: AgentEvents.liveSessions.slice().sort((a, b) => (a.startedAt - b.startedAt) || (a.id < b.id ? -1 : 1)) + + col: 8 + row: 2 + cols: 4 + rows: 2 + icon: "smart_toy" + label: qsTr("Agents") + figure: agents.sessions.length > 0 ? String(agents.sessions.length) : "" + live: AgentEvents.anyRunning + accent: AgentEvents.anyWaiting + + StyledText { + anchors.centerIn: parent + visible: agents.sessions.length === 0 + text: qsTr("No agent running") + color: Colours.inkMuted + font: Tokens.font.body.small + } + + Column { + anchors.fill: parent + spacing: Tokens.spacing.small + + Repeater { + model: agents.sessions.slice(0, Math.max(1, Math.floor(agents.height / 44))) + + StyledRect { + id: session + + required property var modelData + readonly property bool waiting: session.modelData.status === "waiting" + readonly property bool busy: session.modelData.status === "running" || session.modelData.status === "compacting" + + width: parent.width + height: 32 + radius: Tokens.rounding.full + color: session.waiting ? Qt.alpha(Colours.signal, 0.16) : Qt.alpha(Colours.signalStyle.base, 0.5) + border.width: 1 + border.color: session.waiting ? Colours.signal : Colours.signalStyle.hairline + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + spacing: Tokens.spacing.small + + Rectangle { + implicitWidth: 7 + implicitHeight: 7 + radius: Math.min(width, height) / 2 + color: session.waiting ? Colours.signal : session.busy ? Colours.signalOnSurface : Colours.outline + } + + StyledText { + text: AgentRoles.entries.find(e => e.id === session.modelData.harness)?.label ?? session.modelData.harness + color: Colours.ink + font: Tokens.font.label.builders.medium.weight(Font.Medium).build() + } + + StyledText { + Layout.fillWidth: true + text: (session.modelData.cwd || "").split("/").pop() || session.modelData.tool || "" + color: Colours.inkMuted + elide: Text.ElideRight + font: Tokens.font.label.small + } + + StyledText { + text: session.waiting ? qsTr("waiting") : (session.modelData.tool || session.modelData.status) + color: session.waiting ? Colours.signal : Colours.inkMuted + font: Tokens.font.mono.small + } + } + + Interact { + anchors.fill: parent + radius: parent.radius + onClicked: AgentWindowFocus.focusByCwd(session.modelData.cwd ?? "") + } + } + } + } + } + + // ── Row 5-6 ───────────────────────────────────────────────────────── + + Cell { + col: 0 + row: 4 + cols: 4 + rows: 2 + icon: "calendar_month" + label: qsTr("Calendar") + padding: Tokens.padding.medium + + DashCalendar { + anchors.centerIn: parent + scale: Math.min(1.25, parent.width / Math.max(1, implicitWidth), parent.height / Math.max(1, implicitHeight)) + } + } + + Cell { + col: 4 + row: 4 + cols: 2 + rows: 2 + icon: Weather.hasData ? Weather.conditionIcon : "cloud_off" + label: qsTr("Weather") + + Column { + anchors.centerIn: parent + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + anchors.horizontalCenter: parent.horizontalCenter + text: Weather.hasData ? Weather.conditionIcon : "cloud_off" + color: Colours.signal + font.pixelSize: Math.min(64, root.cellH * 0.5) + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: Weather.hasData ? `${Math.round(Weather.currentTemp)}°` : "--" + color: Colours.ink + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.min(48, root.cellH * 0.4) + font.weight: Font.Light + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: Weather.hasData ? Weather.conditionText : qsTr("No location set") + color: Colours.inkMuted + font: Tokens.font.label.small + } + } + } + + Cell { + id: ai + + readonly property var engines: { + const rows = []; + const seen = {}; + for (const e of EngineRegistry.liveEngines) { + if (!seen[e.id]) { + seen[e.id] = true; + rows.push({ id: e.id, label: e.label || e.id, models: (e.models ?? []).map(m => m.name) }); + } + } + const backends = LocalInference.backends; + for (const owner of Object.keys(backends)) { + const models = (backends[owner].models ?? []).map(m => m.name); + if (!seen[owner] && models.length > 0) { + seen[owner] = true; + rows.push({ id: owner, label: backends[owner].label || owner, models: models }); + } + } + return rows.filter(r => r.models.length > 0); + } + readonly property int vram: ai.engines.length > 0 ? ResourceEngine.claimsOf(ai.engines[0].id).filter(c => c.resource === "gpu-vram").reduce((t, c) => t + (c.amount ?? 0), 0) : 0 + + col: 6 + row: 4 + cols: 2 + rows: 2 + icon: "neurology" + label: qsTr("Local AI") + live: ai.engines.length > 0 + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + spacing: Tokens.spacing.extraSmall + + StyledText { + width: parent.width + text: ai.engines.length > 0 ? ai.engines[0].label : qsTr("Idle") + color: ai.engines.length > 0 ? Colours.ink : Colours.inkMuted + elide: Text.ElideRight + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.min(30, root.cellH * 0.26) + } + + StyledText { + width: parent.width + text: ai.engines.length > 0 ? ai.engines[0].models.join(", ") : qsTr("No model loaded") + color: Colours.inkMuted + elide: Text.ElideRight + font: Tokens.font.label.small + } + + StyledText { + visible: ai.vram > 0 + text: qsTr("%1 GiB on the GPU").arg((ai.vram / 1024).toFixed(1)) + color: Colours.signal + font: Tokens.font.mono.small + } + } + } + + Cell { + col: 8 + row: 4 + cols: 4 + rows: 1 + icon: "tune" + label: qsTr("Controls") + padding: Tokens.padding.medium + + DashQuickToggles { + anchors.centerIn: parent + scale: Math.min(1, parent.width / Math.max(1, implicitWidth), parent.height / Math.max(1, implicitHeight)) + } + } + + Cell { + col: 8 + row: 5 + cols: 4 + rows: 1 + icon: "timer" + label: qsTr("Focus") + live: Pomodoro.running + figure: Pomodoro.isBreak ? qsTr("break") : "" + padding: Tokens.padding.medium + + RowLayout { + anchors.fill: parent + spacing: Tokens.spacing.large + + StyledText { + text: Pomodoro.formatTime(Pomodoro.remaining) + color: Colours.ink + font.family: Tokens.font.mono.large.family + font.pixelSize: Math.min(34, parent.height * 0.75) + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 4 + radius: Math.min(width, height) / 2 + color: Colours.signalStyle.hairline + + Rectangle { + width: parent.width * (1 - Pomodoro.remaining / Math.max(1, Pomodoro.isBreak ? Pomodoro.breakSeconds : Pomodoro.workSeconds)) + height: parent.height + radius: Math.min(width, height) / 2 + color: Pomodoro.isBreak ? Colours.signalTertiary : Colours.signal + + Behavior on width { + Anim {} + } + } + } + + StyledRect { + implicitWidth: 40 + implicitHeight: 40 + radius: Tokens.rounding.full + color: Qt.alpha(Colours.signal, 0.18) + border.width: 1 + border.color: Colours.signal + + MaterialIcon { + anchors.centerIn: parent + text: Pomodoro.running ? "pause" : "play_arrow" + color: Colours.signal + fontStyle: Tokens.font.icon.large + } + + Interact { + anchors.fill: parent + radius: parent.radius + onClicked: Pomodoro.toggle() + } + } + } + } +} diff --git a/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml b/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml new file mode 100644 index 00000000..b0585283 --- /dev/null +++ b/Configs/quickshell/aphotic/modules/dashboard/bento/Ring.qml @@ -0,0 +1,89 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Shapes +import qs.components +import qs.config +import qs.services + +// An arc meter that sweeps to its value, with the figure in the middle. +Item { + id: root + + property real value: 0 + property string figure: "" + property string caption: "" + property color colour: Colours.signal + + readonly property real size: Math.min(root.width, root.height) + readonly property real thickness: Math.max(4, root.size * 0.065) + property real shown: root.value + + Behavior on shown { + NumberAnimation { + duration: 600 + easing.type: Easing.OutCubic + } + } + + Shape { + anchors.centerIn: parent + width: root.size + height: root.size + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeColor: Colours.signalStyle.hairline + strokeWidth: root.thickness + fillColor: "transparent" + capStyle: ShapePath.RoundCap + + PathAngleArc { + centerX: root.size / 2 + centerY: root.size / 2 + radiusX: root.size / 2 - root.thickness + radiusY: radiusX + startAngle: 135 + sweepAngle: 270 + } + } + + ShapePath { + strokeColor: root.shown >= 0.9 ? Colours.alert : root.colour + strokeWidth: root.thickness + fillColor: "transparent" + capStyle: ShapePath.RoundCap + + PathAngleArc { + centerX: root.size / 2 + centerY: root.size / 2 + radiusX: root.size / 2 - root.thickness + radiusY: radiusX + startAngle: 135 + sweepAngle: 270 * Math.max(0.001, Math.min(1, root.shown)) + } + } + } + + Column { + anchors.centerIn: parent + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: root.figure + color: Colours.ink + font.family: Tokens.font.headline.large.family + font.pixelSize: Math.max(14, root.size * 0.24) + font.weight: Font.Light + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + visible: root.caption !== "" + text: root.caption + color: Colours.inkMuted + font.family: Tokens.font.mono.small.family + font.pixelSize: Math.max(10, root.size * 0.085) + } + } +} diff --git a/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml b/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml new file mode 100644 index 00000000..f2099327 --- /dev/null +++ b/Configs/quickshell/aphotic/modules/dashboard/bento/Spline.qml @@ -0,0 +1,101 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Shapes +import qs.services + +// A smooth line through `values` with a soft fill under it and a lit point +// on the newest sample. `peak` sets the top of the scale; 0 scales to the +// largest value seen. +Item { + id: root + + property var values: [] + property real peak: 1 + property int capacity: 40 + property color colour: Colours.signal + property bool fill: true + property real lineWidth: 2 + + readonly property real scaleTop: root.peak > 0 ? root.peak : Math.max(1, root.values.reduce((m, v) => Math.max(m, v), 0)) + readonly property var pts: { + const n = root.values.length; + const step = root.width / Math.max(1, root.capacity - 1); + const h = root.height - root.lineWidth * 2; + return root.values.map((v, i) => Qt.point(root.width - (n - 1 - i) * step, root.lineWidth + h - Math.min(1, v / root.scaleTop) * h)); + } + + function curve(p: var): string { + if (p.length < 2) + return ""; + let d = `M ${p[0].x} ${p[0].y}`; + for (let i = 0; i < p.length - 1; i++) { + const a = p[Math.max(0, i - 1)], b = p[i], c = p[i + 1], e = p[Math.min(p.length - 1, i + 2)]; + d += ` C ${b.x + (c.x - a.x) / 6} ${b.y + (c.y - a.y) / 6} ${c.x - (e.x - b.x) / 6} ${c.y - (e.y - b.y) / 6} ${c.x} ${c.y}`; + } + return d; + } + + readonly property string line: root.curve(root.pts) + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: -1 + fillGradient: LinearGradient { + x1: 0 + y1: 0 + x2: 0 + y2: root.height + + GradientStop { + position: 0 + color: Qt.alpha(root.colour, root.fill ? 0.28 : 0) + } + + GradientStop { + position: 1 + color: Qt.alpha(root.colour, 0) + } + } + + PathSvg { + path: root.line ? `${root.line} L ${root.pts[root.pts.length - 1].x} ${root.height} L ${root.pts[0].x} ${root.height} Z` : "" + } + } + + ShapePath { + strokeWidth: root.lineWidth + strokeColor: root.colour + fillColor: "transparent" + capStyle: ShapePath.RoundCap + joinStyle: ShapePath.RoundJoin + + PathSvg { + path: root.line + } + } + } + + Rectangle { + readonly property var at: root.pts.length > 0 ? root.pts[root.pts.length - 1] : null + + visible: at !== null + x: (at?.x ?? 0) - width / 2 + y: (at?.y ?? 0) - height / 2 + width: 7 + height: 7 + radius: Math.min(width, height) / 2 + color: root.colour + + Rectangle { + anchors.centerIn: parent + width: 18 + height: 18 + radius: Math.min(width, height) / 2 + color: Qt.alpha(root.colour, 0.18) + } + } +} diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/README.md b/Configs/quickshell/aphotic/modules/dashboard/spine/README.md deleted file mode 100644 index cec84a93..00000000 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Signal Spine - -The default Dashboard tab layout. It is also the Command Center's one live view of the machine: - -- `SpineSurface.qml` -- the surface: clock, headline readouts, a detail row - (temperatures, storage, upload, battery), the machine's - terrain above the spine, the day's leaves below it in equal slots. The - surface draws its own dark gradient, so a bright wallpaper never - outshouts the text. -- `SpineTerrain.qml` -- the canvas that draws CPU history as a ridge and - network as a low band along the floor. Memory stays a readout. -- `SpineProbe.qml` -- the offscreen render probe. It copies to the - config root at run time because `qs -p` resolves `qs.*` imports only - from the config root. - -The Dashboard tab shows it when `dashboardLayout` is `"spine"`, the default (Settings, Appearance, Dashboard layout). `"classic"` shows the cards and brings back the Performance tab, since the cards carry no machine stats. The leaves -(`DashMedia`, `DashPomodoro`, `DashQuickToggles`, `DashCalendar`) are the -existing dashboard widgets, reused unmodified, so the Spine rides -along with any token or copy pass made to those files. - -## Running the probe - -From `Configs/quickshell/aphotic/`: - -```sh -cp modules/dashboard/spine/SpineProbe.qml SpineProbeRun.qml -QT_QPA_PLATFORM=offscreen \ - HOME=/tmp/opencode/spine-home \ - SPINE_W=1920 SPINE_H=1080 SPINE_MS=14000 \ - SPINE_OUT=/tmp/opencode/spine-1920x1080.png \ - qs -p SpineProbeRun.qml -rm SpineProbeRun.qml -``` - -`HOME` points at a throwaway directory holding copies of -`~/.local/state/aphotic/palette.json`, `settings.json` and -`~/.config/awww/current-wallpaper`, so the probe renders the operator's -real dark palette over their wallpaper (plus the 0.35 dim layer, the -same composition the dashboard window uses) without touching the real -files. `SPINE_OPEN_DELAY=500 SPINE_MOTION_MS=150` grabs a frame -mid-entrance instead. The staged files are copies made at run time and -the originals are only ever read. - -Run it a second time with `SPINE_W=3440 SPINE_H=1440` for the -ultrawide render. The CPU ridge grows one sample per service poll while -the surface is open, so a longer `SPINE_MS` draws more terrain; the -grab happens once, then the probe quits. - -The signal-text clock needs `shaders/signal.frag.qsb`, which installs -build from `shaders/signal.frag`. On a checkout without it the clock -falls back to plain text by design. To preview the real effect locally, -build the shader (same command `lib/install/config_deploy.sh` uses) and -do not commit the result: - -```sh -/usr/lib/qt6/bin/qsb --glsl "100es,120,150" --hlsl 50 --msl 12 \ - -o shaders/signal.frag.qsb shaders/signal.frag -``` - -The probe renders with real service data. With no network fetch or no -players running, the weather leaf and the media leaf show their honest -empty states rather than invented numbers. diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineProbe.qml b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineProbe.qml deleted file mode 100644 index c23dc505..00000000 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineProbe.qml +++ /dev/null @@ -1,100 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import qs.components -import qs.services -import "modules/dashboard/spine" as Spine - -// Never maps a window on a live desktop: run with QT_QPA_PLATFORM=offscreen (Quickshell). -// qs resolves `qs.*` imports only from the config root, so a wrapper copies this file to the root. -ShellRoot { - id: probe - - readonly property int outW: Number(Quickshell.env("SPINE_W") || 1920) - readonly property int outH: Number(Quickshell.env("SPINE_H") || 1080) - readonly property int grabAfterMs: Number(Quickshell.env("SPINE_MS") || 4000) - readonly property int openAfterMs: Number(Quickshell.env("SPINE_OPEN_DELAY") || 0) - readonly property int motionMs: Number(Quickshell.env("SPINE_MOTION_MS") || 150) - readonly property string outFile: Quickshell.env("SPINE_OUT") || "/tmp/opencode/spine.png" - readonly property string wallpaper: Quickshell.env("SPINE_WALLPAPER") || "/tmp/opencode/spine-wallpaper.jpg" - - Component.onCompleted: console.log("PROBE size", probe.outW, "x", probe.outH, "run", probe.grabAfterMs, "ms", "open", probe.openAfterMs, "motion", probe.motionMs) - - FloatingWindow { - implicitWidth: probe.outW - implicitHeight: probe.outH - visible: true - color: Colours.shadow - - SystemUsageWatch {} - NetworkUsageWatch {} - - Item { - id: grabHost - - anchors.fill: parent - - Image { - anchors.fill: parent - source: "file://" + probe.wallpaper - fillMode: Image.PreserveAspectCrop - asynchronous: false - } - - Rectangle { - anchors.fill: parent - color: Colours.shadow - opacity: 0.35 - } - - Spine.SpineSurface { - id: surface - - anchors.fill: parent - shown: probe.openAfterMs <= 0 - } - - Timer { - interval: 200 - running: true - - onTriggered: { - const count = 48; - const cpu = []; - const net = []; - for (let i = 0; i < count; i++) { - const wave = 0.18 + 0.16 * Math.sin(i / 4.5) + 0.08 * Math.sin(i / 1.7); - const spike = (i >= 20 && i <= 23) ? 0.5 : 0; - cpu.push(Math.min(1, Math.max(0.04, wave + spike))); - net.push(Math.max(0, 1024 * (180 + 140 * Math.sin(i / 3.1) + (i >= 33 && i <= 35 ? 900 : 0)))); - } - surface.terrain.seedForProbe(cpu, net); - console.log("PROBE seeded terrain"); - } - } - - Timer { - id: openTimer - - interval: probe.openAfterMs - running: probe.openAfterMs > 0 - - onTriggered: { - surface.shown = true; - console.log("PROBE opened"); - } - } - - Timer { - interval: probe.openAfterMs > 0 ? probe.openAfterMs + probe.motionMs : probe.grabAfterMs - running: true - - onTriggered: grabHost.grabToImage(result => { - console.log("PROBE saved", probe.outFile, result.saveToFile(probe.outFile)); - Qt.quit(); - }) - } - } - } -} diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml deleted file mode 100644 index 9f9270d2..00000000 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineSurface.qml +++ /dev/null @@ -1,438 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Bluetooth -import Quickshell.Services.UPower -import qs.config -import qs.components -import qs.services -import qs.services.profile -import qs.modules.dashboard - -Item { - id: root - - readonly property real margin: Tokens.padding.extraExtraLarge - readonly property bool portrait: stage.height > stage.width - - // Inside the dashboard's own tab frame the surface draws no dark - // gradient of its own; the frame and the window's dim layer already - // carry that. Default false keeps the standalone probe's full - // composition unchanged. - property bool embedded: false - - property bool shown: true - - SystemUsageWatch {} - NetworkUsageWatch {} - - readonly property bool mediaAlive: Players.active !== null - readonly property bool focusAlive: Pomodoro.running - readonly property bool weatherAlive: Weather.hasData - readonly property bool controlsAlive: Nmcli.wifiEnabled || (Bluetooth.defaultAdapter?.enabled ?? false) || DoNotDisturb.enabled - - readonly property var leaves: [mediaLeaf, focusLeaf, weatherLeaf, controlsLeaf] - readonly property var shownLeaves: leaves.filter(l => l.present) - - property alias terrain: terrain - - function rate(bytes: real): string { - if (bytes >= 1024 * 1024) - return qsTr("%1 MiB/s").arg((bytes / 1048576).toFixed(1)); - return qsTr("%1 KiB/s").arg(Math.round(bytes / 1024)); - } - - // Measured card memory in MiB, from the same source Flow draws. - readonly property var vram: ResourceEngine.resourceSpec("gpu-vram")?.measured ?? null - - function gib(kib: real): string { - return (kib / 1048576).toFixed(1); - } - - function temp(celsius: real): string { - const f = Config.services.useFahrenheitPerformance; - return `${Math.round(f ? celsius * 1.8 + 32 : celsius)}°${f ? "F" : "C"}`; - } - - property int diskIndex: 0 - readonly property var disk: SystemUsage.disks.length > 0 ? SystemUsage.disks[root.diskIndex % SystemUsage.disks.length] : null - - component Leaf: Item { - id: leaf - - required property string label - required property bool alive - property bool present: true - default property alias content: body.data - - readonly property int slot: root.shownLeaves.indexOf(leaf) - readonly property real slotWidth: root.portrait ? dayZone.width : dayZone.width / Math.max(1, root.shownLeaves.length) - - visible: leaf.present - x: root.portrait ? 0 : leaf.slot * leaf.slotWidth - y: root.portrait ? root.shownLeaves.slice(0, leaf.slot).reduce((sum, l) => sum + l.implicitHeight + Tokens.spacing.extraLarge, 0) : 0 - width: leaf.slotWidth - implicitHeight: title.implicitHeight + Tokens.spacing.medium + body.childrenRect.height - - Rectangle { - visible: root.portrait - x: -Tokens.spacing.medium - width: 2 - height: leaf.implicitHeight - radius: Tokens.rounding.full - color: Colours.signalStyle.accentLine - opacity: leaf.alive ? 1 : 0 - } - - SectionLabel { - id: title - - anchors.horizontalCenter: root.portrait ? undefined : parent.horizontalCenter - label: leaf.label - alive: leaf.alive - } - - Item { - id: body - - anchors.top: title.bottom - anchors.topMargin: Tokens.spacing.medium - x: root.portrait ? 0 : (leaf.width - body.childrenRect.width) / 2 - width: body.childrenRect.width - height: body.childrenRect.height - } - } - - SurfaceReveal { - id: reveal - - anchors.fill: parent - shown: root.shown - edge: "bottom" - - Item { - id: stage - - anchors.fill: parent - - Rectangle { - anchors.fill: parent - visible: !root.embedded - gradient: Gradient { - GradientStop { - position: 0 - color: Qt.alpha(Colours.shadow, 0.66) - } - GradientStop { - position: 0.62 - color: Qt.alpha(Colours.shadow, 0.66) - } - GradientStop { - position: 1 - color: Qt.alpha(Colours.shadow, 0.4) - } - } - } - - SignalText { - id: clock - - x: root.margin - y: root.margin - text: { - Time.minuteStr; - return Time.format(Settings.twelveHourClock ? "h:mm AP" : "HH:mm"); - } - family: Tokens.font.headline.large.family - ink: Colours.ink - glow: Colours.signalStyle.accentLine - pixelSize: root.portrait ? 88 : Math.round(Math.max(72, Math.min(128, stage.height * 0.1))) - weight: Font.DemiBold - letterSpacing: 2 - property real progress: 0 - - Component.onCompleted: { - if (Motion.reducedMotion) - clock.progress = 1; - } - - Connections { - target: Time - - function onMinuteStrChanged(): void { - if (!Motion.reducedMotion) - clock.progress = 0; - } - } - - Timer { - id: shimmerClock - - interval: 84 - running: !Motion.reducedMotion && clock.progress < 1 - repeat: true - onTriggered: { - clock.time = (clock.time + 0.25) % 3600; - clock.progress = Math.min(1, clock.progress + 0.08); - } - } - - ActivityProbe { - name: "dashboard.spine.shimmer" - kind: "render" - timer: shimmerClock - } - } - - StyledText { - id: dateLine - - anchors.left: parent.left - anchors.leftMargin: root.margin - anchors.top: clock.bottom - anchors.topMargin: Tokens.spacing.extraSmall - text: Time.format("dddd, MMMM d") - color: Colours.ink - opacity: 0.72 - font: Tokens.font.title.builders.small.weight(Font.Medium).letterSpacing(0.6).build() - } - - component CycleReadout: Item { - id: cycle - - required property string value - required property string label - property bool cycles: false - signal next - - implicitWidth: cycleRow.implicitWidth - implicitHeight: cycleRow.implicitHeight - - Readout { - id: cycleRow - - small: true - value: cycle.value - label: cycle.label - } - - Interact { - anchors.fill: parent - anchors.margins: -Tokens.padding.small - enabled: cycle.cycles - radius: Tokens.rounding.small - onClicked: cycle.next() - } - } - - Column { - id: readouts - - spacing: Tokens.spacing.large - anchors.right: root.portrait ? undefined : parent.right - anchors.rightMargin: root.margin - anchors.left: root.portrait ? parent.left : undefined - anchors.leftMargin: root.margin - y: root.portrait ? dateLine.y + dateLine.height + Tokens.spacing.extraLarge : Math.max(root.margin, dateLine.y + dateLine.height - readouts.height) - - Grid { - anchors.right: root.portrait ? undefined : parent.right - columns: root.portrait ? 2 : 5 - columnSpacing: Tokens.spacing.extraLarge * 1.5 - rowSpacing: Tokens.spacing.large - - Readout { - value: `${Math.round(SystemUsage.cpuPerc * 100)}%` - label: qsTr("CPU") - } - - Readout { - visible: (root.vram?.total ?? 0) > 0 - value: ((root.vram?.used ?? 0) / 1024).toFixed(1) - total: qsTr("/ %1 GiB").arg(((root.vram?.total ?? 0) / 1024).toFixed(1)) - label: qsTr("VRAM") - } - - Readout { - value: SystemUsage.gpuStatsAvailable ? `${Math.round(SystemUsage.gpuPerc * 100)}%` : qsTr("N/A") - label: qsTr("GPU load") - } - - Readout { - value: root.gib(SystemUsage.memUsed) - total: qsTr("/ %1 GiB").arg(root.gib(SystemUsage.memTotal)) - label: qsTr("Memory") - } - - Readout { - value: root.rate(NetworkUsage.downloadSpeed) - label: qsTr("Download") - } - } - - Row { - id: details - - anchors.right: root.portrait ? undefined : parent.right - layoutDirection: root.portrait ? Qt.LeftToRight : Qt.RightToLeft - spacing: Tokens.spacing.extraLarge * 1.5 - - CycleReadout { - visible: Config.dashboard.performance.showBattery && UPower.displayDevice.isLaptopBattery - value: `${Math.round(UPower.displayDevice.percentage * 100)}%` - label: qsTr("Battery") - } - - CycleReadout { - visible: Config.dashboard.performance.showNetwork - value: root.rate(NetworkUsage.uploadSpeed) - label: qsTr("Upload") - } - - CycleReadout { - visible: Config.dashboard.performance.showStorage && root.disk !== null - value: root.disk ? `${Math.round(root.disk.perc * 100)}%` : "" - label: !root.disk ? "" : SystemUsage.disks.length > 1 ? qsTr("Storage %1 %2/%3").arg(root.disk.mount).arg(root.diskIndex % SystemUsage.disks.length + 1).arg(SystemUsage.disks.length) : qsTr("Storage %1").arg(root.disk.mount) - cycles: SystemUsage.disks.length > 1 - onNext: root.diskIndex = (root.diskIndex + 1) % SystemUsage.disks.length - } - - CycleReadout { - visible: Config.dashboard.performance.showGpu && SystemUsage.gpuDetected && SystemUsage.gpuTemp > 0 - value: root.temp(SystemUsage.gpuTemp) - label: SystemUsage.gpus.length > 1 ? qsTr("GPU %1 temp").arg(SystemUsage.selectedGpuIndex + 1) : qsTr("GPU temp") - cycles: SystemUsage.gpus.length > 1 - onNext: SystemUsage.selectGpu((SystemUsage.selectedGpuIndex + 1) % SystemUsage.gpus.length) - } - - CycleReadout { - visible: Config.dashboard.performance.showCpu && SystemUsage.cpuTemp > 0 - value: root.temp(SystemUsage.cpuTemp) - label: qsTr("CPU temp") - } - } - } - - SpineTerrain { - id: terrain - - anchors.left: parent.left - anchors.right: parent.right - y: (root.portrait ? readouts.y + readouts.height : Math.max(readouts.y + readouts.height, dateLine.y + dateLine.height)) + Tokens.spacing.extraLarge * 1.5 - height: { - if (root.portrait) - return Math.max(110, Math.min(180, stage.height * 0.14)); - const below = 3 + Tokens.spacing.extraExtraLarge + Math.max(dayZone.height, calendarLeaf.height) + root.margin * 2; - return Math.max(90, Math.min(300, stage.height - terrain.y - below)); - } - } - - Item { - id: spine - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: terrain.bottom - height: 3 - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - height: 1 - color: Colours.signalStyle.hairline - } - - Repeater { - model: root.portrait ? [] : root.shownLeaves - - Rectangle { - id: segment - - required property Item modelData - - x: dayZone.x + segment.modelData.x + segment.modelData.width / 2 - width / 2 - y: 0 - width: Tokens.spacing.extraLarge * 2 - height: 2 - radius: Tokens.rounding.full - color: Colours.signalStyle.accentLine - opacity: segment.modelData.alive ? 1 : 0 - visible: segment.opacity > 0 - - Behavior on opacity { - Anim { - type: Anim.FastEffects - } - } - } - } - } - - Item { - id: dayZone - - x: root.margin - anchors.top: spine.bottom - anchors.topMargin: Tokens.spacing.extraExtraLarge - width: root.portrait ? stage.width * 0.48 - root.margin : calendarLeaf.x - root.margin - Tokens.spacing.extraExtraLarge - height: childrenRect.height - - Leaf { - id: mediaLeaf - - label: qsTr("Now playing") - alive: root.mediaAlive - - DashMedia {} - } - - Leaf { - id: focusLeaf - - label: qsTr("Timer") - alive: root.focusAlive - - DashPomodoro {} - } - - Leaf { - id: weatherLeaf - - label: qsTr("Weather") - alive: root.weatherAlive - present: Weather.hasData - - DashWeather {} - } - - Leaf { - id: controlsLeaf - - label: qsTr("Controls") - alive: root.controlsAlive - - DashQuickToggles {} - } - } - - Column { - id: calendarLeaf - - anchors.right: parent.right - anchors.rightMargin: root.margin - anchors.top: spine.bottom - anchors.topMargin: Tokens.spacing.extraExtraLarge - spacing: Tokens.spacing.medium - - SectionLabel { - anchors.horizontalCenter: parent.horizontalCenter - label: qsTr("Calendar") - alive: true - } - - DashCalendar {} - } - } - } -} diff --git a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml b/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml deleted file mode 100644 index dfc2f6c0..00000000 --- a/Configs/quickshell/aphotic/modules/dashboard/spine/SpineTerrain.qml +++ /dev/null @@ -1,109 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.config -import qs.services - -Canvas { - id: root - - readonly property int capacity: 48 - readonly property real netFlatBelow: 64 * 1024 - property var cpuTrail: [] - property var netTrail: [] - - onCpuTrailChanged: root.requestPaint() - onNetTrailChanged: root.requestPaint() - onWidthChanged: root.requestPaint() - onHeightChanged: root.requestPaint() - - function pushCpu(value: real): void { - const next = root.cpuTrail.concat([Math.max(0, Math.min(1, value))]); - root.cpuTrail = next.length > root.capacity ? next.slice(next.length - root.capacity) : next; - } - - function pushNet(value: real): void { - const next = root.netTrail.concat([Math.max(0, value)]); - root.netTrail = next.length > root.capacity ? next.slice(next.length - root.capacity) : next; - } - - function seedForProbe(cpu: var, net: var): void { - root.cpuTrail = cpu; - root.netTrail = net; - } - - Connections { - target: SystemUsage - - function onCpuPercChanged(): void { - root.pushCpu(SystemUsage.cpuPerc); - } - } - - Connections { - target: NetworkUsage - - function onDownloadSpeedChanged(): void { - root.pushNet(NetworkUsage.downloadSpeed); - } - } - - function _xAt(i: int, count: int): real { - const step = root.width / (root.capacity - 1); - return root.width - (count - 1 - i) * step; - } - - onPaint: { - const ctx = root.getContext("2d"); - const h = root.height; - ctx.clearRect(0, 0, root.width, h); - - const net = root.netTrail; - if (net.length > 1) { - let peak = root.netFlatBelow; - for (let i = 0; i < net.length; i++) - peak = Math.max(peak, net[i]); - const bandH = h * 0.28; - - ctx.beginPath(); - ctx.moveTo(root._xAt(0, net.length), h); - for (let i = 0; i < net.length; i++) - ctx.lineTo(root._xAt(i, net.length), h - (net[i] / peak) * bandH); - ctx.lineTo(root.width, h); - ctx.closePath(); - ctx.fillStyle = Qt.alpha(Colours.signalSecondary, 0.09); - ctx.fill(); - } - - const cpu = root.cpuTrail; - if (cpu.length > 1) { - const crest = 0.92; - const yAt = i => h - cpu[i] * h * crest; - - const fill = ctx.createLinearGradient(0, 0, 0, h); - fill.addColorStop(0, Qt.alpha(Colours.signalStyle.accentLine, 0.26)); - fill.addColorStop(1, Qt.alpha(Colours.signalStyle.accentLine, 0)); - - ctx.beginPath(); - ctx.moveTo(root._xAt(0, cpu.length), h); - for (let i = 0; i < cpu.length; i++) - ctx.lineTo(root._xAt(i, cpu.length), yAt(i)); - ctx.lineTo(root.width, h); - ctx.closePath(); - ctx.fillStyle = fill; - ctx.fill(); - - ctx.beginPath(); - for (let i = 0; i < cpu.length; i++) { - if (i === 0) - ctx.moveTo(root._xAt(i, cpu.length), yAt(i)); - else - ctx.lineTo(root._xAt(i, cpu.length), yAt(i)); - } - ctx.strokeStyle = Qt.alpha(Colours.signalStyle.accentLine, 0.9); - ctx.lineWidth = 1.5; - ctx.lineJoin = "round"; - ctx.stroke(); - } - } -} diff --git a/Configs/quickshell/aphotic/modules/settings/panes/AppearancePane.qml b/Configs/quickshell/aphotic/modules/settings/panes/AppearancePane.qml index b78ad827..01b3e91f 100644 --- a/Configs/quickshell/aphotic/modules/settings/panes/AppearancePane.qml +++ b/Configs/quickshell/aphotic/modules/settings/panes/AppearancePane.qml @@ -481,7 +481,7 @@ Item { Repeater { model: [ - { id: "spine", label: qsTr("Signal Spine") }, + { id: "spine", label: qsTr("Live tiles") }, { id: "classic", label: qsTr("Classic") } ] diff --git a/tests/design_language_baseline.json b/tests/design_language_baseline.json index 927be0fa..9cab5b81 100644 --- a/tests/design_language_baseline.json +++ b/tests/design_language_baseline.json @@ -46,12 +46,13 @@ "files": {} }, "material_icon_uses": { - "total": 197, + "total": 201, "files": { "Configs/quickshell/aphotic/components/AppIcon.qml": 1, "Configs/quickshell/aphotic/components/ColorPickerField.qml": 4, "Configs/quickshell/aphotic/components/SettingsRow.qml": 1, "Configs/quickshell/aphotic/components/SettingsSection.qml": 2, + "Configs/quickshell/aphotic/components/SignalTile.qml": 1, "Configs/quickshell/aphotic/modules/background/DesktopContextMenu.qml": 1, "Configs/quickshell/aphotic/modules/background/Wallpaper.qml": 1, "Configs/quickshell/aphotic/modules/bar/MinimalIndicators.qml": 1, @@ -99,6 +100,7 @@ "Configs/quickshell/aphotic/modules/dashboard/DashWallpapersTab.qml": 1, "Configs/quickshell/aphotic/modules/dashboard/DashWeather.qml": 2, "Configs/quickshell/aphotic/modules/dashboard/PerformanceTab.qml": 8, + "Configs/quickshell/aphotic/modules/dashboard/bento/BentoDashboard.qml": 3, "Configs/quickshell/aphotic/modules/intelligence/IntelligenceHeader.qml": 2, "Configs/quickshell/aphotic/modules/intelligence/IntelligenceInput.qml": 1, "Configs/quickshell/aphotic/modules/intelligence/IntelligenceSessionList.qml": 1,