◆ Alwavelets — The Complete Field Guide

Every key, command, file and service in the dependency-free terminal waveform viewer — from first launch to driving it with an LLM over MCP. Runs anywhere Python 3.8+ and a terminal exist: SSH, tmux, a bare console.

 alwavelets — demo_waveform.vcd · TS:1ns · 0ns‥600ns
              ┬0ns           ┬120ns          ┬240ns         ┬360ns          ┬480ns
top.clk       ▓▓│▓│▓│▓│▓│▓│▓▓│▓│▓│▓│▓│▓│▓▓│▓│▓│▓│▓│▓│▓▓│▓│▓│▓│▓│▓│▓▓│▓│▓│▓│▓│▓│▓▓│▓│▓│▓│▓
top.valid     ▁▁▁▁▁│▔│▁▁▁▁▁▁▁▁▁▁│▔│▁▁▁▁▁▁▁▁▁▁│▔│▁▁▁▁▁▁▁▁▁▁│▔│▁▁▁▁▁▁▁▁▁▁│▔│▁▁▁▁▁▁▁▁▁▁│▔│▁▁
top.ready     ▔▔▔▔▔│▁│▔▔▔▔▔▔▔│▁│▔▔▔▔▔▔▔│▁│▔▔▔▔▔▔▔▔│▁│▔▔▔▔▔▔▔│▁│▔▔▔▔▔▔▔▔│▁│▔▔▔▔▔▔▔│▁│▔▔▔▔▔
top.data_out  XXXXXX╳96═══╳B9════╳62═══╳23════╳17═══╳74════╳94═══╳28════╳77═══╳
top.adc       ⠎⠉⠑⠢⣀⡠⠔⠊⠉⠒⠤⣀⣀⠤⠔⠒⠉⠑⠢⢄⣀⡠⠤⠊⠉⠒⠤⢄⣀⠤⠔⠊⠉⠑⠒⠤⣀⡠⠔⠒⠉⠑⠢⣀⣀⡠⠤⠒⠉⠒⠢⣀ ← braille analog (w×2)
python3 alwavelets.py --demo python3 alwavelets.py trace.vcd python3 alwavelets.py trace.fst python3 alwavelets.py --mcp

Quick start & CLI

Zero dependencies — the launcher is alwavelets.py next to the alwavelets/ package. It parses VCD (streaming, background-loaded, or lazy for huge files) and FST (native pure-Python reader).

FlagEffect
filePath to a .vcd or .fst trace. Files >5 MB background-load (the viewer opens instantly); >50 MB use lazy on-demand chunk loading.
--demoGenerate /tmp/demo_waveform.vcd and open it pre-arranged: nested signal groups (Interface ▸ Handshake / Data bus) and a 3-row analog ADC sine.
--signals, -sComma-separated glob filter applied at load, e.g. -s 'clk,data*,alu.*'.
--syncForce a synchronous parse (no background thread).
--jobs, -j NOpt-in parallel parse with N fork workers for large (>16 MB) synchronous VCD parses: --sync --jobs 8.
--expandedStart with all scopes expanded (default: first-level sub-scopes folded, root-scope signals visible).
--loaduncheckedStart with an empty display; pick signals via the B tree browser.
--no-cacheSkip the persistent parse cache (see Sessions & sidecars).
--no-configIgnore ~/.config/alwavelets/config.json.
--no-sessionIgnore a saved session sidecar and open the trace fresh. --expanded and --loadunchecked imply it, so an explicit layout flag is never overruled by a session.
--mcpRun as an MCP server over stdio instead of the TUI.
--controlExpose a token-authenticated control socket so an authorized local agent can drive this live session — see Agent control.
--list-sessionsPrint the controllable running viewers as JSON, then exit. This is how an agent discovers a session to attach to.
Memory note. Values are bit-packed in RAM: 1 byte per change for 1-bit signals, 2 bits per bit for buses up to 64 bits wide (4-state: 0 1 x z). A bus-heavy 51 MB VCD that used to occupy ~144 MB resident sits at ~42 MB.

Reading the screen

Top to bottom: title bar (file, timescale, signal/change counts) · menu bar with paged key hints (0/9 flip pages, F2 hides it) · time ruler · signal rows · status bar.

The status bar shows the active cursor and its time, ΔAB with its equivalent frequency and the focused signal's transition count between the cursors — e.g. ΔAB=20ns (50MHz · 40t) — the visible window, the signal range, panel focus, and the focused signal's value.

Glyph language: ▔ high · ▁ low · │ edge · ▓ "busy" (transitions denser than columns) · ▚ unknown x · ┄ high-impedance z · ╳══╳ bus segments with value labels · block or braille curves for analog. Press C for the in-app color legend, ? for quick help, F1 for the full 12-page manual.

