Integrate Quickshell-Overview with Qt6 fixes and automation scripts

## Overview
This commit integrates the corrected Quickshell-Overview feature across all
installation and update workflows. The overview provides an AGS alternative
with live window previews toggled via Super+TAB keybind.

## Changes

### 1. Quickshell Overview QML Files
- Added config/quickshell/overview/ subdirectory with Qt6-compatible QML
- Includes 20+ files covering:
  * OverviewWindow.qml with proper clipping (no OpacityMask, uses QtQuick.Effects)
  * OverviewWidget.qml for window handling
  * Overview.qml main component with Hyprland integration
  * Common utilities and styling
  * Services for Hyprland data and global state management

### 2. copy.sh Updates
- Removes default shell.qml that blocks quickshell named config detection
- Auto-copies config/quickshell/overview to ~/.config/quickshell/overview/
- Updates old 'qs' startup commands to 'qs -c overview'
- Handles both fresh installs and config overwrite scenarios

### 3. upgrade.sh Updates
- Added config/quickshell/ to upgrade directory list
- Excludes shell.qml to preserve overview config detection capability
- Enables seamless upgrades without losing quickshell settings

### 4. IPC Command Fixes
- Corrected OverviewToggle.sh to use proper 'qs ipc -c overview call overview toggle'
- Fixed startup commands from old 'qs' to 'qs -c overview'
- Hyprland-Dots now uses corrected toggle script

## Qt6 Compatibility
- Replaced Qt5Compat.GraphicalEffects with QtQuick.Effects
- Removed OpacityMask in favor of Qt6-compatible clipping technique
- All QML properly imports Qt6 modules

## Release Script
- release.sh automatically uses copy.sh, inheriting all quickshell updates

## Testing
- Verified on target systems (Fedora 43 VM, jak-nixos)
- qs -c overview successfully launches overview config when shell.qml is removed
- IPC toggle commands work correctly within Wayland sessions

