Skip to content

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.

Terminal window
omniwmctl shell init quick-menu

Open ~/.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.

Download widgets/quick-menu/init.luau

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)
end

The file has three parts:

  • render(ctx) draws the bar button. onClick = ui.popout tells the native host to toggle this widget’s popout.
  • popout(ctx) draws the open panel. The local item helper builds a row from an icon, label, and action, keeping row styling in one place.
  • Clicking Audio… changes ctx.state.page to "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.

  1. Open Settings > Workspace Bar > Layout > Bar Widgets.
  2. Select Add Widget…, choose quick-menu, and add it to Trailing.
  3. Click Quick menu on the bar, then Audio….
  4. Try Volume +5%, then Back.
  5. 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:

Terminal window
omniwmctl shell toggle quick-menu
  1. Open Settings > Widgets > Drawers and add a drawer named quick-drawer.
  2. Choose the Right edge and add quick-menu to its widgets.
  3. Open it with the command below. To open it at the edge, turn on the drawer’s Hover option.
Terminal window
omniwmctl shell toggle quick-drawer

The 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.

Terminal window
omniwmctl query shell-widgets

Check 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.

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.