Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions hypr-layout-switcher/README.md
Original file line number Diff line number Diff line change
@@ -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 <author/plugin:entry> <target[:bar-name]> <event>`. `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 } })
```
18 changes: 18 additions & 0 deletions hypr-layout-switcher/plugin.toml
Original file line number Diff line number Diff line change
@@ -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"
64 changes: 64 additions & 0 deletions hypr-layout-switcher/service.luau
Original file line number Diff line number Diff line change
@@ -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()

Binary file added hypr-layout-switcher/thumbnail.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions hypr-layout-switcher/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{

}
29 changes: 29 additions & 0 deletions hypr-layout-switcher/widget.luau
Original file line number Diff line number Diff line change
@@ -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()