add full proj

This commit is contained in:
2026-05-31 09:28:44 +02:00
commit f8a4536a02
26 changed files with 1307 additions and 0 deletions

69
widgets/Bar.qml Normal file
View File

@@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "../config"
// One bar instance per monitor.
PanelWindow {
id: panel
required property var modelData
screen: modelData
color: "transparent"
// Auto-rotation flips the output transform and resizes this layer surface
// in place; the old buffer bleeds through the area that isn't fully
// repainted, leaving stale "garbage" pixels. Remapping the surface on any
// geometry change forces Sway to hand us a fresh, fully-painted buffer.
readonly property string geomKey: modelData
? (modelData.width + "x" + modelData.height)
: ""
onGeomKeyChanged: Qt.callLater(function () {
if (!panel.visible) return;
panel.visible = false;
panel.visible = true;
})
anchors {
top: true
left: true
right: true
}
implicitHeight: Theme.barHeight
// background strip
Rectangle {
anchors.fill: parent
color: Theme.barColor
}
// left: workspaces
Workspaces {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.gap + 2
screen: panel.modelData
}
// center: clock
Clock {
anchors.centerIn: parent
}
// right: battery, system metrics, volume, tray
RowLayout {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.gap + 2
spacing: Theme.gap
Battery {}
CpuGraph {}
CpuTemp {}
Ram {}
Disk {}
Network {}
Volume {}
Tray { panelWindow: panel }
}
}

17
widgets/Battery.qml Normal file
View File

@@ -0,0 +1,17 @@
import "../config"
import "../services"
MetricPill {
readonly property bool charging: SysStats.batteryStatus === "Charging"
|| SysStats.batteryStatus === "Full"
visible: SysStats.hasBattery
icon: charging ? Icons.bolt : Icons.battery(SysStats.battery)
iconColor: charging ? Theme.green
: SysStats.battery <= 15 ? Theme.red
: SysStats.battery <= 30 ? Theme.peach
: Theme.green
value: SysStats.battery + "%"
reserve: "100%"
}

52
widgets/Clock.qml Normal file
View File

@@ -0,0 +1,52 @@
import QtQuick
import QtQuick.Layouts
import "../config"
Pill {
id: root
background: Theme.surface1
property var now: new Date()
Timer {
interval: 1000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.now = new Date()
}
Text {
text: Icons.clock
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize + 1
color: Theme.lavender
Layout.alignment: Qt.AlignVCenter
}
TextMetrics {
id: dateMetrics
font: dateText.font
// widest day/month abbreviations in this locale
text: "Wed 00 MMM"
}
Text {
id: dateText
text: Qt.formatDateTime(root.now, "ddd dd MMM")
font.family: Theme.font
font.pixelSize: Theme.fontSize
color: Theme.subtext
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: dateMetrics.advanceWidth
}
Text {
text: Qt.formatDateTime(root.now, "HH:mm")
font.family: Theme.font
font.pixelSize: Theme.fontSize
font.bold: true
color: Theme.text
Layout.alignment: Qt.AlignVCenter
}
}

66
widgets/CpuGraph.qml Normal file
View File

@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Layouts
import "../config"
import "../services"
// CPU usage as a live filled-area graph plus the current percentage.
Pill {
id: root
spacing: Theme.spacing
Text {
text: Icons.cpu
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize + 1
color: Theme.loadColor(SysStats.cpu)
Layout.alignment: Qt.AlignVCenter
}
// One vertical bar per core, filled from the bottom by that core's load.
Row {
id: cores
Layout.alignment: Qt.AlignVCenter
spacing: 1
readonly property int barH: Theme.barHeight - Theme.gap * 2 - 12
Repeater {
model: SysStats.coreCount
delegate: Rectangle {
readonly property real load: SysStats.coreLoads[index] || 0
width: 3
height: cores.barH
radius: 1
color: Qt.rgba(1, 1, 1, 0.08) // unfilled track
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
radius: 1
height: Math.max(1, parent.height * (parent.load / 100))
color: Theme.loadColor(parent.load)
}
}
}
}
Text {
text: SysStats.cpu.toFixed(0) + "%"
font.family: Theme.font
font.pixelSize: Theme.fontSize
color: Theme.text
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 42 // fits "100%" at fontSize 15
horizontalAlignment: Text.AlignRight
}
// Max current core frequency, shown in GHz.
Text {
text: (SysStats.cpuFreq / 1000).toFixed(1) + "GHz"
font.family: Theme.font
font.pixelSize: Theme.fontSize
color: Theme.subtext
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 56 // fits "0.0GHz" at fontSize 15
horizontalAlignment: Text.AlignRight
}
}

