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:
@@ -48,7 +48,10 @@ Item {
|
|||||||
clear()
|
clear()
|
||||||
|
|
||||||
var items = []
|
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 pinnedApps = [...(SessionData.pinnedApps || [])]
|
||||||
var addedApps = new Set()
|
var addedApps = new Set()
|
||||||
|
|
||||||
@@ -73,6 +76,7 @@ Item {
|
|||||||
var unpinnedAppsSet = new Set()
|
var unpinnedAppsSet = new Set()
|
||||||
|
|
||||||
// First: Add ALL currently running apps that aren't pinned
|
// First: Add ALL currently running apps that aren't pinned
|
||||||
|
// They come pre-ordered from NiriService if Niri is available
|
||||||
runningApps.forEach(appId => {
|
runningApps.forEach(appId => {
|
||||||
var lowerAppId = appId.toLowerCase()
|
var lowerAppId = appId.toLowerCase()
|
||||||
if (!addedApps.has(lowerAppId)) {
|
if (!addedApps.has(lowerAppId)) {
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ Item {
|
|||||||
"description": "Display currently focused application title",
|
"description": "Display currently focused application title",
|
||||||
"icon": "window",
|
"icon": "window",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
}, {
|
||||||
|
"id": "runningApps",
|
||||||
|
"text": "Running Apps",
|
||||||
|
"description": "Shows all running applications with focus indication",
|
||||||
|
"icon": "apps",
|
||||||
|
"enabled": true
|
||||||
}, {
|
}, {
|
||||||
"id": "clock",
|
"id": "clock",
|
||||||
"text": "Clock",
|
"text": "Clock",
|
||||||
|
|||||||
129
Modules/TopBar/RunningApps.qml
Normal file
129
Modules/TopBar/RunningApps.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import Quickshell.Wayland
|
|||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
|
import qs.Modules.TopBar
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -285,6 +286,8 @@ PanelWindow {
|
|||||||
return true
|
return true
|
||||||
case "focusedWindow":
|
case "focusedWindow":
|
||||||
return true
|
return true
|
||||||
|
case "runningApps":
|
||||||
|
return true
|
||||||
case "clock":
|
case "clock":
|
||||||
return true
|
return true
|
||||||
case "music":
|
case "music":
|
||||||
@@ -330,6 +333,8 @@ PanelWindow {
|
|||||||
return workspaceSwitcherComponent
|
return workspaceSwitcherComponent
|
||||||
case "focusedWindow":
|
case "focusedWindow":
|
||||||
return focusedWindowComponent
|
return focusedWindowComponent
|
||||||
|
case "runningApps":
|
||||||
|
return runningAppsComponent
|
||||||
case "clock":
|
case "clock":
|
||||||
return clockComponent
|
return clockComponent
|
||||||
case "music":
|
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 {
|
Component {
|
||||||
id: clockComponent
|
id: clockComponent
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ Singleton {
|
|||||||
property string focusedWorkspaceId: ""
|
property string focusedWorkspaceId: ""
|
||||||
property var currentOutputWorkspaces: []
|
property var currentOutputWorkspaces: []
|
||||||
property string currentOutput: ""
|
property string currentOutput: ""
|
||||||
|
|
||||||
|
// Output/Monitor management
|
||||||
|
property var outputs: ({}) // Map of output name to output info with positions
|
||||||
|
|
||||||
// Window management
|
// Window management
|
||||||
property var windows: []
|
property var windows: []
|
||||||
@@ -47,6 +50,7 @@ Singleton {
|
|||||||
root.niriAvailable = exitCode === 0
|
root.niriAvailable = exitCode === 0
|
||||||
if (root.niriAvailable) {
|
if (root.niriAvailable) {
|
||||||
eventStreamSocket.connected = true
|
eventStreamSocket.connected = true
|
||||||
|
fetchOutputs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,6 +58,39 @@ Singleton {
|
|||||||
function checkNiriAvailability() {
|
function checkNiriAvailability() {
|
||||||
niriCheck.running = true
|
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 {
|
Socket {
|
||||||
id: eventStreamSocket
|
id: eventStreamSocket
|
||||||
@@ -84,6 +121,66 @@ Singleton {
|
|||||||
connected: root.niriAvailable
|
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) {
|
function handleNiriEvent(event) {
|
||||||
if (event.WorkspacesChanged) {
|
if (event.WorkspacesChanged) {
|
||||||
handleWorkspacesChanged(event.WorkspacesChanged)
|
handleWorkspacesChanged(event.WorkspacesChanged)
|
||||||
@@ -99,6 +196,10 @@ Singleton {
|
|||||||
handleWindowFocusChanged(event.WindowFocusChanged)
|
handleWindowFocusChanged(event.WindowFocusChanged)
|
||||||
} else if (event.WindowOpenedOrChanged) {
|
} else if (event.WindowOpenedOrChanged) {
|
||||||
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
|
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
|
||||||
|
} else if (event.WindowLayoutsChanged) {
|
||||||
|
handleWindowLayoutsChanged(event.WindowLayoutsChanged)
|
||||||
|
} else if (event.OutputsChanged) {
|
||||||
|
handleOutputsChanged(event.OutputsChanged)
|
||||||
} else if (event.OverviewOpenedOrClosed) {
|
} else if (event.OverviewOpenedOrClosed) {
|
||||||
handleOverviewChanged(event.OverviewOpenedOrClosed)
|
handleOverviewChanged(event.OverviewOpenedOrClosed)
|
||||||
} else if (event.ConfigLoaded) {
|
} else if (event.ConfigLoaded) {
|
||||||
@@ -205,7 +306,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowsChanged(data) {
|
function handleWindowsChanged(data) {
|
||||||
windows = [...data.windows].sort((a, b) => a.id - b.id)
|
windows = sortWindowsByLayout(data.windows)
|
||||||
updateFocusedWindow()
|
updateFocusedWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,9 +336,9 @@ Singleton {
|
|||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
let updatedWindows = [...windows]
|
let updatedWindows = [...windows]
|
||||||
updatedWindows[existingIndex] = window
|
updatedWindows[existingIndex] = window
|
||||||
windows = updatedWindows.sort((a, b) => a.id - b.id)
|
windows = sortWindowsByLayout(updatedWindows)
|
||||||
} else {
|
} else {
|
||||||
windows = [...windows, window].sort((a, b) => a.id - b.id)
|
windows = sortWindowsByLayout([...windows, window])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.is_focused) {
|
if (window.is_focused) {
|
||||||
@@ -249,7 +350,47 @@ Singleton {
|
|||||||
|
|
||||||
windowOpenedOrChanged(window)
|
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) {
|
function handleOverviewChanged(data) {
|
||||||
inOverview = data.is_open
|
inOverview = data.is_open
|
||||||
}
|
}
|
||||||
@@ -393,4 +534,42 @@ Singleton {
|
|||||||
})
|
})
|
||||||
return Array.from(appIds)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user