diff --git a/hypr-layout-switcher/README.md b/hypr-layout-switcher/README.md new file mode 100644 index 00000000..ac437f3f --- /dev/null +++ b/hypr-layout-switcher/README.md @@ -0,0 +1,84 @@ +# Hyprland Layout Switcher + +![Hyprland Layout Switcher thumbnail](thumbnail.webp) + +**Hyprland Layout Switcher** is a bar widget for [Noctalia](https://docs.noctalia.dev) that shows the tiled layout of the currently active Hyprland workspace and cycles to the next layout on click or via a keybinding. Switching is scoped to the current workspace with a workspace rule, so each workspace keeps its own layout instead of changing the global default. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `maddingo/hypr-layout-switcher` | +| Entries | Bar widget: `toggle`; service: `poller` | + +## Requirements + +Hyprland, with `hyprctl` on `PATH` — the plugin declares `hyprland` as a dependency. It reads `hyprctl activeworkspace -j` and applies layouts through `hyprctl eval`, so it does nothing on other compositors (Sway, river, Niri, X11 WMs, …). + +The `monocle` and `scrolling` layouts come from Hyprland layout plugins. If you do not have them installed, cycling onto them is a no-op on Hyprland's side. + +## Usage + +Enable the plugin, then add the widget to a bar section: + +```sh +noctalia msg plugins enable maddingo/hypr-layout-switcher +``` + +In Settings → Bar, add **Hyprland Layout Switcher → toggle** to the section you want. The widget shows an icon plus the layout name (`Dwindle`, `Master`, `Monocle`, `Scrolling`). + +The `poller` service runs `hyprctl activeworkspace -j` once per second and publishes the layout, so the widget stays in sync when you switch workspaces or change the layout from elsewhere. + +Clicking the widget cycles to the next layout in `dwindle → master → monocle → scrolling → dwindle`. Special workspaces (scratchpads) are handled too, matched by name instead of id. + +## IPC + +The `poller` service accepts a `cycle` event, which does the same thing as clicking the widget: + +```sh +noctalia msg plugin maddingo/hypr-layout-switcher:poller all cycle +``` + +The signature is `plugin `. `poller` is a service entry with no visible output, so the target must be `all`. + +Bind it in `~/.config/hypr/hyprland.lua`: + +```lua +-- Cycle the active workspace's tiled layout +bind("SUPER, Tab, exec, " + .. "noctalia msg plugin maddingo/hypr-layout-switcher:poller all cycle") +``` + +Reload with `hyprctl reload`. The widget updates immediately, whether you cycle via the keybinding or by clicking. + +To jump straight to a specific layout instead of cycling, bind `hyprctl` directly — the widget picks the change up on its next poll: + +```lua +bind("SUPER, D, exec, hyprctl keyword general:layout dwindle") +bind("SUPER, M, exec, hyprctl keyword general:layout master") +``` + +Note that `general:layout` changes the global default, whereas the plugin's `cycle` only affects the active workspace. + +## Notes + +- **Commands spawned:** `hyprctl activeworkspace -j` every second while the plugin is enabled, and `hyprctl eval '…'` on each cycle. No network access, no files written. +- **No settings.** To change behaviour, fork the repo and add your fork as the plugin source. The layout list lives at the top of `service.luau`: + + ```lua + local layouts = { "dwindle", "master", "monocle", "scrolling" } + ``` + + Trim it to the layouts you actually have installed, and keep the `glyphs` table at the top of `widget.luau` in sync. Reload the plugin afterwards. +- The plugin only sets the layout; per-layout options stay in your Hyprland config, for example: + + ```lua + -- See https://wiki.hypr.land/Configuring/Layouts/Dwindle-Layout/ for more + hl.config({ dwindle = { preserve_split = true } }) + + -- See https://wiki.hypr.land/Configuring/Layouts/Master-Layout/ for more + hl.config({ master = { new_status = "master" } }) + + -- See https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/ for more + hl.config({ scrolling = { fullscreen_on_one_column = true } }) + ``` diff --git a/hypr-layout-switcher/plugin.toml b/hypr-layout-switcher/plugin.toml new file mode 100644 index 00000000..e93b444b --- /dev/null +++ b/hypr-layout-switcher/plugin.toml @@ -0,0 +1,18 @@ +id = "maddingo/hypr-layout-switcher" +name = "Hyprland Layout Switcher" +description = "Shows and cycles the active workspace's layout" +version = "0.1.0" +plugin_api = 3 +author = "maddingo" +license = "MIT" +icon = "layout-grid" +tags = ["hyprland"] +dependencies = ["hyprland"] + +[[service]] +id = "poller" +entry = "service.luau" + +[[widget]] +id = "toggle" +entry = "widget.luau" diff --git a/hypr-layout-switcher/service.luau b/hypr-layout-switcher/service.luau new file mode 100644 index 00000000..34f232ce --- /dev/null +++ b/hypr-layout-switcher/service.luau @@ -0,0 +1,64 @@ +--!nonstrict + +local layouts = { "dwindle", "master", "monocle", "scrolling" } + +noctalia.setUpdateInterval(1000) + +local lastLayout = nil + +local function setLayoutCmd(layout) + local lua = string.format( + "local w = hl.get_active_special_workspace() or hl.get_active_workspace(); " + .. "if w then if w.special then hl.workspace_rule({workspace = tostring(w.name), layout = %q}) " + .. "else hl.workspace_rule({workspace = tostring(w.id), layout = %q}) end end", + layout, layout + ) + return "hyprctl eval '" .. lua:gsub("'", "'\\''") .. "'" +end + +local function poll() + if not noctalia.commandExists("hyprctl") then + return + end + noctalia.runAsync("hyprctl activeworkspace -j", function(result) + if result.exitCode ~= 0 then + return + end + local data = noctalia.json.decode(result.stdout) + if data == nil then + return + end + local layout = data.tiledLayout + if layout and layout ~= lastLayout then + lastLayout = layout + noctalia.state.set("layout", layout) + end + end) +end + +local function cycle() + local idx = 1 + for i, l in ipairs(layouts) do + if l == lastLayout then + idx = (i % #layouts) + 1 + break + end + end + local nextLayout = layouts[idx] + noctalia.runAsync(setLayoutCmd(nextLayout)) + lastLayout = nextLayout + noctalia.state.set("layout", nextLayout) +end + +function onIpc(event) + if event == "cycle" then + cycle() + end +end + +function update() + poll() +end + +poll() + diff --git a/hypr-layout-switcher/thumbnail.webp b/hypr-layout-switcher/thumbnail.webp new file mode 100644 index 00000000..81f62d13 Binary files /dev/null and b/hypr-layout-switcher/thumbnail.webp differ diff --git a/hypr-layout-switcher/translations/en.json b/hypr-layout-switcher/translations/en.json new file mode 100644 index 00000000..077404aa --- /dev/null +++ b/hypr-layout-switcher/translations/en.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/hypr-layout-switcher/widget.luau b/hypr-layout-switcher/widget.luau new file mode 100644 index 00000000..2d202bdf --- /dev/null +++ b/hypr-layout-switcher/widget.luau @@ -0,0 +1,29 @@ +--!nonstrict + +local glyphs = { + dwindle = "layout-grid", + master = "layout-sidebar", + monocle = "square", + scrolling = "arrows-horizontal", +} + +local current = "dwindle" + +local function render() + barWidget.setGlyph(glyphs[current] or "layout-grid") + barWidget.setText(current:sub(1, 1):upper() .. current:sub(2)) +end + +noctalia.state.watch("layout", function(value) + if value then + current = value + render() + end +end) + +function onClick() + noctalia.runAsync("noctalia msg plugin maddingo/hypr-layout-switcher:poller all cycle") +end + +render() +