diff --git a/flake.lock b/flake.lock
index 5dc347d..d525c31 100644
--- a/flake.lock
+++ b/flake.lock
@@ -487,6 +487,26 @@
"type": "github"
}
},
+ "noctalia": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1769983098,
+ "narHash": "sha256-PKej3N1BxAoKzusrdWuS9gT8bXW0U/Zk8RkedsP3qYc=",
+ "owner": "noctalia-dev",
+ "repo": "noctalia-shell",
+ "rev": "2a98d04b2f5e251935ba296c0d7dc374bdc5e32d",
+ "type": "github"
+ },
+ "original": {
+ "owner": "noctalia-dev",
+ "repo": "noctalia-shell",
+ "type": "github"
+ }
+ },
"nur": {
"inputs": {
"flake-parts": [
@@ -569,6 +589,7 @@
"home-manager": "home-manager",
"nix-flatpak": "nix-flatpak",
"nixpkgs": "nixpkgs",
+ "noctalia": "noctalia",
"prismlauncher-cracked": "prismlauncher-cracked",
"stylix": "stylix",
"wrappers": "wrappers"
diff --git a/flake.nix b/flake.nix
index 76e4923..2b4dc38 100644
--- a/flake.nix
+++ b/flake.nix
@@ -59,6 +59,11 @@
url = "github:lassulus/wrappers";
inputs.nixpkgs.follows = "nixpkgs";
};
+
+ noctalia = {
+ url = "github:noctalia-dev/noctalia-shell";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
};
outputs = {
diff --git a/modules/core/default.nix b/modules/core/default.nix
index 4995b8d..535164d 100644
--- a/modules/core/default.nix
+++ b/modules/core/default.nix
@@ -12,6 +12,7 @@
./nfs.nix
./nh.nix
./printing.nix
+ ./quickshell.nix
./greetd.nix
./security.nix
./services.nix
diff --git a/modules/core/packages/essentials.nix b/modules/core/packages/essentials.nix
index 308fcc3..986cdcf 100644
--- a/modules/core/packages/essentials.nix
+++ b/modules/core/packages/essentials.nix
@@ -1,4 +1,9 @@
-{pkgs, ...}: {
+{
+ pkgs,
+ inputs,
+ system,
+ ...
+}: {
environment.systemPackages = with pkgs; [
mpv
pavucontrol
@@ -53,5 +58,10 @@
hunspell
hunspellDicts.pl_PL
hunspellDicts.en_US
+ # Noctalia Shell Dependencies
+ matugen
+ app2unit
+ gpu-screen-recorder
+ power-profiles-daemon
];
}
diff --git a/modules/core/quickshell.nix b/modules/core/quickshell.nix
new file mode 100644
index 0000000..b7d0651
--- /dev/null
+++ b/modules/core/quickshell.nix
@@ -0,0 +1,30 @@
+{pkgs, ...}: {
+ environment = {
+ systemPackages = with pkgs; [
+ quickshell
+
+ # Qt6 related kits๏ผfor slove Qt5Compat problem๏ผ
+ qt6.qt5compat
+ qt6.qtbase
+ qt6.qtquick3d
+ qt6.qtwayland
+ qt6.qtdeclarative
+ qt6.qtsvg
+
+ # alternate options
+ # libsForQt5.qt5compat
+ kdePackages.qt5compat
+ libsForQt5.qt5.qtgraphicaleffects
+ ];
+ # necessary environment variables
+ variables = {
+ QML_IMPORT_PATH = "${pkgs.qt6.qt5compat}/lib/qt-6/qml:${pkgs.qt6.qtbase}/lib/qt-6/qml";
+ QML2_IMPORT_PATH = "${pkgs.qt6.qt5compat}/lib/qt-6/qml:${pkgs.qt6.qtbase}/lib/qt-6/qml";
+ };
+ # make sure the Qt application is working properly
+ sessionVariables = {
+ QT_QPA_PLATFORM = "wayland;xcb";
+ QT_WAYLAND_DISABLE_WINDOWDECORATION = "1";
+ };
+ };
+}
diff --git a/modules/core/services.nix b/modules/core/services.nix
index 2418dae..4a0d157 100644
--- a/modules/core/services.nix
+++ b/modules/core/services.nix
@@ -53,5 +53,6 @@
];
};
};
+ upower.enable = true; # noctalia shell battery
};
}
diff --git a/modules/home/default.nix b/modules/home/default.nix
index 85ce98e..a14f5ea 100644
--- a/modules/home/default.nix
+++ b/modules/home/default.nix
@@ -29,8 +29,10 @@ in {
./kdeConnect.nix
./lutris.nix
./nextcloud.nix
+ ./noctalia.nix
./obs-studio.nix
./onlyoffice.nix
+ ./overview.nix
./qt.nix
./ssh.nix
./starship.nix
diff --git a/modules/home/hyprland/binds.nix b/modules/home/hyprland/binds.nix
index 9804396..af74894 100644
--- a/modules/home/hyprland/binds.nix
+++ b/modules/home/hyprland/binds.nix
@@ -39,9 +39,9 @@ in {
"SUPER SHIFT, M, exec, dex ${desktopEntriesPath}/messenger.desktop"
"SUPER SHIFT, N, exec, nextcloud"
"SUPER SHIFT, O, exec, onlyoffice-desktopeditors"
- "SUPER SHIFT, Return, exec, rofi-launcher"
+ # "SUPER SHIFT, Return, exec, rofi-launcher"
"SUPER SHIFT, T, exec, tutanota-desktop"
- "SUPER SHIFT, W, exec, web-search"
+ # "SUPER SHIFT, W, exec, web-search"
# =============================================================================
# APLIKACJE - Z ALT
@@ -149,6 +149,20 @@ in {
",XF86MonBrightnessDown, exec, brightnessctl set 5%-"
",XF86MonBrightnessUp, exec, brightnessctl set +5%"
+ # =============================================================================
+ # NOCTALIA SHELL
+ # =============================================================================
+ "SUPER SHIFT, Return, exec, noctalia-shell ipc call launcher toggle"
+ # "SUPER, M, Noctalia Notifications, exec, noctalia-shell ipc call notifications toggleHistory"
+ "SUPER SHIFT, V, exec, noctalia-shell ipc call launcher clipboard"
+ "SUPER SHIFT, comma, exec, noctalia-shell ipc call settings toggle"
+ "SUPER ALT, L, exec, noctalia-shell ipc call sessionMenu lockAndSuspend"
+ "SUPER SHIFT, W, exec, noctalia-shell ipc call wallpaper toggle"
+ "SUPER, X, exec, noctalia-shell ipc call sessionMenu toggle"
+ "SUPER ALT, C, exec, noctalia-shell ipc call controlCenter toggle"
+ "SUPER CTRL, R, exec, noctalia-shell ipc call screenRecorder toggle"
+ "SUPER SHIFT, R, exec, restart.noctalia"
+
# =============================================================================
# NIEUลปYWANE KEYBINDY
# =============================================================================
diff --git a/modules/home/hyprland/exec-once.nix b/modules/home/hyprland/exec-once.nix
index b6e12f7..49c856f 100644
--- a/modules/home/hyprland/exec-once.nix
+++ b/modules/home/hyprland/exec-once.nix
@@ -10,13 +10,19 @@ in {
"dbus-update-activation-environment --all --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP"
"systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP"
"systemctl --user start hyprpolkitagent"
+ "qs -c overview" # Start quickshell-overview daemon
- "killall -q swww;sleep .5 && swww-daemon"
- "killall -q waybar;sleep .5 && waybar"
- "killall -q swaync;sleep .5 && swaync"
- "#wallsetter &"
- "pypr &"
- "nm-applet --indicator"
- "sleep 1.0 && swww img ${stylixImage}"
+ # "killall -q swww;sleep .5 && swww-daemon"
+ # "killall -q waybar;sleep .5 && waybar"
+ # "killall -q swaync;sleep .5 && swaync"
+ # "#wallsetter &"
+ # "pypr &"
+ # "nm-applet --indicator"
+ # "sleep 1.0 && swww img ${stylixImage}"
+ "killall -q waybar"
+ "pkill waybar"
+ "killall -q swaync"
+ "pkill swaync"
+ "noctalia-shell &"
];
}
diff --git a/modules/home/noctalia.nix b/modules/home/noctalia.nix
new file mode 100644
index 0000000..8145f7a
--- /dev/null
+++ b/modules/home/noctalia.nix
@@ -0,0 +1,28 @@
+{
+ pkgs,
+ inputs,
+ system,
+ lib,
+ ...
+}: let
+ noctalia = inputs.noctalia.packages.${system}.default;
+ configDir = "${noctalia}/share/noctalia-shell";
+in {
+ home = {
+ packages = with pkgs; [
+ noctalia
+ quickshell # Ensure quickshell is available for the service
+ ];
+ activation.seedNoctaliaShellCode = lib.hm.dag.entryAfter ["writeBoundary"] ''
+ set -eu
+ DEST="$HOME/.config/quickshell/noctalia-shell"
+ SRC="${configDir}"
+
+ if [ ! -d "$DEST" ]; then
+ $DRY_RUN_CMD mkdir -p "$HOME/.config/quickshell"
+ $DRY_RUN_CMD cp -R "$SRC" "$DEST"
+ $DRY_RUN_CMD chmod -R u+rwX "$DEST"
+ fi
+ '';
+ };
+}
diff --git a/modules/home/overview.nix b/modules/home/overview.nix
new file mode 100644
index 0000000..67c9a83
--- /dev/null
+++ b/modules/home/overview.nix
@@ -0,0 +1,22 @@
+{lib, ...}: let
+ overviewSource = ./overview;
+in {
+ # Quickshell-overview is a Qt6 QML app for Hyprland workspace overview
+ # It shows all workspaces with live window previews, drag-and-drop support
+ # Toggled via: SUPER + TAB (bound in hyprland/binds.nix)
+ # Started via exec-once in hyprland/exec-once.nix
+
+ # Seed the Quickshell overview code into ~/.config/quickshell/overview
+ # Copy (not symlink) so QML module resolution works and users can edit files
+ home.activation.seedOverviewCode = lib.hm.dag.entryAfter ["writeBoundary"] ''
+ set -eu
+ DEST="$HOME/.config/quickshell/overview"
+ SRC="${overviewSource}"
+
+ if [ ! -d "$DEST" ]; then
+ mkdir -p "$HOME/.config/quickshell"
+ cp -R "$SRC" "$DEST"
+ chmod -R u+rwX "$DEST"
+ fi
+ '';
+}
diff --git a/modules/home/overview/README.md b/modules/home/overview/README.md
new file mode 100644
index 0000000..c32e7ac
--- /dev/null
+++ b/modules/home/overview/README.md
@@ -0,0 +1,214 @@
+# Quickshell Overview for Hyprland
+
+
+
+A standalone workspace overview module for Hyprland using Quickshell - shows all workspaces with live window previews, drag-and-drop support, and Super+Tab keybind.
+
+
+
+
+
+
+
+
+---
+
+## ๐ธ Preview
+
+
+
+https://github.com/user-attachments/assets/79ceb141-6b9e-4956-8e09-aaf72b66550c
+
+> *Workspace overview showing live window previews with drag-and-drop support*
+
+---
+
+## โจ Features
+
+- ๐ผ๏ธ Visual workspace overview showing all workspaces and windows
+- ๐ฏ Click windows to focus them
+- ๐ฑ๏ธ Middle-click windows to close them
+- ๐ Drag and drop windows between workspaces
+- โจ๏ธ Keyboard navigation (Arrow keys to switch workspaces, Escape/Enter to close)
+- ๐ก Hover tooltips showing window information
+- ๐จ Material Design 3 theming
+- โก Smooth animations and transitions
+
+## ๐ฆ Installation
+
+### Prerequisites
+
+- **Hyprland** compositor
+- **Quickshell** ([installation guide](https://quickshell.org/docs/v0.1.0/guide/install-setup/))
+- **Qt 6** with modules: QtQuick, QtQuick.Controls, Qt5Compat.GraphicalEffects
+
+### Setup
+
+1. **Clone this repository** to your Quickshell config directory:
+ ```bash
+ git clone https://github.com/Shanu-Kumawat/quickshell-overview ~/.config/quickshell/overview
+ ```
+
+2. **Add keybind** to your Hyprland config (`~/.config/hypr/hyprland.conf`):
+ ```conf
+ bind = Super, TAB, exec, qs ipc -c overview call overview toggle
+ ```
+
+3. **Auto-start** the overview (add to Hyprland config):
+ ```conf
+ exec-once = qs -c overview
+ ```
+
+4. **Reload Hyprland**:
+ ```bash
+ hyprctl reload
+ ```
+
+### Manual Start (if needed)
+
+```bash
+qs -c overview &
+```
+
+## ๐ฎ Usage
+
+| Action | Description |
+|--------|-------------|
+| **Super + Tab** | Toggle the overview |
+| **Left/Right Arrow Keys** | Navigate between workspaces horizontally |
+| **Up/Down Arrow Keys** | Navigate between workspace rows |
+| **Escape / Enter** | Close the overview |
+| **Click workspace** | Switch to that workspace |
+| **Click window** | Focus that window |
+| **Middle-click window** | Close that window |
+| **Drag window** | Move window to different workspace |
+
+---
+
+## โ๏ธ Configuration
+
+> **โ ๏ธ Want to change the size, position, or number of workspaces?**
+> Edit `~/.config/quickshell/overview/common/Config.qml` - it's all there!
+
+### Workspace Grid
+
+Edit `~/.config/quickshell/overview/common/Config.qml`:
+
+```qml
+property QtObject overview: QtObject {
+ property int rows: 2 // Number of workspace rows
+ property int columns: 5 // Number of workspace columns (10 total workspaces)
+ property real scale: 0.16 // Overview scale factor (0.1-0.3, smaller = more compact)
+ property bool enable: true
+}
+```
+
+**Common adjustments:**
+- **Too small?** Increase `scale` (try 0.20 or 0.25)
+- **Too big?** Decrease `scale` (try 0.12 or 0.14)
+- **More workspaces?** Change `rows` and `columns` (e.g., 3 rows ร 4 columns = 12 workspaces)
+
+### Position
+
+Edit `~/.config/quickshell/overview/modules/overview/Overview.qml` (line ~111):
+
+```qml
+anchors {
+ horizontalCenter: parent.horizontalCenter
+ top: parent.top
+ topMargin: 100 // Change this value to move up/down
+}
+```
+
+### Theme & Colors
+
+Edit `~/.config/quickshell/overview/common/Appearance.qml` to customize:
+- Colors (m3colors and colors objects)
+- Font families and sizes
+- Animation curves and durations
+- Border radius values
+
+---
+
+## ๐ Requirements
+
+- **Hyprland** compositor (tested on latest versions)
+- **Quickshell** (Qt6-based shell framework)
+- **Qt 6** with the following modules:
+ - QtQuick
+ - QtQuick.Controls
+ - QtQuick.Layouts
+ - Qt5Compat.GraphicalEffects
+ - Quickshell.Wayland
+ - Quickshell.Hyprland
+
+## ๐ซ Removed Features (from original illogical-impulse)
+
+The following features were removed to make it standalone:
+
+- App search functionality
+- Emoji picker
+- Clipboard history integration
+- Search widget
+- Integration with the full illogical-impulse shell ecosystem
+
+## ๐ File Structure
+
+```
+~/.config/quickshell/overview/
+โโโ shell.qml # Main entry point
+โโโ README.md # This file
+โโโ hyprland-config.conf # Configuration reference
+โโโ common/
+โ โโโ Appearance.qml # Theme and styling
+โ โโโ Config.qml # Configuration options
+โ โโโ functions/
+โ โ โโโ ColorUtils.qml # Color manipulation utilities
+โ โโโ widgets/
+โ โโโ StyledText.qml # Styled text component
+โ โโโ StyledRectangularShadow.qml
+โ โโโ StyledToolTip.qml
+โ โโโ StyledToolTipContent.qml
+โโโ services/
+โ โโโ GlobalStates.qml # Global state management
+โ โโโ HyprlandData.qml # Hyprland data provider
+โโโ modules/
+ โโโ overview/
+ โโโ Overview.qml # Main overview component
+ โโโ OverviewWidget.qml # Workspace grid widget
+ โโโ OverviewWindow.qml # Individual window preview
+```
+
+## ๐ฏ IPC Commands
+
+```bash
+# Toggle overview
+qs ipc -c overview call overview toggle
+
+# Open overview
+qs ipc -c overview call overview open
+
+# Close overview
+qs ipc -c overview call overview close
+```
+
+## ๐ Known Issues
+
+- Window icons may fallback to generic icon if app class name doesn't match icon theme
+- Potential crashes during rapid window state changes due to Wayland screencopy buffer management
+
+## Credits
+
+Extracted from the overview feature in [illogical-impulse](https://github.com/end-4/dots-hyprland) by [end-4](https://github.com/end-4).
+
+Adapted as a standalone component for Hyprland + Quickshell users who want just the overview functionality.
+
+---
+
+
+
+**Note:** Maintenance will be limited due to time constraints, but **PRs and code improvements are welcome!** Feel free to contribute or fork for your own needs.
+
+Made with โค๏ธ for the Hyprland community
+
+
diff --git a/modules/home/overview/assets/image.png b/modules/home/overview/assets/image.png
new file mode 100644
index 0000000..91db17c
Binary files /dev/null and b/modules/home/overview/assets/image.png differ
diff --git a/modules/home/overview/common/Appearance.qml b/modules/home/overview/common/Appearance.qml
new file mode 100644
index 0000000..79a30b4
--- /dev/null
+++ b/modules/home/overview/common/Appearance.qml
@@ -0,0 +1,148 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import "functions"
+
+Singleton {
+ id: root
+ property QtObject m3colors
+ property QtObject animation
+ property QtObject animationCurves
+ property QtObject colors
+ property QtObject rounding
+ property QtObject font
+ property QtObject sizes
+
+ m3colors: QtObject {
+ property bool darkmode: true
+ property color m3primary: "#E5B6F2"
+ property color m3onPrimary: "#452152"
+ property color m3primaryContainer: "#5D386A"
+ property color m3onPrimaryContainer: "#F9D8FF"
+ property color m3secondary: "#D5C0D7"
+ property color m3onSecondary: "#392C3D"
+ property color m3secondaryContainer: "#534457"
+ property color m3onSecondaryContainer: "#F2DCF3"
+ property color m3background: "#161217"
+ property color m3onBackground: "#EAE0E7"
+ property color m3surface: "#161217"
+ property color m3surfaceContainerLow: "#1F1A1F"
+ property color m3surfaceContainer: "#231E23"
+ property color m3surfaceContainerHigh: "#2D282E"
+ property color m3surfaceContainerHighest: "#383339"
+ property color m3onSurface: "#EAE0E7"
+ property color m3surfaceVariant: "#4C444D"
+ property color m3onSurfaceVariant: "#CFC3CD"
+ property color m3inverseSurface: "#EAE0E7"
+ property color m3inverseOnSurface: "#342F34"
+ property color m3outline: "#988E97"
+ property color m3outlineVariant: "#4C444D"
+ property color m3shadow: "#000000"
+ }
+
+ colors: QtObject {
+ property color colSubtext: m3colors.m3outline
+ property color colLayer0: m3colors.m3background
+ property color colOnLayer0: m3colors.m3onBackground
+ property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4)
+ property color colLayer1: m3colors.m3surfaceContainerLow
+ property color colOnLayer1: m3colors.m3onSurfaceVariant
+ property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45)
+ property color colLayer1Hover: ColorUtils.mix(colLayer1, colOnLayer1, 0.92)
+ property color colLayer1Active: ColorUtils.mix(colLayer1, colOnLayer1, 0.85)
+ property color colLayer2: m3colors.m3surfaceContainer
+ property color colOnLayer2: m3colors.m3onSurface
+ property color colLayer2Hover: ColorUtils.mix(colLayer2, colOnLayer2, 0.90)
+ property color colLayer2Active: ColorUtils.mix(colLayer2, colOnLayer2, 0.80)
+ property color colPrimary: m3colors.m3primary
+ property color colOnPrimary: m3colors.m3onPrimary
+ property color colSecondary: m3colors.m3secondary
+ property color colSecondaryContainer: m3colors.m3secondaryContainer
+ property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer
+ property color colTooltip: m3colors.m3inverseSurface
+ property color colOnTooltip: m3colors.m3inverseOnSurface
+ property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
+ property color colOutline: m3colors.m3outline
+ }
+
+ rounding: QtObject {
+ property int unsharpen: 2
+ property int verysmall: 8
+ property int small: 12
+ property int normal: 17
+ property int large: 23
+ property int full: 9999
+ property int screenRounding: large
+ property int windowRounding: 18
+ }
+
+ font: QtObject {
+ property QtObject family: QtObject {
+ property string main: "sans-serif"
+ property string title: "sans-serif"
+ property string expressive: "sans-serif"
+ }
+ property QtObject pixelSize: QtObject {
+ property int smaller: 12
+ property int small: 15
+ property int normal: 16
+ property int larger: 19
+ property int huge: 22
+ }
+ }
+
+ animationCurves: QtObject {
+ readonly property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1]
+ readonly property list expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1]
+ readonly property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
+ readonly property real expressiveDefaultSpatialDuration: 500
+ readonly property real expressiveEffectsDuration: 200
+ }
+
+ animation: QtObject {
+ property QtObject elementMove: QtObject {
+ property int duration: animationCurves.expressiveDefaultSpatialDuration
+ property int type: Easing.BezierSpline
+ property list bezierCurve: animationCurves.expressiveDefaultSpatial
+ property Component numberAnimation: Component {
+ NumberAnimation {
+ duration: root.animation.elementMove.duration
+ easing.type: root.animation.elementMove.type
+ easing.bezierCurve: root.animation.elementMove.bezierCurve
+ }
+ }
+ }
+
+ property QtObject elementMoveEnter: QtObject {
+ property int duration: 400
+ property int type: Easing.BezierSpline
+ property list bezierCurve: animationCurves.emphasizedDecel
+ property Component numberAnimation: Component {
+ NumberAnimation {
+ duration: root.animation.elementMoveEnter.duration
+ easing.type: root.animation.elementMoveEnter.type
+ easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve
+ }
+ }
+ }
+
+ property QtObject elementMoveFast: QtObject {
+ property int duration: animationCurves.expressiveEffectsDuration
+ property int type: Easing.BezierSpline
+ property list bezierCurve: animationCurves.expressiveEffects
+ property Component numberAnimation: Component {
+ NumberAnimation {
+ duration: root.animation.elementMoveFast.duration
+ easing.type: root.animation.elementMoveFast.type
+ easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
+ }
+ }
+ }
+ }
+
+ sizes: QtObject {
+ property real elevationMargin: 10
+ }
+}
diff --git a/modules/home/overview/common/Config.qml b/modules/home/overview/common/Config.qml
new file mode 100644
index 0000000..48601dd
--- /dev/null
+++ b/modules/home/overview/common/Config.qml
@@ -0,0 +1,22 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+
+Singleton {
+ id: root
+
+ property QtObject options: QtObject {
+ property QtObject overview: QtObject {
+ property int rows: 2
+ property int columns: 5
+ property real scale: 0.16
+ property bool enable: true
+ }
+
+ property QtObject hacks: QtObject {
+ property int arbitraryRaceConditionDelay: 150
+ }
+ }
+}
diff --git a/modules/home/overview/common/functions/ColorUtils.qml b/modules/home/overview/common/functions/ColorUtils.qml
new file mode 100644
index 0000000..6162df1
--- /dev/null
+++ b/modules/home/overview/common/functions/ColorUtils.qml
@@ -0,0 +1,68 @@
+pragma Singleton
+import Quickshell
+
+Singleton {
+ id: root
+
+ function colorWithHueOf(color1, color2) {
+ var c1 = Qt.color(color1);
+ var c2 = Qt.color(color2);
+ var hue = c2.hsvHue;
+ var sat = c1.hsvSaturation;
+ var val = c1.hsvValue;
+ var alpha = c1.a;
+ return Qt.hsva(hue, sat, val, alpha);
+ }
+
+ function colorWithSaturationOf(color1, color2) {
+ var c1 = Qt.color(color1);
+ var c2 = Qt.color(color2);
+ var hue = c1.hsvHue;
+ var sat = c2.hsvSaturation;
+ var val = c1.hsvValue;
+ var alpha = c1.a;
+ return Qt.hsva(hue, sat, val, alpha);
+ }
+
+ function colorWithLightness(color, lightness) {
+ var c = Qt.color(color);
+ return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a);
+ }
+
+ function colorWithLightnessOf(color1, color2) {
+ var c2 = Qt.color(color2);
+ return colorWithLightness(color1, c2.hslLightness);
+ }
+
+ function adaptToAccent(color1, color2) {
+ var c1 = Qt.color(color1);
+ var c2 = Qt.color(color2);
+ var hue = c2.hslHue;
+ var sat = c2.hslSaturation;
+ var light = c1.hslLightness;
+ var alpha = c1.a;
+ return Qt.hsla(hue, sat, light, alpha);
+ }
+
+ function mix(color1, color2, percentage = 0.5) {
+ var c1 = Qt.color(color1);
+ var c2 = Qt.color(color2);
+ return Qt.rgba(
+ percentage * c1.r + (1 - percentage) * c2.r,
+ percentage * c1.g + (1 - percentage) * c2.g,
+ percentage * c1.b + (1 - percentage) * c2.b,
+ percentage * c1.a + (1 - percentage) * c2.a
+ );
+ }
+
+ function transparentize(color, percentage = 1) {
+ var c = Qt.color(color);
+ return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
+ }
+
+ function applyAlpha(color, alpha) {
+ var c = Qt.color(color);
+ var a = Math.max(0, Math.min(1, alpha));
+ return Qt.rgba(c.r, c.g, c.b, a);
+ }
+}
diff --git a/modules/home/overview/common/functions/qmldir b/modules/home/overview/common/functions/qmldir
new file mode 100644
index 0000000..4c648e7
--- /dev/null
+++ b/modules/home/overview/common/functions/qmldir
@@ -0,0 +1 @@
+singleton ColorUtils 1.0 ColorUtils.qml
diff --git a/modules/home/overview/common/qmldir b/modules/home/overview/common/qmldir
new file mode 100644
index 0000000..a848518
--- /dev/null
+++ b/modules/home/overview/common/qmldir
@@ -0,0 +1,7 @@
+singleton Appearance 1.0 Appearance.qml
+singleton Config 1.0 Config.qml
+singleton ColorUtils 1.0 functions/ColorUtils.qml
+StyledText 1.0 widgets/StyledText.qml
+StyledRectangularShadow 1.0 widgets/StyledRectangularShadow.qml
+StyledToolTip 1.0 widgets/StyledToolTip.qml
+StyledToolTipContent 1.0 widgets/StyledToolTipContent.qml
diff --git a/modules/home/overview/common/widgets/StyledRectangularShadow.qml b/modules/home/overview/common/widgets/StyledRectangularShadow.qml
new file mode 100644
index 0000000..ccdff1a
--- /dev/null
+++ b/modules/home/overview/common/widgets/StyledRectangularShadow.qml
@@ -0,0 +1,14 @@
+import QtQuick
+import QtQuick.Effects
+import ".."
+
+RectangularShadow {
+ required property var target
+ anchors.fill: target
+ radius: 20
+ blur: 0.9 * Appearance.sizes.elevationMargin
+ offset: Qt.vector2d(0.0, 1.0)
+ spread: 1
+ color: Appearance.colors.colShadow
+ cached: true
+}
diff --git a/modules/home/overview/common/widgets/StyledText.qml b/modules/home/overview/common/widgets/StyledText.qml
new file mode 100644
index 0000000..abfcefa
--- /dev/null
+++ b/modules/home/overview/common/widgets/StyledText.qml
@@ -0,0 +1,16 @@
+import QtQuick
+import ".."
+
+Text {
+ id: root
+ property bool animateChange: false
+
+ renderType: Text.NativeRendering
+ verticalAlignment: Text.AlignVCenter
+ font {
+ hintingPreference: Font.PreferFullHinting
+ family: Appearance?.font.family.main ?? "sans-serif"
+ pixelSize: Appearance?.font.pixelSize.small ?? 15
+ }
+ color: Appearance?.m3colors.m3onBackground ?? "white"
+}
diff --git a/modules/home/overview/common/widgets/StyledToolTip.qml b/modules/home/overview/common/widgets/StyledToolTip.qml
new file mode 100644
index 0000000..4d4678c
--- /dev/null
+++ b/modules/home/overview/common/widgets/StyledToolTip.qml
@@ -0,0 +1,23 @@
+import QtQuick
+import QtQuick.Controls
+import "."
+
+ToolTip {
+ id: root
+ property bool extraVisibleCondition: true
+ property bool alternativeVisibleCondition: false
+ readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
+ verticalPadding: 5
+ horizontalPadding: 10
+ background: null
+
+ visible: internalVisibleCondition
+
+ contentItem: StyledToolTipContent {
+ id: contentItem
+ text: root.text
+ shown: root.internalVisibleCondition
+ horizontalPadding: root.horizontalPadding
+ verticalPadding: root.verticalPadding
+ }
+}
diff --git a/modules/home/overview/common/widgets/StyledToolTipContent.qml b/modules/home/overview/common/widgets/StyledToolTipContent.qml
new file mode 100644
index 0000000..b8c29c1
--- /dev/null
+++ b/modules/home/overview/common/widgets/StyledToolTipContent.qml
@@ -0,0 +1,49 @@
+import QtQuick
+import "."
+import "../"
+
+Item {
+ id: root
+ required property string text
+ property bool shown: false
+ property real horizontalPadding: 10
+ property real verticalPadding: 5
+ implicitWidth: tooltipTextObject.implicitWidth + 2 * root.horizontalPadding
+ implicitHeight: tooltipTextObject.implicitHeight + 2 * root.verticalPadding
+
+ property bool isVisible: backgroundRectangle.implicitHeight > 0
+
+ Rectangle {
+ id: backgroundRectangle
+ anchors {
+ bottom: root.bottom
+ horizontalCenter: root.horizontalCenter
+ }
+ color: Appearance?.colors.colTooltip ?? "#3C4043"
+ radius: Appearance?.rounding.verysmall ?? 7
+ opacity: shown ? 1 : 0
+ implicitWidth: shown ? (tooltipTextObject.implicitWidth + 2 * root.horizontalPadding) : 0
+ implicitHeight: shown ? (tooltipTextObject.implicitHeight + 2 * root.verticalPadding) : 0
+ clip: true
+
+ Behavior on implicitWidth {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on implicitHeight {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on opacity {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ StyledText {
+ id: tooltipTextObject
+ anchors.centerIn: parent
+ text: root.text
+ font.pixelSize: Appearance?.font.pixelSize.smaller ?? 14
+ font.hintingPreference: Font.PreferNoHinting
+ color: Appearance?.colors.colOnTooltip ?? "#FFFFFF"
+ wrapMode: Text.Wrap
+ }
+ }
+}
diff --git a/modules/home/overview/common/widgets/qmldir b/modules/home/overview/common/widgets/qmldir
new file mode 100644
index 0000000..0efe136
--- /dev/null
+++ b/modules/home/overview/common/widgets/qmldir
@@ -0,0 +1,4 @@
+StyledText 1.0 StyledText.qml
+StyledRectangularShadow 1.0 StyledRectangularShadow.qml
+StyledToolTip 1.0 StyledToolTip.qml
+StyledToolTipContent 1.0 StyledToolTipContent.qml
diff --git a/modules/home/overview/modules/overview/Overview.qml b/modules/home/overview/modules/overview/Overview.qml
new file mode 100644
index 0000000..b3a299c
--- /dev/null
+++ b/modules/home/overview/modules/overview/Overview.qml
@@ -0,0 +1,147 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import "../../common"
+import "../../services"
+import "."
+
+Scope {
+ id: overviewScope
+ Variants {
+ id: overviewVariants
+ model: Quickshell.screens
+ PanelWindow {
+ id: root
+ required property var modelData
+ readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
+ property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
+ screen: modelData
+ visible: GlobalStates.overviewOpen
+
+ WlrLayershell.namespace: "quickshell:overview"
+ WlrLayershell.layer: WlrLayer.Overlay
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
+ color: "transparent"
+
+ mask: Region {
+ item: GlobalStates.overviewOpen ? keyHandler : null
+ }
+
+ anchors {
+ top: true
+ bottom: true
+ left: !(Config?.options.overview.enable ?? true)
+ right: !(Config?.options.overview.enable ?? true)
+ }
+
+ HyprlandFocusGrab {
+ id: grab
+ windows: [root]
+ property bool canBeActive: root.monitorIsFocused
+ active: false
+ onCleared: () => {
+ if (!active)
+ GlobalStates.overviewOpen = false;
+ }
+ }
+
+ Connections {
+ target: GlobalStates
+ function onOverviewOpenChanged() {
+ if (GlobalStates.overviewOpen) {
+ delayedGrabTimer.start();
+ }
+ }
+ }
+
+ Timer {
+ id: delayedGrabTimer
+ interval: Config.options.hacks.arbitraryRaceConditionDelay
+ repeat: false
+ onTriggered: {
+ if (!grab.canBeActive)
+ return;
+ grab.active = GlobalStates.overviewOpen;
+ }
+ }
+
+ implicitWidth: columnLayout.implicitWidth
+ implicitHeight: columnLayout.implicitHeight
+
+ Item {
+ id: keyHandler
+ anchors.fill: parent
+ visible: GlobalStates.overviewOpen
+ focus: GlobalStates.overviewOpen
+
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Escape || event.key === Qt.Key_Return) {
+ GlobalStates.overviewOpen = false;
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
+ const workspacesPerGroup = Config.options.overview.rows * Config.options.overview.columns;
+ const currentId = Hyprland.focusedMonitor?.activeWorkspace?.id ?? 1;
+ const currentGroup = Math.floor((currentId - 1) / workspacesPerGroup);
+ const minWorkspaceId = currentGroup * workspacesPerGroup + 1;
+ const maxWorkspaceId = minWorkspaceId + workspacesPerGroup - 1;
+
+ let targetId;
+ if (event.key === Qt.Key_Left) {
+ targetId = currentId - 1;
+ if (targetId < minWorkspaceId) targetId = maxWorkspaceId;
+ } else if (event.key === Qt.Key_Right) {
+ targetId = currentId + 1;
+ if (targetId > maxWorkspaceId) targetId = minWorkspaceId;
+ } else if (event.key === Qt.Key_Up) {
+ targetId = currentId - Config.options.overview.columns;
+ if (targetId < minWorkspaceId) targetId += workspacesPerGroup;
+ } else {
+ targetId = currentId + Config.options.overview.columns;
+ if (targetId > maxWorkspaceId) targetId -= workspacesPerGroup;
+ }
+
+ Hyprland.dispatch("workspace " + targetId);
+ event.accepted = true;
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: columnLayout
+ visible: GlobalStates.overviewOpen
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ top: parent.top
+ topMargin: 100
+ }
+
+ Loader {
+ id: overviewLoader
+ active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
+ sourceComponent: OverviewWidget {
+ panelWindow: root
+ visible: true
+ }
+ }
+ }
+ }
+ }
+
+ IpcHandler {
+ target: "overview"
+
+ function toggle() {
+ GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
+ }
+ function close() {
+ GlobalStates.overviewOpen = false;
+ }
+ function open() {
+ GlobalStates.overviewOpen = true;
+ }
+ }
+}
diff --git a/modules/home/overview/modules/overview/OverviewWidget.qml b/modules/home/overview/modules/overview/OverviewWidget.qml
new file mode 100644
index 0000000..7defa64
--- /dev/null
+++ b/modules/home/overview/modules/overview/OverviewWidget.qml
@@ -0,0 +1,303 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import "../../common"
+import "../../common/functions"
+import "../../common/widgets"
+import "../../services"
+import "."
+
+Item {
+ id: root
+ required property var panelWindow
+ readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
+ readonly property var toplevels: ToplevelManager.toplevels
+ readonly property int workspacesShown: Config.options.overview.rows * Config.options.overview.columns
+ readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown)
+ property bool monitorIsFocused: (Hyprland.focusedMonitor?.name == monitor.name)
+ property var windows: HyprlandData.windowList
+ property var windowByAddress: HyprlandData.windowByAddress
+ property var windowAddresses: HyprlandData.addresses
+ property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor?.id)
+ property real scale: Config.options.overview.scale
+ property color activeBorderColor: Appearance.colors.colSecondary
+
+ property real workspaceImplicitWidth: (monitorData?.transform % 2 === 1) ?
+ ((monitor.height / monitor.scale - (monitorData?.reserved?.[0] ?? 0) - (monitorData?.reserved?.[2] ?? 0)) * root.scale) :
+ ((monitor.width / monitor.scale - (monitorData?.reserved?.[0] ?? 0) - (monitorData?.reserved?.[2] ?? 0)) * root.scale)
+ property real workspaceImplicitHeight: (monitorData?.transform % 2 === 1) ?
+ ((monitor.width / monitor.scale - (monitorData?.reserved?.[1] ?? 0) - (monitorData?.reserved?.[3] ?? 0)) * root.scale) :
+ ((monitor.height / monitor.scale - (monitorData?.reserved?.[1] ?? 0) - (monitorData?.reserved?.[3] ?? 0)) * root.scale)
+
+ property real workspaceNumberMargin: 80
+ property real workspaceNumberSize: 250 * monitor.scale
+ property int workspaceZ: 0
+ property int windowZ: 1
+ property int windowDraggingZ: 99999
+ property real workspaceSpacing: 5
+
+ property int draggingFromWorkspace: -1
+ property int draggingTargetWorkspace: -1
+
+ implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
+ implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2
+
+ property Component windowComponent: OverviewWindow {}
+ property list windowWidgets: []
+
+ StyledRectangularShadow {
+ target: overviewBackground
+ }
+ Rectangle { // Background
+ id: overviewBackground
+ property real padding: 10
+ anchors.fill: parent
+ anchors.margins: Appearance.sizes.elevationMargin
+
+ implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2
+ implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2
+ radius: Appearance.rounding.screenRounding * root.scale + padding
+ color: Appearance.colors.colLayer0
+ border.width: 1
+ border.color: Appearance.colors.colLayer0Border
+
+ ColumnLayout { // Workspaces
+ id: workspaceColumnLayout
+
+ z: root.workspaceZ
+ anchors.centerIn: parent
+ spacing: workspaceSpacing
+ Repeater {
+ model: Config.options.overview.rows
+ delegate: RowLayout {
+ id: row
+ property int rowIndex: index
+ spacing: workspaceSpacing
+
+ Repeater { // Workspace repeater
+ model: Config.options.overview.columns
+ Rectangle { // Workspace
+ id: workspace
+ property int colIndex: index
+ property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * Config.options.overview.columns + colIndex + 1
+ property color defaultWorkspaceColor: Appearance.colors.colLayer1
+ property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1)
+ property color hoveredBorderColor: Appearance.colors.colLayer2Hover
+ property bool hoveredWhileDragging: false
+
+ implicitWidth: root.workspaceImplicitWidth
+ implicitHeight: root.workspaceImplicitHeight
+ color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
+ radius: Appearance.rounding.screenRounding * root.scale
+ border.width: 2
+ border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent"
+
+ StyledText {
+ anchors.centerIn: parent
+ text: workspaceValue
+ font {
+ pixelSize: root.workspaceNumberSize * root.scale
+ weight: Font.DemiBold
+ family: Appearance.font.family.expressive
+ }
+ color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, 0.8)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ MouseArea {
+ id: workspaceArea
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton
+ onClicked: {
+ if (root.draggingTargetWorkspace === -1) {
+ GlobalStates.overviewOpen = false
+ Hyprland.dispatch(`workspace ${workspaceValue}`)
+ }
+ }
+ }
+
+ DropArea {
+ anchors.fill: parent
+ onEntered: {
+ root.draggingTargetWorkspace = workspaceValue
+ if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return;
+ hoveredWhileDragging = true
+ }
+ onExited: {
+ hoveredWhileDragging = false
+ if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1
+ }
+ }
+
+ }
+ }
+ }
+ }
+ }
+
+ Item { // Windows & focused workspace indicator
+ id: windowSpace
+ anchors.centerIn: parent
+ implicitWidth: workspaceColumnLayout.implicitWidth
+ implicitHeight: workspaceColumnLayout.implicitHeight
+
+ Repeater { // Window repeater
+ model: ScriptModel {
+ values: {
+ return ToplevelManager.toplevels.values.filter((toplevel) => {
+ const address = `0x${toplevel.HyprlandToplevel.address}`
+ var win = windowByAddress[address]
+ const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown)
+ return inWorkspaceGroup;
+ }).sort((a, b) => {
+ // Proper stacking order based on Hyprland's window properties
+ const addrA = `0x${a.HyprlandToplevel.address}`
+ const addrB = `0x${b.HyprlandToplevel.address}`
+ const winA = windowByAddress[addrA]
+ const winB = windowByAddress[addrB]
+
+ // 1. Pinned windows are always on top
+ if (winA?.pinned !== winB?.pinned) {
+ return winA?.pinned ? 1 : -1
+ }
+
+ // 2. Floating windows above tiled windows
+ if (winA?.floating !== winB?.floating) {
+ return winA?.floating ? 1 : -1
+ }
+
+ // 3. Within same category, sort by focus history
+ // Lower focusHistoryID = more recently focused = higher in stack
+ return (winB?.focusHistoryID ?? 0) - (winA?.focusHistoryID ?? 0)
+ })
+ }
+ }
+ delegate: OverviewWindow {
+ id: window
+ required property var modelData
+ required property int index
+ property int monitorId: windowData?.monitor
+ property var monitor: HyprlandData.monitors.find(m => m.id === monitorId)
+ property var address: `0x${modelData.HyprlandToplevel.address}`
+ windowData: windowByAddress[address]
+ toplevel: modelData
+ monitorData: monitor
+
+ // Calculate scale relative to window's source monitor
+ property real sourceMonitorWidth: (monitor?.transform % 2 === 1) ?
+ (monitor?.height ?? 1920) / (monitor?.scale ?? 1) - (monitor?.reserved?.[0] ?? 0) - (monitor?.reserved?.[2] ?? 0) :
+ (monitor?.width ?? 1920) / (monitor?.scale ?? 1) - (monitor?.reserved?.[0] ?? 0) - (monitor?.reserved?.[2] ?? 0)
+ property real sourceMonitorHeight: (monitor?.transform % 2 === 1) ?
+ (monitor?.width ?? 1080) / (monitor?.scale ?? 1) - (monitor?.reserved?.[1] ?? 0) - (monitor?.reserved?.[3] ?? 0) :
+ (monitor?.height ?? 1080) / (monitor?.scale ?? 1) - (monitor?.reserved?.[1] ?? 0) - (monitor?.reserved?.[3] ?? 0)
+
+ // Scale windows to fit the workspace size, accounting for different monitor sizes
+ scale: Math.min(
+ root.workspaceImplicitWidth / sourceMonitorWidth,
+ root.workspaceImplicitHeight / sourceMonitorHeight
+ )
+
+ availableWorkspaceWidth: root.workspaceImplicitWidth
+ availableWorkspaceHeight: root.workspaceImplicitHeight
+ widgetMonitorId: root.monitor.id
+
+ property bool atInitPosition: (initX == x && initY == y)
+
+ property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.options.overview.columns
+ property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.options.overview.columns)
+ xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex
+ yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex
+
+ Timer {
+ id: updateWindowPosition
+ interval: Config.options.hacks.arbitraryRaceConditionDelay
+ repeat: false
+ running: false
+ onTriggered: {
+ window.x = Math.round(Math.max((windowData?.at[0] - (monitor?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * root.scale, 0) + xOffset)
+ window.y = Math.round(Math.max((windowData?.at[1] - (monitor?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * root.scale, 0) + yOffset)
+ }
+ }
+
+ z: atInitPosition ? (root.windowZ + index) : root.windowDraggingZ
+ Drag.hotSpot.x: targetWindowWidth / 2
+ Drag.hotSpot.y: targetWindowHeight / 2
+ MouseArea {
+ id: dragArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onEntered: hovered = true
+ onExited: hovered = false
+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton
+ drag.target: parent
+ onPressed: (mouse) => {
+ root.draggingFromWorkspace = windowData?.workspace.id
+ window.pressed = true
+ window.Drag.active = true
+ window.Drag.source = window
+ window.Drag.hotSpot.x = mouse.x
+ window.Drag.hotSpot.y = mouse.y
+ }
+ onReleased: {
+ const targetWorkspace = root.draggingTargetWorkspace
+ window.pressed = false
+ window.Drag.active = false
+ root.draggingFromWorkspace = -1
+ if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) {
+ Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`)
+ updateWindowPosition.restart()
+ }
+ else {
+ window.x = window.initX
+ window.y = window.initY
+ }
+ }
+ onClicked: (event) => {
+ if (!windowData) return;
+
+ if (event.button === Qt.LeftButton) {
+ GlobalStates.overviewOpen = false
+ Hyprland.dispatch(`focuswindow address:${windowData.address}`)
+ event.accepted = true
+ } else if (event.button === Qt.MiddleButton) {
+ Hyprland.dispatch(`closewindow address:${windowData.address}`)
+ event.accepted = true
+ }
+ }
+
+ StyledToolTip {
+ extraVisibleCondition: false
+ alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active
+ text: `${windowData?.title ?? "Unknown"}\n[${windowData?.class ?? "unknown"}] ${windowData?.xwayland ? "[XWayland] " : ""}`
+ }
+ }
+ }
+ }
+
+ Rectangle { // Focused workspace indicator
+ id: focusedWorkspaceIndicator
+ property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown)
+ property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.columns)
+ property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.columns
+ x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex
+ y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex
+ z: root.windowZ
+ width: root.workspaceImplicitWidth
+ height: root.workspaceImplicitHeight
+ color: "transparent"
+ radius: Appearance.rounding.screenRounding * root.scale
+ border.width: 2
+ border.color: root.activeBorderColor
+ Behavior on x {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on y {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ }
+ }
+ }
+}
diff --git a/modules/home/overview/modules/overview/OverviewWindow.qml b/modules/home/overview/modules/overview/OverviewWindow.qml
new file mode 100644
index 0000000..1325322
--- /dev/null
+++ b/modules/home/overview/modules/overview/OverviewWindow.qml
@@ -0,0 +1,109 @@
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Wayland
+import "../../common"
+import "../../common/functions"
+import "../../services"
+
+Item { // Window
+ id: root
+ property var toplevel
+ property var windowData
+ property var monitorData
+ property var scale
+ property var availableWorkspaceWidth
+ property var availableWorkspaceHeight
+ property bool restrictToWorkspace: true
+ property real initX: Math.max(((windowData?.at[0] ?? 0) - (monitorData?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * root.scale, 0) + xOffset
+ property real initY: Math.max(((windowData?.at[1] ?? 0) - (monitorData?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * root.scale, 0) + yOffset
+ property real xOffset: 0
+ property real yOffset: 0
+ property int widgetMonitorId: 0
+
+ property var targetWindowWidth: (windowData?.size[0] ?? 100) * scale
+ property var targetWindowHeight: (windowData?.size[1] ?? 100) * scale
+ property bool hovered: false
+ property bool pressed: false
+
+ property var iconToWindowRatio: 0.25
+ property var xwaylandIndicatorToIconRatio: 0.35
+ property var iconToWindowRatioCompact: 0.45
+ property var entry: DesktopEntries.heuristicLookup(windowData?.class)
+ property var iconPath: Quickshell.iconPath(entry?.icon ?? windowData?.class ?? "application-x-executable", "image-missing")
+ property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth
+
+ property bool indicateXWayland: windowData?.xwayland ?? false
+
+ x: initX
+ y: initY
+ width: Math.min((windowData?.size[0] ?? 100) * root.scale, availableWorkspaceWidth)
+ height: Math.min((windowData?.size[1] ?? 100) * root.scale, availableWorkspaceHeight)
+ opacity: (windowData?.monitor ?? -1) == widgetMonitorId ? 1 : 0.4
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: root.width
+ height: root.height
+ radius: Appearance.rounding.windowRounding * root.scale
+ }
+ }
+
+ Behavior on x {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on y {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on width {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+
+ ScreencopyView {
+ id: windowPreview
+ anchors.fill: parent
+ captureSource: GlobalStates.overviewOpen ? root.toplevel : null
+ live: true
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Appearance.rounding.windowRounding * root.scale
+ color: pressed ? ColorUtils.transparentize(Appearance.colors.colLayer2Active, 0.5) :
+ hovered ? ColorUtils.transparentize(Appearance.colors.colLayer2Hover, 0.7) :
+ ColorUtils.transparentize(Appearance.colors.colLayer2)
+ border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.7)
+ border.width : 1
+ }
+
+ ColumnLayout {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: Appearance.font.pixelSize.smaller * 0.5
+
+ Image {
+ id: windowIcon
+ property var iconSize: {
+ return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
+ }
+ Layout.alignment: Qt.AlignHCenter
+ source: root.iconPath
+ width: iconSize
+ height: iconSize
+ sourceSize: Qt.size(iconSize, iconSize)
+
+ Behavior on width {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ }
+ }
+ }
+}
diff --git a/modules/home/overview/modules/overview/qmldir b/modules/home/overview/modules/overview/qmldir
new file mode 100644
index 0000000..9b15b45
--- /dev/null
+++ b/modules/home/overview/modules/overview/qmldir
@@ -0,0 +1,3 @@
+Overview 1.0 Overview.qml
+OverviewWidget 1.0 OverviewWidget.qml
+OverviewWindow 1.0 OverviewWindow.qml
diff --git a/modules/home/overview/services/GlobalStates.qml b/modules/home/overview/services/GlobalStates.qml
new file mode 100644
index 0000000..7644e38
--- /dev/null
+++ b/modules/home/overview/services/GlobalStates.qml
@@ -0,0 +1,11 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+
+Singleton {
+ id: root
+ property bool overviewOpen: false
+ property bool superReleaseMightTrigger: true
+}
diff --git a/modules/home/overview/services/HyprlandData.qml b/modules/home/overview/services/HyprlandData.qml
new file mode 100644
index 0000000..e23472d
--- /dev/null
+++ b/modules/home/overview/services/HyprlandData.qml
@@ -0,0 +1,137 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+/**
+ * Provides access to some Hyprland data not available in Quickshell.Hyprland.
+ */
+Singleton {
+ id: root
+ property var windowList: []
+ property var addresses: []
+ property var windowByAddress: ({})
+ property var workspaces: []
+ property var workspaceIds: []
+ property var workspaceById: ({})
+ property var activeWorkspace: null
+ property var monitors: []
+ property var layers: ({})
+
+ function updateWindowList() {
+ getClients.running = true;
+ }
+
+ function updateLayers() {
+ getLayers.running = true;
+ }
+
+ function updateMonitors() {
+ getMonitors.running = true;
+ }
+
+ function updateWorkspaces() {
+ getWorkspaces.running = true;
+ getActiveWorkspace.running = true;
+ }
+
+ function updateAll() {
+ updateWindowList();
+ updateMonitors();
+ updateLayers();
+ updateWorkspaces();
+ }
+
+ function biggestWindowForWorkspace(workspaceId) {
+ const windowsInThisWorkspace = HyprlandData.windowList.filter(w => w.workspace.id == workspaceId);
+ return windowsInThisWorkspace.reduce((maxWin, win) => {
+ const maxArea = (maxWin?.size?.[0] ?? 0) * (maxWin?.size?.[1] ?? 0);
+ const winArea = (win?.size?.[0] ?? 0) * (win?.size?.[1] ?? 0);
+ return winArea > maxArea ? win : maxWin;
+ }, null);
+ }
+
+ Component.onCompleted: {
+ updateAll();
+ }
+
+ Connections {
+ target: Hyprland
+
+ function onRawEvent(event) {
+ updateAll()
+ }
+ }
+
+ Process {
+ id: getClients
+ command: ["hyprctl", "clients", "-j"]
+ stdout: StdioCollector {
+ id: clientsCollector
+ onStreamFinished: {
+ root.windowList = JSON.parse(clientsCollector.text)
+ let tempWinByAddress = {};
+ for (var i = 0; i < root.windowList.length; ++i) {
+ var win = root.windowList[i];
+ tempWinByAddress[win.address] = win;
+ }
+ root.windowByAddress = tempWinByAddress;
+ root.addresses = root.windowList.map(win => win.address);
+ }
+ }
+ }
+
+ Process {
+ id: getMonitors
+ command: ["hyprctl", "monitors", "-j"]
+ stdout: StdioCollector {
+ id: monitorsCollector
+ onStreamFinished: {
+ root.monitors = JSON.parse(monitorsCollector.text);
+ }
+ }
+ }
+
+ Process {
+ id: getLayers
+ command: ["hyprctl", "layers", "-j"]
+ stdout: StdioCollector {
+ id: layersCollector
+ onStreamFinished: {
+ root.layers = JSON.parse(layersCollector.text);
+ }
+ }
+ }
+
+ Process {
+ id: getWorkspaces
+ command: ["hyprctl", "workspaces", "-j"]
+ stdout: StdioCollector {
+ id: workspacesCollector
+ onStreamFinished: {
+ root.workspaces = JSON.parse(workspacesCollector.text);
+ let tempWorkspaceById = {};
+ for (var i = 0; i < root.workspaces.length; ++i) {
+ var ws = root.workspaces[i];
+ tempWorkspaceById[ws.id] = ws;
+ }
+ root.workspaceById = tempWorkspaceById;
+ root.workspaceIds = root.workspaces.map(ws => ws.id);
+ }
+ }
+ }
+
+ Process {
+ id: getActiveWorkspace
+ command: ["hyprctl", "activeworkspace", "-j"]
+ stdout: StdioCollector {
+ id: activeWorkspaceCollector
+ onStreamFinished: {
+ root.activeWorkspace = JSON.parse(activeWorkspaceCollector.text);
+ }
+ }
+ }
+}
diff --git a/modules/home/overview/services/qmldir b/modules/home/overview/services/qmldir
new file mode 100644
index 0000000..b900864
--- /dev/null
+++ b/modules/home/overview/services/qmldir
@@ -0,0 +1,2 @@
+singleton HyprlandData 1.0 HyprlandData.qml
+singleton GlobalStates 1.0 GlobalStates.qml
diff --git a/modules/home/overview/shell.qml b/modules/home/overview/shell.qml
new file mode 100644
index 0000000..e92b4eb
--- /dev/null
+++ b/modules/home/overview/shell.qml
@@ -0,0 +1,16 @@
+//@ pragma UseQApplication
+//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
+
+import "./modules/overview/"
+import "./services/"
+import "./common/"
+import "./common/functions/"
+import "./common/widgets/"
+
+import QtQuick
+import Quickshell
+import Quickshell.Hyprland
+
+ShellRoot {
+ Overview {}
+}
diff --git a/modules/home/scripts/default.nix b/modules/home/scripts/default.nix
index 37c694c..6eb6d96 100644
--- a/modules/home/scripts/default.nix
+++ b/modules/home/scripts/default.nix
@@ -30,5 +30,6 @@
inherit username;
})
(import ./web-search.nix {inherit pkgs;})
+ (import ./restart.noctalia.nix {inherit pkgs;})
];
}
diff --git a/modules/home/scripts/restart.noctalia b/modules/home/scripts/restart.noctalia
new file mode 100644
index 0000000..747c18a
--- /dev/null
+++ b/modules/home/scripts/restart.noctalia
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Restart the Noctalia QuickShell session by terminating only the noctalia-shell
+# processes, avoiding any signals to unrelated process groups (e.g. Hyprland).
+
+log() { printf "[restart.noctalia] %s\n" "$*"; }
+
+list_target_pids() {
+ # Collect only PIDs whose command explicitly runs noctalia-shell
+ # - direct wrapper: ".../noctalia-shell"
+ # - quickshell/qs with "-c noctalia-shell"
+ ps -eo pid=,cmd= \
+ | ${GREP:-grep} -E "(^|/)(noctalia-shell)( |$)|(^| )((qs|quickshell))( | ).*-c( |=)?noctalia-shell( |$)" \
+ | awk '{print $1}'
+}
+
+terminate_targets() {
+ local pids left tries
+ mapfile -t pids < <(list_target_pids || true)
+
+ if ((${#pids[@]} > 0)); then
+ kill -TERM "${pids[@]}" 2>/dev/null || true
+ fi
+
+ # Wait up to ~3s for clean exit
+ for tries in {1..15}; do
+ mapfile -t left < <(list_target_pids || true)
+ ((${#left[@]} == 0)) && break
+ sleep 0.2
+ done
+
+ # Force kill leftovers only (do not touch anything else)
+ if ((${#left[@]} > 0)); then
+ kill -KILL "${left[@]}" 2>/dev/null || true
+ fi
+}
+
+start_noctalia() {
+ # Prefer the noctalia-shell wrapper to ensure proper env and runtime flags
+ if command -v noctalia-shell >/dev/null 2>&1; then
+ nohup setsid noctalia-shell >/dev/null 2>&1 &
+ elif command -v quickshell >/dev/null 2>&1; then
+ nohup setsid quickshell -c noctalia-shell >/dev/null 2>&1 &
+ elif command -v qs >/dev/null 2>&1; then
+ nohup setsid qs -c noctalia-shell >/dev/null 2>&1 &
+ else
+ echo "Error: noctalia-shell/quickshell/qs not found in PATH" >&2
+ exit 1
+ fi
+}
+
+terminate_targets
+start_noctalia
diff --git a/modules/home/scripts/restart.noctalia.nix b/modules/home/scripts/restart.noctalia.nix
new file mode 100644
index 0000000..d229149
--- /dev/null
+++ b/modules/home/scripts/restart.noctalia.nix
@@ -0,0 +1,24 @@
+{pkgs, ...}: let
+ binPath = pkgs.lib.makeBinPath [
+ pkgs.coreutils
+ pkgs.procps
+ pkgs.psmisc
+ pkgs.gnugrep
+ pkgs.findutils
+ pkgs.util-linux
+ pkgs.bash
+ ];
+ script = builtins.readFile ./restart.noctalia;
+in
+ pkgs.writeShellScriptBin "restart.noctalia" ''
+ set -euo pipefail
+ export PATH=${binPath}:$PATH
+
+ tmp_script=$(mktemp)
+ trap 'rm -f "$tmp_script"' EXIT
+ cat > "$tmp_script" <<'BASH_EOF'
+ ${script}
+ BASH_EOF
+ chmod +x "$tmp_script"
+ exec ${pkgs.bash}/bin/bash "$tmp_script" "$@"
+ ''
diff --git a/modules/home/stylix.nix b/modules/home/stylix.nix
index 006ac78..b593e7d 100644
--- a/modules/home/stylix.nix
+++ b/modules/home/stylix.nix
@@ -12,5 +12,6 @@
enable = true;
platform = "qtct";
};
+ noctalia-shell.enable = true;
};
}