Build a menu
Build quick-menu, a bar button with an Audio submenu, volume controls, and an entry to OmniWM’s menu. You need OmniWM running, omniwmctl on your PATH, and a text editor. Enable the Workspace Bar to try the bar button. The first widget guide explains UI tables and child elements.
1. Create the menu widget
Section titled “1. Create the menu widget”omniwmctl shell init quick-menuOpen ~/.config/omniwm/shell/widgets/quick-menu/init.luau. Replace its starter contents with the complete file below. Use a different name if you already have a widget called quick-menu; adjust the later toggle command too.
2. Add the button, rows, and submenu
Section titled “2. Add the button, rows, and submenu”Download widgets/quick-menu/init.luau
local function item(key, symbol, label, action) return ui.row{ key = key, w = 260, pad = 10, gap = 10, radius = 8, hover = "accent/0.15", onClick = action, ui.symbol{ name = symbol, size = 14, color = "accent" }, ui.text{ text = label, grow = 1, align = "leading" } }end
function render(ctx) return ui.row{ gap = 6, pad = 6, onClick = ui.popout, ui.symbol{ name = "line.3.horizontal", size = 14 }, ui.text{ text = "Quick menu" } }end
function popout(ctx) local rows = { gap = 4, pad = 8, radius = 12, fill = "surface" } if ctx.state.page == "audio" then rows[#rows + 1] = item("back", "chevron.left", "Back", function(next) next.state.page = "main" end) rows[#rows + 1] = item("volume-up", "speaker.plus", "Volume +5%", function() omniwm.run("volume up 5") end) rows[#rows + 1] = item("mute", "speaker.slash", "Mute output", function() omniwm.run("volume mute") end) else rows[#rows + 1] = item("audio", "speaker.wave.2", "Audio…", function(next) next.state.page = "audio" end) rows[#rows + 1] = item("omniwm-menu", "menubar.rectangle", "OmniWM menu", function() omniwm.run("shell menu") end) end rows[#rows + 1] = item("close", "xmark", "Close", ui.popout) return ui.column(rows)endThe file has three parts:
render(ctx)draws the bar button.onClick = ui.popouttells the native host to toggle this widget’s popout.popout(ctx)draws the open panel. The localitemhelper builds a row from an icon, label, and action, keeping row styling in one place.- Clicking Audio… changes
ctx.state.pageto"audio"; clicking Back changes it to"main". OmniShell rerenders after the callback, so there is no second window or polling loop.
function(next) ... end is an anonymous callback. Its first argument is the current context when clicked. ctx.state survives renders and stays separate for each display/placement; it resets when the widget reloads. Closing and reopening this menu retains the selected submenu.
The volume callbacks use omniwm.run to issue shell commands. Volume +5% and Mute output keep the menu open for repeated use. OmniWM menu opens OmniWM’s existing menu at the widget. Find more commands in shell actions and the CLI action reference.
Close also uses onClick = ui.popout. Keep this as a direct handler value: ui.popout is a native toggle marker, and calling it inside a Luau callback does not toggle a panel. This Close row works in both a normal popout and a drawer.
3. Place and try the menu
Section titled “3. Place and try the menu”- Open Settings > Workspace Bar > Layout > Bar Widgets.
- Select Add Widget…, choose quick-menu, and add it to Trailing.
- Click Quick menu on the bar, then Audio….
- Try Volume +5%, then Back.
- Choose Close, then reopen the button.
You should see the panel replace its main rows with Audio rows, then return to the main page. The Close row dismisses the panel. Volume actions change the system output volume.
You can also open or close the popout from a terminal:
omniwmctl shell toggle quick-menu4. Reuse the popout in a drawer
Section titled “4. Reuse the popout in a drawer”- Open Settings > Widgets > Drawers and add a drawer named quick-drawer.
- Choose the Right edge and add quick-menu to its widgets.
- Open it with the command below. To open it at the edge, turn on the drawer’s Hover option.
omniwmctl shell toggle quick-drawerThe drawer uses the same popout(ctx) tree. Its Close row dismisses the containing drawer, and running the drawer toggle command again also closes it. A drawer works while either the bar or island is enabled, even if quick-menu is not on a bar lane. The bars, popouts, and drawers guide explains drawer width, hover, and close behavior.
5. Verify and extend it
Section titled “5. Verify and extend it”omniwmctl query shell-widgetsCheck quick-menu for a load or render error. To add a row, append another item(...) to the relevant branch and give it a unique key. Use a command from the existing API or update widget state in its callback. Refer to UI elements for sliders, inputs, and other controls.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Check |
|---|---|
| The button does not open | Ensure popout(ctx) is a global function and onClick = ui.popout is assigned directly. |
| A submenu is still shown after reopening | This is retained ctx.state; use Back or reload the source to reset it. |
| The drawer does not open | Enable either the Workspace Bar or Dynamic Island and check that its widget list contains quick-menu. |
| A command fails | Use omniwmctl query shell-widgets to read the reported command error and compare the command with the reference. |
Next, build a Dynamic Island and reuse a component across profiles.