diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2024-05-23 13:56:00 +0300 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-30 12:29:57 +0200 |
| commit | 08297376a85a1719518507e54fca9de954d2376a (patch) | |
| tree | 3b9c58304b40248533bbb2bb5b7bad2da9da1ff0 /home/quickshell | |
| parent | 75c2af4aedd2ac5c2cfc74b346625fa4b265541d (diff) | |
| download | nixos-08297376a85a1719518507e54fca9de954d2376a.tar.zst | |
Agenix configuration
Diffstat (limited to 'home/quickshell')
25 files changed, 1430 insertions, 0 deletions
diff --git a/home/quickshell/PopupContext.qml b/home/quickshell/PopupContext.qml new file mode 100644 index 0000000..6f007c8 --- /dev/null +++ b/home/quickshell/PopupContext.qml @@ -0,0 +1,6 @@ +import QtQuick + +// Tracks which popup of a set is active. +QtObject { + property var popup: null; +} diff --git a/home/quickshell/README.md b/home/quickshell/README.md new file mode 100644 index 0000000..0458afa --- /dev/null +++ b/home/quickshell/README.md @@ -0,0 +1,13 @@ +## Quickshell Titus Config + +This is a minimal DWM style bar for Hyprland. HOWEVER, most of the examples online do a lot of crazy things. If you can think it up you can do it in QUICKSHELL! + +Try it today! + +### Dependencies + +Arch Linux + +``` +yay -S qt6-5compat quickshell +``` diff --git a/home/quickshell/Theme.qml b/home/quickshell/Theme.qml new file mode 100644 index 0000000..e8777db --- /dev/null +++ b/home/quickshell/Theme.qml @@ -0,0 +1,87 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + property Item get: nordic + + Item { + id: windowsXP + + property string barBgColor: "#88235EDC" + property string buttonBorderColor: "#99000000" + property bool buttonBorderShadow: false + property string buttonBackgroundColor: "#1111CC" + property bool onTop: false + property string iconColor: "green" + property string iconPressedColor: "green" + property Gradient barGradient: black.barGradient + } + + Item { + id: black + + property string barBgColor: "#cc000000" + property string buttonBorderColor: "#BBBBBB" + property string buttonBackgroundColor: "#222222" + property bool buttonBorderShadow: true + property bool onTop: true + property string iconColor: "blue" + property string iconPressedColor: "dark_blue" + } + + Item { + id: nordic + + // Nord color palette + property string barBgColor: "#aa2E3440" // Nord0 - Polar Night + property string buttonBorderColor: "#4C566A" // Nord3 - Polar Night + property string buttonBackgroundColor: "#3D4550" + property bool buttonBorderShadow: true + property bool onTop: true + property string iconColor: "#88C0D0" // Nord7 - Frost + property string iconPressedColor: "#81A1C1" // Nord9 - Frost + } + + Item { + id: cyberpunk + + // Tokyo Neon color palette + property string barBgColor: "#881A0B2E" // Deep purple-black + property string buttonBorderColor: "#FF2A6D" // Neon pink + property string buttonBackgroundColor: "#1A1A2E" // Dark blue-black + property bool buttonBorderShadow: true + property bool onTop: true + property string iconColor: "#05D9E8" // Electric blue + property string iconPressedColor: "#FF2A6D" // Neon pink + } + + Item { + id: material + + // Material Design 3 color palette + property string barBgColor: "#cc1F1F1F" // Surface dark + property string buttonBorderColor: "#2D2D2D" // Surface variant + property string buttonBackgroundColor: "#2D2D2D" // Surface variant + property bool buttonBorderShadow: true + property bool onTop: true + property string iconColor: "#90CAF9" // Primary light + property string iconPressedColor: "#64B5F6" // Primary medium + } + + Item { + id: catppuccin + + // Catppuccin Mocha color palette + property string barBgColor: "#aa1E1E2E" // Base + property string buttonBorderColor: "#313244" // Surface0 + property string buttonBackgroundColor: "#313244" // Surface0 + property bool buttonBorderShadow: true + property bool onTop: true + property string iconColor: "#89B4FA" // Blue + property string iconPressedColor: "#74C7EC" // Sapphire + } + +} + 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(); + } + } + } + } +} diff --git a/home/quickshell/default.nix b/home/quickshell/default.nix new file mode 100644 index 0000000..43f7d76 --- /dev/null +++ b/home/quickshell/default.nix @@ -0,0 +1,20 @@ +{ + pkgs, + pkgs-unstable, + lib, + config, + ... +}: +{ + imports = [ ./quickshell.nix ]; + programs.quickshell = { + enable = true; + systemd.enable = true; + package = pkgs-unstable.quickshell; + systemd.target = "hyprland-session.target"; + }; + xdg.configFile."quickshell/shell.qml".source = ./shell.qml; + xdg.configFile."quickshell/Theme.qml".source = ./Theme.qml; + xdg.configFile."quickshell/PopupContext.qml".source = ./PopupContext.qml; + xdg.configFile."quickshell/bar".source = ./bar; +} diff --git a/home/quickshell/quickshell.nix b/home/quickshell/quickshell.nix new file mode 100644 index 0000000..87cc52c --- /dev/null +++ b/home/quickshell/quickshell.nix @@ -0,0 +1,90 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.programs.quickshell; +in +{ + meta.maintainers = [ lib.maintainers.justdeeevin ]; + + options.programs.quickshell = { + enable = lib.mkEnableOption "quickshell, a flexbile QtQuick-based desktop shell toolkit."; + package = lib.mkPackageOption pkgs "quickshell" { nullable = true; }; + configs = lib.mkOption { + type = lib.types.attrsOf lib.types.path; + default = { }; + description = '' + A set of configs to include in the quickshell config directory. The key is the name of the config. + + The configuration that quickshell should use can be specified with the `activeConfig` option. + ''; + }; + activeConfig = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + The name of the config to use. + + If `null`, quickshell will attempt to use a config located in `$XDG_CONFIG_HOME/quickshell` instead of one of the named sub-directories. + ''; + }; + + systemd = { + enable = lib.mkEnableOption "quickshell systemd service"; + target = lib.mkOption { + type = lib.types.str; + default = config.wayland.systemd.target; + defaultText = lib.literalExpression "config.wayland.systemd.target"; + example = "hyprland-session.target"; + description = '' + The systemd target that will automatically start quickshell. + + If you set this to a WM-specific target, make sure that systemd integration for that WM is enabled (e.g. `wayland.windowManager.hyprland.systemd.enable`). **This is typically true by default**. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + (lib.mkIf (cfg.configs != { }) { + xdg.configFile = lib.mapAttrs' (name: path: { + name = "quickshell/${name}"; + value.source = path; + }) cfg.configs; + }) + { + assertions = [ + (lib.hm.assertions.assertPlatform "programs.quickshell" pkgs lib.platforms.linux) + { + assertion = !(builtins.any (name: lib.hasInfix "/" name) (builtins.attrNames cfg.configs)); + message = "The names of configs in `programs.quickshell.configs` must not contain slashes."; + } + ]; + + home.packages = [ cfg.package ]; + + } + (lib.mkIf cfg.systemd.enable { + systemd.user.services.quickshell = { + Unit = { + Description = "quickshell"; + Documentation = "https://quickshell.outfoxxed.me/docs/"; + After = [ cfg.systemd.target ]; + }; + + Service = { + ExecStart = + lib.getExe cfg.package + (if cfg.activeConfig == null then "" else " --config ${cfg.activeConfig}"); + Restart = "on-failure"; + }; + + Install.WantedBy = [ cfg.systemd.target ]; + }; + }) + ] + ); +} diff --git a/home/quickshell/shell.qml b/home/quickshell/shell.qml new file mode 100644 index 0000000..43bb3e9 --- /dev/null +++ b/home/quickshell/shell.qml @@ -0,0 +1,8 @@ +//@ pragma UseQApplication +import "bar" + +import Quickshell + +ShellRoot { + Bar {} +} |
