rewrite bar with quickshell

This commit is contained in:
2025-10-11 16:28:11 +02:00
parent e1a02f7994
commit abadf04aa2
49 changed files with 10246 additions and 7 deletions

View File

@@ -73,7 +73,7 @@
(scale :min 0 :max 101 :class "brightness-scale" :width 150 (scale :min 0 :max 101 :class "brightness-scale" :width 150
:value {brightness == "" ? 0 : brightness} :value {brightness == "" ? 0 : brightness}
:round-digits 0 :round-digits 0
:onchange { brightness-hover ? "brightnessctl set {}\% -d $DISPLAY_DEVICE" : "" }))) :onchange { brightness-hover ? "brightnessctl set {}\% --class=backlight" : "" })))
(box :class "cpu-stats" :hexpand "false" :vexpand "false" :space-evenly "false" (box :class "cpu-stats" :hexpand "false" :vexpand "false" :space-evenly "false"
(label :tooltip "${cpu}%" :class "cpu-icon" :text "󰘚") (label :tooltip "${cpu}%" :class "cpu-icon" :text "󰘚")
(scale :min 0 :max 101 :active false :value {cpu == "" ? 0 : cpu} :class "cpu-scale" :width 150)) (scale :min 0 :max 101 :active false :value {cpu == "" ? 0 : cpu} :class "cpu-scale" :width 150))

View File

@@ -111,8 +111,15 @@ animations {
layer-rule { layer-rule {
match namespace="^swww-daemonbackdrop$" match namespace="^swww-daemonbackdrop$"
place-within-backdrop true place-within-backdrop true
} }
layer-rule {
match namespace="^quickshell-bar$"
place-within-backdrop false
}
/************************Autostart************************/ /************************Autostart************************/
// Switch configs // Switch configs
@@ -247,7 +254,6 @@ window-rule {
open-on-workspace "special" open-on-workspace "special"
} }
/************************Others************************/ /************************Others************************/
cursor { cursor {
@@ -300,7 +306,7 @@ binds {
Mod+Shift+C { spawn-sh "hyprpicker -a"; } Mod+Shift+C { spawn-sh "hyprpicker -a"; }
// Session // Session
Mod+L { spawn "loginctl lock-session"; } Mod+L { spawn-sh "loginctl lock-session"; }
// Media // Media
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ && wp-vol"; } XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ && wp-vol"; }

View File

@@ -111,8 +111,10 @@ animations {
layer-rule { layer-rule {
match namespace="^swww-daemonbackdrop$" match namespace="^swww-daemonbackdrop$"
place-within-backdrop true place-within-backdrop true
} }
/************************Autostart************************/ /************************Autostart************************/
// Switch configs // Switch configs
@@ -247,7 +249,6 @@ window-rule {
open-on-workspace "special" open-on-workspace "special"
} }
/************************Others************************/ /************************Others************************/
cursor { cursor {
@@ -300,7 +301,7 @@ binds {
Mod+Shift+C { spawn-sh "hyprpicker -a"; } Mod+Shift+C { spawn-sh "hyprpicker -a"; }
// Session // Session
Mod+L { spawn "loginctl lock-session"; } Mod+L { spawn-sh "loginctl lock-session"; }
// Media // Media
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ && wp-vol"; } XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ && wp-vol"; }

View File

@@ -0,0 +1,16 @@
Tabler Licenses - Detailed Usage Rights and Guidelines
This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Admin Template, Tabler Icons, Tabler Emails, Tabler Illustrations), constitutes your acceptance of the terms of this license, Tabler terms of service and Tabler private policy.
Tabler Admin Template and Tabler Icons License*
Tabler Admin Template and Tabler Icons are available under MIT License.
Copyright (c) 2018-2025 Tabler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
See more at Tabler Admin Template MIT License See more at Tabler Icons MIT License

View File

Binary file not shown.

2
quickshell/Assets/Ip/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
token.txt
# cache.json

View File

@@ -0,0 +1,38 @@
import QtQuick
import Quickshell
pragma Singleton
Singleton {
id: root
readonly property color transparent: "transparent"
readonly property color rosewater: "#f5e0dc"
readonly property color flamingo: "#f2cdcd"
readonly property color pink: "#f5c2e7"
readonly property color mauve: "#cba6f7"
readonly property color red: "#f38ba8"
readonly property color maroon: "#eba0ac"
readonly property color peach: "#fab387"
readonly property color yellow: "#f9e2af"
readonly property color green: "#a6e3a1"
readonly property color teal: "#94e2d5"
readonly property color sky: "#89dceb"
readonly property color sapphire: "#74c7ec"
readonly property color blue: "#89b4fa"
readonly property color lavender: "#b4befe"
readonly property color text: "#cdd6f4"
readonly property color subtext1: "#bac2de"
readonly property color subtext0: "#a6adc8"
readonly property color overlay2: "#9399b2"
readonly property color overlay1: "#7f849c"
readonly property color overlay0: "#6c7086"
readonly property color surface2: "#585b70"
readonly property color surface1: "#45475a"
readonly property color surface0: "#313244"
readonly property color base: "#1e1e2e"
readonly property color mantle: "#181825"
readonly property color crust: "#11111b"
readonly property color accent: "#89b4fa"
readonly property color distroColor: "#74c7ec"
readonly property var cavaList: ["#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5", "#a6e3a1", "#f9e2af", "#fab387"]
}

View File

@@ -0,0 +1,15 @@
import QtQuick
import Quickshell
pragma Singleton
Singleton {
id: root
readonly property string primary: "Sour Gummy Light"
readonly property string nerd: "Meslo LGM Nerd Font Mono"
readonly property string sans: "Noto Sans"
readonly property int small: 10
readonly property int medium: 12
readonly property int large: 14
readonly property int icon: 14
}

View File

@@ -0,0 +1,97 @@
import QtQuick
import Quickshell
pragma Singleton
Singleton {
id: root
// Nerd fonts icons
readonly property string distro: "󰣇"
readonly property string tray: ""
readonly property string idleInhibitorActivated: ""
readonly property string idleInhibitorDeactivated: ""
readonly property string powerMenu: "󰐥"
readonly property string volumeHigh: ""
readonly property string volumeMedium: ""
readonly property string volumeLow: ""
readonly property string volumeMuted: "󰝟"
readonly property string brightness: ""
readonly property string charging: ""
readonly property string battery100: ""
readonly property string battery75: ""
readonly property string battery50: ""
readonly property string battery25: ""
readonly property string battery00: ""
readonly property string cpu: "󰘚"
readonly property string memory: "󰍛"
readonly property string tempHigh: ""
readonly property string tempMedium: ""
readonly property string tempLow: ""
readonly property string global: ""
readonly property string upload: ""
readonly property string download: ""
// Expose the font family name for easy access
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
readonly property string defaultIcon: TablerIcons.defaultIcon
readonly property var icons: TablerIcons.icons
readonly property var aliases: TablerIcons.aliases
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf"
// Current active font loader
property FontLoader currentFontLoader: null
property int fontVersion: 0
// Create a unique cache-busting path
readonly property string cacheBustingPath: Quickshell.shellDir + fontPath + "?v=" + fontVersion + "&t=" + Date.now()
// Signal emitted when font is reloaded
signal fontReloaded()
// ---------------------------------------
function get(iconName) {
// Check in aliases first
if (aliases[iconName] !== undefined)
iconName = aliases[iconName];
// Find the appropriate codepoint
return icons[iconName];
}
function loadFontWithCacheBusting() {
// Destroy old loader first
if (currentFontLoader) {
currentFontLoader.destroy();
currentFontLoader = null;
}
// Create new loader with cache-busting URL
currentFontLoader = Qt.createQmlObject(`
import QtQuick
FontLoader {
source: "${cacheBustingPath}"
}
`, root, "dynamicFontLoader_" + fontVersion);
// Connect to the new loader's status changes
currentFontLoader.statusChanged.connect(function() {
if (currentFontLoader.status === FontLoader.Ready)
fontReloaded();
else if (currentFontLoader.status === FontLoader.Error)
Logger.error("Font failed to load (version " + fontVersion + ")");
});
}
function reloadFont() {
fontVersion++;
loadFontWithCacheBusting();
}
Component.onCompleted: {
loadFontWithCacheBusting();
}
Connections {
function onReloadCompleted() {
reloadFont();
}
target: Quickshell
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,260 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import Quickshell.Wayland
import qs.Constants
import qs.Modules.Bar.Components
import qs.Modules.Bar.Misc
import qs.Modules.Misc
import qs.Services
Scope {
id: rootScope
property var shell
Item {
id: barRootItem
anchors.fill: parent
Variants {
model: Quickshell.screens
Item {
property var modelData
PanelWindow {
id: panel
screen: modelData
WlrLayershell.namespace: "quickshell-bar"
color: Colors.transparent
implicitHeight: 45
anchors {
left: true
right: true
top: true
}
Rectangle {
id: barBackground
anchors.fill: parent
color: Niri.noFocus ? null : Colors.base
gradient: Gradient {
GradientStop {
position: 0
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0.8 : 1)
Behavior on color {
ColorAnimation {
duration: 1000
easing.type: Easing.InOutCubic
}
}
}
GradientStop {
position: 1
color: Qt.rgba(Colors.base.r, Colors.base.g, Colors.base.b, Niri.noFocus ? 0 : 1)
Behavior on color {
ColorAnimation {
duration: 1000
easing.type: Easing.InOutCubic
}
}
}
}
}
RowLayout {
id: leftLayout
height: parent.height - 10
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: 5
}
SymbolButton {
symbol: Icons.distro
buttonColor: Colors.distroColor
onRightClicked: {
if (action.running) {
action.signal(15);
return ;
}
action.exec(["rofi", "-show", "drun"]);
}
}
Separator {
}
Workspace {
screen: modelData
}
Separator {
}
Item {
width: 10
}
CavaBar {
count: 6
}
Item {
width: 10
}
Separator {
}
Item {
width: 10
}
FocusedWindow {
maxWidth: 400
}
}
RowLayout {
id: middleLayout
height: parent.height - 10
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
Time {
}
}
RowLayout {
id: rightLayout
height: parent.height - 10
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: 5
}
NetworkSpeed {
}
Separator {
}
Item {
width: 10
}
Ip {
showCountryCode: true
}
CpuTemp {
}
MemUsage {
}
CpuUsage {
}
Battery {
}
Brightness {
screen: modelData
}
Volume {
}
Item {
width: 5
}
Separator {
}
Item {
width: 5
}
TrayExpander {
screen: modelData
}
SymbolButton {
symbol: Caffeine.isInhibited ? Icons.idleInhibitorActivated : Icons.idleInhibitorDeactivated
buttonColor: Caffeine.isInhibited ? Colors.peach : Colors.yellow
onClicked: {
Caffeine.manualToggle();
}
Behavior on buttonColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutCubic
}
}
}
SymbolButton {
symbol: Icons.powerMenu
buttonColor: Colors.red
onClicked: {
if (action.running) {
action.signal(15);
return ;
}
action.exec(["wlogout"]);
}
}
}
Process {
id: action
running: false
}
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
import QtQuick
import Quickshell.Services.UPower
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
readonly property var battery: UPower.displayDevice
readonly property bool isReady: (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
readonly property real percent: (isReady ? (battery.percentage * 100) : 0)
readonly property bool charging: (isReady ? battery.state === UPowerDeviceState.Charging : false)
property int lowBatteryThreshold: 15
symbol: {
return charging ? Icons.charging : percent >= 80 ? Icons.battery100 : percent >= 60 ? Icons.battery75 : percent >= 40 ? Icons.battery50 : percent >= 20 ? Icons.battery25 : Icons.battery00;
}
fillColor: !isReady || charging || percent > lowBatteryThreshold ? Colors.sapphire : Colors.red
value: percent
maxValue: 100
textSuffix: "%"
pointerCursor: false
}

View File

@@ -0,0 +1,34 @@
import QtQuick
import Quickshell
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
property ShellScreen screen: null
function getMonitor() {
return BrightnessService.getMonitorForScreen(screen) || null;
}
symbol: Icons.brightness
fillColor: Colors.blue
value: {
const monitor = getMonitor();
return monitor ? Math.round(monitor.brightness * 100) : "N/A";
}
maxValue: 100
textSuffix: "%"
onWheelUp: {
const monitor = getMonitor();
if (monitor)
monitor.increaseBrightness();
}
onWheelDown: {
const monitor = getMonitor();
if (monitor)
monitor.decreaseBrightness();
}
}

View File

@@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
import qs.Modules.Misc
import qs.Services
Item {
id: root
property int count: 6
property int barWidth: 5
property int barSpacing: 3
implicitWidth: root.barWidth * root.count + root.barSpacing * (root.count - 1)
implicitHeight: parent.height - 10
Cava {
id: cavaProcess
count: root.count
}
RowLayout {
anchors.fill: parent
spacing: root.barSpacing
anchors {
verticalCenter: parent.verticalCenter
}
Repeater {
model: cavaProcess.values
Rectangle {
width: root.barWidth
implicitHeight: Math.max(1, modelData * (parent.height - 10))
color: Colors.cavaList[Math.min(Math.floor(modelData * (Colors.cavaList.length - 1)), Colors.cavaList.length - 1)]
Behavior on height {
NumberAnimation {
duration: 100
easing.type: Easing.InOutCubic
}
}
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
MusicManager.playPause();
}
onWheel: function(wheel) {
if (wheel.angleDelta.y > 0)
MusicManager.previous();
else if (wheel.angleDelta.y < 0)
MusicManager.next();
}
}
}

View File

@@ -0,0 +1,27 @@
import QtQuick
import Quickshell.Io
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
symbol: Icons.cpuTemp > 80 ? Icons.tempHigh : Icons.cpuTemp > 50 ? Icons.tempMedium : Icons.tempLow
fillColor: Icons.cpuTemp > 80 ? Colors.red : Colors.yellow
value: Math.round(SystemStatService.cpuTemp)
maxValue: 120
textSuffix: "°C"
onClicked: {
if (action.running) {
action.signal(15);
return ;
}
action.exec(["ghostty", "-e", "btop"]);
}
Process {
id: action
running: false
}
}

View File

@@ -0,0 +1,27 @@
import QtQuick
import Quickshell.Io
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
symbol: Icons.cpu
fillColor: Colors.teal
value: Math.round(SystemStatService.cpuUsage)
maxValue: 100
textSuffix: "%"
onClicked: {
if (action.running) {
action.signal(15);
return ;
}
action.exec(["ghostty", "-e", "btop"]);
}
Process {
id: action
running: false
}
}

View File

@@ -0,0 +1,144 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
import Quickshell.Widgets
import qs.Constants
import qs.Services
Item {
id: root
property real maxWidth: 250
property string fallbackIcon: "application-x-executable"
function getAppIcon() {
try {
const focusedWindow = Niri.getFocusedWindow();
if (focusedWindow && focusedWindow.appId) {
try {
const idValue = focusedWindow.appId;
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue);
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase());
if (iconResult && iconResult !== "")
return iconResult;
} catch (iconError) {
console.warn("Error getting icon from CompositorService:", iconError);
}
}
return ThemeIcons.iconFromName(root.fallbackIcon);
} catch (e) {
console.warn("Error in getAppIcon:", e);
return ThemeIcons.iconFromName(root.fallbackIcon);
}
}
implicitHeight: parent.height
RowLayout {
id: layout
anchors.fill: parent
spacing: 10
visible: Niri.focusedWindowId !== -1
Item {
// Layout.alignment: Qt.AlignVCenter
id: iconContainer
implicitWidth: 18
implicitHeight: 18
IconImage {
id: windowIcon
anchors.fill: parent
source: getAppIcon()
asynchronous: true
smooth: true
visible: source !== ""
}
}
Item {
id: titleContainer
implicitWidth: root.maxWidth
implicitHeight: parent.height
// Layout.alignment: Qt.AlignVCenter
clip: true
Text {
id: windowTitle
text: Niri.focusedWindowTitle
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: Colors.accent
Process {
id: action
running: false
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onEntered: {
if (windowTitle.implicitWidth > titleContainer.width)
windowTitle.x = titleContainer.width - windowTitle.implicitWidth;
}
onExited: {
windowTitle.x = 0;
}
onClicked: function(mouse) {
if (mouse.button === Qt.MiddleButton) {
action.command = ["niri", "msg", "action", "close-window"];
action.startDetached();
} else if (mouse.button === Qt.LeftButton) {
action.command = ["niri", "msg", "action", "center-window"];
action.startDetached();
}
}
onWheel: function(wheel) {
if (wheel.angleDelta.y > 0) {
action.command = ["niri", "msg", "action", "set-column-width", "+10%"];
action.startDetached();
} else if (wheel.angleDelta.y < 0) {
action.command = ["niri", "msg", "action", "set-column-width", "-10%"];
action.startDetached();
} else if (wheel.angleDelta.x > 0) {
action.command = ["niri", "msg", "action", "focus-column-left"];
action.startDetached();
} else if (wheel.angleDelta.x < 0) {
action.command = ["niri", "msg", "action", "focus-column-right"];
action.startDetached();
}
}
}
Behavior on x {
NumberAnimation {
duration: 1000
easing.type: Easing.OutCubic
}
}
}
}
}
}

View File

@@ -0,0 +1,85 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Services
Item {
// Text {
// id: ipText
// anchors.verticalCenter: parent.verticalCenter
// text: Icons.global + " " + (showCountryCode ? IpService.countryCode : IpService.ip)
// font.pixelSize: Fonts.medium
// color: Colors.peach
// }
id: root
property bool showCountryCode: true
implicitHeight: parent.height
implicitWidth: layout.width + 10
RowLayout {
id: layout
anchors.top: parent.top
anchors.bottom: parent.bottom
spacing: 0
Text {
text: Icons.global
font.pointSize: Fonts.icon + 5
color: Colors.peach
}
Item {
id: expander
implicitWidth: mouseArea.containsMouse ? ipText.implicitWidth + 10 : 0
implicitHeight: parent.height
clip: true
Text {
id: ipText
text: showCountryCode ? IpService.countryCode : IpService.ip
font.pointSize: showCountryCode ? Fonts.medium : Fonts.small
font.family: Fonts.primary
color: Colors.peach
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
}
Behavior on implicitWidth {
NumberAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
WriteClipboard.write(showCountryCode ? IpService.countryCode : IpService.ip);
SendNotification.show("Copied to clipboard", showCountryCode ? IpService.countryCode : IpService.ip);
} else if (mouse.button === Qt.RightButton)
showCountryCode = !showCountryCode;
else if (mouse.button === Qt.MiddleButton)
IpService.refresh();
}
}
}

View File

@@ -0,0 +1,28 @@
import QtQuick
import Quickshell.Io
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
symbol: Icons.memory
fillColor: Colors.green
value: Math.round(SystemStatService.memPercent)
maxValue: 100
textValue: SystemStatService.memGb
textSuffix: "G"
onClicked: {
if (action.running) {
action.signal(15);
return ;
}
action.exec(["ghostty", "-e", "btop"]);
}
Process {
id: action
running: false
}
}

