Skip to content

feat(editor): keyboard note entry — place notes without a mouse - #239

Merged
byrongamatos merged 2 commits into
mainfrom
feat/editor-keyboard-entry
Jul 13, 2026
Merged

feat(editor): keyboard note entry — place notes without a mouse#239
byrongamatos merged 2 commits into
mainfrom
feat/editor-keyboard-entry

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Gap-audit item #8 — the biggest Guitar-Pro-parity gap in the place/edit loop: every note add required a mouse double-click.

What's new

In String view with nothing selected, an entry caret appears — a dashed cell at the playhead on the current string. ↑/↓ move it between strings and a fret digit (0-9) places a note there, then advances the caret one snap step, so you can type a run without touching the mouse.

The behaviour is a clean no-selection overload, so nothing changes for editing:

  • digit → places at the caret when the selection is empty; sets the selected note's fret when something is selected (as before);
  • ↑/↓ → move the caret when empty; move the selected note's string when something is selected (as before).

Implementation

  • New S.caretString; _editorPlaceAtCaret reuses the mouse-add AddNoteCmd path, so a keyboard-placed note is byte-identical to a clicked one and undoable; it then clears the selection and advances the caret.
  • Caret string math mirrors _getMoveStringSameFretResult (n.string + direction) so ↑/↓ feel identical whether or not a note is held.
  • Caret indicator drawn in draw.js (String view, empty selection only).

Tests / gates

tests/keyboard_entry.test.mjs (4: place-on-empty, undoable, digit-still-edits-with-a-selection, caret move + clamp). 129 JS suites green, lint 0 errors (3 pre-existing warnings). routes.py untouched.

Verified live

On AC/DC — Back In Black: the dashed caret renders on the caret string; typing "7""Placed fret 7 on string 1"; "Entry caret on string 2"; "5" placed on string 2; both undone by Ctrl+Z. No page errors.

