KWin Scripting

Overview & Architecture

KWin scripting runs JavaScript inside a QJSEngine instance embedded in the compositor process. Scripts execute in the same thread as KWin itself — there is no sandbox, no async I/O, and no separate process. A poorly-written script can stall frame delivery.

Scripts interact with windows and the compositor through two primary channels: the workspace singleton (window enumeration, signals, virtual desktop control) and individual Window objects (properties, geometry, stacking).

KDE Plasma 6 (KWin 6) migrated from KWin 5's custom QScriptEngine to a stricter QJSEngine. Several APIs were removed or renamed. Effect scripts now use a different API surface than window management scripts and are not covered here.

Installation & Loading

Script package layout

~/.local/share/kwin/scripts/myscript/
├── metadata.json          # required
└── contents/
    └── code/
        └── main.js        # entry point

metadata.json

{
  "KPlugin": {
    "Name": "My Script",
    "Description": "Does something useful",
    "Version": "1.0.0",
    "License": "GPL-2.0+",
    "Website": "",
    "Authors": [{ "Name": "You", "Email": "" }],
    "Id": "myscript",            // must match directory name
    "ServiceTypes": ["KWin/Script"]
  }
}

Enabling a script

# Enable via KWin's D-Bus interface (no restart required)
qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript \
    "~/.local/share/kwin/scripts/myscript/contents/code/main.js" \
    "myscript"

# Or through System Settings → Window Management → KWin Scripts
# Reload all loaded scripts:
qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.start
Scripts enabled via D-Bus survive only until the KWin process restarts. For persistence, enable through System Settings or add to the autostart config.

Global Objects

The scripting environment injects several globals into every script's scope at load time. You do not need to import or require them.

GlobalTypePurpose
workspaceobjectCentral API: window lists, signals, virtual desktops, screens, input
optionsobjectRead-only access to KWin configuration values (focus policy, snap zones, etc.)
print()functionWrites to the KWin debug output (journalctl or ~/.local/share/xsession-errors)
readConfig(key, default)functionReads a value from the script's own KConfig group
registerShortcut()functionRegisters a global keyboard shortcut
callDBus()functionMakes a blocking D-Bus call
KWin 6 removed the KWin prefix from many global functions. KWin.readConfig() is now just readConfig(). Same for registerShortcut, registerScreenEdge, etc.

Window Objects

Each open window is represented by a Window object (formerly Client in KWin 5). These objects are live references — property changes on the real window are reflected immediately.

Key properties

PropertyTypeNotes
captionstringWindow title. Read-only.
resourceClassstringWM_CLASS instance (lowercase). Reliable identifier for application matching.
resourceNamestringWM_CLASS name. Use resourceClass for matching in most cases.
geometryQRectRead/write. Sets position and size simultaneously.
frameGeometryQRectGeometry including window decorations.
clientGeometryQRectGeometry of client area only (no frame).
x, y, width, heightintConvenience accessors into geometry. Writable.
desktopintVirtual desktop index (1-based). Deprecated in KWin 6 — use desktops array.
desktopsVirtualDesktop[]Array of virtual desktops the window is present on.
onAllDesktopsboolRead/write. Sticky window flag.
screenintScreen index the window center is on. Read-only.
outputOutputScreen/output object. Preferred over screen index in KWin 6.
minimizedboolRead/write.
maximizeModeint0=none, 1=vertical, 2=horizontal, 3=full. Write via setMaximize().
fullScreenboolRead/write. Triggers fullscreen state.
keepAboveboolRead/write. Always-on-top.
keepBelowboolRead/write.
skipTaskbarboolRead/write.
skipPagerboolRead/write.
skipSwitcherboolRead/write.
tileTile|nullAssigned tiling tile. Null if untiled.
normalWindowboolTrue if the window is a normal application window (not panel, dock, dialog, etc.).
transientboolTrue if this is a transient/dialog window.
transientForWindow|nullParent window for transients.
pidintProcess ID. May be 0 for some XWayland windows.
windowIdintXID under X11. Not meaningful on Wayland.
internalIdstring (UUID)Stable identifier valid across both X11 and Wayland.
layerintStacking layer (0=Desktop … 7=OverlayLayer).
activeboolRead-only. Writable via workspace.activeWindow.
colorSchemestringPath to KColorScheme .colors file. Writable.
opacityfloat0.0–1.0. Composited opacity.

