quickshell lets gooo

This commit is contained in:
zastian@mrthoddata.com
2025-06-17 18:54:28 +01:00
parent 8d618a8ae3
commit 040bc459a4
124 changed files with 11667 additions and 83 deletions

View File

@@ -0,0 +1,140 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
import QtQuick
Item {
id: root
required property Brightness.Monitor monitor
property color colour: Colours.palette.m3primary
readonly property Item child: child
implicitWidth: child.implicitWidth
implicitHeight: child.implicitHeight
MouseArea {
anchors.top: parent.top
anchors.bottom: child.top
anchors.left: parent.left
anchors.right: parent.right
onWheel: event => {
if (event.angleDelta.y > 0)
Audio.setVolume(Audio.volume + 0.1);
else if (event.angleDelta.y < 0)
Audio.setVolume(Audio.volume - 0.1);
}
}
MouseArea {
anchors.top: child.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
onWheel: event => {
const monitor = root.monitor;
if (event.angleDelta.y > 0)
monitor.setBrightness(monitor.brightness + 0.1);
else if (event.angleDelta.y < 0)
monitor.setBrightness(monitor.brightness - 0.1);
}
}
Item {
id: child
property Item current: text1
anchors.centerIn: parent
clip: true
implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight)
implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin
MaterialIcon {
id: icon
animate: true
text: Icons.getAppCategoryIcon(Hyprland.activeClient?.wmClass, "desktop_windows")
color: root.colour
anchors.horizontalCenter: parent.horizontalCenter
}
Title {
id: text1
}
Title {
id: text2
}
TextMetrics {
id: metrics
text: Hyprland.activeClient?.title ?? qsTr("Desktop")
font.pointSize: Appearance.font.size.smaller
font.family: Appearance.font.family.mono
elide: Qt.ElideRight
elideWidth: root.height - icon.height
onTextChanged: {
const next = child.current === text1 ? text2 : text1;
next.text = elidedText;
child.current = next;
}
onElideWidthChanged: child.current.text = elidedText
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
component Title: StyledText {
id: text
anchors.horizontalCenter: icon.horizontalCenter
anchors.top: icon.bottom
anchors.topMargin: Appearance.spacing.small
font.pointSize: metrics.font.pointSize
font.family: metrics.font.family
color: root.colour
opacity: child.current === this ? 1 : 0
transform: Rotation {
angle: 90
origin.x: text.implicitHeight / 2
origin.y: text.implicitHeight / 2
}
width: implicitHeight
height: implicitWidth
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}

View File

@@ -0,0 +1,33 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Column {
id: root
property color colour: Colours.palette.m3tertiary
spacing: Appearance.spacing.small
MaterialIcon {
id: icon
text: "calendar_month"
color: root.colour
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
id: text
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: StyledText.AlignHCenter
text: Time.format("hh\nmm")
font.pointSize: Appearance.font.size.smaller
font.family: Appearance.font.family.mono
color: root.colour
}
}

View File

@@ -0,0 +1,11 @@
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
StyledText {
text: Icons.osIcon
font.pointSize: Appearance.font.size.smaller
font.family: Appearance.font.family.mono
color: Colours.palette.m3tertiary
}

View File

@@ -0,0 +1,27 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
MaterialIcon {
text: "power_settings_new"
color: Colours.palette.m3error
font.bold: true
font.pointSize: Appearance.font.size.normal
StateLayer {
anchors.fill: undefined
anchors.centerIn: parent
anchors.horizontalCenterOffset: 1
implicitWidth: parent.implicitHeight + Appearance.padding.small * 2
implicitHeight: implicitWidth
radius: Appearance.rounding.full
function onClicked(): void {
const v = Visibilities.screens[QsWindow.window.screen];
v.session = !v.session;
}
}
}

View File

@@ -0,0 +1,114 @@
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
import Quickshell
import Quickshell.Services.UPower
import QtQuick
Item {
id: root
property color colour: Colours.palette.m3secondary
readonly property Item network: network
readonly property real bs: bluetooth.y
readonly property real be: repeater.count > 0 ? devices.y + devices.implicitHeight : bluetooth.y + bluetooth.implicitHeight
readonly property Item battery: battery
clip: true
implicitWidth: Math.max(network.implicitWidth, bluetooth.implicitWidth, devices.implicitWidth, battery.implicitWidth)
implicitHeight: network.implicitHeight + bluetooth.implicitHeight + bluetooth.anchors.topMargin + (repeater.count > 0 ? devices.implicitHeight + devices.anchors.topMargin : 0) + battery.implicitHeight + battery.anchors.topMargin
MaterialIcon {
id: network
animate: true
text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off"
color: root.colour
anchors.horizontalCenter: parent.horizontalCenter
}
MaterialIcon {
id: bluetooth
anchors.horizontalCenter: network.horizontalCenter
anchors.top: network.bottom
anchors.topMargin: Appearance.spacing.small
animate: true
text: Bluetooth.powered ? "bluetooth" : "bluetooth_disabled"
color: root.colour
}
Column {
id: devices
anchors.horizontalCenter: bluetooth.horizontalCenter
anchors.top: bluetooth.bottom
anchors.topMargin: Appearance.spacing.small
Repeater {
id: repeater
model: ScriptModel {
values: Bluetooth.devices.filter(d => d.connected)
}
MaterialIcon {
required property Bluetooth.Device modelData
animate: true
text: Icons.getBluetoothIcon(modelData.icon)
color: root.colour
}
}
}
MaterialIcon {
id: battery
anchors.horizontalCenter: devices.horizontalCenter
anchors.top: repeater.count > 0 ? devices.bottom : bluetooth.bottom
anchors.topMargin: Appearance.spacing.small
animate: true
text: {
if (!UPower.displayDevice.isLaptopBattery) {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
if (PowerProfiles.profile === PowerProfile.Performance)
return "rocket_launch";
return "balance";
}
const perc = UPower.displayDevice.percentage;
const charging = !UPower.onBattery;
if (perc === 1)
return charging ? "battery_charging_full" : "battery_full";
let level = Math.floor(perc * 7);
if (charging && (level === 4 || level === 1))
level--;
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
}
color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error
fill: 1
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}

View File

@@ -0,0 +1,56 @@
import "root:/config"
import Quickshell.Services.SystemTray
import QtQuick
Item {
id: root
readonly property Repeater items: items
clip: true
visible: width > 0 && height > 0 // To avoid warnings about being visible with no size
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
Column {
id: layout
spacing: Appearance.spacing.small
add: Transition {
NumberAnimation {
properties: "scale"
from: 0
to: 1
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
Repeater {
id: items
model: SystemTray.items
TrayItem {}
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}

View File

@@ -0,0 +1,48 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import QtQuick
MouseArea {
id: root
required property SystemTrayItem modelData
acceptedButtons: Qt.LeftButton | Qt.RightButton
implicitWidth: Appearance.font.size.small * 2
implicitHeight: Appearance.font.size.small * 2
onClicked: event => {
if (event.button === Qt.LeftButton)
modelData.activate();
else if (modelData.hasMenu)
menu.open();
}
// TODO custom menu
QsMenuAnchor {
id: menu
menu: root.modelData.menu
anchor.window: this.QsWindow.window
}
IconImage {
id: icon
source: {
let icon = root.modelData.icon;
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
icon = `file://${path}/${name.slice(name.lastIndexOf("/") + 1)}`;
}
return icon;
}
asynchronous: true
anchors.fill: parent
}
}

View File

@@ -0,0 +1,111 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Effects
StyledRect {
id: root
required property list<Workspace> workspaces
required property Item mask
required property real maskWidth
required property real maskHeight
required property int groupOffset
readonly property int currentWsIdx: Hyprland.activeWsId - 1 - groupOffset
property real leading: getWsY(currentWsIdx)
property real trailing: getWsY(currentWsIdx)
property real currentSize: workspaces[currentWsIdx]?.size ?? 0
property real offset: Math.min(leading, trailing)
property real size: {
const s = Math.abs(leading - trailing) + currentSize;
if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx)
return Math.min(getWsY(lastWs) + (workspaces[lastWs]?.size ?? 0) - offset, s);
return s;
}
property int cWs
property int lastWs
function getWsY(idx: int): real {
let y = 0;
for (let i = 0; i < idx; i++)
y += workspaces[i]?.size ?? 0;
return y;
}
onCurrentWsIdxChanged: {
lastWs = cWs;
cWs = currentWsIdx;
}
clip: true
x: 1
y: offset + 1
implicitWidth: Config.bar.sizes.innerHeight - 2
implicitHeight: size - 2
radius: Config.bar.workspaces.rounded ? Appearance.rounding.full : 0
color: Colours.palette.m3primary
StyledRect {
id: base
visible: false
anchors.fill: parent
color: Colours.palette.m3onPrimary
}
MultiEffect {
source: base
maskSource: root.mask
maskEnabled: true
maskSpreadAtMin: 1
maskThresholdMin: 0.5
x: 0
y: -parent.offset
implicitWidth: root.maskWidth
implicitHeight: root.maskHeight
anchors.horizontalCenter: parent.horizontalCenter
}
Behavior on leading {
enabled: Config.bar.workspaces.activeTrail
Anim {}
}
Behavior on trailing {
enabled: Config.bar.workspaces.activeTrail
Anim {
duration: Appearance.anim.durations.normal * 2
}
}
Behavior on currentSize {
enabled: Config.bar.workspaces.activeTrail
Anim {}
}
Behavior on offset {
enabled: !Config.bar.workspaces.activeTrail
Anim {}
}
Behavior on size {
enabled: !Config.bar.workspaces.activeTrail
Anim {}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}

View File

@@ -0,0 +1,99 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import QtQuick
Item {
id: root
required property list<Workspace> workspaces
required property var occupied
required property int groupOffset
property list<var> pills: []
onOccupiedChanged: {
let count = 0;
const start = groupOffset;
const end = start + Config.bar.workspaces.shown;
for (const [ws, occ] of Object.entries(occupied)) {
if (ws > start && ws <= end && occ) {
if (!occupied[ws - 1]) {
if (pills[count])
pills[count].start = ws;
else
pills.push(pillComp.createObject(root, {
start: ws
}));
count++;
}
if (!occupied[ws + 1])
pills[count - 1].end = ws;
}
}
if (pills.length > count)
pills.splice(count, pills.length - count).forEach(p => p.destroy());
}
Repeater {
model: ScriptModel {
values: root.pills.filter(p => p)
}
StyledRect {
id: rect
required property var modelData
readonly property Workspace start: root.workspaces[modelData.start - 1 - root.groupOffset] ?? null
readonly property Workspace end: root.workspaces[modelData.end - 1 - root.groupOffset] ?? null
color: Colours.alpha(Colours.palette.m3surfaceContainerHigh, true)
radius: Config.bar.workspaces.rounded ? Appearance.rounding.full : 0
x: start?.x ?? 0
y: start?.y ?? 0
implicitWidth: Config.bar.sizes.innerHeight
implicitHeight: end?.y + end?.height - start?.y
anchors.horizontalCenter: parent.horizontalCenter
scale: 0
Component.onCompleted: scale = 1
Behavior on scale {
Anim {
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
Behavior on x {
Anim {}
}
Behavior on y {
Anim {}
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
component Pill: QtObject {
property int start
property int end
}
Component {
id: pillComp
Pill {}
}
}

View File

@@ -0,0 +1,93 @@
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
import Quickshell
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property int index
required property var occupied
required property int groupOffset
readonly property bool isWorkspace: true // Flag for finding workspace children
// Unanimated prop for others to use as reference
readonly property real size: childrenRect.height + (hasWindows ? Appearance.padding.normal : 0)
readonly property int ws: groupOffset + index + 1
readonly property bool isOccupied: occupied[ws] ?? false
readonly property bool hasWindows: isOccupied && Config.bar.workspaces.showWindows
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: size
StyledText {
id: indicator
readonly property string label: Config.bar.workspaces.label || root.ws
readonly property string occupiedLabel: Config.bar.workspaces.occupiedLabel || label
readonly property string activeLabel: Config.bar.workspaces.activeLabel || (root.isOccupied ? occupiedLabel : label)
animate: true
text: Hyprland.activeWsId === root.ws ? activeLabel : root.isOccupied ? occupiedLabel : label
color: Config.bar.workspaces.occupiedBg || root.isOccupied || Hyprland.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.palette.m3outlineVariant
horizontalAlignment: StyledText.AlignHCenter
verticalAlignment: StyledText.AlignVCenter
width: Config.bar.sizes.innerHeight
height: Config.bar.sizes.innerHeight
}
Loader {
id: windows
active: Config.bar.workspaces.showWindows
asynchronous: true
anchors.horizontalCenter: indicator.horizontalCenter
anchors.top: indicator.bottom
sourceComponent: Column {
spacing: Appearance.spacing.small
add: Transition {
Anim {
properties: "scale"
from: 0
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
Repeater {
model: ScriptModel {
values: Hyprland.clients.filter(c => c.workspace?.id === root.ws)
}
MaterialIcon {
required property Hyprland.Client modelData
text: Icons.getAppCategoryIcon(modelData.wmClass, "terminal")
color: Colours.palette.m3onSurfaceVariant
}
}
}
}
Behavior on Layout.preferredWidth {
Anim {}
}
Behavior on Layout.preferredHeight {
Anim {}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View File

@@ -0,0 +1,75 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Layouts
Item {
id: root
readonly property list<Workspace> workspaces: layout.children.filter(c => c.isWorkspace).sort((w1, w2) => w1.ws - w2.ws)
readonly property var occupied: Hyprland.workspaces.values.reduce((acc, curr) => {
acc[curr.id] = curr.lastIpcObject.windows > 0;
return acc;
}, {})
readonly property int groupOffset: Math.floor((Hyprland.activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
spacing: 0
layer.enabled: true
layer.smooth: true
Repeater {
model: Config.bar.workspaces.shown
Workspace {
occupied: root.occupied
groupOffset: root.groupOffset
}
}
}
Loader {
active: Config.bar.workspaces.occupiedBg
asynchronous: true
z: -1
anchors.fill: parent
sourceComponent: OccupiedBg {
workspaces: root.workspaces
occupied: root.occupied
groupOffset: root.groupOffset
}
}
Loader {
active: Config.bar.workspaces.activeIndicator
asynchronous: true
sourceComponent: ActiveIndicator {
workspaces: root.workspaces
mask: layout
maskWidth: root.width
maskHeight: root.height
groupOffset: root.groupOffset
}
}
MouseArea {
anchors.fill: parent
onPressed: event => {
const ws = layout.childAt(event.x, event.y).index + root.groupOffset + 1;
if (Hyprland.activeWsId !== ws)
Hyprland.dispatch(`workspace ${ws}`);
}
}
}