13
widgets/CpuTemp.qml Normal file
View File

@@ -0,0 +1,13 @@
import "../config"
import "../services"
MetricPill {
icon: Icons.thermo
// temperature thresholds differ from load: warm >70, hot >85
iconColor: SysStats.temp >= 85 ? Theme.red
: SysStats.temp >= 70 ? Theme.peach
: SysStats.temp >= 55 ? Theme.yellow
: Theme.green
value: SysStats.temp + "°C"
reserve: "100°C"
}

9
widgets/Disk.qml Normal file
View File

@@ -0,0 +1,9 @@
import "../config"
import "../services"
MetricPill {
icon: Icons.disk
iconColor: Theme.loadColor(SysStats.disk)
value: SysStats.disk + "%"
reserve: "100%"
}

40
widgets/MetricPill.qml Normal file
View File

@@ -0,0 +1,40 @@
import QtQuick
import QtQuick.Layouts
import "../config"
// A pill showing a coloured Nerd Font icon followed by a value string.
Pill {
id: root
property string icon: ""
property string value: ""
property color iconColor: Theme.text
property alias label: valueText
// Widest value the pill will ever show, e.g. "100%". When set, the value
// column reserves that width so changing values never shift other widgets.
property string reserve: ""
Text {
text: root.icon
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize + 1
color: root.iconColor
Layout.alignment: Qt.AlignVCenter
}
TextMetrics {
id: reserveMetrics
font: valueText.font
text: root.reserve
}
Text {
id: valueText
text: root.value
font.family: Theme.font
font.pixelSize: Theme.fontSize
color: Theme.text
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: root.reserve ? reserveMetrics.advanceWidth : implicitWidth
}
}

43
widgets/Network.qml Normal file
View File

@@ -0,0 +1,43 @@
import QtQuick
import QtQuick.Layouts
import "../config"
import "../services"
// Down/up throughput on the active interface, with a wifi/ethernet icon.
Pill {
Text {
text: SysStats.iface.startsWith("wl") ? Icons.wifi : Icons.ethernet
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize + 1
color: Theme.sky
Layout.alignment: Qt.AlignVCenter
}
// reserve the width of the widest rate string so values never shift
TextMetrics {
id: rateMetrics
font: rxText.font
text: Icons.down + " 99.9M"
}
Text {
id: rxText
text: Icons.down + " " + SysStats.fmtRate(SysStats.rxRate)
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize - 1
color: Theme.green
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: rateMetrics.advanceWidth
}
Text {
text: Icons.up + " " + SysStats.fmtRate(SysStats.txRate)
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize - 1
color: Theme.peach
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: rateMetrics.advanceWidth
}
}

23
widgets/Pill.qml Normal file
View File

@@ -0,0 +1,23 @@
import QtQuick
import QtQuick.Layouts
import "../config"
// Rounded background container used by every bar module for a consistent look.
Rectangle {
id: pill
default property alias content: row.data
property int spacing: Theme.spacing
property color background: Theme.surface0
implicitWidth: row.implicitWidth + Theme.padding * 2
implicitHeight: Theme.barHeight - Theme.gap * 2
radius: Theme.radius
color: background
RowLayout {
id: row
anchors.centerIn: parent
spacing: pill.spacing
}
}

9
widgets/Ram.qml Normal file
View File

@@ -0,0 +1,9 @@
import "../config"
import "../services"
MetricPill {
icon: Icons.memory
iconColor: Theme.loadColor(SysStats.mem)
value: SysStats.mem.toFixed(0) + "%"
reserve: "100%"
}

62
widgets/Tray.qml Normal file
View File