Window methods

// Close (sends WM_DELETE_WINDOW):
win.closeWindow();

// Move to virtual desktop by object:
win.desktops = [workspace.desktops[0]];

// Move to a specific output:
win.output = workspace.screens[1];

// Set maximize state (h, v flags):
workspace.setMaximize(win, true, true);

// Resize and reposition:
win.geometry = Qt.rect(0, 0, 1280, 800);

Signals

KWin exposes Qt signals on both the workspace object and individual Window objects. Connect with standard JavaScript function assignment — there is no connect() wrapper in QJSEngine; you assign directly.

// Workspace-level signal: fires when any new window is mapped
workspace.windowAdded.connect(function(win) {
    print("added: " + win.caption + " class=" + win.resourceClass);
});

// Window-level signal: fires when geometry changes
workspace.windowAdded.connect(function(win) {
    win.frameGeometryChanged.connect(function() {
        print(win.caption + " moved to " + win.x + "," + win.y);
    });
});

Workspace signals

SignalArgsFires when
windowAddedWindowA window is mapped
windowRemovedWindowA window is unmapped/destroyed
windowActivatedWindow|nullFocus changes; null when no window is active
currentDesktopChangedVirtualDesktop, Window|nullActive virtual desktop switches
desktopsChangedVirtual desktop count or layout changes
screensChangedScreen layout changes (connect/disconnect/resize)
currentActivityChangedstringKActivities: active activity changes

Window signals

SignalArgsFires when
captionChangedWindow title changes
frameGeometryChangedPosition or size changes (includes moves)
clientGeometryChangedClient area geometry changes
desktopsChangedWindow moved to different virtual desktop
outputChangedWindow moved to different screen
minimizedChangedMinimized state toggles
maximizedAboutToChangeMaximizeModeBefore maximize state transitions
fullScreenChangedFullscreen state toggles
activitiesChangedstring[]Window's activity list changes
colorSchemeChangedColor scheme is applied/changed
applicationMenuChangedApplication menu service or path changes
windowShownWindow becomes visible after being hidden
windowHiddenWindow becomes hidden (not minimized)
Signals connected to a Window object hold a reference to that object. Always disconnect or avoid closures that capture the window when handling windowRemoved, or you will leak the object.

Workspace API

Window enumeration

// All mapped windows (normal + special):
var all = workspace.windowList();

// Filter to normal application windows:
var normal = workspace.windowList().filter(function(w) {
    return w.normalWindow;
});

// Currently active window:
var active = workspace.activeWindow;

// Set focus:
workspace.activeWindow = someWindow;

Workspace properties

PropertyTypeNotes
activeWindowWindow|nullRead/write. Setting activates the window.
currentDesktopVirtualDesktopRead/write active virtual desktop.
desktopsVirtualDesktop[]All virtual desktops in order.
screensOutput[]All connected screens.
activeScreenOutputScreen containing the cursor. Read-only.
numScreensintNumber of connected screens. Read-only.
cursorPosQPointCurrent pointer position in global coordinates. Read-only.
workspaceWidthintTotal width across all screens.
workspaceHeightintTotal height across all screens.

Workspace methods

MethodReturnsNotes
windowList()Window[]All windows in current stacking order
getClient(windowId)Window|nullLook up by X11 window ID. Not useful on pure Wayland.
setMaximize(win, h, v)voidPreferred maximize API since KWin 6
raiseWindow(win)voidRaises to top of its stacking layer
lowerWindow(win)voidLowers to bottom of its stacking layer
getScreen(pt)intReturns screen index containing QPoint
screenAt(pt)OutputReturns Output object at QPoint. Preferred in KWin 6.
clientArea(AreaOption, win)QRectReturns available geometry minus panels for the given area type
clientArea(AreaOption, output, desktop)QRectArea query without a specific window
sendClientToScreen(win, screen)voidMoves window to specified screen index. Prefer win.output = ....
windowAt(pt)Window|nullReturns topmost window at point

clientArea AreaOption values

