From 755f13e06bb993d8e35897c55d10238a3cdb702d Mon Sep 17 00:00:00 2001 From: bbedward Date: Sun, 13 Jul 2025 11:37:33 -0400 Subject: [PATCH] Focused window service --- Services/FocusedWindowService.qml | 133 ++++++++++++++++++++++++++++ Services/qmldir | 3 +- Widgets/TopBar/FocusedAppWidget.qml | 81 +++++++++++++++++ Widgets/TopBar/TopBar.qml | 4 + Widgets/TopBar/qmldir | 1 + 5 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 Services/FocusedWindowService.qml create mode 100644 Widgets/TopBar/FocusedAppWidget.qml diff --git a/Services/FocusedWindowService.qml b/Services/FocusedWindowService.qml new file mode 100644 index 00000000..1cb3771c --- /dev/null +++ b/Services/FocusedWindowService.qml @@ -0,0 +1,133 @@ +import QtQuick +import Quickshell +import Quickshell.Io +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + + property bool niriAvailable: false + property string focusedAppId: "" + property string focusedAppName: "" + property string focusedWindowTitle: "" + property int focusedWindowId: -1 + + Component.onCompleted: { + // Use the availability from NiriWorkspaceService to avoid duplicate checks + root.niriAvailable = NiriWorkspaceService.niriAvailable + + // Connect to workspace service events + NiriWorkspaceService.onNiriAvailableChanged.connect(() => { + root.niriAvailable = NiriWorkspaceService.niriAvailable + if (root.niriAvailable) { + loadInitialFocusedWindow() + } + }) + + if (root.niriAvailable) { + loadInitialFocusedWindow() + } + } + + // Listen to window focus changes from NiriWorkspaceService + Connections { + target: NiriWorkspaceService + function onFocusedWindowIdChanged() { + root.focusedWindowId = NiriWorkspaceService.focusedWindowId + updateFocusedWindowData() + } + function onFocusedWindowTitleChanged() { + root.focusedWindowTitle = NiriWorkspaceService.focusedWindowTitle + } + } + + // Process to get focused window info + Process { + id: focusedWindowQuery + command: ["niri", "msg", "--json", "focused-window"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + try { + const windowData = JSON.parse(text.trim()) + root.focusedAppId = windowData.app_id || "" + root.focusedWindowTitle = windowData.title || "" + root.focusedAppName = getDisplayName(windowData.app_id || "") + root.focusedWindowId = windowData.id || -1 + } catch (e) { + console.warn("FocusedWindowService: Failed to parse focused window data:", e) + clearFocusedWindow() + } + } else { + clearFocusedWindow() + } + } + } + } + + function loadInitialFocusedWindow() { + if (root.niriAvailable) { + focusedWindowQuery.running = true + } + } + + function updateFocusedWindowData() { + if (root.niriAvailable && root.focusedWindowId !== -1) { + focusedWindowQuery.running = true + } else { + clearFocusedWindow() + } + } + + function clearFocusedWindow() { + root.focusedAppId = "" + root.focusedAppName = "" + root.focusedWindowTitle = "" + } + + // Convert app_id to a more user-friendly display name + function getDisplayName(appId) { + if (!appId) return "" + + // Common app_id to display name mappings + const appNames = { + "com.mitchellh.ghostty": "Ghostty", + "org.mozilla.firefox": "Firefox", + "org.gnome.Nautilus": "Files", + "org.gnome.TextEditor": "Text Editor", + "com.google.Chrome": "Chrome", + "org.telegram.desktop": "Telegram", + "com.spotify.Client": "Spotify", + "org.kde.konsole": "Konsole", + "org.gnome.Terminal": "Terminal", + "code": "VS Code", + "code-oss": "VS Code", + "org.mozilla.Thunderbird": "Thunderbird", + "org.libreoffice.LibreOffice": "LibreOffice", + "org.gimp.GIMP": "GIMP", + "org.blender.Blender": "Blender", + "discord": "Discord", + "slack": "Slack", + "zoom": "Zoom" + } + + // Return mapped name or clean up the app_id + if (appNames[appId]) { + return appNames[appId] + } + + // Try to extract a clean name from the app_id + // Remove common prefixes and make first letter uppercase + let cleanName = appId + .replace(/^(org\.|com\.|net\.|io\.)/, '') + .replace(/\./g, ' ') + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + + return cleanName + } +} \ No newline at end of file diff --git a/Services/qmldir b/Services/qmldir index d8080f2a..48dddfcd 100644 --- a/Services/qmldir +++ b/Services/qmldir @@ -12,4 +12,5 @@ singleton AppSearchService 1.0 AppSearchService.qml singleton LauncherService 1.0 LauncherService.qml singleton NiriWorkspaceService 1.0 NiriWorkspaceService.qml singleton CalendarService 1.0 CalendarService.qml -singleton UserInfoService 1.0 UserInfoService.qml \ No newline at end of file +singleton UserInfoService 1.0 UserInfoService.qml +singleton FocusedWindowService 1.0 FocusedWindowService.qml \ No newline at end of file diff --git a/Widgets/TopBar/FocusedAppWidget.qml b/Widgets/TopBar/FocusedAppWidget.qml new file mode 100644 index 00000000..e5b94425 --- /dev/null +++ b/Widgets/TopBar/FocusedAppWidget.qml @@ -0,0 +1,81 @@ +import QtQuick +import "../../Common" +import "../../Services" + +Rectangle { + id: root + + width: Math.max(contentRow.implicitWidth + Theme.spacingS * 2, 60) + height: 30 + radius: Theme.cornerRadius + color: mouseArea.containsMouse ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : + Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) + + visible: FocusedWindowService.niriAvailable && (FocusedWindowService.focusedAppName || FocusedWindowService.focusedWindowTitle) + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Row { + id: contentRow + anchors.centerIn: parent + spacing: Theme.spacingS + + Text { + id: appText + text: FocusedWindowService.focusedAppName || "" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + + // Limit app name width + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, 120) + } + + Text { + text: "•" + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + anchors.verticalCenter: parent.verticalCenter + visible: appText.text && titleText.text + } + + Text { + id: titleText + text: FocusedWindowService.focusedWindowTitle || "" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + + // Limit title width - increased for longer titles + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, 350) + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + + // Non-interactive widget - just provides hover state for visual feedback + } + + // Smooth width animation when the text changes + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} \ No newline at end of file diff --git a/Widgets/TopBar/TopBar.qml b/Widgets/TopBar/TopBar.qml index 2c0af6d0..733c85b8 100644 --- a/Widgets/TopBar/TopBar.qml +++ b/Widgets/TopBar/TopBar.qml @@ -156,6 +156,10 @@ PanelWindow { anchors.verticalCenter: parent.verticalCenter screenName: topBar.screenName } + + FocusedAppWidget { + anchors.verticalCenter: parent.verticalCenter + } } ClockWidget { diff --git a/Widgets/TopBar/qmldir b/Widgets/TopBar/qmldir index 3ff62967..4dc1d712 100644 --- a/Widgets/TopBar/qmldir +++ b/Widgets/TopBar/qmldir @@ -1,6 +1,7 @@ TopBar 1.0 TopBar.qml LauncherButton 1.0 LauncherButton.qml WorkspaceSwitcher 1.0 WorkspaceSwitcher.qml +FocusedAppWidget 1.0 FocusedAppWidget.qml ClockWidget 1.0 ClockWidget.qml MediaWidget 1.0 MediaWidget.qml WeatherWidget 1.0 WeatherWidget.qml