Skip to content

Scripted Control

MorquinDevlar edited this page Sep 7, 2026 · 7 revisions

Scripted Control

Everything the header menus and the mouse can do, a script can do by name - so a game package can offer keyboard commands (ui show quests), hotkeys, or an accessible text interface over the same layout.

These functions are plain set-semantics: each applies a change and returns ok, code[, detail], never echoing anything itself, so your package owns every word the player sees. The header menus and the drag paths call the same functions, so a scripted change leaves exactly the state a mouse action would (docks re-laid, layout saved, Widgets menu refreshed).

code is one of "ok", "already", "unknown_widget", "sidebar_hidden" (detail = the side), "fill", "alone", "same", "target_hidden", "unsupported", "invalid".

Resolving what the player typed

Function Does
mdw.findWidget(query) Resolve a typed name: exact name, exact title, then unique prefix of either (case, spaces, and underscores ignored). Returns widget, or nil, candidates - the sorted candidate names when a prefix was ambiguous, an empty table when nothing matched. Groups are never returned
mdw.widgetConsole(name) The live MiniConsole behind a name: a group resolves to its active member, a tabbed widget to its active tab; nil for an embedded mapper

Visibility and placement

Function Does
mdw.showWidget(name) Reveal a widget and front its tab. Puts it BACK where it was: into the group it was closed from, else at the end of the dock it came from; returns "sidebar_hidden" instead of forcing a group into a hidden sidebar
mdw.hideWidget(name) Close its tab when it has siblings, else hide its (lone) group
mdw.focusWidget(name) showWidget, then raise its group above other floating groups
mdw.floatWidget(name, opts) Give it a group of its own, floating. Centred and cascaded past other floats by default; opts.anchor puts it in a corner instead (see below)
mdw.dockWidget(name, side, position) Its own group docked "left"/"right", at the "top" or (default) bottom of that dock
mdw.groupWidget(name, targetName) Add it to targetName's group as a tab, fronted (migrating out of its old group)
mdw.ungroupWidget(name) Pull it out of its group into one of its own, directly below the old group (or floating, when the old group floats)
mdw.setSidebarVisible(side, on) Show or hide a sidebar by value
mdw.setPromptBarVisible(on) Show or hide the prompt bar by value

One deliberate divergence from the mouse: the Widgets menu reveals a hidden widget floating in the centre, because the player can drag it from there. showWidget puts it back where it was instead - a keyboard user cannot drag a float back into a dock.

Anchoring a float to a corner

A panel that belongs in a corner rather than in a dock - a HUD readout, a diagnostics pane - takes an anchor:

mdw.floatWidget("Connection", { anchor = "topright" })

opts.anchor is "topleft", "topright", "bottomleft", "bottomright" or "center" (the default); opts.margin is the gap from the two edges a corner sits against, mdw.config.floatMargin (10) by default. An unknown anchor is refused with "invalid".

The corner is a corner of the main console area - the window less the visible sidebars, the header, the prompt bar and any chrome bars - not of the window. It is measured exactly as MDW reserves those strips, dockGap included, so both edges of a corner show the same margin. Turn a sidebar off and the same anchor reaches the window edge; add a top bar and everything anchored to the top moves down with it. mdw.floatPos(anchor, w, h, margin) returns those positions on their own, for placing something MDW does not own.

A right-hand anchor keeps mdw.config.mainScrollBarWidth (15) clear as well as the margin: Mudlet draws the main console's scrollbar inside the console's own right edge, so an anchor measured to that edge alone puts the panel underneath it. Left anchors and centring are untouched. Lower it on a profile whose scrollbar is narrower, or hidden.

Two differences from a centred float:

  • No cascade. A centred float steps down-and-left past any float already there so titles stay visible, because a centred reveal has no opinion about where it lands. A caller who named a corner does, so it is placed exactly - and two panels anchored to the same corner sit on top of each other.
  • It moves a float that is already floating. A bare mdw.floatWidget on a lone floating group reports "already" and only raises it; with an anchor, a group sitting somewhere else is exactly the case that has to move.