KWin.PlacementArea      // usable area (minus panels)
KWin.MaximizeArea       // area a maximized window occupies
KWin.MaximizeFullArea   // full screen area ignoring struts
KWin.FullScreenArea     // area for fullscreen windows
KWin.MovementArea       // area window centers may move within
KWin.ScreenArea         // full screen geometry
KWin.WorkArea           // union of all placement areas
KWin.FullArea           // union of all screen areas

Virtual Desktops & Screens

VirtualDesktop object

var desktops = workspace.desktops;

desktops.forEach(function(vd) {
    print(vd.id, vd.name, vd.x11DesktopNumber);
});

// Switch to first desktop:
workspace.currentDesktop = desktops[0];

// Assign window to desktop:
win.desktops = [desktops[2]];

// Pin window to all desktops:
win.onAllDesktops = true;

Output (screen) object

var screens = workspace.screens;

screens.forEach(function(out) {
    print(out.name,          // e.g. "DP-1", "HDMI-A-1"
          out.geometry,      // QRect: position and size in global coords
          out.devicePixelRatio // HiDPI scale factor
    );
});

// Move window to second screen:
if (screens.length > 1) {
    win.output = screens[1];
}

Geometry & Placement

The Qt namespace provides geometry constructors available in all scripts. Coordinates are in logical pixels (accounting for HiDPI scale).

// Center a window on its current screen:
function centerOnScreen(win) {
    var area = workspace.clientArea(KWin.PlacementArea, win);
    var nx = area.x + (area.width  - win.width)  / 2;
    var ny = area.y + (area.height - win.height) / 2;
    win.geometry = Qt.rect(nx, ny, win.width, win.height);
}

// Tile two windows side-by-side on the active screen:
function tileSideBySide(left, right) {
    var area = workspace.clientArea(KWin.MaximizeArea,
                                     workspace.activeScreen,
                                     workspace.currentDesktop);
    var half = Math.floor(area.width / 2);
    left.geometry  = Qt.rect(area.x,        area.y, half, area.height);
    right.geometry = Qt.rect(area.x + half, area.y, area.width - half, area.height);
}
Always query clientArea rather than using raw screen geometry. Panel struts are only accounted for in PlacementArea and MaximizeArea — raw screen geometry ignores them.

Geometry constraints

KWin enforces minimum window size constraints from the client's size hints. Setting win.geometry to a rectangle smaller than win.minSize will silently clamp to the minimum. Read win.minSize (QSize) and win.maxSize (QSize) before attempting constrained placement.

var minW = win.minSize.width;
var minH = win.minSize.height;

Effects Integration

Window management scripts (under kwin/scripts/) and effect scripts (under kwin/effects/) use different API surfaces and run in different contexts. This article covers window management scripts only. Effect scripts have access to effect, effects, and the animating/painting APIs not available here.

From a window management script you can trigger built-in effects via D-Bus or by toggling window properties that effects react to:

// Trigger the present windows effect for current desktop:
callDBus("org.kde.KWin",
          "/Effects",
          "org.kde.kwin.Effects",
          "toggleEffect",
          "presentwindows");

// Activate "slide" desktop switch programmatically:
callDBus("org.kde.KWin",
          "/KWin",
          "org.kde.KWin",
          "nextDesktop");

Options & Config

Reading KWin options

// options is a read-only global reflecting kwinrc
print(options.focusPolicy);         // 0=ClickToFocus, 1=FocusFollowsMouse, etc.
print(options.xwaylandCrashPolicy);
print(options.borderSnapZone);      // snap distance in px

Reading script-specific config

Scripts can have their own configuration entries in kwinrc under a group named after the script's ID. Use readConfig() to access them:

// In kwinrc: [Script-myscript] / myKey=someValue
var val = readConfig("myKey", "defaultValue");

Scripts cannot write to their own config at runtime via the scripting API. For persistent user settings, write to kwinrc externally (e.g. via a KCM or CLI tool) and reload the script.

D-Bus & Shortcuts

D-Bus calls

// Blocking call — do not use in signal handlers that fire frequently
callDBus(service, path, iface, method[, arg1, arg2, ...][, callback]);

// With return value callback:
callDBus("org.kde.KWin", "/KWin", "org.kde.KWin", "supportInformation",
    function(result) { print(result); }
);

