From f40fc29e3f549f71353255cb5478f7438aac21a9 Mon Sep 17 00:00:00 2001 From: Sho Date: Sun, 2 Aug 2026 15:04:58 +0900 Subject: [PATCH] feat: add dynamic preview panel resizing and individual window instance close button --- quickshell/modules/dock/BottomDock.qml | 16 ++++++++--- quickshell/services/TaskService.qml | 10 +++++++ quickshell/services/js/kwin_close.js | 30 ++++++++++++++++++++ quickshell/services/python/kwin_close.py | 36 ++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 quickshell/services/js/kwin_close.js create mode 100644 quickshell/services/python/kwin_close.py diff --git a/quickshell/modules/dock/BottomDock.qml b/quickshell/modules/dock/BottomDock.qml index 56ed647..a38b0b8 100644 --- a/quickshell/modules/dock/BottomDock.qml +++ b/quickshell/modules/dock/BottomDock.qml @@ -495,10 +495,14 @@ Item { GlassPanel { id: previewGlass - implicitWidth: previewMainCol.implicitWidth + 20 + implicitWidth: previewMainCol.implicitWidth + 24 implicitHeight: previewMainCol.implicitHeight + 16 anchors.fill: parent + Behavior on implicitWidth { + NumberAnimation { duration: 180; easing.type: Easing.OutCubic } + } + opacity: dockWindowPreview.animProgress scale: 0.90 + 0.10 * dockWindowPreview.animProgress transformOrigin: Item.Bottom @@ -634,9 +638,13 @@ Item { anchors.fill: parent hoverEnabled: true onClicked: { - TaskService.closeApp(modelData.appId) - previewPopIn.running = false - previewPopOut.restart() + TaskService.closeSpecificWindow(modelData) + let rem = root.previewWindowInstances ? root.previewWindowInstances.filter(w => w.id !== modelData.id) : [] + root.previewWindowInstances = rem + if (!rem || rem.length === 0) { + previewPopIn.running = false + previewPopOut.restart() + } } } } diff --git a/quickshell/services/TaskService.qml b/quickshell/services/TaskService.qml index 90500a2..512d37d 100644 --- a/quickshell/services/TaskService.qml +++ b/quickshell/services/TaskService.qml @@ -169,6 +169,16 @@ Item { proc.running = true } + function closeSpecificWindow(winObj) { + if (!winObj) return; + let winId = (winObj.id || "").toString().trim() + let query = (winObj.caption || winObj.appId || "").toLowerCase().trim() + + proc.running = false + proc.command = ["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/kwin_close.py", winId, query] + proc.running = true + } + function focusApp(appId, cmd, iconX, iconY) { if (!appId) return; let lower = appId.toLowerCase().trim() diff --git a/quickshell/services/js/kwin_close.js b/quickshell/services/js/kwin_close.js new file mode 100644 index 0000000..52586b8 --- /dev/null +++ b/quickshell/services/js/kwin_close.js @@ -0,0 +1,30 @@ +// KWin JavaScript Script: Gracefully Close Specific Window Instance +var targetId = "%WIN_ID%".trim(); +var targetQuery = "%TARGET_QUERY%".toLowerCase().trim(); + +if (targetId || targetQuery) { + var windows = workspace.windowList(); + for (var i = 0; i < windows.length; i++) { + var win = windows[i]; + var id = String(win.internalId || win.windowId || ""); + var cls = (win.desktopFileName || win.resourceClass || win.resourceName || "").toLowerCase().trim(); + var caption = (win.caption || "").toLowerCase().trim(); + + var matches = false; + if (targetId && id && (id === targetId || id.indexOf(targetId) !== -1)) { + matches = true; + } + if (!matches && targetQuery) { + if (caption && caption.indexOf(targetQuery) !== -1) { + matches = true; + } else if (cls && cls.indexOf(targetQuery) !== -1) { + matches = true; + } + } + + if (matches) { + win.closeWindow(); + break; + } + } +} diff --git a/quickshell/services/python/kwin_close.py b/quickshell/services/python/kwin_close.py new file mode 100644 index 0000000..7810983 --- /dev/null +++ b/quickshell/services/python/kwin_close.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import sys +import os +import subprocess + +JS_TEMPLATE_PATH = os.path.expanduser('~/.config/quickshell/services/js/kwin_close.js') + +def close_window(win_id="", target_query=""): + try: + with open(JS_TEMPLATE_PATH, 'r', encoding='utf-8') as f: + template = f.read() + + script = template.replace('%WIN_ID%', win_id).replace('%TARGET_QUERY%', target_query) + + target_js = '/tmp/kwin_close.js' + with open(target_js, 'w', encoding='utf-8') as f: + f.write(script) + + # Unload previous script instance to prevent KWin memory leaks + subprocess.run(['busctl', '--user', 'call', 'org.kde.KWin', '/Scripting', 'org.kde.kwin.Scripting', 'unloadScript', 's', target_js], capture_output=True) + + res = subprocess.check_output( + ['busctl', '--user', 'call', 'org.kde.KWin', '/Scripting', 'org.kde.kwin.Scripting', 'loadScript', 's', target_js], + text=True + ) + sid = res.strip().split()[-1] + subprocess.run(['busctl', '--user', 'call', 'org.kde.KWin', f'/Scripting/Script{sid}', 'org.kde.kwin.Script', 'run'], capture_output=True) + subprocess.run(['busctl', '--user', 'call', 'org.kde.KWin', f'/Scripting/Script{sid}', 'org.kde.kwin.Script', 'stop'], capture_output=True) + subprocess.run(['busctl', '--user', 'call', 'org.kde.KWin', '/Scripting', 'org.kde.kwin.Scripting', 'unloadScript', 's', target_js], capture_output=True) + except Exception: + pass + +if __name__ == '__main__': + win_id = sys.argv[1] if len(sys.argv) >= 2 else "" + target_query = sys.argv[2] if len(sys.argv) >= 3 else "" + close_window(win_id, target_query)