]> Freerunner's - simpledwindle.git/commitdiff
Bugfixes:
authorAndre Ramnitz <andre.ramnitz@mail.de>
Mon, 19 Oct 2026 19:01:03 +0000 (21:01 +0200)
committerAndre Ramnitz <andre.ramnitz@mail.de>
Mon, 19 Oct 2026 19:01:03 +0000 (21:01 +0200)
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
metadata.json

index a0b71c349958ff790e3d75da59a99d6d2140348e..5016ed79764f97a1a4115f6c9de2ba400df0c9cf 100644 (file)
@@ -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");
 
index 6a2acd3f43fd0a78a7a496e85b678399067e2e6d..f26011c313423c04f2429b0c4c28107134e487bf 100644 (file)
@@ -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",