diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..59c89ee --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A REAPER MIDI-editor grid/step-entry mode written in Lua, built for +keyboard-and-speech use with OSARA. Pure ReaScript — no ReaPack, no +js_ReaScriptAPI, no ReaImGui. `README.md` is the user manual and is unusually +detailed; read the relevant section before changing behaviour, and update it +when behaviour changes. + +## Commands + +``` +lua test_lib.lua # run the tests +powershell -ExecutionPolicy Bypass -File install.ps1 # install (REAPER must be CLOSED) +powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall +``` + +There is no build, no lint, no package manager, no test framework. `test_lib.lua` +is a single script of `eq(got, want, label)` assertions printing `ALL PASS` or a +failure count; to run one case, comment out the others. It stubs a global +`reaper` table, so it only covers the pure logic (scale stepping, note naming, +grid labels, chord construction). Anything touching MIDI data or the REAPER API +can only be exercised inside REAPER. Note it `dofile`s an **absolute** path to +`midigrid_lib.lua`. + +## Architecture + +**`midigrid_lib.lua`** holds essentially all the logic as module `M`. Every +action script is a thin adapter: resolve its own directory from +`reaper.get_action_context()`, `dofile` the lib, check `G.isActive()`, and either +`G.passThrough(...)` or call a couple of lib functions. New behaviour belongs in +the lib; new action scripts should stay ~10 lines. + +**No private cursor state.** Time is REAPER's edit cursor; pitch is the MIDI +editor's `active_note_row`; cell size is `MIDI_GetGrid()`. This is what lets grid +mode and plain OSARA editing interleave without drifting, and why the user's +existing grid-size keybindings just work. Do not introduce a cursor of your own. + +**Action scripts must never call `reaper.defer`.** A still-running script makes +REAPER show its "already running — terminate or launch new instance?" prompt on +the next keypress, which destroys key repeat. All timing lives in +`MidiGrid_Daemon.lua`, one background loop that auto-starts (`G.ensureDaemon()`), +heartbeats via ExtState, refuses to start a second copy, and releases notes on +`atexit`. Actions post a request to ExtState and exit immediately. If you need +something to happen later — note release, stop-at-bar-end, restoring borrowed +loop/repeat state — add it to the daemon loop, not to an action. + +**ExtState section `midigrid`** is the only state. Persisted (`M.set`/`M.get`): +`active`, `hold`, `root`, `scale`, `vel`, `velstep`, `chan`, `previewdur`, +`stoplead`. Transient (`M.setTemp`/`M.getTemp`, persist flag false): `seq`, +`pitches`, `dur`, `hb`, `stopat`, `stoparm`, `savedloop`, `savedrep`. Preview +traffic must stay transient — persisting it would write `reaper.ini` on every +keypress. + +**Every grid key falls back.** When grid mode is off, each bound key forwards to +what it did before via `G.passThrough` (e.g. `_OSARA_PREVCHORD`, or a numeric +command id), so nothing is permanently taken away. + +**All feedback is speech**, via `G.say` → `osara_outputMessage`, falling back to +the console when OSARA is absent. There is no window and no `gfx`. + +Musical positions are converted through quarter-notes (`MIDI_GetPPQPosFromProjQN` +and friends), not raw ticks, so edits stay correct across tempo changes. + +## Installation model + +`install.ps1` edits `reaper-kb.ini` directly and is idempotent (it strips all +`RSmidigrid` lines first, and backs up to `reaper-kb.ini.midigrid-backup`). +REAPER rewrites that file on exit, so it must be closed or changes are lost. + +Adding a new action means adding a row to **both** tables in `install.ps1`: the +`$actions` table (`SCR 4
"" `) and, if it needs +a key, `$keys` (flags: 1 base, +4 Shift, +8 Ctrl, +16 Alt; section 0 = Main, +32060 = MIDI Editor). `KEY` lines must reference the named command — the SCR id +with a **leading underscore** (`_RSmidigrid_open`). Without the underscore REAPER +silently converts the binding to a no-op; this is exactly why importing +`MidiGrid.ReaperKeyMap` does not work and the file is kept for reference only.