View File

@@ -0,0 +1,52 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Services
Item {
implicitHeight: parent.height
implicitWidth: layout.width + 10
RowLayout {
id: layout
anchors.top: parent.top
anchors.bottom: parent.bottom
spacing: 5
Text {
text: Icons.download
font.pointSize: Fonts.icon - 3
color: Colors.accent
Layout.leftMargin: 10
}
Text {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: Colors.accent
}
Item {
width: 5
}
Text {
text: Icons.upload
font.pointSize: Fonts.icon - 3
color: Colors.accent
}
Text {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: Colors.accent
}
}
}

View File

@@ -0,0 +1,18 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
Item {
id: root
implicitHeight: parent.height
Rectangle {
anchors.centerIn: parent
width: 1.5
height: parent.height * 0.32
color: Colors.text
}
}

View File

@@ -0,0 +1,10 @@
import QtQuick
import qs.Constants
import qs.Services
Text {
text: TimeService.time + " | " + TimeService.dateString
font.pointSize: Fonts.medium
font.family: Fonts.primary
color: Colors.accent
}

View File

@@ -0,0 +1,60 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Constants
import qs.Modules.Bar.Misc
Item {
id: root
property ShellScreen screen
implicitHeight: parent.height
implicitWidth: layout.implicitWidth
RowLayout {
id: layout
anchors.top: parent.top
anchors.bottom: parent.bottom
SymbolButton {
symbol: Icons.tray
buttonColor: Colors.green
}
Item {
id: trayContainer
implicitHeight: parent.height
implicitWidth: mouseArea.containsMouse ? expandedTray.implicitWidth : 0
clip: true
SystemTray {
id: expandedTray
screen: root.screen
}
Behavior on implicitWidth {
NumberAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
}

View File

@@ -0,0 +1,21 @@
import QtQuick
import qs.Constants
import qs.Modules.Bar.Misc
import qs.Services
MonitorItem {
symbol: AudioService.muted ? Icons.volumeMuted : (AudioService.volume >= 0.66 ? Icons.volumeHigh : (AudioService.volume >= 0.33 ? Icons.volumeMedium : Icons.volumeLow))
fillColor: Colors.lavender
value: Math.round(AudioService.volume * 100)
maxValue: 100
textSuffix: "%"
onWheelUp: {
AudioService.increaseVolume();
}
onWheelDown: {
AudioService.decreaseVolume();
}
onClicked: {
AudioService.toggleMute();
}
}

View File

@@ -0,0 +1,299 @@
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Quickshell
import Quickshell.Io
import qs.Constants
import qs.Services
Item {
id: root
required property ShellScreen screen
property bool hovered: false
property ListModel localWorkspaces
property real masterProgress: 0
property bool effectsActive: false
property color effectColor: Colors.accent
property int horizontalPadding: 16
property int spacingBetweenPills: 8
property bool isDestroying: false
signal workspaceChanged(int workspaceId, color accentColor)
function triggerUnifiedWave() {
effectColor = Colors.accent;
masterAnimation.restart();
}
function updateWorkspaceFocus() {
for (let i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i);
if (ws.isFocused === true) {
root.triggerUnifiedWave();
root.workspaceChanged(ws.id, Colors.accent);
break;
}
}
}
implicitWidth: {
let total = 0;
for (let i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i);
if (ws.isFocused)
total += 44;
else if (ws.isActive)
total += 28;
else
total += 16;
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills;
total += horizontalPadding * 2;
return total;
}
height: parent.height
Component.onCompleted: {
localWorkspaces.clear();
for (let i = 0; i < WorkspaceManager.workspaces.count; i++) {
const ws = WorkspaceManager.workspaces.get(i);
if (ws.output.toLowerCase() === screen.name.toLowerCase())
localWorkspaces.append(ws);
}
workspaceRepeater.model = localWorkspaces;
updateWorkspaceFocus();
}
Component.onDestruction: {
root.isDestroying = true;
}
Connections {
function onWorkspacesChanged() {
localWorkspaces.clear();
for (let i = 0; i < WorkspaceManager.workspaces.count; i++) {
const ws = WorkspaceManager.workspaces.get(i);
if (ws.output.toLowerCase() === screen.name.toLowerCase())
localWorkspaces.append(ws);
}
workspaceRepeater.model = localWorkspaces;
updateWorkspaceFocus();
}
target: WorkspaceManager
}
SequentialAnimation {
id: masterAnimation
PropertyAction {
target: root
property: "effectsActive"
value: true
}
NumberAnimation {
target: root
property: "masterProgress"
from: 0
to: 1
duration: 1000
easing.type: Easing.OutQuint
}
PropertyAction {
target: root
property: "effectsActive"
value: false
}
PropertyAction {
target: root
property: "masterProgress"
value: 0
}
}
Rectangle {
id: workspaceBackground
width: parent.width - 15
height: 26
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
radius: 12
color: Colors.transparent
layer.enabled: true
layer.effect: DropShadow {
color: "black"
radius: 12
samples: 24
verticalOffset: 0
horizontalOffset: 0
opacity: 0.1
}
}
Row {
id: pillRow
spacing: spacingBetweenPills
anchors.verticalCenter: workspaceBackground.verticalCenter
width: root.width - horizontalPadding * 2
x: horizontalPadding
Repeater {
id: workspaceRepeater
model: localWorkspaces
Item {
id: workspacePillContainer
height: 12
width: {
if (model.isFocused)
return 44;
else if (model.isActive)
return 28;
else
return 16;
}
Rectangle {
// half of focused height (if you want to animate this too)
id: workspacePill
anchors.fill: parent
radius: {
if (model.isFocused)
return 12;
else
return 6;
}
color: {
if (model.isFocused)
return Colors.accent;
if (model.isActive)
return Colors.accent.lighter(130);
if (model.isUrgent)
return Theme.error;
return Colors.surface2;
}
scale: model.isFocused ? 1 : 0.9
z: 0
MouseArea {
id: pillMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceManager.switchToWorkspace(model.idx);
}
z: 20
hoverEnabled: true
}
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
Behavior on scale {
NumberAnimation {
duration: 300
easing.type: Easing.OutBack
}
}
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
Behavior on opacity {
NumberAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
Behavior on radius {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
}
Rectangle {
id: pillBurst
anchors.centerIn: workspacePillContainer
width: workspacePillContainer.width + 18 * root.masterProgress
height: workspacePillContainer.height + 18 * root.masterProgress
radius: width / 2
color: "transparent"
border.color: root.effectColor
border.width: 2 + 6 * (1 - root.masterProgress)
opacity: root.effectsActive && model.isFocused ? (1 - root.masterProgress) * 0.7 : 0
visible: root.effectsActive && model.isFocused
z: 1
}
Behavior on width {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: 350
easing.type: Easing.OutBack
}
}
}
}
}
localWorkspaces: ListModel {
}
}

View File

@@ -0,0 +1,140 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Constants
Item {
id: root
required property string symbol
property real maxValue: 100
property real value: 100
property string textValue: "" // override value in textDisplay if set
property color fillColor: Colors.accent
property string textSuffix: ""
property bool pointerCursor: true
property alias hovered: mouseArea.containsMouse
readonly property real ratio: value / maxValue
signal wheelUp()
signal wheelDown()
signal clicked()
implicitHeight: parent.height - 5
implicitWidth: parent.height + (hovered ? textDisplay.width : 0)
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: pointerCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton)
root.clicked();
else if (mouse.button === Qt.RightButton)
root.rightClicked();
}
onWheel: (wheel) => {
if (wheel.angleDelta.y > 0)
root.wheelUp();
else if (wheel.angleDelta.y < 0)
root.wheelDown();
}
}
RowLayout {
anchors.top: parent.top
anchors.bottom: parent.bottom
spacing: 0
Item {
id: progressDisplay
Layout.preferredHeight: parent.height
Layout.preferredWidth: parent.height
Canvas {
id: progressCircle
anchors.fill: parent
anchors.centerIn: parent
onPaint: {
var ctx = getContext("2d");
ctx.reset();
var centerX = width / 2;
var centerY = height / 2;
var radius = width / 2 - 3;
var startAngle = -Math.PI / 2;
var endAngle = startAngle - (2 * Math.PI * root.ratio);
ctx.beginPath();
ctx.arc(centerX, centerY, radius, endAngle, startAngle, false);
ctx.lineWidth = 3;
ctx.strokeStyle = root.fillColor;
ctx.lineCap = "round";
ctx.stroke();
}
Connections {
function onRatioChanged() {
progressCircle.requestPaint();
}
function onFillColorChanged() {
progressCircle.requestPaint();
}
target: root
}
}
Text {
id: symbolText
anchors.fill: parent
text: symbol
font.family: Fonts.nerd
font.pointSize: Fonts.icon
color: fillColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
Item {
id: textDisplay
implicitHeight: parent.height
implicitWidth: root.hovered ? textLabel.implicitWidth + 10 : 0
clip: true
Text {
id: textLabel
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
text: (textValue || Math.round(root.value)) + root.textSuffix
font.pointSize: Fonts.small
font.family: Fonts.primary
color: root.fillColor
opacity: root.hovered ? 1 : 0
}
Behavior on implicitWidth {
NumberAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Constants
Item {
id: root
required property string symbol
property color buttonColor: Colors.distroColor
readonly property alias hovered: mouseArea.containsMouse
signal clicked()
signal rightClicked()
implicitHeight: parent.height
implicitWidth: parent.height
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton)
root.rightClicked();
else if (mouse.button === Qt.LeftButton)
root.clicked();
}
}
Text {
anchors.fill: parent
text: symbol
font.family: Fonts.nerd
font.pointSize: Fonts.icon
font.bold: false
color: buttonColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
anchors.fill: parent
color: parent.hovered ? buttonColor : Colors.transparent
opacity: 0.3
radius: 14
Behavior on color {
ColorAnimation {
duration: 120
}
}
}
}

View File

@@ -0,0 +1,160 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import qs.Modules.Bar.Misc
import qs.Constants
import qs.Services
Rectangle {
id: root
property ShellScreen screen
implicitWidth: trayFlow.implicitWidth + 20
implicitHeight: parent.height
radius: 0
color: Colors.transparent
Layout.alignment: Qt.AlignVCenter
Flow {
id: trayFlow
anchors.centerIn: parent
spacing: 8
flow: Flow.LeftToRight
Repeater {
id: repeater
model: SystemTray.items
delegate: Item {
width: 18
height: 18
visible: modelData
IconImage {
id: trayIcon
property ShellScreen screen: root.screen
anchors.centerIn: parent
width: 14
height: 14
smooth: false
asynchronous: true
backer.fillMode: Image.PreserveAspectFit
source: {
let icon = modelData?.icon || ""
if (!icon) {
return ""
}
// Process icon path
if (icon.includes("?path=")) {
// Seems qmlfmt does not support the following ES6 syntax: const[name, path] = icon.split
const chunks = icon.split("?path=")
const name = chunks[0]
const path = chunks[1]
const fileName = name.substring(name.lastIndexOf("/") + 1)
return `file://${path}/${fileName}`
}
return icon
}
opacity: status === Image.Ready ? 1 : 0
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (!modelData) {
return
}
if (mouse.button === Qt.LeftButton) {
// Close any open menu first
trayPanel.close()
if (!modelData.onlyMenu) {
modelData.activate()
}
} else if (mouse.button === Qt.MiddleButton) {
// Close any open menu first
trayPanel.close()
modelData.secondaryActivate && modelData.secondaryActivate()
} else if (mouse.button === Qt.RightButton) {
// Close the menu if it was visible
if (trayPanel && trayPanel.visible) {
trayPanel.close()
return
}
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
trayPanel.open()
// Position menu based on bar position
let menuX, menuY
// For horizontal bars: center horizontally and position below
menuX = (width / 2) - (trayMenu.item.width / 2)
menuY = root.height
trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY)
} else {
// Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
console.log("No menu available for", modelData.id, "or trayMenu not set")
}
}
}
onEntered: {
trayPanel.close()
}
}
}
}
}
PanelWindow {
id: trayPanel
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
visible: false
color: Colors.transparent
screen: root.screen
function open() {
visible = true
PanelService.willOpenPanel(trayPanel)
}
function close() {
visible = false
if (trayMenu.item) {
trayMenu.item.hideMenu()
}
}
// Clicking outside of the rectangle to close
MouseArea {
anchors.fill: parent
onClicked: trayPanel.close()
}
Loader {
id: trayMenu
Component.onCompleted: {
setSource("../Misc/TrayMenu.qml", {
"screen": root.screen
})
}
}
}
}

