Add KineticWE Layouts plugin (#252)
This commit is contained in:
@@ -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 <kind>` — 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.
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"show_text": {
|
||||||
|
"label": "Show layout name",
|
||||||
|
"description": "Show the current layout name next to the glyph in the bar. When off, only the glyph is shown."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
--!nonstrict
|
||||||
|
-- Bar indicator for the current KineticWE tiling layout.
|
||||||
|
-- Purely reactive: the bridge service publishes state changes.
|
||||||
|
|
||||||
|
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 showText = true
|
||||||
|
|
||||||
|
local function readShowText()
|
||||||
|
local value = noctalia.getConfig("show_text")
|
||||||
|
if value == nil then
|
||||||
|
value = true
|
||||||
|
end
|
||||||
|
showText = value
|
||||||
|
end
|
||||||
|
|
||||||
|
readShowText()
|
||||||
|
|
||||||
|
local function render(state)
|
||||||
|
if state == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not state.ok then
|
||||||
|
barWidget.setGlyph("alert-triangle")
|
||||||
|
barWidget.setText("")
|
||||||
|
barWidget.setTooltip("KineticWE layouts: /Tiling DBus interface not found — rebuild KineticWE")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if state.current == "" then
|
||||||
|
barWidget.setGlyph("layout-off")
|
||||||
|
barWidget.setText(showText and "Floating" or "")
|
||||||
|
barWidget.setTooltip("KineticWE tiling is disabled")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local info = KIND_INFO[state.current]
|
||||||
|
local display = info and info.display or state.current
|
||||||
|
barWidget.setGlyph(info and info.glyph or "layout-grid")
|
||||||
|
barWidget.setText(showText and display or "")
|
||||||
|
barWidget.setTooltip("KineticWE layout: " .. display)
|
||||||
|
end
|
||||||
|
|
||||||
|
noctalia.state.watch("layout", render)
|
||||||
|
-- Paint the latest value immediately in case the service published first.
|
||||||
|
render(noctalia.state.get("layout"))
|
||||||
|
|
||||||
|
-- Apply the show_text setting without a reload.
|
||||||
|
function onConfigChanged()
|
||||||
|
readShowText()
|
||||||
|
render(noctalia.state.get("layout"))
|
||||||
|
end
|
||||||
|
|
||||||
|
function onClick()
|
||||||
|
noctalia.togglePanel("theblackdon/kineticwe-layouts:layouts")
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user