Anchor once, on your package's first run, rather than every time you reveal the panel. MDW saves a float's x/y in the layout and mdw.showWidget brings a hidden float back where it was, so the player's own arrangement survives every close and reopen - see Layout Persistence.

Sizes and fonts

Function Does
mdw.setDockWidth(side, px) Dock width by value, clamped like the splitter drag (minDockWidth..maxDockWidth); returns the applied width as the third value
mdw.setWidgetHeight(name, px) Height of the widget's dock occupant (its group), clamped like the drag handle; "fill" when the occupant is the auto-filled bottom of a column
mdw.setMainFontSize(size), mdw.setMenuFontSize(size), mdw.setWidgetHeaderFontSize(size), mdw.setPromptFontSize(size), mdw.setWidgetFontSize(name, size) Absolute font sizes (the prompt and per-widget sizes are stored as offsets from the content size, so they follow a later content-size change). Each clamps, applies, saves, and returns the applied size
mdw.setFontFamily(name) The font family every MDW surface renders in (widget consoles, tab bars, the prompt bar, chrome bars, menus). Validated against the fonts Mudlet has loaded: a name it does not know is refused with "invalid" (detail = the name) rather than applied, because Qt would substitute silently while MDW's column arithmetic kept measuring the requested font. Applies in place and is persisted in the layout. No menu offers this - Mudlet cannot tell a monospace family from a proportional one, and MDW's layout math is meaningless in the latter, so a game package exposes it in its own command
mdw.getFontFamily() preferred, effective - the same name twice, unless the preferred font is not loaded and MDW is rendering in the fallback for this session
mdw.getFontSizes() { main, menu, header, prompt, widgets = { [name] = effectiveSize } }

The Font Size menu's +/- rows are thin wrappers over these setters; a setter never opens a menu.

Reading and scrolling

Function Does
mdw.scrollWidget(name, action, lines) "up"/"down" by lines (default 10), "top", "bottom". Per-window scrolling needs Mudlet 4.17+, else "unsupported"
mdw.widgetText(name) The widget's current console text as an array of plain lines (colours dropped, trailing blank lines removed) - for reading a widget aloud or echoing it elsewhere. Returns nil, code on failure

The whole layout

Function Does
mdw.describeLayout() The layout as plain data: { sidebars = { left/right = { visible, width } }, promptBar = { visible, height }, theme, fonts, docks = { left/right = { rows = { { occupants } } } }, floating = { occupant... }, hidden = { { name, title, reason } } }. An occupant is { group, members = { { name, title } }, active, height, fill, visible, docked }; side-by-side columns are flattened into the row's occupant list in column order. reason is closed, group_hidden, or sidebar_hidden
mdw.resetLayout(opts) Delete the saved layout, restore the factory value of every persisted key (mdw.layoutDefaults), and rebuild the UI now. opts.keepGameSettings (default true) preserves mdw.gameSettings; originalMainFontSize and originalMainFont always survive, since they are the uninstall restore values

Rows in the gear menu

The gear dropdown holds MDW's own Rebuild UI and Uninstall. A game package can put its own rows above them - a diagnostics panel, a mode switch, anything belonging to the UI rather than to one widget.

Function Does
mdw.addMenuItem(spec) Add (or replace) one row: { id, label, onClick, checked? }. A known id replaces that row in place, so re-declaring never duplicates or reorders. "ok", "replaced", or "invalid" (no id, or no label)
mdw.removeMenuItem(id) Withdraw one row by hand. "ok" or "unknown_item"
mdw.menuItems() The declared rows in display order, as { id, label, checked, owner } - a copy, with getters resolved. checked is nil for a row that declared none
mdw.addMenuItem({
  id = "connstats",
  label = "Connection Stats",
  checked = function() return mdw.isWidgetShown(mdw.widgets["Connection"]) end,
  onClick = function() mdw.toggleWidget("Connection") end,
})