@@ -0,0 +1,62 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.SystemTray
import "../config"
// StatusNotifier system tray. Left-click activates, middle-click does the
// secondary action, right-click opens the item's native menu.
Pill {
id: root
// the PanelWindow this tray lives in, needed to anchor context menus
property var panelWindow
visible: SystemTray.items.values.length > 0
spacing: Theme.spacing + 2
Repeater {
model: SystemTray.items
Item {
id: entry
required property var modelData
Layout.alignment: Qt.AlignVCenter
implicitWidth: 18
implicitHeight: 18
Image {
anchors.fill: parent
source: entry.modelData.icon
sourceSize.width: 18
sourceSize.height: 18
fillMode: Image.PreserveAspectFit
smooth: true
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: mouse => {
const item = entry.modelData;
if (mouse.button === Qt.LeftButton) {
if (item.onlyMenu) openMenu();
else item.activate();
} else if (mouse.button === Qt.MiddleButton) {
item.secondaryActivate();
} else if (mouse.button === Qt.RightButton) {
openMenu();
}
}
function openMenu() {
const item = entry.modelData;
if (!item.hasMenu || !root.panelWindow) return;
const p = entry.mapToItem(null, 0, entry.height + Theme.gap);
item.display(root.panelWindow, p.x, p.y);
}
onWheel: wheel =>
entry.modelData.scroll(wheel.angleDelta.y, false)
}
}
}
}

63
widgets/Volume.qml Normal file
View File

@@ -0,0 +1,63 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import "../config"
// PipeWire default sink volume. Scroll to adjust, click to mute.
MouseArea {
id: root
readonly property var sink: Pipewire.defaultAudioSink
readonly property var audio: sink ? sink.audio : null
readonly property bool muted: audio ? audio.muted : true
readonly property int volPct: audio ? Math.round(audio.volume * 100) : 0
implicitWidth: pill.implicitWidth
implicitHeight: pill.implicitHeight
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onClicked: { if (audio) audio.muted = !audio.muted; }
onWheel: wheel => {
if (!audio) return;
audio.volume = Math.max(0, Math.min(1,
audio.volume + (wheel.angleDelta.y > 0 ? 0.05 : -0.05)));
}
// keep the sink's audio properties live
PwObjectTracker { objects: root.sink ? [root.sink] : [] }
Pill {
id: pill
anchors.fill: parent
Text {
text: root.muted ? Icons.volMute
: root.volPct < 50 ? Icons.volLow
: Icons.volHigh
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize + 1
color: root.muted ? Theme.overlay : Theme.teal
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Theme.fontSize + 4
horizontalAlignment: Text.AlignHCenter
}
TextMetrics {
id: volMetrics
font: volText.font
text: "muted"
}
Text {
id: volText
text: root.muted ? "muted" : root.volPct + "%"
font.family: Theme.font
font.pixelSize: Theme.fontSize
color: Theme.text
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: volMetrics.advanceWidth
}
}
}

54
widgets/Workspaces.qml Normal file
View File

@@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.I3
import "../config"
// Sway/i3 workspace switcher for a single monitor.
Row {
id: root
required property var screen // ShellScreen this bar lives on
spacing: Theme.gap
// Name of the i3/sway output this bar is on, used to filter workspaces.
readonly property string monitorName: screen ? screen.name : ""
Repeater {
model: I3.workspaces
Rectangle {
id: chip
required property var modelData
// only show workspaces belonging to this monitor
// I3Workspace.monitor is an I3Monitor object, so compare its name
visible: modelData.monitor && modelData.monitor.name === root.monitorName
width: visible ? Math.max(height, label.implicitWidth + 16) : 0
height: Theme.barHeight - Theme.gap * 2
radius: Theme.radius
readonly property bool isFocused: modelData.focused
readonly property bool isUrgent: modelData.urgent
color: isUrgent ? Theme.red
: isFocused ? Theme.blue
: modelData.active ? Theme.surface1
: Theme.surface0
Text {
id: label
anchors.centerIn: parent
text: chip.modelData.name
font.family: Theme.font
font.pixelSize: Theme.fontSize
font.bold: chip.isFocused || chip.isUrgent
color: (chip.isFocused || chip.isUrgent) ? Theme.mantle : Theme.text
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: chip.modelData.activate()
}
}
}
}