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.
~/.local/share/kwin/scripts/myscript/
├── metadata.json # required
└── contents/
└── code/
└── main.js # entry point
{
"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"]
}
}
# 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
The scripting environment injects several globals into every script's scope at load time. You do not need to import or require them.
| Global | Type | Purpose |
|---|---|---|
| workspace | object | Central API: window lists, signals, virtual desktops, screens, input |
| options | object | Read-only access to KWin configuration values (focus policy, snap zones, etc.) |
| print() | function | Writes to the KWin debug output (journalctl or ~/.local/share/xsession-errors) |
| readConfig(key, default) | function | Reads a value from the script's own KConfig group |
| registerShortcut() | function | Registers a global keyboard shortcut |
| callDBus() | function | Makes a blocking D-Bus call |
KWin prefix from many global functions. KWin.readConfig() is now just readConfig(). Same for registerShortcut, registerScreenEdge, etc.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.
| Property | Type | Notes |
|---|---|---|
| caption | string | Window title. Read-only. |
| resourceClass | string | WM_CLASS instance (lowercase). Reliable identifier for application matching. |
| resourceName | string | WM_CLASS name. Use resourceClass for matching in most cases. |
| geometry | QRect | Read/write. Sets position and size simultaneously. |
| frameGeometry | QRect | Geometry including window decorations. |
| clientGeometry | QRect | Geometry of client area only (no frame). |
| x, y, width, height | int | Convenience accessors into geometry. Writable. |
| desktop | int | Virtual desktop index (1-based). Deprecated in KWin 6 — use desktops array. |
| desktops | VirtualDesktop[] | Array of virtual desktops the window is present on. |
| onAllDesktops | bool | Read/write. Sticky window flag. |
| screen | int | Screen index the window center is on. Read-only. |
| output | Output | Screen/output object. Preferred over screen index in KWin 6. |
| minimized | bool | Read/write. |
| maximizeMode | int | 0=none, 1=vertical, 2=horizontal, 3=full. Write via setMaximize(). |
| fullScreen | bool | Read/write. Triggers fullscreen state. |
| keepAbove | bool | Read/write. Always-on-top. |
| keepBelow | bool | Read/write. |
| skipTaskbar | bool | Read/write. |
| skipPager | bool | Read/write. |
| skipSwitcher | bool | Read/write. |
| tile | Tile|null | Assigned tiling tile. Null if untiled. |
| normalWindow | bool | True if the window is a normal application window (not panel, dock, dialog, etc.). |
| transient | bool | True if this is a transient/dialog window. |
| transientFor | Window|null | Parent window for transients. |
| pid | int | Process ID. May be 0 for some XWayland windows. |
| windowId | int | XID under X11. Not meaningful on Wayland. |
| internalId | string (UUID) | Stable identifier valid across both X11 and Wayland. |
| layer | int | Stacking layer (0=Desktop … 7=OverlayLayer). |
| active | bool | Read-only. Writable via workspace.activeWindow. |
| colorScheme | string | Path to KColorScheme .colors file. Writable. |
| opacity | float | 0.0–1.0. Composited opacity. |
// 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);
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);
});
});
| Signal | Args | Fires when |
|---|---|---|
| windowAdded | Window | A window is mapped |
| windowRemoved | Window | A window is unmapped/destroyed |
| windowActivated | Window|null | Focus changes; null when no window is active |
| currentDesktopChanged | VirtualDesktop, Window|null | Active virtual desktop switches |
| desktopsChanged | — | Virtual desktop count or layout changes |
| screensChanged | — | Screen layout changes (connect/disconnect/resize) |
| currentActivityChanged | string | KActivities: active activity changes |
| Signal | Args | Fires when |
|---|---|---|
| captionChanged | — | Window title changes |
| frameGeometryChanged | — | Position or size changes (includes moves) |
| clientGeometryChanged | — | Client area geometry changes |
| desktopsChanged | — | Window moved to different virtual desktop |
| outputChanged | — | Window moved to different screen |
| minimizedChanged | — | Minimized state toggles |
| maximizedAboutToChange | MaximizeMode | Before maximize state transitions |
| fullScreenChanged | — | Fullscreen state toggles |
| activitiesChanged | string[] | Window's activity list changes |
| colorSchemeChanged | — | Color scheme is applied/changed |
| applicationMenuChanged | — | Application menu service or path changes |
| windowShown | — | Window becomes visible after being hidden |
| windowHidden | — | Window becomes hidden (not minimized) |
windowRemoved, or you will leak the object.// 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;
| Property | Type | Notes |
|---|---|---|
| activeWindow | Window|null | Read/write. Setting activates the window. |
| currentDesktop | VirtualDesktop | Read/write active virtual desktop. |
| desktops | VirtualDesktop[] | All virtual desktops in order. |
| screens | Output[] | All connected screens. |
| activeScreen | Output | Screen containing the cursor. Read-only. |
| numScreens | int | Number of connected screens. Read-only. |
| cursorPos | QPoint | Current pointer position in global coordinates. Read-only. |
| workspaceWidth | int | Total width across all screens. |
| workspaceHeight | int | Total height across all screens. |
| Method | Returns | Notes |
|---|---|---|
| windowList() | Window[] | All windows in current stacking order |
| getClient(windowId) | Window|null | Look up by X11 window ID. Not useful on pure Wayland. |
| setMaximize(win, h, v) | void | Preferred maximize API since KWin 6 |
| raiseWindow(win) | void | Raises to top of its stacking layer |
| lowerWindow(win) | void | Lowers to bottom of its stacking layer |
| getScreen(pt) | int | Returns screen index containing QPoint |
| screenAt(pt) | Output | Returns Output object at QPoint. Preferred in KWin 6. |
| clientArea(AreaOption, win) | QRect | Returns available geometry minus panels for the given area type |
| clientArea(AreaOption, output, desktop) | QRect | Area query without a specific window |
| sendClientToScreen(win, screen) | void | Moves window to specified screen index. Prefer win.output = .... |
| windowAt(pt) | Window|null | Returns topmost window at point |
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
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;
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];
}
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);
}
clientArea rather than using raw screen geometry. Panel struts are only accounted for in PlacementArea and MaximizeArea — raw screen geometry ignores them.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;
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 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
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.
// 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); }
);
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);
}
);
// 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);
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
# 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"
# List all loaded scripts and their IDs:
qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.loadedScripts
| Error | Cause |
|---|---|
| TypeError: win.desktop is not assignable | Using the old integer desktop API; switch to win.desktops = [vd] |
| ReferenceError: KWin is not defined | Old KWin 5 API; use bare readConfig(), registerShortcut(), etc. |
| Signal connects but never fires | Script 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 move | Window may be maximized or fullscreen; clear those states first |
| callDBus hangs KWin briefly | D-Bus call is blocking; avoid in frequently-firing signals |
KWin 6 (Plasma 6) introduced breaking changes. The main areas to update:
| KWin 5 | KWin 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.numDesktops | workspace.desktops.length |
| workspace.activeClient | workspace.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 signal | maximizedAboutToChange on the Window object |
| QScriptEngine quirks (arguments.callee, etc.) | QJSEngine strict semantics; use named functions |
| metadata.desktop (KDE4 format) | metadata.json (KPlugin format) |
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).// 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
);
});