- Vertical bar on DP-2 with rounded-square pills throughout - Per-monitor workspace groups sorted by screen x position, with Nerd Font icons for named workspaces and apex-neon red active indicator - Bar layout: datetime+weather top, workspaces centered, gamemode+media+notif+system bottom - Popouts anchor to triggering icon (top-right for datetime/weather, bottom-right for media/notif/system) - Lock command switched from hyprlock to swaylock - Hyprland blur/ignore_alpha layerrules for quickshell namespace Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
179 lines
6.4 KiB
QML
179 lines
6.4 KiB
QML
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Pipewire
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import "../shared" as Shared
|
|
|
|
Scope {
|
|
id: root
|
|
|
|
PwObjectTracker { objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource] }
|
|
|
|
property string osdIcon: ""
|
|
property real osdValue: 0
|
|
property bool osdMuted: false
|
|
property string osdLabel: ""
|
|
property bool osdVisible: false
|
|
|
|
function showOsd(icon, value, muted, label) {
|
|
osdIcon = icon;
|
|
osdValue = value;
|
|
osdMuted = muted;
|
|
osdLabel = label;
|
|
osdVisible = true;
|
|
hideTimer.restart();
|
|
}
|
|
|
|
Timer {
|
|
id: hideTimer
|
|
interval: 1500
|
|
onTriggered: root.osdVisible = false
|
|
}
|
|
|
|
// Event-driven audio change detection
|
|
property var sinkAudio: Pipewire.defaultAudioSink?.audio ?? null
|
|
property var sourceAudio: Pipewire.defaultAudioSource?.audio ?? null
|
|
|
|
Connections {
|
|
target: root.sinkAudio
|
|
function onVolumeChanged() {
|
|
let a = root.sinkAudio;
|
|
if (a) root.showOsd("\u{f057e}", a.volume, a.muted, "Volume");
|
|
}
|
|
function onMutedChanged() {
|
|
let a = root.sinkAudio;
|
|
if (a) root.showOsd(a.muted ? "\u{f057f}" : "\u{f057e}", a.volume, a.muted, a.muted ? "Muted" : "Volume");
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: root.sourceAudio
|
|
function onVolumeChanged() {
|
|
let a = root.sourceAudio;
|
|
if (a) root.showOsd("\u{f036c}", a.volume, a.muted, "Mic");
|
|
}
|
|
function onMutedChanged() {
|
|
let a = root.sourceAudio;
|
|
if (a) root.showOsd(a.muted ? "\u{f036d}" : "\u{f036c}", a.volume, a.muted, a.muted ? "Mic muted" : "Mic");
|
|
}
|
|
}
|
|
|
|
// Brightness monitoring via brightnessctl (auto-disables if no backlight device)
|
|
property real lastBrightness: -1
|
|
property bool hasBrightness: false
|
|
Process {
|
|
id: brightProc
|
|
command: ["brightnessctl", "-m"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
let parts = this.text.trim().split(",");
|
|
if (parts.length >= 5) {
|
|
root.hasBrightness = true;
|
|
let pct = parseInt(parts[4]) / 100;
|
|
if (root.lastBrightness >= 0 && Math.abs(pct - root.lastBrightness) > 0.005)
|
|
root.showOsd("\u{f00df}", pct, false, "Brightness");
|
|
root.lastBrightness = pct;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Timer {
|
|
interval: 500
|
|
running: root.hasBrightness
|
|
repeat: true
|
|
onTriggered: { brightProc.running = false; brightProc.running = true; }
|
|
}
|
|
Component.onCompleted: { brightProc.running = true; }
|
|
|
|
Variants {
|
|
model: Quickshell.screens
|
|
|
|
delegate: Component {
|
|
PanelWindow {
|
|
required property var modelData
|
|
screen: modelData
|
|
WlrLayershell.namespace: "quickshell:osd"
|
|
surfaceFormat { opaque: false }
|
|
|
|
visible: modelData.name === Shared.Config.monitor && root.osdVisible
|
|
|
|
anchors {
|
|
top: true
|
|
right: true
|
|
bottom: true
|
|
}
|
|
|
|
exclusionMode: ExclusionMode.Ignore
|
|
implicitWidth: Shared.Theme.barWidth + Shared.Theme.popoutWidth + 12
|
|
color: "transparent"
|
|
|
|
Rectangle {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Shared.Theme.barWidth + 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 44
|
|
height: 180
|
|
radius: 22
|
|
color: Shared.Theme.popoutBackground
|
|
border.width: 1
|
|
border.color: Shared.Theme.borderSubtle
|
|
|
|
opacity: root.osdVisible ? 1.0 : 0.0
|
|
scale: root.osdVisible ? 1.0 : 0.95
|
|
transformOrigin: Item.Right
|
|
|
|
Behavior on opacity { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } }
|
|
Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.topMargin: 12
|
|
anchors.bottomMargin: 12
|
|
spacing: 8
|
|
|
|
// Percentage
|
|
Text {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: root.osdMuted ? "M" : Math.round(root.osdValue * 100)
|
|
color: root.osdMuted ? Shared.Theme.overlay0 : Shared.Theme.text
|
|
font.pixelSize: Shared.Theme.fontSmall
|
|
font.family: Shared.Theme.fontFamily
|
|
font.bold: true
|
|
}
|
|
|
|
// Vertical bar (fills bottom-up)
|
|
Rectangle {
|
|
Layout.fillHeight: true
|
|
Layout.alignment: Qt.AlignHCenter
|
|
implicitWidth: 6
|
|
radius: 3
|
|
color: Shared.Theme.surface0
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
height: root.osdMuted ? 0 : parent.height * Math.min(1, root.osdValue)
|
|
radius: 3
|
|
color: root.osdMuted ? Shared.Theme.overlay0 : Shared.Theme.sky
|
|
Behavior on height { NumberAnimation { duration: 80; easing.type: Easing.OutCubic } }
|
|
}
|
|
}
|
|
|
|
// Icon
|
|
Text {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: root.osdIcon
|
|
color: root.osdMuted ? Shared.Theme.overlay0 : Shared.Theme.sky
|
|
font.pixelSize: 16
|
|
font.family: Shared.Theme.iconFont
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|