diff --git a/CLAUDE.md b/CLAUDE.md index 59c89ee..af57fe0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,8 +14,10 @@ when behaviour changes. ``` lua test_lib.lua # run the tests -powershell -ExecutionPolicy Bypass -File install.ps1 # install (REAPER must be CLOSED) +powershell -ExecutionPolicy Bypass -File install.ps1 # install on Windows (REAPER must be CLOSED) powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall +./install.sh # install on macOS/Linux +./install.sh --uninstall ``` There is no build, no lint, no package manager, no test framework. `test_lib.lua` @@ -67,11 +69,17 @@ 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. +`install.ps1` (Windows) and `install.sh` (macOS/Linux) edit `reaper-kb.ini` +directly and are idempotent (they strip all `RSmidigrid` lines first, and back +up to `reaper-kb.ini.midigrid-backup`). REAPER rewrites that file on exit, so +it must be closed or changes are lost. The two scripts are deliberate mirrors: +same action list, same key table, same output. **Any change to one must be made +to the other.** They differ only in resource-path discovery +(`%APPDATA%\REAPER` vs `~/Library/Application Support/REAPER`) and in +`install.sh --real-control`, which exists because modifier bit 8 means Ctrl on +Windows but Command on macOS (physical Control is bit 32). -Adding a new action means adding a row to **both** tables in `install.ps1`: the +Adding a new action means adding a row to **both** tables in each installer: 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 diff --git a/README.md b/README.md index db6f5c2..fcac886 100644 --- a/README.md +++ b/README.md @@ -26,21 +26,95 @@ Clone or download this repository anywhere you like, then: 1. **Close REAPER completely.** It rewrites `reaper-kb.ini` when it exits and will throw away anything written while it is running. -2. Run: +2. Run the installer for your platform: + + **Windows** ```powershell powershell -ExecutionPolicy Bypass -File install.ps1 ``` -3. Start REAPER, select a MIDI item, press **`Alt+Shift+G`**. + **macOS / Linux** + + ```bash + chmod +x install.sh # first time only + ./install.sh + ``` + +3. Start REAPER, select a MIDI item, press **`Alt+Shift+G`** (`Option+Shift+G` + on a Mac). You should hear something like *"Grid mode on, C major, grid 1/16, hold off, C5, 1.1.00"*. -To remove it again: `powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall` +To remove it again: -The installer backs up `reaper-kb.ini` to `reaper-kb.ini.midigrid-backup` -before its first change, and is safe to re-run. +```powershell +powershell -ExecutionPolicy Bypass -File install.ps1 -Uninstall # Windows +``` +```bash +./install.sh --uninstall # macOS / Linux +``` + +Both installers back up `reaper-kb.ini` to `reaper-kb.ini.midigrid-backup` +before their first change, and are safe to re-run. + +### macOS notes + +`install.sh` does exactly what `install.ps1` does — same actions, same +bindings — but finds the resource folder at +`~/Library/Application Support/REAPER` (falling back to `~/.config/REAPER` on +Linux). If yours is elsewhere, find it via **Options → Show REAPER resource +path** and pass it: + +```bash +./install.sh --resource-path "/path/to/REAPER" +``` + +**Modifiers.** In `reaper-kb.ini` the modifier bit that means Ctrl on Windows +means **Command** on macOS; the physical Control key is a different bit. So by +default every `Ctrl+…` binding in the [key table](#keys) is `Command+…` on a +Mac, which is what Mac users expect. To bind the physical Control key instead: + +```bash +./install.sh --real-control +``` + +`Alt` in the key table is the `Option` key on a Mac either way. + +### Manual installation + +If you would rather not run a script — or you want a different key layout — +you can do the whole thing from REAPER's UI. Nothing about MIDI Grid depends +on the installer; it only writes action registrations and key bindings. + +1. **Register the scripts.** In REAPER, open **Actions → Show action list**, + set the section selector to **Main**, and choose **New action → Load + ReaScript…**. Select these two files from wherever you put the repository: + + - `MidiGrid_OpenInGrid.lua` + - `MidiGrid_Daemon.lua` + + Then switch the section selector to **MIDI Editor** and load the remaining + `MidiGrid_*.lua` files the same way (everything except `midigrid_lib.lua` + and `test_lib.lua`, which are not actions). The section matters: an action + loaded into the wrong section will not be offered for the keys you want. + +2. **Bind the keys.** Still in the action list, select an action, click + **Add…** under Shortcuts, and press the key combination. Use the + [key table](#keys) below for the intended layout; you can of course choose + your own. Only `Alt+Shift+G` needs to be in the Main section — everything + else belongs in MIDI Editor. + +3. **Say yes when REAPER warns about a key that is already bound.** Grid mode + deliberately takes over `Left`, `Right`, `Up`, `Down` and `Enter` in the + MIDI editor; each of those forwards to its previous behaviour when grid + mode is off, so nothing is lost. + +You do not need to bind everything. The minimum useful set is +`Alt+Shift+G` (open), `Alt+G` (toggle), the four arrows, and `Enter`. +`MidiGrid_Daemon.lua` needs to be **registered** as an action but never needs +a key — it auto-starts. ### Why not "Import key map"? @@ -52,10 +126,10 @@ action list but do nothing. The cause is that a `KEY` line must reference the *named command* — the `SCR` id with a **leading underscore** (`_RSmidigrid_open`), not the bare id -(`RSmidigrid_open`). `install.ps1` writes the underscored form directly. +(`RSmidigrid_open`). Both installers write the underscored form directly. -If you would rather not run a script, you can bind the ten `MIDI Grid:` -actions by hand in Actions → Show action list. That always works. +If you would rather not run a script, see [Manual installation](#manual-installation) +above. Binding by hand in the action list always works. ## Keys diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1af76c7 --- /dev/null +++ b/install.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# +# MIDI Grid installer for macOS (and Linux REAPER). +# +# The macOS counterpart of install.ps1, and deliberately a line-for-line +# mirror of it: same actions, same key table, same idempotent rewrite of +# reaper-kb.ini. If you add an action, add it to *both* files. +# +# REAPER MUST BE CLOSED. It rewrites reaper-kb.ini on exit and will +# discard anything written while it is running. +# +# Usage: ./install.sh +# ./install.sh --uninstall +# ./install.sh --resource-path ~/Library/Application\ Support/REAPER +# ./install.sh --real-control # see "Modifiers on macOS" below +# +# Modifiers on macOS +# ------------------ +# In reaper-kb.ini the modifier bit 8 means Ctrl on Windows but *Command* +# on macOS; the physical Control key is bit 32. By default this script +# keeps bit 8, so every "Ctrl+..." binding in the README is Command+... on +# a Mac, which is what Mac users expect. Pass --real-control to bind the +# physical Control key instead. + +set -euo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +resource_path="" +uninstall=0 +real_control=0 + +while [ $# -gt 0 ]; do + case "$1" in + --uninstall) uninstall=1 ;; + --real-control) real_control=1 ;; + --script-dir) script_dir="$2"; shift ;; + --resource-path) resource_path="$2"; shift ;; + -h|--help) sed -n '2,25p' "$0"; exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 2 ;; + esac + shift +done + +if [ -z "$resource_path" ]; then + if [ -d "$HOME/Library/Application Support/REAPER" ]; then + resource_path="$HOME/Library/Application Support/REAPER" + elif [ -d "$HOME/.config/REAPER" ]; then + resource_path="$HOME/.config/REAPER" + else + echo "Could not find your REAPER resource folder." >&2 + echo "Find it via Options > Show REAPER resource path, then pass" >&2 + echo " --resource-path '/path/to/REAPER'" >&2 + exit 1 + fi +fi + +kb="$resource_path/reaper-kb.ini" +[ -f "$kb" ] || { echo "reaper-kb.ini not found at $kb" >&2; exit 1; } + +if pgrep -x REAPER >/dev/null 2>&1 || pgrep -x reaper >/dev/null 2>&1; then + echo "REAPER is running. Close it completely and run this again." >&2 + exit 1 +fi + +# id | section | description | script filename +actions=' +RSmidigrid_open|0|MIDI Grid: Open selected item in grid mode|MidiGrid_OpenInGrid.lua +RSmidigrid_daemon|0|MIDI Grid: Preview daemon (auto-started)|MidiGrid_Daemon.lua +RSmidigrid_mode|32060|MIDI Grid: Toggle grid mode|MidiGrid_ToggleMode.lua +RSmidigrid_left|32060|MIDI Grid: Cursor left one cell|MidiGrid_Left.lua +RSmidigrid_right|32060|MIDI Grid: Cursor right one cell|MidiGrid_Right.lua +RSmidigrid_up|32060|MIDI Grid: Pitch up one scale degree|MidiGrid_Up.lua +RSmidigrid_down|32060|MIDI Grid: Pitch down one scale degree|MidiGrid_Down.lua +RSmidigrid_cell|32060|MIDI Grid: Toggle note at cursor cell|MidiGrid_ToggleCell.lua +RSmidigrid_hold|32060|MIDI Grid: Toggle hold mode|MidiGrid_ToggleHold.lua +RSmidigrid_audit|32060|MIDI Grid: Repeat current cell|MidiGrid_AuditionCell.lua +RSmidigrid_scale|32060|MIDI Grid: Set root and scale|MidiGrid_SetScale.lua +RSmidigrid_triad|32060|MIDI Grid: Toggle diatonic triad|MidiGrid_ChordTriad.lua +RSmidigrid_seventh|32060|MIDI Grid: Toggle diatonic seventh chord|MidiGrid_ChordSeventh.lua +RSmidigrid_velup|32060|MIDI Grid: Velocity up|MidiGrid_VelUp.lua +RSmidigrid_veldn|32060|MIDI Grid: Velocity down|MidiGrid_VelDown.lua +RSmidigrid_longer|32060|MIDI Grid: Lengthen note one cell|MidiGrid_Lengthen.lua +RSmidigrid_shorter|32060|MIDI Grid: Shorten note one cell|MidiGrid_Shorten.lua +RSmidigrid_join|32060|MIDI Grid: Join run of repeated notes|MidiGrid_Join.lua +RSmidigrid_shl|32060|MIDI Grid: Shift all notes one cell left|MidiGrid_ShiftLeft.lua +RSmidigrid_shr|32060|MIDI Grid: Shift all notes one cell right|MidiGrid_ShiftRight.lua +RSmidigrid_shlf|32060|MIDI Grid: Shift all notes left (force)|MidiGrid_ShiftLeftForce.lua +RSmidigrid_shrf|32060|MIDI Grid: Shift all notes right (force)|MidiGrid_ShiftRightForce.lua +RSmidigrid_playbar|32060|MIDI Grid: Play from start of current bar|MidiGrid_PlayFromBar.lua +RSmidigrid_playone|32060|MIDI Grid: Play current bar only|MidiGrid_PlayBarOnly.lua +RSmidigrid_loopbar|32060|MIDI Grid: Loop current bar|MidiGrid_LoopBar.lua +' + +# flags | keycode | section | id +# flags: 1 base, +4 Shift, +8 Ctrl (Command on macOS), +16 Alt +# section: 0 = Main, 32060 = MIDI Editor +keys=' +21|71|0|RSmidigrid_open +17|71|32060|RSmidigrid_mode +1|32805|32060|RSmidigrid_left +1|32807|32060|RSmidigrid_right +1|32806|32060|RSmidigrid_up +1|32808|32060|RSmidigrid_down +1|13|32060|RSmidigrid_cell +17|72|32060|RSmidigrid_hold +17|65|32060|RSmidigrid_audit +17|83|32060|RSmidigrid_scale +17|51|32060|RSmidigrid_triad +17|55|32060|RSmidigrid_seventh +21|32806|32060|RSmidigrid_velup +21|32808|32060|RSmidigrid_veldn +1|76|32060|RSmidigrid_longer +5|76|32060|RSmidigrid_shorter +17|74|32060|RSmidigrid_join +13|32805|32060|RSmidigrid_shl +13|32807|32060|RSmidigrid_shr +29|32805|32060|RSmidigrid_shlf +29|32807|32060|RSmidigrid_shrf +17|32|32060|RSmidigrid_playbar +25|32|32060|RSmidigrid_playone +29|32|32060|RSmidigrid_loopbar +' + +backup="$kb.midigrid-backup" +if [ ! -f "$backup" ]; then + cp "$kb" "$backup" + echo "Backed up original to $backup" +fi + +tmp=$(mktemp) +trap 'rm -f "$tmp" "$tmp.2"' EXIT + +# Drop any previous MIDI Grid lines so re-running is idempotent. +grep -v -i 'RSmidigrid' "$kb" > "$tmp" || true + +if [ "$uninstall" -eq 1 ]; then + cat "$tmp" > "$kb" + echo "Removed MIDI Grid actions and bindings." + echo "Note: keys it had taken over (arrows, Enter) are now unbound in the" + echo "MIDI editor. Rebind them, or restore $backup." + exit 0 +fi + +# SCR lines go at the top, in table order. +: > "$tmp.2" +while IFS='|' read -r id section desc file; do + [ -n "${id:-}" ] || continue + path="$script_dir/$file" + [ -f "$path" ] || { echo "Missing script: $path" >&2; exit 1; } + printf 'SCR 4 %s %s "%s" %s\n' "$section" "$id" "$desc" "$path" >> "$tmp.2" +done <> "$tmp.2" +mv "$tmp.2" "$tmp" + +# KEY lines reference the *named command*, which is the SCR id with a +# leading underscore. Without it REAPER cannot resolve the binding and the +# key silently does nothing. +n_keys=0 +while IFS='|' read -r flags code section id; do + [ -n "${flags:-}" ] || continue + if [ "$real_control" -eq 1 ] && [ $(( flags & 8 )) -ne 0 ]; then + flags=$(( flags - 8 + 32 )) + fi + # Replace an existing binding for the same key+section, else append. + awk -v flags="$flags" -v code="$code" -v section="$section" -v id="$id" ' + BEGIN { pat = "^KEY " flags " " code " [^ ]+ " section "([ \t]|$)" + new = "KEY " flags " " code " _" id " " section + done = 0 } + !done && $0 ~ pat { print new; done = 1; next } + { print } + END { if (!done) print new } + ' "$tmp" > "$tmp.2" + mv "$tmp.2" "$tmp" + n_keys=$(( n_keys + 1 )) +done < "$kb" + +n_actions=$(printf '%s' "$actions" | grep -c 'RSmidigrid') +echo "Installed $n_actions actions and $n_keys key bindings." +if [ "$real_control" -eq 1 ]; then + echo "Ctrl bindings use the physical Control key." +else + echo "On macOS the README's Ctrl+... bindings are Command+... here" + echo "(re-run with --real-control to use the Control key instead)." +fi +echo "Start REAPER, select a MIDI item, and press Alt+Shift+G (Option+Shift+G)."