label and checked may each be a function instead of a value. The gear is rebuilt every time it opens, so a getter is read then - which is how the row above shows a live tick without your package repainting anything. A checked of nil draws no checkbox; false draws an empty one.

Declare your rows from your onReady callback, on every build. Rows declared there are stamped with your package and reaped with it, the same as your widgets.

Your own menu in the header bar

When the choices are a set rather than a single action - combat modes, map layers, channel filters - give them a dropdown of their own next to Font Size and Theme instead of stacking rows in the gear. Your menus follow MDW's own five, so none of those move.

Function Does
mdw.addHeaderMenu(spec) Add (or replace) one dropdown: { id, title, items }. A known id replaces it in place. "ok", "replaced", or "invalid" (no id, no title, or items that is neither a table nor a function)
mdw.removeHeaderMenu(id) Withdraw one menu by hand, button and all. "ok" or "unknown_menu"
mdw.headerMenus() The declared menus in bar order, as { id, title, owner } - a copy
mdw.addHeaderMenu({
  id = "combat",
  title = "Combat",
  items = function() return {
    { label = "Auto-attack", checked = mdw.gameSettings.MyGame.auto,
      keepOpen = true, onClick = function() toggleAuto() end },
    { separator = true },
    { label = "Reset counters", onClick = function() send("reset") end },
  } end,
})

Rows take the same shape as a context menu's: { label, onClick } actions, { separator = true } dividers, checked for a [x]/[ ] box, and keepOpen to re-open the menu after the click so a toggled box redraws.

items may be a function returning the array. It is re-evaluated on every open, so the menu can list what exists right now - your open channels, the current room's exits. A row's label and checked may be getters for the same reason. title is the button's text and must be a plain string: the bar is laid out from its glyph width once, not re-measured per open, so keep it short.

Row kinds

Row Shape
Action { label, onClick, checked?, keepOpen? }. checked nil draws no box, false an empty one; keepOpen re-opens the menu after the click so a toggled box redraws
Split action The same plus onCheck: the CHECKBOX runs onCheck (and always re-opens), the rest of the row runs onClick. A list row whose box and whose text mean different things - a tick beside a title that plays
Inert A row with neither onClick nor onCheck: no pointer cursor, no hover highlight. For captions and hints
Divider { separator = true }
Slider { type = "slider", value, max, step, text, front, back, fgColor, fontSize, textStyle, onChange, onPreview } - the widget slider row's fields, through the same gesture code. Dragged, not clicked; the menu stays open while the pointer is down
Segments { parts = { ... } } - labels, checkboxes and sliders laid left to right, each with its own hit zone

A slider's value should be read from your own state inside the items function, so every open opens at what the game currently holds.

parts makes "Volume [====] [ ] Mute" one row rather than three:

{ parts = {
    { label = "Volume" },
    { type = "slider", flex = true, value = vol, max = 100,
      onChange = function(v) setVolume(v) end },
    { label = "Mute", checked = muted, onCheck = function() toggleMute() end },
  } }

Each part takes the same fields it would as a whole row. One may be flex and takes whatever width the fixed ones leave, never shrinking below 12 glyphs. A segment with no callback is inert, like a row with none.

Re-declaring a menu that is currently OPEN repaints it in place: pass the same id from your own data handler and a checkbox the server confirms a moment after the click catches up on screen, instead of sitting stale until the player reopens the menu.

Declare your menus from your onReady callback, on every build, like your gear rows. They are stamped with your package and reaped with it, and they survive an MDW update without your scripts re-running.

Example

-- "show quests" from your own alias, in your own words
local widget, candidates = mdw.findWidget("que")
if not widget then
  echo(#candidates > 0 and ("Did you mean: " .. table.concat(candidates, ", ") .. "\n")
    or "No such widget.\n")
  return
end
local ok, code, detail = mdw.showWidget(widget.name)
if not ok and code == "sidebar_hidden" then
  echo("The " .. detail .. " sidebar is off.\n")
end

Clone this wiki locally