diff options
| author | coast <coasteen@proton.me> | 2026-09-07 07:07:22 +0330 |
|---|---|---|
| committer | coast <coasteen@proton.me> | 2026-09-07 07:07:22 +0330 |
| commit | 7922ded35ff292e485c3369dab9726b9142ce526 (patch) | |
| tree | ea47cb7bb569b5fb8c44ede3357f1a8b4b41722c /home-configuration/config | |
| parent | a7cdf9580a901ed65f179417f6ed8a0848491b66 (diff) | |
| download | nix-7922ded35ff292e485c3369dab9726b9142ce526.tar.gz nix-7922ded35ff292e485c3369dab9726b9142ce526.tar.bz2 nix-7922ded35ff292e485c3369dab9726b9142ce526.tar.xz nix-7922ded35ff292e485c3369dab9726b9142ce526.tar.zst | |
a lot of quickshell
Diffstat (limited to 'home-configuration/config')
12 files changed, 1875 insertions, 180 deletions
diff --git a/home-configuration/config/niri/config.kdl b/home-configuration/config/niri/config.kdl index b5ad75d..e8fe85c 100644 --- a/home-configuration/config/niri/config.kdl +++ b/home-configuration/config/niri/config.kdl @@ -1,8 +1,5 @@ -spawn-at-startup "waybar" -spawn-at-startup "awww-daemon" -spawn-at-startup "awww" "img" "/home/coast/Wallpapers/nature2art.png" spawn-sh-at-startup "XDG_DATA_DIRS=/etc/profiles/per-user/coast/share:/run/current-system/sw/share:$HOME/.nix-profile/share:$XDG_DATA_DIRS" -spawn-at-startup "qs" +spawn-at-startup "quickshell -c ~/.config/quickshell" spawn-at-startup "dbus-update-activation-environment" "--systemd" "WAYLAND_DISPLAY" "XDG_CURRENT_DESKTOP" input { @@ -236,6 +233,8 @@ binds { Mod+Space { toggle-window-floating; } Mod+Shift+V { switch-focus-between-floating-and-tiling; } + Mod+Ctrl+P { spawn-sh "qs ipc call shell restartBar"; } + Mod+Shift+F { toggle-column-tabbed-display; } Mod+F hotkey-overlay-title="Spawn file manager (nautilus)" { spawn-sh "XDG_DATA_DIRS=/etc/profiles/per-user/coast/share:/run/current-system/sw/share:$HOME/.nix-profile/share:$XDG_DATA_DIRS nautilus"; } Print { screenshot; } diff --git a/home-configuration/config/quickshell/config.js b/home-configuration/config/quickshell/config.js index 08d6518..8005a13 100644 --- a/home-configuration/config/quickshell/config.js +++ b/home-configuration/config/quickshell/config.js @@ -1,22 +1,64 @@ -const colors = { - bg: "#121212", - bgDark: "#121212", - fg: "#cdd6f4", - muted: "#444b6a", - cyan: "#d3869b", - purple: "#cba6f7", - red: "#f38ba8", - yellow: "#e0af68", - blue: "#89b4fa", -} -const bar = { - fontFamily: "Iosevka Nerd Font", - fontSize: 14, - height: 44, -} -const notifications = { - timeout: 5000, -} -const wallpaper = { - wallpaper: "file:///home/coast/Wallpapers/nature2art.png" -} +const barHeight = 42 +const barOutput = "HDMI-A-1" +const barBackground = "#000000" +const barMargin = 8 + +const accentGreen = "#a6e3a1" +const accentYellow = "#f9e2af" +const accentOrange = "#fab387" +const accentRed = "#f38ba8" + +const textPrimary = "#cdd6f4" +const textSecondary = "#a6adc8" +const textInactive = "#89b4fa" +const textWindow = "#d3869b" +const textDark = "#09090F" + +const workspaceBackground = "#16161a" + +const fontFamily = "Iosevka Nerd Font" +const fontSize = 14 + +const blockHeight = 30 +const blockRadius = 12 +const blockPadding = 24 + +const workspaceWidth = 39 +const workspaceSpacing = 6 + +const gradientAnimationDuration = 3000 + +var wallpapers = [ + { + path: "/home/coast/Wallpapers/nature2art.png", + outputs: ["HDMI-A-1"], + fillMode: "PreserveAspectCrop", + opacity: 1.0, + blur: 0.0, + asynchronous: true, + cache: true + }, + { + path: "/home/coast/Wallpapers/nature2art.png", + outputs: ["DP-1"], + fillMode: "PreserveAspectCrop", + opacity: 1.0, + blur: 0.0, + asynchronous: true, + cache: true + } +] + +var notificationWidth = 360 +var notificationMarginTop = 50 +var notificationMarginRight = 12 +var notificationSpacing = 8 +var notificationBorder = "#313244" +var notificationTimeout = 5000 +var notificationAnimationDuration = 200 +var notificationSlideDistance = 30 + +const notificationVesktopAppId = "vesktop" +const notificationVesktopNames = ["vesktop", "discord"] + +const musicMaxWidth = 400 diff --git a/home-configuration/config/quickshell/modules/bar/Bar.qml b/home-configuration/config/quickshell/modules/bar/Bar.qml new file mode 100644 index 0000000..3fc6b36 --- /dev/null +++ b/home-configuration/config/quickshell/modules/bar/Bar.qml @@ -0,0 +1,154 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import "../../config.js" as Config + +PanelWindow { + id: bar + + anchors { + top: true + left: true + right: true + } + + implicitHeight: Config.barHeight + color: "transparent" + + Rectangle { + id: content + + anchors.fill: parent + color: Config.barBackground + opacity: 0 + y: -Config.barHeight + + RowLayout { + anchors { + left: parent.left + leftMargin: Config.barMargin + verticalCenter: parent.verticalCenter + } + + spacing: Config.workspaceSpacing + + Loader { + active: true + sourceComponent: Workspaces {} + } + } + + Rectangle { + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + height: Config.blockHeight + width: windowTitle.contentWidth + Config.blockPadding + radius: Config.blockRadius + color: "transparent" + + Text { + id: windowTitle + + anchors.centerIn: parent + + text: niri.focusedWindow?.title ?? "" + + color: Config.textWindow + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + elide: Text.ElideRight + } + } + + RowLayout { + id: rightLayout + + anchors { + right: parent.right + rightMargin: Config.barMargin + verticalCenter: parent.verticalCenter + } + + spacing: Config.workspaceSpacing + + Loader { + active: true + sourceComponent: Music {} + } + + Loader { + active: true + sourceComponent: Time {} + } + + Loader { + active: true + sourceComponent: QuickControl {} + } + } + } + + ParallelAnimation { + id: showAnimation + + NumberAnimation { + target: content + property: "y" + from: -Config.barHeight + to: 0 + duration: Config.barAnimationDuration + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: content + property: "opacity" + from: 0 + to: 1 + duration: Config.barAnimationDuration + easing.type: Easing.OutCubic + } + } + + ParallelAnimation { + id: hideAnimation + + NumberAnimation { + target: content + property: "y" + from: 0 + to: -Config.barHeight + duration: Config.barAnimationDuration + easing.type: Easing.InCubic + } + + NumberAnimation { + target: content + property: "opacity" + from: 1 + to: 0 + duration: Config.barAnimationDuration + easing.type: Easing.InCubic + } + + onFinished: { + showAnimation.start() + } + } + + Component.onCompleted: { + showAnimation.start() + } + + function restart() { + showAnimation.stop() + hideAnimation.stop() + hideAnimation.start() + } +} diff --git a/home-configuration/config/quickshell/modules/bar/Music.qml b/home-configuration/config/quickshell/modules/bar/Music.qml new file mode 100644 index 0000000..ec16fed --- /dev/null +++ b/home-configuration/config/quickshell/modules/bar/Music.qml @@ -0,0 +1,219 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Mpris +import "../../config.js" as Config + +Rectangle { + id: musicBlock + + property var player: null + + implicitWidth: visible + ? Math.min(textMetrics.width + Config.blockPadding + 8, 400) + : 0 + + implicitHeight: Config.blockHeight + + radius: Config.blockRadius + + visible: player !== null && player.isPlaying + + function findFeishin() { + player = null + + for (var i = 0; i < Mpris.players.values.length; i++) { + var p = Mpris.players.values[i] + + console.log( + "MPRIS:", + "identity =", p.identity, + "desktopEntry =", p.desktopEntry + ) + + var identity = (p.identity || "").toLowerCase() + var desktopEntry = (p.desktopEntry || "").toLowerCase() + + if ( + identity === "feishin" || + desktopEntry === "feishin" + ) { + player = p + console.log("FEISHIN FOUND") + return + } + } + + console.log("FEISHIN NOT FOUND") + } + + function cleanTitle(title) { + if (!title) + return "" + + return title + .replace(/\s*[\(\[][^)\]]*\blive\b[^)\]]*[\)\]]/gi, "") + .replace(/\s*[-–—]\s*\blive\b.*$/i, "") + .trim() + } + + Component.onCompleted: { + findFeishin() + } + + Connections { + target: Mpris.players + + function onValuesChanged() { + findFeishin() + } + } + + gradient: Gradient { + GradientStop { + id: stop1 + position: 0.0 + color: Config.accentGreen + } + + GradientStop { + id: stop2 + position: 0.33 + color: Config.accentYellow + } + + GradientStop { + id: stop3 + position: 0.66 + color: Config.accentOrange + } + + GradientStop { + id: stop4 + position: 1.0 + color: Config.accentRed + } + } + + TextMetrics { + id: textMetrics + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + text: player + ? " " + player.trackArtist + " — " + cleanTitle(player.trackTitle) + : "" + } + + Text { + id: musicText + + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + } + + leftPadding: Config.blockPadding / 2 + rightPadding: Config.blockPadding / 2 + + text: player + ? " " + player.trackArtist + " — " + cleanTitle(player.trackTitle) + : "" + + color: Config.textDark + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + elide: Text.ElideRight + maximumLineCount: 1 + } + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (player && player.canRaise) + player.raise() + } + } + + SequentialAnimation { + running: true + loops: Animation.Infinite + + ParallelAnimation { + ColorAnimation { + target: stop1 + property: "color" + from: Config.accentGreen + to: Config.accentRed + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop2 + property: "color" + from: Config.accentYellow + to: Config.accentGreen + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop3 + property: "color" + from: Config.accentOrange + to: Config.accentYellow + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop4 + property: "color" + from: Config.accentRed + to: Config.accentOrange + duration: Config.gradientAnimationDuration + } + } + + ParallelAnimation { + ColorAnimation { + target: stop1 + property: "color" + from: Config.accentRed + to: Config.accentGreen + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop2 + property: "color" + from: Config.accentGreen + to: Config.accentYellow + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop3 + property: "color" + from: Config.accentYellow + to: Config.accentOrange + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop4 + property: "color" + from: Config.accentOrange + to: Config.accentRed + duration: Config.gradientAnimationDuration + } + } + } +} diff --git a/home-configuration/config/quickshell/modules/bar/QuickControl.qml b/home-configuration/config/quickshell/modules/bar/QuickControl.qml new file mode 100644 index 0000000..ffe6f6b --- /dev/null +++ b/home-configuration/config/quickshell/modules/bar/QuickControl.qml @@ -0,0 +1,519 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire +import Quickshell.Services.SystemTray +import "../../config.js" as Config + +Rectangle { + id: root + + property bool expanded: false + + implicitWidth: layout.implicitWidth + 24 + implicitHeight: Config.blockHeight + + radius: Config.blockRadius + + gradient: Gradient { + GradientStop { + id: stop1 + position: 0.0 + color: Config.accentGreen + } + + GradientStop { + id: stop2 + position: 0.33 + color: Config.accentYellow + } + + GradientStop { + id: stop3 + position: 0.66 + color: Config.accentOrange + } + + GradientStop { + id: stop4 + position: 1.0 + color: Config.accentRed + } + } + + Behavior on implicitWidth { + NumberAnimation { + duration: 250 + easing.type: Easing.OutCubic + } + } + + Component.onCompleted: { + console.info("QuickControl: loaded") + console.info( + "QuickControl: tray items =", + SystemTray.items.values.length + ) + + if (Pipewire.defaultAudioSink) { + console.info( + "QuickControl: audio sink =", + Pipewire.defaultAudioSink.name + ) + + if (Pipewire.defaultAudioSink.audio) { + console.info( + "QuickControl: volume =", + Math.round( + Pipewire.defaultAudioSink.audio.volume * 100 + ) + "%", + "muted =", + Pipewire.defaultAudioSink.audio.muted + ) + } + } else { + console.warn("QuickControl: no default audio sink") + } + } + + onExpandedChanged: { + console.info( + "QuickControl: expanded =", + expanded + ) + } + + RowLayout { + id: layout + + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + leftMargin: 12 + rightMargin: 12 + } + + spacing: 6 + + Text { + id: volumeIcon + + text: Pipewire.defaultAudioSink?.audio?.muted + ? "" + : "" + + visible: root.expanded + + color: Config.textDark + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (!Pipewire.defaultAudioSink?.audio) { + console.warn( + "QuickControl: no audio sink" + ) + return + } + + var audio = Pipewire.defaultAudioSink.audio + + audio.muted = !audio.muted + + console.info( + "QuickControl: mute =", + audio.muted + ) + } + } + } + + Text { + id: volumeText + + text: Pipewire.defaultAudioSink?.audio + ? Math.round( + Pipewire.defaultAudioSink.audio.volume * 100 + ) + "%" + : "0%" + + visible: root.expanded + + color: Config.textDark + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (!Pipewire.defaultAudioSink?.audio) { + console.warn( + "QuickControl: no audio sink" + ) + return + } + + var audio = Pipewire.defaultAudioSink.audio + + audio.volume = Math.min( + audio.volume + 0.05, + 1.0 + ) + + console.info( + "QuickControl: volume =", + Math.round(audio.volume * 100) + "%" + ) + } + } + } + + Text { + id: brightnessIcon + + text: "" + + visible: root.expanded + + color: Config.textDark + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + console.info( + "QuickControl: brightness clicked" + ) + + Quickshell.execDetached([ + "brightnessctl", + "set", + "+5%" + ]) + } + } + } + + Text { + id: networkIcon + + text: { + var item = SystemTray.items.values.find(function(item) { + return item.id === "nm-applet" + }) + + if (!item) + return "" + + var title = (item.title || "").toLowerCase() + + if (title.indexOf("ethernet") !== -1 || + title.indexOf("wired") !== -1 || + title.indexOf("lan") !== -1) + return "" + + return "" + } + + visible: root.expanded + color: Config.textDark + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + + onClicked: { + console.info("QuickControl: network clicked") + Quickshell.execDetached(["nm-connection-editor"]) + } + } +} + + Repeater { + model: root.expanded + ? SystemTray.items + : null + + delegate: Item { + id: trayItem + + required property var modelData + + implicitWidth: 18 + implicitHeight: 18 + + Layout.preferredWidth: 18 + Layout.preferredHeight: 18 + + Component.onCompleted: { + console.info( + "QuickControl: tray item:", + modelData.title, + "id =", + modelData.id, + "hasMenu =", + modelData.hasMenu, + "onlyMenu =", + modelData.onlyMenu + ) + } + + QsMenuAnchor { + id: menuAnchor + + anchor.item: trayItem + + menu: modelData.menu + + onOpened: { + console.info( + "QuickControl: context menu opened:", + modelData.title + ) + } + + onClosed: { + console.info( + "QuickControl: context menu closed:", + modelData.title + ) + } + } + + Image { + anchors.fill: parent + + source: modelData.icon + + sourceSize.width: 18 + sourceSize.height: 18 + + fillMode: Image.PreserveAspectFit + } + + MouseArea { + anchors.fill: parent + + acceptedButtons: + Qt.LeftButton | + Qt.RightButton | + Qt.MiddleButton + + cursorShape: Qt.PointingHandCursor + + onClicked: function(mouse) { + if (mouse.button === Qt.RightButton) { + console.info( + "QuickControl: right click:", + modelData.title, + "hasMenu =", + modelData.hasMenu + ) + + if (modelData.hasMenu) { + console.info( + "QuickControl: opening menu:", + modelData.title + ) + + menuAnchor.open() + } else { + console.info( + "QuickControl: no context menu:", + modelData.title + ) + } + + return + } + + if (mouse.button === Qt.MiddleButton) { + console.info( + "QuickControl: middle click:", + modelData.title + ) + + modelData.secondaryActivate() + return + } + + console.info( + "QuickControl: left click:", + modelData.title + ) + + if (modelData.onlyMenu && modelData.hasMenu) { + console.info( + "QuickControl: onlyMenu item, opening menu:", + modelData.title + ) + + menuAnchor.open() + } else { + modelData.activate() + } + } + } + } + } + + Text { + id: dots + + text: "•••" + + color: Config.textDark + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + Layout.alignment: Qt.AlignVCenter + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.expanded = !root.expanded + + console.info( + "QuickControl: toggled, expanded =", + root.expanded + ) + } + } + } + } + + Connections { + target: SystemTray.items + + function onValuesChanged() { + console.info( + "QuickControl: system tray changed, items =", + SystemTray.items.values.length + ) + } + } + + Connections { + target: Pipewire.defaultAudioSink?.audio ?? null + + function onVolumeChanged() { + if (Pipewire.defaultAudioSink?.audio) { + console.info( + "QuickControl: volume changed:", + Math.round( + Pipewire.defaultAudioSink.audio.volume * 100 + ) + "%" + ) + } + } + + function onMutedChanged() { + if (Pipewire.defaultAudioSink?.audio) { + console.info( + "QuickControl: muted changed:", + Pipewire.defaultAudioSink.audio.muted + ) + } + } + } + + SequentialAnimation { + running: true + loops: Animation.Infinite + + ParallelAnimation { + ColorAnimation { + target: stop1 + property: "color" + from: Config.accentGreen + to: Config.accentRed + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop2 + property: "color" + from: Config.accentYellow + to: Config.accentGreen + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop3 + property: "color" + from: Config.accentOrange + to: Config.accentYellow + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop4 + property: "color" + from: Config.accentRed + to: Config.accentOrange + duration: Config.gradientAnimationDuration + } + } + + ParallelAnimation { + ColorAnimation { + target: stop1 + property: "color" + from: Config.accentRed + to: Config.accentGreen + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop2 + property: "color" + from: Config.accentGreen + to: Config.accentYellow + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop3 + property: "color" + from: Config.accentYellow + to: Config.accentOrange + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop4 + property: "color" + from: Config.accentOrange + to: Config.accentRed + duration: Config.gradientAnimationDuration + } + } + } +} diff --git a/home-configuration/config/quickshell/modules/bar/Time.qml b/home-configuration/config/quickshell/modules/bar/Time.qml new file mode 100644 index 0000000..dfa3f0d --- /dev/null +++ b/home-configuration/config/quickshell/modules/bar/Time.qml @@ -0,0 +1,144 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import "../../config.js" as Config + +Rectangle { + id: clockBlock + + implicitWidth: timeText.contentWidth + Config.blockPadding + implicitHeight: Config.blockHeight + + radius: Config.blockRadius + + property bool alternateFormat: false + + gradient: Gradient { + GradientStop { + id: stop1 + position: 0.0 + color: Config.accentGreen + } + + GradientStop { + id: stop2 + position: 0.33 + color: Config.accentYellow + } + + GradientStop { + id: stop3 + position: 0.66 + color: Config.accentOrange + } + + GradientStop { + id: stop4 + position: 1.0 + color: Config.accentRed + } + } + + SystemClock { + id: clock + precision: SystemClock.Seconds + } + + Text { + id: timeText + + anchors.centerIn: parent + + text: alternateFormat + ? Qt.formatDateTime(clock.date, " dddd, dd MMMM yyyy") + : Qt.formatDateTime(clock.date, " hh:mm AP • ddd, dd/MM") + + color: Config.textDark + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + } + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + alternateFormat = !alternateFormat + } + } + + SequentialAnimation { + running: true + loops: Animation.Infinite + + ParallelAnimation { + ColorAnimation { + target: stop1 + property: "color" + from: Config.accentGreen + to: Config.accentRed + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop2 + property: "color" + from: Config.accentYellow + to: Config.accentGreen + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop3 + property: "color" + from: Config.accentOrange + to: Config.accentYellow + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop4 + property: "color" + from: Config.accentRed + to: Config.accentOrange + duration: Config.gradientAnimationDuration + } + } + + ParallelAnimation { + ColorAnimation { + target: stop1 + property: "color" + from: Config.accentRed + to: Config.accentGreen + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop2 + property: "color" + from: Config.accentGreen + to: Config.accentYellow + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop3 + property: "color" + from: Config.accentYellow + to: Config.accentOrange + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop4 + property: "color" + from: Config.accentOrange + to: Config.accentRed + duration: Config.gradientAnimationDuration + } + } + } +} diff --git a/home-configuration/config/quickshell/modules/bar/Workspaces.qml b/home-configuration/config/quickshell/modules/bar/Workspaces.qml new file mode 100644 index 0000000..3962a63 --- /dev/null +++ b/home-configuration/config/quickshell/modules/bar/Workspaces.qml @@ -0,0 +1,169 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import "../../config.js" as Config + +Rectangle { + id: workspaceBlock + + implicitWidth: workspaceRow.width + 16 + implicitHeight: Config.blockHeight + + color: "transparent" + + RowLayout { + id: workspaceRow + + anchors.verticalCenter: parent.verticalCenter + spacing: Config.workspaceSpacing + + Repeater { + model: niri.workspaces + + delegate: Rectangle { + id: workspace + + visible: model.output === Config.barOutput && index < 11 + + implicitWidth: Config.workspaceWidth + implicitHeight: Config.blockHeight + + radius: Config.blockRadius + + color: model.isActive + ? "transparent" + : Config.workspaceBackground + + layer.enabled: true + + Rectangle { + anchors.fill: parent + radius: parent.radius + visible: model.isActive + + gradient: Gradient { + GradientStop { + id: stop1 + position: 0.0 + color: Config.accentGreen + } + + GradientStop { + id: stop2 + position: 0.33 + color: Config.accentYellow + } + + GradientStop { + id: stop3 + position: 0.66 + color: Config.accentOrange + } + + GradientStop { + id: stop4 + position: 1.0 + color: Config.accentRed + } + } + + SequentialAnimation { + running: true + loops: Animation.Infinite + + ParallelAnimation { + ColorAnimation { + target: stop1 + property: "color" + from: Config.accentGreen + to: Config.accentRed + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop2 + property: "color" + from: Config.accentYellow + to: Config.accentGreen + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop3 + property: "color" + from: Config.accentOrange + to: Config.accentYellow + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop4 + property: "color" + from: Config.accentRed + to: Config.accentOrange + duration: Config.gradientAnimationDuration + } + } + + ParallelAnimation { + ColorAnimation { + target: stop1 + property: "color" + from: Config.accentRed + to: Config.accentGreen + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop2 + property: "color" + from: Config.accentGreen + to: Config.accentYellow + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop3 + property: "color" + from: Config.accentYellow + to: Config.accentOrange + duration: Config.gradientAnimationDuration + } + + ColorAnimation { + target: stop4 + property: "color" + from: Config.accentOrange + to: Config.accentRed + duration: Config.gradientAnimationDuration + } + } + } + } + + Text { + anchors.centerIn: parent + + text: index + color: model.isActive + ? Config.textDark + : Config.textInactive + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + } + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + niri.focusWorkspaceById(model.id) + } + } + } + } + } +} diff --git a/home-configuration/config/quickshell/modules/desktop/Wallpaper.qml b/home-configuration/config/quickshell/modules/desktop/Wallpaper.qml new file mode 100644 index 0000000..557fef9 --- /dev/null +++ b/home-configuration/config/quickshell/modules/desktop/Wallpaper.qml @@ -0,0 +1,63 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import "../../config.js" as Config + +Variants { + model: Quickshell.screens + + PanelWindow { + required property var modelData + + property var wallpaper: Config.wallpapers.find(function(w) { + return w.outputs.indexOf(modelData.name) !== -1 + }) + + screen: modelData + visible: wallpaper !== undefined + + anchors { + top: true + bottom: true + left: true + right: true + } + + exclusionMode: ExclusionMode.Ignore + + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.namespace: "wallpaper" + + Image { + id: wallpaperImage + + anchors.fill: parent + + source: wallpaper ? wallpaper.path : "" + + fillMode: wallpaper + ? Image[wallpaper.fillMode] + : Image.PreserveAspectCrop + + opacity: wallpaper ? wallpaper.opacity : 1.0 + + asynchronous: wallpaper + ? wallpaper.asynchronous + : true + + cache: wallpaper + ? wallpaper.cache + : true + } + + MultiEffect { + anchors.fill: wallpaperImage + + source: wallpaperImage + + blurEnabled: wallpaper ? wallpaper.blur > 0 : false + blur: wallpaper ? wallpaper.blur : 0 + } + } +} diff --git a/home-configuration/config/quickshell/modules/notifications/NotificationCard.qml b/home-configuration/config/quickshell/modules/notifications/NotificationCard.qml new file mode 100644 index 0000000..983cbd7 --- /dev/null +++ b/home-configuration/config/quickshell/modules/notifications/NotificationCard.qml @@ -0,0 +1,257 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Notifications +import "../../config.js" as Config + +Rectangle { + id: root + + required property var notification + + signal activated() + + property bool dismissing: false + + Layout.fillWidth: true + + implicitHeight: body.implicitHeight + 20 + + radius: Config.blockRadius + + color: Config.barBackground + + border.width: 1 + border.color: Config.notificationBorder + + x: dismissing ? Config.notificationSlideDistance : 0 + opacity: dismissing ? 0 : 1 + + Component.onCompleted: { + x = Config.notificationSlideDistance + opacity = 0 + + enterAnimation.start() + + if (notification.urgency !== NotificationUrgency.Critical) + dismissTimer.start() + } + + Timer { + id: dismissTimer + + interval: Config.notificationTimeout + repeat: false + + onTriggered: { + dismiss() + } + } + + function dismiss() { + if (dismissing) + return + + dismissTimer.stop() + dismissing = true + exitAnimation.start() + } + + SequentialAnimation { + id: enterAnimation + + ParallelAnimation { + NumberAnimation { + target: root + property: "x" + from: Config.notificationSlideDistance + to: 0 + duration: Config.notificationAnimationDuration + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: root + property: "opacity" + from: 0 + to: 1 + duration: Config.notificationAnimationDuration + easing.type: Easing.OutCubic + } + } + } + + SequentialAnimation { + id: exitAnimation + + ParallelAnimation { + NumberAnimation { + target: root + property: "x" + to: Config.notificationSlideDistance + duration: Config.notificationAnimationDuration + easing.type: Easing.InCubic + } + + NumberAnimation { + target: root + property: "opacity" + to: 0 + duration: Config.notificationAnimationDuration + easing.type: Easing.InCubic + } + } + + ScriptAction { + script: root.notification.expire() + } + } + + ColumnLayout { + id: body + + anchors.fill: parent + anchors.margins: 10 + + spacing: 6 + + RowLayout { + Layout.fillWidth: true + + Image { + visible: root.notification.appIcon !== "" + + source: root.notification.appIcon + + sourceSize.width: 24 + sourceSize.height: 24 + + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + + fillMode: Image.PreserveAspectFit + asynchronous: true + } + + Text { + Layout.fillWidth: true + + text: root.notification.appName || "Notification" + + color: Config.textPrimary + + font.family: Config.fontFamily + font.pixelSize: 12 + font.bold: true + + elide: Text.ElideRight + } + + Text { + text: "×" + + color: "#6c7086" + + font.family: Config.fontFamily + font.pixelSize: 18 + font.bold: true + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.dismiss() + } + } + } + } + + Text { + Layout.fillWidth: true + + text: root.notification.summary + + color: Config.textPrimary + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + wrapMode: Text.Wrap + } + + Text { + visible: root.notification.body !== "" + + Layout.fillWidth: true + + text: root.notification.body + + color: Config.textSecondary + + font.family: Config.fontFamily + font.pixelSize: 12 + + wrapMode: Text.Wrap + + maximumLineCount: 4 + elide: Text.ElideRight + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 3 + + radius: 2 + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Config.accentGreen + } + + GradientStop { + position: 0.33 + color: Config.accentYellow + } + + GradientStop { + position: 0.66 + color: Config.accentOrange + } + + GradientStop { + position: 1.0 + color: Config.accentRed + } + } + } + } + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.activated() + } + } + + MouseArea { + anchors.right: parent.right + anchors.top: parent.top + + width: 36 + height: 36 + + z: 10 + + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.dismiss() + } + } +} diff --git a/home-configuration/config/quickshell/modules/notifications/NotificationFocus.qml b/home-configuration/config/quickshell/modules/notifications/NotificationFocus.qml new file mode 100644 index 0000000..af52eaa --- /dev/null +++ b/home-configuration/config/quickshell/modules/notifications/NotificationFocus.qml @@ -0,0 +1,123 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import "../../config.js" as Config + +Item { + id: root + + property string pendingApp: "" + + Process { + id: windowsProcess + + command: ["niri", "msg", "-j", "windows"] + + stdout: StdioCollector { + id: windowsOutput + + onStreamFinished: { + var windows + + try { + windows = JSON.parse(this.text) + } catch (error) { + console.error( + "Failed to parse niri windows:", + error + ) + + root.pendingApp = "" + return + } + + var app = root.pendingApp.toLowerCase() + root.pendingApp = "" + + if (Config.notificationVesktopNames.indexOf(app) !== -1) { + for (var i = 0; i < windows.length; i++) { + var niriWindow = windows[i] + var appId = (niriWindow.app_id || "").toLowerCase() + + if (appId === Config.notificationVesktopAppId) { + console.log( + "FOCUSING VESKTOP WINDOW:", + niriWindow.id, + niriWindow.title + ) + + focusProcess.exec([ + "niri", + "msg", + "action", + "focus-window", + "--id", + String(niriWindow.id) + ]) + + return + } + } + + console.log( + "No matching Niri window found for:", + app + ) + + return + } + + for (var j = 0; j < windows.length; j++) { + var window = windows[j] + var windowAppId = (window.app_id || "").toLowerCase() + + if ( + windowAppId === app || + windowAppId === app + ".desktop" + ) { + console.log( + "FOCUSING WINDOW:", + window.id, + window.app_id, + window.title + ) + + focusProcess.exec([ + "niri", + "msg", + "action", + "focus-window", + "--id", + String(window.id) + ]) + + return + } + } + + console.log( + "No matching Niri window found for:", + app + ) + } + } + } + + Process { + id: focusProcess + } + + function focusSource(notification) { + if (!notification) + return + + root.pendingApp = notification.appName + + windowsProcess.exec([ + "niri", + "msg", + "-j", + "windows" + ]) + } +} diff --git a/home-configuration/config/quickshell/modules/notifications/Notifications.qml b/home-configuration/config/quickshell/modules/notifications/Notifications.qml new file mode 100644 index 0000000..2965b90 --- /dev/null +++ b/home-configuration/config/quickshell/modules/notifications/Notifications.qml @@ -0,0 +1,125 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import "../../config.js" as Config +import "./" + +Item { + id: root + + NotificationServer { + id: server + + bodySupported: true + imageSupported: true + actionsSupported: true + actionIconsSupported: true + bodyMarkupSupported: true + + onNotification: function(notification) { + console.log("NOTIFICATION") + console.log(" app:", notification.appName) + console.log(" summary:", notification.summary) + console.log(" body:", notification.body) + console.log(" actions:", notification.actions.length) + + for (var i = 0; i < notification.actions.length; i++) { + console.log( + " action:", + notification.actions[i].identifier, + notification.actions[i].text + ) + } + + notification.tracked = true + } + } + + NotificationFocus { + id: focus + } + + function activateNotification(notification) { + if (!notification) + return + + console.log( + "ACTIVATE:", + notification.appName, + notification.summary + ) + + focus.focusSource(notification) + + for (var i = 0; i < notification.actions.length; i++) { + var action = notification.actions[i] + + if (action.identifier === "default") { + console.log( + "Invoking default action:", + action.text + ) + + action.invoke() + return + } + } + + console.log("No default action available") + notification.dismiss() + } + + PanelWindow { + id: window + + screen: Quickshell.screens.find(function(screen) { + return screen.name === Config.barOutput + }) + + anchors { + top: true + right: true + } + + margins { + top: Config.notificationMarginTop + right: Config.notificationMarginRight + } + + exclusionMode: ExclusionMode.Ignore + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "notifications" + + color: "transparent" + + implicitWidth: Config.notificationWidth + implicitHeight: list.implicitHeight + + ColumnLayout { + id: list + + anchors.top: parent.top + anchors.right: parent.right + + width: Config.notificationWidth + spacing: Config.notificationSpacing + + Repeater { + model: server.trackedNotifications + + delegate: NotificationCard { + required property var modelData + + notification: modelData + + onActivated: { + root.activateNotification(modelData) + } + } + } + } + } +} diff --git a/home-configuration/config/quickshell/shell.qml b/home-configuration/config/quickshell/shell.qml index c9425cd..bda5dbb 100644 --- a/home-configuration/config/quickshell/shell.qml +++ b/home-configuration/config/quickshell/shell.qml @@ -1,169 +1,50 @@ -import Quickshell -import Quickshell.Wayland -import Quickshell.Services.Notifications +//@ pragma UseQApplication + import QtQuick import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Niri +import "./modules/bar/" +import "./modules/desktop/" +import "./modules/notifications/" -import "config.js" as Config - -Scope { +ShellRoot { id: root - NotificationServer { - id: server - actionsSupported: true - bodySupported: true - imageSupported: true + Niri { + id: niri + Component.onCompleted: connect() - onNotification: n => { - n.tracked = true + onConnected: console.info("Connected to niri") + onErrorOccurred: function(error) { + console.error("Niri error:", error) } } - PanelWindow { - anchors { top: true; right: true } - margins { top: 12; right: 12 } - - implicitWidth: 380 - implicitHeight: Math.max(1, column.implicitHeight) - color: "transparent" - exclusionMode: ExclusionMode.Ignore - - ColumnLayout { - id: column - width: parent.width - spacing: 10 - - Repeater { - model: server.trackedNotifications - - delegate: Rectangle { - id: card - required property var modelData - - Layout.fillWidth: true - implicitHeight: cardLayout.implicitHeight + 20 - - radius: 12 - color: Config.colors.bgDark - border.color: hoverArea.containsMouse ? Config.colors.purple : "transparent" - border.width: 1 - - Behavior on border.color { ColorAnimation { duration: 150 } } - - Timer { - id: dismissTimer - running: card.modelData.urgency !== NotificationUrgency.Critical && !hoverArea.containsMouse - interval: Config.notifications.timeout - repeat: false - onTriggered: card.modelData.dismiss() - } - - Rectangle { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - width: 4 - radius: 12 - color: card.modelData.urgency === NotificationUrgency.Critical - ? Config.colors.red - : Config.colors.purple - } - - ColumnLayout { - id: cardLayout - anchors.fill: parent - anchors.margins: 10 - anchors.leftMargin: 18 - spacing: 8 - - RowLayout { - Layout.fillWidth: true - spacing: 10 - - Image { - Layout.preferredWidth: 36 - Layout.preferredHeight: 36 - Layout.alignment: Qt.AlignTop - fillMode: Image.PreserveAspectFit - visible: source.toString() !== "" - source: card.modelData.image || card.modelData.appIcon || "" - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Text { - Layout.fillWidth: true - text: card.modelData.summary - color: Config.colors.cyan - font.family: Config.bar.fontFamily - font.pixelSize: Config.bar.fontSize - font.bold: true - elide: Text.ElideRight - } - - Text { - Layout.fillWidth: true - visible: text !== "" - text: card.modelData.body - color: Config.colors.fg - font.family: Config.bar.fontFamily - font.pixelSize: Config.bar.fontSize - 1 - wrapMode: Text.WordWrap - maximumLineCount: 4 - elide: Text.ElideRight - } - } - } - - RowLayout { - Layout.fillWidth: true - visible: card.modelData.actions.length > 0 - spacing: 8 - - Repeater { - model: card.modelData.actions - - delegate: Rectangle { - required property var modelData - - Layout.fillWidth: true - Layout.preferredHeight: 26 - radius: 6 - color: actionArea.containsMouse ? Config.colors.purple : Qt.rgba(1, 1, 1, 0.05) + LazyLoader { + active: true + component: Wallpaper {} + } - Text { - anchors.centerIn: parent - text: parent.modelData.text - color: Config.colors.fg - font.family: Config.bar.fontFamily - font.pixelSize: Config.bar.fontSize - 2 - } + LazyLoader { + id: barLoader + active: true + component: Bar {} + } - MouseArea { - id: actionArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: card.modelData.invokeAction(parent.modelData.identifier) - } - } - } - } - } + IpcHandler { + target: "shell" - MouseArea { - id: hoverArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: card.modelData.dismiss() - z: -1 - } - } - } + function restartBar(): void { + if (barLoader.item) + barLoader.item.restart() } } + + LazyLoader { + active: true + component: Notifications {} + } } |
