a BUNCH of qml which I havent reviewed yet
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 !== ""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)}%`
|
||||
: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)`
|
||||
: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)}%`
|
||||
: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}")`])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user