v1 scope: String view / fretted parts (keys enter by pitch); the caret time is positioned by the playhead (click the timeline to move it) and auto-advances on each placement. A future pass could add ←/→ caret-time nudge and selection-time nudge (gap #1).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added keyboard-only string-lane entry in String view: when no note is selected, a dashed caret appears at the playhead for the current string.
    • Use ↑/↓ to move the entry caret between strings, then type fret digits 0–9 to place notes.
    • Caret advances automatically after each placed note; entries are undoable.
    • Existing keyboard behavior is preserved when a note is selected (digits edit fret; ↑/↓ changes the selected note’s string).
  • Tests
    • Added coverage for caret placement, undo behavior, and caret clamping.

Gap-audit item #8 — the biggest Guitar-Pro-parity gap in the place/edit loop:
every note add required a mouse double-click. Now, in String view with NOTHING
selected, an entry caret appears (a dashed cell at the playhead on the current
string); ↑/↓ move it between strings and a fret digit (0-9) places a note there,
then advances the caret one snap step so you can type a run.

- New `S.caretString`; `_editorPlaceAtCaret` reuses the mouse-add AddNoteCmd path
  (so a keyboard-placed note is identical + undoable), then clears the selection
  and advances the caret for rapid sequential entry.
- Clean "no-selection overload": `_editorSetSelectedFret` places at the caret
  when the selection is empty (else edits the selected fret as before), and the
  moveStringUp/Down dispatch moves the caret when empty (else moves the selected
  note's string). Caret string math mirrors `_getMoveStringSameFretResult`
  (n.string + direction) so ↑/↓ feel identical held or not.
- Caret indicator drawn in draw.js (String view, empty selection only).

`tests/keyboard_entry.test.mjs` (4: place-on-empty, undoable, digit-still-edits-
with-a-selection, caret move + clamp). 129 JS suites green, lint 0-err (3
pre-existing warnings). routes.py untouched.

Verified live on AC/DC — Back In Black: the dashed caret renders on the caret
string; "7" → "Placed fret 7 on string 1"; ↑ → "Entry caret on string 2"; "5"
placed on string 2; both undone by Ctrl+Z. No page errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a400354b-d14e-4754-80df-9c58db711d11

📥 Commits

Reviewing files that changed from the base of the PR and between f9894e3 and ba754dd.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/draw.js
  • src/input.js
  • src/state.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/state.js
  • CHANGELOG.md
  • src/input.js
  • src/draw.js

📝 Walkthrough

Walkthrough

String view now supports keyboard note entry through a caret. Arrow keys move the caret between strings, fret digits place undoable notes and advance time, and a dashed overlay shows the insertion location when no note is selected.

Changes

Keyboard note-entry workflow

Layer / File(s) Summary
Caret state and keyboard placement
src/state.js, src/input.js, CHANGELOG.md
Adds S.caretString, caret navigation, fret-digit note placement with clamping and undo support, while preserving selected-note editing behavior.
Entry caret rendering
src/draw.js
Displays the caret cell in String view when keys mode is inactive and no note is selected.
Keyboard workflow validation
tests/keyboard_entry.test.mjs
Tests placement, undo, selected-note editing, string navigation, and bounds clamping.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Keyboard
  participant EditorInput
  participant History
  participant EditorState
  participant StringView
  Keyboard->>EditorInput: Type fret digit
  EditorInput->>EditorState: Read cursor time and caret string
  EditorInput->>History: Execute AddNoteCmd
  History->>EditorState: Add note and clear selection
  EditorInput->>StringView: Redraw advanced caret
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: keyboard note entry in the editor without using the mouse.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-keyboard-entry

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/draw.js`:
- Around line 479-481: Update the caret overlay calculation in the !keysMode and
empty-selection branch to clamp S.caretString to the active arrangement’s string
count from _stringCountFor(S.arrangements[S.currentArr]) before passing it to
strToY(). Keep the existing caret fallback and coordinate calculation unchanged
so the overlay matches placement behavior.

In `@src/input.js`:
- Around line 111-120: The note insertion flow around AddNoteCmd must keep the
target and caret quantized consistently. Use the already computed snapped time
for the caret advance instead of raw S.cursorTime, and ensure the rendered
insertion target uses that same quantized time when calling _editBlipAt;
preserve the existing selection-clearing and snap-step behavior.
- Around line 105-110: Update _editorPlaceAtCaret and the related caret-command
helpers at the referenced branches to return without changing state when
S.drumEditMode or S.tempoMapMode is active, in addition to the existing
keys-mode guard. Preserve normal String-note editing behavior, and add
regression coverage confirming these commands neither add notes nor move the
caret in drum or tempo-map modes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3112c52e-f986-430e-87f7-fb55bb339645

📥 Commits

Reviewing files that changed from the base of the PR and between 33521a4 and f9894e3.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/draw.js
  • src/input.js
  • src/state.js
  • tests/keyboard_entry.test.mjs

Comment thread src/draw.js
Comment on lines +479 to +481
if (!keysMode && S.sel.size === 0) {
const cx = timeToX(S.cursorTime || 0);
const cy = strToY(S.caretString || 0) + NOTE_PAD;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clamp the displayed caret to the active arrangement.

After switching from a wider part to a narrower one, S.caretString can be out of range: placement clamps it locally in src/input.js, while this overlay renders the stale value. Clamp against _stringCountFor(S.arrangements[S.currentArr]) before strToY() so the visible target matches the note that will be placed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/draw.js` around lines 479 - 481, Update the caret overlay calculation in
the !keysMode and empty-selection branch to clamp S.caretString to the active
arrangement’s string count from _stringCountFor(S.arrangements[S.currentArr])
before passing it to strToY(). Keep the existing caret fallback and coordinate
calculation unchanged so the overlay matches placement behavior.

Comment thread src/input.js
Comment on lines +105 to +110
function _editorPlaceAtCaret(fret) {
if (isKeysMode()) { setStatus('Select notes first'); return false; }
const arr = S.arrangements && S.arrangements[S.currentArr];
const nStr = arr ? _stringCountFor(arr) : 0;
if (!nStr) { setStatus('Select notes first'); return false; }
const string = Math.max(0, Math.min(nStr - 1, Number(S.caretString) || 0));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent caret commands outside normal String-note editing.

_editorCurrentNoteIndices() intentionally returns no selection in drum and tempo-map modes, so these branches can add a fretted note or move the hidden caret there. Guard both helpers against S.drumEditMode and S.tempoMapMode (as well as keys mode) before changing state, and add regression coverage for those modes.

Also applies to: 129-138, 830-831

🧰 Tools
🪛 ast-grep (0.44.1)

[error] 105-105: React's useState should not be directly called
Context: setStatus('Select notes first')
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)


[error] 108-108: React's useState should not be directly called
Context: setStatus('Select notes first')
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/input.js` around lines 105 - 110, Update _editorPlaceAtCaret and the
related caret-command helpers at the referenced branches to return without
changing state when S.drumEditMode or S.tempoMapMode is active, in addition to
the existing keys-mode guard. Preserve normal String-note editing behavior, and
add regression coverage confirming these commands neither add notes nor move the
caret in drum or tempo-map modes.

Comment thread src/input.js
Comment on lines +111 to +120
const time = snapTime(S.cursorTime || 0);
const note = { time, string, fret: Math.max(0, Math.min(24, Number(fret) || 0)), sustain: 0, techniques: {} };
const cmd = new AddNoteCmd(note);
S.history.exec(cmd);
_tourNoteAction('placeNote');
_editBlipAt();
// Entry flow: leave NO selection (so the next digit places again) and advance
// the caret one snap step for rapid sequential entry.
S.sel.clear();
_editorSeekToTime((S.cursorTime || 0) + _editorSnapStepSeconds());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the insertion target and caret position quantized consistently.

Placement uses snapTime(S.cursorTime), but the caret advances from raw S.cursorTime. With an off-grid playhead, the dashed target in src/draw.js line 480 differs from the inserted note and remains offset after entry. Advance from the snapped time and render the same quantized insertion time.

🧰 Tools
🪛 OpenGrep (1.23.0)

[ERROR] 114-114: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/input.js` around lines 111 - 120, The note insertion flow around
AddNoteCmd must keep the target and caret quantized consistently. Use the
already computed snapped time for the caret advance instead of raw S.cursorTime,
and ensure the rendered insertion target uses that same quantized time when
calling _editBlipAt; preserve the existing selection-clearing and snap-step
behavior.

@byrongamatos
byrongamatos merged commit 309c769 into main Jul 13, 2026
5 of 7 checks passed
@byrongamatos
byrongamatos deleted the feat/editor-keyboard-entry branch July 13, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants