원클릭으로
ztrackerprime
zTracker Prime (m6502/ztrackerprime) contributor skill — orienting context, key files, and pointers to deeper reference material.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
zTracker Prime (m6502/ztrackerprime) contributor skill — orienting context, key files, and pointers to deeper reference material.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | ztrackerprime |
| description | zTracker Prime (m6502/ztrackerprime) contributor skill — orienting context, key files, and pointers to deeper reference material. |
| domain | repository-specific |
| source_repo | https://github.com/m6502/ztrackerprime |
| source_platform | github |
| tags | ["cpp","sdl3","tracker","midi","cross-platform"] |
| triggers | {"keywords":{"primary":["ztracker","ztrackerprime","zt","zTracker"],"secondary":["m6502","tracker","MIDI tracker","impulse tracker","SDL3 tracker"]}} |
Last verified: 2026-06-15. If this date is more than ~7 days old when you load this skill, your first move is to check what merged since:
gh pr list --repo m6502/ztrackerprime --state merged --json number,title,mergedAt --jq '[.[] | select(.mergedAt > "<this date>")]'. Reconcile any architecture / shortcut / invariant claims below against currentmasterbefore acting on them. Then bump this date in the same PR that fixes the drift.What lives elsewhere on purpose: the open-PR list and merged landmarks are NOT in this skill — they go stale fast. For current state run
gh pr list --repo m6502/ztrackerprime(open) orgh pr list --repo m6502/ztrackerprime --state merged --limit 30(recent landings). The skill stays timeless: architecture, invariants, foot-guns, conventions.
zTracker Prime (m6502/ztrackerprime) is Manuel Montoto's maintained fork of zTracker — an Impulse-Tracker-inspired MIDI tracker originally by Christopher Micali (2000–2001) with contributions from Daniel Kahlin. SDL 3, cross-platform (Windows XP through 11, macOS, Linux). C++17, CMake.
MIDI-only. No sample playback, no audio mixing. Pattern data drives MIDI Out events.
mkdir -p build && cd build
cmake -G "Unix Makefiles" .. # or Ninja, MSVC, MinGW
make -j8 # → build/Program/zt(.exe) or build/Program/zt.app
ctest --output-on-failure # unit-test harness (Linux CI runs this)
extlibs/ (libpng + zlib + lua) must be populated; CI fetches them from upstream tarballs (see .github/workflows/build.yml). SDL3 from Homebrew on macOS (brew install sdl3).
ZT_BUILD_TESTS CMake option (default ON) controls whether tests/ builds. Production binaries are unaffected.
open <path>/zt.app (or open <path>/zt) — gives the app its own session. Avoid ./zt & from a non-interactive shell; it can exit silently when stdin is detached.
scripts/zt-screenshot.sh (macOS) launches zt.app and captures F1/F2/F3/F11/F12/Ctrl+F12 via screencapture -R + AppleScript. Output at /tmp/zt-shots/. Use it to self-verify layout changes.
zt has a built-in windowless renderer that dumps its real framebuffer to a PNG — no window, no compositor, no screen-recording permission. This is the most reliable way to verify page layout and the only method that works in headless / remote / CI contexts.
# Script = one command per line. Commands:
# key <chord> e.g. key F11, key shift+F3, key ctrl+s, key space
# mousemove <x> <y> / mousedown <x> <y> / mouseup <x> <y>
# click <x> <y> full left click (DOWN then UP on separate frames)
# wait <ms> let update()/draw() run a few frames
# shot <path> dump the current framebuffer to PNG
# quit tear down cleanly (always end with this)
# Coords are internal-resolution pixels — the col()/row() space, col(N)=N*8.
cat > /tmp/f11.script <<'EOF'
wait 400
key F11
wait 400
shot /tmp/f11.png
quit
EOF
build/Program/zt.app/Contents/MacOS/zt --headless --script /tmp/f11.script
# then Read /tmp/f11.png to eyeball it
--headless sets SDL_HINT_VIDEO_DRIVER=dummy, so it runs anywhere (no
display needed). The binary reads zt.conf relative to its location
(Contents/Resources/zt.conf for a .app) — edit that file to set up
preconditions (enabled flags, loaded song, etc.) before driving the UI.
Mouse injection (commands above) makes mouse-only controls testable. Many
F11 checkboxes/sliders are NOT in the keyboard Tab cycle (F11's Tab only toggles
title↔order-list), so drive them with click <x> <y>. Coordinates are
internal-resolution pixels — the same col(N)=N*8 / row(M)=M*8 space the
widgets use — so a checkbox the page positions at cb->x/cb->y is clicked at
click <cb->x * 8> <cb->y * 8> (aim a pixel or two inside it). Capture a shot
before and after to diff the resulting state change. Because click defers its
button-up to the next pump, a checkbox latches on DOWN and toggles on UP exactly
as it does under a real click.
click spans two frames (widgets latch on DOWN, act on UP), so a wait after
it before shot is good practice.
# 1. Relaunch a fresh binary (kill the old instance first, or you screenshot stale pixels)
pkill -f "zt.app/Contents/MacOS/zt"; sleep 1
open build/Program/zt.app && sleep 2
# 2. Bring zt to the front — screencapture grabs whatever is frontmost, and
# `open` does NOT guarantee focus over the terminal you launched it from
osascript -e 'tell application "zt" to activate' \
|| osascript -e 'tell application "System Events" to set frontmost of (first process whose name is "zt") to true'
sleep 2
# 3. Capture. Full-screen is the robust fallback; -o omits the window shadow, -x silences the shutter
screencapture -o -x /tmp/zt_shot.png
Then Read the PNG to eyeball it. Foot-guns learned the hard way:
cp is interactive. A bare cp src dst prompts overwrite? (y/n)
and, with no tty answer, silently does NOT overwrite — your swap
never happened. Use /bin/cp -f for any scripted overwrite.open then capture grabs the terminal, not zt, unless you
activate zt first (step 2). The first capture attempt usually shows
iTerm — bring zt forward and recapture.python3 - <<'PY' ... Quartz ... window-id lookup will
fail on stock macOS; don't depend on it. Full-screen screencapture
needs no window id.To verify how a palette .conf (e.g. palettes/hotdog_stand.conf) or a
skin renders, you don't have to click through the Palette Editor. The
startup colours come from the skin named in zt.conf (skin: default),
loaded from <bundle>/skins/<name>/colors.conf — same slot format as
palettes/*.conf. So:
RES=build/Program/zt.app/Contents/Resources
/bin/cp -f "$RES/skins/default/colors.conf" /tmp/colors_backup.conf # back up
/bin/cp -f "$RES/palettes/hotdog_stand.conf" "$RES/skins/default/colors.conf" # swap in
# ... relaunch + capture per the recipe above ...
/bin/cp -f /tmp/colors_backup.conf "$RES/skins/default/colors.conf" # ALWAYS restore
COLORS.Background is the full-screen fill (main.cpp fillRect(0,0,…,COLORS.Background)),
so a wrong Background value shows instantly. Note that palettes/skins
are copied into the bundle by a CMake POST_BUILD step — edit the source
palettes/*.conf (repo root) and rebuild to propagate; editing only the
bundle copy is for throwaway verification and must be restored.
Palette gotcha (PR: Hotdog Stand): scripts/import-schism-palettes.py
maps Schism colour index 1 to Background, but some Schism palettes
(Hotdog Stand) put the desktop fill on index 2 — the importer left
Background black and Lowlight yellow, so the desktop was black and, worse,
black Text on a black desktop was invisible. Frames draw with both
Lowlight (top/left) and Highlight (bottom/right), so a Lowlight that
equals Background makes half of every frame vanish. Hand-tune imported
palettes against an actual screenshot; don't trust the 16→18 slot mapping.
| File | Purpose |
|---|---|
src/main.cpp | Entry, main loop, key dispatch, switch_page, CLI parser |
src/CUI_Patterneditor.cpp | Pattern editor (F2) + 10+ pattern operations + Ctrl+Shift+§ CC drawmode |
src/CUI_Midimacroeditor.cpp / CUI_Arpeggioeditor.cpp | F4 / Shift+F4 editors |
src/CUI_KeyBindings.cpp | Unified Shortcuts & MIDI Mappings page (Shift+F2 — moved from Shift+F3 to free that slot) |
src/CUI_CcConsole.cpp | CC Console (Shift+F3) — Paketti CCizer file load + sliders/knobs send-out + MIDI Learn |
src/CUI_SysExLibrarian.cpp | SysEx Librarian (Shift+F5) — .syx file send + auto-capture of incoming SysEx |
src/CUI_LuaConsole.cpp | Lua Console (Ctrl+Alt+L) — Renoise-style interactive console over the embedded Lua engine. Tab cycling, rprint/oprint/help, scrollback wrapping. |
src/lua_engine.{cpp,h} | Embedded Lua API: object model (zt, song, transport, pattern, track, cells, order list) + notifiers (zt.on/off/fire for play/stop/row/idle). lua/selftest.lua exercises the surface via --lua-test (a ctest target). |
src/ableton_link.{cpp,h} | Ableton Link tempo + transport sync (PR #159). C API: zt_ableton_link_startup/teardown/pump/defer_play/available/get_tempo. Driven from playback.cpp; configured from F11 Song Config; conf keys in conf.cpp. Rebuilt on the shared phase_chase.h controller. |
src/phase_chase.h | Shared closed-loop position controller (pure logic, SDL-free, unit-tested), used by both Ableton Link and MIDI clock sync. phase_chase_step() returns a bounded skew that cancels rate error (int-BPM rounding, crystal drift); deadband-tiered feedback. |
src/midi_clock_sync.{cpp,h} | MIDI clock slave (midi_in_sync / midi_in_sync_chase_tempo). MIDI-in thread only records into atomic g_mclk_* counters; zt_midi_clock_pump() (main thread, once/frame) consumes transport, estimates tempo, phase-locks via phase_chase.h. Six-state zt_sync_state; zt_midi_clock_get_status(). Honors sync_offset_ms. |
src/midi_timestamp.h | Pure helpers (SDL-free, unit-tested) to use a MIDI hardware timestamp as clock-arrival time. macOS mach_absolute_time → SDL_GetTicks() ms via zt_mach_stamp_to_sdl_ms, sanity-gated (garbage stamp falls back to software clock). |
src/CUI_*.cpp | Other pages (Sysconfig, Songconfig, Help, InstEditor, MainMenu, Playsong, etc.) |
src/CUI_Page.{h,cpp} | Base class for pages |
src/UserInterface.{h,cpp} | Widget classes — CheckBox, ValueSlider, TextInput, Frame, Button, TextBox, ListBox, MidiOutDeviceOpener, SkinSelector, VUPlay. Fix rendering bugs HERE, not in callers. |
src/keybuffer.{h,cpp} | Key state (KS_ALT/KS_CTRL/KS_META/KS_SHIFT) + KS_HAS_ALT macro. ZT_TEST_NO_SDL guard for SDL-free unit tests. |
src/font.cpp | print() / printtitle() / printBG() drawing primitives |
src/zt.h | STATE_/CMD_ enums, page globals (UIP_CcConsole, UIP_SysExLibrarian, ...), layout macros, g_cc_drawmode |
src/conf.{h,cpp} | zt.conf parsing, zt_config_globals. Keys include ccizer_folder, syx_folder |
src/module.{h,cpp} | Song data (patterns, tracks, events, instruments, arpeggios, midimacros). instrument carries ccizer_bank (per-instrument Paketti CCizer file). |
src/playback.{h,cpp} | MIDI output timing, R-effect arpeggio + Z-effect macro dispatch. Z on a *.syx-named midimacro dispatches the file as SysEx (see sysex_macro.h). |
src/winmm_compat.h | Cross-platform MIDI shim. Exposes midiOut* (WinMM-style API) on Win/macOS/Linux. Adds zt_midi_out_sysex(handle, bytes, len) for SysEx send. macOS zt_coremidi_read_proc parses incoming F0..F7 into zt_sysex_inq_push. |
src/sysex_inq.{h,cpp} | Process-wide receive queue for incoming SysEx (16 slots × 8 KB; overflow drops oldest). std::mutex protected. |
src/sysex_macro.{h,cpp} | Helpers for the *.syx-named-midimacro convention: predicate, path resolution against syx_folder, file read with F0..F7 framing check. |
src/ccizer.{h,cpp} | CCizer parser, .cc-view slider/knob sidecar, .cc-midi MIDI Learn sidecar, folder scan, MIDI byte builder, zt_ccizer_current_file() (read by Pattern Editor for friendly slot names in CC drawmode status). |
src/preset_data.h | F4/Shift+F4 preset arrays + pure apply functions |
src/preset_selector.h | Pure listbox decision logic (click / arrow / Space / P-cycle) |
src/page_sync.h | F4/Shift+F4 widget↔data sync helpers |
src/save_key_dispatch.h | Global Ctrl-S dispatch decision |
src/editor_layout.h | Shared character-grid constants for F4/Shift+F4 |
assets/ccizer/ | Bundled CCizer banks (sc88st, microfreak, minilogue, monologue, Prophet6, deepmind6, waldorf_blofeld, tb03, se02, pro800, polyend_*, midi_control_example) — copied from Paketti. README.md documents the format. |
assets/syx/ | Bundled SysEx files. request_universal_inquiry.syx = F0 7E 7F 06 01 F7 for first-test handshakes. README.md documents the librarian workflow. |
tests/ | CTest unit-test executables — 14 suites: presets, selector, page_sync, save_key, keybuffer, ccizer, sysex_inq, sysex_stress, sysex_macro, ccbn_roundtrip, ccenv, keyjazz_map, ableton_link, lua_api. The lua_api suite drives the real zt binary headless (--headless --lua-test) against a scratch song and runs on macOS CI (PRs #154, #156); the rest are SDL-free and run on Linux CI. The MIDI clock phase-lock feature (integration PR #169) adds 4 more (phase_chase, midi_timestamp, midi_clock_sync, midi_clock_estimator → 18). (Count drifts as suites are added — ctest -N is the source of truth.) |
doc/help.txt | In-app F1 help — update when adding keybinds or CLI flags |
doc/CHANGELOG.txt | Release notes, chronological |
.github/workflows/build.yml | 5-platform CI matrix; Linux job runs ctest |
For the live PR queue: gh pr list --repo m6502/ztrackerprime.
| Key | Page | Notes |
|---|---|---|
| F1 | Help | |
| F2 | Pattern Editor | F2 again from PE → PEParms. F2 F2 toggles "PianoKey" — the Ableton/Logic piano keyjazz layout (PR #135, keyjazz_piano_layout). |
| F3 | Instrument Editor | F3 row 11 shows CCizer Bank: <basename> (per-instrument bank). F3 again → "Create 16 Channels" popup: fills the next empty instrument slots with the current device on ch 1..16 (PR #136). |
| F4 | MIDI Macro Editor | A macro whose name ends in .syx is dispatched as a SysEx file send by the playback engine; data grid is ignored (hint shown). |
| Shift+F2 | Shortcuts & MIDI Mappings | Was Shift+F3 historically; moved 2026-04-30 to free the slot for CC Console. |
| Shift+F3 | CC Console | Loads CCizer .txt, sliders/knobs send CC out, L to MIDI Learn, B to assign as current instrument's bank. |
| Shift+F4 | Arpeggio Editor | |
| F5 | Play Song | |
| Shift+F5 | SysEx Librarian | Lists .syx files. Enter sends. Incoming SysEx auto-saved as recv_<timestamp>.syx. |
| Shift+F6 | CC Envelope Editor | Per-instrument, CCizer-aware CC envelopes (Schism-style). New page, PR #132. Persisted in the optional INSE chunk; editor loads/saves .env presets from a configurable folder. |
| F6 / F7 / F8 / F9 | Play Pattern / From Cursor / Stop / Panic | |
| F10 | Song Message Editor | |
| F11 | Song Configuration / Order | Hosts the Ableton Link controls (enable checkbox, start/stop-sync checkbox, quantize-offset slider). See "Ableton Link" below. |
| F12 | System Configuration | Includes CCizer Folder text input (binds to ccizer_folder zt.conf key). |
| Ctrl+Alt+L | Lua Console | Interactive Lua over the embedded engine. Sibling MainMenu chords: Ctrl+Alt+K (Keybindings), Ctrl+Alt+N (New Song). |
| Ctrl+Shift+§ | CC drawmode toggle | While ON, incoming CC writes Sxxyy and PB writes Wxxxx at the cursor row. Per-session UNDO_SAVE. [CC DRAW] badge shown top-right of Pattern Editor while active. Extended by PRs #123–#129: mouse drag-to-draw drawbars (MD_CC_DRAW), CCizer-slot cycling, one-key arm-and-draw, double-click reset, Ctrl+F2 slot cycle, keyjazz audition while mouse-drawing. Windows fires the toggle via Ctrl+Shift+`` ``. |
row(N) = N * 8 pixels (Y offset). Named for character rows but returns pixels.col(M) = M * 8 pixels (X offset).print(x, y, str, color, drawable) — x first, y second.print(row(2), col(13), ...) — this works only because FONT_SIZE_X == FONT_SIZE_Y == 8. Confusing; don't propagate.Layout macros: INITIAL_ROW=1, PAGE_TITLE_ROW_Y=9, TRACKS_ROW_Y=11, SPACE_AT_BOTTOM=8.
Seven-PR feature stack landed end of April 2026 wiring Paketti-style CCizer banks and a complete SysEx librarian into zTracker. The architecture has three layers worth keeping in mind:
1. Per-page (UI)
CUI_CcConsole.cpp) — file list of .txt CCizer banks, slot grid with sliders/knobs, V toggles slider↔knob, L to MIDI Learn ((status, data1) → slot), U to unbind, B to assign loaded file as current instrument's bank. [/] adjusts MIDI channel.CUI_SysExLibrarian.cpp) — file list of .syx files in syx_folder. Enter/S sends. Receive log auto-pops from zt_sysex_inq and writes new files as recv_<TS>.syx. R rescans, C clears log.CUI_Patterneditor.cpp) — Ctrl+Shift+§ flips g_cc_drawmode. While on, the existing MIDI-in pump in update() routes 0xB0..0xBF / 0xE0..0xEF to update_event(...) writing S<cc><val> / W<14bit>. Cursor advances by EditStep. Single UNDO_SAVE per session (file-static g_cc_draw_session_snapped).2. Process-wide
g_cc_drawmode (in zt.h) — toggle.zt_ccizer_current_file() (in ccizer.h) — last-loaded file pointer, so the Pattern Editor's drawmode status can show friendly slot names ("Cutoff = 92") instead of raw CC numbers.zt_sysex_inq_* (in sysex_inq.h) — ring buffer of incoming complete SysEx messages. Filled by the macOS CoreMIDI read_proc; drained by CUI_SysExLibrarian::drain_recv every update().MidiOut->sendSysEx(dev, bytes, len) — virtual on OutputDevice, default no-op. MidiOutputDevice impl asserts F0/F7 framing and calls zt_midi_out_sysex in winmm_compat.h. Backed by CoreMIDI MIDIPacketListAdd (256-byte chunks), Win midiOutLongMsg, ALSA snd_seq_ev_set_sysex.3. Persistence
ccizer_folder zt.conf key — folder of .txt files. Resolution: override → <exe-dir>/ccizer → macOS Resources/ccizer → Linux share/zt/ccizer → ~/.config/zt/ccizer. F12 page exposes the text input.syx_folder zt.conf key — folder of .syx files. Resolution: override → ./syx (next to binary or in .app Resources) → ~/.config/zt/syx. Auto-created.<file>.cc-view sidecar — per-CCizer slot slider/knob choice. Written next to the .txt.<file>.cc-midi sidecar — per-CCizer slot MIDI Learn binding. Format: <slot> <status hex> <data1 hex>.instrument.ccizer_bank[256] — per-instrument CCizer file. Persisted via new optional CCBN chunk in .zt. Format: short int count then per entry unsigned char inst_idx, short int length, bytes path. Fully backward+forward compatible: old zTracker reading new .zt skips the unrecognized chunk via readblock's built-in advance-past behavior.SysEx-by-filename convention (sysex_macro.h): a midimacro whose name ends in .syx (case insensitive, requires len > 4) is dispatched by the playback engine's Z effect handler as a file send. The macro's data array is ignored. No .zt format change — the existing MMAC chunk already round-trips the name. Old zTracker sees an empty data array and silently does nothing — safe forward-compat.
Cross-platform status (verified 2026-05-02):
MIDIPacketListAdd), Windows (WinMM midiOutLongMsg + MHDR_DONE poll, PR #87), Linux (ALSA snd_seq_ev_set_sysex).MIM_LONGDATA accumulator, PR #90). Initial Linux ALSA MIDI input landed in PR #113 (marked "needs hardware verification") — so the Linux-in platform layer is no longer a bare stub, but confirm SysEx-receive-on-Linux against that path on real hardware before relying on it.Test suites added:
tests/test_ccizer.cpp — 51 checks: parser, comments/blanks, out-of-range, view sidecar round-trip, MIDI byte builder, folder resolution, dir scan.tests/test_sysex_inq.cpp — ~25 checks: empty / push-pop / FIFO / overflow / buffer-too-small / invalid input.tests/test_sysex_macro.cpp — ~20 checks: predicate, path resolution, valid/malformed/oversized/missing file read.An embedded Lua engine + interactive console landed across ten PRs at the end of May 2026. Treat it as a first-class subsystem.
src/lua_engine.{cpp,h}) — exposes a Renoise-flavoured object model: zt (top-level), song, transport, pattern, track, plus full cell access, the order list, note names, and constants. Notifier API: zt.on(event, fn) / zt.off(...) / zt.fire(...) for play / stop / row / idle events, pumped from the main loop. MIDI macros expose :send() (PR #157).src/CUI_LuaConsole.cpp, Ctrl+Alt+L) — Tab cycling, rprint / oprint / help helpers, Renoise-style object printing, and scrollback that wraps (PR #153) rather than clips (PR #152) long lines.lua/selftest.lua exercises the whole API surface; runs via --lua-test and is wired into ctest, and the macOS CI jobs run it (PRs #154, #156). When you add or rename a Lua binding, update lua/selftest.lua in the same PR — CI will catch a regression there.Tempo + transport sync with Ableton Live and other Link-aware apps over the local network.
src/ableton_link.{cpp,h}) — C API: zt_ableton_link_startup / _teardown / _pump / _defer_play(row, pattern, pm) / _available / _get_tempo. Pumped from playback.cpp; the pump can move song->bpm underneath the UI, so F11 re-reads it each frame.conf.cpp, keys persisted in zt.conf): ableton_link_enable (OFF by default — no surprise LAN traffic), ableton_link_start_stop_sync (ON by default; only effective once Link is enabled), ableton_link_quantum (default 4 = one bar of 4/4), ableton_link_offset_ms (fire quantized starts early by this much).CUI_Songconfig.cpp) hosts the enable + start/stop-sync checkboxes and the offset slider.conf.cpp).External MIDI-clock slave sync built on the same phase_chase.h controller as Ableton Link, so a fractional-tempo master cannot drift the engine. midi_in_sync follows start/stop; midi_in_sync_chase_tempo derives tempo and phase-locks; the --midi-clock CLI flag turns both on. The engine is the merge of two independent rewrites that converged on an identical phase_chase.h: Chris Micali's tempo estimator (cmicali/midi-sync-improvements) under the hardening branch's observability + robustness.
midi_clock_sync.{cpp,h}) — the MIDI-in callback thread only records into std::atomic counters (g_mclk_*, same advisory contract as player::stop_count); all decisions run in zt_midi_clock_pump() on the main thread once/frame, next to zt_ableton_link_pump(). ThreadSanitizer-clean (TSan CI job). Relies only on per-counter atomicity, tolerating a bump seen a frame late.beatAtTime), fed through phase_chase_step() into player::chase_skew_us.midi_timestamp.h, macOS) — CoreMIDI packet stamp → SDL_GetTicks() epoch removes callback scheduling jitter and feeds the estimator's arrival edge; sanity-gated so a bad stamp falls back to the software clock.zt_sync_state: off/waiting/dropout/transport/chasing/locked) via zt_midi_clock_get_status() → F11 lock meter + Lua zt.transport.sync_state/sync_bpm/sync_offset_ms + the sync notifier. sync_offset_ms conf key (legacy alias ableton_link_offset_ms).phase_chase, midi_timestamp, midi_clock_sync, and Chris's midi_clock_estimator (the 4 suites that take the harness from 14 → 18 when this lands).These rules are non-negotiable. Every one of them was learned the hard way during the CCizer + SysEx + CC-Console work (PRs #78–#84, #95, #96). Don't re-learn them.
Every key handler that mutates visible state ends with need_refresh++; — including enter() and any background pump that changes what should be on screen. Forget this and arrow keys silently mutate state without a redraw; the page looks frozen until some other path bumps the flag. main.cpp gates ActivePage->draw() on need_refresh != 0.
Interactive elements are real widgets, not ASCII art. A slider is a ValueSlider registered with UI->add_element, drawn by UI->draw(S), mutated by ValueSlider::mouseupdate, and absorbed by the page's update() reading the changed flag. Never ship printBG text bars as a stand-in for "we'll iterate later" — visually identical in one screenshot, completely unclickable to the user. The pool-of-N pattern (pre-allocate N widgets in the ctor, position the visible window per-frame, hide off-screen ones with xsize=0) is the idiom for variable-count grids; CUI_CcConsole.cpp::position_sliders is the canonical worked example.
Any list pane uses a real ListBox subclass. Never hand-draw > filename with a custom highlight. F11 SkinSelector / F4 / Shift+F4 / Shift+F3 all share one pattern, documented in references/list-pane-idiom.md. New list-driven pages copy that pattern verbatim — anonymous-namespace : ListBox subclass, is_sorted=true, use_checks=true, use_key_select=true, Space-applies override on update(), never call ListBox::OnSelect from the subclass.
Per-frame need_redraw = 1 on always-visible widgets. UserInterface::draw skips elements with need_redraw == 0. If any sibling fires needaclear++ the whole element area is cleared and stale-need_redraw widgets vanish. The page's update() (or position_sliders()) stamps need_redraw = 1 on the listbox, the grid-focus stub, and every visible pool widget, every frame. Worked example: PR #96.
Each visual column is its own print / printBG anchored to its own col(CC_*_COL) constant. Never pack adjacent columns into one packed snprintf — adjacent fields will smash into each other (PBPitchbend, LeUani) the first time a string is longer than expected. Headers and data rows reference the same constants so they stay aligned.
Tab cycles UI->cur_element between IDs. Page-level "focus" is derived from cur_element each frame, not stored. Use a 1x1 UserInterfaceElement stub (e.g. MmGridFocus, CcSlotGridFocus) for non-list panes that need Tab focus.
Pages whose own ESC handler matters add their STATE_* to the global ESC exclusion list in main.cpp::global_keys (the list with STATE_HELP / ABOUT / LUA_CONSOLE / SONG_MESSAGE / KEYBINDINGS). Otherwise the global handler eats ESC first and the page's "ESC cancels capture" hint is a lie. PR #95 was a one-line fix for STATE_KEYBINDINGS.
Visual verification before claiming done. Build green + ctest green ≠ "the page works." Use the headless framebuffer screenshots (--headless --script ... shot, see "Launching the app") when you need to visually verify features. Drive each user-visible control: key for F-keys / arrows / Tab / Space / Enter / Esc, and click <x> <y> for mouse-only widgets, then shot and Read the PNG. If you genuinely cannot do it, say so explicitly — don't ship hope.
The full list-pane construction pattern (with code) lives in references/list-pane-idiom.md. Read it before adding any new page that has a list, a grid, or both.
references/list-pane-idiom.md — REQUIRED READING before adding any list / grid page. The full pattern: ListBox subclass + grid-focus stub + Tab cycling + per-frame need_redraw=1 + column-per-print + global-ESC exclusion + visual-verification gate.references/foot-guns.md — recurring bug classes: ListBox mousestate fragility, widget-upstream rule, vanishing-sibling on needaclear, user-reported-inputs-as-ground-truth.references/keyjazz-slot.md — Shift+F4 keyjazz UIE pattern.references/test-harness.md — sdl_stub / module_stub / ZT_TEST_NO_SDL.references/cli-flags.md — zt_parse_cli + Copy Relaunch Command.references/keyboard.md — Cmd→KS_META, KS_HAS_ALT, § remap, Cmd-Q strip.references/conventions.md — coding conventions, status-bar diagnostic idiom.recipes/add-test-suite.md — adding a new CTest executable.glossary.md — abbreviation lookup (TPB, MMAC, ARPG, KS_/CMD_/STATE_, etc.).effects.md — pattern-effect letter table.origin, open PRs against master, don't merge without maintainer approval.doc/CHANGELOG.txt gets a section per PR.Co-Authored-By: Claude <noreply@anthropic.com> (or current model identifier).When Esa reports a bug, the default response is: investigate → fix → branch → commit → push → open PR. Do not ask "want me to fix?" or "should I open a PR?" — that's the implicit ask. Just do it.
The only acceptable preamble is one or two sentences naming the root cause and the fix, then go.
esa/<short-descriptive> (e.g. esa/cc-console-file-switch-leak).ctest before pushing.origin; Esa has write access.Ask for clarification only if the bug report itself is ambiguous about the symptom — never about whether to fix it. Reference: PR #106 (CC Console file-switch slider value leak, 2026-05-02).
Paketti (a Renoise tool) is a reference for tracker quality-of-life features ported / portable to zTracker. Already landed: Replicate at Cursor, Clone Pattern, Humanize Velocities, MIDI Macro + Arpeggio editors. Future candidates: Fill-with-note-at-cursor, Find/Replace note, Transpose selection, Groove templates, Chord memory. zTracker is MIDI-only, so sample-editor features are N/A.
Update this skill (and the relevant reference file) when the project's context shifts materially: new architecture area, new feature region, new recurring foot-gun.