Appendix A

May 15, 2026 · View on GitHub

Three tools are built into every zeichenwerk app and worth knowing the moment you hit a layout you don't understand.

.Debug() — turn on the debug bar

Call .Debug() on the *UI before Run():

ui := NewBuilder(themes.TokyoNight()).
    /* … */ .
    Build()

ui.Debug()
ui.Run()

This adds a one-line debug bar at the bottom showing:

  • Current focused widget (ID and type)
  • Hovered widget under the mouse
  • Mouse coordinates
  • Last event key
  • Redraw / refresh counters (handy for spotting layout thrash)

The bar updates live; nothing else changes about your UI.

Ctrl-D — the inspector

With .Debug() enabled, Ctrl-D opens an inspector popup over the live UI. It's a tree view of every widget in your app:

$ ╭─ \text{Inspector} ──────────────────────────────╮ │ ▼ \text{ui} │ │ ▼ \text{root} (\text{VFlex}) 24 \times 80 \text{bg}=$\text{bg0} │ │ ▶ \text{header} (\text{HFlex}) 1 \times 80 │ │ ▼ \text{body} (\text{Grid}) 20 \times 80 │ │ ▶ \text{tables} (\text{List}) 20 \times 24 │ │ ▼ \text{sql} (\text{Editor}) 10 \times 56 \text{focused} │ │ … │ ╰───────────────────────────────────────────╯ $

You can:

  • Expand / collapse nodes to focus on one subtree.
  • See bounds for every widget — perfect for spotting "why is this 0 cells wide?"
  • See the resolved style (foreground, background, border, padding) for any selected widget.
  • See the active flags (focused, hovered, focusable, hidden, …).

Press Esc to close the inspector and return to the app.

Logging

Every widget can call Log(self, level, message, kvs...):

editor.Log(editor, core.Debug, "running query", "sql", q)

Logs land in a rotating buffer (*widgets.TableLog) the UI keeps internally. To see them:

  • They're rendered live in the debug bar's "last log" entry when .Debug() is enabled.
  • ui.Logs() returns the *TableLog. You can drop it into your UI yourself with Builder.Add(ui.Logs()) to get a permanent log pane.
  • ui.SetLogLevel(slog.LevelDebug) changes the threshold at runtime.

Levels in core: Debug, Info, Warning, Error, Fatal.

Dump

Sometimes you want the widget tree as text — for diffing, for a bug report, for git. *UI has a Dump method:

ui.SetBounds(0, 0, 120, 40)
ui.Layout()
ui.Dump(os.Stdout, DumpOptions{Style: true})

You'll get an indented tree with bounds, types, and (with Style: true) the resolved styles for every node. The showcase command-line uses exactly this approach for its --dump / --dump-verbose flags — see cmd/showcase/main.go.

Common pitfalls and what they look like

SymptomLikely causeFix
Widget doesn't appearForgotten End() — it landed inside a previous containerCount End()s; open inspector to confirm where it is.
Whole panel won't grow with the terminalNo fractional track on that axisGive one column / row -1.
MustFind panicsWrong ID or wrong typeSearch source: grep '"name"'. Or untyped core.Find and check for nil.
Tab skips a widget you want focusableCustom widget didn't set FlagFocusable, or FlagSkip is onwidget.SetFlag(core.FlagFocusable, true) in your constructor; clear FlagSkip.
OnChange handler never firesEvent data is the wrong type (e.g. Checkbox sends bool, not string)Use raw widget.On(EvtChange, …) and assert data[0].(bool).
Theme variable shows up as literal $cyanRenderer used directly without theme wrappingAlways render through core.Renderer (you do automatically inside Render).

When to ask the framework

Most "weird layout" questions are answered by the inspector in five seconds. Make .Debug() + Ctrl-D your first move; the rest follows.