From c33b079159f2b6188fc3197dfd4cfc7f7a9eddd3 Mon Sep 17 00:00:00 2001 From: atagen Date: Mon, 21 Jul 2025 00:53:00 +1000 Subject: [PATCH] add arbyd quickshell conf --- desktop/quickshell/bink/Bink.qml | 133 +++++++++++ desktop/quickshell/launcher/Launcher.qml | 222 ++++++++++++++++++ desktop/quickshell/noti/Noti.qml | 18 ++ .../particle-test/particle-test.qml | 84 +++++++ desktop/quickshell/rice/Colours.qml | 34 +++ desktop/quickshell/shell.qml | 212 +++++++++++++++++ desktop/quickshell/tags/Tags.qml | 80 +++++++ desktop/quickshell/title/Title.qml | 52 ++++ desktop/shell.nix | 14 ++ flake.lock | 16 ++ flake.nix | 2 + 11 files changed, 867 insertions(+) create mode 100644 desktop/quickshell/bink/Bink.qml create mode 100644 desktop/quickshell/launcher/Launcher.qml create mode 100644 desktop/quickshell/noti/Noti.qml create mode 100644 desktop/quickshell/particle-test/particle-test.qml create mode 100644 desktop/quickshell/rice/Colours.qml create mode 100644 desktop/quickshell/shell.qml create mode 100644 desktop/quickshell/tags/Tags.qml create mode 100644 desktop/quickshell/title/Title.qml diff --git a/desktop/quickshell/bink/Bink.qml b/desktop/quickshell/bink/Bink.qml new file mode 100644 index 0000000..8498bdd --- /dev/null +++ b/desktop/quickshell/bink/Bink.qml @@ -0,0 +1,133 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +Rectangle { + id: base + anchors { + fill: parent + margins: 2 + } + color: colours[0] + + required property string format + required property var colours + property var clock: genClock(format) + property var date: new Date() + property string time: date.toLocaleString(Qt.locale()) + property int cols: getColSum(clock) + property real colWidth: (base.width - topgrid.spacing - base.anchors.margins) / cols + + property var keys: { + "a": [(date.getHours() > 11) | 0, -1], + "H": [-1].concat(binarise(date.getHours(), 5)), + "h": binarise(date.getHours() % 12, 4), + "m": binarise(date.getMinutes(), 6), + "s": binarise(date.getSeconds(), 6) + } + + function genClock(format) { + return format.split('').map(k => keys[k]); + } + + function getColSum(clock) { + return clock.map(x => Math.ceil(x.length / 2)).reduce((acc, el) => acc + el); + } + + function binarise(n, p) { + return n.toString(2) // to base-2 string + .padStart(p, 0) // zero pad + .split('') // split to array + .slice(-p) // take only desired bits, lsb first + .map(x => parseInt(x)); // map to int + } + Grid { + id: topgrid + rows: 1 + columns: base.clock.length + spacing: 2 + anchors.fill: parent + + Repeater { + model: base.clock.length + Grid { + id: inner + required property int index + property var bits: base.clock[index] + property int cols: bits.length / 2 + width: base.colWidth * cols + height: base.height + rows: 2 + columns: cols + spacing: 1 + + function calcBitSize() { + let cell = inner.width - inner.spacing * 2; + let def = (cell / inner.cols); + return (def > inner.height / 2) ? inner.height / 2 : def; + } + + Repeater { + model: inner.bits.length + Rectangle { + required property int index + height: inner.calcBitSize() + width: height + color: "transparent" + Rectangle { + property int bit: inner.bits[parent.index] + property string on: base.colours[1] + property string off: base.colours[0] + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + height: parent.height + width: parent.width + + border.width: 1 + border.color: bit == -1 ? off : on + color: (bit == -1 || !bit) ? off : on + Behavior on color { + ColorAnimation { + duration: 150 + } + } + radius: height + + // + // height: bit ? parent.height / 3 * 2 : parent.height + // Behavior on height { + // NumberAnimation { + // duration: 50 + // } + // } + // width: bit ? 1 : height + // Behavior on width { + // NumberAnimation { + // duration: 50 + // } + // } + // radius: bit ? 0 : height + // Behavior on radius { + // NumberAnimation { + // duration: 50 + // } + // } + + } + } + } + } + } + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: { + base.date = new Date(); + } + } + } +} diff --git a/desktop/quickshell/launcher/Launcher.qml b/desktop/quickshell/launcher/Launcher.qml new file mode 100644 index 0000000..2a23a9e --- /dev/null +++ b/desktop/quickshell/launcher/Launcher.qml @@ -0,0 +1,222 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import Quickshell.Io + +Singleton { + id: topLevel + required property real width + + Timer { + id: closeTimer + interval: 400 + running: false + repeat: false + onTriggered: launcherData.active = false + } + + PersistentProperties { + id: launcherData + property bool open: false + property bool active: false + property real curWidth: 0 + onOpenChanged: { + if (open) { + curWidth = topLevel.width; + launcherData.active = true; + } else { + curWidth = 0; + closeTimer.start(); + } + } + Behavior on curWidth { + NumberAnimation { + duration: 400 + easing.type: Easing.InOutQuad + } + } + } + + IpcHandler { + target: "launch" + function open(): void { + launcherData.open = true; + } + function close(): void { + launcherData.open = false; + } + function toggle(): void { + launcherData.open = !launcherData.open; + } + } + + LazyLoader { + id: loader + activeAsync: launcherData.active + + PanelWindow { + id: launcherBase + + anchors { + top: true + bottom: true + right: true + } + + color: "transparent" + implicitWidth: topLevel.width + + WlrLayershell.layer: WlrLayer.Overlay + focusable: true + exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "shell:launcher" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + // launcherData.curWidth + + Rectangle { + color: "#ffab5b" + anchors { + fill: parent + topMargin: parent.height / 3 + bottomMargin: anchors.topMargin + leftMargin: topLevel.width - launcherData.curWidth + } + bottomLeftRadius: 10 + // topLeftRadius: 10 + + Rectangle { + color: "#272a2a" + anchors { + fill: parent + topMargin: 3 + bottomMargin: 3 + leftMargin: 3 + } + bottomLeftRadius: 10 + // topLeftRadius: 10 + // implicitWidth: topLevel.width + + TextField { + id: searchField + anchors { + top: parent.top + left: parent.left + right: parent.right + topMargin: 10 + leftMargin: 10 + } + font { + family: "Inria Sans" + pointSize: 12 + } + color: "#202e2f" + height: 24 + background: Rectangle { + color: "#caccce" + radius: 5 + } + focus: true + + Keys.forwardTo: [list] + Keys.onEscapePressed: launcherData.open = false + + onAccepted: { + if (list.currentItem) { + list.currentItem.clicked(null); + } + } + } + + Rectangle { + id: emptyScrollbar + anchors { + left: parent.left + top: searchField.bottom + bottom: list.bottom + topMargin: 6 + bottomMargin: 4 + leftMargin: 4 + } + color: "#3a5299ff" + width: 3 + radius: 10 + } + + ListView { + id: list + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + top: searchField.bottom + topMargin: 4 + leftMargin: 12 + bottomMargin: 12 + } + clip: true + cacheBuffer: 0 + model: ScriptModel { + values: DesktopEntries.applications.values.map(x => x).filter(entry => { + const search = searchField.text.toLowerCase(); + const name = entry.name.toLowerCase(); + return search.length ? name.indexOf(search) > -1 : true; + }) + } + onModelChanged: list.currentIndex = 0 + highlight: Rectangle { + anchors { + left: parent.left + right: parent.right + } + color: "#7f5299ff" + border.color: "#928cc9" + border.width: 2 + } + spacing: 0 + delegate: MouseArea { + id: clickableEntry + required property DesktopEntry modelData + onClicked: { + modelData.execute(); + launcherData.open = false; + } + implicitHeight: 24 + implicitWidth: ListView.view.width + + RowLayout { + id: rowEntry + + anchors { + left: parent.left + leftMargin: 4 + verticalCenter: parent.verticalCenter + } + + IconImage { + asynchronous: true + implicitSize: 18 + source: Quickshell.iconPath(clickableEntry.modelData.icon) + } + + Text { + font { + family: "Inria Sans" + pointSize: 12 + } + color: "#ffab5b" + text: clickableEntry.modelData.name + Layout.alignment: Qt.AlignBottom + } + } + } + } + } + } + } + } +} diff --git a/desktop/quickshell/noti/Noti.qml b/desktop/quickshell/noti/Noti.qml new file mode 100644 index 0000000..81127ff --- /dev/null +++ b/desktop/quickshell/noti/Noti.qml @@ -0,0 +1,18 @@ +pragma Singleton + +import Quickshell +import Quickshell.Services.Notifications + +Singleton { + NotificationServer { + id: notifications + actionsSupported: true + bodyHyperlinksSupported: true + // bodyImagesSupported: true + bodyMarkupSupported: true + imageSupported: true + onNotification: noti => { + + } + } +} diff --git a/desktop/quickshell/particle-test/particle-test.qml b/desktop/quickshell/particle-test/particle-test.qml new file mode 100644 index 0000000..b4262c3 --- /dev/null +++ b/desktop/quickshell/particle-test/particle-test.qml @@ -0,0 +1,84 @@ +import Quickshell +import QtQuick +import QtQuick.Particles + + +PanelWindow { + id: particleRoot + anchors { + top: true + bottom: true + left: true + right: true + } + color: "transparent" + exclusionMode: ExclusionMode.Ignore + + screen: Quickshell.screens.find(s => s.name == "DP-1") + property var mousePos: [width/2, height/2] + MouseArea { + id: mouse + anchors.fill: parent + hoverEnabled: true + onPositionChanged: { + particleRoot.mousePos = [mouse.x, mouse.y]; + } + } + + ParticleSystem { + id: sys + running: true + anchors.fill: parent + + ItemParticle { + system: sys + delegate: Component { + Rectangle { + color: "black" + width: 7 + height: width + radius: width + Rectangle { + color: "white" + width: 5 + height: width + radius: width + } + } + } + } + + Emitter { + system: sys + // x: particleRoot.mousePos[0]-10 + // y: particleRoot.mousePos[1]-10 + x: particleRoot.width/2 + y: particleRoot.height/2 + height: 60 + width: 60 + emitRate: 200 + lifeSpan: 3000 + startTime: 0 + velocity: CumulativeDirection { + + AngleDirection { + angle: 0 + angleVariation: 360 + magnitude: 10 + magnitudeVariation: 0.2 + } + + + } + } + + Attractor { + pointX: particleRoot.mousePos[0] + pointY: particleRoot.mousePos[1] + strength: 200 + affectedParameter: Attractor.Position + proportionalToDistance: Attractor.InverseLinear + } + } + + } diff --git a/desktop/quickshell/rice/Colours.qml b/desktop/quickshell/rice/Colours.qml new file mode 100644 index 0000000..f0cd711 --- /dev/null +++ b/desktop/quickshell/rice/Colours.qml @@ -0,0 +1,34 @@ +pragma Singleton +import Quickshell + +Singleton { + property var c: { + "bg": "#1b2021", + "fg": "#cecbca", + + "black": "#272a2a", + "black_b": "#202e2f", + + "red": "#c43325", + "red_b": "#c46056", + + "green": "#8cc992", + "green_b": "#c2dab0", + + "yellow": "#ffb852", + "yellow_b": "#ffab5b", + + "blue": "#5299ff", + "blue_b": "#92beff", + + "magenta": "#645ac9", + "magenta_b": "#928cc9", + + "cyan": "#5abfc9", + "cyan_b": "#8cc4c9", + + "white": "#b0c2da", + "white_b": "#caccce", + + } +} diff --git a/desktop/quickshell/shell.qml b/desktop/quickshell/shell.qml new file mode 100644 index 0000000..a577dac --- /dev/null +++ b/desktop/quickshell/shell.qml @@ -0,0 +1,212 @@ +// components +import "bink" as Bink +import "launcher" as Launcher +// singletons +import "title" +import "tags" +import "rice" +import Quickshell +import Quickshell.Wayland +import QtQuick +import QtQuick.Controls + +ShellRoot { + // rhs main + Variants { + model: Quickshell.screens.filter(s => s.name == "DP-2") + delegate: PanelWindow { + id: bink + property var modelData + + anchors { + top: true + right: true + } + + implicitHeight: 22 + implicitWidth: 115 + color: "transparent" + + Rectangle { + anchors.fill: parent + bottomLeftRadius: 10 + color: Colours.c.black + } + + Rectangle { + anchors { + leftMargin: 8 + bottomMargin: 3 + fill: parent + } + color: Colours.c.black + bottomLeftRadius: 10 + Bink.Bink { + format: "ahms" + colours: ["transparent", Colours.c.yellow_b] + } + } + + exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Top + screen: modelData + } + } + + // centre main + Variants { + model: Quickshell.screens.filter(s => s.name == "DP-2") + delegate: PanelWindow { + id: windowTitle + property var modelData + anchors { + top: true + } + + TextMetrics { + id: textInfo + font { + family: "Inria Sans" + pointSize: 12 + } + elideWidth: 550 + elide: Qt.ElideMiddle + text: Title.currentWindow + } + + implicitHeight: textInfo.height + 6 + implicitWidth: (textInfo.width > textInfo.elideWidth ? textInfo.elideWidth : textInfo.width) + 24 + Behavior on implicitWidth { + NumberAnimation { + duration: 150 + easing.type: Easing.InOutQuad + } + } + color: "transparent" + + Rectangle { + anchors { + fill: parent + } + color: Colours.c.black + bottomLeftRadius: 10 + bottomRightRadius: 10 + + Rectangle { + anchors { + fill: parent + leftMargin: 12 + rightMargin: 12 + topMargin: 4 + } + color: "transparent" + Text { + font { + family: "Inria Sans" + pointSize: 12 + } + color: Colours.c.yellow_b + text: textInfo.elidedText + } + } + } + + exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Top + screen: modelData + } + } + + // bottom middle main + Variants { + model: Quickshell.screens.filter(s => s.name == "DP-2") + delegate: PanelWindow { + id: tags + property var modelData + anchors { + bottom: true + } + + implicitHeight: 22 + implicitWidth: Tags.keys.length * 13 + 24 + // implicitWidth:.width + 24 + color: "transparent" + + Rectangle { + anchors { + fill: parent + } + color: Colours.c.black + topLeftRadius: 10 + topRightRadius: 10 + + Rectangle { + anchors { + fill: parent + leftMargin: 12 + rightMargin: 12 + topMargin: 3 + } + Row { + anchors { + fill: parent + } + spacing: 1 + Repeater { + model: Tags.keys + delegate: Column { + id: baseCol + required property var modelData + spacing: 3 + Rectangle { + property var tag: Tags.tags[baseCol.modelData] + width: 12 + height: width + radius: width + color: tag.urgent ? Colours.c.red_b : (tag.enabled) ? Colours.c.yellow_b : Colours.c.black + Behavior on color { + ColorAnimation { + duration: 300 + } + } + border { + width: 1 + color: tag.urgent ? Colours.c.red_b : Colours.c.yellow_b + Behavior on color { + ColorAnimation { + duration: 300 + } + } + } + } + Rectangle { + property var tag: Tags.tags[baseCol.modelData] + anchors.horizontalCenter: parent.horizontalCenter + width: 1 + height: 1 + color: tag.urgent ? Colours.c.red_b : (tag.occupied) ? Colours.c.yellow_b : Colours.c.black + Behavior on color { + ColorAnimation { + duration: 300 + } + } + } + } + } + } + color: "transparent" + } // inner container + + }// outer visible rect + + exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Top + screen: modelData + }//invisible rect + } + + // pops up on current monitor + Launcher.Launcher { + width: 190 + } +} diff --git a/desktop/quickshell/tags/Tags.qml b/desktop/quickshell/tags/Tags.qml new file mode 100644 index 0000000..ab99b91 --- /dev/null +++ b/desktop/quickshell/tags/Tags.qml @@ -0,0 +1,80 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: data + property var tags: {} + property var keys: [] + Timer { + id: reconnectTimer + running: false + repeat: true + onTriggered: () => sock.connected = true + interval: 5000 + } + + Socket { + id: sock + connected: true + onConnectionStateChanged: () => { + reconnectTimer.running = sock.connected ? false : true + if (!sock.connected) { + data.tags = {} + data.keys = [] + } + } + path: Quickshell.env("XDG_RUNTIME_DIR") + "/niri-tag-events.sock" + parser: SplitParser { + onRead: tag_data => { + let event = JSON.parse(tag_data); + + let ensure = (d, t) => { + if (!d[t]) { + d[t] = { + occupied: false, + urgent: false, + enabled: false, + id: t + }; + } + return d; + }; + + if (event.TagEmpty) { + data.tags = ensure(data.tags, event.TagEmpty); + data.tags[event.TagEmpty].occupied = false; + } else if (event.TagOccupied) { + data.tags = ensure(data.tags, event.TagOccupied); + data.tags[event.TagOccupied].occupied = true; + } else if (event.TagUrgent) { + data.tags = ensure(data.tags, event.TagUrgent); + data.tags[event.TagUrgent].urgent = true; + } else if (event.TagEnabled) { + data.tags = ensure(data.tags, event.TagEnabled); + data.tags[event.TagEnabled].enabled = true; + } else if (event.TagDisabled) { + data.tags = ensure(data.tags, event.TagDisabled); + data.tags[event.TagDisabled].enabled = false; + } else if (event.TagExclusive) { + data.tags = ensure(data.tags, event.TagExclusive); + for (const [k] of Object.keys(data.tags)) { + data.tags[k].enabled = false; + } + data.tags[event.TagExclusive].enabled = true; + } else if (event.TagFullState) { + data["tags"] = event.TagFullState; + for (const [k, v] of Object.entries(data.tags)) { + data.tags[k].id = k; + } + } + + data.keys = Object.keys(data.tags); + data.tagsChanged(); + data.keysChanged(); + } + } + } +} diff --git a/desktop/quickshell/title/Title.qml b/desktop/quickshell/title/Title.qml new file mode 100644 index 0000000..7563d59 --- /dev/null +++ b/desktop/quickshell/title/Title.qml @@ -0,0 +1,52 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: data + property var currentWindow: " " + property var currentId: 0 + property var windows: {} + Process { + id: proc + command: ["niri", "msg", "-j", "event-stream"] + running: true + stdout: SplitParser { + onRead: niri_data => { + var event = JSON.parse(niri_data); + if (event.WindowsChanged) { + let new_data = event.WindowsChanged.windows.reduce((acc, el) => { + acc[el.id] = el.title == "" ? el.app_id : el.title; + return acc; + }, {}); + data.windows = new_data; + let focused = event.WindowsChanged.windows.find(w => w.is_focused); + if (focused === undefined) { + data.currentId = 0; + } + } else if (event.WindowOpenedOrChanged) { + let target = event.WindowOpenedOrChanged.window; + data.windows[target.id] = target.title == "" ? target.app_id : target.title; + if (target.is_focused) { + data.currentId = target.id; + } + } else if (event.WindowClosed) { + delete data[event.WindowClosed.id]; + if (Object.keys(data).length == 0) { + data.currentId = 0; + } + } else if (event.WindowFocusChanged) { + if (event.WindowFocusChanged.id === null) { + data.currentId = 0; + } else { + data.currentId = event.WindowFocusChanged.id; + } + } + + data.currentWindow = data.currentId == 0 ? " " : data.windows[data.currentId]; + } + } + } +} diff --git a/desktop/shell.nix b/desktop/shell.nix index 69e40b9..09891da 100644 --- a/desktop/shell.nix +++ b/desktop/shell.nix @@ -3,6 +3,7 @@ lib, inputs, rice, + mainUser, ... }: let @@ -58,4 +59,17 @@ in Requires = [ "graphical-session.target" ]; }; }; + + imports = [ inputs.arbys.nixosModules.arbys ]; + environment = { + arbys = { + enable = true; + clobber = true; + }; + files."/home/${mainUser}/.config/quickshell" = { + source = "/home/${mainUser}/.nix/desktop/quickshell"; + uid = 1000; + gid = 100; + }; + }; } diff --git a/flake.lock b/flake.lock index 71fce62..a788687 100644 --- a/flake.lock +++ b/flake.lock @@ -38,6 +38,21 @@ "type": "github" } }, + "arbys": { + "locked": { + "lastModified": 1753022411, + "narHash": "sha256-m4pEYNwUZh64FyRIM1TSmRn1n4A85WEzVu5Gk2VHvAY=", + "ref": "refs/heads/meats", + "rev": "e7fa12d6f3f5b723bce413acdfa5461fbaa3ec97", + "revCount": 1, + "type": "git", + "url": "https://git.atagen.co/atagen/arbys" + }, + "original": { + "type": "git", + "url": "https://git.atagen.co/atagen/arbys" + } + }, "crane": { "locked": { "lastModified": 1751562746, @@ -1154,6 +1169,7 @@ "inputs": { "__flake-compat": "__flake-compat", "angrr": "angrr", + "arbys": "arbys", "culr": "culr", "helix": "helix", "hjem": "hjem", diff --git a/flake.nix b/flake.nix index 941f2dc..260ae0c 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,8 @@ angrr.url = "github:linyinfeng/angrr"; + arbys.url = "git+https://git.atagen.co/atagen/arbys"; + __flake-compat = { url = "git+https://git.lix.systems/lix-project/flake-compat.git"; flake = false;