View File

@@ -0,0 +1,282 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Constants
PopupWindow {
id: root
property QsMenuHandle menu
property var anchorItem: null
property real anchorX
property real anchorY
property bool isSubMenu: false
property bool isHovered: rootMouseArea.containsMouse
property ShellScreen screen
readonly property int menuWidth: 180
implicitWidth: menuWidth
// Use the content height of the Flickable for implicit height
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + 20)
visible: false
color: Colors.transparent
anchor.item: anchorItem
anchor.rect.x: anchorX
anchor.rect.y: anchorY - (isSubMenu ? 0 : 4)
function showAt(item, x, y) {
if (!item) {
console.warn("AnchorItem is undefined, won't show menu.")
return
}
if (!opener.children || opener.children.values.length === 0) {
Qt.callLater(() => showAt(item, x, y))
return
}
anchorItem = item
anchorX = x
anchorY = y
visible = true
forceActiveFocus()
// Force update after showing.
Qt.callLater(() => {
root.anchor.updateAnchor()
})
}
function hideMenu() {
visible = false
// Clean up all submenus recursively
for (var i = 0; i < columnLayout.children.length; i++) {
const child = columnLayout.children[i]
if (child?.subMenu) {
child.subMenu.hideMenu()
child.subMenu.destroy()
child.subMenu = null
}
}
}
// Full-sized, transparent MouseArea to track the mouse.
MouseArea {
id: rootMouseArea
anchors.fill: parent
hoverEnabled: true
}
Item {
anchors.fill: parent
Keys.onEscapePressed: root.hideMenu()
}
QsMenuOpener {
id: opener
menu: root.menu
}
Rectangle {
anchors.fill: parent
color: Colors.base
border.color: Colors.accent
border.width: 2
radius: 14
}
Flickable {
id: flickable
anchors.fill: parent
anchors.margins: 10
contentHeight: columnLayout.implicitHeight
interactive: true
// Use a ColumnLayout to handle menu item arrangement
ColumnLayout {
id: columnLayout
width: flickable.width
spacing: 0
Repeater {
model: opener.children ? [...opener.children.values] : []
delegate: Rectangle {
id: entry
required property var modelData
Layout.preferredWidth: parent.width
Layout.preferredHeight: {
if (modelData?.isSeparator) {
return 8
} else {
// Calculate based on text content
const textHeight = text.contentHeight || (Fonts.small)
return textHeight + 16
}
}
color: Colors.transparent
property var subMenu: null
Rectangle {
width: parent.width - 16
height: 1
anchors.centerIn: parent
visible: modelData?.isSeparator ?? false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Colors.transparent
}
GradientStop {
position: 0.1
color: Colors.accent
}
GradientStop {
position: 0.9
color: Colors.accent
}
GradientStop {
position: 1.0
color: Colors.transparent
}
}
}
Rectangle {
anchors.fill: parent
color: mouseArea.containsMouse ? Colors.accent : Colors.transparent
radius: 10
visible: !(modelData?.isSeparator ?? false)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
Text {
id: text
Layout.fillWidth: true
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Colors.base : Colors.text) : Colors.text
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
font.pointSize: Fonts.small
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
Image {
Layout.preferredWidth: 14
Layout.preferredHeight: 14
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
Text {
verticalAlignment: Text.AlignVCenter
visible: modelData?.hasChildren ?? false
color: (mouseArea.containsMouse ? Colors.base : Colors.text)
text: {
const icon = modelData?.hasChildren ? "menu" : ""
if ((icon === undefined) || (icon === "")) {
return ""
}
if (Icons.get(icon) === undefined) {
console.warn("Icon", `"${icon}"`, "doesn't exist in the icons font")
return Icons.get(Icons.defaultIcon)
}
return Icons.get(icon)
}
font.family: Icons.fontFamily
font.pointSize: Fonts.small
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && root.visible
onClicked: {
if (modelData && !modelData.isSeparator && !modelData.hasChildren) {
modelData.triggered()
root.hideMenu()
}
}
onEntered: {
if (!root.visible)
return
// Close all sibling submenus
for (var i = 0; i < columnLayout.children.length; i++) {
const sibling = columnLayout.children[i]
if (sibling !== entry && sibling?.subMenu) {
sibling.subMenu.hideMenu()
sibling.subMenu.destroy()
sibling.subMenu = null
}
}
// Create submenu if needed
if (modelData?.hasChildren) {
if (entry.subMenu) {
entry.subMenu.hideMenu()
entry.subMenu.destroy()
}
// Need a slight overlap so that menu don't close when moving the mouse to a submenu
const submenuWidth = menuWidth // Assuming a similar width as the parent
const overlap = 4 // A small overlap to bridge the mouse path
// Position with overlap
const anchorX = -submenuWidth + overlap
// Create submenu
entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, {
"menu": modelData,
"anchorItem": entry,
"anchorX": anchorX,
"anchorY": 0,
"isSubMenu": true,
"screen": root.screen
})
if (entry.subMenu) {
entry.subMenu.showAt(entry, anchorX, 0)
}
}
}
onExited: {
Qt.callLater(() => {
if (entry.subMenu && !entry.subMenu.isHovered) {
entry.subMenu.hideMenu()
entry.subMenu.destroy()
entry.subMenu = null
}
})
}
}
}
Component.onDestruction: {
if (subMenu) {
subMenu.destroy()
subMenu = null
}
}
}
}
}
}
}

