Rearrange for tidier structure (#2212)

This commit is contained in:
clsty
2025-10-16 07:19:55 +08:00
parent 13065d7e5a
commit 8b493e091d
529 changed files with 165 additions and 138 deletions
@@ -0,0 +1,52 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Item {
id: root
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
property string activeWindowAddress: `0x${activeWindow?.HyprlandToplevel?.address}`
property bool focusingThisMonitor: HyprlandData.activeWorkspace?.monitor == monitor?.name
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(HyprlandData.monitors[root.monitor?.id]?.activeWorkspace.id)
implicitWidth: colLayout.implicitWidth
ColumnLayout {
id: colLayout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: -4
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
elide: Text.ElideRight
text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ?
root.activeWindow?.appId :
(root.biggestWindow?.class) ?? Translation.tr("Desktop")
}
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer0
elide: Text.ElideRight
text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ?
root.activeWindow?.title :
(root.biggestWindow?.title) ?? `${Translation.tr("Workspace")} ${monitor?.activeWorkspace?.id ?? 1}`
}
}
}
@@ -0,0 +1,253 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
Scope {
id: bar
property bool showBarBackground: Config.options.bar.showBackground
Variants {
// For each monitor
model: {
const screens = Quickshell.screens;
const list = Config.options.bar.screenList;
if (!list || list.length === 0)
return screens;
return screens.filter(screen => list.includes(screen.name));
}
LazyLoader {
id: barLoader
active: GlobalStates.barOpen && !GlobalStates.screenLocked
required property ShellScreen modelData
component: PanelWindow { // Bar window
id: barRoot
screen: barLoader.modelData
property var brightnessMonitor: Brightness.getMonitorForScreen(barLoader.modelData)
property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen.width) ? 1 : 0
readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth
Timer {
id: showBarTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
barRoot.superShow = true
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
if (GlobalStates.superDown) showBarTimer.restart();
else {
showBarTimer.stop();
barRoot.superShow = false;
}
}
}
property bool superShow: false
property bool mustShow: hoverRegion.containsMouse || superShow
exclusionMode: ExclusionMode.Ignore
exclusiveZone: (Config?.options.bar.autoHide.enable && (!mustShow || !Config?.options.bar.autoHide.pushWindows)) ? 0 :
Appearance.sizes.baseBarHeight + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0)
WlrLayershell.namespace: "quickshell:bar"
implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding
mask: Region {
item: hoverMaskRegion
}
color: "transparent"
anchors {
top: !Config.options.bar.bottom
bottom: Config.options.bar.bottom
left: true
right: true
}
margins {
right: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.right) * -1
bottom: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.bottom) * -1
}
MouseArea {
id: hoverRegion
hoverEnabled: true
anchors {
fill: parent
rightMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.right) * 1
bottomMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.bottom) * 1
}
Item {
id: hoverMaskRegion
anchors {
fill: barContent
topMargin: -Config.options.bar.autoHide.hoverRegionWidth
bottomMargin: -Config.options.bar.autoHide.hoverRegionWidth
}
}
BarContent {
id: barContent
implicitHeight: Appearance.sizes.barHeight
anchors {
right: parent.right
left: parent.left
top: parent.top
bottom: undefined
topMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight : 0
bottomMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.bottom) * -1
rightMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.right) * -1
}
Behavior on anchors.topMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
states: State {
name: "bottom"
when: Config.options.bar.bottom
AnchorChanges {
target: barContent
anchors {
right: parent.right
left: parent.left
top: undefined
bottom: parent.bottom
}
}
PropertyChanges {
target: barContent
anchors.topMargin: 0
anchors.bottomMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight : 0
}
}
}
// Round decorators
Loader {
id: roundDecorators
anchors {
left: parent.left
right: parent.right
top: barContent.bottom
bottom: undefined
}
height: Appearance.rounding.screenRounding
active: showBarBackground && Config.options.bar.cornerStyle === 0 // Hug
states: State {
name: "bottom"
when: Config.options.bar.bottom
AnchorChanges {
target: roundDecorators
anchors {
right: parent.right
left: parent.left
top: undefined
bottom: barContent.top
}
}
}
sourceComponent: Item {
implicitHeight: Appearance.rounding.screenRounding
RoundCorner {
id: leftCorner
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
implicitSize: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
corner: RoundCorner.CornerEnum.TopLeft
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
leftCorner.corner: RoundCorner.CornerEnum.BottomLeft
}
}
}
RoundCorner {
id: rightCorner
anchors {
right: parent.right
top: !Config.options.bar.bottom ? parent.top : undefined
bottom: Config.options.bar.bottom ? parent.bottom : undefined
}
implicitSize: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
corner: RoundCorner.CornerEnum.TopRight
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
rightCorner.corner: RoundCorner.CornerEnum.BottomRight
}
}
}
}
}
}
}
}
}
IpcHandler {
target: "bar"
function toggle(): void {
GlobalStates.barOpen = !GlobalStates.barOpen
}
function close(): void {
GlobalStates.barOpen = false
}
function open(): void {
GlobalStates.barOpen = true
}
}
GlobalShortcut {
name: "barToggle"
description: "Toggles bar on press"
onPressed: {
GlobalStates.barOpen = !GlobalStates.barOpen;
}
}
GlobalShortcut {
name: "barOpen"
description: "Opens bar on press"
onPressed: {
GlobalStates.barOpen = true;
}
}
GlobalShortcut {
name: "barClose"
description: "Closes bar on press"
onPressed: {
GlobalStates.barOpen = false;
}
}
}
@@ -0,0 +1,349 @@
import "./weather"
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
Item { // Bar content region
id: root
property var screen: root.QsWindow.window?.screen
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen?.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen?.width) ? 1 : 0
readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth
component VerticalBarSeparator: Rectangle {
Layout.topMargin: Appearance.sizes.baseBarHeight / 3
Layout.bottomMargin: Appearance.sizes.baseBarHeight / 3
Layout.fillHeight: true
implicitWidth: 1
color: Appearance.colors.colOutlineVariant
}
// Background shadow
Loader {
active: Config.options.bar.showBackground && Config.options.bar.cornerStyle === 1
anchors.fill: barBackground
sourceComponent: StyledRectangularShadow {
anchors.fill: undefined // The loader's anchors act on this, and this should not have any anchor
target: barBackground
}
}
// Background
Rectangle {
id: barBackground
anchors {
fill: parent
margins: Config.options.bar.cornerStyle === 1 ? (Appearance.sizes.hyprlandGapsOut) : 0 // idk why but +1 is needed
}
color: Config.options.bar.showBackground ? Appearance.colors.colLayer0 : "transparent"
radius: Config.options.bar.cornerStyle === 1 ? Appearance.rounding.windowRounding : 0
border.width: Config.options.bar.cornerStyle === 1 ? 1 : 0
border.color: Appearance.colors.colLayer0Border
}
FocusedScrollMouseArea { // Left side | scroll to change brightness
id: barLeftSideMouseArea
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
right: middleSection.left
}
implicitWidth: leftSectionRowLayout.implicitWidth
implicitHeight: Appearance.sizes.baseBarHeight
onScrollDown: root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness - 0.05)
onScrollUp: root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness + 0.05)
onMovedAway: GlobalStates.osdBrightnessOpen = false
onPressed: event => {
if (event.button === Qt.LeftButton)
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
// Visual content
ScrollHint {
reveal: barLeftSideMouseArea.hovered
icon: "light_mode"
tooltipText: Translation.tr("Scroll to change brightness")
side: "left"
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
RowLayout {
id: leftSectionRowLayout
anchors.fill: parent
spacing: 10
LeftSidebarButton { // Left sidebar button
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Appearance.rounding.screenRounding
colBackground: barLeftSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
}
ActiveWindow {
visible: root.useShortenedForm === 0
Layout.rightMargin: Appearance.rounding.screenRounding
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
Row { // Middle section
id: middleSection
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
spacing: 4
BarGroup {
id: leftCenterGroup
anchors.verticalCenter: parent.verticalCenter
implicitWidth: root.centerSideModuleWidth
Resources {
alwaysShowAllResources: root.useShortenedForm === 2
Layout.fillWidth: root.useShortenedForm === 2
}
Media {
visible: root.useShortenedForm < 2
Layout.fillWidth: true
}
}
VerticalBarSeparator {
visible: Config.options?.bar.borderless
}
BarGroup {
id: middleCenterGroup
anchors.verticalCenter: parent.verticalCenter
padding: workspacesWidget.widgetPadding
Workspaces {
id: workspacesWidget
Layout.fillHeight: true
MouseArea {
// Right-click to toggle overview
anchors.fill: parent
acceptedButtons: Qt.RightButton
onPressed: event => {
if (event.button === Qt.RightButton) {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
}
}
}
VerticalBarSeparator {
visible: Config.options?.bar.borderless
}
MouseArea {
id: rightCenterGroup
anchors.verticalCenter: parent.verticalCenter
implicitWidth: root.centerSideModuleWidth
implicitHeight: rightCenterGroupContent.implicitHeight
onPressed: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
BarGroup {
id: rightCenterGroupContent
anchors.fill: parent
ClockWidget {
showDate: (Config.options.bar.verbose && root.useShortenedForm < 2)
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
}
UtilButtons {
visible: (Config.options.bar.verbose && root.useShortenedForm === 0)
Layout.alignment: Qt.AlignVCenter
}
BatteryIndicator {
visible: (root.useShortenedForm < 2 && UPower.displayDevice.isLaptopBattery)
Layout.alignment: Qt.AlignVCenter
}
}
}
}
FocusedScrollMouseArea { // Right side | scroll to change volume
id: barRightSideMouseArea
anchors {
top: parent.top
bottom: parent.bottom
left: middleSection.right
right: parent.right
}
implicitWidth: rightSectionRowLayout.implicitWidth
implicitHeight: Appearance.sizes.baseBarHeight
onScrollDown: {
const currentVolume = Audio.value;
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
Audio.sink.audio.volume -= step;
}
onScrollUp: {
const currentVolume = Audio.value;
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step);
}
onMovedAway: GlobalStates.osdVolumeOpen = false;
onPressed: event => {
if (event.button === Qt.LeftButton) {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
}
// Visual content
ScrollHint {
reveal: barRightSideMouseArea.hovered
icon: "volume_up"
tooltipText: Translation.tr("Scroll to change volume")
side: "right"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
RowLayout {
id: rightSectionRowLayout
anchors.fill: parent
spacing: 5
layoutDirection: Qt.RightToLeft
RippleButton { // Right sidebar button
id: rightSidebarButton
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.rightMargin: Appearance.rounding.screenRounding
Layout.fillWidth: false
implicitWidth: indicatorsRowLayout.implicitWidth + 10 * 2
implicitHeight: indicatorsRowLayout.implicitHeight + 5 * 2
buttonRadius: Appearance.rounding.full
colBackground: barRightSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarRightOpen
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
Behavior on colText {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
onPressed: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
RowLayout {
id: indicatorsRowLayout
anchors.centerIn: parent
property real realSpacing: 15
spacing: 0
Revealer {
reveal: Audio.sink?.audio?.muted ?? false
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
Behavior on Layout.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
MaterialSymbol {
text: "volume_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
Revealer {
reveal: Audio.source?.audio?.muted ?? false
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
Behavior on Layout.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
MaterialSymbol {
text: "mic_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
HyprlandXkbIndicator {
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: indicatorsRowLayout.realSpacing
color: rightSidebarButton.colText
}
Revealer {
reveal: Notifications.silent || Notifications.unread > 0
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
implicitHeight: reveal ? notificationUnreadCount.implicitHeight : 0
implicitWidth: reveal ? notificationUnreadCount.implicitWidth : 0
Behavior on Layout.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
NotificationUnreadCount {
id: notificationUnreadCount
}
}
MaterialSymbol {
Layout.rightMargin: indicatorsRowLayout.realSpacing
text: Network.materialSymbol
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
MaterialSymbol {
visible: BluetoothStatus.available
text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
}
SysTray {
visible: root.useShortenedForm === 0
Layout.fillWidth: false
Layout.fillHeight: true
invertSide: Config?.options.bar.bottom
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
// Weather
Loader {
Layout.leftMargin: 4
active: Config.options.bar.weather.enable
sourceComponent: BarGroup {
WeatherBar {}
}
}
}
}
}
@@ -0,0 +1,41 @@
import qs.modules.common
import QtQuick
import QtQuick.Layouts
Item {
id: root
property bool vertical: false
property real padding: 5
implicitWidth: vertical ? Appearance.sizes.baseVerticalBarWidth : (gridLayout.implicitWidth + padding * 2)
implicitHeight: vertical ? (gridLayout.implicitHeight + padding * 2) : Appearance.sizes.baseBarHeight
default property alias items: gridLayout.children
Rectangle {
id: background
anchors {
fill: parent
topMargin: root.vertical ? 0 : 4
bottomMargin: root.vertical ? 0 : 4
leftMargin: root.vertical ? 4 : 0
rightMargin: root.vertical ? 4 : 0
}
color: Config.options?.bar.borderless ? "transparent" : Appearance.colors.colLayer1
radius: Appearance.rounding.small
}
GridLayout {
id: gridLayout
columns: root.vertical ? 1 : -1
anchors {
verticalCenter: root.vertical ? undefined : parent.verticalCenter
horizontalCenter: root.vertical ? parent.horizontalCenter : undefined
left: root.vertical ? undefined : parent.left
right: root.vertical ? undefined : parent.right
top: root.vertical ? parent.top : undefined
bottom: root.vertical ? parent.bottom : undefined
margins: root.padding
}
columnSpacing: 4
rowSpacing: 12
}
}
@@ -0,0 +1,59 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
MouseArea {
id: root
property bool borderless: Config.options.bar.borderless
readonly property var chargeState: Battery.chargeState
readonly property bool isCharging: Battery.isCharging
readonly property bool isPluggedIn: Battery.isPluggedIn
readonly property real percentage: Battery.percentage
readonly property bool isLow: percentage <= Config.options.battery.low / 100
implicitWidth: batteryProgress.implicitWidth
implicitHeight: Appearance.sizes.barHeight
hoverEnabled: true
ClippedProgressBar {
id: batteryProgress
anchors.centerIn: parent
value: percentage
highlightColor: (isLow && !isCharging) ? Appearance.m3colors.m3error : Appearance.colors.colOnSecondaryContainer
Item {
anchors.centerIn: parent
width: batteryProgress.valueBarWidth
height: batteryProgress.valueBarHeight
RowLayout {
anchors.centerIn: parent
spacing: 0
MaterialSymbol {
id: boltIcon
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: -2
Layout.rightMargin: -2
fill: 1
text: "bolt"
iconSize: Appearance.font.pixelSize.smaller
visible: isCharging && percentage < 1 // TODO: animation
}
StyledText {
Layout.alignment: Qt.AlignVCenter
font: batteryProgress.font
text: batteryProgress.text
}
}
}
}
BatteryPopup {
id: batteryPopup
hoverTarget: root
}
}
@@ -0,0 +1,133 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
// Header
Row {
id: header
spacing: 5
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 0
font.weight: Font.Medium
text: "battery_android_full"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "Battery"
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
// This row is hidden when the battery is full.
RowLayout {
spacing: 5
Layout.fillWidth: true
property bool rowVisible: {
let timeValue = Battery.isCharging ? Battery.timeToFull : Battery.timeToEmpty;
let power = Battery.energyRate;
return !(Battery.chargeState == 4 || timeValue <= 0 || power <= 0.01);
}
visible: rowVisible
opacity: rowVisible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 500
}
}
MaterialSymbol {
text: "schedule"
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: Battery.isCharging ? Translation.tr("Time to full:") : Translation.tr("Time to empty:")
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
color: Appearance.colors.colOnSurfaceVariant
text: {
function formatTime(seconds) {
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
if (h > 0)
return `${h}h, ${m}m`;
else
return `${m}m`;
}
if (Battery.isCharging)
return formatTime(Battery.timeToFull);
else
return formatTime(Battery.timeToEmpty);
}
}
}
RowLayout {
spacing: 5
Layout.fillWidth: true
property bool rowVisible: !(Battery.chargeState != 4 && Battery.energyRate == 0)
visible: rowVisible
opacity: rowVisible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 500
}
}
MaterialSymbol {
text: "bolt"
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: {
if (Battery.chargeState == 4) {
return Translation.tr("Fully charged");
} else if (Battery.chargeState == 1) {
return Translation.tr("Charging:");
} else {
return Translation.tr("Discharging:");
}
}
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
color: Appearance.colors.colOnSurfaceVariant
text: {
if (Battery.chargeState == 4) {
return "";
} else {
return `${Battery.energyRate.toFixed(2)}W`;
}
}
}
}
}
}
@@ -0,0 +1,15 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: button
required default property Item content
property bool extraActiveCondition: false
implicitHeight: Math.max(content.implicitHeight, 26, content.implicitHeight)
implicitWidth: implicitHeight
contentItem: content
}
@@ -0,0 +1,50 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
Item {
id: root
property bool borderless: Config.options.bar.borderless
property bool showDate: Config.options.bar.verbose
implicitWidth: rowLayout.implicitWidth
implicitHeight: Appearance.sizes.barHeight
RowLayout {
id: rowLayout
anchors.centerIn: parent
spacing: 4
StyledText {
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
text: DateTime.time
}
StyledText {
visible: root.showDate
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
text: "•"
}
StyledText {
visible: root.showDate
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
text: DateTime.date
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
ClockWidgetTooltip {
hoverTarget: mouseArea
}
}
}
@@ -0,0 +1,110 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
property string formattedDate: Qt.locale().toString(DateTime.clock.date, "dddd, MMMM dd, yyyy")
property string formattedTime: DateTime.time
property string formattedUptime: DateTime.uptime
property string todosSection: getUpcomingTodos()
function getUpcomingTodos() {
const unfinishedTodos = Todo.list.filter(function (item) {
return !item.done;
});
if (unfinishedTodos.length === 0) {
return Translation.tr("No pending tasks");
}
// Limit to first 5 todos to keep popup manageable
const limitedTodos = unfinishedTodos.slice(0, 5);
let todoText = limitedTodos.map(function (item, index) {
return `${index + 1}. ${item.content}`;
}).join('\n');
if (unfinishedTodos.length > 5) {
todoText += `\n${Translation.tr("... and %1 more").arg(unfinishedTodos.length - 5)}`;
}
return todoText;
}
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
// Date + Time row
Row {
spacing: 5
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 0
font.weight: Font.Medium
text: "calendar_month"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
color: Appearance.colors.colOnSurfaceVariant
text: `${root.formattedDate}`
font.weight: Font.Medium
}
}
// Uptime row
RowLayout {
spacing: 5
Layout.fillWidth: true
MaterialSymbol {
text: "timelapse"
color: Appearance.colors.colOnSurfaceVariant
font.pixelSize: Appearance.font.pixelSize.large
}
StyledText {
text: Translation.tr("System uptime:")
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
color: Appearance.colors.colOnSurfaceVariant
text: root.formattedUptime
}
}
// Tasks
Column {
spacing: 0
Layout.fillWidth: true
Row {
spacing: 4
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
text: "checklist"
color: Appearance.colors.colOnSurfaceVariant
font.pixelSize: Appearance.font.pixelSize.large
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Translation.tr("To Do:")
color: Appearance.colors.colOnSurfaceVariant
}
}
StyledText {
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
color: Appearance.colors.colOnSurfaceVariant
text: root.todosSection
}
}
}
}
@@ -0,0 +1,34 @@
import QtQuick
import qs.services
import qs.modules.common
import qs.modules.common.widgets
Loader {
id: root
property bool vertical: false
property color color: Appearance.colors.colOnSurfaceVariant
active: HyprlandXkb.layoutCodes.length > 1
visible: active
function abbreviateLayoutCode(fullCode) {
return fullCode.split(':').map(layout => {
const baseLayout = layout.split('-')[0];
return baseLayout.slice(0, 4);
}).join('\n');
}
sourceComponent: Item {
implicitWidth: root.vertical ? null : layoutCodeText.implicitWidth
implicitHeight: root.vertical ? layoutCodeText.implicitHeight : null
StyledText {
id: layoutCodeText
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: abbreviateLayoutCode(HyprlandXkb.currentLayoutCode)
font.pixelSize: text.includes("\n") ? Appearance.font.pixelSize.smallie : Appearance.font.pixelSize.small
color: root.color
animateChange: true
}
}
}
@@ -0,0 +1,78 @@
import QtQuick
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
RippleButton {
id: root
property bool showPing: false
property real buttonPadding: 5
implicitWidth: distroIcon.width + buttonPadding * 2
implicitHeight: distroIcon.height + buttonPadding * 2
buttonRadius: Appearance.rounding.full
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarLeftOpen
onPressed: {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
Connections {
target: Ai
function onResponseFinished() {
if (GlobalStates.sidebarLeftOpen) return;
root.showPing = true;
}
}
Connections {
target: Booru
function onResponseFinished() {
if (GlobalStates.sidebarLeftOpen) return;
root.showPing = true;
}
}
Connections {
target: GlobalStates
function onSidebarLeftOpenChanged() {
root.showPing = false;
}
}
CustomIcon {
id: distroIcon
anchors.centerIn: parent
width: 19.5
height: 19.5
source: Config.options.bar.topLeftIcon == 'distro' ? SystemInfo.distroIcon : `${Config.options.bar.topLeftIcon}-symbolic`
colorize: true
color: Appearance.colors.colOnLayer0
Rectangle {
opacity: root.showPing ? 1 : 0
visible: opacity > 0
anchors {
bottom: parent.bottom
right: parent.right
bottomMargin: -2
rightMargin: -2
}
implicitWidth: 8
implicitHeight: 8
radius: Appearance.rounding.full
color: Appearance.colors.colTertiary
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
}
}
@@ -0,0 +1,89 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Mpris
import Quickshell.Hyprland
Item {
id: root
property bool borderless: Config.options.bar.borderless
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property string cleanedTitle: StringUtils.cleanMusicTitle(activePlayer?.trackTitle) || Translation.tr("No media")
Layout.fillHeight: true
implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
implicitHeight: Appearance.sizes.barHeight
Timer {
running: activePlayer?.playbackState == MprisPlaybackState.Playing
interval: Config.options.resources.updateInterval
repeat: true
onTriggered: activePlayer.positionChanged()
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton | Qt.RightButton | Qt.LeftButton
onPressed: (event) => {
if (event.button === Qt.MiddleButton) {
activePlayer.togglePlaying();
} else if (event.button === Qt.BackButton) {
activePlayer.previous();
} else if (event.button === Qt.ForwardButton || event.button === Qt.RightButton) {
activePlayer.next();
} else if (event.button === Qt.LeftButton) {
GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen
}
}
}
RowLayout { // Real content
id: rowLayout
spacing: 4
anchors.fill: parent
ClippedFilledCircularProgress {
id: mediaCircProg
Layout.alignment: Qt.AlignVCenter
lineWidth: Appearance.rounding.unsharpen
value: activePlayer?.position / activePlayer?.length
implicitSize: 20
colPrimary: Appearance.colors.colOnSecondaryContainer
enableAnimation: false
Item {
anchors.centerIn: parent
width: mediaCircProg.implicitSize
height: mediaCircProg.implicitSize
MaterialSymbol {
anchors.centerIn: parent
fill: 1
text: activePlayer?.isPlaying ? "pause" : "music_note"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onSecondaryContainer
}
}
}
StyledText {
visible: Config.options.bar.verbose
width: rowLayout.width - (CircularProgress.size + rowLayout.spacing * 2)
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true // Ensures the text takes up available space
Layout.rightMargin: rowLayout.spacing
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight // Truncates the text on the right
color: Appearance.colors.colOnLayer1
text: `${cleanedTitle}${activePlayer?.trackArtist ? ' • ' + activePlayer.trackArtist : ''}`
}
}
}
@@ -0,0 +1,38 @@
import QtQuick
import qs.services
import qs.modules.common
import qs.modules.common.widgets
MaterialSymbol {
id: root
readonly property bool showUnreadCount: Config.options.bar.indicators.notifications.showUnreadCount
text: Notifications.silent ? "notifications_paused" : "notifications"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
Rectangle {
id: notifPing
visible: !Notifications.silent && Notifications.unread > 0
anchors {
right: parent.right
top: parent.top
rightMargin: root.showUnreadCount ? 0 : 1
topMargin: root.showUnreadCount ? 0 : 3
}
radius: Appearance.rounding.full
color: Appearance.colors.colOnLayer0
z: 1
implicitHeight: root.showUnreadCount ? Math.max(notificationCounterText.implicitWidth, notificationCounterText.implicitHeight) : 8
implicitWidth: implicitHeight
StyledText {
id: notificationCounterText
visible: root.showUnreadCount
anchors.centerIn: parent
font.pixelSize: Appearance.font.pixelSize.smallest
color: Appearance.colors.colLayer0
text: Notifications.unread
}
}
}
@@ -0,0 +1,92 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property string iconName
required property double percentage
property int warningThreshold: 100
property bool shown: true
clip: true
visible: width > 0 && height > 0
implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth
implicitHeight: Appearance.sizes.barHeight
property bool warning: percentage * 100 >= warningThreshold
RowLayout {
id: resourceRowLayout
spacing: 2
x: shown ? 0 : -resourceRowLayout.width
anchors {
verticalCenter: parent.verticalCenter
}
ClippedFilledCircularProgress {
id: resourceCircProg
Layout.alignment: Qt.AlignVCenter
lineWidth: Appearance.rounding.unsharpen
value: percentage
implicitSize: 20
colPrimary: root.warning ? Appearance.colors.colError : Appearance.colors.colOnSecondaryContainer
accountForLightBleeding: !root.warning
enableAnimation: false
Item {
anchors.centerIn: parent
width: resourceCircProg.implicitSize
height: resourceCircProg.implicitSize
MaterialSymbol {
anchors.centerIn: parent
font.weight: Font.DemiBold
fill: 1
text: iconName
iconSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onSecondaryContainer
}
}
}
Item {
Layout.alignment: Qt.AlignVCenter
implicitWidth: fullPercentageTextMetrics.width
implicitHeight: percentageText.implicitHeight
TextMetrics {
id: fullPercentageTextMetrics
text: "100"
font.pixelSize: Appearance.font.pixelSize.small
}
StyledText {
id: percentageText
anchors.centerIn: parent
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
text: `${Math.round(percentage * 100).toString()}`
}
}
Behavior on x {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
enabled: resourceRowLayout.x >= 0 && root.width > 0 && root.visible
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
}
@@ -0,0 +1,53 @@
import qs.modules.common
import qs.services
import QtQuick
import QtQuick.Layouts
MouseArea {
id: root
property bool borderless: Config.options.bar.borderless
property bool alwaysShowAllResources: false
implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin
implicitHeight: Appearance.sizes.barHeight
hoverEnabled: true
RowLayout {
id: rowLayout
spacing: 0
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
Resource {
iconName: "memory"
percentage: ResourceUsage.memoryUsedPercentage
warningThreshold: Config.options.bar.resources.memoryWarningThreshold
}
Resource {
iconName: "swap_horiz"
percentage: ResourceUsage.swapUsedPercentage
shown: (Config.options.bar.resources.alwaysShowSwap && percentage > 0) ||
(MprisController.activePlayer?.trackTitle == null) ||
root.alwaysShowAllResources
Layout.leftMargin: shown ? 6 : 0
warningThreshold: Config.options.bar.resources.swapWarningThreshold
}
Resource {
iconName: "planner_review"
percentage: ResourceUsage.cpuUsage
shown: Config.options.bar.resources.alwaysShowCpu ||
!(MprisController.activePlayer?.trackTitle?.length > 0) ||
root.alwaysShowAllResources
Layout.leftMargin: shown ? 6 : 0
warningThreshold: Config.options.bar.resources.cpuWarningThreshold
}
}
ResourcesPopup {
hoverTarget: root
}
}
@@ -0,0 +1,145 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
// Helper function to format KB to GB
function formatKB(kb) {
return (kb / (1024 * 1024)).toFixed(1) + " GB";
}
component ResourceItem: RowLayout {
id: resourceItem
required property string icon
required property string label
required property string value
spacing: 4
MaterialSymbol {
text: resourceItem.icon
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: resourceItem.label
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
visible: resourceItem.value !== ""
color: Appearance.colors.colOnSurfaceVariant
text: resourceItem.value
}
}
component ResourceHeaderItem: Row {
id: headerItem
required property var icon
required property var label
spacing: 5
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 0
font.weight: Font.Medium
text: headerItem.icon
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: headerItem.label
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
Row {
anchors.centerIn: parent
spacing: 12
Column {
anchors.top: parent.top
spacing: 8
ResourceHeaderItem {
icon: "memory"
label: "RAM"
}
Column {
spacing: 4
ResourceItem {
icon: "clock_loader_60"
label: Translation.tr("Used:")
value: formatKB(ResourceUsage.memoryUsed)
}
ResourceItem {
icon: "check_circle"
label: Translation.tr("Free:")
value: formatKB(ResourceUsage.memoryFree)
}
ResourceItem {
icon: "empty_dashboard"
label: Translation.tr("Total:")
value: formatKB(ResourceUsage.memoryTotal)
}
}
}
Column {
visible: ResourceUsage.swapTotal > 0
anchors.top: parent.top
spacing: 8
ResourceHeaderItem {
icon: "swap_horiz"
label: "Swap"
}
Column {
spacing: 4
ResourceItem {
icon: "clock_loader_60"
label: Translation.tr("Used:")
value: formatKB(ResourceUsage.swapUsed)
}
ResourceItem {
icon: "check_circle"
label: Translation.tr("Free:")
value: formatKB(ResourceUsage.swapFree)
}
ResourceItem {
icon: "empty_dashboard"
label: Translation.tr("Total:")
value: formatKB(ResourceUsage.swapTotal)
}
}
}
Column {
anchors.top: parent.top
spacing: 8
ResourceHeaderItem {
icon: "planner_review"
label: "CPU"
}
Column {
spacing: 4
ResourceItem {
icon: "bolt"
label: Translation.tr("Load:")
value: (ResourceUsage.cpuUsage > 0.8 ? Translation.tr("High") : ResourceUsage.cpuUsage > 0.4 ? Translation.tr("Medium") : Translation.tr("Low")) + ` (${Math.round(ResourceUsage.cpuUsage * 100)}%)`
}
}
}
}
}
@@ -0,0 +1,60 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Revealer { // Scroll hint
id: root
property string icon
property string side: "left"
property string tooltipText: ""
MouseArea {
id: mouseArea
anchors.right: root.side === "left" ? parent.right : undefined
anchors.left: root.side === "right" ? parent.left : undefined
implicitWidth: contentColumn.implicitWidth
implicitHeight: contentColumn.implicitHeight
property bool hovered: false
hoverEnabled: true
onEntered: hovered = true
onExited: hovered = false
acceptedButtons: Qt.NoButton
property bool showHintTimedOut: false
onHoveredChanged: showHintTimedOut = false
Timer {
running: mouseArea.hovered
interval: 500
onTriggered: mouseArea.showHintTimedOut = true
}
PopupToolTip {
extraVisibleCondition: (tooltipText.length > 0 && mouseArea.showHintTimedOut)
text: tooltipText
}
Column {
id: contentColumn
anchors {
fill: parent
}
spacing: -5
MaterialSymbol {
text: "keyboard_arrow_up"
iconSize: 14
color: Appearance.colors.colSubtext
}
MaterialSymbol {
text: root.icon
iconSize: 14
color: Appearance.colors.colSubtext
}
MaterialSymbol {
text: "keyboard_arrow_down"
iconSize: 14
color: Appearance.colors.colSubtext
}
}
}
}
@@ -0,0 +1,81 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
LazyLoader {
id: root
property Item hoverTarget
default property Item contentItem
property real popupBackgroundMargin: 0
active: hoverTarget && hoverTarget.containsMouse
component: PanelWindow {
id: popupWindow
color: "transparent"
anchors.left: !Config.options.bar.vertical || (Config.options.bar.vertical && !Config.options.bar.bottom)
anchors.right: Config.options.bar.vertical && Config.options.bar.bottom
anchors.top: Config.options.bar.vertical || (!Config.options.bar.vertical && !Config.options.bar.bottom)
anchors.bottom: !Config.options.bar.vertical && Config.options.bar.bottom
implicitWidth: popupBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 + root.popupBackgroundMargin
implicitHeight: popupBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + root.popupBackgroundMargin
mask: Region {
item: popupBackground
}
exclusionMode: ExclusionMode.Ignore
exclusiveZone: 0
margins {
left: {
if (!Config.options.bar.vertical) return root.QsWindow?.mapFromItem(
root.hoverTarget,
(root.hoverTarget.width - popupBackground.implicitWidth) / 2, 0
).x;
return Appearance.sizes.verticalBarWidth
}
top: {
if (!Config.options.bar.vertical) return Appearance.sizes.barHeight;
return root.QsWindow?.mapFromItem(
root.hoverTarget,
(root.hoverTarget.height - popupBackground.implicitHeight) / 2, 0
).y;
}
right: Appearance.sizes.verticalBarWidth
bottom: Appearance.sizes.barHeight
}
WlrLayershell.namespace: "quickshell:popup"
WlrLayershell.layer: WlrLayer.Overlay
StyledRectangularShadow {
target: popupBackground
}
Rectangle {
id: popupBackground
readonly property real margin: 10
anchors {
fill: parent
leftMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.left)
rightMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.right)
topMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.top)
bottomMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.bottom)
}
implicitWidth: root.contentItem.implicitWidth + margin * 2
implicitHeight: root.contentItem.implicitHeight + margin * 2
color: ColorUtils.applyAlpha(Appearance.colors.colSurfaceContainer, 1 - Appearance.backgroundTransparency)
radius: Appearance.rounding.small
children: [root.contentItem]
border.width: 1
border.color: Appearance.colors.colLayer0Border
}
}
}
@@ -0,0 +1,155 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.SystemTray
Item {
id: root
implicitWidth: gridLayout.implicitWidth
implicitHeight: gridLayout.implicitHeight
property bool vertical: false
property bool invertSide: false
property bool trayOverflowOpen: false
property bool showSeparator: true
property bool showOverflowMenu: true
property var activeMenu: null
property list<var> itemsInUserList: SystemTray.items.values.filter(i => (Config.options.bar.tray.pinnedItems.includes(i.id) && i.status !== Status.Passive))
property list<var> itemsNotInUserList: SystemTray.items.values.filter(i => (!Config.options.bar.tray.pinnedItems.includes(i.id) && i.status !== Status.Passive))
property bool invertPins: Config.options.bar.tray.invertPinnedItems
property list<var> pinnedItems: invertPins ? itemsNotInUserList : itemsInUserList
property list<var> unpinnedItems: invertPins ? itemsInUserList : itemsNotInUserList
onUnpinnedItemsChanged: {
if (unpinnedItems.length == 0) root.closeOverflowMenu();
}
function grabFocus() {
focusGrab.active = true;
}
function setExtraWindowAndGrabFocus(window) {
root.activeMenu = window;
root.grabFocus();
}
function releaseFocus() {
focusGrab.active = false;
}
function closeOverflowMenu() {
focusGrab.active = false;
}
onTrayOverflowOpenChanged: {
if (root.trayOverflowOpen) {
root.grabFocus();
}
}
HyprlandFocusGrab {
id: focusGrab
active: false
windows: [trayOverflowLayout.QsWindow?.window, root.activeMenu]
onCleared: {
root.trayOverflowOpen = false;
if (root.activeMenu) {
root.activeMenu.close();
root.activeMenu = null;
}
}
}
GridLayout {
id: gridLayout
columns: root.vertical ? 1 : -1
anchors.fill: parent
rowSpacing: 8
columnSpacing: 15
RippleButton {
id: trayOverflowButton
visible: root.showOverflowMenu && root.unpinnedItems.length > 0
toggled: root.trayOverflowOpen
property bool containsMouse: hovered
downAction: () => root.trayOverflowOpen = !root.trayOverflowOpen
Layout.fillHeight: !root.vertical
Layout.fillWidth: root.vertical
background.implicitWidth: 24
background.implicitHeight: 24
background.anchors.centerIn: this
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
contentItem: MaterialSymbol {
anchors.centerIn: parent
iconSize: Appearance.font.pixelSize.larger
text: "expand_more"
horizontalAlignment: Text.AlignHCenter
color: root.trayOverflowOpen ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer2
rotation: (root.trayOverflowOpen ? 180 : 0) - (90 * root.vertical) + (180 * root.invertSide)
Behavior on rotation {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
StyledPopup {
id: overflowPopup
hoverTarget: trayOverflowButton
active: root.trayOverflowOpen && root.unpinnedItems.length > 0
popupBackgroundMargin: 300 // This should be plenty... makes sure tooltips don't get cutoff (easily)
GridLayout {
id: trayOverflowLayout
anchors.centerIn: parent
columns: Math.ceil(Math.sqrt(root.unpinnedItems.length))
columnSpacing: 10
rowSpacing: 10
Repeater {
model: root.unpinnedItems
delegate: SysTrayItem {
required property SystemTrayItem modelData
item: modelData
Layout.fillHeight: !root.vertical
Layout.fillWidth: root.vertical
onMenuClosed: root.releaseFocus();
onMenuOpened: (qsWindow) => root.setExtraWindowAndGrabFocus(qsWindow);
}
}
}
}
}
Repeater {
model: ScriptModel {
values: root.pinnedItems
}
delegate: SysTrayItem {
required property SystemTrayItem modelData
item: modelData
Layout.fillHeight: !root.vertical
Layout.fillWidth: root.vertical
onMenuClosed: root.releaseFocus();
onMenuOpened: (qsWindow) => {
root.setExtraWindowAndGrabFocus(qsWindow);
}
}
}
StyledText {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colSubtext
text: "•"
visible: root.showSeparator && SystemTray.items.values.length > 0
}
}
}
@@ -0,0 +1,101 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import Qt5Compat.GraphicalEffects
MouseArea {
id: root
required property SystemTrayItem item
property bool targetMenuOpen: false
signal menuOpened(qsWindow: var)
signal menuClosed()
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
implicitWidth: 20
implicitHeight: 20
onPressed: (event) => {
switch (event.button) {
case Qt.LeftButton:
item.activate();
break;
case Qt.RightButton:
if (item.hasMenu) menu.open();
break;
}
event.accepted = true;
}
onEntered: {
tooltip.text = item.tooltipTitle.length > 0 ? item.tooltipTitle
: (item.title.length > 0 ? item.title : item.id);
if (item.tooltipDescription.length > 0) tooltip.text += " • " + item.tooltipDescription;
if (Config.options.bar.tray.showItemId) tooltip.text += "\n[" + item.id + "]";
}
Loader {
id: menu
function open() {
menu.active = true;
}
active: false
sourceComponent: SysTrayMenu {
Component.onCompleted: this.open();
trayItemMenuHandle: root.item.menu
anchor {
window: root.QsWindow.window
rect.x: root.x + (Config.options.bar.vertical ? 0 : QsWindow.window?.width)
rect.y: root.y + (Config.options.bar.vertical ? QsWindow.window?.height : 0)
rect.height: root.height
rect.width: root.width
edges: Config.options.bar.bottom ? (Edges.Top | Edges.Left) : (Edges.Bottom | Edges.Right)
gravity: Config.options.bar.bottom ? (Edges.Top | Edges.Left) : (Edges.Bottom | Edges.Right)
}
onMenuOpened: (window) => root.menuOpened(window);
onMenuClosed: {
root.menuClosed();
menu.active = false;
}
}
}
IconImage {
id: trayIcon
visible: !Config.options.bar.tray.monochromeIcons
source: root.item.icon
anchors.centerIn: parent
width: parent.width
height: parent.height
}
Loader {
active: Config.options.bar.tray.monochromeIcons
anchors.fill: trayIcon
sourceComponent: Item {
Desaturate {
id: desaturatedIcon
visible: false // There's already color overlay
anchors.fill: parent
source: trayIcon
desaturation: 0.8 // 1.0 means fully grayscale
}
ColorOverlay {
anchors.fill: desaturatedIcon
source: desaturatedIcon
color: ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.9)
}
}
}
PopupToolTip {
id: tooltip
extraVisibleCondition: root.containsMouse
alternativeVisibleCondition: extraVisibleCondition
anchorEdges: (!Config.options.bar.bottom && !Config.options.bar.vertical) ? Edges.Bottom : Edges.Top
}
}
@@ -0,0 +1,217 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
PopupWindow {
id: root
required property QsMenuHandle trayItemMenuHandle
property real popupBackgroundMargin: 0
signal menuClosed
signal menuOpened(qsWindow: var) // Correct type is QsWindow, but QML does not like that
color: "transparent"
property real padding: Appearance.sizes.elevationMargin
implicitHeight: {
let result = 0;
for (let child of stackView.children) {
result = Math.max(child.implicitHeight, result);
}
return result + popupBackground.padding * 2 + root.padding * 2;
}
implicitWidth: {
let result = 0;
for (let child of stackView.children) {
result = Math.max(child.implicitWidth, result);
}
return result + popupBackground.padding * 2 + root.padding * 2;
}
function open() {
root.visible = true;
root.menuOpened(root);
}
function close() {
root.visible = false;
while (stackView.depth > 1)
stackView.pop();
root.menuClosed();
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.BackButton | Qt.RightButton
onPressed: event => {
if ((event.button === Qt.BackButton || event.button === Qt.RightButton) && stackView.depth > 1)
stackView.pop();
}
StyledRectangularShadow {
target: popupBackground
opacity: popupBackground.opacity
}
Rectangle {
id: popupBackground
readonly property real padding: 4
anchors {
left: parent.left
right: parent.right
verticalCenter: Config.options.bar.vertical ? parent.verticalCenter : undefined
top: Config.options.bar.vertical ? undefined : Config.options.bar.bottom ? undefined : parent.top
bottom: Config.options.bar.vertical ? undefined : Config.options.bar.bottom ? parent.bottom : undefined
margins: root.padding
}
color: Appearance.colors.colLayer0
radius: Appearance.rounding.windowRounding
border.width: 1
border.color: Appearance.colors.colLayer0Border
clip: true
opacity: 0
Component.onCompleted: opacity = 1
implicitWidth: stackView.implicitWidth + popupBackground.padding * 2
implicitHeight: stackView.implicitHeight + popupBackground.padding * 2
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on implicitWidth {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
StackView {
id: stackView
anchors {
fill: parent
margins: popupBackground.padding
}
pushEnter: NoAnim {}
pushExit: NoAnim {}
popEnter: NoAnim {}
popExit: NoAnim {}
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
initialItem: SubMenu {
handle: root.trayItemMenuHandle
}
}
}
}
component NoAnim: Transition {
NumberAnimation {
duration: 0
}
}
component SubMenu: ColumnLayout {
id: submenu
required property QsMenuHandle handle
property bool isSubMenu: false
property bool shown: false
opacity: shown ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Component.onCompleted: shown = true
StackView.onActivating: shown = true
StackView.onDeactivating: shown = false
StackView.onRemoved: destroy()
QsMenuOpener {
id: menuOpener
menu: submenu.handle
}
spacing: 0
Loader {
Layout.fillWidth: true
visible: submenu.isSubMenu
active: visible
sourceComponent: RippleButton {
id: backButton
buttonRadius: popupBackground.radius - popupBackground.padding
horizontalPadding: 12
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
implicitHeight: 36
downAction: () => stackView.pop()
contentItem: RowLayout {
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: parent.right
leftMargin: backButton.horizontalPadding
rightMargin: backButton.horizontalPadding
}
spacing: 8
MaterialSymbol {
iconSize: 20
text: "chevron_left"
}
StyledText {
Layout.fillWidth: true
text: Translation.tr("Back")
}
}
}
}
Repeater {
id: menuEntriesRepeater
property bool iconColumnNeeded: {
for (let i = 0; i < menuOpener.children.values.length; i++) {
if (menuOpener.children.values[i].icon.length > 0)
return true;
}
return false;
}
property bool specialInteractionColumnNeeded: {
for (let i = 0; i < menuOpener.children.values.length; i++) {
if (menuOpener.children.values[i].buttonType !== QsMenuButtonType.None)
return true;
}
return false;
}
model: menuOpener.children
delegate: SysTrayMenuEntry {
required property QsMenuEntry modelData
forceIconColumn: menuEntriesRepeater.iconColumnNeeded
forceSpecialInteractionColumn: menuEntriesRepeater.specialInteractionColumnNeeded
menuEntry: modelData
buttonRadius: popupBackground.radius - popupBackground.padding
onDismiss: root.close()
onOpenSubmenu: handle => {
stackView.push(subMenuComponent.createObject(null, {
handle: handle,
isSubMenu: true
}));
}
}
}
}
Component {
id: subMenuComponent
SubMenu {}
}
}
@@ -0,0 +1,126 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
RippleButton {
id: root
required property QsMenuEntry menuEntry
property bool forceIconColumn: false
property bool forceSpecialInteractionColumn: false
readonly property bool hasIcon: menuEntry.icon.length > 0
readonly property bool hasSpecialInteraction: menuEntry.buttonType !== QsMenuButtonType.None
signal dismiss()
signal openSubmenu(handle: QsMenuHandle)
colBackground: menuEntry.isSeparator ? Appearance.m3colors.m3outlineVariant : ColorUtils.transparentize(Appearance.colors.colLayer0)
enabled: !menuEntry.isSeparator
opacity: 1
horizontalPadding: 12
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
implicitHeight: menuEntry.isSeparator ? 1 : 36
Layout.topMargin: menuEntry.isSeparator ? 4 : 0
Layout.bottomMargin: menuEntry.isSeparator ? 4 : 0
Layout.fillWidth: true
Component.onCompleted: {
if (menuEntry.isSeparator) {
root.buttonColor = root.colBackground;
}
}
releaseAction: () => {
if (menuEntry.hasChildren) {
root.openSubmenu(root.menuEntry);
return;
}
menuEntry.triggered();
root.dismiss();
}
altAction: (event) => { // Not hog right-click
event.accepted = false;
}
contentItem: RowLayout {
id: contentItem
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: parent.right
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
}
spacing: 8
visible: !root.menuEntry.isSeparator
// Interaction: checkbox or radio button
Item {
visible: root.hasSpecialInteraction || root.forceSpecialInteractionColumn
implicitWidth: 20
implicitHeight: 20
Loader {
anchors.fill: parent
active: root.menuEntry.buttonType === QsMenuButtonType.RadioButton
sourceComponent: StyledRadioButton {
enabled: false
padding: 0
checked: root.menuEntry.checkState === Qt.Checked
}
}
Loader {
anchors.fill: parent
active: root.menuEntry.buttonType === QsMenuButtonType.CheckBox && root.menuEntry.checkState !== Qt.Unchecked
sourceComponent: MaterialSymbol {
text: root.menuEntry.checkState === Qt.PartiallyChecked ? "check_indeterminate_small" : "check"
iconSize: 20
}
}
}
// Button icon
Item {
visible: root.hasIcon || root.forceIconColumn
implicitWidth: 20
implicitHeight: 20
Loader {
anchors.centerIn: parent
active: root.menuEntry.icon.length > 0
sourceComponent: IconImage {
asynchronous: true
source: root.menuEntry.icon
implicitSize: 20
mipmap: true
}
}
}
StyledText {
id: label
text: root.menuEntry.text
font.pixelSize: Appearance.font.pixelSize.smallie
Layout.fillWidth: true
}
Loader {
active: root.menuEntry.hasChildren
sourceComponent: MaterialSymbol {
text: "chevron_right"
iconSize: 20
}
}
}
}
@@ -0,0 +1,142 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Pipewire
import Quickshell.Services.UPower
Item {
id: root
property bool borderless: Config.options.bar.borderless
implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
implicitHeight: rowLayout.implicitHeight
RowLayout {
id: rowLayout
spacing: 4
anchors.centerIn: parent
Loader {
active: Config.options.bar.utilButtons.showScreenSnip
visible: Config.options.bar.utilButtons.showScreenSnip
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")])
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 1
text: "screenshot_region"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showColorPicker
visible: Config.options.bar.utilButtons.showColorPicker
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["hyprpicker", "-a"])
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 1
text: "colorize"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showKeyboardToggle
visible: Config.options.bar.utilButtons.showKeyboardToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: GlobalStates.oskOpen = !GlobalStates.oskOpen
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 0
text: "keyboard"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showMicToggle
visible: Config.options.bar.utilButtons.showMicToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_SOURCE@", "toggle"])
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 0
text: Pipewire.defaultAudioSource?.audio?.muted ? "mic_off" : "mic"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showDarkModeToggle
visible: Config.options.bar.utilButtons.showDarkModeToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: event => {
if (Appearance.m3colors.darkmode) {
Hyprland.dispatch(`exec ${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`);
} else {
Hyprland.dispatch(`exec ${Directories.wallpaperSwitchScriptPath} --mode dark --noswitch`);
}
}
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 0
text: Appearance.m3colors.darkmode ? "light_mode" : "dark_mode"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showPerformanceProfileToggle
visible: Config.options.bar.utilButtons.showPerformanceProfileToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: event => {
if (PowerProfiles.hasPerformanceProfile) {
switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: PowerProfiles.profile = PowerProfile.Balanced
break;
case PowerProfile.Balanced: PowerProfiles.profile = PowerProfile.Performance
break;
case PowerProfile.Performance: PowerProfiles.profile = PowerProfile.PowerSaver
break;
}
} else {
PowerProfiles.profile = PowerProfiles.profile == PowerProfile.Balanced ? PowerProfile.PowerSaver : PowerProfile.Balanced
}
}
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 0
text: switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "energy_savings_leaf"
case PowerProfile.Balanced: return "settings_slow_motion"
case PowerProfile.Performance: return "local_fire_department"
}
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
}
}
@@ -0,0 +1,330 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Widgets
import Qt5Compat.GraphicalEffects
Item {
id: root
property bool vertical: false
property bool borderless: Config.options.bar.borderless
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
readonly property int workspacesShown: Config.options.bar.workspaces.shown
readonly property int workspaceGroup: Math.floor((monitor?.activeWorkspace?.id - 1) / root.workspacesShown)
property list<bool> workspaceOccupied: []
property int widgetPadding: 4
property int workspaceButtonWidth: 26
property real activeWorkspaceMargin: 2
property real workspaceIconSize: workspaceButtonWidth * 0.69
property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55
property real workspaceIconOpacityShrinked: 1
property real workspaceIconMarginShrinked: -4
property int workspaceIndexInGroup: (monitor?.activeWorkspace?.id - 1) % root.workspacesShown
property bool showNumbers: false
Timer {
id: showNumbersTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
root.showNumbers = true
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
if (GlobalStates.superDown) showNumbersTimer.restart();
else {
showNumbersTimer.stop();
root.showNumbers = false;
}
}
function onSuperReleaseMightTriggerChanged() {
showNumbersTimer.stop()
}
}
// Function to update workspaceOccupied
function updateWorkspaceOccupied() {
workspaceOccupied = Array.from({ length: root.workspacesShown }, (_, i) => {
return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * root.workspacesShown + i + 1);
})
}
// Occupied workspace updates
Component.onCompleted: updateWorkspaceOccupied()
Connections {
target: Hyprland.workspaces
function onValuesChanged() {
updateWorkspaceOccupied();
}
}
Connections {
target: Hyprland
function onFocusedWorkspaceChanged() {
updateWorkspaceOccupied();
}
}
onWorkspaceGroupChanged: {
updateWorkspaceOccupied();
}
implicitWidth: root.vertical ? Appearance.sizes.verticalBarWidth : (root.workspaceButtonWidth * root.workspacesShown)
implicitHeight: root.vertical ? (root.workspaceButtonWidth * root.workspacesShown) : Appearance.sizes.barHeight
// Scroll to switch workspaces
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0)
Hyprland.dispatch(`workspace r+1`);
else if (event.angleDelta.y > 0)
Hyprland.dispatch(`workspace r-1`);
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.BackButton
onPressed: (event) => {
if (event.button === Qt.BackButton) {
Hyprland.dispatch(`togglespecialworkspace`);
}
}
}
// Workspaces - background
Grid {
z: 1
anchors.centerIn: parent
rowSpacing: 0
columnSpacing: 0
columns: root.vertical ? 1 : root.workspacesShown
rows: root.vertical ? root.workspacesShown : 1
Repeater {
model: root.workspacesShown
Rectangle {
z: 1
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
radius: (width / 2)
property var previousOccupied: (workspaceOccupied[index-1] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index))
property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index+2))
property var radiusPrev: previousOccupied ? 0 : (width / 2)
property var radiusNext: rightOccupied ? 0 : (width / 2)
topLeftRadius: radiusPrev
bottomLeftRadius: root.vertical ? radiusNext : radiusPrev
topRightRadius: root.vertical ? radiusPrev : radiusNext
bottomRightRadius: radiusNext
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
opacity: (workspaceOccupied[index] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index+1)) ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on radiusPrev {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on radiusNext {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
}
// Active workspace
Rectangle {
z: 2
// Make active ws indicator, which has a brighter color, smaller to look like it is of the same size as ws occupied highlight
radius: Appearance.rounding.full
color: Appearance.colors.colPrimary
anchors {
verticalCenter: vertical ? undefined : parent.verticalCenter
horizontalCenter: vertical ? parent.horizontalCenter : undefined
}
// idx1 is the "leading" indicator position, idx2 is the "following" one
// The former animates faster than the latter, see the NumberAnimations below
property real idx1: workspaceIndexInGroup
property real idx2: workspaceIndexInGroup
property real indicatorPosition: Math.min(idx1, idx2) * workspaceButtonWidth + root.activeWorkspaceMargin
property real indicatorLength: Math.abs(idx1 - idx2) * workspaceButtonWidth + workspaceButtonWidth - root.activeWorkspaceMargin * 2
property real indicatorThickness: workspaceButtonWidth - root.activeWorkspaceMargin * 2
x: root.vertical ? null : indicatorPosition
implicitWidth: root.vertical ? indicatorThickness : indicatorLength
y: root.vertical ? indicatorPosition : null
implicitHeight: root.vertical ? indicatorLength : indicatorThickness
Behavior on idx1 {
NumberAnimation {
duration: 100
easing.type: Easing.OutSine
}
}
Behavior on idx2 {
NumberAnimation {
duration: 300
easing.type: Easing.OutSine
}
}
}
// Workspaces - numbers
Grid {
z: 3
columns: root.vertical ? 1 : root.workspacesShown
rows: root.vertical ? root.workspacesShown : 1
columnSpacing: 0
rowSpacing: 0
anchors.fill: parent
Repeater {
model: root.workspacesShown
Button {
id: button
property int workspaceValue: workspaceGroup * root.workspacesShown + index + 1
implicitHeight: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.barHeight
implicitWidth: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.verticalBarWidth
onPressed: Hyprland.dispatch(`workspace ${workspaceValue}`)
width: vertical ? undefined : workspaceButtonWidth
height: vertical ? workspaceButtonWidth : undefined
background: Item {
id: workspaceButtonBackground
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(button.workspaceValue)
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
StyledText { // Workspace number text
opacity: root.showNumbers
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || root.showNumbers))
|| (root.showNumbers && !Config.options?.bar.workspaces.showAppIcons)
) ? 1 : 0
z: 3
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font {
pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
family: Config.options?.bar.workspaces.useNerdFont ? Appearance.font.family.iconNerd : Appearance.font.family.main
}
text: Config.options?.bar.workspaces.numberMap[button.workspaceValue - 1] || button.workspaceValue
elide: Text.ElideRight
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Rectangle { // Dot instead of ws number
id: wsDot
opacity: (Config.options?.bar.workspaces.alwaysShowNumbers
|| root.showNumbers
|| (Config.options?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow)
) ? 0 : 1
visible: opacity > 0
anchors.centerIn: parent
width: workspaceButtonWidth * 0.18
height: width
radius: width / 2
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Item { // Main app icon
anchors.centerIn: parent
width: workspaceButtonWidth
height: workspaceButtonWidth
opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 :
(workspaceButtonBackground.biggestWindow && !root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0
visible: opacity > 0
IconImage {
id: mainAppIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.bottomMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
anchors.rightMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
source: workspaceButtonBackground.mainAppIconSource
implicitSize: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on implicitSize {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Loader {
active: Config.options.bar.workspaces.monochromeIcons
anchors.fill: mainAppIcon
sourceComponent: Item {
Desaturate {
id: desaturatedIcon
visible: false // There's already color overlay
anchors.fill: parent
source: mainAppIcon
desaturation: 0.8
}
ColorOverlay {
anchors.fill: desaturatedIcon
source: desaturatedIcon
color: ColorUtils.transparentize(wsDot.color, 0.9)
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,52 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Quickshell
import QtQuick
import QtQuick.Layouts
MouseArea {
id: root
property bool hovered: false
implicitWidth: rowLayout.implicitWidth + 10 * 2
implicitHeight: Appearance.sizes.barHeight
hoverEnabled: true
onPressed: {
Weather.getData();
Quickshell.execDetached(["notify-send",
Translation.tr("Weather"),
Translation.tr("Refreshing (manually triggered)")
, "-a", "Shell"
])
}
RowLayout {
id: rowLayout
anchors.centerIn: parent
MaterialSymbol {
fill: 0
text: WeatherIcons.codeToName[Weather.data.wCode] ?? "cloud"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
Layout.alignment: Qt.AlignVCenter
}
StyledText {
visible: true
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
text: Weather.data?.temp ?? "--°"
Layout.alignment: Qt.AlignVCenter
}
}
WeatherPopup {
id: weatherPopup
hoverTarget: root
}
}
@@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
Rectangle {
id: root
radius: Appearance.rounding.small
color: Appearance.colors.colSurfaceContainerHigh
implicitWidth: columnLayout.implicitWidth + 14 * 2
implicitHeight: columnLayout.implicitHeight + 14 * 2
Layout.fillWidth: parent
property alias title: title.text
property alias value: value.text
property alias symbol: symbol.text
ColumnLayout {
id: columnLayout
anchors.fill: parent
spacing: -10
RowLayout {
Layout.alignment: Qt.AlignHCenter
MaterialSymbol {
id: symbol
fill: 0
iconSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
id: title
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOnSurfaceVariant
}
}
StyledText {
id: value
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnSurfaceVariant
}
}
}
@@ -0,0 +1,59 @@
pragma Singleton
import Quickshell
Singleton {
// credits: calestia
// this snippet is taken from
// https://github.com/caelestia-dots/shell
readonly property var codeToName: ({
"113": "clear_day",
"116": "partly_cloudy_day",
"119": "cloud",
"122": "cloud",
"143": "foggy",
"176": "rainy",
"179": "rainy",
"182": "rainy",
"185": "rainy",
"200": "thunderstorm",
"227": "cloudy_snowing",
"230": "snowing_heavy",
"248": "foggy",
"260": "foggy",
"263": "rainy",
"266": "rainy",
"281": "rainy",
"284": "rainy",
"293": "rainy",
"296": "rainy",
"299": "rainy",
"302": "weather_hail",
"305": "rainy",
"308": "weather_hail",
"311": "rainy",
"314": "rainy",
"317": "rainy",
"320": "cloudy_snowing",
"323": "cloudy_snowing",
"326": "cloudy_snowing",
"329": "snowing_heavy",
"332": "snowing_heavy",
"335": "snowing",
"338": "snowing_heavy",
"350": "rainy",
"353": "rainy",
"356": "rainy",
"359": "weather_hail",
"362": "rainy",
"365": "rainy",
"368": "cloudy_snowing",
"371": "snowing",
"374": "rainy",
"377": "rainy",
"386": "thunderstorm",
"389": "thunderstorm",
"392": "thunderstorm",
"395": "snowing"
})
}
@@ -0,0 +1,104 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import "../"
StyledPopup {
id: root
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
implicitWidth: Math.max(header.implicitWidth, gridLayout.implicitWidth)
implicitHeight: gridLayout.implicitHeight
spacing: 5
// Header
ColumnLayout {
id: header
Layout.alignment: Qt.AlignHCenter
spacing: 2
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 6
MaterialSymbol {
fill: 0
font.weight: Font.Medium
text: "location_on"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
text: Weather.data.city
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
StyledText {
id: temp
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOnSurfaceVariant
text: Weather.data.temp + " • " + Translation.tr("Feels like %1").arg(Weather.data.tempFeelsLike)
}
}
// Metrics grid
GridLayout {
id: gridLayout
columns: 2
rowSpacing: 5
columnSpacing: 5
uniformCellWidths: true
WeatherCard {
title: Translation.tr("UV Index")
symbol: "wb_sunny"
value: Weather.data.uv
}
WeatherCard {
title: Translation.tr("Wind")
symbol: "air"
value: `(${Weather.data.windDir}) ${Weather.data.wind}`
}
WeatherCard {
title: Translation.tr("Precipitation")
symbol: "rainy_light"
value: Weather.data.precip
}
WeatherCard {
title: Translation.tr("Humidity")
symbol: "humidity_low"
value: Weather.data.humidity
}
WeatherCard {
title: Translation.tr("Visibility")
symbol: "visibility"
value: Weather.data.visib
}
WeatherCard {
title: Translation.tr("Pressure")
symbol: "readiness_score"
value: Weather.data.press
}
WeatherCard {
title: Translation.tr("Sunrise")
symbol: "wb_twilight"
value: Weather.data.sunrise
}
WeatherCard {
title: Translation.tr("Sunset")
symbol: "bedtime"
value: Weather.data.sunset
}
}
}
}