1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 05:55:37 -05:00

RunningApps widget

- Sorts by monitor, workspace, then position (on a new enough niri
  version)
This commit is contained in:
bbedward
2025-08-18 11:17:33 -04:00
parent 4414b863c7
commit be9bd388c2
5 changed files with 344 additions and 4 deletions

View File

@@ -48,7 +48,10 @@ Item {
clear()
var items = []
var runningApps = NiriService.getRunningAppIds()
// Use ordered app IDs if available from Niri, fallback to unordered
var runningApps = NiriService.niriAvailable && NiriService.getRunningAppIdsOrdered
? NiriService.getRunningAppIdsOrdered()
: NiriService.getRunningAppIds()
var pinnedApps = [...(SessionData.pinnedApps || [])]
var addedApps = new Set()
@@ -73,6 +76,7 @@ Item {
var unpinnedAppsSet = new Set()
// First: Add ALL currently running apps that aren't pinned
// They come pre-ordered from NiriService if Niri is available
runningApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {

View File

@@ -25,6 +25,12 @@ Item {
"description": "Display currently focused application title",
"icon": "window",
"enabled": true
}, {
"id": "runningApps",
"text": "Running Apps",
"description": "Shows all running applications with focus indication",
"icon": "apps",
"enabled": true
}, {
"id": "clock",
"text": "Clock",

View File

@@ -0,0 +1,129 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property string section: "left"
property var parentScreen
readonly property int windowCount: NiriService.windows.length
readonly property int calculatedWidth: windowCount > 0 ? windowCount * 24 + (windowCount - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0
width: calculatedWidth
height: 30
radius: Theme.cornerRadius
visible: windowCount > 0
color: {
if (windowCount === 0)
return "transparent"
const baseColor = Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
Row {
id: windowRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
id: windowRepeater
model: NiriService.windows
delegate: Item {
property bool isFocused: String(modelData.id) === String(FocusedWindowService.focusedWindowId)
property string appId: modelData.app_id || ""
property string windowTitle: modelData.title || "(Unnamed)"
property int windowId: modelData.id
property string tooltipText: {
var appName = "Unknown"
if (appId) {
var desktopEntry = DesktopEntries.byId(appId)
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
}
return appName + (windowTitle ? " • " + windowTitle : "")
}
width: 24
height: 24
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: {
if (isFocused) {
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
} else {
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// App icon
IconImage {
id: iconImg
anchors.centerIn: parent
width: 18
height: 18
source: {
if (!appId) return ""
var desktopEntry = DesktopEntries.byId(appId)
if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(
desktopEntry.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme)
return iconPath
}
return ""
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
}
// Fallback text if no icon found
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
if (!appId) return "?"
var desktopEntry = DesktopEntries.byId(appId)
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
}
return appId.charAt(0).toUpperCase()
}
font.pixelSize: 10
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NiriService.focusWindow(windowId)
}
}
}
}
}
}

View File

@@ -10,6 +10,7 @@ import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules
import qs.Modules.TopBar
import qs.Services
import qs.Widgets
@@ -285,6 +286,8 @@ PanelWindow {
return true
case "focusedWindow":
return true
case "runningApps":
return true
case "clock":
return true
case "music":
@@ -330,6 +333,8 @@ PanelWindow {
return workspaceSwitcherComponent
case "focusedWindow":
return focusedWindowComponent
case "runningApps":
return runningAppsComponent
case "clock":
return clockComponent
case "music":
@@ -651,6 +656,23 @@ PanelWindow {
}
}
Component {
id: runningAppsComponent
RunningApps {
section: {
if (parent && parent.parent === leftSection)
return "left"
if (parent && parent.parent === rightSection)
return "right"
if (parent && parent.parent === centerSection)
return "center"
return "left"
}
parentScreen: root.screen
}
}
Component {
id: clockComponent

View File

@@ -17,6 +17,9 @@ Singleton {
property var currentOutputWorkspaces: []
property string currentOutput: ""
// Output/Monitor management
property var outputs: ({}) // Map of output name to output info with positions
// Window management
property var windows: []
property int focusedWindowIndex: -1
@@ -47,6 +50,7 @@ Singleton {
root.niriAvailable = exitCode === 0
if (root.niriAvailable) {
eventStreamSocket.connected = true
fetchOutputs()
}
}
}
@@ -55,6 +59,39 @@ Singleton {
niriCheck.running = true
}
function fetchOutputs() {
if (niriAvailable) {
outputsProcess.running = true
}
}
Process {
id: outputsProcess
command: ["niri", "msg", "-j", "outputs"]
stdout: StdioCollector {
onStreamFinished: {
try {
var outputsData = JSON.parse(text)
outputs = outputsData
console.log("NiriService: Loaded", Object.keys(outputsData).length, "outputs")
// Re-sort windows with monitor positions
if (windows.length > 0) {
windows = sortWindowsByLayout(windows)
}
} catch (e) {
console.warn("NiriService: Failed to parse outputs:", e)
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
console.warn("NiriService: Failed to fetch outputs, exit code:", exitCode)
}
}
}
Socket {
id: eventStreamSocket
path: root.socketPath
@@ -84,6 +121,66 @@ Singleton {
connected: root.niriAvailable
}
function sortWindowsByLayout(windowList) {
return [...windowList].sort((a, b) => {
// Get workspace info for both windows
var aWorkspace = workspaces[a.workspace_id]
var bWorkspace = workspaces[b.workspace_id]
if (aWorkspace && bWorkspace) {
var aOutput = aWorkspace.output
var bOutput = bWorkspace.output
// 1. First, sort by monitor position (left to right, top to bottom)
var aOutputInfo = outputs[aOutput]
var bOutputInfo = outputs[bOutput]
if (aOutputInfo && bOutputInfo &&
aOutputInfo.logical && bOutputInfo.logical) {
// Sort by monitor X position (left to right)
if (aOutputInfo.logical.x !== bOutputInfo.logical.x) {
return aOutputInfo.logical.x - bOutputInfo.logical.x
}
// If same X, sort by Y position (top to bottom)
if (aOutputInfo.logical.y !== bOutputInfo.logical.y) {
return aOutputInfo.logical.y - bOutputInfo.logical.y
}
}
// 2. If same monitor, sort by workspace index
if (aOutput === bOutput && aWorkspace.idx !== bWorkspace.idx) {
return aWorkspace.idx - bWorkspace.idx
}
}
// 3. If same workspace, sort by actual position within workspace
if (a.workspace_id === b.workspace_id &&
a.layout && b.layout) {
// Use pos_in_scrolling_layout [x, y] coordinates
if (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
if (aPos.length > 1 && bPos.length > 1) {
// Sort by X (horizontal) position first
if (aPos[0] !== bPos[0]) {
return aPos[0] - bPos[0]
}
// Then sort by Y (vertical) position
if (aPos[1] !== bPos[1]) {
return aPos[1] - bPos[1]
}
}
}
}
// 4. Fallback to window ID for consistent ordering
return a.id - b.id
})
}
function handleNiriEvent(event) {
if (event.WorkspacesChanged) {
handleWorkspacesChanged(event.WorkspacesChanged)
@@ -99,6 +196,10 @@ Singleton {
handleWindowFocusChanged(event.WindowFocusChanged)
} else if (event.WindowOpenedOrChanged) {
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
} else if (event.WindowLayoutsChanged) {
handleWindowLayoutsChanged(event.WindowLayoutsChanged)
} else if (event.OutputsChanged) {
handleOutputsChanged(event.OutputsChanged)
} else if (event.OverviewOpenedOrClosed) {
handleOverviewChanged(event.OverviewOpenedOrClosed)
} else if (event.ConfigLoaded) {
@@ -205,7 +306,7 @@ Singleton {
}
function handleWindowsChanged(data) {
windows = [...data.windows].sort((a, b) => a.id - b.id)
windows = sortWindowsByLayout(data.windows)
updateFocusedWindow()
}
@@ -235,9 +336,9 @@ Singleton {
if (existingIndex >= 0) {
let updatedWindows = [...windows]
updatedWindows[existingIndex] = window
windows = updatedWindows.sort((a, b) => a.id - b.id)
windows = sortWindowsByLayout(updatedWindows)
} else {
windows = [...windows, window].sort((a, b) => a.id - b.id)
windows = sortWindowsByLayout([...windows, window])
}
if (window.is_focused) {
@@ -250,6 +351,46 @@ Singleton {
windowOpenedOrChanged(window)
}
function handleWindowLayoutsChanged(data) {
// Update layout positions for windows that have changed
if (!data.changes)
return
let updatedWindows = [...windows]
let hasChanges = false
for (const change of data.changes) {
const windowId = change[0]
const layoutData = change[1]
const windowIndex = updatedWindows.findIndex(w => w.id === windowId)
if (windowIndex >= 0) {
// Create a new object with updated layout
var updatedWindow = {}
for (var prop in updatedWindows[windowIndex]) {
updatedWindow[prop] = updatedWindows[windowIndex][prop]
}
updatedWindow.layout = layoutData
updatedWindows[windowIndex] = updatedWindow
hasChanges = true
}
}
if (hasChanges) {
windows = sortWindowsByLayout(updatedWindows)
// Trigger update in dock and widgets
windowsChanged()
}
}
function handleOutputsChanged(data) {
if (data.outputs) {
outputs = data.outputs
// Re-sort windows with new monitor positions
windows = sortWindowsByLayout(windows)
}
}
function handleOverviewChanged(data) {
inOverview = data.is_open
}
@@ -393,4 +534,42 @@ Singleton {
})
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
}
}