View File

@@ -0,0 +1,81 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
Scope {
id: root
property int count: 32
property int noiseReduction: 60
property string channels: "mono"
property string monoOption: "average"
property var config: ({
"general": {
"bars": count,
"framerate": 30,
"autosens": 1
},
"smoothing": {
"monstercat": 1,
"gravity": 1e+06,
"noise_reduction": noiseReduction
},
"output": {
"method": "raw",
"bit_format": 8,
"channels": channels,
"mono_option": monoOption
}
})
property var values: Array(count).fill(0)
Process {
id: process
property int index: 0
stdinEnabled: true
running: !MusicManager.isAllPaused()
command: ["cava", "-p", "/dev/stdin"]
onExited: {
stdinEnabled = true;
index = 0;
values = Array(count).fill(0);
}
onStarted: {
for (const k in config) {
if (typeof config[k] !== "object") {
write(k + "=" + config[k] + "\n");
continue;
}
write("[" + k + "]\n");
const obj = config[k];
for (const k2 in obj) {
write(k2 + "=" + obj[k2] + "\n");
}
}
stdinEnabled = false;
}
stdout: SplitParser {
splitMarker: ""
onRead: (data) => {
const newValues = Array(count).fill(0);
for (let i = 0; i < values.length; i++) {
newValues[i] = values[i];
}
if (process.index + data.length > count)
process.index = 0;
for (let i = 0; i < data.length; i += 1) {
newValues[i + process.index] = Math.min(data.charCodeAt(i), 128) / 128;
}
process.index += data.length;
values = newValues;
}
}
}
}

View File

@@ -0,0 +1,10 @@
import QtQuick
import Quickshell
import qs.Constants
pragma Singleton
Singleton {
id: root
readonly property var colorList: [Colors.lavender, Colors.blue, Colors.sapphire, Colors.sky, Colors.teal, Colors.green, Colors.yellow, Colors.peach]
}

View File

@@ -0,0 +1,87 @@
import QtQuick
import QtQuick.Shapes
import qs.Constants
Shape {
id: root
property string position: "topleft" // Corner position: topleft/topright/bottomleft/bottomright
property real size: 1 // Scale multiplier for entire corner
property int concaveWidth: 100 * size
property int concaveHeight: 60 * size
property int offsetX: -20
property int offsetY: -20
property color fillColor: Colors.base
property int arcRadius: 20 * size
property var modelData: null
// Position flags derived from position string
property bool _isTop: position.includes("top")
property bool _isLeft: position.includes("left")
property bool _isRight: position.includes("right")
property bool _isBottom: position.includes("bottom")
// Shift the path vertically if offsetY is negative to pull shape up
property real pathOffsetY: Math.min(offsetY, 0)
// Base coordinates for left corner shape, shifted by pathOffsetY vertically
property real _baseStartX: 30 * size
property real _baseStartY: (_isTop ? 20 * size : 0) + pathOffsetY
property real _baseLineX: 30 * size
property real _baseLineY: (_isTop ? 0 : 20 * size) + pathOffsetY
property real _baseArcX: 50 * size
property real _baseArcY: (_isTop ? 20 * size : 0) + pathOffsetY
// Mirror coordinates for right corners
property real _startX: _isRight ? (concaveWidth - _baseStartX) : _baseStartX
property real _startY: _baseStartY
property real _lineX: _isRight ? (concaveWidth - _baseLineX) : _baseLineX
property real _lineY: _baseLineY
property real _arcX: _isRight ? (concaveWidth - _baseArcX) : _baseArcX
property real _arcY: _baseArcY
// Arc direction varies by corner to maintain proper concave shape
property int _arcDirection: {
if (_isTop && _isLeft)
return PathArc.Counterclockwise;
if (_isTop && _isRight)
return PathArc.Clockwise;
if (_isBottom && _isLeft)
return PathArc.Clockwise;
if (_isBottom && _isRight)
return PathArc.Counterclockwise;
return PathArc.Counterclockwise;
}
width: concaveWidth
height: concaveHeight
// Position relative to parent based on corner type
x: _isLeft ? offsetX : (parent ? parent.width - width + offsetX : 0)
y: _isTop ? offsetY : (parent ? parent.height - height + offsetY : 0)
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.samples: 4
ShapePath {
strokeWidth: 0
fillColor: root.fillColor
strokeColor: root.fillColor
startX: root._startX
startY: root._startY
PathLine {
x: root._lineX
y: root._lineY
}
PathArc {
x: root._arcX
y: root._arcY
radiusX: root.arcRadius
radiusY: root.arcRadius
useLargeArc: false
direction: root._arcDirection
}
}
}

View File

