diff --git a/mangowm-status/README.md b/mangowm-status/README.md new file mode 100644 index 00000000..c60cf430 --- /dev/null +++ b/mangowm-status/README.md @@ -0,0 +1,96 @@ +# Mangowm Status + +Shows the current Mangowm tiling layout and keymode on your bar. Click to open a picker and switch layouts instantly. + +![Thumbnail](thumbnail.webp) + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `x0d7x/mangowm-status` | +| Entries | Bar widget: `status`; panel: `layout-picker` | + +## Requirements + +- Noctalia v5 +- [`mmsg`](https://github.com/mangowm/mango) — Mangowm's IPC CLI tool (must be on `PATH`) + +## Usage + +### Bar widget + +Add the **Mangowm Status** widget from the Add-widget picker in your bar settings, or configure it by hand with: + +```toml +[widget.mangowm-status] +type = "x0d7x/mangowm-status:status" +``` + +The widget displays the current layout's icon and letter code. When the active keymode is not `default`, the keymode name is shown beside the layout code. + +- **Left-click** the widget → opens the **Layout Picker** panel. Click any layout to switch immediately. +- **Horizontal scroll** over the widget → cycles through the core layouts (T → S → G → …). + +### Layout picker panel + +Open with: + +```sh +noctalia msg panel-toggle x0d7x/mangowm-status:layout-picker +``` + +A scrollable list of all 14 supported layouts. The current layout is highlighted. Click any other layout to switch — the panel closes automatically. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| Hide on default | `bool` | `false` | Hide the widget when keymode is `default` | +| Show text | `bool` | `true` | Show the keymode name beside the layout glyph | +| Notify on change | `bool` | `true` | Show a notification when the keymode changes | + +## IPC + +Control the widget from the terminal: + +```sh +# Refresh layout data +noctalia msg plugin x0d7x/mangowm-status:status focused refresh + +# Get current status (logged as JSON) +noctalia msg plugin x0d7x/mangowm-status:status focused get-status + +# Set layout by code (T, S, G, M, K, CT, RT, DW, F, VS, VT, VG, VK, VF) +noctalia msg plugin x0d7x/mangowm-status:status focused set-layout S +``` + +## Layout Codes + +| Code | Layout | Icon | +|------|--------|------| +| T | Tile | columns | +| S | Scroller | menu | +| G | Grid | dashboard | +| M | Monocle | window | +| K | Deck | cards | +| CT | Center Tile | layout | +| RT | Right Tile | columns | +| DW | Dwindle | columns | +| F | Fair | layout | +| VS | Vertical Scroller | menu | +| VT | Vertical Tile | columns | +| VG | Vertical Grid | dashboard | +| VK | Vertical Deck | cards | +| VF | Vertical Fair | layout | + +## Notes + +- This plugin spawns `mmsg` subprocesses to query and change the compositor state. `mmsg` must be installed and on `PATH`. +- Layout changes use `mmsg dispatch setlayout,`. +- The widget keeps two long-running `mmsg watch` streams (keymode + all-tags) for live updates, with a 1-second polling fallback. +- The panel entry fetches tag data on open and does not keep a persistent connection. + +## License + +MIT diff --git a/mangowm-status/panel.luau b/mangowm-status/panel.luau new file mode 100644 index 00000000..ca4ecbb9 --- /dev/null +++ b/mangowm-status/panel.luau @@ -0,0 +1,135 @@ +--!nonstrict +-- Mangowm Status — Layout Picker Panel +-- Shows all available tiling layouts; click one to switch immediately. + +-- Layout manifest: glyph + letter + name (same as widget.luau) +local layoutConfig = { + T = { glyph = "columns", letter = "T", name = "tile" }, + S = { glyph = "menu", letter = "S", name = "scroller" }, + G = { glyph = "dashboard", letter = "G", name = "grid" }, + M = { glyph = "window", letter = "M", name = "monocle" }, + K = { glyph = "cards", letter = "K", name = "deck" }, + CT = { glyph = "layout", letter = "C", name = "center_tile" }, + RT = { glyph = "columns", letter = "R", name = "right_tile" }, + VS = { glyph = "menu", letter = "V", name = "vertical_scroller" }, + VT = { glyph = "columns", letter = "V", name = "vertical_tile" }, + VG = { glyph = "dashboard", letter = "G", name = "vertical_grid" }, + VK = { glyph = "cards", letter = "K", name = "vertical_deck" }, + DW = { glyph = "columns", letter = "D", name = "dwindle" }, + F = { glyph = "layout", letter = "F", name = "fair" }, + VF = { glyph = "layout", letter = "F", name = "vertical_fair" }, +} + +-- Layout codes in display order +local layoutOrder = { "T", "S", "G", "M", "K", "CT", "RT", "DW", "F", "VS", "VT", "VG", "VK", "VF" } + +local currentLayoutCode = "T" +local switching = false + +-- Helper to switch layout and close panel +local function setLayout(code) + if switching or code == currentLayoutCode then return end + switching = true + + local cfg = layoutConfig[code] + local name = cfg and cfg.name or "tile" + noctalia.runAsync("mmsg dispatch setlayout," .. name, function(result) + switching = false + if result.exitCode == 0 then + currentLayoutCode = code + panel.close() + else + noctalia.notifyError("Mangowm Status", "Failed to set layout: " .. (result.stderr or "unknown error")) + doRender() + end + end) +end + +-- Named global functions for each layout button (onClick doesn't pass arguments) +function onLayoutT() setLayout("T") end +function onLayoutS() setLayout("S") end +function onLayoutG() setLayout("G") end +function onLayoutM() setLayout("M") end +function onLayoutK() setLayout("K") end +function onLayoutCT() setLayout("CT") end +function onLayoutRT() setLayout("RT") end +function onLayoutDW() setLayout("DW") end +function onLayoutF() setLayout("F") end +function onLayoutVS() setLayout("VS") end +function onLayoutVT() setLayout("VT") end +function onLayoutVG() setLayout("VG") end +function onLayoutVK() setLayout("VK") end +function onLayoutVF() setLayout("VF") end + +local function doRender() + -- Map layout code -> onClick handler name + local onClickHandler = { + T = "onLayoutT", S = "onLayoutS", G = "onLayoutG", + M = "onLayoutM", K = "onLayoutK", CT = "onLayoutCT", + RT = "onLayoutRT", DW = "onLayoutDW", F = "onLayoutF", + VS = "onLayoutVS", VT = "onLayoutVT", VG = "onLayoutVG", + VK = "onLayoutVK", VF = "onLayoutVF", + } + + -- Current layout indicator (compact, shown at top) + local currentCfg = layoutConfig[currentLayoutCode] or layoutConfig["T"] + local currentBadge = ui.row({ gap = 6, align = "center", fill = "primary/0.12", radius = 6, paddingH = 10, paddingV = 5 }, { + ui.label({ text = "Now:", color = "on_surface_variant", fontSize = 11 }), + ui.glyph({ name = currentCfg.glyph, size = 12, color = "primary" }), + ui.label({ text = currentCfg.letter .. " — " .. currentCfg.name, color = "primary", fontWeight = "bold", fontSize = 11 }), + }) + + -- Build layout buttons + local buttons = {} + for _, code in ipairs(layoutOrder) do + local cfg = layoutConfig[code] + if cfg then + local isActive = code == currentLayoutCode + table.insert(buttons, ui.button({ + key = "layout-" .. code, + glyph = cfg.glyph, + glyphSize = 14, + text = " " .. cfg.letter .. " — " .. cfg.name, + contentAlign = "start", + variant = isActive and "primary" or "ghost", + controlSize = "sm", + onClick = onClickHandler[code], + })) + end + end + + panel.render(ui.column({ flexGrow = 1, gap = 2, padding = 8 }, { + currentBadge, + ui.separator({ thickness = 1, color = "outline/0.15", spacing = 3 }), + ui.scroll({ flexGrow = 1, gap = 2 }, buttons), + })) +end + +-- Fetch current layout from mmsg, then render +local function refreshCurrentLayout() + noctalia.runAsync("mmsg get all-tags", function(result) + if result.exitCode == 0 and result.stdout then + local ok, data = pcall(noctalia.json.decode, result.stdout) + if ok and data and data.all_tags then + for _, monitorEntry in ipairs(data.all_tags) do + if type(monitorEntry) == "table" and monitorEntry.tags then + for _, tag in ipairs(monitorEntry.tags) do + if type(tag) == "table" and tag.is_active and tag.layout then + currentLayoutCode = tag.layout + doRender() + return + end + end + end + end + end + end + -- Render even if fetch failed (uses fallback currentLayoutCode) + doRender() + end) +end + +function onOpen(_context) + -- Fetch current layout first, then render (avoids flash of wrong layout) + refreshCurrentLayout() +end diff --git a/mangowm-status/plugin.toml b/mangowm-status/plugin.toml new file mode 100644 index 00000000..5a5fdb82 --- /dev/null +++ b/mangowm-status/plugin.toml @@ -0,0 +1,44 @@ +id = "x0d7x/mangowm-status" +name = "Mangowm Status" +version = "1.1.0" +plugin_api = 4 +author = "x0d7x" +license = "MIT" +dependencies = ["mmsg"] +icon = "grid" +description = "Shows current tiling layout and keymode for Mangowm" +tags = ["bar", "mangowc"] + +[[widget]] +id = "status" +entry = "widget.luau" + + [[widget.setting]] + key = "hide_on_default" + type = "bool" + label_key = "settings.hide_on_default.label" + description_key = "settings.hide_on_default.description" + default = false + + [[widget.setting]] + key = "show_text" + type = "bool" + label_key = "settings.show_text.label" + description_key = "settings.show_text.description" + default = true + + [[widget.setting]] + key = "notify_change" + type = "bool" + label_key = "settings.notify_change.label" + description_key = "settings.notify_change.description" + default = true + +# Layout picker panel — opens on click, shows all layouts to choose from +[[panel]] +id = "layout-picker" +entry = "panel.luau" +width = 280 +height = 400 +placement = "floating" +position = "auto" diff --git a/mangowm-status/thumbnail.webp b/mangowm-status/thumbnail.webp new file mode 100644 index 00000000..0c725362 Binary files /dev/null and b/mangowm-status/thumbnail.webp differ diff --git a/mangowm-status/translations/en.json b/mangowm-status/translations/en.json new file mode 100644 index 00000000..b16f859b --- /dev/null +++ b/mangowm-status/translations/en.json @@ -0,0 +1,19 @@ +{ + "plugin": { + "description": "Shows current tiling layout and keymode for Mangowm" + }, + "settings": { + "hide_on_default": { + "label": "Hide on default", + "description": "Hide the widget when keymode is default" + }, + "show_text": { + "label": "Show text", + "description": "Show the keymode name beside the glyph" + }, + "notify_change": { + "label": "Notify on change", + "description": "Show a notification when keymode changes" + } + } +} diff --git a/mangowm-status/widget.luau b/mangowm-status/widget.luau new file mode 100644 index 00000000..2f0b3a46 --- /dev/null +++ b/mangowm-status/widget.luau @@ -0,0 +1,206 @@ +--!nonstrict +-- Mangowm Status — shows tiling layout icon + letter + keymode when not default + +local showText = noctalia.getConfig("show_text") +local notifyChange = noctalia.getConfig("notify_change") +local hideOnDefault = noctalia.getConfig("hide_on_default") + +local currentKeymode = "default" +local currentLayoutCode = "T" -- default tile code + +-- Layout display config: glyph + letter + name +local layoutConfig = { + T = { glyph = "columns", letter = "T", name = "tile" }, + S = { glyph = "menu", letter = "S", name = "scroller" }, + G = { glyph = "dashboard", letter = "G", name = "grid" }, + M = { glyph = "window", letter = "M", name = "monocle" }, + K = { glyph = "cards", letter = "K", name = "deck" }, + CT = { glyph = "layout", letter = "C", name = "center_tile" }, + RT = { glyph = "columns", letter = "R", name = "right_tile" }, + VS = { glyph = "menu", letter = "V", name = "vertical_scroller" }, + VT = { glyph = "columns", letter = "V", name = "vertical_tile" }, + VG = { glyph = "dashboard", letter = "G", name = "vertical_grid" }, + VK = { glyph = "cards", letter = "K", name = "vertical_deck" }, + DW = { glyph = "columns", letter = "D", name = "dwindle" }, + F = { glyph = "layout", letter = "F", name = "fair" }, + VF = { glyph = "layout", letter = "F", name = "vertical_fair" }, +} + +-- Layout order for cycling (horizontal scroll) +local layoutOrder = { "T", "S", "G", "M", "K", "CT", "RT", "DW", "F" } + +local function getLayoutConfig(code) + local cfg = layoutConfig[code] + if cfg then return cfg end + return layoutConfig["T"] -- fallback to tile +end + +-- Extract layout code from mmsg JSON +local function extractLayoutCode(data) + if type(data) ~= "table" then return nil end + + local allTags = data.all_tags + if not allTags or type(allTags) ~= "table" then return nil end + + for _, monitorEntry in ipairs(allTags) do + if type(monitorEntry) == "table" and monitorEntry.tags then + for _, tag in ipairs(monitorEntry.tags) do + if type(tag) == "table" and tag.is_active and tag.layout then + return tag.layout + end + end + end + end + return nil +end + +-- Re-render the bar widget +local function refreshDisplay() + local isDefault = currentKeymode == "default" + + if hideOnDefault and isDefault then + barWidget.setVisible(false) + return + else + barWidget.setVisible(true) + end + + local cfg = getLayoutConfig(currentLayoutCode) + + local keymodeStr = "" + if showText and not isDefault then + keymodeStr = " [" .. currentKeymode .. "]" + end + + barWidget.setGlyph(cfg.glyph) + barWidget.setText(cfg.letter .. keymodeStr) + barWidget.setTooltip("Layout: " .. cfg.name .. " (" .. currentLayoutCode .. ") | Mode: " .. currentKeymode) +end + +-- Watch keymode changes +noctalia.runStream("mmsg watch keymode", function(line) + local ok, data = pcall(noctalia.json.decode, line) + if ok and data then + local km = data.keymode or data.mode or data.name + if km and currentKeymode ~= km then + currentKeymode = km + if notifyChange then + noctalia.notify("Keymode changed", currentKeymode) + end + refreshDisplay() + end + end +end) + +-- Watch tag/layout changes — instant re-render +noctalia.runStream("mmsg watch all-tags", function(line) + local ok, data = pcall(noctalia.json.decode, line) + if ok and data then + local code = extractLayoutCode(data) + if code and code ~= currentLayoutCode then + currentLayoutCode = code + refreshDisplay() + end + end +end) + +-- Polling fallback +local function pollLayout() + noctalia.runAsync("mmsg get all-tags", function(result) + if result.exitCode == 0 and result.stdout then + local ok, data = pcall(noctalia.json.decode, result.stdout) + if ok and data then + local code = extractLayoutCode(data) + if code then + currentLayoutCode = code + refreshDisplay() + end + end + end + end) +end + +pollLayout() + +function update() + noctalia.setUpdateInterval(1000) + pollLayout() + refreshDisplay() +end + +function onClick() + noctalia.togglePanel("x0d7x/mangowm-status:layout-picker") +end + +-- Cleanup handler +function onExit(_signal) + noctalia.log("mangowm-status: cleaning up streams") +end + +-- Settings change handler +function onConfigChanged() + showText = noctalia.getConfig("show_text") + notifyChange = noctalia.getConfig("notify_change") + hideOnDefault = noctalia.getConfig("hide_on_default") + refreshDisplay() + noctalia.log("mangowm-status: config reloaded") +end + +-- IPC command handler +function onIpc(event, payload) + if event == "refresh" then + pollLayout() + noctalia.log("mangowm-status: IPC refresh triggered") + elseif event == "set-layout" and payload then + local cfg = getLayoutConfig(payload) + local name = cfg and cfg.name or payload + noctalia.runAsync("mmsg dispatch setlayout," .. name, function(result) + if result.exitCode == 0 then + pollLayout() + noctalia.log("mangowm-status: layout set to " .. name) + else + noctalia.notifyError("Mangowm Status", "Failed to set layout: " .. (result.stderr or "unknown error")) + end + end) + elseif event == "get-status" then + local cfg = getLayoutConfig(currentLayoutCode) + local status = { + layout = currentLayoutCode, + layout_name = cfg.name, + keymode = currentKeymode, + } + noctalia.log("mangowm-status: " .. noctalia.json.encode(status)) + else + noctalia.log("mangowm-status: unknown IPC event: " .. event) + end +end + +-- Scroll handler for layout cycling +function onScroll(axis, steps) + if axis ~= "horizontal" then return end + + -- Find current layout index + local currentIndex = 1 + for i, code in ipairs(layoutOrder) do + if code == currentLayoutCode then + currentIndex = i + break + end + end + + -- Calculate new index + local newIndex = currentIndex + steps + if newIndex < 1 then + newIndex = #layoutOrder + elseif newIndex > #layoutOrder then + newIndex = 1 + end + + local newLayoutCode = layoutOrder[newIndex] + local cfg = getLayoutConfig(newLayoutCode) + noctalia.runAsync("mmsg dispatch setlayout," .. cfg.name, function(result) + if result.exitCode == 0 then + pollLayout() + end + end) +end