From a1b3da192209d84c55325b132b4e87a876450652 Mon Sep 17 00:00:00 2001 From: Black Don Date: Wed, 5 Aug 2026 17:48:45 -0400 Subject: [PATCH] Add KineticWE Layouts plugin (#252) --- kineticwe-layouts/README.md | 68 +++++++++++ kineticwe-layouts/panel.luau | 79 +++++++++++++ kineticwe-layouts/plugin.toml | 38 +++++++ kineticwe-layouts/service.luau | 149 +++++++++++++++++++++++++ kineticwe-layouts/thumbnail.webp | Bin 0 -> 12658 bytes kineticwe-layouts/translations/en.json | 8 ++ kineticwe-layouts/widget.luau | 64 +++++++++++ 7 files changed, 406 insertions(+) create mode 100644 kineticwe-layouts/README.md create mode 100644 kineticwe-layouts/panel.luau create mode 100644 kineticwe-layouts/plugin.toml create mode 100644 kineticwe-layouts/service.luau create mode 100644 kineticwe-layouts/thumbnail.webp create mode 100644 kineticwe-layouts/translations/en.json create mode 100644 kineticwe-layouts/widget.luau diff --git a/kineticwe-layouts/README.md b/kineticwe-layouts/README.md new file mode 100644 index 0000000..afd3e75 --- /dev/null +++ b/kineticwe-layouts/README.md @@ -0,0 +1,68 @@ +# KineticWE Layouts + +A bar widget that shows the current [KineticWE](https://github.com/notethene/KineticWE) +tiling layout under KWin and a dropdown panel to switch between the layouts you have +enabled. Rather than guessing, the plugin reads KineticWE's `/Tiling` DBus interface, so +the indicator stays in sync with layout keybinds, per-desktop layouts, and kwinrc changes. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `theblackdon/kineticwe-layouts` | +| Entries | Service: `bridge`; bar widget: `indicator`; panel: `layouts` | + +## Requirements + +- **KDE Plasma (KWin)** with **KineticWE** installed and built the standard way, which + exposes the `/Tiling` DBus interface (`org.kde.KWin.Tiling`). Without it the widget + shows an error tooltip and the panel explains that KineticWE must be rebuilt. +- **`qdbus-qt6`** (falls back to `qdbus6` / `qdbus`) on `PATH` — used to query and set the + current tiling layout. +- **`dbus-monitor`** on `PATH` — used to subscribe to layout and desktop-switch signals. + +## Usage + +Add the widget from **Settings → Bar**, choose a section, and pick **KineticWE Layouts** +from the widget list. The bar shows the active layout's glyph and, with the `show_text` +setting on, its name. Left-click the widget to open the layouts panel, which lists every +layout enabled in kwinrc (`[Tiling] EnabledLayouts`); clicking one switches the active +output's current desktop straight away. + +Open the panel from anywhere with: + +```sh +noctalia msg panel-toggle theblackdon/kineticwe-layouts:layouts +``` + +KineticWE applies layouts per virtual desktop, so the indicator follows the active +output's current desktop as you switch desktops, cycle layouts with keybinds, or change +kwinrc settings. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `show_text` | `bool` | `true` | Show the layout name next to the glyph in the bar. When off, only the glyph is shown. | + +## Notes + +**Processes.** The plugin shells out to two KWin/KDE tools, nothing else: + +- `qdbus-qt6 org.kde.KWin /Tiling org.kde.KWin.Tiling.currentLayout` — on load and + whenever a signal fires, plus a 5-second self-heal poll. +- `qdbus-qt6 org.kde.KWin /Tiling org.kde.KWin.Tiling.enabledLayouts` — on the same + cadence; keeps the panel's list in sync with kwinrc. +- `qdbus-qt6 org.kde.KWin /Tiling org.kde.KWin.Tiling.setLayout ` — only when a + layout is picked in the panel. +- `dbus-monitor --session "type='signal',interface='org.kde.KWin.Tiling'" + "type='signal',interface='org.kde.KWin.VirtualDesktopManager',member='currentChanged'"` + — a single long-lived stream that triggers the re-queries above; updates feel instant + while the 5-second poll doubles as a self-heal if the stream dies. + +**No network access. No filesystem reads or writes.** All state lives in the plugin's +in-memory state channel (`noctalia.state`) and is re-queried from the compositor; nothing +is written to disk. + +**Compositor support.** KWin / KDE Plasma only — KineticWE's tiling DBus interface is +KWin-specific, so the plugin shows a placeholder under any other compositor. diff --git a/kineticwe-layouts/panel.luau b/kineticwe-layouts/panel.luau new file mode 100644 index 0000000..e36d6d8 --- /dev/null +++ b/kineticwe-layouts/panel.luau @@ -0,0 +1,79 @@ +--!nonstrict +-- Layout picker panel: lists the layouts enabled in kwinrc and switches +-- the active output's current desktop on click. + +local KIND_INFO = { + MasterStack = { display = "MasterStack", glyph = "layout-sidebar-right" }, + Stacked = { display = "Stacked", glyph = "layout-rows" }, + CenterTile = { display = "Center Tile", glyph = "layout-columns" }, + Monocle = { display = "Monocle", glyph = "rectangle" }, + AutoGrid = { display = "Auto Grid", glyph = "layout-grid" }, + Columns = { display = "Columns", glyph = "columns-3" }, +} + +local isOpen = false + +local function selectLayout(kind) + local state = noctalia.state.get("layout") + local qdbus = (state and state.qdbus) or "qdbus-qt6" + noctalia.runAsync(qdbus .. " org.kde.KWin /Tiling org.kde.KWin.Tiling.setLayout " .. kind) + panel.close() +end + +local function render() + local state = noctalia.state.get("layout") + + local children = { + ui.row({ align = "center", justify = "space_between" }, { + ui.label({ text = "Layouts", fontSize = 16, fontWeight = "bold", color = "primary", flexGrow = 1 }), + ui.button({ glyph = "close", variant = "ghost", onClick = "onCloseClicked" }), + }), + ui.separator({}), + } + + if state == nil or not state.ok then + table.insert(children, ui.label({ + text = "KineticWE /Tiling DBus interface not found. Rebuild KineticWE and log back in.", + maxLines = 3, + })) + elseif #state.enabled == 0 then + table.insert(children, ui.label({ text = "No layouts enabled in kwinrc ([Tiling] EnabledLayouts)." })) + else + for _, kind in ipairs(state.enabled) do + local info = KIND_INFO[kind] or { display = kind, glyph = "layout-grid" } + table.insert(children, ui.button({ + key = "layout-" .. kind, + text = info.display, + glyph = info.glyph, + variant = (kind == state.current) and "primary" or "default", + contentAlign = "start", + onClick = function() + selectLayout(kind) + end, + })) + end + end + + panel.render(ui.column({ gap = 6, padding = 10 }, children)) +end + +function onOpen(_context) + isOpen = true + render() +end + +function onClose() + isOpen = false +end + +function onCloseClicked() + panel.close() +end + +-- Keep the highlight in sync if the layout changes while the panel is open +-- (e.g. a keybind cycle). +noctalia.state.watch("layout", function() + if isOpen then + render() + end +end) diff --git a/kineticwe-layouts/plugin.toml b/kineticwe-layouts/plugin.toml new file mode 100644 index 0000000..e52f23d --- /dev/null +++ b/kineticwe-layouts/plugin.toml @@ -0,0 +1,38 @@ +id = "theblackdon/kineticwe-layouts" +name = "KineticWE Layouts" +version = "1.0.0" +plugin_api = 9 +author = "theblackdon" +license = "MIT" +icon = "layout-grid" +description = "Shows the current KineticWE tiling layout on the bar and switches layouts from a dropdown." +tags = ["bar", "indicator", "service", "system"] +dependencies = ["qdbus-qt6", "dbus-monitor"] + +# Headless bridge: watches the compositor's /Tiling DBus interface and +# publishes the layout state for the widget and panel entries. +[[service]] +id = "bridge" +entry = "service.luau" + +# Bar indicator: glyph + current layout name; click opens the layouts panel. +[[widget]] +id = "indicator" +entry = "widget.luau" + + [[widget.setting]] + key = "show_text" + type = "bool" + label_key = "settings.show_text.label" + description_key = "settings.show_text.description" + default = true + +# Dropdown listing the enabled layouts; attached so it opens from the bar +# next to the widget that was clicked. +[[panel]] +id = "layouts" +entry = "panel.luau" +width = 260 +height = 340 +placement = "attached" +open_near_click = true diff --git a/kineticwe-layouts/service.luau b/kineticwe-layouts/service.luau new file mode 100644 index 0000000..3c8dadc --- /dev/null +++ b/kineticwe-layouts/service.luau @@ -0,0 +1,149 @@ +--!nonstrict +-- KineticWE layout bridge service. +-- +-- Watches the compositor's /Tiling DBus interface (plus virtual-desktop +-- switches, which implicitly change the "current" layout) and publishes a +-- single plain-data table on noctalia.state key "layout" for the widget and +-- panel entries: +-- +-- { ok = true, current = "Columns", enabled = { "MasterStack", ... }, +-- qdbus = "qdbus-qt6" } +-- { ok = false, reason = "missing-qdbus" | "missing-monitor" | "missing-interface" } +-- +-- "current" is the kind string for the active output's current desktop, or +-- "" when native tiling is disabled. + +local KINDS = { + MasterStack = true, + Stacked = true, + CenterTile = true, + Monocle = true, + AutoGrid = true, + Columns = true, +} + +local QDBUS = nil +local refreshing = false +local refreshQueued = false +local lastPublished = nil + +local function findQdbus() + for _, name in ipairs({ "qdbus-qt6", "qdbus6", "qdbus" }) do + if noctalia.commandExists(name) then + return name + end + end + return nil +end + +local function publish(state) + noctalia.state.set("layout", state) + lastPublished = state +end + +-- qdbus prints a QStringList reply as { "MasterStack", "Stacked", ... }. +-- Some builds print bare words instead, so fall back to word splitting and +-- keep only tokens that are real layout kinds. +local function parseKindList(out) + local items, seen = {}, {} + local function add(s) + if KINDS[s] and not seen[s] then + seen[s] = true + table.insert(items, s) + end + end + for s in string.gmatch(out, '"([^"]+)"') do + add(s) + end + if #items == 0 then + for s in string.gmatch(out, "[%a]+") do + add(s) + end + end + return items +end + +local function finishRefresh() + refreshing = false + if refreshQueued then + refreshQueued = false + refresh() + end +end + +function refresh() + if refreshing then + -- Coalesce signal bursts (e.g. one per desktop on a reconcile pass) + -- into a single follow-up query. + refreshQueued = true + return + end + refreshing = true + + if QDBUS == nil then + QDBUS = findQdbus() + if QDBUS == nil then + publish({ ok = false, reason = "missing-qdbus" }) + finishRefresh() + return + end + end + + noctalia.runAsync(QDBUS .. " org.kde.KWin /Tiling org.kde.KWin.Tiling.currentLayout", function(res) + if res.exitCode ~= 0 then + publish({ ok = false, reason = "missing-interface" }) + finishRefresh() + return + end + local current = noctalia.string.trim(res.stdout) + noctalia.runAsync(QDBUS .. " org.kde.KWin /Tiling org.kde.KWin.Tiling.enabledLayouts", function(res2) + if res2.exitCode ~= 0 then + publish({ ok = false, reason = "missing-interface" }) + finishRefresh() + return + end + local enabled = parseKindList(res2.stdout) + local unchanged = lastPublished + and lastPublished.ok + and lastPublished.current == current + and table.concat(lastPublished.enabled, ",") == table.concat(enabled, ",") + if not unchanged then + publish({ ok = true, current = current, enabled = enabled, qdbus = QDBUS }) + end + finishRefresh() + end) + end) +end + +local function onMonitorLine(line) + -- Any of: member=layoutChanged, member=enabledLayoutsChanged, + -- member=currentChanged (desktop switch). Re-query rather than parse + -- arguments: it is cheap and always yields a consistent snapshot. + if string.find(line, "member=") then + refresh() + end +end + +local function startMonitor() + if not noctalia.commandExists("dbus-monitor") then + noctalia.log("dbus-monitor not found; falling back to polling only") + return + end + noctalia.runStream( + "dbus-monitor --session" + .. " \"type='signal',interface='org.kde.KWin.Tiling'\"" + .. " \"type='signal',interface='org.kde.KWin.VirtualDesktopManager',member='currentChanged'\"", + onMonitorLine + ) +end + +-- Startup: query immediately, then keep a slow poll as a self-healing net +-- (recovers when the monitor dies or the compositor gains /Tiling after a +-- rebuild). Signal-driven refreshes make updates feel instant. +refresh() +startMonitor() +noctalia.setUpdateInterval(5000) + +function update() + refresh() +end diff --git a/kineticwe-layouts/thumbnail.webp b/kineticwe-layouts/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..b314a8986efd3e8ec228b3e8a0ba23bc0849a8a2 GIT binary patch literal 12658 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