@@ -0,0 +1,178 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Constants
import qs.Modules.Misc
import qs.Services
Scope {
id: rootScope
property var shell
property string namespace: "quickshell-corners"
property int topMargin: 45
property int cornerHeight: 20
property real cornerSize: 1
property real opacity: Niri.noFocus ? 0 : 1
Item {
id: cornersRootItem
anchors.fill: parent
Variants {
model: Quickshell.screens
Item {
property var modelData
PanelWindow {
id: fakeBar
anchors.top: true
anchors.left: true
anchors.right: true
color: "transparent"
screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: namespace
implicitHeight: topMargin
Rectangle {
anchors.fill: parent
color: Colors.base
opacity: rootScope.opacity
}
}
PanelWindow {
id: topLeftPanel
anchors.top: true
anchors.left: true
color: "transparent"
screen: modelData
margins.top: topMargin
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: namespace
implicitHeight: cornerHeight
Corner {
id: topLeftCorner
position: "bottomleft"
size: rootScope.cornerSize
offsetX: -32
offsetY: 0
anchors.top: parent.top
opacity: rootScope.opacity
}
}
PanelWindow {
id: topRightPanel
anchors.top: true
anchors.right: true
color: "transparent"
screen: modelData
margins.top: topMargin
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: namespace
implicitHeight: cornerHeight
Corner {
id: topRightCorner
position: "bottomright"
size: rootScope.cornerSize
offsetX: 32
offsetY: 0
anchors.top: parent.top
opacity: rootScope.opacity
}
}
PanelWindow {
id: bottomLeftPanel
anchors.bottom: true
anchors.left: true
color: "transparent"
screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: namespace
implicitHeight: cornerHeight
Corner {
id: bottomLeftCorner
position: "topleft"
size: rootScope.cornerSize
offsetX: -32
offsetY: 0
anchors.top: parent.top
opacity: rootScope.opacity
}
}
PanelWindow {
id: bottomRightPanel
anchors.bottom: true
anchors.right: true
color: "transparent"
screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: namespace
implicitHeight: cornerHeight
Corner {
id: bottomRightCorner
position: "topright"
size: rootScope.cornerSize
offsetX: 32
offsetY: 0
anchors.top: parent.top
opacity: rootScope.opacity
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.InOutCubic
}
}
}

View File

@@ -0,0 +1,143 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
Singleton {
id: root
readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => {
if (!node.isStream) {
if (node.isSink) {
acc.sinks.push(node)
} else if (node.audio) {
acc.sources.push(node)
}
}
return acc
}, {
"sources": [],
"sinks": []
})
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
readonly property list<PwNode> sinks: nodes.sinks
readonly property list<PwNode> sources: nodes.sources
// Volume [0..1] is readonly from outside
readonly property alias volume: root._volume
property real _volume: sink?.audio?.volume ?? 0
readonly property alias muted: root._muted
property bool _muted: !!sink?.audio?.muted
// Input volume [0..1] is readonly from outside
readonly property alias inputVolume: root._inputVolume
property real _inputVolume: source?.audio?.volume ?? 0
readonly property alias inputMuted: root._inputMuted
property bool _inputMuted: !!source?.audio?.muted
readonly property real stepVolume: 5 / 100.0
PwObjectTracker {
objects: [...root.sinks, ...root.sources]
}
Connections {
target: sink?.audio ? sink?.audio : null
function onVolumeChanged() {
var vol = (sink?.audio.volume ?? 0)
if (isNaN(vol)) {
vol = 0
}
root._volume = vol
}
function onMutedChanged() {
root._muted = (sink?.audio.muted ?? true)
}
}
Connections {
target: source?.audio ? source?.audio : null
function onVolumeChanged() {
var vol = (source?.audio.volume ?? 0)
if (isNaN(vol)) {
vol = 0
}
root._inputVolume = vol
}
function onMutedChanged() {
root._inputMuted = (source?.audio.muted ?? true)
}
}
function increaseVolume() {
setVolume(volume + stepVolume)
}
function decreaseVolume() {
setVolume(volume - stepVolume)
}
function setVolume(newVolume: real) {
if (sink?.ready && sink?.audio) {
// Clamp it accordingly
sink.audio.muted = false
sink.audio.volume = Math.max(0, Math.min(1.0, newVolume))
} else {
console.warn("No sink available")
}
}
function setOutputMuted(muted: bool) {
if (sink?.ready && sink?.audio) {
sink.audio.muted = muted
} else {
console.warn("No sink available")
}
}
function setInputVolume(newVolume: real) {
if (source?.ready && source?.audio) {
// Clamp it accordingly
source.audio.muted = false
source.audio.volume = Math.max(0, Math.min(1.0, newVolume))
} else {
console.warn("No source available")
}
}
function setInputMuted(muted: bool) {
if (source?.ready && source?.audio) {
source.audio.muted = muted
} else {
console.warn("No source available")
}
}
function setAudioSink(newSink: PwNode): void {
Pipewire.preferredDefaultAudioSink = newSink
// Volume is changed by the sink change
root._volume = newSink?.audio?.volume ?? 0
root._muted = !!newSink?.audio?.muted
}
function setAudioSource(newSource: PwNode): void {
Pipewire.preferredDefaultAudioSource = newSource
// Volume is changed by the source change
root._inputVolume = newSource?.audio?.volume ?? 0
root._inputMuted = !!newSource?.audio?.muted
}
function toggleMute() {
setOutputMuted(!muted)
}
}

View File

@@ -0,0 +1,303 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property list<var> ddcMonitors: []
readonly property list<Monitor> monitors: variants.instances
property bool appleDisplayPresent: false
function getMonitorForScreen(screen: ShellScreen): var {
return monitors.find(m => m.modelData === screen)
}
// Signal emitted when a specific monitor's brightness changes, includes monitor context
signal monitorBrightnessChanged(var monitor, real newBrightness)
function getAvailableMethods(): list<string> {
var methods = []
if (monitors.some(m => m.isDdc))
methods.push("ddcutil")
if (monitors.some(m => !m.isDdc))
methods.push("internal")
if (appleDisplayPresent)
methods.push("apple")
return methods
}
// Global helpers for IPC and shortcuts
function increaseBrightness(): void {
monitors.forEach(m => m.increaseBrightness())
}
function decreaseBrightness(): void {
monitors.forEach(m => m.decreaseBrightness())
}
function getDetectedDisplays(): list<var> {
return detectedDisplays
}
reloadableId: "brightness"
onMonitorsChanged: {
ddcMonitors = []
ddcProc.running = true
}
Variants {
id: variants
model: Quickshell.screens
Monitor {}
}
// Check for Apple Display support
Process {
running: true
command: ["sh", "-c", "which asdbctl >/dev/null 2>&1 && asdbctl get || echo ''"]
stdout: StdioCollector {
onStreamFinished: root.appleDisplayPresent = text.trim().length > 0
}
}
// Detect DDC monitors
Process {
id: ddcProc
property list<var> ddcMonitors: []
command: ["ddcutil", "detect", "--sleep-multiplier=0.5"]
stdout: StdioCollector {
onStreamFinished: {
var displays = text.trim().split("\n\n")
ddcProc.ddcMonitors = displays.map(d => {
var ddcModelMatch = d.match(/(This monitor does not support DDC\/CI|Invalid display)/)
var modelMatch = d.match(/Model:\s*(.*)/)
var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)
var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false
var model = modelMatch ? modelMatch[1] : "Unknown"
var bus = busMatch ? busMatch[1] : "Unknown"
console.log("Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
return {
"model": model,
"busNum": bus,
"isDdc": !ddcModel
}
})
root.ddcMonitors = ddcProc.ddcMonitors.filter(m => m.isDdc)
}
}
}
component Monitor: QtObject {
id: monitor
required property ShellScreen modelData
readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model)
readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? ""
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal")
property real brightness
property real lastBrightness: 0
property real queuedBrightness: NaN
// For internal displays - store the backlight device path
property string backlightDevice: ""
property string brightnessPath: ""
property string maxBrightnessPath: ""
property int maxBrightness: 100
property bool ignoreNextChange: false
// Signal for brightness changes
signal brightnessUpdated(real newBrightness)
// Execute a system command to get the current brightness value directly
readonly property Process refreshProc: Process {
stdout: StdioCollector {
onStreamFinished: {
var dataText = text.trim()
if (dataText === "") {
return
}
var lines = dataText.split("\n")
if (lines.length >= 2) {
var current = parseInt(lines[0].trim())
var max = parseInt(lines[1].trim())
if (!isNaN(current) && !isNaN(max) && max > 0) {
var newBrightness = current / max
// Only update if it's actually different (avoid feedback loops)
if (Math.abs(newBrightness - monitor.brightness) > 0.01) {
// Update internal value to match system state
monitor.brightness = newBrightness
monitor.brightnessUpdated(monitor.brightness)
root.monitorBrightnessChanged(monitor, monitor.brightness)
}
}
}
}
}
}
// Function to actively refresh the brightness from system
function refreshBrightnessFromSystem() {
if (!monitor.isDdc && !monitor.isAppleDisplay) {
// For internal displays, query the system directly
refreshProc.command = ["sh", "-c", "cat " + monitor.brightnessPath + " && " + "cat " + monitor.maxBrightnessPath]
refreshProc.running = true
} else if (monitor.isDdc) {
// For DDC displays, get the current value
refreshProc.command = ["ddcutil", "-b", monitor.busNum, "getvcp", "10", "--brief"]
refreshProc.running = true
} else if (monitor.isAppleDisplay) {
// For Apple displays, get the current value
refreshProc.command = ["asdbctl", "get"]
refreshProc.running = true
}
}
// FileView to watch for external brightness changes (internal displays only)
readonly property FileView brightnessWatcher: FileView {
id: brightnessWatcher
// Only set path for internal displays with a valid brightness path
path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : ""
watchChanges: path !== ""
onFileChanged: {
// When a file change is detected, actively refresh from system
// to ensure we get the most up-to-date value
Qt.callLater(() => {
monitor.refreshBrightnessFromSystem()
})
}
}
// Initialize brightness
readonly property Process initProc: Process {
stdout: StdioCollector {
onStreamFinished: {
var dataText = text.trim()
if (dataText === "") {
return
}
if (monitor.isAppleDisplay) {
var val = parseInt(dataText)
if (!isNaN(val)) {
monitor.brightness = val / 101
console.log("Apple display brightness:", monitor.brightness)
}
} else if (monitor.isDdc) {
var parts = dataText.split(" ")
if (parts.length >= 4) {
var current = parseInt(parts[3])
var max = parseInt(parts[4])
if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.brightness = current / max
console.log("DDC brightness:", monitor.brightness)
}
}
} else {
// Internal backlight - parse the response which includes device path
var lines = dataText.split("\n")
if (lines.length >= 3) {
monitor.backlightDevice = lines[0]
monitor.brightnessPath = monitor.backlightDevice + "/brightness"
monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness"
var current = parseInt(lines[1])
var max = parseInt(lines[2])
if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.maxBrightness = max
monitor.brightness = current / max
console.log("Internal brightness:", monitor.brightness)
console.log("Using backlight device:", monitor.backlightDevice)
}
}
}
// Always update
monitor.brightnessUpdated(monitor.brightness)
root.monitorBrightnessChanged(monitor, monitor.brightness)
}
}
}
readonly property real stepSize: 5.0 / 100.0
// Timer for debouncing rapid changes
readonly property Timer timer: Timer {
interval: 100
onTriggered: {
if (!isNaN(monitor.queuedBrightness)) {
monitor.setBrightness(monitor.queuedBrightness)
monitor.queuedBrightness = NaN
}
}
}
function setBrightnessDebounced(value: real): void {
monitor.queuedBrightness = value
timer.start()
}
function increaseBrightness(): void {
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness
setBrightnessDebounced(value + stepSize)
}
function decreaseBrightness(): void {
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness
setBrightnessDebounced(value - stepSize)
}
function setBrightness(value: real): void {
value = Math.max(0, Math.min(1, value))
var rounded = Math.round(value * 100)
if (timer.running) {
monitor.queuedBrightness = value
return
}
// Update internal value and trigger UI feedback
monitor.brightness = value
monitor.brightnessUpdated(value)
root.monitorBrightnessChanged(monitor, monitor.brightness)
if (isAppleDisplay) {
monitor.ignoreNextChange = true
Quickshell.execDetached(["asdbctl", "set", rounded])
} else if (isDdc) {
monitor.ignoreNextChange = true
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded])
} else {
monitor.ignoreNextChange = true
Quickshell.execDetached(["brightnessctl", "s", rounded + "%"])
}
if (isDdc) {
timer.restart()
}
}
function initBrightness(): void {
if (isAppleDisplay) {
initProc.command = ["asdbctl", "get"]
} else if (isDdc) {
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]
} else {
// Internal backlight - find the first available backlight device and get its info
// This now returns: device_path, current_brightness, max_brightness (on separate lines)
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do " + " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; " + " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"]
}
initProc.running = true
}
onBusNumChanged: initBrightness()
Component.onCompleted: initBrightness()
}
}

