Skip to content

Scripted Control

MorquinDevlar edited this page Sep 4, 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. 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.

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.

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