By default the split panel (scope tree, width 30) is open on the left and Tab switches focus between panel and waves; press s to close it and give the full width to names + waves.

Navigation & zoom

KeyAction
←→ / hlScroll time — or move the active cursor when the cursor is on.
↑↓ / kjMove signal focus up / down (extends the selection in visual mode).
+/= · -Zoom in · zoom out (2× steps).
ZZoom to fit the whole trace. Also the "undo" for a mouse drag-zoom.
WZoom to the A↔B cursor span.
Home / EndJump the window (and cursor A) to the start / end of the trace.
PgUp / PgDnPage through the signal list.
gg · GFirst / last signal in the current scope level (vim-style).
gtGo to time (prompts; accepts raw ticks).
Shift+↑/↓Jump to the previous / next module (scope boundary).
Shift+←/→Collapse / expand the focused signal's scope.
{ · }Collapse all first-level sub-scopes (root signals stay visible) · expand everything.

Cursors, markers & measurement

KeyAction
cHide/show only the selected cursor, keeping its time position. To hide both: 1, c, 2, c.
1 · 2Show and select cursor A · cursor B, leaving the other cursor's visibility unchanged. Left/right move the selected cursor when visible; when hidden, they scroll the view. With both visible, the status bar live-computes ΔAB, frequency (via the trace timescale) and the focused signal's transitions in the span.
SSnap the cursor to the nearest edge of the focused signal.
] · [Jump to next / previous edge of the focused signal.
eEdge-to-edge measure: pick two edges interactively, get the distance.
m · MPlace a marker at the cursor · clear all markers. Empty Enter accepts an unlabeled marker; Esc cancels the label prompt. Both actions are undoable.
F12Open the marker list (also ;markers or Manage markers in the action palette). Times and labels are sorted by time; / filters the list.
</, · >/.Jump to previous · next marker and reveal the active cursor. A brief status message shows the marker number, label, and time; wrapped indicates navigation passed an end of the list.

In the marker list, ↑/↓, PgUp/PgDn, and Home/End select a marker; Enter jumps to it, shows the active cursor, and closes the list. Press F12 again to select another marker in the list. Use r to rename, Delete or d to delete one, m to add at the cursor, and M to clear all. u/Ctrl-R undo/redo edits using the shared editing history. Esc or F12 closes the list. Click a row to select, double-click to jump, or click a control. Renaming to an empty label clears it; Esc cancels. Save edited markers with ;session.

To search markers, press / inside the list, enter a label or time, and press Enter to apply. Esc cancels the filter prompt. Matching ignores case; every space-separated term must appear in the label, displayed time, or raw tick count. The title shows matching/total counts. Ctrl-U in the list resets the filter; an empty query also shows all markers. Filtering preserves the markers themselves: Delete removes the selected match, while M still clears all markers, including those hidden by the filter.

;stats — the deep measurement. Focus a signal, optionally set cursors A and B, then run ;stats: transitions, median rise-to-rise period + frequency, cycle count and duty for 1-bit signals; numeric min/max for buses and reals. Scope is A‥B when both cursors are on, else the visible window.

Managing the signal list

KeyAction
sToggle the split panel (scope tree + per-scope signal list).
BFull-screen scope tree browser: ↑↓ navigate, →/← expand/collapse, Space check a signal or a whole scope in/out of the display, t/T tag signals / load a tag file, B/Esc close.
TabPanel open: switch panel↔waveform focus. Panel closed: fold/unfold the focused group header (does nothing on a plain signal).
VVisual line-select mode; ↑↓ extends. Basis for grouping and bulk removal.
SpaceToggle-highlight the focused signal (scattered multi-select).
J / KMove the focused signal down / up in the list.
d · BackspaceRemove the focused signal · remove the whole selection.
u · Ctrl-RUndo / redo supported edits: removes (single and bulk), moves, finder adds, radix/color/height changes, custom-format settings, analog toggles, and group edits. Also available as ;undo / ;redo.
xExpand a bus into per-bit virtual lanes.
FExpand the focused signal into numeric components, using its chosen radix: sign / exponent / mantissa for floats, or sign / integer / fraction for fixed-point, plus a decoded-value lane. Press again on the parent to collapse.

Groups — collapsible, nestable

Bundle related signals under a named header row that folds with Tab. Groups are an overlay: the canonical signal order is untouched.

# 1. select members
V            # visual mode on the first signal, ↑↓ to extend
# 2. create
;group Handshake             # top-level group
;group Interface/Handshake   # same, nested under "Interface" (auto-created)
# 3. operate
Tab          # on the ▼/▶ header: fold / unfold
;ungroup     # dissolve (focused header, or ;ungroup NAME) — children re-attach to its parent
u            # undo any group edit
  • Headers render as ▼ name (n); nested headers indent by depth and show child counts (2▸).
  • Collapsing a parent hides child headers and their members; collapsing a child leaves the parent open.
  • Nesting cycles are rejected; empty leafless parent nodes are pruned automatically.
  • Groups persist through ;session, saved arrangements, and undo snapshots.

Display & styling

KeyAction
r · RCycle the global radix · apply the next radix to the selection, or the focused signal if nothing is selected. Choices: bin / hex / dec / uns / f16 / bf16 / f32 / f64 / fixed / float.
F10Open signal actions → Number format to choose a radix or configure the two custom slots. Default (global) clears per-signal overrides. Also available by right-clicking a signal label or entering ;signal-menu.
aToggle analog rendering for the focused signal (line graph instead of bits).
HCycle selected/focused signal height 1–4 rows. Multi-row analog gets real vertical resolution.
wWave style: ultra-thin → thin → hi-res. Hi-res doubles horizontal resolution for bits (sub-cell ▀▄▚▞) and draws multi-row analog as a braille curve (2×4 sub-pixels per cell) — see the ADC in the hero above.
vCycle the color of selected/focused signals (6 user colors + default).
LBus value label size: hidden / compact (4 chars) / full.
t · EnterToggle at-cursor value column · full vs short signal names.
iIndent names by scope depth.
( ) · |Shrink/grow the name column · cycle row spacing: Compact → Blank gap → Divider.
T · QRuler in time units vs raw ticks · grid off / vertical / both.
\ · CCycle built-in themes · show the color legend.
DLoad a translation JSON (enum decode: raw value → state name shown on the wave).
IProtocol decode menu: SPI, I²C, UART overlays on the selected signals.
p · `RISC-V register panel · ISA trace query prompt. Registers dock right by default. The panel's top controls select 3 Right / 4 Bottom / 5 Top / 6 Left / 7 Overlay. Columns adapt to terminal size; PgUp/PgDn or the wheel over the panel page through registers when needed. p/Esc closes it.
AAdd a time-anchored annotation (saved to the notes sidecar).
F5Live reload: watch the file and re-parse on change; ;follow instead tails appended changes incrementally and auto-scrolls.

Separating adjacent signals

If the low level of one signal appears to touch the high level of the next, press | once from the default Compact mode to select Blank gap. It leaves one empty terminal row between signals. Press again for Divider, which draws a faint dotted line in that gap; another press returns to Compact.

;spacing blank
;spacing divider
;spacing compact

Blank gap and Divider show fewer signals at once. Their gaps sit below the complete waveform, including tall analog plots and comparison lanes. Q's horizontal-grid option can also draw dotted guides in gaps. Save your choice with ;session; changing workmode resets spacing to Compact.

Older sessions keep their row spacing: the former 0.25/0.5 choices become a full-row dotted divider instead of block characters, while saved two/three-row gaps retain their height.

Custom number formats

Define how a bus encodes a number using two reusable radix slots: fixed for fixed-point values and float for a custom floating-point layout.

The built-in floating-point choices remain available: f16, bf16 (bfloat16), f32, and f64. Choose BF16 in the signal menu or enter ;radix bf16; F honors BF16 when it is selected on a 16-bit bus.

Configure, then apply

  1. Focus a waveform, or select several with Space / V.
  2. Open F10 → Number format → Configure custom fixed… or Configure custom float…. Edit the prefilled settings; Ctrl-U clears the input, Enter accepts, and Esc cancels.
  3. Back in the number-format menu, choose Custom fixed or Custom float to apply that slot to the selected/focused signals. Its label shows the current layout and total width.

;fixed and ;float also open the configuration prompts. With arguments they configure directly, using the syntax below. Configuration alone does not assign the radix: use the menu for selected signals, or ;radix fixed / ;radix float for the global default. Signals with explicit overrides keep them until you choose Default (global).

Two shared slots. Each viewer has one fixed-point definition and one float definition. Editing a slot updates all signals using it in that viewer. Both settings edits and per-signal assignments support u / Ctrl-R undo/redo. The slots also appear in r / R cycling.

Custom fixed-point

;fixed <integer bits> <fraction bits> [signed|unsigned]

Integer bits exclude the sign bit. Signed values use two's complement with a total width of 1 + I + F; unsigned values use I + F. The displayed value is the decoded integer divided by 2^F, with exact decimal output.

SettingMeaning and limits
Integer bits (I)0–1024 bits to the left of the binary point, excluding the optional sign.
Fraction bits (F)0–1024 bits to the right of the binary point; zero gives integer values.
Signednesssigned (the command default) or unsigned.
Total widthMust be 1–1024 bits and match the signal width.
Initial slot7 8 signed: a 16-bit word with 1 sign, 7 integer, and 8 fraction bits.

Example: interpret an 8-bit bus as signed fixed-point with 3 integer and 4 fraction bits:

;fixed 3 4 signed
;radix fixed

The raw word 11101000 represents -24 / 16 = -1.5. With ;fixed 4 4 unsigned, the same 8-bit word represents 232 / 16 = 14.5. Changing the definition refreshes signals already using fixed.

Custom floating-point

;float <exponent bits> <mantissa bits> [bias|auto] [ieee|finite|nan]

Words contain one sign bit, then E exponent bits and M stored mantissa bits: 1 + E + M bits in total. Normal numbers have an implicit leading 1; exponent zero represents signed zero or subnormal values.

SettingMeaning and limits
Exponent bits (E)1–15.
Mantissa bits (M)0–1022, subject to the 1024-bit total limit. Counts stored bits only.
Biasauto (the command default) means 2^(E-1)-1. Alternatively, supply an integer from -65535 to 65535.
Total widthMust be 2–1024 bits and match the signal width.
Initial slot5 10 auto ieee: a 16-bit word with bias 15.

The final argument controls how the maximum exponent is interpreted. Supply a bias or auto before choosing a policy:

PolicyMaximum-exponent behavior
ieee (default)Zero mantissa means infinity; nonzero mantissa means NaN.
finiteAll mantissas represent ordinary finite values; there are no infinity/NaN encodings.
nanOnly the maximum exponent together with the maximum mantissa represents NaN, with either sign. Other patterns are finite. Requires at least one mantissa bit.

Example: an 8-bit float with 4 exponent bits, 3 mantissa bits, and automatic bias 7:

;float 4 3 auto ieee
;radix float

00111100 splits into 0 | 0111 | 100 and displays 1.5 [+|E:7|M:4]. ;float 2 1 auto finite instead defines a 4-bit format whose 0111 word represents 6.

Debugging numeric components

Choose a numeric radix, focus the signal, then press F, choose F10 → Expand components, or enter ;expand-fields. The parent stays visible, followed by its component lanes:

Selected radixLanes
f16, bf16, f32, f64, custom float.sign, .exp (stored exponent), .mant (stored mantissa), .value (decoded number).
Custom fixed.sign (signed only), .integer, .fraction, .value (decoded number).

Fields with zero bits are omitted. Exponent and integer fields start in unsigned decimal; sign, mantissa, and fraction start in binary. Use the number-format menu to change an individual field's display, or x to expand it into bits. The decoded .value lane uses an analog plot.

Fixed-point fields show the raw stored bits. For signed I=3/F=4, 11101000 splits into 1 | 110 | 1000, while .value reads -1.5. The integer field therefore starts at 6 and the fraction field at 1000; their bits form a two's-complement word, not a sign-and-magnitude encoding. Unknown X/Z bits remain visible in their own fields; known fields stay readable.

Press F on the parent again to collapse its children, including any expanded bits. Lanes share the parent history without copying it and follow newly loaded samples. Collapse and expand again after editing a custom definition to rebuild the layout. Component lanes are temporary: expand them again after restoring a session/arrangement or reloading the trace.

For convenience, F also selects f16/f32/f64 on an ordinary 16/32/64-bit bus. An explicitly selected numeric format always takes precedence and must match the signal width.

Width checks, saving, and exports

A width! (need 8b) label means the signal width does not match the configured word width. Adjust the definition or select a matching bus. Short binary strings in a correctly sized VCD signal are zero-extended; unknown X/Z values remain unknown.

;session saves both definitions and the global/per-signal assignments for the next open. Arrangements saved through E → a also contain them. Set startup defaults through custom_radixes in config.json; interactive edits do not rewrite that file.

Text and SVG snapshots using a saved arrangement honor its custom definitions and per-signal assignments. An explicit snapshot radix overrides the assignments while using the arrangement's definition for that slot. Without an arrangement, custom snapshot formats use the initial definitions above.

These formats decode individual words. External block/tensor scaling is separate, and changing a radix does not change analog plots' existing numeric interpretation.

Mouse

GestureAction
ClickPlace the active cursor at that time; focus that row.
DragDrag-to-zoom: hold and sweep a time range — the band inverts live — release to zoom to it. Under 3 columns counts as a click. Z fits back out; drag jumps are in the zoom history.
Right-clickOn a signal label: open signal actions, including Number format. A selected label keeps the selection; an unselected label targets that signal. On the waveform: drop a marker at that time.
Scroll ↑ / ↓Zoom in / out, centered on the pointer.

The ; command line

Press ;, type a command. Unrecognized input falls through to the Tcl engine, so ;puts [signals] works too.

View & navigation

zoom A BSet the window to [A‥B] (ticks).
fitZoom to the whole trace.
goto TMove the cursor to time T.
find PATFocus a signal by name pattern — among those already displayed.
add PAT…Put signals on screen: add top.valid top.ready, add top.alu.*. This is how a script or agent changes which signals are shown; the interactive routes are / and B.
remove PAT… · rmTake signals off screen. Both are undoable with u.
grid · names · treeCycle grid · full/short names · open the tree browser.
spacing [compact|blank|divider]Select row separation; omit the argument to cycle, as with |.

Analysis

when EXPR / matchJump to where a boolean expression over signals becomes true: when valid && state==3 && addr>=0x40. Operators && || ! == != < <= > >=, literals decimal/0x/0b/x/z.
glitchesJump to the next x-hazard or zero-width/runt pulse.
derive N=ECreate a virtual signal from an expression: derive bus=(a1<<1)|a0.
stats / measureOne-line measurement of the focused signal over A‥B or the window (period, frequency, duty, min/max…).
formal / cexFormal counter-example analyzer: finds assertion-style signals and marks the failure time.
powerSwitching-activity (power) heatmap estimate.
axi · wb · ahbDecode AXI / Wishbone / AHB bus transactions.
trace · queryBuild the RISC-V instruction trace · query it.
diffToggle comparison against a second trace: differing regions get a second row + highlighted ranges.

Layout, persistence & misc

group NAME · group P/C · ungroup [NAME]Group the selection (nested with a path) · dissolve.
session [save] · session load · session clearWhole-session sidecar — see Sessions.
loadgtkw F · savegtkw FGTKWave .gtkw interop: apply / write a save file (signal list + radixes).
radix R · theme TSet global radix · switch theme by name or load a theme JSON path.
fixed I F [signed|unsigned]Configure the custom fixed-point slot; integer bits exclude sign. Use radix fixed to select it globally.
float E M [bias|auto] [ieee|finite|nan]Configure the custom float slot; use radix float to select it globally.
expand-fieldsExpand/collapse the focused signal's fixed-point or float components, as with F.
signal-menu · undo · redoOpen signal actions · undo a supported edit · redo it.
decode FLoad a decode/translation JSON file.
followLive-tail a growing dump (running simulation) with auto-scroll.
bookmark/bm · gb · bookmarks · savebmCreate / jump to / list / save bookmarks.
annotate · annotations · saveann · hideannTime-anchored notes: add / list / save / hide.
tags · autotag · savetagsISA-role signal tags: list / auto-detect / save.
exportOpen the export menu (same as E).
tcl · eval CMDOpen the Tcl prompt · run one Tcl command.
manual/man · help · quit/qManual · command list · exit.

Tcl scripting :

Press : for a small GTKWave-flavoured Tcl engine — enough to script view setup and batch measurements. Commands:

add_signal <glob>      remove_signal <glob>    signals [glob]
zoom <a> <b>           zoom_fit                goto <t>
cursor <t>             marker <t> [label]      radix <r> [signal]
analog <signal>        expand <bus>            translate <file>
measure <signal>       export_csv <file>       screenshot <file>
theme <name>           set / puts / foreach    # variables & loops

Example — arrange a debug view in one line: :foreach s [signals cpu.*] { add_signal $s }

Scripts can also configure the two custom slots with fixed I F [signed|unsigned] and float E M [bias|auto] [ieee|finite|nan], then select one with radix fixed or radix float.

Sessions & sidecar files

Everything persistent lives in small JSON sidecars next to the trace (never inside it), plus one global cache and one global config.

FileWhat it holdsWritten by
<trace>.alwavelets_session.jsonThe whole session: display order, groups (incl. nesting), analog flags, global + per-signal radixes, custom fixed/float definitions, colors, heights, markers, zoom window, cursors, name width, spacing, grid, wave style, theme. Auto-restores on the next open of that trace.;session
<trace>.alwavelets_bookmarks.jsonNamed time bookmarks.;savebm
<trace>.alwavelets_notes.jsonTime-anchored annotations.;saveann / A
arrangement .jsonPortable layout (order, groups, global/per-signal radixes, custom fixed/float definitions, markers) at a path you choose — reusable across traces with matching signal identities.E → a / load with l
*.gtkwGTKWave save file (signal list + radixes) for interop both ways.;savegtkw
~/.cache/alwavelets/*.wlcBinary parse cache validated against file size + mtime — an unchanged 51 MB VCD reopens in ~0.15 s. Disable per-run with --no-cache.automatic
~/.config/alwavelets/config.jsonStartup defaults + key remaps — see below.you
Session flow. Arrange your debug view once, run ;session, quit. Next alwavelets.py trace.vcd reopens exactly where you left off — the status bar confirms "Session restored". ;session clear forgets it; stale entries (renamed signals) are silently dropped rather than failing.

Themes & configuration

Themes

\ cycles built-ins (dark, light, phosphor, …); ;theme <name|file.json> loads by name or file. A theme JSON maps ~30 roles (HIGH, LOW, EDGE, BUS, CURSOR, MENU, …) to colors:

{
  "name": "Nord",
  "HIGH":   ["#A3BE8C", -1],
  "BUS":    ["#88C0D0", -1],
  "CURSOR": ["#2E3440", "#88C0D0"],
  "SELECT": ["#2E3440", "#A3BE8C"]
}

Colors are xterm-256 indices or #rrggbb. Hex resolves at the best fidelity the terminal offers: exact RGB on direct-color terminfo → redefined palette slots where the palette is writable → nearest-256 quantization. theme_nord.json ships as a full true-color example; theme_solarized.json as an indexed one.

config.json

Optional, at ~/.config/alwavelets/config.json (or $XDG_CONFIG_HOME). Every key optional; malformed files never block startup.

{
  "theme": "phosphor",
  "radix": "hex",
  "custom_radixes": {
    "fixed": {
      "integer_bits": 7,
      "fraction_bits": 8,
      "signed": true
    },
    "float": {
      "exponent_bits": 5,
      "mantissa_bits": 10,
      "bias": null,
      "specials": "ieee"
    }
  },
  "wave_style": 2,
  "menu_bar": false,
  "grid": 1,
  "name_width": 28,
  "keys": {
    "quit": "Q",
    "zoom_in": "=",
    "toggle_cursor": " "
  }
}

Action names are the handler names — quit, zoom_in, next_edge, toggle_panel, … (any _kb_* with the prefix dropped).

bias: null selects automatic bias. Set radix to fixed or float to use a custom slot globally at startup. A saved session restores its own definitions and assignments. Invalid optional custom definitions are ignored; valid settings still load.

Export & graphics

OutputHowNotes
CSVE→cTime/value table of the visible window.
SVGX or E→vVector snapshot of the current view; theme-aware.
PDFE→pSingle-page PDF, hand-written writer (still zero deps).
Text screenshotE→sThe screen as plain Unicode text.
Pixel imageE→iWaveform rasterized to PNG and shown inline in the terminal on kitty / iTerm2 / sixel; falls back to saving the PNG.
ArrangementE→a/l/tSave / load layout · load theme.

MCP server — connect an LLM service

Alwavelets speaks the Model Context Protocol natively (stdio, newline-delimited JSON-RPC 2.0, protocol 2024-11-05) — implemented in the stdlib, no SDK. Any MCP client (Claude Code, Claude Desktop, agent frameworks) can open traces and interrogate them through seven tools. Parsed traces are cached per file+mtime, so repeated queries are instant.

Hook-up

Claude Code (one line):

claude mcp add alwavelets -- \
  python3 /path/to/alwavelets.py --mcp

Claude Desktop — claude_desktop_config.json:

{
  "mcpServers": {
    "alwavelets": {
      "command": "python3",
      "args": ["/path/to/alwavelets.py", "--mcp"]
    }
  }
}

Tool reference

Common conventions: file is a VCD/FST path (required everywhere); signals is a list of names or globs (top.alu.*); start/end accept ticks or unit strings like "500ns", "1.5us" and default to the trace extent; arrangement points at a saved arrangement JSON to supply the signal list + radixes.

Accepted radix names: bin, hex, dec, unsigned (or uns), f16, bf16, f32, f64, fixed, and float. For custom text/SVG labels, provide an arrangement containing your slot definitions.

render_waveform

Unicode-text waveform of the given signals over a window — directly readable by the model.

widthcharacter width, default 100
radixAny radix listed above; default hex.

render_waveform_svg

Same snapshot as inline SVG image content (vision-capable clients render it).

widthpixel width, default 900
themedark | light | phosphor | theme-JSON path

render_waveform_png

Pixel-accurate PNG raster of the window, inline image content.

widthpixel width, default 900
radixAny radix listed above; default hex.

list_signals

Discover signal full-names, optionally glob-filtered.

patternglob, default *
limitmax names, default 200

measure

Per-signal stats over a window — transition count, period, frequency — plus the edge-to-edge delta between the first two signals.

signalsrequired

find_when

Times where a boolean expression becomes true: "valid && state==3 && addr>=0x40". Operators && || ! == != < <= > >=, parentheses; literals decimal/0x/0b/x/z.

exprrequired
limitmax matches, default 50

find_glitches

Anomaly sweep: transitions into x and zero-width / runt pulses, across all or chosen signals.

max_pulserunt threshold in ticks; default auto per signal (~⅓ detected period)

On the wire

One JSON-RPC message per line on stdin/stdout. A complete exchange:

→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}
← {"jsonrpc":"2.0","id":1,"result":{"serverInfo":{"name":"alwavelets","version":"0.1.0"}, …}}
→ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
      "name":"find_when",
      "arguments":{"file":"bench.vcd","expr":"fifo_full && wr_en","limit":5}}}
← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text",
      "text":"3 time(s) where 'fifo_full && wr_en' becomes true:\n  1.24us\t(t=1240) …"}],
      "isError":false}}

Tool failures come back as isError: true content (not protocol errors), so the model can read the message and self-correct — e.g. a mistyped signal name.

What to ask the model

  • "Open bench.vcd, list the FIFO signals, and find when fifo_full && wr_en first happens" — chains list_signals → find_when.
  • "Show me 200 ns around the first overflow" — find_when → render_waveform with a computed window.
  • "Any glitches or x-propagation in the ALU?" — find_glitches with signals: ["top.alu.*"].
  • "Is the core clock really 100 MHz across the whole run?" — measure on clk.

Live-viewer tools drives your screen

The seven tools above are headless — they parse a trace of their own and know nothing about what you have open. These five attach to a running viewer started with --control, so an MCP client can drive the session in front of you with no extra plumbing. They're a thin proxy over the same socket documented in Agent control — one implementation, two transports.

list_viewer_sessions

Which running viewers can be driven. Call it first when the user talks about "my screen", or when another viewer_* tool reports several open.

viewer_state

Read-only: window, cursors, focused/displayed signals, groups, markers, radix. Returns a compact summary rather than raw JSON.

viewer_render

Read-only: the current view as Unicode waveforms — literally what the user is looking at. Use it to answer screen questions and to confirm a change landed.

viewer_command

Runs one ; command and returns the new state. when valid && ready, zoom, group A/B, stats, glitches… The acting tool.

viewer_key

Keystrokes for key-only actions: x expand a bus, a analog, w wave style, tab fold a group, u undo.

All five take an optional session (a pid or trace filename). With one viewer open it's inferred; with several the tool refuses to guess and lists the choices — silently steering the wrong window would be the worst failure mode here. With none open, the error tells the agent to ask you to start one with --control.

Which family to use. Analyzing a file nobody has open → the headless tools. The user is looking at something and describes what they want done → the viewer_* tools.

Agent control of a live session service

The direct socket protocol behind the viewer_* MCP tools above — use this when your agent isn't an MCP client, or wants a persistent connection. Start a viewer with --control and an authorized local agent can drive that exact session while you watch — group signals, jump the cursor to where an expression holds, zoom, restyle — and read back both the view state and the waveform on your screen. Built for exactly this: describe what you want to your agent, watch your own viewer do it.

Turning it on

python3 alwavelets.py bench.vcd --control     # your terminal / tmux pane
python3 alwavelets.py --list-sessions         # how an agent finds it

Nothing listens unless you pass --control. The status bar then shows who's driving: ◇ctl armed but idle · ◆agent:1 connected · ◆AGENT acted within the last two seconds.

Protocol

Each session publishes a 0600 descriptor (socket path + per-session token) under $XDG_RUNTIME_DIR/alwavelets/sessions/<pid>.json. The agent connects to the Unix socket and exchanges one JSON object per line.

OpArgumentsReturns
ping—protocol version, trace, signal count
capabilities—ops + the live list of ; verbs, generated from the viewer's own dispatch table so it can't drift
state—window, cursors + Δ, focused/displayed/selected signals, groups, markers, radix, last status message
renderwidth 20–400the current view as Unicode waveforms — what the user is seeing
commandtextruns one ; command; returns its message and fresh state
keykeys ≤64feeds keystrokes for key-only actions (x, a, w, tab…)

The first four never modify the view. command takes exactly what you'd type at the ; prompt, so the agent's vocabulary is the same one documented above — nothing is duplicated, nothing can drift. Commands that would open an interactive prompt are no-ops over the channel rather than freezing the viewer for a keyboard nobody is at.

Python client

from alwavelets.control import list_sessions, ControlClient

session = next(s for s in list_sessions() if not s["stale"])
with ControlClient(session) as c:
    c.command("group Interface/Handshake")   # organize the view
    r = c.command("when valid && ready")      # cursor lands on the match
    t = r["state"]["cursor"]["a"]
    c.command(f"zoom {t-200} {t+200}")        # frame it
    print(c.render(width=100)["text"])        # see what the user sees
The contract lives in SKILL.md at the project root — frontmatter plus protocol, ops, command vocabulary, state schema, working method and worked recipes. Point your agent at that file rather than re-describing the API.
Trust boundary. Opt-in only; socket and descriptor are 0600 so just your account can reach them, and the token stops an unrelated process that merely guesses the path. Commands run with your authority — a few write files (session, savegtkw, Tcl export_csv). command: "quit" closes your viewer. The ◆AGENT indicator exists so you can always see when something else is steering.

Python API

The same snapshot machinery the MCP server uses is importable directly — handy in scripts, notebooks and CI checks.

Two lines in a REPL

The quickest way to show what this does — no curses terminal, no file needed. demo() generates the built-in trace (cached after the first call), prints it, and hands back the parsed VCDFile so the session can continue:

>>> import alwavelets
>>> vcd = alwavelets.demo()
waveform  [0ns .. 300ns]
              ┬0ns          ┬60ns          ┬120ns         ┬180ns         ┬240ns
top.clk       ▁││▁││││▁││││▁││││▁│││││▔││││▔││││▔│││││▁││││▁││││▁││││▁│││││▔││
top.valid     ▁▁▁▁▁▁▁▁▁▁│▔│▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁│▔│▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁│▔▔│
top.data_out  XXXXXXXX░░░╳96═════════╳B9══════════╳62═════════╳23═════════╳17═

>>> alwavelets.demo(signals=["top.adc"], end="1us")   # pick signals / window
>>> alwavelets.measure(vcd, ["top.clk"])              # keep going with the trace

Full surface

import alwavelets

vcd = alwavelets.parse_file("trace.vcd")                       # VCD or FST
print(alwavelets.render_text(vcd, ["top.clk", "top.data*"],
                          "0ns", "2us", width=100))          # Unicode snapshot
svg = alwavelets.render_svg(vcd, ["top.*"], None, None)         # vector snapshot
stats = alwavelets.measure(vcd, ["top.clk", "top.valid"])       # period/freq/edges
names = alwavelets.list_signals(vcd, "cpu.*")                   # discovery

sig = vcd.signals[names and vcd.signal_order[0]]             # raw access:
sig.value_at(1500); sig.changes_in_window(0, 2000)           # O(log n) reads

Values are 4-state throughout (0 1 x z); buses return full-width bit strings, reals return numeric strings.

Guided demo tour — five minutes

#Do thisYou learn
1python3 alwavelets.py --demo, then sLaunch; close the split panel to see names, group headers and waves full-width.
2Tab on ▼ InterfaceNested groups: the parent folds Handshake and Data bus away in one keystroke.
3/ then type adc, EnterThe fuzzy finder jumps you to the ADC.
4w wHi-res style: the 3-row ADC becomes a smooth braille sine curve.
5drag across two sine periodsDrag-to-zoom; Z fits back out.
61, ], 2, ] on clk, then ;statsCursor measurement: ΔAB + frequency in the status bar, full stats on demand.
7;when valid && readyExpression search lands the cursor on the first handshake.
8;session, quit, reopenThe whole arrangement comes back on its own.

Register panel demo

python3 alwavelets.py demo_register_panel.vcd --expanded --no-session

The panel docks right by default. While it is open, click a placement command at its top or press 3 Right, 4 Bottom, 5 Top, 6 Left, or 7 Overlay. Narrow panels use arrows for dock directions. Docks reserve waveform space so cursors remain unobstructed; overlay covers the waveform. Moving or closing the panel preserves cursor times and the displayed time range. Sessions remember the chosen position.

Press p, then type ;goto 6500ns and press Enter. This synthetic RV32 trace supplies automatically detected instruction, PC, writeback, clock and hart signals. The panel reconstructs earlier writes and displays hexadecimal values for the hart at the cursor.

Cursor commandObserve
;goto 0nsx0 is zero; registers without a write show --------.
;goto 6200nsAll registers have values; hart 1 has x1=00000201 through x31=0000021f.
;goto 6500nsHart 0 adds a0=5 and a1=7 into a2=0000000c. Read the PC and instruction below the grid.
;goto 6600ns, then ;goto 6700nsIdentical consecutive instructions increment a0 to 6, then 7.
;goto 6800nsAn idle cycle preserves all register values and the last retired instruction.
;goto 7100nsRead full-width patterns: t0=ffffffff, t1=80000000, t2=7fffffff.
;goto 7200nsA nop retires without changing registers; x0 stays zero.
;goto 7500nsHart 1 has a0=100, a1=200 and a2=300 (hex 64, c8, 12c).
;goto 7600nsHart 0 retains a0=7 and a2=12; its new a3 is 19 (00000013).
;goto 7700nsHart 1 retains its own values and writes a3=400 (00000190).

New writes are highlighted at retirement. Resize the terminal to try adaptive columns; use PgUp/PgDn when the registers need multiple pages. Close with p or Esc. Regenerate the capture with python3 tools/generate_register_demo.py.