1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

compositor service & use toplevels instead of niri data

This commit is contained in:
bbedward
2025-08-20 17:31:10 -04:00
parent 835d46a7af
commit be4c09e56d
13 changed files with 236 additions and 537 deletions

View File

@@ -381,7 +381,7 @@ Singleton {
}
function hasNamedWorkspaces() {
if (typeof NiriService === "undefined" || !NiriService.niriAvailable)
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
return false
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
@@ -394,7 +394,7 @@ Singleton {
function getNamedWorkspaces() {
var namedWorkspaces = []
if (typeof NiriService === "undefined" || !NiriService.niriAvailable)
if (typeof NiriService === "undefined" || !CompositorService.isNiri)
return namedWorkspaces
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {

View File

@@ -625,6 +625,7 @@ Singleton {
}
}
Component.onCompleted: {
if (typeof Colors !== "undefined")
Colors.colorsUpdated.connect(root.onColorsUpdated)

View File

@@ -16,24 +16,18 @@ PanelWindow {
property var modelData
property var contextMenu
property var windowsMenu
property bool autoHide: SettingsData.dockAutoHide
property real backgroundTransparency: SettingsData.dockTransparency
property bool contextMenuOpen: (contextMenu && contextMenu.visible
&& contextMenu.screen === modelData)
|| (windowsMenu && windowsMenu.visible
&& windowsMenu.screen === modelData)
property bool windowIsFullscreen: {
if (!NiriService.focusedWindowId || !NiriService.niriAvailable)
return false
var focusedWindow = NiriService.windows.find(
w => w.id === NiriService.focusedWindowId)
if (!focusedWindow)
if (!ToplevelManager.activeToplevel)
return false
var activeWindow = ToplevelManager.activeToplevel
var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
return fullscreenApps.some(app => focusedWindow.app_id
&& focusedWindow.app_id.toLowerCase(
return fullscreenApps.some(app => activeWindow.appId
&& activeWindow.appId.toLowerCase(
).includes(app))
}
property bool reveal: (!autoHide || dockMouseArea.containsMouse
@@ -142,7 +136,6 @@ PanelWindow {
anchors.bottomMargin: 4
contextMenu: dock.contextMenu
windowsMenu: dock.windowsMenu
}
}

View File

@@ -11,7 +11,6 @@ Item {
property var appData
property var contextMenu: null
property var windowsMenu: null
property var dockApps: null
property int index: -1
property bool longPressing: false
@@ -203,9 +202,10 @@ Item {
["gtk-launch", appData.appId])
}
} else if (appData.type === "window") {
// Focus the specific window
if (appData.windowId)
NiriService.focusWindow(appData.windowId)
// Focus the specific window using toplevel
if (appData.toplevelObject) {
appData.toplevelObject.activate()
}
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
@@ -301,8 +301,8 @@ Item {
if (!appData)
return "transparent"
// For window type, check if focused
if (appData.type === "window" && appData.isFocused)
// For window type, check if focused using reactive property
if (appData.type === "window" && appData.toplevelObject && appData.toplevelObject.activated)
return Theme.primary
// For running apps, show dimmer indicator

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
@@ -9,7 +10,6 @@ Item {
id: root
property var contextMenu: null
property var windowsMenu: null
property bool requestDockShow: false
property int pinnedAppCount: 0
@@ -62,15 +62,17 @@ Item {
"isPinned"// Use -1 instead of null
: true,
"isRunning": false,
"isFocused": false
})
})
root.pinnedAppCount = pinnedApps.length
// Get sorted toplevels from CompositorService
var sortedToplevels = CompositorService.sortedToplevels
// Add separator between pinned and running if both exist
if (pinnedApps.length > 0
&& NiriService.windows.length > 0) {
&& sortedToplevels.length > 0) {
items.push({
"type": "separator",
"appId": "__SEPARATOR__",
@@ -85,33 +87,25 @@ Item {
})
}
// Second section: Running windows (sorted by display->workspace->position)
// NiriService.windows is already sorted by sortWindowsByLayout
NiriService.windows.forEach(window => {
// Limit window title length for tooltip
var title = window.title
|| "(Unnamed)"
if (title.length > 50) {
title = title.substring(
0, 47) + "..."
}
// Check if this window is focused - compare as numbers
var isFocused = window.id
== NiriService.focusedWindowId
items.push({
"type": "window",
"appId": window.app_id
|| "",
"windowId": window.id || -1,
"windowTitle": title,
"workspaceId": window.workspace_id || -1,
"isPinned": false,
"isRunning": true,
"isFocused": isFocused
})
})
// Second section: Running windows (sorted using Theme.sortToplevels)
sortedToplevels.forEach(toplevel => {
// Limit window title length for tooltip
var title = toplevel.title || "(Unnamed)"
if (title.length > 50) {
title = title.substring(0, 47) + "..."
}
items.push({
"type": "window",
"appId": toplevel.appId || "",
"windowId": -1, // Toplevel doesn't have numeric ID
"windowTitle": title,
"workspaceId": -1, // Will be handled by sorting
"isPinned": false,
"isRunning": true,
"toplevelObject": toplevel
})
})
items.forEach(item => {
append(item)
@@ -146,7 +140,6 @@ Item {
appData: model
contextMenu: root.contextMenu
windowsMenu: root.windowsMenu
dockApps: root
index: model.index
@@ -159,17 +152,12 @@ Item {
}
Connections {
target: NiriService
function onWindowsChanged() {
dockModel.updateModel()
}
function onWindowOpenedOrChanged() {
dockModel.updateModel()
}
function onFocusedWindowIdChanged() {
target: CompositorService
function onSortedToplevelsChanged() {
dockModel.updateModel()
}
}
Connections {
target: SessionData

View File

@@ -187,59 +187,7 @@ PanelWindow {
}
Rectangle {
visible: !!(root.appData && root.appData.windows
&& root.appData.windows.count > 0)
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
Repeater {
model: root.appData
&& root.appData.windows ? root.appData.windows : null
Rectangle {
required property var model
width: menuColumn.width
height: 28
radius: Theme.cornerRadius
color: windowArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: model.title || "Untitled Window"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: windowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NiriService.focusWindow(model.id)
root.close()
}
}
}
}
Rectangle {
visible: !!(root.appData && root.appData.windows
&& root.appData.windows.count > 1)
visible: root.appData && root.appData.type === "window"
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
@@ -247,16 +195,23 @@ PanelWindow {
}
Rectangle {
visible: !!(root.appData && root.appData.windows
&& root.appData.windows.count > 1)
visible: root.appData && root.appData.type === "window"
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
}
Rectangle {
visible: root.appData && root.appData.type === "window"
width: parent.width
height: 28
radius: Theme.cornerRadius
color: closeAllArea.containsMouse ? Qt.rgba(
Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : "transparent"
color: closeArea.containsMouse ? Qt.rgba(
Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
@@ -264,25 +219,22 @@ PanelWindow {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Close All Windows"
text: "Close Window"
font.pixelSize: Theme.fontSizeSmall
color: closeAllArea.containsMouse ? Theme.error : Theme.surfaceText
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: closeAllArea
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.appData || !root.appData.windows)
return
for (var i = 0; i < root.appData.windows.count; i++) {
var window = root.appData.windows.get(i)
NiriService.closeWindow(window.id)
if (root.appData && root.appData.toplevelObject) {
root.appData.toplevelObject.close()
}
root.close()
}

View File

@@ -1,215 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
property bool showWindowsMenu: false
property var appData: null
property var anchorItem: null
property real dockVisibleHeight: 40
property int margin: 10
function showForButton(button, data, dockHeight) {
anchorItem = button
appData = data
dockVisibleHeight = dockHeight || 40
var dockWindow = button.Window.window
if (dockWindow) {
for (var i = 0; i < Quickshell.screens.length; i++) {
var s = Quickshell.screens[i]
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
root.screen = s
break
}
}
}
showWindowsMenu = true
}
function close() {
showWindowsMenu = false
}
screen: Quickshell.screens[0]
visible: showWindowsMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
onAnchorItemChanged: updatePosition()
onVisibleChanged: if (visible)
updatePosition()
function updatePosition() {
if (!anchorItem) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
}
var dockWindow = anchorItem.Window.window
if (!dockWindow) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
}
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
var actualDockHeight = root.dockVisibleHeight // fallback
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item
}
for (var i = 0; i < item.children.length; i++) {
var found = findDockBackground(item.children[i])
if (found)
return found
}
return null
}
var dockBackground = findDockBackground(dockWindow.contentItem)
if (dockBackground) {
actualDockHeight = dockBackground.height
}
var dockBottomMargin = 16 // The dock has bottom margin
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
var dockContentWidth = dockWindow.width
var screenWidth = root.screen.width
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
}
Rectangle {
id: menuContainer
width: Math.min(600, Math.max(
250,
windowColumn.implicitWidth + Theme.spacingS * 2))
height: Math.max(60, windowColumn.implicitHeight + Theme.spacingS * 2)
x: {
var left = 10
var right = root.width - width - 10
var want = root.anchorPos.x - width / 2
return Math.max(left, Math.min(right, want))
}
y: Math.max(10, root.anchorPos.y - height + 30)
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
opacity: showWindowsMenu ? 1 : 0
scale: showWindowsMenu ? 1 : 0.85
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: windowColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
Repeater {
model: root.appData
&& root.appData.windows ? root.appData.windows : null
Rectangle {
required property var model
width: windowColumn.width
height: 32
radius: Theme.cornerRadius
color: windowArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: model.title || "Untitled Window"
font.pixelSize: Theme.fontSizeSmall
color: model.is_focused ? Theme.primary : Theme.surfaceText
font.weight: model.is_focused ? Font.Medium : Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: windowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NiriService.focusWindow(model.id)
root.close()
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
hoverEnabled: false
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
root.close()
}
}
}

View File

@@ -1,4 +1,5 @@
import Quickshell
import Quickshell.Wayland
import QtQuick
import qs.Common
import qs.Services
@@ -12,6 +13,7 @@ Rectangle {
readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2
readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
width: compactMode ? Math.min(baseWidth,
maxCompactWidth) : Math.min(baseWidth,
@@ -19,7 +21,7 @@ Rectangle {
height: 30
radius: Theme.cornerRadius
color: {
if (!NiriService.focusedWindowTitle)
if (!activeWindow || !activeWindow.title)
return "transparent"
const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover
@@ -27,7 +29,7 @@ Rectangle {
baseColor.a * Theme.widgetTransparency)
}
clip: true
visible: NiriService.niriAvailable && NiriService.focusedWindowTitle
visible: activeWindow && activeWindow.title
Row {
id: contentRow
@@ -39,18 +41,12 @@ Rectangle {
id: appText
text: {
if (!NiriService.focusedWindowId)
if (!activeWindow || !activeWindow.appId)
return ""
var window = NiriService.windows.find(w => {
return w.id == NiriService.focusedWindowId
})
if (!window || !window.app_id)
return ""
var desktopEntry = DesktopEntries.byId(window.app_id)
var desktopEntry = DesktopEntries.byId(activeWindow.appId)
return desktopEntry
&& desktopEntry.name ? desktopEntry.name : window.app_id
&& desktopEntry.name ? desktopEntry.name : activeWindow.appId
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
@@ -74,7 +70,7 @@ Rectangle {
id: titleText
text: {
var title = NiriService.focusedWindowTitle || ""
var title = activeWindow && activeWindow.title ? activeWindow.title : ""
var appName = appText.text
if (!title || !appName)

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
@@ -15,7 +16,8 @@ Rectangle {
property var topBar: null
// The visual root for this window
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
readonly property int windowCount: NiriService.windows.length
readonly property var sortedToplevels: CompositorService.sortedToplevels
readonly property int windowCount: sortedToplevels.length
readonly property int calculatedWidth: {
if (windowCount === 0)
return 0
@@ -50,16 +52,15 @@ Rectangle {
Repeater {
id: windowRepeater
model: NiriService.windows
model: sortedToplevels
delegate: Item {
id: delegateItem
property bool isFocused: String(modelData.id) === String(
NiriService.focusedWindowId)
property string appId: modelData.app_id || ""
property bool isFocused: modelData.activated
property string appId: modelData.appId || ""
property string windowTitle: modelData.title || "(Unnamed)"
property int windowId: modelData.id
property var toplevelObject: modelData
property string tooltipText: {
var appName = "Unknown"
if (appId) {
@@ -176,7 +177,9 @@ Rectangle {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NiriService.focusWindow(windowId)
if (toplevelObject) {
toplevelObject.activate()
}
}
onEntered: {
root.hoveredItem = delegateItem

View File

@@ -24,7 +24,7 @@ Rectangle {
}
function getDisplayWorkspaces() {
if (!NiriService.niriAvailable
if (!CompositorService.isNiri
|| NiriService.allWorkspaces.length === 0)
return [1, 2]
@@ -41,7 +41,7 @@ Rectangle {
}
function getDisplayActiveWorkspace() {
if (!NiriService.niriAvailable
if (!CompositorService.isNiri
|| NiriService.allWorkspaces.length === 0)
return 1
@@ -68,7 +68,7 @@ Rectangle {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
visible: NiriService.niriAvailable
visible: CompositorService.isNiri
Connections {
function onAllWorkspacesChanged() {
@@ -83,16 +83,10 @@ Rectangle {
root.currentWorkspace = root.getDisplayActiveWorkspace()
}
function onNiriAvailableChanged() {
if (NiriService.niriAvailable) {
root.workspaceList = SettingsData.showWorkspacePadding ? root.padWorkspaces(root.getDisplayWorkspaces()) : root.getDisplayWorkspaces()
root.currentWorkspace = root.getDisplayActiveWorkspace()
}
}
target: NiriService
}
Connections {
function onShowWorkspacePaddingChanged() {
var baseList = root.getDisplayWorkspaces()
@@ -118,7 +112,7 @@ Rectangle {
property bool isHovered: mouseArea.containsMouse
property int sequentialNumber: index + 1
property var workspaceData: {
if (isPlaceholder || !NiriService.niriAvailable)
if (isPlaceholder || !CompositorService.isNiri)
return null
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i]

View File

@@ -0,0 +1,84 @@
pragma Singleton
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Singleton {
id: root
// Compositor detection
property bool isHyprland: false
property bool isNiri: false
property string compositor: "unknown"
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
property bool useNiriSorting: isNiri && NiriService
// Unified sorted toplevels - automatically chooses sorting based on compositor
property var sortedToplevels: {
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) {
return []
}
// Only use niri sorting when both compositor is niri AND niri service is ready
if (useNiriSorting) {
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
}
// For non-niri compositors or when niri isn't ready yet, return unsorted toplevels
return ToplevelManager.toplevels.values
}
Component.onCompleted: {
console.log("CompositorService: Starting detection...")
detectCompositor()
}
function detectCompositor() {
// Check for Hyprland first
if (hyprlandSignature && hyprlandSignature.length > 0) {
isHyprland = true
isNiri = false
compositor = "hyprland"
console.log("CompositorService: Detected Hyprland with signature:", hyprlandSignature)
return
}
// Check for Niri
if (niriSocket && niriSocket.length > 0) {
// Verify the socket actually exists
niriSocketCheck.running = true
} else {
// No compositor detected, default to Niri
isHyprland = false
isNiri = false
compositor = "unknown"
console.warn("CompositorService: No compositor detected")
}
}
Process {
id: niriSocketCheck
command: ["test", "-S", root.niriSocket]
onExited: exitCode => {
if (exitCode === 0) {
root.isNiri = true
root.isHyprland = false
root.compositor = "niri"
console.log("CompositorService: Detected Niri with socket:", root.niriSocket)
} else {
root.isHyprland = false
root.isNiri = true
root.compositor = "niri"
console.warn("CompositorService: Niri socket check failed, defaulting to Niri anyway")
}
}
}
}

View File

@@ -5,6 +5,7 @@ pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Singleton {
id: root
@@ -22,45 +23,19 @@ Singleton {
// Window management
property var windows: []
property int focusedWindowIndex: -1
property string focusedWindowTitle: "(No active window)"
property string focusedWindowId: ""
// Overview state
property bool inOverview: false
// Config validation
// Internal state (not exposed to external components)
property string configValidationOutput: ""
property bool hasInitialConnection: false
signal windowOpenedOrChanged(var windowData)
// Feature availability
property bool niriAvailable: false
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
Component.onCompleted: checkNiriAvailability()
Process {
id: niriCheck
command: ["test", "-S", root.socketPath]
onExited: exitCode => {
root.niriAvailable = exitCode === 0
if (root.niriAvailable) {
eventStreamSocket.connected = true
fetchOutputs()
}
}
}
function checkNiriAvailability() {
niriCheck.running = true
}
function fetchOutputs() {
if (niriAvailable) {
if (CompositorService.isNiri) {
outputsProcess.running = true
}
}
@@ -98,7 +73,7 @@ Singleton {
Socket {
id: eventStreamSocket
path: root.socketPath
connected: false
connected: CompositorService.isNiri
onConnectionStateChanged: {
if (connected) {
@@ -121,7 +96,7 @@ Singleton {
Socket {
id: requestSocket
path: root.socketPath
connected: root.niriAvailable
connected: CompositorService.isNiri
}
function sortWindowsByLayout(windowList) {
@@ -203,8 +178,6 @@ Singleton {
handleWindowsChanged(event.WindowsChanged)
} else if (event.WindowClosed) {
handleWindowClosed(event.WindowClosed)
} else if (event.WindowFocusChanged) {
handleWindowFocusChanged(event.WindowFocusChanged)
} else if (event.WindowOpenedOrChanged) {
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
} else if (event.WindowLayoutsChanged) {
@@ -279,10 +252,6 @@ Singleton {
// This is crucial for handling floating window close scenarios
if (data.active_window_id !== null
&& data.active_window_id !== undefined) {
focusedWindowId = String(data.active_window_id)
focusedWindowIndex = windows.findIndex(
w => w.id == data.active_window_id)
// Create new windows array with updated focus states to trigger property change
let updatedWindows = []
for (var i = 0; i < windows.length; i++) {
@@ -295,13 +264,8 @@ Singleton {
updatedWindows.push(updatedWindow)
}
windows = updatedWindows
updateFocusedWindow()
} else {
// No active window in this workspace
focusedWindowId = ""
focusedWindowIndex = -1
// Create new windows array with cleared focus states for this workspace
let updatedWindows = []
for (var i = 0; i < windows.length; i++) {
@@ -315,42 +279,15 @@ Singleton {
updatedWindows.push(updatedWindow)
}
windows = updatedWindows
updateFocusedWindow()
}
}
function handleWindowsChanged(data) {
windows = sortWindowsByLayout(data.windows)
// Extract focused window from initial state
var focusedWindow = windows.find(w => w.is_focused)
if (focusedWindow) {
focusedWindowId = String(focusedWindow.id)
focusedWindowIndex = windows.findIndex(
w => w.id === focusedWindow.id)
} else {
focusedWindowId = ""
focusedWindowIndex = -1
}
updateFocusedWindow()
}
function handleWindowClosed(data) {
windows = windows.filter(w => w.id !== data.id)
updateFocusedWindow()
}
function handleWindowFocusChanged(data) {
if (data.id) {
focusedWindowId = data.id
focusedWindowIndex = windows.findIndex(w => w.id === data.id)
} else {
focusedWindowId = ""
focusedWindowIndex = -1
}
updateFocusedWindow()
}
function handleWindowOpenedOrChanged(data) {
@@ -368,14 +305,6 @@ Singleton {
windows = sortWindowsByLayout([...windows, window])
}
if (window.is_focused) {
focusedWindowId = window.id
focusedWindowIndex = windows.findIndex(w => w.id === window.id)
}
updateFocusedWindow()
windowOpenedOrChanged(window)
}
function handleWindowLayoutsChanged(data) {
@@ -477,17 +406,8 @@ Singleton {
currentOutputWorkspaces = outputWs
}
function updateFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
var focusedWin = windows[focusedWindowIndex]
focusedWindowTitle = focusedWin.title || "(Unnamed window)"
} else {
focusedWindowTitle = "(No active window)"
}
}
function send(request) {
if (!niriAvailable || !requestSocket.connected)
if (!CompositorService.isNiri || !requestSocket.connected)
return false
requestSocket.write(JSON.stringify(request) + "\n")
return true
@@ -518,25 +438,7 @@ Singleton {
return 1
}
function focusWindow(windowId) {
return send({
"Action": {
"FocusWindow": {
"id": windowId
}
}
})
}
function closeWindow(windowId) {
return send({
"Action": {
"CloseWindow": {
"id": windowId
}
}
})
}
function quit() {
return send({
@@ -548,58 +450,66 @@ Singleton {
})
}
function getWindowsByAppId(appId) {
if (!appId)
return []
return windows.filter(w => w.app_id && w.app_id.toLowerCase(
) === appId.toLowerCase())
function sortToplevels(toplevels) {
if (!toplevels || toplevels.length === 0 || !CompositorService.isNiri || windows.length === 0) {
return [...toplevels]
}
// Create a map to match toplevels to niri windows
// We'll match by appId and title since toplevels don't have numeric IDs
var toplevelToNiriMap = {}
for (var i = 0; i < toplevels.length; i++) {
var toplevel = toplevels[i]
if (!toplevel.appId) continue
// Find matching niri window by appId and optionally title
for (var j = 0; j < windows.length; j++) {
var niriWindow = windows[j]
// Match by appId
if (niriWindow.app_id === toplevel.appId) {
// If title also matches or niri window has no title, use this match
if (!niriWindow.title || niriWindow.title === toplevel.title) {
toplevelToNiriMap[i] = {
niriIndex: j,
niriWindow: niriWindow
}
break
}
// If we found appId match but no title match yet, store as fallback
if (!(i in toplevelToNiriMap)) {
toplevelToNiriMap[i] = {
niriIndex: j,
niriWindow: niriWindow
}
}
}
}
}
// Sort toplevels using niri's ordering
return [...toplevels].sort((a, b) => {
var aIndex = toplevels.indexOf(a)
var bIndex = toplevels.indexOf(b)
var aNiri = toplevelToNiriMap[aIndex]
var bNiri = toplevelToNiriMap[bIndex]
// If both have niri data, use niri ordering
if (aNiri && bNiri) {
return aNiri.niriIndex - bNiri.niriIndex
}
// If only one has niri data, prioritize it
if (aNiri && !bNiri) return -1
if (!aNiri && bNiri) return 1
// If neither has niri data, keep original toplevel order
return 0
})
}
function getRunningAppIds() {
var appIds = new Set()
windows.forEach(w => {
if (w.app_id) {
appIds.add(w.app_id.toLowerCase())
}
})
return Array.from(appIds)
}
function getRunningAppIdsOrdered() {
// Get unique app IDs in order they appear in the Niri layout
// Windows are sorted by workspace and then by position within the workspace
var sortedWindows = [...windows].sort((a, b) => {
// If both have layout info, sort by position
if (a.layout && b.layout
&& a.layout.pos_in_scrolling_layout
&& b.layout.pos_in_scrolling_layout) {
var aPos = a.layout.pos_in_scrolling_layout
var bPos = b.layout.pos_in_scrolling_layout
// First compare workspace index
if (aPos[0] !== bPos[0]) {
return aPos[0] - bPos[0]
}
// Then compare position within workspace
return aPos[1] - bPos[1]
}
// Fallback to window ID if no layout info
return a.id - b.id
})
var appIds = []
var seenApps = new Set()
sortedWindows.forEach(w => {
if (w.app_id) {
var lowerAppId = w.app_id.toLowerCase()
if (!seenApps.has(lowerAppId)) {
appIds.push(lowerAppId)
seenApps.add(lowerAppId)
}
}
})
return appIds
}
}

View File

@@ -44,11 +44,9 @@ ShellRoot {
delegate: Dock {
modelData: item
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
windowsMenu: dockWindowsMenuLoader.item ? dockWindowsMenuLoader.item : null
Component.onCompleted: {
dockContextMenuLoader.active = true
dockWindowsMenuLoader.active = true
}
}
}
@@ -72,14 +70,6 @@ ShellRoot {
}
}
LazyLoader {
id: dockWindowsMenuLoader
active: false
DockWindowsMenu {
id: dockWindowsMenu
}
}
LazyLoader {
id: notificationCenterLoader
@@ -258,6 +248,9 @@ ShellRoot {
}
}
Variants {
model: Quickshell.screens