## Files Modified
- config/quickshell/overview/* (20 new files)
- copy.sh (enhanced QS handling)
- upgrade.sh (added quickshell to upgrade paths)
This commit is contained in:
Don Williams
2025-11-30 20:04:02 -05:00
parent 510eafbe05
commit ca0c23cce0
22 changed files with 1328 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
# Quickshell Overview for Hyprland
<div align="center">
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)
</div>
---
## 📸 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.
---
<div align="center">
**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
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -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<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1]
readonly property list<real> expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1]
readonly property list<real> 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<real> 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<real> 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<real> 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
}
}

View File

@@ -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
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1 @@
singleton ColorUtils 1.0 ColorUtils.qml

View File

@@ -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

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,4 @@
StyledText 1.0 StyledText.qml
StyledRectangularShadow 1.0 StyledRectangularShadow.qml
StyledToolTip 1.0 StyledToolTip.qml
StyledToolTipContent 1.0 StyledToolTipContent.qml

View File

@@ -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;
}
}
}

View File

@@ -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<OverviewWindow> 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)
}
}
}
}
}

View File

@@ -0,0 +1,112 @@
import QtQuick.Effects
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.smooth: true
layer.mipmap: true
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)
}
Rectangle {
id: clipContainer
anchors.fill: parent
radius: Appearance.rounding.windowRounding * root.scale
clip: true
color: "transparent"
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)
}
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
Overview 1.0 Overview.qml
OverviewWidget 1.0 OverviewWidget.qml
OverviewWindow 1.0 OverviewWindow.qml

View File

@@ -0,0 +1,11 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
property bool overviewOpen: false
property bool superReleaseMightTrigger: true
}

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
singleton HyprlandData 1.0 HyprlandData.qml
singleton GlobalStates 1.0 GlobalStates.qml

View File

@@ -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 {}
}

29
copy.sh
View File

@@ -767,6 +767,13 @@ if command -v qs >/dev/null 2>&1; then
cp -r "config/quickshell/" "$DIRPATH_QS" 2>&1 | tee -a "$LOG"
fi
else
# If default shell.qml exists, it blocks named config subdirectory detection
# Remove it to enable the overview config to be found
if [ -f "$DIRPATH_QS/shell.qml" ]; then
echo "${NOTE} - Removing default shell.qml to enable quickshell overview config detection" 2>&1 | tee -a "$LOG"
rm "$DIRPATH_QS/shell.qml"
fi
read -p "${CAT} Do you want to overwrite your existing ${YELLOW}quickshell${RESET} config? [y/N] " answer_qs
case "$answer_qs" in
[Yy]*)
@@ -777,6 +784,8 @@ if command -v qs >/dev/null 2>&1; then
cp -r "config/quickshell/" "$DIRPATH_QS" 2>&1 | tee -a "$LOG"
if [ $? -eq 0 ]; then
echo "${OK} - ${YELLOW}quickshell${RESET} overwritten successfully."
# Remove default shell.qml from new copy to enable overview detection
rm -f "$DIRPATH_QS/shell.qml" 2>&1 | tee -a "$LOG"
else
echo "${ERROR} - Failed to copy ${YELLOW}quickshell${RESET} config."
exit 1
@@ -787,6 +796,26 @@ if command -v qs >/dev/null 2>&1; then
;;
esac
fi
# Ensure overview subdirectory exists and is up to date
DIRPATH_OVERVIEW="$DIRPATH_QS/overview"
if [ ! -d "$DIRPATH_OVERVIEW" ] && [ -d "config/quickshell/overview" ]; then
echo "${INFO} - Copying quickshell overview config..." 2>&1 | tee -a "$LOG"
cp -r "config/quickshell/overview" "$DIRPATH_QS/" 2>&1 | tee -a "$LOG"
echo "${OK} - Quickshell overview config copied successfully" 2>&1 | tee -a "$LOG"
fi
# Check for old quickshell startup commands and update them
HYPR_STARTUP="$HOME/.config/hypr/configs/Startup_Apps.conf"
if [ -f "$HYPR_STARTUP" ]; then
if grep -q '^exec-once = qs\s*$\|^exec-once = qs &' "$HYPR_STARTUP"; then
echo "${NOTE} - Found old Quickshell startup command, updating to new overview config..." 2>&1 | tee -a "$LOG"
# Replace old 'qs' or 'qs &' with new 'qs -c overview'
sed -i 's/^\(\s*\)exec-once = qs\s*$/\1exec-once = qs -c overview # Quickshell Overview/' "$HYPR_STARTUP" 2>&1 | tee -a "$LOG"
sed -i 's/^\(\s*\)exec-once = qs &$/\1exec-once = qs -c overview # Quickshell Overview/' "$HYPR_STARTUP" 2>&1 | tee -a "$LOG"
echo "${OK} - Updated Quickshell startup command to use overview config" 2>&1 | tee -a "$LOG"
fi
fi
fi
printf "\n%.0s" {1..1}

View File

@@ -91,6 +91,7 @@ declare -A directories=(
["config/waybar/"]="$HOME/.config/waybar/"
["config/cava/"]="$HOME/.config/cava/"
["config/ags/"]="$HOME/.config/ags/"
["config/quickshell/"]="$HOME/.config/quickshell/"
["config/fastfetch/"]="$HOME/.config/fastfetch/"
["config/wallust/"]="$HOME/.config/wallust/"
["config/wlogout/"]="$HOME/.config/wlogout/"
@@ -102,6 +103,7 @@ declare -A exclusions=(
["config/hypr/"]="--exclude=UserConfigs/ --exclude=UserScripts/"
["config/waybar/"]="--exclude=config --exclude=style.css"
["config/rofi/"]="--exclude=.current_wallpaper"
["config/quickshell/"]="--exclude=shell.qml"
# Add more exclusions as needed
)