From 100dcf32fa6b493a963679d80fc33e129147e99a Mon Sep 17 00:00:00 2001 From: Andre Ramnitz Date: Mon, 19 Oct 2026 21:01:03 +0200 Subject: [PATCH] Bugfixes: fix: manual tiling for FLOATING_CLASSES is now possible fix: add wl-copy to IGNORED_CLASSES to suppress ghost-windows in the tree add: extend logging capabilities --- contents/code/main.js | 303 ++++++++++++++++++++++++++++++++++++------ metadata.json | 2 +- 2 files changed, 266 insertions(+), 39 deletions(-) diff --git a/contents/code/main.js b/contents/code/main.js index a0b71c3..5016ed7 100644 --- a/contents/code/main.js +++ b/contents/code/main.js @@ -1,30 +1,86 @@ - "use strict"; -// SimpleDwindle 0.2 -// Minimal chronological Dwindle layout for KWin 6 +// SimpleDwindle +const VERSION = "0.3.2"; +// KWin 6.8 - Minimal chronological Dwindle + +// ------------------------------------------------------------ +// Configuration +// ------------------------------------------------------------ const GAP = 8; +// Always floating, never tiled +const FLOATING_CLASSES = [ + // "org.kde.dolphin", + "org.kde.kcalc", + "systemsettings", +]; + +// Completely ignored by SimpleDwindle +const IGNORED_CLASSES = [ + "wl-copy", +]; + +// ------------------------------------------------------------ +// State +// ------------------------------------------------------------ + const layouts = []; -const floating = []; const managed = []; +const floating = []; +const suspended = []; +const watched = []; + +// ------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------ function log(msg) { - print("[SimpleDwindle] " + msg); + console.log("[SimpleDwindle] " + msg); +} + +function contains(list, w) { + return list.indexOf(w) !== -1; +} + +function removeFromList(list, w) { + const index = list.indexOf(w); + + if (index !== -1) + list.splice(index, 1); +} + +function windowClass(w) { + return String(w.resourceClass || "").toLowerCase(); +} + +function matchesClass(w, classes) { + return classes.some(c => + c.toLowerCase() === windowClass(w) + ); } -function eligible(w) { - return w && w.normalWindow && - w.moveable && w.resizeable && - !w.fullScreen && !w.minimized && +function isCandidate(w) { + return w && + w.normalWindow && + w.moveable && + w.resizeable && !w.transient; } -function contains(list, w) { - return list.indexOf(w) !== -1; +function isIgnored(w) { + return matchesClass(w, IGNORED_CLASSES); +} + +function isForcedFloating(w) { + return matchesClass(w, FLOATING_CLASSES); } +// ------------------------------------------------------------ +// Layout management +// ------------------------------------------------------------ + function layoutFor(w) { const desktop = w.desktops.length ? w.desktops[0] @@ -42,12 +98,23 @@ function layoutFor(w) { root: null, last: null }; + layouts.push(layout); } return layout; } +function findWindowLayout(w) { + return layouts.find(l => + findLeaf(l.root, w) !== null + ); +} + +// ------------------------------------------------------------ +// Binary tree +// ------------------------------------------------------------ + function leaf(w, parent = null) { return { window: w, @@ -127,6 +194,7 @@ function removeNode(layout, w) { } const parent = node.parent; + const sibling = parent.first === node ? parent.second : parent.first; @@ -145,12 +213,18 @@ function removeNode(layout, w) { if (layout.last === w) { const lastNode = rightmost(layout.root); - layout.last = lastNode ? lastNode.window : null; + layout.last = lastNode + ? lastNode.window + : null; } return true; } +// ------------------------------------------------------------ +// Geometry +// ------------------------------------------------------------ + function applyNode(node, rect) { if (!node) return; @@ -158,11 +232,18 @@ function applyNode(node, rect) { if (node.window) { const w = node.window; + // Fullscreen windows retain their tree position, + // but KWin controls their geometry. + if (w.fullScreen || w.minimized) + return; + const x = Math.round(rect.x + GAP / 2); const y = Math.round(rect.y + GAP / 2); + const width = Math.max( 1, Math.round(rect.width - GAP) ); + const height = Math.max( 1, Math.round(rect.height - GAP) ); @@ -216,7 +297,7 @@ function applyNode(node, rect) { } function arrange(layout) { - if (!layout.root) + if (!layout || !layout.root) return; const area = workspace.clientArea( @@ -228,46 +309,62 @@ function arrange(layout) { applyNode(layout.root, area); } +// ------------------------------------------------------------ +// Window states +// ------------------------------------------------------------ + function tileWindow(w) { - if (!eligible(w) || contains(managed, w)) + log("TILED: " + w.caption + " [" + w.resourceClass + "]"); + + if (!isCandidate(w) || + isIgnored(w) || + contains(managed, w) || + w.minimized) return; - const fi = floating.indexOf(w); - if (fi !== -1) - floating.splice(fi, 1); + removeFromList(floating, w); + removeFromList(suspended, w); managed.push(w); const layout = layoutFor(w); insertWindow(layout, w); + + if (!w.fullScreen) { + w.keepAbove = false; + w.keepBelow = true; + } + arrange(layout); log("TILED: " + w.caption); } function detachWindow(w) { - const index = managed.indexOf(w); - - if (index === -1) + if (!contains(managed, w)) return false; - managed.splice(index, 1); + removeFromList(managed, w); - for (const layout of layouts) { - if (removeNode(layout, w)) { - arrange(layout); - break; - } + const layout = findWindowLayout(w); + + if (layout) { + removeNode(layout, w); + arrange(layout); } return true; } function floatWindow(w) { - if (!detachWindow(w)) + if (isIgnored(w)) return; + detachWindow(w); + + removeFromList(suspended, w); + if (!contains(floating, w)) floating.push(w); @@ -279,34 +376,142 @@ function floatWindow(w) { function toggleFloating() { const w = workspace.activeWindow; - if (!w || !w.normalWindow) + if (!isCandidate(w) || isIgnored(w)) return; if (contains(managed, w)) { floatWindow(w); - } else { + } else if (!w.minimized) { tileWindow(w); } } -workspace.windowAdded.connect(function(w) { - if (eligible(w)) +// ------------------------------------------------------------ +// Fullscreen handling +// ------------------------------------------------------------ + +function handleFullscreen(w) { + if (!contains(managed, w)) + return; + + if (w.fullScreen) { + // Temporarily release KWin's BelowLayer. + // The tree node stays reserved. + w.keepBelow = false; + + log("FULLSCREEN: " + w.caption); + } else { + w.keepAbove = false; + w.keepBelow = true; + + const layout = findWindowLayout(w); + + if (layout) + arrange(layout); + + log("FULLSCREEN EXIT: " + w.caption); + } +} + +// ------------------------------------------------------------ +// Minimize handling +// ------------------------------------------------------------ + +function handleMinimized(w) { + if (w.minimized) { + if (!contains(managed, w)) + return; + + detachWindow(w); + + w.keepBelow = false; + + if (!contains(suspended, w)) + suspended.push(w); + + log("MINIMIZED: " + w.caption); + } else { + if (!contains(suspended, w)) + return; + + removeFromList(suspended, w); + tileWindow(w); -}); -workspace.windowRemoved.connect(function(w) { + log("RESTORED: " + w.caption); + } +} + +// ------------------------------------------------------------ +// Window lifecycle +// ------------------------------------------------------------ + +function watchWindow(w) { + if (!w || contains(watched, w)) + return; + + watched.push(w); + + w.fullScreenChanged.connect(function() { + handleFullscreen(w); + }); + + w.minimizedChanged.connect(function() { + handleMinimized(w); + }); +} + +function handleWindowAdded(w) { + log( + "WINDOW ADDED: " + + "caption=" + w.caption + + " class=" + w.resourceClass + + " normal=" + w.normalWindow + + " transient=" + Boolean(w.transient) + + " id=" + w.internalId + ); + if (!isCandidate(w) || isIgnored(w)) + return; + + watchWindow(w); + + if (isForcedFloating(w)) { + floatWindow(w); + return; + } + + if (w.minimized) { + suspended.push(w); + return; + } + + tileWindow(w); +} + +function handleWindowRemoved(w) { detachWindow(w); - const index = floating.indexOf(w); - if (index !== -1) - floating.splice(index, 1); -}); + removeFromList(floating, w); + removeFromList(suspended, w); + removeFromList(watched, w); +} + +// ------------------------------------------------------------ +// Events +// ------------------------------------------------------------ + +workspace.windowAdded.connect(handleWindowAdded); +workspace.windowRemoved.connect(handleWindowRemoved); workspace.screensChanged.connect(function() { for (const layout of layouts) arrange(layout); }); +// ------------------------------------------------------------ +// Shortcut +// ------------------------------------------------------------ + registerShortcut( "SimpleDwindleToggleFloating", "SimpleDwindle: Toggle Floating", @@ -314,5 +519,27 @@ registerShortcut( toggleFloating ); -log("Version 0.2 initialized"); +registerShortcut( + "SimpleDwindleListWindows", + "SimpleDwindle: List Windows", + "Meta+Shift+I", + function () { + log("========== WINDOW LIST =========="); + + for (const w of workspace.windowList()) { + log( + "class=" + w.resourceClass + + " | caption=" + w.caption + + " | normal=" + w.normalWindow + + " | transient=" + Boolean(w.transient) + + " | minimized=" + w.minimized + + " | id=" + w.internalId + ); + } + + log("================================="); + } +); + +log("Version " + VERSION + " initialized"); diff --git a/metadata.json b/metadata.json index 6a2acd3..f26011c 100644 --- a/metadata.json +++ b/metadata.json @@ -3,7 +3,7 @@ "Id": "simpledwindle", "Name": "SimpleDwindle", "Description": "Minimal tiling and floating window manager", - "Version": "0.2", + "Version": "0.3.1", "License": "MIT" }, "X-Plasma-API": "javascript", -- 2.55.0