quickshell and hyprland additions

This commit is contained in:
2026-03-15 13:56:00 +02:00
parent c9c27d1554
commit 1ad06b82a6
509 changed files with 68371 additions and 19 deletions

View File

@@ -0,0 +1,163 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.config
import qs.services
import qs.modules.components
Scope {
id: root
GothCorners {
opacity: ConfigResolver.bar(bar.displayName).gothCorners && !ConfigResolver.bar(bar.displayName).floating && ConfigResolver.bar(bar.displayName).enabled && !ConfigResolver.bar(bar.displayName).merged ? 1 : 0
}
Variants {
model: Quickshell.screens
PanelWindow {
// some exclusiveSpacing so it won't look like its sticking into the window when floating
id: bar
required property var modelData
property string displayName: modelData.name
property int rd: ConfigResolver.bar(displayName).radius * Config.runtime.appearance.rounding.factor // So it won't be modified when factor is 0
property int margin: ConfigResolver.bar(displayName).margins
property bool floating: ConfigResolver.bar(displayName).floating
property bool merged: ConfigResolver.bar(displayName).merged
property string pos: ConfigResolver.bar(displayName).position
property bool vertical: pos === "left" || pos === "right"
// Simple position properties
property bool attachedTop: pos === "top"
property bool attachedBottom: pos === "bottom"
property bool attachedLeft: pos === "left"
property bool attachedRight: pos === "right"
screen: modelData // Show bar on all screens
visible: ConfigResolver.bar(displayName).enabled && Config.initialized
WlrLayershell.namespace: "nucleus:bar"
exclusiveZone: ConfigResolver.bar(displayName).floating ? ConfigResolver.bar(displayName).density + Metrics.margin("tiny") : ConfigResolver.bar(displayName).density
implicitHeight: ConfigResolver.bar(displayName).density // density === height. (horizontal orientation)
implicitWidth: ConfigResolver.bar(displayName).density // density === width. (vertical orientation)
color: "transparent" // Keep panel window's color transparent, so that it can be modified by background rect
// This is probably a little weird way to set anchors but I think it's the best way. (and it works)
anchors {
top: ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right"
bottom: ConfigResolver.bar(displayName).position === "bottom" || ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right"
left: ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom"
right: ConfigResolver.bar(displayName).position === "right" || ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom"
}
margins {
top: {
if (floating)
return margin;
if (merged && vertical)
return margin;
return 0;
}
bottom: {
if (floating)
return margin;
if (merged && vertical)
return margin;
return 0;
}
left: {
if (floating)
return margin;
if (merged && !vertical)
return margin;
return 0;
}
right: {
if (floating)
return margin;
if (merged && !vertical)
return margin;
return 0;
}
}
StyledRect {
id: background
color: Appearance.m3colors.m3background
anchors.fill: parent
topLeftRadius: {
if (floating)
return rd;
if (!merged)
return 0;
return attachedBottom || attachedRight ? rd : 0;
}
topRightRadius: {
if (floating)
return rd;
if (!merged)
return 0;
return attachedBottom || attachedLeft ? rd : 0;
}
bottomLeftRadius: {
if (floating)
return rd;
if (!merged)
return 0;
return attachedTop || attachedRight ? rd : 0;
}
bottomRightRadius: {
if (floating)
return rd;
if (!merged)
return 0;
return attachedTop || attachedLeft ? rd : 0;
}
BarContent {
anchors.fill: parent
}
Behavior on bottomLeftRadius {
enabled: Config.runtime.appearance.animations.enabled
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on topLeftRadius {
enabled: Config.runtime.appearance.animations.enabled
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on bottomRightRadius {
enabled: Config.runtime.appearance.animations.enabled
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on topRightRadius {
enabled: Config.runtime.appearance.animations.enabled
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
}
}

View File

@@ -0,0 +1,178 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "content/"
import qs.config
import qs.services
import qs.modules.components
Item {
property string displayName: screen?.name ?? ""
property bool isHorizontal: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
Row {
id: hCenterRow
visible: isHorizontal
anchors.centerIn: parent
spacing: Metrics.spacing(4)
SystemUsageModule {}
MediaPlayerModule {}
ActiveWindowModule {}
ClockModule {}
BatteryIndicatorModule {}
}
RowLayout {
id: hLeftRow
visible: isHorizontal
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Metrics.spacing(4)
anchors.leftMargin: ConfigResolver.bar(displayName).density * 0.3
ToggleModule {
icon: "menu"
iconSize: Metrics.iconSize(22)
iconColor: Appearance.m3colors.m3error
toggle: Globals.visiblility.sidebarLeft
onToggled: function(value) {
Globals.visiblility.sidebarLeft = value
}
}
WorkspaceModule {}
}
RowLayout {
id: hRightRow
visible: isHorizontal
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Metrics.spacing(4)
anchors.rightMargin: ConfigResolver.bar(displayName).density * 0.3
SystemTray {
id: sysTray
}
StyledText {
id: seperator
visible: (sysTray.items.count > 0) && ConfigResolver.bar(displayName).modules.statusIcons.enabled
Layout.alignment: Qt.AlignLeft
font.pixelSize: Metrics.fontSize("hugeass")
text: "·"
}
StatusIconsModule {}
StyledText {
id: seperator2
Layout.alignment: Qt.AlignLeft
font.pixelSize: Metrics.fontSize("hugeass")
text: "·"
}
ToggleModule {
icon: "power_settings_new"
iconSize: Metrics.iconSize(22)
iconColor: Appearance.m3colors.m3error
toggle: Globals.visiblility.powermenu
onToggled: function(value) {
Globals.visiblility.powermenu = value
}
}
}
// Vertical Layout
Item {
visible: !isHorizontal
anchors.top: parent.top
anchors.topMargin: ConfigResolver.bar(displayName).density * 0.1
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: vRow.implicitHeight
implicitHeight: vRow.implicitWidth
Row {
id: vRow
anchors.centerIn: parent
spacing: Metrics.spacing(8)
rotation: 90
ToggleModule {
icon: "menu"
iconSize: Metrics.iconSize(22)
iconColor: Appearance.m3colors.m3error
toggle: Globals.visiblility.sidebarLeft
rotation: 270
onToggled: function(value) {
Globals.visiblility.sidebarLeft = value
}
}
SystemUsageModule {}
MediaPlayerModule {}
SystemTray {
rotation: 0
}
}
}
Item {
visible: !isHorizontal
anchors.centerIn: parent
anchors.verticalCenterOffset: 35
implicitWidth: centerRow.implicitHeight
implicitHeight: centerRow.implicitWidth
Row {
id: centerRow
anchors.centerIn: parent
WorkspaceModule {
rotation: 90
}
}
}
Item {
visible: !isHorizontal
anchors.bottom: parent.bottom
anchors.bottomMargin: ConfigResolver.bar(displayName).density * 0.1
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: row.implicitHeight
implicitHeight: row.implicitWidth
Row {
id: row
anchors.centerIn: parent
spacing: Metrics.spacing(6)
rotation: 90
ClockModule {
rotation: 270
}
StatusIconsModule {}
BatteryIndicatorModule {}
ToggleModule {
icon: "power_settings_new"
iconSize: Metrics.iconSize(22)
iconColor: Appearance.m3colors.m3error
toggle: Globals.visiblility.powermenu
rotation: 270
onToggled: function(value) {
Globals.visiblility.powermenu = value
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Wayland
import qs.config
import qs.modules.components
PanelWindow {
id: root
property int opacity: 0
color: "transparent"
visible: Config.initialized
WlrLayershell.layer: WlrLayer.Top
anchors {
top: true
left: true
bottom: true
right: true
}
Item {
id: container
anchors.fill: parent
StyledRect {
anchors.fill: parent
color: Appearance.m3colors.m3background
layer.enabled: true
opacity: root.opacity
layer.effect: MultiEffect {
maskSource: mask
maskEnabled: true
maskInverted: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Behavior on opacity {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration("large")
easing.type: Easing.InOutExpo
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
StyledRect {
anchors.fill: parent
anchors.topMargin: Config.runtime.bar.position === "bottom" ? -15 : 0
anchors.bottomMargin: Config.runtime.bar.position === "top" ? -15 : 0
anchors.leftMargin: Config.runtime.bar.position === "right" ? -15 : 0
anchors.rightMargin: Config.runtime.bar.position === "left" ? -15 : 0
radius: Metrics.radius("normal")
}
}
}
mask: Region {
item: container
intersection: Intersection.Xor
}
}

View File

@@ -0,0 +1,136 @@
import qs.config
import qs.modules.components
import qs.modules.functions
import qs.services
import QtQuick
import Quickshell
import Quickshell.Wayland
import QtQuick.Layouts
Item {
id: container
property string displayName: screen?.name ?? ""
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
property Toplevel activeToplevel: Compositor.isWorkspaceOccupied(Compositor.focusedWorkspaceId)
? Compositor.activeToplevel
: null
implicitWidth: row.implicitWidth + 30
implicitHeight: ConfigResolver.bar(displayName).modules.height
function simplifyTitle(title) {
if (!title)
return ""
title = title.replace(/[●⬤○◉◌◎]/g, "") // Symbols to remove
// Normalize separators
title = title
.replace(/\s*[|—]\s*/g, " - ")
.replace(/\s+/g, " ")
.trim()
const parts = title.split(" - ").map(p => p.trim()).filter(Boolean)
if (parts.length === 1)
return parts[0]
// Known app names (extend freely my fellow contributors)
const apps = [
"Firefox", "Mozilla Firefox",
"Chromium", "Google Chrome",
"Neovim", "VS Code", "Code",
"Kitty", "Alacritty", "Terminal",
"Discord", "Spotify", "Steam",
"Settings - Nucleus", "Settings"
]
let app = ""
for (let i = parts.length - 1; i >= 0; i--) { // loop over
for (let a of apps) {
if (parts[i].includes(a)) {
app = a
break
}
}
if (app) break
}
if (!app)
app = parts[parts.length - 1]
const context = parts.find(p => p !== app)
return context ? `${app} · ${context}` : app
}
function formatAppId(appId) { // Random ass function to make it look good
if (!appId || appId.length === 0)
return "";
// split on dashes/underscores
const parts = appId.split(/[-_]/);
// capitalize each segment
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
parts[i] = p.charAt(0).toUpperCase() + p.slice(1);
}
return parts.join("-");
}
/* Column {
id: col
anchors.centerIn: parent
StyledText {
id: workspaceText
font.pixelSize: Metrics.fontSize("smallie")
text: {
if (!activeToplevel)
return "Desktop"
const id = activeToplevel.appId || ""
return id // Just for aesthetics
}
horizontalAlignment: Text.AlignHCenter
}
StyledText {
id: titleText
text: StringUtils.shortText(simplifyTitle(activeToplevel?.title, 24) || `Workspace ${Hyprland.focusedWorkspaceId}`)
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Metrics.fontSize("smalle")
}
} */
Rectangle {
visible: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
color: Appearance.m3colors.m3paddingContainer
anchors.fill: parent
height: 34
width: row.height + 30
radius: ConfigResolver.bar(displayName).modules.radius
}
RowLayout {
id: row
spacing: 12
anchors.centerIn: parent
MaterialSymbol {
icon: "desktop_windows"
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
}
StyledText {
text: StringUtils.shortText(simplifyTitle(activeToplevel?.title), 24) || `Workspace ${Hyprland.focusedWorkspaceId}`
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.size.small
}
}
}

View File

@@ -0,0 +1,57 @@
import QtQuick
import QtQuick.Layouts
import qs.config
import qs.modules.components
import qs.services
Item {
id: batteryIndicatorModuleContainer
visible: UPower.batteryPresent
Layout.alignment: Qt.AlignVCenter
// Determine if bar is isVertical
property bool isVertical: ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right"
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
Rectangle {
id: bgRect
color: isVertical ? Appearance.m3colors.m3primary : Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor // No need to use metrics here...
implicitWidth: child.implicitWidth + Appearance.margin.large - (isVertical ? 10 : 0)
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
}
RowLayout {
id: child
anchors.centerIn: parent
spacing: isVertical ? 0 : Metrics.spacing(8)
// Icon for isVertical bars
MaterialSymbol {
visible: isVertical
icon: UPower.battIcon
iconSize: Metrics.iconSize(20)
}
// Battery percentage text
StyledText {
animate: false
font.pixelSize: Metrics.fontSize(16)
rotation: isVertical ? 270 : 0
text: (isVertical ? UPower.percentage : UPower.percentage + "%")
}
// Circular progress for horizontal bars
CircularProgressBar {
visible: !isVertical
value: UPower.percentage / 100
icon: UPower.battIcon
iconSize: Metrics.iconSize(18)
Layout.bottomMargin: Metrics.margin(2)
}
}
}

View File

@@ -0,0 +1,27 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.config
import qs.modules.components
Item {
id: clockContainer
property string format: isVertical ? "hh\nmm\nAP" : "hh:mm • dd/MM"
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
Layout.alignment: Qt.AlignVCenter
implicitWidth: 37
implicitHeight: 30
AnimatedImage {
id: art
anchors.fill: parent
source: Directories.assetsPath + "/gifs/bongo-cat.gif"
cache: false // this is important
smooth: true // smooooooth
rotation: isVertical ? 270 : 0
}
}

View File

@@ -0,0 +1,36 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.config
import qs.modules.components
Item {
id: clockContainer
property string format: isVertical ? "hh\nmm\nAP" : "hh:mm • dd/MM"
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
Layout.alignment: Qt.AlignVCenter
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
// Let the layout compute size automatically
Rectangle {
id: bgRect
color: isVertical ? "transparent" : Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor
// Padding around the text
implicitWidth: isVertical ? textItem.implicitWidth + 40 : textItem.implicitWidth + Metrics.margin("large")
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
}
StyledText {
id: textItem
anchors.centerIn: parent
animate: false
text: Time.format(clockContainer.format)
}
}

View File

@@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import qs.config
import qs.modules.functions
import qs.modules.components
import qs.services
Item {
id: mediaPlayer
property bool isVertical: (
ConfigResolver.bar(screen?.name ?? "").position === "left" ||
ConfigResolver.bar(screen?.name ?? "").position === "right"
)
Layout.alignment: Qt.AlignCenter | Qt.AlignVCenter
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
Rectangle {
id: bgRect
color: Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius *
Config.runtime.appearance.rounding.factor
implicitWidth: isVertical
? row.implicitWidth + Metrics.margin("large") - 10
: row.implicitWidth + Metrics.margin("large")
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
}
Row {
id: row
anchors.centerIn: parent
spacing: Metrics.margin("small")
ClippingRectangle {
id: iconButton
width: 24
height: 24
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius / 1.2
color: Appearance.colors.colLayer1Hover
opacity: 0.9
clip: true
layer.enabled: true
Item {
anchors.fill: parent
Image {
id: art
anchors.fill: parent
visible: Mpris.artUrl !== ""
source: Mpris.artUrl
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
}
MaterialSymbol {
anchors.centerIn: parent
visible: Mpris.artUrl === ""
icon: "music_note"
iconSize: 18
color: Config.runtime.appearance.theme === "dark"
? "#b1a4a4"
: "grey"
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: Mpris.playPause()
onEntered: iconButton.opacity = 1
onExited: iconButton.opacity = 0.9
}
RotationAnimation on rotation {
from: 0
to: 360
duration: Metrics.chronoDuration(4000)
loops: Animation.Infinite
running: Mpris.isPlaying &&
Config.runtime.appearance.animations.enabled
}
}
StyledText {
id: textItem
anchors.verticalCenter: parent.verticalCenter
text: StringUtils.shortText(Mpris.title, 16)
visible: !mediaPlayer.isVertical
}
}
}

View File

@@ -0,0 +1,65 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.config
import qs.modules.components
import qs.services
Item {
id: statusIconsContainer
property bool isVertical: (ConfigResolver.bar(screen?.name ?? "").position === "left" || ConfigResolver.bar(screen?.name ?? "").position === "right")
Layout.alignment: Qt.AlignVCenter
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.enabled
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
StyledRect {
id: bgRect
color: Globals.visiblility.sidebarRight ? Appearance.m3colors.m3paddingContainer : "transparent"
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius * Config.runtime.appearance.rounding.factor
implicitWidth: isVertical ? contentRow.implicitWidth + Metrics.margin("large") - 8 : contentRow.implicitWidth + Metrics.margin("large")
implicitHeight: ConfigResolver.bar(screen?.name ?? "").modules.height
RowLayout {
id: contentRow
anchors.centerIn: parent
spacing: isVertical ? Metrics.spacing(8) : Metrics.spacing(16)
MaterialSymbol {
id: wifi
animate: false
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.networkStatusEnabled
rotation: isVertical ? 270 : 0
icon: Network.icon
iconSize: Metrics.fontSize("huge")
}
MaterialSymbol {
id: btIcon
animate: false
visible: ConfigResolver.bar(screen?.name ?? "").modules.statusIcons.bluetoothStatusEnabled
rotation: isVertical ? 270 : 0
icon: Bluetooth.icon
iconSize: Metrics.fontSize("huge")
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (Globals.visiblility.sidebarLeft)
return
Globals.visiblility.sidebarRight = !Globals.visiblility.sidebarRight
}
}
}
}

View File

@@ -0,0 +1,165 @@
import qs.modules.components
import qs.config
import qs.services
import Quickshell.Services.SystemTray
import QtQuick
import Quickshell
import Quickshell.Widgets
import QtQuick.Layouts
Item {
id: root
readonly property Repeater items: items
property bool horizontalMode: (ConfigResolver.bar(screen?.name ?? "").position === "top" || ConfigResolver.bar(screen?.name ?? "").position === "bottom")
clip: true
implicitWidth: layout.implicitWidth + Metrics.margin("verylarge")
implicitHeight: 34
Rectangle {
visible: (items.count > 0) ? 1 : 0
id: padding
implicitHeight: padding.height
anchors.fill: parent
radius: ConfigResolver.bar(screen?.name ?? "").modules.radius
color: "transparent"
}
GridLayout {
id: layout
anchors.centerIn: parent
rows: 1
columns: items.count
rowSpacing: Metrics.spacing(10)
columnSpacing: Metrics.spacing(10)
Repeater {
id: items
model: SystemTray.items
delegate: Item {
id: trayItemRoot
required property SystemTrayItem modelData
implicitWidth: 20
implicitHeight: 20
IconImage {
visible: trayItemRoot.modelData.icon !== ""
source: trayItemRoot.modelData.icon
asynchronous: true
anchors.fill: parent
rotation: root.horizontalMode ? 0 : 270
}
HoverHandler {
id: hover
}
QsMenuOpener {
id: menuOpener
menu: trayItemRoot.modelData.menu
}
StyledPopout {
id: popout
hoverTarget: hover
interactable: true
hCenterOnItem: true
requiresHover: false
Component {
Item {
width: childColumn.implicitWidth
height: childColumn.height
ColumnLayout {
id: childColumn
spacing: Metrics.spacing(5)
Repeater {
model: menuOpener.children
delegate: TrayMenuItem {
parentColumn: childColumn
Layout.preferredWidth: childColumn.width > 0 ? childColumn.width : implicitWidth
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
if (popout.isVisible)
popout.hide();
else
popout.show();
}
}
}
}
}
component TrayMenuItem: Item {
id: itemRoot
required property QsMenuEntry modelData
required property ColumnLayout parentColumn
Layout.fillWidth: true
implicitWidth: rowLayout.implicitWidth + 10
implicitHeight: !itemRoot.modelData.isSeparator ? rowLayout.implicitHeight + 10 : 1
MouseArea {
id: hover
hoverEnabled: itemRoot.modelData.enabled
anchors.fill: parent
onClicked: {
if (!itemRoot.modelData.hasChildren)
itemRoot.modelData.triggered();
}
}
Rectangle {
id: itemBg
anchors.fill: parent
opacity: itemRoot.modelData.isSeparator ? 0.5 : 1
color: itemRoot.modelData.isSeparator ? Appearance.m3colors.m3outline : hover.containsMouse ? Appearance.m3colors.m3surfaceContainer : Appearance.m3colors.m3surface
}
RowLayout {
id: rowLayout
visible: !itemRoot.modelData.isSeparator
opacity: itemRoot.modelData.isSeparator ? 0.5 : 1
spacing: Metrics.spacing(5)
anchors {
left: itemBg.left
leftMargin: Metrics.margin(5)
top: itemBg.top
topMargin:Metrics.margin(5)
}
IconImage {
visible: itemRoot.modelData.icon !== ""
source: itemRoot.modelData.icon
width: 15
height: 15
}
StyledText {
text: itemRoot.modelData.text
font.pixelSize: Metrics.fontSize(14)
color: Appearance.m3colors.m3onSurface
}
MaterialSymbol {
visible: itemRoot.modelData.hasChildren
icon: "chevron_right"
iconSize: Metrics.iconSize(16)
color: Appearance.m3colors.m3onSurface
}
}
}
}

View File

@@ -0,0 +1,99 @@
import QtQuick
import QtQuick.Layouts
import qs.config
import qs.modules.components
import qs.services
Item {
id: systemUsageContainer
property string displayName: screen?.name ?? ""
property bool isHorizontal: (ConfigResolver.bar(displayName).position === "top" || ConfigResolver.bar(displayName).position === "bottom")
visible: ConfigResolver.bar(displayName).modules.systemUsage.enabled && haveWidth
Layout.alignment: Qt.AlignVCenter
implicitWidth: bgRect.implicitWidth
implicitHeight: bgRect.implicitHeight
property bool haveWidth:
ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled ||
ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled ||
ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled
// Normalize values so UI always receives correct ranges
function normalize(v) {
if (v > 1) return v / 100
return v
}
function percent(v) {
if (v <= 1) return Math.round(v * 100)
return Math.round(v)
}
Rectangle {
id: bgRect
color: Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
implicitWidth: child.implicitWidth + Metrics.margin("large")
implicitHeight: ConfigResolver.bar(displayName).modules.height
}
RowLayout {
id: child
anchors.centerIn: parent
spacing: Metrics.spacing(4)
// CPU
CircularProgressBar {
rotation: !isHorizontal ? 270 : 0
icon: "developer_board"
visible: ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled
iconSize: Metrics.iconSize(14)
value: normalize(SystemDetails.cpuPercent)
Layout.bottomMargin: Metrics.margin(2)
}
StyledText {
visible: ConfigResolver.bar(displayName).modules.systemUsage.cpuStatsEnabled && isHorizontal
animate: false
text: percent(SystemDetails.cpuPercent) + "%"
}
// RAM
CircularProgressBar {
rotation: !isHorizontal ? 270 : 0
Layout.leftMargin: Metrics.margin(4)
icon: "memory_alt"
visible: ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled
iconSize: Metrics.iconSize(14)
value: normalize(SystemDetails.ramPercent)
Layout.bottomMargin: Metrics.margin(2)
}
StyledText {
visible: ConfigResolver.bar(displayName).modules.systemUsage.memoryStatsEnabled && isHorizontal
animate: false
text: percent(SystemDetails.ramPercent) + "%"
}
// Temperature
CircularProgressBar {
rotation: !isHorizontal ? 270 : 0
visible: ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled
Layout.leftMargin: Metrics.margin(4)
icon: "device_thermostat"
iconSize: Metrics.iconSize(14)
value: normalize(SystemDetails.cpuTempPercent)
Layout.bottomMargin: Metrics.margin(2)
}
StyledText {
visible: ConfigResolver.bar(displayName).modules.systemUsage.tempStatsEnabled && isHorizontal
animate: false
text: percent(SystemDetails.cpuTempPercent) + "%"
}
}
}

View File

@@ -0,0 +1,43 @@
import qs.config
import qs.modules.components
import QtQuick
import Quickshell
import QtQuick.Layouts
StyledRect {
id: bg
property string icon
property color iconColor: Appearance.syntaxHighlightingTheme
property int iconSize
property bool toggle
property bool transparentBg: false
signal toggled(bool value)
color: (ma.containsMouse && !transparentBg)
? Appearance.m3colors.m3paddingContainer
: "transparent"
radius: Metrics.radius("childish")
implicitWidth: textItem.implicitWidth + 12
implicitHeight: textItem.implicitHeight + 6
MaterialSymbol {
id: textItem
anchors.centerIn: parent
anchors.verticalCenterOffset: 0.4
anchors.horizontalCenterOffset: 0.499
iconSize: bg.iconSize
icon: bg.icon
color: bg.iconColor
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
onClicked: bg.toggled(!bg.toggle)
}
}

View File

@@ -0,0 +1,254 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.config
import qs.modules.components
import qs.modules.functions
import qs.services
Item {
id: workspaceContainer
property string displayName: screen?.name ?? ""
property int numWorkspaces: ConfigResolver.bar(displayName).modules.workspaces.workspaceIndicators
property var workspaceOccupied: []
property var occupiedRanges: []
function japaneseNumber(num) {
var kanjiMap = {
"0": "零",
"1": "一",
"2": "二",
"3": "三",
"4": "四",
"5": "五",
"6": "六",
"7": "七",
"8": "八",
"9": "九",
"10": "十"
};
return kanjiMap[num] !== undefined ? kanjiMap[num] : "Number out of range";
}
function updateWorkspaceOccupied() {
const offset = 1;
workspaceOccupied = Array.from({
"length": numWorkspaces
}, (_, i) => {
return Compositor.isWorkspaceOccupied(i + 1);
});
const ranges = [];
let start = -1;
for (let i = 0; i < workspaceOccupied.length; i++) {
if (workspaceOccupied[i]) {
if (start === -1)
start = i;
} else if (start !== -1) {
ranges.push({
"start": start,
"end": i - 1
});
start = -1;
}
}
if (start !== -1)
ranges.push({
"start": start,
"end": workspaceOccupied.length - 1
});
occupiedRanges = ranges;
}
visible: ConfigResolver.bar(displayName).modules.workspaces.enabled
implicitWidth: bg.implicitWidth
implicitHeight: ConfigResolver.bar(displayName).modules.height
Component.onCompleted: updateWorkspaceOccupied()
Connections {
function onStateChanged() {
updateWorkspaceOccupied();
}
target: Compositor
}
Rectangle {
id: bg
color: Appearance.m3colors.m3paddingContainer
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
implicitWidth: workspaceRow.implicitWidth + Metrics.margin("large") - 8
implicitHeight: ConfigResolver.bar(displayName).modules.height
// occupied background highlight
Item {
id: occupiedStretchLayer
anchors.centerIn: workspaceRow
width: workspaceRow.width
height: 26
z: 0
visible: Compositor.require("hyprland") // Hyprland only
Repeater {
model: occupiedRanges
Rectangle {
height: 26
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
color: ColorUtils.mix(Appearance.m3colors.m3tertiary, Appearance.m3colors.m3surfaceContainerLowest)
opacity: 0.8
x: modelData.start * (26 + workspaceRow.spacing)
width: (modelData.end - modelData.start + 1) * 26 + (modelData.end - modelData.start) * workspaceRow.spacing
}
}
}
// workspace highlight
Rectangle {
id: highlight
property int offset: Compositor.require("hyprland") ? 1 : 0
property int index: Math.max(0, Compositor.focusedWorkspaceId - 1 - offset)
property real itemWidth: 26
property real spacing: workspaceRow.spacing
property int highlightIndex: {
if (!Compositor.focusedWorkspaceId)
return 0;
if (Compositor.require("hyprland"))
return Compositor.focusedWorkspaceId - 1;
// Hyprland starts at 2 internally
return Compositor.focusedWorkspaceId - 2; // Niri or default
}
property real targetX: Math.min(highlightIndex, numWorkspaces - 1) * (itemWidth + spacing) + 7.3
property real animatedX1: targetX
property real animatedX2: targetX
x: Math.min(animatedX1, animatedX2)
anchors.verticalCenter: parent.verticalCenter
width: Math.abs(animatedX2 - animatedX1) + itemWidth - 1
height: 24
radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor
color: Appearance.m3colors.m3tertiary
onTargetXChanged: {
animatedX1 = targetX;
animatedX2 = targetX;
}
Behavior on animatedX1 {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration(400)
easing.type: Easing.OutSine
}
}
Behavior on animatedX2 {
enabled: Config.runtime.appearance.animations.enabled
NumberAnimation {
duration: Metrics.chronoDuration(133)
easing.type: Easing.OutSine
}
}
}
RowLayout {
id: workspaceRow
anchors.centerIn: parent
spacing: Metrics.spacing(10)
Repeater {
model: numWorkspaces
Item {
property int wsIndex: index + 1
property bool occupied: Compositor.isWorkspaceOccupied(wsIndex)
property bool focused: wsIndex === Compositor.focusedWorkspaceId
width: 26
height: 26
// Icon container — only used on Hyprland
ClippingRectangle {
id: iconContainer
anchors.centerIn: parent
width: 20
height: 20
color: "transparent"
radius: Appearance.rounding.small
clip: true
IconImage {
id: appIcon
anchors.fill: parent
visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && occupied
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
source: {
const win = Compositor.focusedWindowForWorkspace(wsIndex);
return win ? AppRegistry.iconForClass(win.class) : "";
}
layer.enabled: true
layer.effect: MultiEffect {
saturation: (Config.runtime.appearance.tintIcons || (Config.runtime.appearance.colors.matugenScheme === "scheme-monochrome" && Config.runtime.appearance.colors.autogenerated) || Config.runtime.appearance.colors.scheme.toLowerCase() === "monochrome") ? -1.0 : 1.0
}
}
}
// Kanji mode — only if not Hyprland
StyledText {
anchors.centerIn: parent
visible: ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons
text: japaneseNumber(index + 1)
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
}
// Numbers mode — only if not Hyprland
StyledText {
anchors.centerIn: parent
visible: !ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons
text: index + 1
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
}
// Symbols for unoccupied workspaces — only for Hyprland icons
MaterialSymbol {
property string displayText: Config.runtime.appearance.rounding.factor === 0 ? "crop_square" : "fiber_manual_record"
anchors.centerIn: parent
visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && !occupied
text: displayText
rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0
font.pixelSize: Metrics.iconSize(10)
fill: 1
}
MouseArea {
anchors.fill: parent
onClicked: Compositor.changeWorkspace(wsIndex)
}
}
}
}
}
}