View File

@@ -0,0 +1,146 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
pragma Singleton
Singleton {
// "systemd", "wayland", or "auto"
// systemd-inhibit not found, try Wayland tools
// wayhibitor not found
id: root
property string reason: "Application request"
property bool isInhibited: false
property var activeInhibitors: []
// Different inhibitor strategies
property string strategy: "systemd"
// Auto-detect the best strategy
function detectStrategy() {
if (strategy === "auto") {
// Check if systemd-inhibit is available
try {
var systemdResult = Quickshell.execDetached(["which", "systemd-inhibit"]);
strategy = "systemd";
return ;
} catch (e) {
}
try {
var waylandResult = Quickshell.execDetached(["which", "wayhibitor"]);
strategy = "wayland";
return ;
} catch (e) {
}
strategy = "systemd"; // Fallback to systemd even if not detected
}
}
// Add an inhibitor
function addInhibitor(id, reason = "Application request") {
if (activeInhibitors.includes(id))
return false;
activeInhibitors.push(id);
updateInhibition(reason);
return true;
}
// Remove an inhibitor
function removeInhibitor(id) {
const index = activeInhibitors.indexOf(id);
if (index === -1) {
console.log("Inhibitor not found:", id);
return false;
}
activeInhibitors.splice(index, 1);
updateInhibition();
return true;
}
// Update the actual system inhibition
function updateInhibition(newReason = reason) {
const shouldInhibit = activeInhibitors.length > 0;
if (shouldInhibit === isInhibited)
return ;
// No change needed
if (shouldInhibit)
startInhibition(newReason);
else
stopInhibition();
}
// Start system inhibition
function startInhibition(newReason) {
reason = newReason;
if (strategy === "systemd")
startSystemdInhibition();
else if (strategy === "wayland")
startWaylandInhibition();
else
return ;
isInhibited = true;
}
// Stop system inhibition
function stopInhibition() {
if (!isInhibited)
return ;
if (inhibitorProcess.running)
inhibitorProcess.signal(15);
// SIGTERM
isInhibited = false;
}
// Systemd inhibition using systemd-inhibit
function startSystemdInhibition() {
inhibitorProcess.command = ["systemd-inhibit", "--what=idle", "--why=" + reason, "--mode=block", "sleep", "infinity"];
inhibitorProcess.running = true;
}
// Wayland inhibition using wayhibitor or similar
function startWaylandInhibition() {
inhibitorProcess.command = ["wayhibitor"];
inhibitorProcess.running = true;
}
// Manual toggle for user control
function manualToggle() {
if (activeInhibitors.includes("manual")) {
removeInhibitor("manual");
return false;
} else {
addInhibitor("manual", "Manually activated by user");
return true;
}
}
Component.onCompleted: {
detectStrategy();
}
// Clean up on shutdown
Component.onDestruction: {
stopInhibition();
}
// Process for maintaining the inhibition
Process {
id: inhibitorProcess
running: false
onExited: function(exitCode, exitStatus) {
if (isInhibited)
isInhibited = false;
console.log("Inhibitor process exited with code:", exitCode, "status:", exitStatus);
}
onStarted: function() {
console.log("Inhibitor process started with strategy:", root.strategy);
}
}
}

View File

@@ -0,0 +1,131 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
Singleton {
property string ip: "N/A"
property string countryCode: "N/A"
property real fetchInterval: 30 // in s
property real fetchTimeout: 10 // in s
property string ipURL: "https://api.uyanide.com/ip"
property string geoURL: "curl https://api.ipinfo.io/lite/"
property string geoURLToken: ""
function fetchIP() {
if (fetchIPProcess.running) {
console.warn("Fetch IP process is still running, skipping fetchIP");
return ;
}
fetchIPProcess.running = true;
}
function fetchGeoInfo() {
if (fetchGeoProcess.running) {
console.warn("Fetch geo process is still running, skipping fetchGeoInfo");
return ;
}
if (!ip || ip === "N/A") {
countryCode = "N/A";
return ;
}
fetchGeoProcess.command = ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${geoURL}${ip}${geoURLToken ? "?token=" + geoURLToken : ""}`];
fetchGeoProcess.running = true;
}
function refresh() {
fetchTimer.stop();
ip = "N/A";
fetchIP();
fetchTimer.start();
}
Component.onCompleted: {
}
FileView {
id: tokenFile
path: Qt.resolvedUrl("../Assets/Ip/token.txt")
onLoaded: {
geoURLToken = tokenFile.text();
if (!geoURLToken)
console.warn("No token found for geoIP service, assuming none is required");
fetchIP();
fetchTimer.start();
}
}
Timer {
id: fetchTimer
interval: fetchInterval * 1000
repeat: true
running: false
onTriggered: {
fetchIP();
}
}
Process {
id: fetchIPProcess
command: ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${ipURL}`]
running: false
stdout: SplitParser {
splitMarker: ""
onRead: (data) => {
let newIP = "";
try {
const response = JSON.parse(data);
if (response && response.ip) {
newIP = response.ip;
console.log("Fetched IP: " + newIP);
}
} catch (e) {
console.error("Failed to parse IP response: " + e);
}
if (newIP && newIP !== ip) {
ip = newIP;
fetchGeoInfo();
} else if (!newIP) {
ip = "N/A";
countryCode = "N/A";
}
}
}
}
Process {
id: fetchGeoProcess
command: []
running: false
stdout: SplitParser {
splitMarker: ""
onRead: (data) => {
let newCountryCode = "";
try {
const response = JSON.parse(data);
if (response && response.country) {
newCountryCode = response.country_code;
console.log("Fetched country code: " + newCountryCode);
SendNotification.show("New IP", `IP: ${ip}\nCountry: ${newCountryCode}`);
}
} catch (e) {
console.error("Failed to parse geo response: " + e);
}
if (newCountryCode && newCountryCode !== countryCode)
countryCode = newCountryCode;
else if (!newCountryCode)
countryCode = "N/A";
}
}
}
}

View File

