◆ 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.
┬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).
| Flag | Effect |
|---|---|
file | Path to a .vcd or .fst trace. Files >5 MB background-load (the viewer opens instantly); >50 MB use lazy on-demand chunk loading. |
--demo | Generate /tmp/demo_waveform.vcd and open it pre-arranged: nested signal groups (Interface ▸ Handshake / Data bus) and a 3-row analog ADC sine. |
--signals, -s | Comma-separated glob filter applied at load, e.g. -s 'clk,data*,alu.*'. |
--sync | Force a synchronous parse (no background thread). |
--jobs, -j N | Opt-in parallel parse with N fork workers for large (>16 MB) synchronous VCD parses: --sync --jobs 8. |
--expanded | Start with all scopes expanded (default: first-level sub-scopes folded, root-scope signals visible). |
--loadunchecked | Start with an empty display; pick signals via the B tree browser. |
--no-cache | Skip the persistent parse cache (see Sessions & sidecars). |
--no-config | Ignore ~/.config/alwavelets/config.json. |
--no-session | Ignore 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. |
--mcp | Run as an MCP server over stdio instead of the TUI. |
--control | Expose a token-authenticated control socket so an authorized local agent can drive this live session — see Agent control. |
--list-sessions | Print the controllable running viewers as JSON, then exit. This is how an agent discovers a session to attach to. |
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.
Cursors, markers & measurement
| Key | Action |
|---|---|
| c | Hide/show only the selected cursor, keeping its time position. To hide both: 1, c, 2, c. |
| 1 · 2 | Show 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. |
| S | Snap the cursor to the nearest edge of the focused signal. |
| ] · [ | Jump to next / previous edge of the focused signal. |
| e | Edge-to-edge measure: pick two edges interactively, get the distance. |
| m · M | Place a marker at the cursor · clear all markers. Empty Enter accepts an unlabeled marker; Esc cancels the label prompt. Both actions are undoable. |
| F12 | Open 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: 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.Search: the fuzzy finder & friends
/ opens an fzf-style overlay that filters every signal in the file live — not just the displayed ones. It is also "go to signal": an already-displayed match is jumped to instead of re-added.
| Inside the finder | |
|---|---|
| type | Filter live. Space separates AND-terms; each term matches as a substring or a scattered subsequence — cwe finds cpu.wr_en. Word-boundary and leaf-name matches rank first; shorter names win ties. |
| ↑↓ / Ctrl+P/N | Move the highlight. |
| Tab | Mark for multi-select (▪), advance. |
| Enter | Add marked/highlighted signals, jump to the first (unfolds its group if collapsed), close. Seeds n. |
| Ctrl+U · Ctrl+W | Clear query · delete last word. |
| Esc | Cancel without changes. |
| Other search keys | |
|---|---|
| n | Cycle to the next displayed match of the last finder query. |
| f | Find a value on the focused signal (e.g. 3F, 0x3F) — cursor jumps to the next occurrence, wrapping. |
| P | Pattern trigger across signals: clk=1 & enable=1 & state=0x3 — cursor jumps to the next time all hold. |
;when EXPR | Boolean expression search with full operators: valid && state==3 && addr>=0x40. |
;glitches | Jump to the next x-hazard / zero-width / runt pulse. |
Finder performance: re-ranking 50 000 names takes ~15 ms, and extending a query only re-ranks the previous matches — filtering is effectively instant per keystroke.
Managing the signal list
| Key | Action |
|---|---|
| s | Toggle the split panel (scope tree + per-scope signal list). |
| B | Full-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. |
| Tab | Panel open: switch panel↔waveform focus. Panel closed: fold/unfold the focused group header (does nothing on a plain signal). |
| V | Visual line-select mode; ↑↓ extends. Basis for grouping and bulk removal. |
| Space | Toggle-highlight the focused signal (scattered multi-select). |
| J / K | Move the focused signal down / up in the list. |
| d · Backspace | Remove the focused signal · remove the whole selection. |
| u · Ctrl-R | Undo / 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. |
| x | Expand a bus into per-bit virtual lanes. |
| F | Expand 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
| Key | Action |
|---|---|
| r · R | Cycle 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. |
| F10 | Open 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. |
| a | Toggle analog rendering for the focused signal (line graph instead of bits). |
| H | Cycle selected/focused signal height 1–4 rows. Multi-row analog gets real vertical resolution. |
| w | Wave 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. |
| v | Cycle the color of selected/focused signals (6 user colors + default). |
| L | Bus value label size: hidden / compact (4 chars) / full. |
| t · Enter | Toggle at-cursor value column · full vs short signal names. |
| i | Indent names by scope depth. |
| ( ) · | | Shrink/grow the name column · cycle row spacing: Compact → Blank gap → Divider. |
| T · Q | Ruler in time units vs raw ticks · grid off / vertical / both. |
| \ · C | Cycle built-in themes · show the color legend. |
| D | Load a translation JSON (enum decode: raw value → state name shown on the wave). |
| I | Protocol 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. |
| A | Add a time-anchored annotation (saved to the notes sidecar). |
| F5 | Live 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
- Focus a waveform, or select several with Space / V.
- 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.
- 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).
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.
| Setting | Meaning 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. |
| Signedness | signed (the command default) or unsigned. |
| Total width | Must be 1–1024 bits and match the signal width. |
| Initial slot | 7 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.
| Setting | Meaning and limits |
|---|---|
Exponent bits (E) | 1–15. |
Mantissa bits (M) | 0–1022, subject to the 1024-bit total limit. Counts stored bits only. |
| Bias | auto (the command default) means 2^(E-1)-1. Alternatively, supply an integer from -65535 to 65535. |
| Total width | Must be 2–1024 bits and match the signal width. |
| Initial slot | 5 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:
| Policy | Maximum-exponent behavior |
|---|---|
ieee (default) | Zero mantissa means infinity; nonzero mantissa means NaN. |
finite | All mantissas represent ordinary finite values; there are no infinity/NaN encodings. |
nan | Only 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 radix | Lanes |
|---|---|
| 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
| Gesture | Action |
|---|---|
| Click | Place the active cursor at that time; focus that row. |
| Drag | Drag-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-click | On 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 B | Set the window to [A‥B] (ticks). |
fit | Zoom to the whole trace. |
goto T | Move the cursor to time T. |
find PAT | Focus 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… · rm | Take signals off screen. Both are undoable with u. |
grid · names · tree | Cycle 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 / match | Jump to where a boolean expression over signals becomes true: when valid && state==3 && addr>=0x40. Operators && || ! == != < <= > >=, literals decimal/0x/0b/x/z. |
glitches | Jump to the next x-hazard or zero-width/runt pulse. |
derive N=E | Create a virtual signal from an expression: derive bus=(a1<<1)|a0. |
stats / measure | One-line measurement of the focused signal over A‥B or the window (period, frequency, duty, min/max…). |
formal / cex | Formal counter-example analyzer: finds assertion-style signals and marks the failure time. |
power | Switching-activity (power) heatmap estimate. |
axi · wb · ahb | Decode AXI / Wishbone / AHB bus transactions. |
trace · query | Build the RISC-V instruction trace · query it. |
diff | Toggle 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 clear | Whole-session sidecar — see Sessions. |
loadgtkw F · savegtkw F | GTKWave .gtkw interop: apply / write a save file (signal list + radixes). |
radix R · theme T | Set 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-fields | Expand/collapse the focused signal's fixed-point or float components, as with F. |
signal-menu · undo · redo | Open signal actions · undo a supported edit · redo it. |
decode F | Load a decode/translation JSON file. |
follow | Live-tail a growing dump (running simulation) with auto-scroll. |
bookmark/bm · gb · bookmarks · savebm | Create / jump to / list / save bookmarks. |
annotate · annotations · saveann · hideann | Time-anchored notes: add / list / save / hide. |
tags · autotag · savetags | ISA-role signal tags: list / auto-detect / save. |
export | Open the export menu (same as E). |
tcl · eval CMD | Open the Tcl prompt · run one Tcl command. |
manual/man · help · quit/q | Manual · 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.
| File | What it holds | Written by |
|---|---|---|
<trace>.alwavelets_session.json | The 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.json | Named time bookmarks. | ;savebm |
<trace>.alwavelets_notes.json | Time-anchored annotations. | ;saveann / A |
arrangement .json | Portable 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 |
*.gtkw | GTKWave save file (signal list + radixes) for interop both ways. | ;savegtkw |
~/.cache/alwavelets/*.wlc | Binary 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.json | Startup defaults + key remaps — see below. | you |
;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
| Output | How | Notes |
|---|---|---|
| CSV | E→c | Time/value table of the visible window. |
| SVG | X or E→v | Vector snapshot of the current view; theme-aware. |
E→p | Single-page PDF, hand-written writer (still zero deps). | |
| Text screenshot | E→s | The screen as plain Unicode text. |
| Pixel image | E→i | Waveform rasterized to PNG and shown inline in the terminal on kitty / iTerm2 / sixel; falls back to saving the PNG. |
| Arrangement | E→a/l/t | Save / 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.
width | character width, default 100 |
radix | Any radix listed above; default hex. |
render_waveform_svg
Same snapshot as inline SVG image content (vision-capable clients render it).
width | pixel width, default 900 |
theme | dark | light | phosphor | theme-JSON path |
render_waveform_png
Pixel-accurate PNG raster of the window, inline image content.
width | pixel width, default 900 |
radix | Any radix listed above; default hex. |
list_signals
Discover signal full-names, optionally glob-filtered.
pattern | glob, default * |
limit | max 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.
signals | required |
find_when
Times where a boolean expression becomes true: "valid && state==3 && addr>=0x40". Operators && || ! == != < <= > >=, parentheses; literals decimal/0x/0b/x/z.
expr | required |
limit | max matches, default 50 |
find_glitches
Anomaly sweep: transitions into x and zero-width / runt pulses, across all or chosen signals.
max_pulse | runt 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 whenfifo_full && wr_enfirst happens" — chainslist_signals → find_when. - "Show me 200 ns around the first overflow" —
find_when → render_waveformwith a computed window. - "Any glitches or x-propagation in the ALU?" —
find_glitcheswithsignals: ["top.alu.*"]. - "Is the core clock really 100 MHz across the whole run?" —
measureonclk.
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.
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.
| Op | Arguments | Returns |
|---|---|---|
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 |
render | width 20–400 | the current view as Unicode waveforms — what the user is seeing |
command | text | runs one ; command; returns its message and fresh state |
key | keys ≤64 | feeds 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
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.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 this | You learn |
|---|---|---|
| 1 | python3 alwavelets.py --demo, then s | Launch; close the split panel to see names, group headers and waves full-width. |
| 2 | Tab on ▼ Interface | Nested groups: the parent folds Handshake and Data bus away in one keystroke. |
| 3 | / then type adc, Enter | The fuzzy finder jumps you to the ADC. |
| 4 | w w | Hi-res style: the 3-row ADC becomes a smooth braille sine curve. |
| 5 | drag across two sine periods | Drag-to-zoom; Z fits back out. |
| 6 | 1, ], 2, ] on clk, then ;stats | Cursor measurement: ΔAB + frequency in the status bar, full stats on demand. |
| 7 | ;when valid && ready | Expression search lands the cursor on the first handshake. |
| 8 | ;session, quit, reopen | The 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 command | Observe |
|---|---|
;goto 0ns | x0 is zero; registers without a write show --------. |
;goto 6200ns | All registers have values; hart 1 has x1=00000201 through x31=0000021f. |
;goto 6500ns | Hart 0 adds a0=5 and a1=7 into a2=0000000c. Read the PC and instruction below the grid. |
;goto 6600ns, then ;goto 6700ns | Identical consecutive instructions increment a0 to 6, then 7. |
;goto 6800ns | An idle cycle preserves all register values and the last retired instruction. |
;goto 7100ns | Read full-width patterns: t0=ffffffff, t1=80000000, t2=7fffffff. |
;goto 7200ns | A nop retires without changing registers; x0 stays zero. |
;goto 7500ns | Hart 1 has a0=100, a1=200 and a2=300 (hex 64, c8, 12c). |
;goto 7600ns | Hart 0 retains a0=7 and a2=12; its new a3 is 19 (00000013). |
;goto 7700ns | Hart 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.