Files
NixOS-Configs/home/hypr/quickshell/components/SystemHUD.qml
2026-08-31 21:20:26 -07:00

698 lines
26 KiB
QML

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import "../theme"
import ".."
PanelWindow {
id: root
anchors {
top: true
bottom: true
left: true
}
margins {
left: Theme.sidebarWidth
top: Theme.topStripHeight
}
implicitWidth: Theme.hudWidth
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "quickshell-systemhud"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
visible: GlobalState.systemHudVisible
color: "transparent"
property real slideOffset: GlobalState.systemHudVisible ? 0 : -Theme.hudWidth
Behavior on slideOffset {
NumberAnimation {
duration: Theme.animSmooth
easing.type: Easing.OutQuint
}
}
Shortcut {
sequence: "Escape"
onActivated: GlobalState.systemHudVisible = false
}
component SectionHeader: Item {
property string sectionNum: "00"
property string title: "Section"
implicitWidth: parent.width
implicitHeight: 24
RowLayout {
anchors.fill: parent
spacing: 8
Text {
text: sectionNum + " //"
font.family: Theme.fontMono
font.pixelSize: 12
color: Theme.accentVolt
}
Text {
text: title
font.family: Theme.fontSerif
font.italic: true
font.pixelSize: 16
color: Theme.textPrimary
Layout.fillWidth: true
}
}
}
component ToggleCard: Rectangle {
property string title: ""
property string icon: ""
property string glyph: "" // Nerd Font glyph alternative to icon file
property bool active: false
signal clicked()
Layout.preferredWidth: 160
Layout.preferredHeight: 56
color: Theme.bgPrimary
border.color: active ? Qt.rgba(Theme.accentVolt.r, Theme.accentVolt.g, Theme.accentVolt.b, 0.3) : Theme.glassBorder
border.width: 1
radius: 4
Behavior on border.color { ColorAnimation { duration: Theme.animFast } }
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: clicked()
onEntered: if (!active) border.color = Theme.glassBorderHover
onExited: if (!active) border.color = Theme.glassBorder
}
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
Text {
visible: parent.parent.glyph !== ""
text: parent.parent.glyph
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 20
color: parent.parent.active ? Theme.accentVolt : Theme.textMuted
}
Image {
source: parent.parent.icon && parent.parent.glyph === "" ? Qt.resolvedUrl("../" + parent.parent.icon) : ""
Layout.preferredWidth: 24
Layout.preferredHeight: 24
sourceSize: Qt.size(24, 24)
visible: parent.parent.icon !== "" && parent.parent.glyph === ""
}
Text {
text: parent.parent.title
color: Theme.textPrimary
font.family: Theme.fontSans
font.pixelSize: 13
Layout.fillWidth: true
elide: Text.ElideRight
}
}
}
component CustomSlider: Item {
property real value: 0
signal valueChangedUser(real newVal)
implicitWidth: parent.width
implicitHeight: 24
Rectangle {
id: track
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 40
height: 6
color: Theme.bgSecondary
radius: 3
Rectangle {
id: fill
width: track.width * (parent.parent.value / 100)
height: track.height
color: Theme.accentVolt
radius: 3
}
Rectangle {
width: 14
height: 14
radius: 7
color: Theme.textPrimary
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, fill.width - 7)
}
MouseArea {
anchors.fill: parent
anchors.margins: -10
function updateValue(mouseX) {
let p = Math.max(0, Math.min(1, mouseX / track.width))
parent.parent.valueChangedUser(p * 100)
}
onPressed: (mouse) => updateValue(mouse.x)
onPositionChanged: (mouse) => updateValue(mouse.x)
}
}
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: Math.round(parent.value) + "%"
color: Theme.textPrimary
font.family: Theme.fontMono
font.pixelSize: 12
}
}
component ProgressBarItem: ColumnLayout {
property string label: ""
property real percent: 0
property string valueText: ""
property color barColor: Theme.accentVolt
Layout.fillWidth: true
spacing: 4
RowLayout {
Layout.fillWidth: true
Text {
text: parent.parent.label
color: Theme.textPrimary
font.family: Theme.fontMono
font.pixelSize: 12
}
Item { Layout.fillWidth: true }
Text {
text: parent.parent.valueText
color: Theme.textSecondary
font.family: Theme.fontMono
font.pixelSize: 12
}
}
Rectangle {
Layout.fillWidth: true
height: 8
color: Theme.bgSecondary
radius: 4
Rectangle {
width: parent.width * (parent.parent.percent / 100)
height: parent.height
color: parent.parent.barColor
radius: 4
Behavior on width { NumberAnimation { duration: Theme.animFast; easing.type: Easing.OutCubic } }
Behavior on color { ColorAnimation { duration: Theme.animFast } }
}
}
}
Rectangle {
id: panelContent
anchors.fill: parent
color: Theme.glassBg
opacity: GlobalState.systemHudVisible ? 1 : 0
transform: Translate { x: slideOffset }
Behavior on opacity {
NumberAnimation {
duration: Theme.animSmooth
easing.type: Easing.OutQuint
}
}
Rectangle {
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 1
color: Theme.glassBorder
}
Item {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 60
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 1
color: Theme.glassBorder
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 20
anchors.rightMargin: 20
Text {
text: "System HUD"
font.family: Theme.fontSerif
font.italic: true
font.pixelSize: 19
color: Theme.textPrimary
Layout.fillWidth: true
}
Text {
text: "✕"
color: Theme.textMuted
font.pixelSize: 16
MouseArea {
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
onEntered: parent.color = Theme.textPrimary
onExited: parent.color = Theme.textMuted
onClicked: GlobalState.systemHudVisible = false
}
}
}
}
ScrollView {
id: scrollView
anchors.top: header.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
contentWidth: availableWidth
clip: true
ColumnLayout {
id: contentLayout
width: parent.width - 40
x: 20
spacing: 32
Item { Layout.preferredHeight: 16 } // Top padding
// Section 1: Toggles
ColumnLayout {
id: sectionToggles
Layout.fillWidth: true
spacing: 16
SectionHeader { sectionNum: "01"; title: "Toggles" }
GridLayout {
columns: 2
columnSpacing: 12
rowSpacing: 12
Layout.fillWidth: true
ToggleCard {
title: GlobalState.wifiSsid !== "" ? GlobalState.wifiSsid : "Wi-Fi"
icon: "assets/wifi.svg"
active: GlobalState.wifiConnected
onClicked: {
Quickshell.execDetached(["sh", "-c", "nm-connection-editor || foot -e nmtui"])
}
}
ToggleCard {
title: "Bluetooth"
icon: "assets/bluetooth.svg"
active: false // Adjust if there's a bluetooth state
onClicked: {
let proc = Quickshell.createProcess("sh", ["-c", "foot -e bluetoothctl"])
proc.start()
}
}
ToggleCard {
property bool dndActive: false
title: "Do Not Disturb"
glyph: "\uF186"
active: dndActive
onClicked: dndActive = !dndActive
}
ToggleCard {
title: GlobalState.volumeMuted ? "Muted" : "Sound"
glyph: GlobalState.volumeMuted ? "\uF026" : "\uF028"
active: !GlobalState.volumeMuted
onClicked: GlobalState.toggleVolumeMute()
}
}
}
// Section 2: Volume
ColumnLayout {
id: sectionVolume
Layout.fillWidth: true
spacing: 16
SectionHeader { sectionNum: "02"; title: "Volume" }
CustomSlider {
Layout.fillWidth: true
value: GlobalState.volume
onValueChangedUser: (val) => GlobalState.volume = val // Adjust to actual volume setter if needed
}
RowLayout {
Layout.fillWidth: true
Text {
text: GlobalState.volumeMuted ? "Unmute" : "Mute"
color: Theme.textSecondary
font.family: Theme.fontSans
font.pixelSize: 13
MouseArea {
anchors.fill: parent
onClicked: GlobalState.toggleVolumeMute()
}
}
Item { Layout.fillWidth: true }
Text {
text: "Sound Preferences..."
color: Theme.textMuted
font.family: Theme.fontSans
font.pixelSize: 13
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: parent.color = Theme.textPrimary
onExited: parent.color = Theme.textMuted
onClicked: {
let proc = Quickshell.createProcess("pavucontrol", [])
proc.start()
}
}
}
}
}
// Section 3: Brightness
ColumnLayout {
id: sectionBrightness
Layout.fillWidth: true
spacing: 16
SectionHeader { sectionNum: "03"; title: "Brightness" }
CustomSlider {
Layout.fillWidth: true
value: GlobalState.brightness
onValueChangedUser: (val) => GlobalState.setBrightness(val)
}
}
// Section 4: Performance
ColumnLayout {
id: sectionPerformance
Layout.fillWidth: true
spacing: 16
SectionHeader { sectionNum: "04"; title: "Performance" }
ProgressBarItem {
label: "CPU"
percent: GlobalState.cpuPercent
valueText: GlobalState.cpuPercent + "%"
barColor: GlobalState.cpuPercent > 80 ? Theme.accentCoral : Theme.accentVolt
}
ProgressBarItem {
label: "RAM"
percent: (GlobalState.memUsedGb / Math.max(1, GlobalState.memTotalGb)) * 100
valueText: GlobalState.memUsedGb + " / " + GlobalState.memTotalGb + " GB"
}
ProgressBarItem {
label: "DISK"
percent: GlobalState.diskPercent
valueText: GlobalState.diskUsedGb + " / " + GlobalState.diskTotalGb + " GB"
barColor: GlobalState.diskPercent > 90 ? Theme.accentCoral : Theme.accentVolt
}
ProgressBarItem {
label: "VRAM"
percent: (GlobalState.gpuVramUsed / Math.max(1, GlobalState.gpuVramTotal)) * 100
valueText: GlobalState.gpuVramUsed + " / " + GlobalState.gpuVramTotal + " GB"
}
}
// Section 5: Network
ColumnLayout {
id: sectionNetwork
Layout.fillWidth: true
spacing: 16
SectionHeader { sectionNum: "05"; title: "Network" }
RowLayout {
spacing: 8
Rectangle {
width: 8
height: 8
radius: 4
color: GlobalState.wifiConnected ? Theme.accentVolt : Theme.accentCoral
}
Text {
text: {
if (!GlobalState.wifiConnected) return "Disconnected";
if (GlobalState.connectionType === "wifi") return "Wi-Fi — " + (GlobalState.wifiSsid !== "" ? GlobalState.wifiSsid : "unknown");
if (GlobalState.connectionType === "eth") return "Ethernet";
return "Connected";
}
color: Theme.textPrimary
font.family: Theme.fontSans
font.pixelSize: 14
}
}
Text {
visible: GlobalState.wifiConnected
text: "IP: " + (GlobalState.ipAddress !== "" ? GlobalState.ipAddress : "—")
color: Theme.textSecondary
font.family: Theme.fontMono
font.pixelSize: 13
}
Text {
text: "Network Preferences..."
color: Theme.textMuted
font.family: Theme.fontSans
font.pixelSize: 13
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: parent.color = Theme.textPrimary
onExited: parent.color = Theme.textMuted
onClicked: {
Quickshell.execDetached(["sh", "-c", "nm-connection-editor || foot -e nmtui"])
}
}
}
}
// Section 6: Battery
ColumnLayout {
id: sectionBattery
Layout.fillWidth: true
spacing: 16
visible: GlobalState.batteryPresent
SectionHeader { sectionNum: "06"; title: "Battery" }
RowLayout {
spacing: 16
Text {
text: GlobalState.batteryPercent + "%"
font.family: Theme.fontMono
font.pixelSize: 24
color: GlobalState.batteryStatus === "Charging" ? Theme.accentVolt :
(GlobalState.batteryPercent < 20 ? Theme.accentCoral : Theme.textPrimary)
}
Text {
text: GlobalState.batteryStatus
color: Theme.textSecondary
font.family: Theme.fontSans
font.pixelSize: 14
}
}
}
// Section 7: Now Playing
ColumnLayout {
id: sectionNowPlaying
Layout.fillWidth: true
spacing: 16
visible: GlobalState.mediaTitle !== ""
SectionHeader { sectionNum: "07"; title: "Now Playing" }
ColumnLayout {
spacing: 4
Text {
text: GlobalState.mediaTitle
color: Theme.textPrimary
font.family: Theme.fontSans
font.bold: true
font.pixelSize: 14
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
text: GlobalState.mediaArtist
color: Theme.textSecondary
font.family: Theme.fontSans
font.pixelSize: 12
Layout.fillWidth: true
elide: Text.ElideRight
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 16
Text {
text: "⏮"
font.pixelSize: 20
color: Theme.textSecondary
MouseArea {
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
onEntered: parent.color = Theme.accentVolt
onExited: parent.color = Theme.textSecondary
onClicked: GlobalState.mediaPrevious()
}
}
Text {
text: GlobalState.mediaPlaying ? "⏸" : "▶"
font.pixelSize: 24
color: Theme.textSecondary
MouseArea {
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
onEntered: parent.color = Theme.accentVolt
onExited: parent.color = Theme.textSecondary
onClicked: GlobalState.mediaPlayPause()
}
}
Text {
text: "⏭"
font.pixelSize: 20
color: Theme.textSecondary
MouseArea {
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
onEntered: parent.color = Theme.accentVolt
onExited: parent.color = Theme.textSecondary
onClicked: GlobalState.mediaNext()
}
}
}
}
// Section 8: Calendar
ColumnLayout {
id: sectionCalendar
Layout.fillWidth: true
spacing: 16
SectionHeader { sectionNum: "08"; title: "Calendar" }
RowLayout {
spacing: 12
Text {
text: GlobalState.timeString
font.family: Theme.fontMono
font.pixelSize: 22
color: Theme.accentVolt
}
Text {
text: GlobalState.fullDateString
font.family: Theme.fontSans
font.pixelSize: 13
color: Theme.textSecondary
}
}
GridLayout {
columns: 7
columnSpacing: 4
rowSpacing: 4
Layout.alignment: Qt.AlignHCenter
Repeater {
model: ["S", "M", "T", "W", "T", "F", "S"]
Text {
text: modelData
font.family: Theme.fontMono
font.pixelSize: 11
color: Theme.textMuted
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: 32
}
}
Repeater {
model: 31
Rectangle {
property bool isToday: (index + 1) === new Date().getDate()
Layout.preferredWidth: 32
Layout.preferredHeight: 32
color: isToday ? Theme.accentVolt : "transparent"
radius: isToday ? 0 : 4
Text {
anchors.centerIn: parent
text: index + 1
color: parent.isToday ? "#ffffff" : Theme.textSecondary
font.family: Theme.fontMono
font.pixelSize: 12
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: if (!parent.isToday) parent.color = Qt.rgba(1, 1, 1, 0.04)
onExited: if (!parent.isToday) parent.color = "transparent"
}
}
}
}
}
Item { Layout.preferredHeight: 20 } // Bottom padding
}
}
}
// Auto-scroll to active section when changed
Connections {
target: GlobalState
function onSystemHudActiveSectionChanged() {
if (!GlobalState.systemHudVisible) return;
let targetY = 0;
if (GlobalState.systemHudActiveSection === "volume") targetY = sectionVolume.y;
else if (GlobalState.systemHudActiveSection === "brightness") targetY = sectionBrightness.y;
else if (GlobalState.systemHudActiveSection === "performance") targetY = sectionPerformance.y;
else if (GlobalState.systemHudActiveSection === "network") targetY = sectionNetwork.y;
else if (GlobalState.systemHudActiveSection === "battery") targetY = sectionBattery.y;
else if (GlobalState.systemHudActiveSection === "nowplaying") targetY = sectionNowPlaying.y;
else if (GlobalState.systemHudActiveSection === "calendar") targetY = sectionCalendar.y;
if (targetY > 0) {
// Ensure targetY doesn't scroll past max possible scroll
let maxY = Math.max(0, contentLayout.height - scrollView.height);
scrollView.ScrollBar.vertical.position = Math.min(targetY, maxY) / contentLayout.height;
}
}
}
}