@@ -0,0 +1,164 @@
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
import qs.Modules.Misc
pragma Singleton
Singleton {
id: manager
// Properties
property var currentPlayer: null
property real currentPosition: 0
property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : ""
property real trackLength: currentPlayer ? currentPlayer.length : 0
property bool canPlay: currentPlayer ? currentPlayer.canPlay : false
property bool canPause: currentPlayer ? currentPlayer.canPause : false
property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false
property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false
property bool canSeek: currentPlayer ? currentPlayer.canSeek : false
property bool hasPlayer: getAvailablePlayers().length > 0
// Expose cava values
property alias cavaValues: cava.values
// Returns available MPRIS players
function getAvailablePlayers() {
if (!Mpris.players || !Mpris.players.values)
return [];
let allPlayers = Mpris.players.values;
let controllablePlayers = [];
for (let i = 0; i < allPlayers.length; i++) {
let player = allPlayers[i];
if (player && player.canControl)
controllablePlayers.push(player);
}
return controllablePlayers;
}
// Returns active player or first available
function findActivePlayer() {
let availablePlayers = getAvailablePlayers();
if (availablePlayers.length === 0)
return null;
// Get the first playing player
for (let i = availablePlayers.length - 1; i >= 0; i--) {
if (availablePlayers[i].isPlaying)
return availablePlayers[i];
}
// Fallback to last player
return availablePlayers[availablePlayers.length - 1];
}
// Updates currentPlayer and currentPosition
function updateCurrentPlayer() {
let newPlayer = findActivePlayer();
if (newPlayer !== currentPlayer) {
currentPlayer = newPlayer;
currentPosition = currentPlayer ? currentPlayer.position : 0;
}
}
// Player control functions
function playPause() {
if (currentPlayer) {
if (currentPlayer.isPlaying)
currentPlayer.pause();
else
currentPlayer.play();
}
}
function isAllPaused() {
let availablePlayers = getAvailablePlayers();
for (let i = 0; i < availablePlayers.length; i++) {
if (availablePlayers[i].isPlaying)
return false;
}
return true;
}
function play() {
if (currentPlayer && currentPlayer.canPlay)
currentPlayer.play();
}
function pause() {
if (currentPlayer && currentPlayer.canPause)
currentPlayer.pause();
}
function next() {
if (currentPlayer && currentPlayer.canGoNext)
currentPlayer.next();
}
function previous() {
if (currentPlayer && currentPlayer.canGoPrevious)
currentPlayer.previous();
}
function seek(position) {
if (currentPlayer && currentPlayer.canSeek) {
currentPlayer.position = position;
currentPosition = position;
}
}
function seekByRatio(ratio) {
if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) {
let seekPosition = ratio * currentPlayer.length;
currentPlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
// Initialize
Item {
Component.onCompleted: {
updateCurrentPlayer();
}
}
// Updates progress bar every second
Timer {
id: positionTimer
interval: 1000
running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0
repeat: true
onTriggered: {
if (currentPlayer && currentPlayer.isPlaying)
currentPosition = currentPlayer.position;
}
}
// Reacts to player list changes
Connections {
function onValuesChanged() {
updateCurrentPlayer();
}
target: Mpris.players
}
Cava {
id: cava
count: 44
}
}

View File

@@ -0,0 +1,184 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property var workspaces: []
property var windows: {}
property int focusedWindowId: -1
property bool noFocus: focusedWindowId === -1
property bool inOverview: false
property string focusedWindowTitle: ""
// property string focusedWindowAppId: ""
function updateFocusedWindowTitle() {
if (windows && windows[focusedWindowId]) {
focusedWindowTitle = windows[focusedWindowId].title || "";
// focusedWindowAppId = windows[focusedWindowId].appId || "";
} else {
focusedWindowTitle = "";
// focusedWindowAppId = "";
}
}
function getFocusedWindow() {
return (windows && windows[focusedWindowId]) || null;
}
onWindowsChanged: updateFocusedWindowTitle()
onFocusedWindowIdChanged: updateFocusedWindowTitle()
Component.onCompleted: {
eventStream.running = true;
}
Process {
id: workspaceProcess
running: false
command: ["niri", "msg", "--json", "workspaces"]
stdout: SplitParser {
onRead: function(line) {
try {
const workspacesData = JSON.parse(line);
const workspacesList = [];
for (const ws of workspacesData) {
workspacesList.push({
"id": ws.id,
"idx": ws.idx,
"name": ws.name || "",
"output": ws.output || "",
"isFocused": ws.is_focused === true,
"isActive": ws.is_active === true,
"isUrgent": ws.is_urgent === true,
"activeWindowId": ws.active_window_id
});
}
workspacesList.sort((a, b) => {
if (a.output !== b.output)
return a.output.localeCompare(b.output);
return a.id - b.id;
});
root.workspaces = workspacesList;
} catch (e) {
console.error("Failed to parse workspaces:", e, line);
}
}
}
}
Process {
id: eventStream
running: false
command: ["niri", "msg", "--json", "event-stream"]
stdout: SplitParser {
onRead: (data) => {
try {
const event = JSON.parse(data.trim());
if (event.WorkspacesChanged) {
workspaceProcess.running = true;
} else if (event.WindowsChanged) {
try {
const windowsData = event.WindowsChanged.windows;
const windowsMap = {};
for (const win of windowsData) {
if (win.is_focused === true) {
root.focusedWindowId = win.id;
}
windowsMap[win.id] = {
"title": win.title || "",
"appId": win.app_id || "",
"workspaceId": win.workspace_id || null,
"isFocused": win.is_focused === true
};
}
root.windows = windowsMap;
} catch (e) {
console.error("Error parsing windows event:", e);
}
} else if (event.WorkspaceActivated) {
workspaceProcess.running = true;
} else if (event.WindowFocusChanged) {
try {
const focusedId = event.WindowFocusChanged.id;
if (focusedId) {
if (root.windows[focusedId]) {
root.focusedWindowId = focusedId;
} else {
root.focusedWindowId = -1;
}
} else {
root.focusedWindowId = -1;
}
} catch (e) {
console.error("Error parsing window focus event:", e);
}
} else if (event.OverviewOpenedOrClosed) {
try {
root.inOverview = event.OverviewOpenedOrClosed.is_open === true;
} catch (e) {
console.error("Error parsing overview state:", e);
}
} else if (event.WindowOpenedOrChanged) {
try {
const targetWin = event.WindowOpenedOrChanged.window;
const id = targetWin.id;
const isFocused = targetWin.is_focused === true;
let needUpdateTitle = false;
if (id) {
if (root.windows && root.windows[id]) {
const win = root.windows[id];
// Update existing window
needUpdateTitle = win.title !== targetWin.title;
win.title = targetWin.title || win.title;
win.appId = targetWin.app_id || win.appId;
win.workspaceId = targetWin.workspace_id || win.workspaceId;
win.isFocused = isFocused;
} else {
// New window
const newWin = {
"title": targetWin.title || "",
"appId": targetWin.app_id || "",
"workspaceId": targetWin.workspace_id || null,
"isFocused": isFocused
};
root.windows[id] = targetWin;
}
if (isFocused) {
root.focusedWindowId = id;
if (needUpdateTitle)
root.updateFocusedWindowTitle();
}
}
} catch (e) {
console.error("Error parsing window opened/changed event:", e);
}
} else if (event.windowClosed) {
try {
const closedId = event.windowClosed.id;
if (closedId && (root.windows && root.windows[closedId])) {
delete root.windows[closedId];
}
} catch (e) {
console.error("Error parsing window closed event:", e);
}
}
} catch (e) {
console.error("Error parsing event stream:", e, data);
}
}
}
}
}

View File

@@ -0,0 +1,67 @@
pragma Singleton
import Quickshell
Singleton {
id: root
// A ref. to the lockScreen, so it's accessible from anywhere
// This is not a panel...
property var lockScreen: null
// Panels
property var registeredPanels: ({})
property var openedPanel: null
signal willOpen
// Currently opened popups, can have more than one.
// ex: when opening an NIconPicker from a widget setting.
property var openedPopups: []
property bool hasOpenedPopup: false
signal popupChanged
// Register this panel
function registerPanel(panel) {
registeredPanels[panel.objectName] = panel
}
// Returns a panel
function getPanel(name) {
return registeredPanels[name] || null
}
// Check if a panel exists
function hasPanel(name) {
return name in registeredPanels
}
// Helper to keep only one panel open at any time
function willOpenPanel(panel) {
if (openedPanel && openedPanel !== panel) {
openedPanel.close()
}
openedPanel = panel
// emit signal
willOpen()
}
function closedPanel(panel) {
if (openedPanel && openedPanel === panel) {
openedPanel = null
}
}
// Popups
function willOpenPopup(popup) {
openedPopups.push(popup)
hasOpenedPopup = (openedPopups.length !== 0)
popupChanged()
}
function willClosePopup(popup) {
openedPopups = openedPopups.filter(p => p !== popup)
hasOpenedPopup = (openedPopups.length !== 0)
popupChanged()
}
}

View File

