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));