import { Edge, LayoutDirection, Tile, Window, Workspace } from "kwin-api"; declare const workspace: Workspace; declare const registerShortcut: any; declare const print: any; enum Axis { Horiz = 1, Vert = 2, } let lastFocused = new Map<Tile, number>(); let selectedTile: Tile | null = null; let resizeSense = 20; function dir(axis: Axis): LayoutDirection { switch (axis) { case Axis.Horiz: return LayoutDirection.Horizontal; case Axis.Vert: return LayoutDirection.Vertical; } } function mod(a: number, b: number): number { while (a < 0) { a += b; } a %= b; return a; } function get_tile_index(tile: Tile): number { let p = tile.parent!; return p.tiles.findIndex((t: Tile) => t == tile); } /// Gets the closest ancestor of `tile` which is inside an `axis` layout. function get_least_in_axis(axis: Axis, tile: Tile): Tile | null { while (true) { if (tile.parent == null) return null; if (tile.parent?.layoutDirection == dir(axis)) return tile; tile = tile.parent; } } function move(dir: Edge) { const axis = dir == Edge.LeftEdge || dir == Edge.RightEdge ? Axis.Horiz : Axis.Vert; const sign = dir == Edge.RightEdge || dir == Edge.BottomEdge ? 1 : -1; let t = workspace.activeWindow?.tile; if (t == null) return; t = get_least_in_axis(axis, t); if (t == null) return; // t.parent is not null because otherwise get_least_in_axis would have returned null const i = mod(get_tile_index(t) + sign, t.parent!.tiles.length); t = t.parent!.tiles[i]!; while (t.tiles.length != 0) { t = t.tiles[lastFocused.get(t) ?? 0]; } workspace.activeWindow = t.windows[0]; } function resize(axis: Axis, sign: number) { if (workspace.activeWindow?.tile == null) return; let child = get_least_in_axis(axis, workspace.activeWindow!.tile); if (child == null) return; let n = get_tile_index(child); let l = child.parent!.tiles.length; if (n == 0) { child.resizeByPixels(resizeSense * sign, axis == Axis.Horiz ? Edge.RightEdge : Edge.BottomEdge); } else if (n == l-1) { child.resizeByPixels(-resizeSense * sign, axis == Axis.Horiz ? Edge.LeftEdge : Edge.TopEdge); } else { child.resizeByPixels(resizeSense * sign * 0.5, axis == Axis.Horiz ? Edge.RightEdge : Edge.BottomEdge); child.resizeByPixels(-resizeSense * sign * 0.5, axis == Axis.Horiz ? Edge.LeftEdge : Edge.TopEdge); } } function relayout(dir: LayoutDirection) { if (selectedTile == null) return; selectedTile.layoutDirection = dir; } workspace.windowActivated.connect((w: Window) => { if (w?.tile == null) return; const t = w.tile; selectedTile = t; const p = t.parent; if (p == null) return; lastFocused.set(p, get_tile_index(t)); }); workspace.windowAdded.connect((w: Window) => { const root = workspace.tilingForScreen(workspace.activeScreen).rootTile; if (["krunner", "yakuake", "kded", "polkit", "plasmashell", "ksmserver-logout-greeter", "kwin_wayland", "spectacle"] .includes(w.resourceName)) return; if (!w.normalWindow || w.transient || w.resourceClass == "") return; if (root.tiles.length == 0 && root.windows.length == 0) { w.tile = root; root.layoutDirection = LayoutDirection.Horizontal; selectedTile = root; return; } let t = selectedTile; if (t == null) return; const dir = t.layoutDirection; t.split(dir); let p = t.parent; let i: number; if (p?.layoutDirection == dir) { // we actually split p, not t i = mod(get_tile_index(t) + 1, p.tiles.length); } else if (p == null) { // t is the root p = t; i = 1; } else { // we split t for real p = t; i = 1; } t = p.tiles[i]; w.tile = t; selectedTile = t; }); workspace.windowRemoved.connect((w: Window) => { if (w.tile == null) return; const t = w.tile; let sibling_windows: Window[] = []; if (t.parent?.tiles?.length == 2) { // if this tile has only one sibling, we need to move that sibling's windows into the parent // when this tile gets destroyed // kwin kind of tries to do this but fails - it resizes the windows properly but the windows are // left untiled somehow const sibling_index = 1 - get_tile_index(t); const sibling_tile = t.parent!.tiles[sibling_index]; // the other window has just been activated but we are going to destroy its tile, // so we preemptively select the parent, which will soon contain the window selectedTile = t.parent!; if (sibling_tile.tiles.length == 0) { // it contains only windows and no tiles // hopefully kwin takes the wheel in the other case for (const w of sibling_tile.windows) { sibling_windows.push(w); } } } const p = t.parent; t.remove(); if (p != null) { for (const w of sibling_windows) { w.tile = p; } } }); registerShortcut( "kwi3:moveleft", "kwi3: Move one container to the left", "Meta+N", () => move(Edge.LeftEdge)); registerShortcut( "kwi3:movedown", "kwi3: Move one container down", "Meta+E", () => move(Edge.BottomEdge)); registerShortcut( "kwi3:moveup", "kwi3: Move one container up", "Meta+I", () => move(Edge.TopEdge)); registerShortcut( "kwi3:moveright", "kwi3: Move one container to the right", "Meta+O", () => move(Edge.RightEdge)); registerShortcut( "kwi3:shrinkhorizontal", "kwi3: Shrink the current window horizontally", "Meta+Ctrl+N", () => resize(Axis.Horiz, -1)); registerShortcut( "kwi3:growvertical", "kwi3: Grow the current window vertically", "Meta+Ctrl+E", () => resize(Axis.Vert, 1)); registerShortcut( "kwi3:shrinkvertical", "kwi3: Shrink the current window vertically", "Meta+Ctrl+I", () => resize(Axis.Vert, -1)); registerShortcut( "kwi3:growhorizontal", "kwi3: Grow the current window horizontally", "Meta+Ctrl+O", () => resize(Axis.Horiz, 1)); registerShortcut( "kwi3:splith", "kwi3: Horizontal split", "Meta+V", () => relayout(LayoutDirection.Horizontal)); registerShortcut( "kwi3:splitv", "kwi3: Vertical split", "Meta+B", () => relayout(LayoutDirection.Vertical));