@@ -0,0 +1,23 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
Singleton {
id: root
function show(title, message, icon = "", urgency = "normal", timeout = 5000) {
if (icon)
action.command = ["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message];
else
action.command = ["notify-send", "-u", urgency, "-t", timeout.toString(), title, message];
action.startDetached();
}
Process {
id: action
running: false
}
}

View File

@@ -0,0 +1,392 @@
import Qt.labs.folderlistmodel
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
Singleton {
// For Intel coretemp, start averaging all available sensors/cores
id: root
// Public values
property real cpuUsage: 0
property real cpuTemp: 0
property real memGb: 0
property real memPercent: 0
property real diskPercent: 0
property real rxSpeed: 0
property real txSpeed: 0
// Configuration
property int sleepDuration: 3000
property int fasterSleepDuration: 1000
// Internal state for CPU calculation
property var prevCpuStats: null
// Internal state for network speed calculation
// Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered
// since the computer started, so their value will easily overlfow a 32bit int.
property real prevRxBytes: 0
property real prevTxBytes: 0
property real prevTime: 0
// Cpu temperature is the most complex
readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
property string cpuTempSensorName: ""
property string cpuTempHwmonPath: ""
// For Intel coretemp averaging of all cores/sensors
property var intelTempValues: []
property int intelTempFilesChecked: 0
property int intelTempMaxFiles: 20 // Will test up to temp20_input
// -------------------------------------------------------
// -------------------------------------------------------
// Parse memory info from /proc/meminfo
function parseMemoryInfo(text) {
if (!text)
return ;
const lines = text.split('\n');
let memTotal = 0;
let memAvailable = 0;
for (const line of lines) {
if (line.startsWith('MemTotal:'))
memTotal = parseInt(line.split(/\s+/)[1]) || 0;
else if (line.startsWith('MemAvailable:'))
memAvailable = parseInt(line.split(/\s+/)[1]) || 0;
}
if (memTotal > 0) {
const usageKb = memTotal - memAvailable;
root.memGb = (usageKb / 1e+06).toFixed(1);
root.memPercent = Math.round((usageKb / memTotal) * 100);
}
}
// -------------------------------------------------------
// Calculate CPU usage from /proc/stat
function calculateCpuUsage(text) {
if (!text)
return ;
const lines = text.split('\n');
const cpuLine = lines[0];
// First line is total CPU
if (!cpuLine.startsWith('cpu '))
return ;
const parts = cpuLine.split(/\s+/);
const stats = {
"user": parseInt(parts[1]) || 0,
"nice": parseInt(parts[2]) || 0,
"system": parseInt(parts[3]) || 0,
"idle": parseInt(parts[4]) || 0,
"iowait": parseInt(parts[5]) || 0,
"irq": parseInt(parts[6]) || 0,
"softirq": parseInt(parts[7]) || 0,
"steal": parseInt(parts[8]) || 0,
"guest": parseInt(parts[9]) || 0,
"guestNice": parseInt(parts[10]) || 0
};
const totalIdle = stats.idle + stats.iowait;
const total = Object.values(stats).reduce((sum, val) => {
return sum + val;
}, 0);
if (root.prevCpuStats) {
const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait;
const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => {
return sum + val;
}, 0);
const diffTotal = total - prevTotal;
const diffIdle = totalIdle - prevTotalIdle;
if (diffTotal > 0)
root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1);
}
root.prevCpuStats = stats;
}
// -------------------------------------------------------
// Calculate RX and TX speed from /proc/net/dev
// Average speed of all interfaces excepted 'lo'
function calculateNetworkSpeed(text) {
if (!text)
return ;
const currentTime = Date.now() / 1000;
const lines = text.split('\n');
let totalRx = 0;
let totalTx = 0;
for (var i = 2; i < lines.length; i++) {
const line = lines[i].trim();
if (!line)
continue;
const colonIndex = line.indexOf(':');
if (colonIndex === -1)
continue;
const iface = line.substring(0, colonIndex).trim();
if (iface === 'lo')
continue;
const statsLine = line.substring(colonIndex + 1).trim();
const stats = statsLine.split(/\s+/);
const rxBytes = parseInt(stats[0], 10) || 0;
const txBytes = parseInt(stats[8], 10) || 0;
totalRx += rxBytes;
totalTx += txBytes;
}
// Compute only if we have a previous run to compare to.
if (root.prevTime > 0) {
const timeDiff = currentTime - root.prevTime;
// Avoid division by zero if time hasn't passed.
if (timeDiff > 0) {
let rxDiff = totalRx - root.prevRxBytes;
let txDiff = totalTx - root.prevTxBytes;
// Handle counter resets (e.g., WiFi reconnect), which would cause a negative value.
if (rxDiff < 0)
rxDiff = 0;
if (txDiff < 0)
txDiff = 0;
root.rxSpeed = Math.round(rxDiff / timeDiff); // Speed in Bytes/s
root.txSpeed = Math.round(txDiff / timeDiff);
}
}
root.prevRxBytes = totalRx;
root.prevTxBytes = totalTx;
root.prevTime = currentTime;
}
// -------------------------------------------------------
// Helper function to format network speeds
function formatSpeed(bytesPerSecond) {
if (bytesPerSecond < 1024 * 1024) {
const kb = bytesPerSecond / 1024;
if (kb < 10)
return kb.toFixed(1) + "KB";
else
return Math.round(kb) + "KB";
} else if (bytesPerSecond < 1024 * 1024 * 1024) {
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB";
} else {
return (bytesPerSecond / (1024 * 1024 * 1024)).toFixed(1) + "GB";
}
}
// -------------------------------------------------------
// Compact speed formatter for vertical bar display
function formatCompactSpeed(bytesPerSecond) {
if (!bytesPerSecond || bytesPerSecond <= 0)
return "0";
const units = ["", "K", "M", "G"];
let value = bytesPerSecond;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value = value / 1024;
unitIndex++;
}
// Promote at ~100 of current unit (e.g., 100k -> ~0.1M shown as 0.1M or 0M if rounded)
if (unitIndex < units.length - 1 && value >= 100) {
value = value / 1024;
unitIndex++;
}
const display = Math.round(value).toString();
return display + units[unitIndex];
}
// -------------------------------------------------------
// Function to start fetching and computing the cpu temperature
function updateCpuTemperature() {
// For AMD sensors (k10temp and zenpower), only use Tctl sensor
// temp1_input corresponds to Tctl (Temperature Control) on these sensors
if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") {
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`;
cpuTempReader.reload();
} else if (root.cpuTempSensorName === "coretemp") {
root.intelTempValues = [];
root.intelTempFilesChecked = 0;
checkNextIntelTemp();
}
}
// -------------------------------------------------------
// Function to check next Intel temperature sensor
function checkNextIntelTemp() {
if (root.intelTempFilesChecked >= root.intelTempMaxFiles) {
// Calculate average of all found temperatures
if (root.intelTempValues.length > 0) {
let sum = 0;
for (var i = 0; i < root.intelTempValues.length; i++) {
sum += root.intelTempValues[i];
}
root.cpuTemp = Math.round(sum / root.intelTempValues.length);
} else {
console.warn("No temperature sensors found for coretemp");
root.cpuTemp = 0;
}
return ;
}
// Check next temperature file
root.intelTempFilesChecked++;
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`;
cpuTempReader.reload();
}
// --------------------------------------------
Component.onCompleted: {
// Kickoff the cpu name detection for temperature
cpuTempNameReader.checkNext();
}
// --------------------------------------------
// Timer for periodic updates
Timer {
id: updateTimer
interval: root.sleepDuration
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
// Trigger all direct system files reads
memInfoFile.reload();
cpuStatFile.reload();
// Run df (disk free) one time
dfProcess.running = true;
updateCpuTemperature();
}
}
Timer {
id: fasterUpdateTimer
interval: root.fasterSleepDuration
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
netDevFile.reload();
}
}
// --------------------------------------------
// FileView components for reading system files
FileView {
id: memInfoFile
path: "/proc/meminfo"
onLoaded: parseMemoryInfo(text())
}
FileView {
id: cpuStatFile
path: "/proc/stat"
onLoaded: calculateCpuUsage(text())
}
FileView {
id: netDevFile
path: "/proc/net/dev"
onLoaded: calculateNetworkSpeed(text())
}
// --------------------------------------------
// Process to fetch disk usage in percent
// Uses 'df' aka 'disk free'
Process {
id: dfProcess
command: ["df", "--output=pcent", "/"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split('\n');
if (lines.length >= 2) {
const percent = lines[1].replace(/[^0-9]/g, '');
root.diskPercent = parseInt(percent) || 0;
}
}
}
}
// --------------------------------------------
// --------------------------------------------
// CPU Temperature
// It's more complex.
// ----
// #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower"
FileView {
id: cpuTempNameReader
property int currentIndex: 0
function checkNext() {
if (currentIndex >= 16) {
// Check up to hwmon10
console.warn("No supported temperature sensor found");
return ;
}
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
cpuTempNameReader.reload();
}
printErrors: false
onLoaded: {
const name = text().trim();
if (root.supportedTempCpuSensorNames.includes(name)) {
root.cpuTempSensorName = name;
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`;
console.log(`Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`);
} else {
currentIndex++;
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNext();
});
}
}
onLoadFailed: function(error) {
currentIndex++;
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNext();
});
}
}
// ----
// #2 - Read sensor value
FileView {
id: cpuTempReader
printErrors: false
onLoaded: {
const data = text().trim();
if (root.cpuTempSensorName === "coretemp") {
// For Intel, collect all temperature values
const temp = parseInt(data) / 1000;
//console.log(temp, cpuTempReader.path)
root.intelTempValues.push(temp);
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNextIntelTemp();
});
} else {
// For AMD sensors (k10temp and zenpower), directly set the temperature
root.cpuTemp = Math.round(parseInt(data) / 1000);
}
}
onLoadFailed: function(error) {
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNextIntelTemp();
});
}
}
}

View File

@@ -0,0 +1,46 @@
import QtQuick
import Quickshell
pragma Singleton
Singleton {
// ignore and fall back
id: root
function iconFromName(iconName, fallbackName) {
const fallback = fallbackName || "application-x-executable";
try {
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
const p = Quickshell.iconPath(iconName, fallback);
if (p && p !== "")
return p;
}
} catch (e) {
}
try {
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
} catch (e2) {
return "";
}
}
// Resolve icon path for a DesktopEntries appId - safe on missing entries
function iconForAppId(appId, fallbackName) {
const fallback = fallbackName || "application-x-executable";
if (!appId)
return iconFromName(fallback, fallback);
try {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return iconFromName(fallback, fallback);
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId);
const name = entry && entry.icon ? entry.icon : "";
return iconFromName(name || fallback, fallback);
} catch (e) {
return iconFromName(fallback, fallback);
}
}
}

View File

@@ -0,0 +1,44 @@
import QtQuick
import Quickshell
pragma Singleton
Singleton {
id: root
property var date: new Date()
property string time: Qt.formatDateTime(date, "HH:mm")
property string dateString: {
let now = date;
let dayName = now.toLocaleDateString(Qt.locale(), "ddd");
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1);
let day = now.getDate();
let suffix;
if (day > 3 && day < 21)
suffix = 'th';
else
switch (day % 10) {
case 1:
suffix = "st";
break;
case 2:
suffix = "nd";
break;
case 3:
suffix = "rd";
break;
default:
suffix = "th";
};
let month = now.toLocaleDateString(Qt.locale(), "MMMM");
let year = now.toLocaleDateString(Qt.locale(), "yyyy");
return `${dayName}, ` + `${day}${suffix} ${month} ${year}`;
}
Timer {
interval: 1000
repeat: true
running: true
onTriggered: root.date = new Date()
}
}

View File

@@ -0,0 +1,55 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
Singleton {
id: root
property ListModel workspaces
workspaces: ListModel {
}
function initNiri() {
updateNiriWorkspaces();
}
function updateNiriWorkspaces() {
const niriWorkspaces = Niri.workspaces || [];
workspaces.clear();
for (let i = 0; i < niriWorkspaces.length; i++) {
const ws = niriWorkspaces[i];
workspaces.append({
"id": ws.id,
"idx": ws.idx || 1,
"name": ws.name || "",
"output": ws.output || "",
"isFocused": ws.isFocused === true,
"isActive": ws.isActive === true,
"isUrgent": ws.isUrgent === true
});
}
workspacesChanged();
}
function switchToWorkspace(workspaceId) {
try {
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]);
} catch (e) {
console.error("Error switching Niri workspace:", e);
}
}
Connections {
function onWorkspacesChanged() {
updateNiriWorkspaces();
}
target: Niri
}
}

View File

@@ -0,0 +1,20 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
Singleton {
id: root
function write(text) {
action.command = ["sh", "-c", `echo ${text} | wl-copy -n`];
action.startDetached();
}
Process {
id: action
running: false
}
}

23
quickshell/shell.qml Normal file
View File

@@ -0,0 +1,23 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.Constants
import qs.Modules.Bar
import qs.Modules.Misc
Scope {
id: root
Bar {
id: bar
shell: root
}
Corners {
id: corners
shell: root
}
}

View File

View File

@@ -136,8 +136,8 @@
"format-alt-click": "click-right", "format-alt-click": "click-right",
//"format-icons": ["", ""], //"format-icons": ["", ""],
"format-icons": [""], "format-icons": [""],
"on-scroll-down": "brightnessctl -d $HYPR_DISPLAY_DEVICE set 5%-", "on-scroll-down": "brightnessctl --class=backlight set -5%",
"on-scroll-up": "brightnessctl -d $HYPR_DISPLAY_DEVICE set +5%", "on-scroll-up": "brightnessctl --class=backlight set +5%",
"max-length": 6, "max-length": 6,
"min-length": 6 "min-length": 6
}, },