summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorcoast <coasteen@proton.me>2026-09-07 07:07:22 +0330
committercoast <coasteen@proton.me>2026-09-07 07:07:22 +0330
commit7922ded35ff292e485c3369dab9726b9142ce526 (patch)
treeea47cb7bb569b5fb8c44ede3357f1a8b4b41722c
parenta7cdf9580a901ed65f179417f6ed8a0848491b66 (diff)
downloadnix-7922ded35ff292e485c3369dab9726b9142ce526.tar.gz
nix-7922ded35ff292e485c3369dab9726b9142ce526.tar.bz2
nix-7922ded35ff292e485c3369dab9726b9142ce526.tar.xz
nix-7922ded35ff292e485c3369dab9726b9142ce526.tar.zst
a lot of quickshell
-rw-r--r--flake.lock117
-rw-r--r--flake.nix5
-rw-r--r--home-configuration/config/niri/config.kdl7
-rw-r--r--home-configuration/config/quickshell/config.js86
-rw-r--r--home-configuration/config/quickshell/modules/bar/Bar.qml154
-rw-r--r--home-configuration/config/quickshell/modules/bar/Music.qml219
-rw-r--r--home-configuration/config/quickshell/modules/bar/QuickControl.qml519
-rw-r--r--home-configuration/config/quickshell/modules/bar/Time.qml144
-rw-r--r--home-configuration/config/quickshell/modules/bar/Workspaces.qml169
-rw-r--r--home-configuration/config/quickshell/modules/desktop/Wallpaper.qml63
-rw-r--r--home-configuration/config/quickshell/modules/notifications/NotificationCard.qml257
-rw-r--r--home-configuration/config/quickshell/modules/notifications/NotificationFocus.qml123
-rw-r--r--home-configuration/config/quickshell/modules/notifications/Notifications.qml125
-rw-r--r--home-configuration/config/quickshell/shell.qml189
-rw-r--r--home-configuration/default.nix8
15 files changed, 2000 insertions, 185 deletions
diff --git a/flake.lock b/flake.lock
index 8b36cc5..f22f2b5 100644
--- a/flake.lock
+++ b/flake.lock
@@ -19,6 +19,24 @@
"url": "https://git.coasteen.xyz/coast/fetchit.git"
}
},
+ "flake-parts": {
+ "inputs": {
+ "nixpkgs-lib": "nixpkgs-lib"
+ },
+ "locked": {
+ "lastModified": 1762040540,
+ "narHash": "sha256-z5PlZ47j50VNF3R+IMS9LmzI5fYRGY/Z5O5tol1c9I4=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "0010412d62a25d959151790968765a70c436598b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "type": "github"
+ }
+ },
"flake-utils": {
"inputs": {
"systems": "systems"
@@ -74,6 +92,21 @@
"type": "github"
}
},
+ "nixpkgs-lib": {
+ "locked": {
+ "lastModified": 1761765539,
+ "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "type": "github"
+ }
+ },
"nixpkgs_2": {
"locked": {
"lastModified": 1788531059,
@@ -90,6 +123,22 @@
"type": "github"
}
},
+ "nixpkgs_3": {
+ "locked": {
+ "lastModified": 1761236834,
+ "narHash": "sha256-+pthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE+fV2Q=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "d5faa84122bc0a1fd5d378492efce4e289f8eac1",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
"old-nixpkgs": {
"locked": {
"lastModified": 1747953325,
@@ -106,12 +155,60 @@
"type": "github"
}
},
+ "qml-niri": {
+ "inputs": {
+ "flake-parts": "flake-parts",
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "quickshell": [
+ "quickshell"
+ ],
+ "treefmt-nix": "treefmt-nix"
+ },
+ "locked": {
+ "lastModified": 1786189893,
+ "narHash": "sha256-7iy5fo8tZ7BFw4Tgz6msS9B34xs1g7GVr9M6wU2XEJI=",
+ "owner": "imiric",
+ "repo": "qml-niri",
+ "rev": "93e603901bed2c4465d5675ae43fd52b7f7c4adf",
+ "type": "github"
+ },
+ "original": {
+ "owner": "imiric",
+ "ref": "main",
+ "repo": "qml-niri",
+ "type": "github"
+ }
+ },
+ "quickshell": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1787994703,
+ "narHash": "sha256-6SUZyyALo19heYPa/Xmu2H4zV+wvdfVX4fTcqbgHRnI=",
+ "ref": "refs/heads/master",
+ "rev": "2d3b3e9c70ef380dff751b61d334dc88df016c29",
+ "revCount": 856,
+ "type": "git",
+ "url": "https://git.outfoxxed.me/outfoxxed/quickshell"
+ },
+ "original": {
+ "type": "git",
+ "url": "https://git.outfoxxed.me/outfoxxed/quickshell"
+ }
+ },
"root": {
"inputs": {
"fetchit": "fetchit",
"home-manager": "home-manager",
"nixpkgs": "nixpkgs_2",
- "old-nixpkgs": "old-nixpkgs"
+ "old-nixpkgs": "old-nixpkgs",
+ "qml-niri": "qml-niri",
+ "quickshell": "quickshell"
}
},
"systems": {
@@ -128,6 +225,24 @@
"repo": "default",
"type": "github"
}
+ },
+ "treefmt-nix": {
+ "inputs": {
+ "nixpkgs": "nixpkgs_3"
+ },
+ "locked": {
+ "lastModified": 1762410071,
+ "narHash": "sha256-aF5fvoZeoXNPxT0bejFUBXeUjXfHLSL7g+mjR/p5TEg=",
+ "owner": "numtide",
+ "repo": "treefmt-nix",
+ "rev": "97a30861b13c3731a84e09405414398fbf3e109f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "treefmt-nix",
+ "type": "github"
+ }
}
},
"root": "root",
diff --git a/flake.nix b/flake.nix
index 6e17161..6d759c6 100644
--- a/flake.nix
+++ b/flake.nix
@@ -6,6 +6,11 @@
home-manager.url = "github:nix-community/home-manager/release-26.05";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
fetchit.url = "git+https://git.coasteen.xyz/coast/fetchit.git";
+ quickshell.url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
+ quickshell.inputs.nixpkgs.follows = "nixpkgs";
+ qml-niri.url = "github:imiric/qml-niri/main";
+ qml-niri.inputs.nixpkgs.follows = "nixpkgs";
+ qml-niri.inputs.quickshell.follows = "quickshell";
};
outputs = { self, old-nixpkgs, nixpkgs, home-manager, fetchit, ... }@inputs: {
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 {}
+ }
}
diff --git a/home-configuration/default.nix b/home-configuration/default.nix
index 9008258..caf2f76 100644
--- a/home-configuration/default.nix
+++ b/home-configuration/default.nix
@@ -1,4 +1,4 @@
-{ config, pkgs, ... }:
+{ inputs, config, pkgs, ... }:
let
configs = {
niri = "niri";
@@ -11,7 +11,7 @@ let
in
{
imports = [
- ./managed/waybar
+ #./managed/waybar
./managed/discord
./managed/git
./managed/prismlauncher
@@ -91,7 +91,6 @@ in
pkgs.libarchive
pkgs.fastfetch
pkgs.alacritty
- pkgs.awww
pkgs.xwayland-satellite
pkgs.telegram-desktop
pkgs.mpv
@@ -130,7 +129,6 @@ in
pkgs.gamescope
pkgs.xkill
pkgs.trayer
- pkgs.quickshell
pkgs.libnotify
pkgs.kdePackages.kdenlive
pkgs.obs-cmd
@@ -145,6 +143,8 @@ in
pkgs.unzip
pkgs.zig
pkgs.btop
+ pkgs.playerctl
+ inputs.qml-niri.packages.${pkgs.system}.quickshell
];
services.polkit-gnome.enable = true;