Keyboard shortcuts

registerShortcut(
    "Tile Left",           // display name (unique per KWin)
    "Tile Active Left",     // i18n name
    "Meta+Left",           // default key sequence (QKeySequence syntax)
    function() {
        var win = workspace.activeWindow;
        if (!win || !win.normalWindow) return;
        var area = workspace.clientArea(KWin.MaximizeArea, win);
        win.geometry = Qt.rect(area.x, area.y,
                                area.width / 2, area.height);
    }
);

Screen edge triggers

// Edge constants: 0=Top, 1=TopRight, 2=Right, 3=BottomRight,
//                 4=Bottom, 5=BottomLeft, 6=Left, 7=TopLeft

registerScreenEdge(6, function() {     // left edge
    callDBus("org.kde.plasmashell",
              "/PlasmaShell",
              "org.kde.PlasmaShell",
              "toggleDashboard");
});

// Unregister with:
unregisterScreenEdge(6);

Debugging

Print output

print("message");          // writes to KWin's logger
console.log("message");   // also works; same destination

Read output with:

journalctl -f SYSLOG_IDENTIFIER=kwin_wayland
# or for X11:
journalctl -f SYSLOG_IDENTIFIER=kwin_x11

Reloading a script without restarting

# Unload by script ID, then reload:
qdbus org.kde.KWin /Scripting stopScript 1    # script id from loadScript return
qdbus org.kde.KWin /Scripting loadScript \
    "/path/to/contents/code/main.js" "myscript"

Script IDs

# List all loaded scripts and their IDs:
qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.loadedScripts

Common errors

ErrorCause
TypeError: win.desktop is not assignableUsing the old integer desktop API; switch to win.desktops = [vd]
ReferenceError: KWin is not definedOld KWin 5 API; use bare readConfig(), registerShortcut(), etc.
Signal connects but never firesScript loaded before the window manager is fully initialised; wrap initial setup in workspace.windowAdded.connect() or defer with a timer
Geometry set but window doesn't moveWindow may be maximized or fullscreen; clear those states first
callDBus hangs KWin brieflyD-Bus call is blocking; avoid in frequently-firing signals

Porting: KWin 5 → 6

KWin 6 (Plasma 6) introduced breaking changes. The main areas to update:

KWin 5KWin 6 replacement
KWin.readConfig()readConfig()
KWin.registerShortcut()registerShortcut()
KWin.registerScreenEdge()registerScreenEdge()
KWin.callDBus()callDBus()
Client (type name)Window
win.desktop (int, 1-based)win.desktops (VirtualDesktop[])
workspace.currentDesktop (int)workspace.currentDesktop (VirtualDesktop object)
workspace.numDesktopsworkspace.desktops.length
workspace.activeClientworkspace.activeWindow
workspace.clientList()workspace.windowList()
workspace.clientArea(opt, screen_int, desktop_int)workspace.clientArea(opt, Output, VirtualDesktop)
win.screen (int index)win.output (Output object); win.screen still works but deprecated
workspace.sendClientToScreen(win, idx)win.output = workspace.screens[idx]
windowMaximizedStateChanged signalmaximizedAboutToChange on the Window object
QScriptEngine quirks (arguments.callee, etc.)QJSEngine strict semantics; use named functions
metadata.desktop (KDE4 format)metadata.json (KPlugin format)
KWin 6 scripts that used workspace.currentDesktop as an integer index will fail silently or produce unexpected results. The property now returns a VirtualDesktop object. To get an index, use workspace.desktops.indexOf(workspace.currentDesktop).

Minimal working KWin 6 script

// main.js — KWin 6 compatible
// Moves every newly opened normal window to the primary screen.

workspace.windowAdded.connect(function(win) {
    if (!win.normalWindow) return;

    var primary = workspace.screens[0];
    if (win.output === primary) return;

    win.output = primary;

    var area = workspace.clientArea(KWin.PlacementArea,
                                     primary,
                                     workspace.currentDesktop);
    win.geometry = Qt.rect(
        area.x + (area.width  - win.width)  / 2,
        area.y + (area.height - win.height) / 2,
        win.width,
        win.height
    );
});