aboutsummaryrefslogtreecommitdiffstats
path: root/home/quickshell/bar
diff options
context:
space:
mode:
Diffstat (limited to 'home/quickshell/bar')
-rw-r--r--home/quickshell/bar/Bar.qml115
-rw-r--r--home/quickshell/bar/BarBlock.qml75
-rw-r--r--home/quickshell/bar/BarText.qml57
-rw-r--r--home/quickshell/bar/Notification.qml11
-rw-r--r--home/quickshell/bar/NotificationPanel.qml101
-rw-r--r--home/quickshell/bar/Tooltip.qml89
-rw-r--r--home/quickshell/bar/blocks/ActiveWorkspace.qml34
-rw-r--r--home/quickshell/bar/blocks/Battery.qml50
-rw-r--r--home/quickshell/bar/blocks/Date.qml10
-rw-r--r--home/quickshell/bar/blocks/Datetime.qml31
-rw-r--r--home/quickshell/bar/blocks/Icon.qml146
-rw-r--r--home/quickshell/bar/blocks/Memory.qml31
-rw-r--r--home/quickshell/bar/blocks/Notifications.qml34
-rw-r--r--home/quickshell/bar/blocks/Sound.qml176
-rw-r--r--home/quickshell/bar/blocks/SystemTray.qml80
-rw-r--r--home/quickshell/bar/blocks/Time.qml10
-rw-r--r--home/quickshell/bar/blocks/Workspace.qml28
-rw-r--r--home/quickshell/bar/blocks/Workspaces.qml74
-rw-r--r--home/quickshell/bar/utils/HyprlandUtils.qml54
19 files changed, 1206 insertions, 0 deletions
diff --git a/home/quickshell/bar/Bar.qml b/home/quickshell/bar/Bar.qml
new file mode 100644
index 0000000..5f5cae8
--- /dev/null
+++ b/home/quickshell/bar/Bar.qml
@@ -0,0 +1,115 @@
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+import QtQuick
+import QtQuick.Layouts
+import "blocks" as Blocks
+import "root:/"
+
+Scope {
+ IpcHandler {
+ target: "bar"
+
+ function toggleVis(): void {
+ // Toggle visibility of all bar instances
+ for (let i = 0; i < Quickshell.screens.length; i++) {
+ barInstances[i].visible = !barInstances[i].visible;
+ }
+ }
+ }
+
+ property var barInstances: []
+
+ Variants {
+ model: Quickshell.screens
+
+ PanelWindow {
+ id: bar
+ property var modelData
+ screen: modelData
+
+ Component.onCompleted: {
+ barInstances.push(bar);
+ }
+
+ color: "transparent"
+
+ Rectangle {
+ id: highlight
+ anchors.fill: parent
+ color: Theme.get.barBgColor
+ }
+
+ height: 30
+
+ visible: true
+
+ anchors {
+ top: Theme.get.onTop
+ bottom: !Theme.get.onTop
+ left: true
+ right: true
+ }
+
+ RowLayout {
+ id: allBlocks
+ spacing: 0
+ anchors.fill: parent
+
+ // Left side
+ RowLayout {
+ id: leftBlocks
+ spacing: 10
+ Layout.alignment: Qt.AlignLeft
+ Layout.fillWidth: true
+
+ //Blocks.Icon {}
+ Blocks.Workspaces {}
+ }
+
+ Blocks.ActiveWorkspace {
+ id: activeWorkspace
+ Layout.leftMargin: 10
+ anchors.centerIn: undefined
+
+ chopLength: {
+ var space = Math.floor(bar.width - (rightBlocks.implicitWidth + leftBlocks.implicitWidth))
+ return space * 0.08;
+ }
+
+ text: {
+ var str = activeWindowTitle
+ return str.length > chopLength ? str.slice(0, chopLength) + '...' : str;
+ }
+
+ color: {
+ return Hyprland.focusedMonitor == Hyprland.monitorFor(screen)
+ ? "#FFFFFF" : "#CCCCCC"
+ }
+ }
+
+ // Without this filler item, the active window block will be centered
+ // despite setting left alignment
+ Item {
+ Layout.fillWidth: true
+ }
+
+ // Right side
+ RowLayout {
+ id: rightBlocks
+ spacing: 0
+ Layout.alignment: Qt.AlignRight
+ Layout.fillWidth: true
+
+ Blocks.SystemTray {}
+ Blocks.Memory {}
+ Blocks.Sound {}
+ Blocks.Battery {}
+ Blocks.Date {}
+ Blocks.Time {}
+ }
+ }
+ }
+ }
+}
+
diff --git a/home/quickshell/bar/BarBlock.qml b/home/quickshell/bar/BarBlock.qml
new file mode 100644
index 0000000..edd4aca
--- /dev/null
+++ b/home/quickshell/bar/BarBlock.qml
@@ -0,0 +1,75 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+
+Rectangle {
+ id: root
+ Layout.preferredWidth: contentContainer.implicitWidth + 10
+ Layout.preferredHeight: 30
+
+ property Item content
+ property Item mouseArea: mouseArea
+
+ property string text
+ property bool dim: false
+ property bool underline
+ property var onClicked: function() {}
+ property int leftPadding
+ property int rightPadding
+
+ property string hoveredBgColor: "#666666"
+
+ // Background color
+ color: {
+ if (mouseArea.containsMouse)
+ return hoveredBgColor;
+ return "transparent";
+ }
+
+ states: [
+ State {
+ when: mouseArea.containsMouse
+ PropertyChanges {
+ target: root
+ }
+ }
+ ]
+
+ Behavior on color {
+ ColorAnimation {
+ duration: 200
+ }
+ }
+
+ Item {
+ // Contents of the bar block
+ id: contentContainer
+ implicitWidth: content.implicitWidth
+ implicitHeight: content.implicitHeight
+ anchors.centerIn: parent
+ children: content
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: root
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton
+ onClicked: root.onClicked()
+ }
+
+ // While line underneath workspace
+ Rectangle {
+ id: wsLine
+ width: parent.width
+ height: 2
+
+ color: {
+ if (parent.underline)
+ return "white";
+ return "transparent";
+ }
+ anchors.bottom: parent.bottom
+ }
+}
+
diff --git a/home/quickshell/bar/BarText.qml b/home/quickshell/bar/BarText.qml
new file mode 100644
index 0000000..4cf42cc
--- /dev/null
+++ b/home/quickshell/bar/BarText.qml
@@ -0,0 +1,57 @@
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Effects
+import Qt5Compat.GraphicalEffects
+
+Text {
+ property string mainFont: "FiraCode"
+ property string symbolFont: "Symbols Nerd Font Mono"
+ property int pointSize: 12
+ property int symbolSize: pointSize * 1.4
+ property string symbolText
+ property bool dim
+ text: wrapSymbols(symbolText)
+ anchors.centerIn: parent
+ color: dim ? "#CCCCCC" : "white"
+ textFormat: Text.RichText
+ font {
+ family: mainFont
+ pointSize: pointSize
+ }
+
+ Text {
+ visible: false
+ id: textcopy
+ text: parent.text
+ textFormat: parent.textFormat
+ color: parent.color
+ font: parent.font
+ }
+
+ DropShadow {
+ anchors.fill: parent
+ horizontalOffset: 1
+ verticalOffset: 1
+ color: "#000000"
+ source: textcopy
+ }
+
+ function wrapSymbols(text) {
+ if (!text)
+ return ""
+
+ const isSymbol = (codePoint) =>
+ (codePoint >= 0xE000 && codePoint <= 0xF8FF) // Private Use Area
+ || (codePoint >= 0xF0000 && codePoint <= 0xFFFFF) // Supplementary Private Use Area-A
+ || (codePoint >= 0x100000 && codePoint <= 0x10FFFF); // Supplementary Private Use Area-B
+
+ return text.replace(/./gu, (c) => isSymbol(c.codePointAt(0))
+ ? `<span style='font-family: ${symbolFont}; letter-spacing: 5px; font-size: ${symbolSize}px'>${c}</span>`
+ // ? c
+ : c);
+ }
+}
+
diff --git a/home/quickshell/bar/Notification.qml b/home/quickshell/bar/Notification.qml
new file mode 100644
index 0000000..b86a966
--- /dev/null
+++ b/home/quickshell/bar/Notification.qml
@@ -0,0 +1,11 @@
+import QtQuick
+
+Text {
+ required property int id
+ required property string body
+ required property string summary
+ property int margin
+
+ text: `- ${summary}: ${body}`
+}
+
diff --git a/home/quickshell/bar/NotificationPanel.qml b/home/quickshell/bar/NotificationPanel.qml
new file mode 100644
index 0000000..0ef8712
--- /dev/null
+++ b/home/quickshell/bar/NotificationPanel.qml
@@ -0,0 +1,101 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Services.Notifications
+
+PanelWindow {
+ // required property font custom_font
+ required property color text_color
+ property list<QtObject> notification_objects
+
+ width: 500
+ height: 600
+
+ color: "#171a18"
+
+ WlrLayershell.layer: WlrLayer.Overlay
+
+ Rectangle {
+ border.width: 5
+ border.color: "#8ec07c"
+ anchors.fill: parent
+ color: "transparent"
+
+ ColumnLayout {
+ id: content
+ anchors {
+ left: parent.left
+ leftMargin: 10
+ right: parent.right
+ rightMargin: 10
+ top: parent.top
+ topMargin: 10
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ Layout.fillWidth: true
+ text: "Notifications:"
+ // font: custom_font
+ color: text_color
+ }
+
+ Text {
+ text: "clear"
+ // font: custom_font
+ color: text_color
+
+ TapHandler {
+ id: tapHandler
+ gesturePolicy: TapHandler.ReleaseWithinBounds
+ onTapped: {
+ server.trackedNotifications.values.forEach((notification) => {
+ notification.tracked = false
+ })
+ notification_objects.forEach((object) => {
+ object.destroy();
+ })
+ notification_objects = [];
+ }
+ }
+
+ HoverHandler {
+ id: mouse
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ cursorShape: Qt.PointingHandCursor
+ }
+ }
+ }
+ }
+ }
+
+ NotificationServer {
+ id: server
+ onNotification: (notification) => {
+ notification.tracked = true
+ console.log(JSON.stringify(notification));
+ var notification_component = Qt.createComponent("Notification.qml");
+ var notification_object = notification_component
+ .createObject(content,
+ {
+ id: notification.id,
+ body: notification.body,
+ summary: notification.summary,
+ // font: custom_font,
+ color: text_color,
+ margin: 10
+ }
+ )
+ if (notification_object == null) {
+ console.log("Error creating notification")
+ } else {
+ notification_objects.push(notification_object);
+ }
+ }
+ }
+}
+
diff --git a/home/quickshell/bar/Tooltip.qml b/home/quickshell/bar/Tooltip.qml
new file mode 100644
index 0000000..7ab247d
--- /dev/null
+++ b/home/quickshell/bar/Tooltip.qml
@@ -0,0 +1,89 @@
+import QtQuick
+import Quickshell
+import "root:/" // for Globals
+
+LazyLoader {
+ id: root
+
+ // The item to display the tooltip at. If set to null the tooltip will be hidden.
+ property Item relativeItem: null
+
+ // Tracks the item after relativeItem is unset.
+ property Item displayItem: null
+
+ property PopupContext popupContext: Globals.popupContext
+
+ property bool hoverable: false;
+ readonly property bool hovered: item?.hovered ?? false
+
+ // The content to show in the tooltip.
+ required default property Component contentDelegate
+
+ active: displayItem != null && popupContext.popup == this
+
+ onRelativeItemChanged: {
+ if (relativeItem == null) {
+ if (item != null) item.hideTimer.start();
+ } else {
+ if (item != null) item.hideTimer.stop();
+ displayItem = relativeItem;
+ popupContext.popup = this;
+ }
+ }
+
+ PopupWindow {
+ anchor {
+ window: root.displayItem.QsWindow.window
+ rect.y: anchor.window.height + 3
+ rect.x: anchor.window.contentItem.mapFromItem(root.displayItem, root.displayItem.width / 2, 0).x
+ edges: Edges.Top
+ gravity: Edges.Bottom
+ }
+
+ visible: true
+
+ property alias hovered: body.containsMouse;
+
+ property Timer hideTimer: Timer {
+ interval: 250
+
+ // unloads the popup by causing active to become false
+ onTriggered: root.popupContext.popup = null;
+ }
+
+ color: "transparent"
+
+ // don't accept mouse input if !hoverable
+ Region { id: emptyRegion }
+ mask: root.hoverable ? null : emptyRegion
+
+ width: body.implicitWidth
+ height: body.implicitHeight
+
+ MouseArea {
+ id: body
+
+ anchors.fill: parent
+ implicitWidth: content.implicitWidth + 10
+ implicitHeight: content.implicitHeight + 10
+
+ hoverEnabled: root.hoverable
+
+ Rectangle {
+ anchors.fill: parent
+
+ radius: 5
+ border.width: 1
+ color: palette.active.toolTipBase
+ border.color: palette.active.light
+
+ Loader {
+ id: content
+ anchors.centerIn: parent
+ sourceComponent: contentDelegate
+ active: true
+ }
+ }
+ }
+ }
+}
diff --git a/home/quickshell/bar/blocks/ActiveWorkspace.qml b/home/quickshell/bar/blocks/ActiveWorkspace.qml
new file mode 100644
index 0000000..7969212
--- /dev/null
+++ b/home/quickshell/bar/blocks/ActiveWorkspace.qml
@@ -0,0 +1,34 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell.Hyprland
+import "../"
+
+BarText {
+ // text: {
+ // var str = activeWindowTitle
+ // return str.length > chopLength ? str.slice(0, chopLength) + '...' : str;
+ // }
+
+ property int chopLength
+ property string activeWindowTitle
+
+ Process {
+ id: titleProc
+ command: ["sh", "-c", "hyprctl activewindow | grep title: | sed 's/^[^:]*: //'"]
+ running: true
+
+ stdout: SplitParser {
+ onRead: data => activeWindowTitle = data
+ }
+ }
+
+ Component.onCompleted: {
+ Hyprland.rawEvent.connect(hyprEvent)
+ }
+
+ function hyprEvent(e) {
+ titleProc.running = true
+ }
+}
+
diff --git a/home/quickshell/bar/blocks/Battery.qml b/home/quickshell/bar/blocks/Battery.qml
new file mode 100644
index 0000000..dd52f7f
--- /dev/null
+++ b/home/quickshell/bar/blocks/Battery.qml
@@ -0,0 +1,50 @@
+import QtQuick
+import Quickshell.Io
+import "../"
+
+BarBlock {
+ property string battery
+ property bool hasBattery: false
+ visible: hasBattery
+
+ content: BarText {
+ symbolText: battery
+ }
+
+ Process {
+ id: batteryCheck
+ command: ["sh", "-c", "test -d /sys/class/power_supply/BAT*"]
+ running: true
+ onExited: function(exitCode) { hasBattery = exitCode === 0 }
+ }
+
+ Process {
+ id: batteryProc
+ // Modify command to get both capacity and status in one call
+ command: ["sh", "-c", "echo $(cat /sys/class/power_supply/BAT*/capacity),$(cat /sys/class/power_supply/BAT*/status)"]
+ running: hasBattery
+
+ stdout: SplitParser {
+ onRead: function(data) {
+ const [capacityStr, status] = data.trim().split(',')
+ const capacity = parseInt(capacityStr)
+ let batteryIcon = "󰂂"
+ if (capacity <= 20) batteryIcon = "󰁺"
+ else if (capacity <= 40) batteryIcon = "󰁽"
+ else if (capacity <= 60) batteryIcon = "󰁿"
+ else if (capacity <= 80) batteryIcon = "󰂁"
+ else batteryIcon = "󰂂"
+
+ const symbol = status === "Charging" ? "🔌" : batteryIcon
+ battery = `${symbol} ${capacity}%`
+ }
+ }
+ }
+
+ Timer {
+ interval: 1000
+ running: hasBattery
+ repeat: true
+ onTriggered: batteryProc.running = true
+ }
+}
diff --git a/home/quickshell/bar/blocks/Date.qml b/home/quickshell/bar/blocks/Date.qml
new file mode 100644
index 0000000..11ce193
--- /dev/null
+++ b/home/quickshell/bar/blocks/Date.qml
@@ -0,0 +1,10 @@
+import QtQuick
+import "../"
+
+BarBlock {
+ id: text
+ content: BarText {
+ symbolText: ` ${Datetime.date}`
+ }
+}
+
diff --git a/home/quickshell/bar/blocks/Datetime.qml b/home/quickshell/bar/blocks/Datetime.qml
new file mode 100644
index 0000000..743e785
--- /dev/null
+++ b/home/quickshell/bar/blocks/Datetime.qml
@@ -0,0 +1,31 @@
+pragma Singleton
+
+import Quickshell
+import Quickshell.Io
+import QtQuick
+
+Singleton {
+ property string time;
+ property string date;
+
+ Process {
+ id: dateProc
+ command: ["date", "+%a %e %b|%R"]
+ running: true
+
+ stdout: SplitParser {
+ onRead: data => {
+ date = data.split("|")[0]
+ time = data.split("|")[1]
+ }
+ }
+ }
+
+ Timer {
+ interval: 1000
+ running: true
+ repeat: true
+ onTriggered: dateProc.running = true
+ }
+}
+
diff --git a/home/quickshell/bar/blocks/Icon.qml b/home/quickshell/bar/blocks/Icon.qml
new file mode 100644
index 0000000..c111a7e
--- /dev/null
+++ b/home/quickshell/bar/blocks/Icon.qml
@@ -0,0 +1,146 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Qt5Compat.GraphicalEffects
+import "../"
+import "root:/"
+
+BarBlock {
+ id: root
+ Layout.preferredWidth: 20
+
+ content: BarText {
+ text: "󰣇"
+ pointSize: 24
+ anchors.horizontalCenterOffset: 4
+ anchors.verticalCenterOffset: 3
+ }
+
+ color: "transparent"
+
+ Process {
+ id: appListProc
+ command: ["sh", "-c", "for f in /usr/share/applications/*.desktop; do if ! grep -qi 'terminal=true' \"$f\"; then name=$(grep -i '^Name=' \"$f\" | head -n1 | cut -d= -f2); basename=$(basename \"$f\" .desktop); echo \"$name|$basename|$f\"; fi; done"]
+ running: false
+ stdout: SplitParser {
+ onRead: data => {
+ const [appName, launchName, desktopFile] = data.trim().split("|")
+ if (appName && launchName && desktopFile) {
+ appListModel.append({ name: appName, launchName: launchName, path: desktopFile })
+ }
+ }
+ }
+ }
+
+ Process {
+ id: appLauncher
+ running: false
+ command: ["gtk-launch"]
+ }
+
+ ListModel {
+ id: appListModel
+ }
+
+ PopupWindow {
+ id: menuWindow
+ width: 300
+ height: 400
+ visible: false
+
+ anchor {
+ window: root.QsWindow?.window
+ edges: Edges.Bottom
+ gravity: Edges.Top
+ }
+
+ FocusScope {
+ anchors.fill: parent
+ focus: true
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ onExited: {
+ if (!containsMouse) {
+ closeTimer.start()
+ }
+ }
+ onEntered: closeTimer.stop()
+
+ Timer {
+ id: closeTimer
+ interval: 500
+ onTriggered: menuWindow.visible = false
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "#2E3440" // Using Nord theme color
+ border.color: "#4C566A"
+ border.width: 1
+ radius: 4
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: 10
+ spacing: 5
+
+ ListView {
+ id: appListView
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ model: appListModel
+ delegate: Rectangle {
+ width: parent.width
+ height: 35
+ color: mouseArea.containsMouse ? "#4C566A" : "transparent"
+ radius: 4
+
+ Text {
+ anchors.fill: parent
+ anchors.leftMargin: 10
+ text: model.name
+ color: "white"
+ font.pixelSize: 12
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: {
+ console.log("Launching:", model.launchName, "from", model.path)
+ appLauncher.command = ["gtk-launch", model.launchName]
+ appLauncher.running = true
+ menuWindow.visible = false
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function filterApps() {
+ const searchText = searchField.text.toLowerCase()
+ for (let i = 0; i < appListModel.count; i++) {
+ const item = appListModel.get(i)
+ item.visible = item.name.toLowerCase().includes(searchText)
+ }
+ }
+ onClicked: function() {
+ if (!menuWindow.visible) {
+ appListModel.clear()
+ appListProc.running = true
+ }
+ menuWindow.visible = !menuWindow.visible
+ }
+} \ No newline at end of file
diff --git a/home/quickshell/bar/blocks/Memory.qml b/home/quickshell/bar/blocks/Memory.qml
new file mode 100644
index 0000000..4a931c1
--- /dev/null
+++ b/home/quickshell/bar/blocks/Memory.qml
@@ -0,0 +1,31 @@
+import QtQuick
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Io
+import "../"
+
+BarBlock {
+ id: text
+ content: BarText {
+ symbolText: `- ${Math.floor(percentFree)}%`
+ }
+
+ property real percentFree
+
+ Process {
+ id: memProc
+ command: ["sh", "-c", "free | grep Mem | awk '{print $3/$2 * 100.0}'"]
+ running: true
+
+ stdout: SplitParser {
+ onRead: data => percentFree = data
+ }
+ }
+
+ Timer {
+ interval: 2000
+ running: true
+ repeat: true
+ onTriggered: memProc.running = true
+ }
+}
diff --git a/home/quickshell/bar/blocks/Notifications.qml b/home/quickshell/bar/blocks/Notifications.qml
new file mode 100644
index 0000000..3871cc4
--- /dev/null
+++ b/home/quickshell/bar/blocks/Notifications.qml
@@ -0,0 +1,34 @@
+import QtQuick
+import Quickshell.Services.Notifications
+import "../"
+
+BarBlock {
+ id: root
+ property bool showNotification: false
+
+ text: " " + notifServer.trackedNotifications.values.length
+ onClicked: function() {
+ showNotification = !showNotification
+ }
+
+ NotificationServer {
+ id: notifServer
+ onNotification: (notification) => {
+ notification.tracked = true
+ }
+ }
+
+ NotificationPanel {
+ text_color: root.color
+ visible: showNotification
+
+ anchors {
+ top: parent.top
+ }
+
+ margins {
+ top: 10
+ }
+ }
+}
+
diff --git a/home/quickshell/bar/blocks/Sound.qml b/home/quickshell/bar/blocks/Sound.qml
new file mode 100644
index 0000000..e8be70b
--- /dev/null
+++ b/home/quickshell/bar/blocks/Sound.qml
@@ -0,0 +1,176 @@
+import QtQuick
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Services.Pipewire
+import Quickshell.Io
+import "../"
+import "root:/"
+
+BarBlock {
+ id: root
+ property var sink: Pipewire.defaultAudioSink
+
+ PwObjectTracker {
+ objects: [Pipewire.defaultAudioSink]
+ onObjectsChanged: {
+ sink = Pipewire.defaultAudioSink
+ if (sink?.audio) {
+ sink.audio.volumeChanged.connect(updateVolume)
+ }
+ }
+ }
+
+ function updateVolume() {
+ if (sink?.audio) {
+ const icon = sink.audio.muted ? "󰖁" : "󰕾"
+ content.symbolText = `${icon} ${Math.round(sink.audio.volume * 100)}%`
+ }
+ }
+
+ content: BarText { symbolText: `${sink?.audio?.muted ? "󰖁" : "󰕾"} ${Math.round(sink?.audio?.volume * 100)}%` }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: toggleMenu()
+ onWheel: function(event) {
+ if (sink?.audio) {
+ sink.audio.volume = Math.max(0, Math.min(1, sink.audio.volume + (event.angleDelta.y / 120) * 0.05))
+ }
+ }
+ }
+
+ Process {
+ id: pavucontrol
+ command: ["pavucontrol"]
+ running: false
+ }
+
+ PopupWindow {
+ id: menuWindow
+ width: 200
+ height: 150
+ visible: false
+
+ anchor {
+ window: root.QsWindow?.window
+ edges: Edges.Bottom
+ gravity: Edges.Top
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ onExited: {
+ if (!containsMouse) {
+ closeTimer.start()
+ }
+ }
+ onEntered: closeTimer.stop()
+
+ Timer {
+ id: closeTimer
+ interval: 500
+ onTriggered: menuWindow.visible = false
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "#2c2c2c"
+ border.color: "#3c3c3c"
+ border.width: 1
+ radius: 4
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: 10
+ spacing: 10
+
+ // Volume Slider
+ Rectangle {
+ width: parent.width
+ height: 35
+ color: "transparent"
+
+ Slider {
+ id: volumeSlider
+ anchors.fill: parent
+ from: 0
+ to: 1
+ value: sink?.audio?.volume || 0
+ onValueChanged: {
+ if (sink?.audio) {
+ sink.audio.volume = value
+ }
+ }
+
+ background: Rectangle {
+ x: volumeSlider.leftPadding
+ y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2
+ width: volumeSlider.availableWidth
+ height: 4
+ radius: 2
+ color: "#3c3c3c"
+
+ Rectangle {
+ width: volumeSlider.visualPosition * parent.width
+ height: parent.height
+ color: "#4a9eff"
+ radius: 2
+ }
+ }
+
+ handle: Rectangle {
+ x: volumeSlider.leftPadding + volumeSlider.visualPosition * (volumeSlider.availableWidth - width)
+ y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2
+ width: 16
+ height: 16
+ radius: 8
+ color: volumeSlider.pressed ? "#4a9eff" : "#ffffff"
+ border.color: "#3c3c3c"
+ }
+ }
+ }
+
+ Repeater {
+ model: [
+ { text: sink?.audio?.muted ? "Unmute" : "Mute", action: () => sink?.audio && (sink.audio.muted = !sink.audio.muted) },
+ { text: "Pavucontrol", action: () => { pavucontrol.running = true; menuWindow.visible = false } }
+ ]
+
+ Rectangle {
+ width: parent.width
+ height: 35
+ color: mouseArea.containsMouse ? "#3c3c3c" : "transparent"
+ radius: 4
+
+ Text {
+ anchors.fill: parent
+ anchors.leftMargin: 10
+ text: modelData.text
+ color: "white"
+ font.pixelSize: 12
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: {
+ modelData.action()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function toggleMenu() {
+ if (root.QsWindow?.window?.contentItem) {
+ menuWindow.anchor.rect = root.QsWindow.window.contentItem.mapFromItem(root, 0, -menuWindow.height - 5, root.width, root.height)
+ menuWindow.visible = !menuWindow.visible
+ }
+ }
+} \ No newline at end of file
diff --git a/home/quickshell/bar/blocks/SystemTray.qml b/home/quickshell/bar/blocks/SystemTray.qml
new file mode 100644
index 0000000..15c4691
--- /dev/null
+++ b/home/quickshell/bar/blocks/SystemTray.qml
@@ -0,0 +1,80 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Services.SystemTray
+import "root:/bar"
+
+RowLayout {
+ spacing: 5
+
+ Repeater {
+ model: ScriptModel {
+ values: {[...SystemTray.items.values]
+ .filter((item) => {
+ return (item.id != "spotify-client"
+ && item.id != "chrome_status_icon_1")
+ })
+ }
+ }
+
+ MouseArea {
+ id: delegate
+ required property SystemTrayItem modelData
+ property alias item: delegate.modelData
+
+ Layout.fillHeight: true
+ implicitWidth: icon.implicitWidth + 5
+
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ hoverEnabled: true
+
+ onClicked: event => {
+ if (event.button == Qt.LeftButton) {
+ item.activate();
+ } else if (event.button == Qt.MiddleButton) {
+ item.secondaryActivate();
+ } else if (event.button == Qt.RightButton) {
+ menuAnchor.open();
+ }
+ }
+
+ onWheel: event => {
+ event.accepted = true;
+ const points = event.angleDelta.y / 120
+ item.scroll(points, false);
+ }
+
+ IconImage {
+ id: icon
+ anchors.centerIn: parent
+ source: item.icon
+ implicitSize: 16
+ }
+
+ QsMenuAnchor {
+ id: menuAnchor
+ menu: item.menu
+
+ anchor.window: delegate.QsWindow.window
+ anchor.adjustment: PopupAdjustment.Flip
+
+ anchor.onAnchoring: {
+ const window = delegate.QsWindow.window;
+ const widgetRect = window.contentItem.mapFromItem(delegate, 0, delegate.height, delegate.width, delegate.height);
+
+ menuAnchor.anchor.rect = widgetRect;
+ }
+ }
+
+ Tooltip {
+ relativeItem: delegate.containsMouse ? delegate : null
+
+ Label {
+ text: delegate.item.tooltipTitle || delegate.item.id
+ }
+ }
+ }
+ }
+}
diff --git a/home/quickshell/bar/blocks/Time.qml b/home/quickshell/bar/blocks/Time.qml
new file mode 100644
index 0000000..5650fcb
--- /dev/null
+++ b/home/quickshell/bar/blocks/Time.qml
@@ -0,0 +1,10 @@
+import QtQuick
+import "../"
+
+BarBlock {
+ id: text
+ content: BarText {
+ symbolText: ` ${Datetime.time}`
+ }
+}
+
diff --git a/home/quickshell/bar/blocks/Workspace.qml b/home/quickshell/bar/blocks/Workspace.qml
new file mode 100644
index 0000000..232a3f3
--- /dev/null
+++ b/home/quickshell/bar/blocks/Workspace.qml
@@ -0,0 +1,28 @@
+import QtQuick
+import QtQuick.Layouts
+
+Rectangle {
+ id: ws
+
+ property bool hovered: false
+
+ Layout.preferredWidth: 10
+ Layout.preferredHeight: 10
+ Layout.minimumWidth: 10
+ Layout.minimumHeight: 10
+ Layout.alignment: Qt.AlignHCenter
+ radius: height / 2
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+
+ onEntered: () => {
+ ws.hovered = true;
+ }
+ onExited: () => {
+ ws.hovered = false;
+ }
+ onClicked: () => console.log(`workspace ?`)
+ }
+}
diff --git a/home/quickshell/bar/blocks/Workspaces.qml b/home/quickshell/bar/blocks/Workspaces.qml
new file mode 100644
index 0000000..66d48a6
--- /dev/null
+++ b/home/quickshell/bar/blocks/Workspaces.qml
@@ -0,0 +1,74 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Hyprland
+import Quickshell.Widgets
+import Qt5Compat.GraphicalEffects
+import "../utils" as Utils
+import "root:/"
+
+RowLayout {
+ property HyprlandMonitor monitor: Hyprland.monitorFor(screen)
+
+ Rectangle {
+ id: workspaceBar
+ Layout.preferredWidth: Math.max(50, Utils.HyprlandUtils.maxWorkspace * 25)
+ Layout.preferredHeight: 23
+ radius: 7
+ color: Theme.get.barBgColor
+
+ Row {
+ anchors.centerIn: parent
+ spacing: 15
+
+ Repeater {
+ model: Utils.HyprlandUtils.maxWorkspace || 1
+
+ Item {
+ required property int index
+ property bool focused: Hyprland.focusedMonitor?.activeWorkspace?.id === (index + 1)
+
+ width: workspaceText.width
+ height: workspaceText.height
+
+ Text {
+ id: workspaceText
+ text: (index + 1).toString()
+ color: "white"
+ font.pixelSize: 15
+ font.bold: focused
+ }
+
+ Rectangle {
+ visible: focused
+ anchors {
+ left: workspaceText.left
+ right: workspaceText.right
+ top: workspaceText.bottom
+ topMargin: -3
+ }
+ height: 2
+ color: "white"
+ }
+
+ DropShadow {
+ visible: focused
+ anchors.fill: workspaceText
+ horizontalOffset: 2
+ verticalOffset: 2
+ radius: 8.0
+ samples: 20
+ color: "#000000"
+ source: workspaceText
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: Utils.HyprlandUtils.switchWorkspace(index + 1)
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/home/quickshell/bar/utils/HyprlandUtils.qml b/home/quickshell/bar/utils/HyprlandUtils.qml
new file mode 100644
index 0000000..dde3b31
--- /dev/null
+++ b/home/quickshell/bar/utils/HyprlandUtils.qml
@@ -0,0 +1,54 @@
+pragma Singleton
+
+import Quickshell
+import Quickshell.Hyprland
+import QtQuick
+
+Singleton {
+ id: hyprland
+
+ property list<HyprlandWorkspace> workspaces: sortWorkspaces(Hyprland.workspaces.values)
+ property int maxWorkspace: findMaxId()
+
+ function sortWorkspaces(ws) {
+ return [...ws].sort((a, b) => a?.id - b?.id);
+ }
+
+ function switchWorkspace(w: int): void {
+ Hyprland.dispatch(`workspace ${w}`);
+ }
+
+ function findMaxId(): int {
+ if (hyprland.workspaces.length === 0) {
+ console.log("No workspaces found, defaulting to 1");
+ return 1; // Return 1 if no workspaces exist
+ }
+ let num = hyprland.workspaces.length;
+ let maxId = hyprland.workspaces[num - 1]?.id || 1;
+ console.log("Current max workspace ID:", maxId);
+ return maxId;
+ }
+
+ Connections {
+ target: Hyprland
+ function onRawEvent(event) {
+ let eventName = event.name;
+ console.log("Hyprland event received:", eventName);
+
+ switch (eventName) {
+ case "createworkspacev2":
+ {
+ console.log("Workspace created, updating workspace list");
+ hyprland.workspaces = hyprland.sortWorkspaces(Hyprland.workspaces.values);
+ hyprland.maxWorkspace = findMaxId();
+ }
+ case "destroyworkspacev2":
+ {
+ console.log("Workspace destroyed, updating workspace list");
+ hyprland.workspaces = hyprland.sortWorkspaces(Hyprland.workspaces.values);
+ hyprland.maxWorkspace = findMaxId();
+ }
+ }
+ }
+ }
+}