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. + +![Quickshell](https://img.shields.io/badge/Quickshell-0.2.0-blue?style=flat-square) +![Hyprland](https://img.shields.io/badge/Hyprland-Compatible-purple?style=flat-square) +![Qt6](https://img.shields.io/badge/Qt-6-green?style=flat-square) +![License](https://img.shields.io/badge/License-GPL-orange?style=flat-square) + +
+ +--- + +## ๐Ÿ“ธ Preview + +![Overview Screenshot](assets/image.png) + +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; }; }