From 0d297296ca7631ae225c88284c8993c0e4d468e8 Mon Sep 17 00:00:00 2001 From: yage Date: Fri, 23 Oct 2026 16:19:22 -0600 Subject: [PATCH] a BUNCH of qml which I havent reviewed yet --- config/user/quickshell/Bar.qml | 76 +++++++ config/user/quickshell/BarLabel.qml | 10 + config/user/quickshell/Battery.qml | 43 ++++ config/user/quickshell/Clock.qml | 14 ++ config/user/quickshell/Colors.qml | 44 ++++ config/user/quickshell/Cpu.qml | 36 ++++ config/user/quickshell/Disk.qml | 23 +++ config/user/quickshell/LogoutPrompt.qml | 81 ++++++++ config/user/quickshell/Memory.qml | 24 +++ config/user/quickshell/Network.qml | 39 ++++ config/user/quickshell/Notifications.qml | 145 ++++++++++++++ config/user/quickshell/PollingCommand.qml | 28 +++ config/user/quickshell/Tray.qml | 51 +++++ config/user/quickshell/VolumeOsd.qml | 234 ++++++++++++++++++++++ config/user/quickshell/Workspaces.qml | 51 +++++ config/user/quickshell/shell.qml | 22 ++ 16 files changed, 921 insertions(+) create mode 100644 config/user/quickshell/Bar.qml create mode 100644 config/user/quickshell/BarLabel.qml create mode 100644 config/user/quickshell/Battery.qml create mode 100644 config/user/quickshell/Clock.qml create mode 100644 config/user/quickshell/Colors.qml create mode 100644 config/user/quickshell/Cpu.qml create mode 100644 config/user/quickshell/Disk.qml create mode 100644 config/user/quickshell/LogoutPrompt.qml create mode 100644 config/user/quickshell/Memory.qml create mode 100644 config/user/quickshell/Network.qml create mode 100644 config/user/quickshell/Notifications.qml create mode 100644 config/user/quickshell/PollingCommand.qml create mode 100644 config/user/quickshell/Tray.qml create mode 100644 config/user/quickshell/VolumeOsd.qml create mode 100644 config/user/quickshell/Workspaces.qml create mode 100644 config/user/quickshell/shell.qml diff --git a/config/user/quickshell/Bar.qml b/config/user/quickshell/Bar.qml new file mode 100644 index 0000000..cceb2c4 --- /dev/null +++ b/config/user/quickshell/Bar.qml @@ -0,0 +1,76 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Services.UPower + +// port of the polybar 'i3' bar ($DOTWRYN/config/user/polybar/config.ini) +PanelWindow { + id: bar + + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(screen) + readonly property int activeWorkspaceId: monitor?.activeWorkspace?.id ?? 1 + readonly property bool activeWorkspaceFullscreen: monitor?.activeWorkspace?.hasFullscreen ?? false + + // the bar shows on every workspace except these + readonly property var hiddenWorkspaceIds: [2] + + // fullscreen is checked here too: re-mapping the bar (on workspace switch) would + // otherwise draw it over a window that was already fullscreen + visible: !hiddenWorkspaceIds.includes(activeWorkspaceId) + && !activeWorkspaceFullscreen + + anchors { + bottom: true + left: true + right: true + } + + implicitHeight: 32 + color: Colors.background + + TextMetrics { + id: padding + font.family: Colors.font + font.pointSize: Colors.fontSize + text: " " + } + + readonly property real separatorWidth: padding.advanceWidth * 2 + + Row { // modules-left + anchors.left: parent.left + anchors.leftMargin: padding.advanceWidth + anchors.verticalCenter: parent.verticalCenter + height: parent.height + spacing: bar.separatorWidth + + Workspaces { height: parent.height; monitor: bar.monitor } + } + + Row { // modules-center + anchors.centerIn: parent + height: parent.height + spacing: bar.separatorWidth + + Tray { height: parent.height; window: bar } + Repeater { + model: UPower.devices.values.filter(device => device.isLaptopBattery) + Battery { required property var modelData; device: modelData; anchors.verticalCenter: parent.verticalCenter } + } + } + + Row { // modules-right + anchors.right: parent.right + anchors.rightMargin: padding.advanceWidth + anchors.verticalCenter: parent.verticalCenter + height: parent.height + spacing: bar.separatorWidth + + Disk { anchors.verticalCenter: parent.verticalCenter } + Memory { anchors.verticalCenter: parent.verticalCenter } + Cpu { anchors.verticalCenter: parent.verticalCenter } + Network { anchors.verticalCenter: parent.verticalCenter; interfaceType: "wireless" } + Network { anchors.verticalCenter: parent.verticalCenter; interfaceType: "wired" } + Clock { anchors.verticalCenter: parent.verticalCenter } + } +} diff --git a/config/user/quickshell/BarLabel.qml b/config/user/quickshell/BarLabel.qml new file mode 100644 index 0000000..d41fb1b --- /dev/null +++ b/config/user/quickshell/BarLabel.qml @@ -0,0 +1,10 @@ +import QtQuick + +// polybar-style text; hidden whenever it has nothing to say (like an empty polybar label) +Text { + color: Colors.foreground + font.family: Colors.font + font.pointSize: Colors.fontSize + font.bold: true + visible: text !== "" +} diff --git a/config/user/quickshell/Battery.qml b/config/user/quickshell/Battery.qml new file mode 100644 index 0000000..9f42197 --- /dev/null +++ b/config/user/quickshell/Battery.qml @@ -0,0 +1,43 @@ +import QtQuick +import Quickshell.Services.UPower + +// polybar 'internal/battery' (battery0 / battery1) +Row { + id: battery + + required property UPowerDevice device + + readonly property int fullAt: 74 + readonly property int lowAt: 39 + + readonly property real percentage: device.percentage <= 1 ? device.percentage * 100 : device.percentage + readonly property bool discharging: device.state === UPowerDeviceState.Discharging + + readonly property var ramp: ["", "", "", "", ""] + readonly property int rampIndex: Math.min(ramp.length - 1, Math.floor(percentage / (100 / ramp.length))) + readonly property var rampColors: [ + Colors.alert, + Colors.foreground, + Colors.secondaryGradient[2], + Colors.secondaryGradient[3], + Colors.secondaryGradient[4], + ] + + spacing: 4 + visible: device.isPresent + + BarLabel { + text: battery.ramp[battery.rampIndex] + color: battery.rampColors[battery.rampIndex] + } + + BarLabel { // label-full + text: battery.percentage >= battery.fullAt ? "c:" : "" + color: Colors.secondaryGradient[4] + } + + BarLabel { // label-low + text: battery.discharging && battery.percentage <= battery.lowAt ? "⚠" : "" + color: Colors.alert + } +} diff --git a/config/user/quickshell/Clock.qml b/config/user/quickshell/Clock.qml new file mode 100644 index 0000000..3fb34fd --- /dev/null +++ b/config/user/quickshell/Clock.qml @@ -0,0 +1,14 @@ +import QtQuick +import Quickshell + +// polybar 'datetime' : Monday October 19, 2026 : 09:29:00-am {15:29-UTC} +BarLabel { + SystemClock { + id: clock + precision: SystemClock.Seconds + } + + readonly property date utc: new Date(clock.date.getTime() + clock.date.getTimezoneOffset() * 60000) + + text: `${Qt.formatDateTime(clock.date, "dddd MMMM dd, yyyy : hh:mm:ss-ap")} {${Qt.formatDateTime(utc, "HH:mm")}-UTC}` +} diff --git a/config/user/quickshell/Colors.qml b/config/user/quickshell/Colors.qml new file mode 100644 index 0000000..6812fa9 --- /dev/null +++ b/config/user/quickshell/Colors.qml @@ -0,0 +1,44 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// colors.json is generated by 'scwrypts set theme'; watching it means a theme +// change recolors the shell without a restart +Singleton { + readonly property color background: json.background + readonly property color foreground: json.foreground + readonly property color backgroundAlt: json.backgroundAlt + readonly property color foregroundAlt: json.foregroundAlt + readonly property color primary: json.primary + readonly property color secondary: json.secondary + readonly property color alert: json.alert + readonly property color disabled: json.disabled + + readonly property var secondaryGradient: json.secondaryGradient + + readonly property string font: "Monaspace Krypton" + readonly property real fontSize: 11 + + FileView { + path: Quickshell.shellPath("colors.json") + watchChanges: true + onFileChanged: reload() + + JsonAdapter { + id: json + + property string background: "#000505" + property string foreground: "#d0f0f0" + property string backgroundAlt: "#6911aa" + property string foregroundAlt: "#d0f0f0" + property string primary: "#6911aa" + property string secondary: "#11bb98" + property string alert: "#c80064" + property string disabled: "#424253" + + property var secondaryGradient: ["#006942", "#009949", "#00aa79", "#11bb98", "#22ccba"] + } + } +} diff --git a/config/user/quickshell/Cpu.qml b/config/user/quickshell/Cpu.qml new file mode 100644 index 0000000..0da8e09 --- /dev/null +++ b/config/user/quickshell/Cpu.qml @@ -0,0 +1,36 @@ +import QtQuick + +// polybar 'internal/cpu'; silent until cpu usage is high +BarLabel { + id: cpu + + property int warnPercentage: 79 + + property real lastIdle: 0 + property real lastTotal: 0 + + color: Colors.alert + text: "" + + PollingCommand { + interval: 3000 + command: ["head", "-n1", "/proc/stat"] + onOutput: output => { + const fields = output.trim().split(/\s+/).slice(1).map(Number) + const idle = fields[3] + fields[4] // idle + iowait + const total = fields.reduce((sum, value) => sum + value, 0) + + const deltaTotal = total - cpu.lastTotal + const used = cpu.lastTotal > 0 && deltaTotal > 0 + ? Math.round((1 - (idle - cpu.lastIdle) / deltaTotal) * 100) + : 0 + + cpu.lastIdle = idle + cpu.lastTotal = total + + cpu.text = used >= cpu.warnPercentage + ? `CPU${String(used).padStart(2)}%` + : "" + } + } +} diff --git a/config/user/quickshell/Disk.qml b/config/user/quickshell/Disk.qml new file mode 100644 index 0000000..0e8768d --- /dev/null +++ b/config/user/quickshell/Disk.qml @@ -0,0 +1,23 @@ +import QtQuick + +// polybar 'internal/fs'; silent until the disk is nearly full +BarLabel { + id: disk + + property int warnPercentage: 83 + + color: Colors.alert + text: "" + + PollingCommand { + interval: 17000 + command: ["df", "--human-readable", "--output=pcent,avail", "/"] + onOutput: output => { + const [percentage, free] = output.trim().split("\n").pop().trim().split(/\s+/) + const used = parseInt(percentage) + disk.text = used >= disk.warnPercentage + ? `${free} remaining on disk (${used}% full)` + : "" + } + } +} diff --git a/config/user/quickshell/LogoutPrompt.qml b/config/user/quickshell/LogoutPrompt.qml new file mode 100644 index 0000000..e786bff --- /dev/null +++ b/config/user/quickshell/LogoutPrompt.qml @@ -0,0 +1,81 @@ +import QtQuick +import Quickshell +import Quickshell.Io + +// i3-nagbar replacement; shown with 'qs ipc call logout show' +Scope { + id: prompt + + property bool shown: false + + IpcHandler { + target: "logout" + + function show(): void { prompt.shown = true } + function hide(): void { prompt.shown = false } + } + + PanelWindow { + visible: prompt.shown + + anchors { + top: true + left: true + right: true + } + + implicitHeight: 32 + color: Colors.alert + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: "Do you really want to exit Hyprland?" + color: Colors.foreground + font.family: Colors.font + font.pointSize: Colors.fontSize + font.bold: true + } + + Row { + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + Repeater { + model: [ + { label: "Yes", action: () => Quickshell.execDetached(["sh", "-c", "notify-send 'system' 'exiting hyprland...' -i face-tired; hyprctl dispatch 'hl.dsp.exit()'"]) }, + { label: "X", action: () => prompt.shown = false }, + ] + + Rectangle { + required property var modelData + + implicitWidth: buttonLabel.implicitWidth + 20 + implicitHeight: 24 + color: Colors.background + border.width: 1 + border.color: Colors.foreground + + Text { + id: buttonLabel + anchors.centerIn: parent + text: parent.modelData.label + color: Colors.foreground + font.family: Colors.font + font.pointSize: Colors.fontSize + font.bold: true + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: parent.modelData.action() + } + } + } + } + } +} diff --git a/config/user/quickshell/Memory.qml b/config/user/quickshell/Memory.qml new file mode 100644 index 0000000..a93d5f4 --- /dev/null +++ b/config/user/quickshell/Memory.qml @@ -0,0 +1,24 @@ +import QtQuick + +// polybar 'internal/memory'; silent until memory usage is high +BarLabel { + id: memory + + property int warnPercentage: 73 + + color: Colors.alert + text: "" + + PollingCommand { + interval: 2000 + command: ["cat", "/proc/meminfo"] + onOutput: output => { + const total = parseInt(output.match(/^MemTotal:\s+(\d+)/m)[1]) + const available = parseInt(output.match(/^MemAvailable:\s+(\d+)/m)[1]) + const used = Math.round((total - available) * 100 / total) + memory.text = used >= memory.warnPercentage + ? `RAM${String(used).padStart(2)}%` + : "" + } + } +} diff --git a/config/user/quickshell/Network.qml b/config/user/quickshell/Network.qml new file mode 100644 index 0000000..e60637e --- /dev/null +++ b/config/user/quickshell/Network.qml @@ -0,0 +1,39 @@ +import QtQuick + +// polybar 'internal/network' (wlan / eth); shows the first physical interface of the given type +BarLabel { + id: network + + required property string interfaceType // "wireless" | "wired" + + text: "" + + PollingCommand { + interval: 31000 + command: ["sh", "-c", ` + for path in /sys/class/net/* + do + [ -e "$path/device" ] || continue + [ -d "$path/wireless" ] && type=wireless || type=wired + [ "$type" = "${network.interfaceType}" ] || continue + + ifname="$(basename "$path")" + ip="$(ip -4 -o addr show dev "$ifname" | awk '{print $4}' | cut -d/ -f1 | head -n1)" + echo "$ifname $ip" + break + done + `] + onOutput: output => { + const [ifname, ip] = output.trim().split(/\s+/) + if (!ifname) { + network.text = "" + } else if (ip) { + network.text = `${ifname}/${ip}` + network.color = Colors.secondary + } else { + network.text = `(${ifname}:⊘)` + network.color = Colors.disabled + } + } + } +} diff --git a/config/user/quickshell/Notifications.qml b/config/user/quickshell/Notifications.qml new file mode 100644 index 0000000..c5daf1f --- /dev/null +++ b/config/user/quickshell/Notifications.qml @@ -0,0 +1,145 @@ +import QtQuick +import Quickshell +import Quickshell.Services.Notifications + +// notification daemon (replaces xfce4-notifyd for the hyprland session) +Scope { + id: notifications + + readonly property int defaultTimeout: 5000 + + NotificationServer { + id: server + + keepOnReload: true + bodySupported: true + bodyMarkupSupported: true + actionsSupported: true + imageSupported: true + + onNotification: notification => notification.tracked = true + } + + PanelWindow { + visible: server.trackedNotifications.values.length > 0 + + anchors { + top: true + right: true + } + margins { + top: 8 + right: 8 + } + + exclusiveZone: 0 + implicitWidth: 380 + implicitHeight: stack.implicitHeight + color: "transparent" + + Column { + id: stack + + width: parent.width + spacing: 8 + + Repeater { + model: server.trackedNotifications + + Rectangle { + id: card + + required property Notification modelData + readonly property bool critical: modelData.urgency === NotificationUrgency.Critical + + width: stack.width + implicitHeight: content.implicitHeight + 20 + color: Colors.background + border.width: 2 + border.color: critical ? Colors.alert : Colors.secondary + + Timer { + running: !card.critical && card.modelData.expireTimeout !== 0 + interval: card.modelData.expireTimeout > 0 + ? card.modelData.expireTimeout * 1000 + : notifications.defaultTimeout + onTriggered: card.modelData.expire() + } + + Column { + id: content + + anchors { + left: parent.left + right: parent.right + top: parent.top + margins: 10 + } + spacing: 4 + + Text { + width: parent.width + text: card.modelData.summary + color: card.critical ? Colors.alert : Colors.foreground + font.family: Colors.font + font.pointSize: Colors.fontSize + font.bold: true + elide: Text.ElideRight + } + + Text { + width: parent.width + visible: text !== "" + text: card.modelData.body + textFormat: Text.StyledText + color: Colors.foreground + font.family: Colors.font + font.pointSize: Colors.fontSize - 1 + wrapMode: Text.Wrap + maximumLineCount: 6 + elide: Text.ElideRight + } + + Row { + visible: card.modelData.actions.length > 0 + spacing: 8 + + Repeater { + model: card.modelData.actions + + Rectangle { + required property NotificationAction modelData + + implicitWidth: actionLabel.implicitWidth + 16 + implicitHeight: actionLabel.implicitHeight + 8 + color: Colors.backgroundAlt + + Text { + id: actionLabel + anchors.centerIn: parent + text: parent.modelData.text + color: Colors.foregroundAlt + font.family: Colors.font + font.pointSize: Colors.fontSize - 1 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: parent.modelData.invoke() + } + } + } + } + } + + MouseArea { // click anywhere else on the card to dismiss + anchors.fill: parent + z: -1 + onClicked: card.modelData.dismiss() + } + } + } + } + } +} diff --git a/config/user/quickshell/PollingCommand.qml b/config/user/quickshell/PollingCommand.qml new file mode 100644 index 0000000..7b96d82 --- /dev/null +++ b/config/user/quickshell/PollingCommand.qml @@ -0,0 +1,28 @@ +import QtQuick +import Quickshell.Io + +// runs a command every 'interval' ms and emits its stdout (polybar module 'interval') +Item { + id: polling + + required property var command + property int interval: 1000 + + signal output(string text) + + Process { + id: process + command: polling.command + stdout: StdioCollector { + onStreamFinished: polling.output(this.text) + } + } + + Timer { + interval: polling.interval + running: true + repeat: true + triggeredOnStart: true + onTriggered: process.running = true + } +} diff --git a/config/user/quickshell/Tray.qml b/config/user/quickshell/Tray.qml new file mode 100644 index 0000000..7d2b1a3 --- /dev/null +++ b/config/user/quickshell/Tray.qml @@ -0,0 +1,51 @@ +import QtQuick +import Quickshell +import Quickshell.Services.SystemTray +import Quickshell.Widgets + +// polybar 'internal/tray' +Row { + id: tray + + required property var window + + spacing: 8 + leftPadding: 8 + rightPadding: 8 + visible: SystemTray.items.values.length > 0 + + Repeater { + model: SystemTray.items + + IconImage { + id: icon + + required property SystemTrayItem modelData + + anchors.verticalCenter: parent.verticalCenter + implicitSize: 18 + source: modelData.icon + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + cursorShape: Qt.PointingHandCursor + + onClicked: mouse => { + const item = icon.modelData + const showMenu = mouse.button === Qt.RightButton || item.onlyMenu + if (showMenu && item.hasMenu) { + const position = icon.mapToItem(null, 0, 0) + item.display(tray.window, position.x, position.y - 4) + } else if (mouse.button === Qt.MiddleButton) { + item.secondaryActivate() + } else { + item.activate() + } + } + + onWheel: wheel => icon.modelData.scroll(wheel.angleDelta.y, false) + } + } + } +} diff --git a/config/user/quickshell/VolumeOsd.qml b/config/user/quickshell/VolumeOsd.qml new file mode 100644 index 0000000..ea64215 --- /dev/null +++ b/config/user/quickshell/VolumeOsd.qml @@ -0,0 +1,234 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire + +Scope { + id: osd + + readonly property PwNode sink: Pipewire.defaultAudioSink + readonly property PwNode source: Pipewire.defaultAudioSource + + property string label: "" + property real level: 0 + property bool muted: false + property bool shown: false + property string warning: "" + + // going past 100% : at 100%, it takes 'unlockPresses' separate presses of + // volume-up (the first shows a warning); above 100% there is no limit + readonly property real step: 0.10 + readonly property int unlockPresses: 3 + readonly property int repeatGuardMs: 150 // key-repeat events don't count toward unlocking + property int pressesAtMax: 0 + property real lastUpPress: 0 + + IpcHandler { + target: "volume" + function up(): void { osd.volumeUp() } + } + + Timer { + id: unlockReset + interval: 3000 + onTriggered: osd.pressesAtMax = 0 + } + + function volumeUp(): void { + const audio = sink?.audio + if (!audio) return + + const now = Date.now() + const isRepeat = now - lastUpPress < repeatGuardMs + lastUpPress = now + + const percent = Math.round(audio.volume * 100) + + if (percent < 100) { // normal range; stop exactly at 100% + pressesAtMax = 0 + audio.volume = Math.min(1.0, audio.volume + step) + return + } + + if (percent > 100) { // already past 100% : no limit + audio.volume = audio.volume + step + return + } + + // exactly 100% + if (!isRepeat) { + pressesAtMax += 1 + unlockReset.restart() + } + + if (pressesAtMax >= unlockPresses) { + pressesAtMax = 0 + audio.volume = audio.volume + step + return + } + + if (isRepeat && shown) { // holding the key at 100% : just keep the warning up + hideTimer.restart() + return + } + + const remaining = unlockPresses - pressesAtMax + show("volume", sink, "volume-max", `press ${remaining} more to go past 100%`) + } + + // sound effects : names from $DOTWRYN/config/sfx.map, files in DESKTOP__SFX_PATH + property string sfxPath: "" + property var sfxFiles: ({}) + + // default devices must be tracked for their volume / mute to update + PwObjectTracker { objects: [osd.sink, osd.source] } + + Process { // DESKTOP__SFX_PATH lives in the scwrypts environment; read it once + running: true + command: ["scwrypts", "--group", "scwrypts", "--type", "zsh", "--name", "scwrypts/environment/getenv", "--", "DESKTOP__SFX_PATH"] + environment: ({ SCWRYPTS_LOG_LEVEL: "0" }) + stdout: StdioCollector { + onStreamFinished: osd.sfxPath = this.text.trim().replace(/^\$HOME|^~/, Quickshell.env("HOME")) + } + } + + FileView { + path: Quickshell.env("DOTWRYN") + "/config/sfx.map" + watchChanges: true + onFileChanged: reload() + onLoaded: { + const files = {} + for (const line of text().split("\n")) { + const [name, file] = line.trim().split(/\s+/) + if (name && file && !name.startsWith("#")) files[name] = file + } + osd.sfxFiles = files + } + } + + function playSfx(name: string): void { + if (sfxPath === "" || !sfxFiles[name]) return + Quickshell.execDetached(["canberra-gtk-play", "-f", `${sfxPath}/${sfxFiles[name]}`]) + } + + function show(label: string, node: PwNode, sfx: string, warning: string): void { + if (!node?.audio) return + osd.warning = warning ?? "" + osd.label = label + osd.level = node.audio.volume + osd.muted = node.audio.muted + osd.shown = true + hideTimer.restart() + playSfx(sfx) + } + + // ignore the initial values reported while the devices bind at startup + property bool armed: false + Timer { running: true; interval: 1500; onTriggered: osd.armed = true } + + Connections { + target: osd.sink?.audio ?? null + ignoreUnknownSignals: true + function onVolumeChanged() { if (osd.armed) osd.show("volume", osd.sink, "volume", "") } + function onMutedChanged() { if (osd.armed) osd.show("volume", osd.sink, "mute", "") } + } + + Connections { + target: osd.source?.audio ?? null + ignoreUnknownSignals: true + function onVolumeChanged() { if (osd.armed) osd.show("mic", osd.source, "volume", "") } + function onMutedChanged() { if (osd.armed) osd.show("mic", osd.source, "mute", "") } + } + + Timer { + id: hideTimer + interval: 1500 + onTriggered: osd.shown = false + } + + PanelWindow { + visible: osd.shown + + anchors.bottom: true + margins.bottom: 120 + exclusiveZone: 0 + + readonly property bool boosted: osd.level > 1.005 + + implicitWidth: 400 + implicitHeight: osd.warning !== "" ? 84 : 56 + color: "transparent" + + Rectangle { + anchors.fill: parent + color: Colors.background + border.width: 2 + border.color: osd.muted || parent.boosted || osd.warning !== "" ? Colors.alert : Colors.secondary + + TextMetrics { + id: percentSlot + font.family: Colors.font + font.pointSize: Colors.fontSize + font.bold: true + text: "1000%" + } + + Text { // past-100% warning; sits above the row so the row never moves + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: row.top + anchors.bottomMargin: 8 + visible: osd.warning !== "" + text: osd.warning + color: Colors.alert + font.family: Colors.font + font.pointSize: Colors.fontSize - 1 + font.bold: true + } + + Row { + id: row + + // pinned to the bottom edge : the popup is anchored to the screen + // bottom, so it grows upward for the warning and the bar stays put + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 18 + spacing: 14 + + Text { + anchors.verticalCenter: parent.verticalCenter + width: 64 + text: osd.muted ? `${osd.label} \u2298` : osd.label + color: osd.muted ? Colors.alert : Colors.foreground + font.family: Colors.font + font.pointSize: Colors.fontSize + font.bold: true + } + + Rectangle { // level bar + anchors.verticalCenter: parent.verticalCenter + width: 200 + height: 10 + color: Colors.disabled + + Rectangle { + width: parent.width * Math.min(osd.level, 1) + height: parent.height + color: osd.muted || osd.level > 1.005 ? Colors.alert : Colors.secondary + } + } + + Text { + anchors.verticalCenter: parent.verticalCenter + width: percentSlot.advanceWidth + horizontalAlignment: Text.AlignRight + text: `${Math.round(osd.level * 100)}%` + color: osd.level > 1.005 ? Colors.alert : Colors.foreground + font.family: Colors.font + font.pointSize: Colors.fontSize + font.bold: true + } + } + } + } +} diff --git a/config/user/quickshell/Workspaces.qml b/config/user/quickshell/Workspaces.qml new file mode 100644 index 0000000..bf79de2 --- /dev/null +++ b/config/user/quickshell/Workspaces.qml @@ -0,0 +1,51 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland + +// polybar 'internal/xworkspaces' +Row { + id: workspaces + + required property HyprlandMonitor monitor + + // numbered workspaces first (🎮 is 100), then named in creation order; specials are hidden + readonly property var visibleWorkspaces: Hyprland.workspaces.values + .filter(workspace => !workspace.name.startsWith("special")) + .sort((a, b) => { + if ((a.id > 0) !== (b.id > 0)) return a.id > 0 ? -1 : 1 + return a.id > 0 ? a.id - b.id : b.id - a.id + }) + + Repeater { + model: workspaces.visibleWorkspaces + + Rectangle { + id: workspace + + required property HyprlandWorkspace modelData + readonly property bool isActive: modelData.id === workspaces.monitor?.activeWorkspace?.id + + height: workspaces.height + width: label.implicitWidth + label.font.pixelSize * 1.2 + color: modelData.urgent ? Colors.alert + : isActive ? Colors.backgroundAlt + : "transparent" + + BarLabel { + id: label + anchors.centerIn: parent + text: workspace.modelData.name + color: workspace.isActive ? Colors.foregroundAlt : Colors.foreground + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + // through hyprland.lua so the slide direction matches the keybinds + onClicked: Quickshell.execDetached(["hyprctl", "eval", `dotwryn_focus_workspace("${ + workspace.modelData.id > 0 ? workspace.modelData.id : "name:" + workspace.modelData.name + }")`]) + } + } + } +} diff --git a/config/user/quickshell/shell.qml b/config/user/quickshell/shell.qml new file mode 100644 index 0000000..3a0be12 --- /dev/null +++ b/config/user/quickshell/shell.qml @@ -0,0 +1,22 @@ +//@ pragma UseQApplication + +import QtQuick +import Quickshell + +// hyprland shell; a port of the polybar / xfce4-notifyd / i3-nagbar setup (+ volume popup) +ShellRoot { + Variants { + model: Quickshell.screens + + Bar { + required property var modelData + screen: modelData + } + } + + Notifications {} + + VolumeOsd {} + + LogoutPrompt {} +}