feat: add mango_layouts plugin (#192)

* feat: add mango_layouts plugin

* fix: address CI validation errors

* refactor: inline apply_layout script for cross-device portability

* fix: commit inlined script changes in panel.luau

* fix: address official PR review feedback

* fix: revert noctalia.translate which caused runtime crash

* fix: remove setGlyph empty string to prevent fallback skull icon

* feat: restore translations using correct noctalia.tr API

* refactor: remove unused label fields from layouts table
This commit is contained in:
Ezequiel
2026-08-03 16:17:29 -04:00
committed by GitHub
parent d61e327515
commit 44925e4be6
6 changed files with 467 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
# Mango Layouts
A clean, minimalist layout switcher plugin for MangoWC. It provides a bar widget and a fast popup panel to switch workspace layouts on the fly, directly from your desktop.
## Plugin
| Field | Value |
| --- | --- |
| ID | `ezequiel/mango_layouts` |
| Entries | Bar widget: `btn`; panel: `panel` |
## Requirements
Install `jq` and `mmsg` (MangoWC CLI) on `PATH`.
Requires MangoWC as the compositor.
## Usage
Add the widget to your Noctalia bar in Settings. Click the widget to open the layout switcher panel and change the current workspace layout.
Alternatively, toggle the panel directly via command line or keybinding:
```sh
noctalia msg panel-toggle ezequiel/mango_layouts:panel
```
## Settings
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `list_mode` | `bool` | `false` | Display layouts in a vertical list instead of a grid. |
| `show_tile` | `bool` | `true` | Show Tile layout. |
| `show_scroller` | `bool` | `true` | Show Scroller layout. |
| `show_monocle` | `bool` | `true` | Show Monocle layout. |
| `show_grid` | `bool` | `true` | Show Grid layout. |
| `show_fair` | `bool` | `true` | Show Fair layout. |
| `show_deck` | `bool` | `true` | Show Deck layout. |
| `show_dwindle` | `bool` | `true` | Show Dwindle layout. |
| `show_center_tile` | `bool` | `true` | Show Center Tile layout. |
| `show_vertical_tile` | `bool` | `true` | Show Vertical Tile layout. |
| `show_right_tile` | `bool` | `true` | Show Right Tile layout. |
| `show_vertical_scroller` | `bool` | `true` | Show Vertical Scroller layout. |
| `show_vertical_grid` | `bool` | `true` | Show Vertical Grid layout. |
| `show_vertical_deck` | `bool` | `true` | Show Vertical Deck layout. |
| `show_vertical_fair` | `bool` | `true` | Show Vertical Fair layout. |
Widget Specific Settings:
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `show_glyph` | `bool` | `true` | Show the widget icon. |
| `show_text` | `bool` | `false` | Show the active layout name as text. |
| `custom_color` | `string` | `""` | Custom icon/text color (e.g. primary, on_surface). |
## Notes
The plugin uses `mmsg` to communicate with MangoWC for reading the active monitor state and applying the selected layout. Make sure you don't use this plugin on compositors other than MangoWC.
+174
View File
@@ -0,0 +1,174 @@
local layouts = {
{ sym = "T", name = "tile", glyph = "layout-sidebar" },
{ sym = "S", name = "scroller", glyph = "carousel-horizontal" },
{ sym = "M", name = "monocle", glyph = "square" },
{ sym = "G", name = "grid", glyph = "layout-grid" },
{ sym = "F", name = "fair", glyph = "layout-board-split" },
{ sym = "D", name = "deck", glyph = "layers-difference" },
{ sym = "DW", name = "dwindle", glyph = "layout-2" },
{ sym = "CT", name = "center_tile", glyph = "layout-distribute-vertical" },
{ sym = "VT", name = "vertical_tile", glyph = "layout-rows" },
{ sym = "RT", name = "right_tile", glyph = "layout-sidebar-right" },
{ sym = "VS", name = "vertical_scroller",glyph = "carousel-vertical" },
{ sym = "VG", name = "vertical_grid", glyph = "grid-dots" },
{ sym = "VD", name = "vertical_deck", glyph = "chart-funnel" },
{ sym = "VF", name = "vertical_fair", glyph = "layout-board" },
}
local COLUMNS = 4
local COLUMN_GAP = 2
local selectedLayout = ""
local pickSlots = {}
local render
local function onPickAt(index)
local slot = pickSlots[index]
if slot == nil then return end
selectedLayout = slot.sym
noctalia.runAsync("mmsg dispatch setlayout," .. slot.entry.name)
panel.close()
end
local function makeGrid(children)
local rows = {}
local row = {}
for i, child in ipairs(children) do
table.insert(row, child)
if #row >= COLUMNS or i == #children then
while #row < COLUMNS do
table.insert(row, ui.spacer({ flexGrow = 1 }))
end
table.insert(rows, ui.row({ gap = COLUMN_GAP, align = "stretch" }, row))
row = {}
end
end
return ui.column({ gap = COLUMN_GAP }, rows)
end
local function getShowConfig(name)
if name == "tile" then return noctalia.getConfig("show_tile") end
if name == "scroller" then return noctalia.getConfig("show_scroller") end
if name == "monocle" then return noctalia.getConfig("show_monocle") end
if name == "grid" then return noctalia.getConfig("show_grid") end
if name == "fair" then return noctalia.getConfig("show_fair") end
if name == "deck" then return noctalia.getConfig("show_deck") end
if name == "dwindle" then return noctalia.getConfig("show_dwindle") end
if name == "center_tile" then return noctalia.getConfig("show_center_tile") end
if name == "vertical_tile" then return noctalia.getConfig("show_vertical_tile") end
if name == "right_tile" then return noctalia.getConfig("show_right_tile") end
if name == "vertical_scroller" then return noctalia.getConfig("show_vertical_scroller") end
if name == "vertical_grid" then return noctalia.getConfig("show_vertical_grid") end
if name == "vertical_deck" then return noctalia.getConfig("show_vertical_deck") end
if name == "vertical_fair" then return noctalia.getConfig("show_vertical_fair") end
return true
end
local function renderButtons(is_list_mode)
local buttons = {}
local i = 0
for _, entry in ipairs(layouts) do
local raw_val = getShowConfig(entry.name)
local enabled = (raw_val == true) or (raw_val == "true") or (raw_val == nil)
if enabled then
local slotIndex = i
local isSelected = selectedLayout == entry.sym
pickSlots[slotIndex] = { sym = entry.sym, entry = entry }
if is_list_mode then
table.insert(buttons, ui.button({
key = entry.sym,
glyph = entry.glyph,
text = noctalia.tr("name_" .. entry.name),
variant = if isSelected then "primary" else "ghost",
onClick = `onPick{slotIndex}`,
-- If noctalia supports button alignment, this will align it to the left:
-- (If not, it will just remain centered like an elegant menu)
}))
else
-- Grid Mode
table.insert(buttons, ui.column({
key = entry.sym,
gap = 2,
align = "center",
flexGrow = 1,
}, {
ui.button({
glyph = entry.glyph,
glyphSize = 18,
variant = if isSelected then "primary" else "ghost",
onClick = `onPick{slotIndex}`,
}),
ui.label({
text = noctalia.tr("name_" .. entry.name),
fontSize = 9,
fontWeight = "bold",
color = if isSelected then "primary" else "on_surface_variant",
textAlign = "center",
maxLines = 1,
}),
}))
end
i += 1
end
end
return buttons
end
render = function()
pickSlots = {}
local list_mode_raw = noctalia.getConfig("list_mode")
local is_list_mode = (list_mode_raw == true) or (list_mode_raw == "true")
local content
if is_list_mode then
content = ui.column({ padding = 8, gap = 4 }, renderButtons(true))
else
content = ui.column({ padding = 12, gap = COLUMN_GAP }, {
makeGrid(renderButtons(false))
})
end
panel.render(ui.column({ flexGrow = 1 }, {
ui.scroll({ flexGrow = 1 }, {
content
})
}))
end
function onOpen(context)
selectedLayout = ""
render()
noctalia.runAsync(
"mmsg get all-monitors | jq -r '.monitors[] | select(.active == true and .last_open_surface != null) | .layout_symbol'",
function(result)
if result == nil or result.exitCode ~= 0 then return end
local sym = result.stdout:gsub("%s+", "")
-- Check if sym exists in our ordered list
local exists = false
for _, l in ipairs(layouts) do
if l.sym == sym then exists = true end
end
selectedLayout = if exists then sym else ""
render()
end
)
end
function onPick0() onPickAt(0) end
function onPick1() onPickAt(1) end
function onPick2() onPickAt(2) end
function onPick3() onPickAt(3) end
function onPick4() onPickAt(4) end
function onPick5() onPickAt(5) end
function onPick6() onPickAt(6) end
function onPick7() onPickAt(7) end
function onPick8() onPickAt(8) end
function onPick9() onPickAt(9) end
function onPick10() onPickAt(10) end
function onPick11() onPickAt(11) end
function onPick12() onPickAt(12) end
function onPick13() onPickAt(13) end
+134
View File
@@ -0,0 +1,134 @@
id = "ezequiel/mango_layouts"
name = "Mango Layouts"
version = "1.0.0"
plugin_api = 14
author = "ezequiel"
license = "MIT"
icon = "layout-filled"
description = "Layout switcher for MangoWC"
tags = ["bar", "panel", "mangowc", "utility"]
dependencies = ["jq", "mmsg"]
[[widget]]
id = "btn"
entry = "widget.luau"
actions = {}
# -- WIDGET SETTINGS --
[[widget.setting]]
key = "show_glyph"
type = "bool"
label_key = "show_icon_label"
default = true
[[widget.setting]]
key = "show_text"
type = "bool"
label_key = "show_text_label"
default = false
[[widget.setting]]
key = "custom_color"
type = "string"
label_key = "custom_color_label"
description_key = "custom_color_desc"
default = ""
[[panel]]
id = "panel"
entry = "panel.luau"
width = 240
height = 250
placement = "floating"
position = "bottom_right"
# -- GLOBAL SETTINGS --
[[setting]]
key = "list_mode"
type = "bool"
label_key = "list_mode_label"
default = false
[[setting]]
key = "show_tile"
type = "bool"
label_key = "layout_tile_label"
default = true
[[setting]]
key = "show_scroller"
type = "bool"
label_key = "layout_scroller_label"
default = true
[[setting]]
key = "show_monocle"
type = "bool"
label_key = "layout_monocle_label"
default = true
[[setting]]
key = "show_grid"
type = "bool"
label_key = "layout_grid_label"
default = true
[[setting]]
key = "show_fair"
type = "bool"
label_key = "layout_fair_label"
default = true
[[setting]]
key = "show_deck"
type = "bool"
label_key = "layout_deck_label"
default = true
[[setting]]
key = "show_dwindle"
type = "bool"
label_key = "layout_dwindle_label"
default = true
[[setting]]
key = "show_center_tile"
type = "bool"
label_key = "layout_center_tile_label"
default = true
[[setting]]
key = "show_vertical_tile"
type = "bool"
label_key = "layout_vertical_tile_label"
default = true
[[setting]]
key = "show_right_tile"
type = "bool"
label_key = "layout_right_tile_label"
default = true
[[setting]]
key = "show_vertical_scroller"
type = "bool"
label_key = "layout_vertical_scroller_label"
default = true
[[setting]]
key = "show_vertical_grid"
type = "bool"
label_key = "layout_vertical_grid_label"
default = true
[[setting]]
key = "show_vertical_deck"
type = "bool"
label_key = "layout_vertical_deck_label"
default = true
[[setting]]
key = "show_vertical_fair"
type = "bool"
label_key = "layout_vertical_fair_label"
default = true
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+35
View File
@@ -0,0 +1,35 @@
{
"show_icon_label": "Show Icon",
"show_text_label": "Show Text",
"custom_color_label": "Icon/Text Color",
"custom_color_desc": "E.g. primary, on_surface, etc.",
"list_mode_label": "Vertical List Mode",
"layout_tile_label": "Layout: Tile",
"layout_scroller_label": "Layout: Scroller",
"layout_monocle_label": "Layout: Monocle",
"layout_grid_label": "Layout: Grid",
"layout_fair_label": "Layout: Fair",
"layout_deck_label": "Layout: Deck",
"layout_dwindle_label": "Layout: Dwindle",
"layout_center_tile_label": "Layout: Center Tile",
"layout_vertical_tile_label": "Layout: Vertical Tile",
"layout_right_tile_label": "Layout: Right Tile",
"layout_vertical_scroller_label": "Layout: Vertical Scroller",
"layout_vertical_grid_label": "Layout: Vertical Grid",
"layout_vertical_deck_label": "Layout: Vertical Deck",
"layout_vertical_fair_label": "Layout: Vertical Fair",
"name_tile": "Tile",
"name_scroller": "Scroller",
"name_monocle": "Monocle",
"name_grid": "Grid",
"name_fair": "Fair",
"name_deck": "Deck",
"name_dwindle": "Dwindle",
"name_center_tile": "Center Tile",
"name_vertical_tile": "Vertical Tile",
"name_right_tile": "Right Tile",
"name_vertical_scroller": "Vertical Scroller",
"name_vertical_grid": "Vertical Grid",
"name_vertical_deck": "Vertical Deck",
"name_vertical_fair": "Vertical Fair"
}
+68
View File
@@ -0,0 +1,68 @@
local layouts = {
T = { name = "tile", glyph = "layout-sidebar" },
S = { name = "scroller", glyph = "carousel-horizontal" },
M = { name = "monocle", glyph = "square" },
G = { name = "grid", glyph = "layout-grid" },
F = { name = "fair", glyph = "layout-board-split" },
D = { name = "deck", glyph = "layers-difference" },
DW = { name = "dwindle", glyph = "layout-2" },
CT = { name = "center_tile", glyph = "layout-distribute-vertical" },
VT = { name = "vertical_tile", glyph = "layout-rows" },
RT = { name = "right_tile", glyph = "layout-sidebar-right" },
VS = { name = "vertical_scroller",glyph = "carousel-vertical" },
VG = { name = "vertical_grid", glyph = "grid-dots" },
VD = { name = "vertical_deck", glyph = "chart-funnel" },
VF = { name = "vertical_fair", glyph = "layout-board" },
}
local show_glyph_raw = noctalia.getConfig("show_glyph")
local show_glyph = (show_glyph_raw == true) or (show_glyph_raw == "true") or (show_glyph_raw == nil)
local show_text_raw = noctalia.getConfig("show_text")
local show_text = (show_text_raw == true) or (show_text_raw == "true")
local custom_color = noctalia.getConfig("custom_color")
function update()
noctalia.setUpdateInterval(1000)
noctalia.runAsync(
"mmsg get all-monitors | jq -r '.monitors[] | select(.active == true) | .layout_symbol'",
function(result)
if result and result.exitCode == 0 then
local sym = result.stdout:gsub("%s+", "")
local current = layouts[sym]
if current then
if show_glyph then
barWidget.setGlyph(current.glyph)
end
if show_text then
barWidget.setText(noctalia.tr("name_" .. current.name))
else
barWidget.setText("")
end
if custom_color and custom_color ~= "" then
barWidget.setColor(custom_color)
barWidget.setGlyphColor(custom_color)
end
else
if show_glyph then
barWidget.setGlyph("layout")
end
if show_text then
barWidget.setText("Layout")
else
barWidget.setText("")
end
end
end
end
)
end
function onClick()
noctalia.togglePanel("ezequiel/mango_layouts:panel")
end