Skip to content

feat(editor): export a track to Guitar Pro (.gp5) - #244

Merged
byrongamatos merged 3 commits into
mainfrom
feat/editor-gp5-export
Jul 13, 2026
Merged

feat(editor): export a track to Guitar Pro (.gp5)#244
byrongamatos merged 3 commits into
mainfrom
feat/editor-gp5-export

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What & why

The editor could import from Guitar Pro / MIDI / XML / sloppak but had no export out — no way to take a chart back into another tool. Gap-audit #4. This adds File ▸ Export ▸ Guitar Pro (.gp5), which downloads the current fretted track as a .gp5 file.

Approach

It reuses the exact bytes the read-only Tab preview already engraves: the Tab View plugin's GP5 conversion of the last-saved pack, fetched from the same endpoint. _tabPreviewUrlPure is now exported from tab-preview.js and imported here, so the tabview conversion contract lives in one place — the two surfaces can't drift.

New src/gp5-export.js owns the browser download plus its own pure, export-worded helpers:

  • _gp5ExportGuardPure — fretted-only (keys/drums have no tab), and a saved pack is required (the converter reads the last-saved pack).
  • _gp5ExportNamePure — the download filename: pack-extension drop + track name + cross-OS illegal-char sanitise.
  • _gp5ExportHttpMessagePure — honest 404 (Tab View plugin missing / unsaved) and 501 (host too old) messages.

Wired as a proper File command (shortcuts.js registry entry + _editorRunEofCommand case + menu-bar item), mirroring the sibling Tab-preview command; also exposed on window for parity.

Tests

  • tests/gp5_export.test.mjs (11) — guard truth table (no song / keys / piano / synth / drums / unsaved / fretted-OK), filename cases (extension drop incl. case-insensitive, no-part, missing-filename → track, illegal-char sanitise + whitespace collapse), the failure messages, and that export fetches the same tabview GP5 URL the preview does.
  • Full JS suite green (135), ESLint 0 errors. routes.py untouched (no pytest).
  • Live-verified on AC/DC — Back In Black with the Tab View plugin loaded (its PyGuitarPro dep already in the testbed venv): File ▸ Export triggers a real browser download named AC DC - Back In Black - Back In Black — Lead.gp5, 17,878 bytes with a valid FICHIER GUITAR PRO v5.1 header, and the status line confirms "Exported …".

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added File ▸ Export ▸ Guitar Pro (.gp5) to download the current fretted track as a .gp5 file.
    • Added a keyboard shortcut and shortcut-panel entry for GP5 export.
  • Bug Fixes
    • Improved export status messaging when export prerequisites aren’t met (e.g., unsaved content, unsupported arrangements, or missing prerequisites).
    • Added session-safe behavior: export is blocked/cancelled appropriately if the required save step is dismissed or the song changes mid-process.
  • Tests
    • Added automated coverage for GP5 export helpers and the export flow/session-safety behavior.

The editor had no export OUT — you could import from GP/MIDI/XML/sloppak
but never take a chart back out. Add File ▸ Export ▸ Guitar Pro (.gp5),
which downloads the current fretted track as a .gp5 file.

It reuses the exact bytes the read-only Tab preview already engraves: the
Tab View plugin's GP5 conversion of the last-saved pack, fetched from the
same endpoint. The endpoint format (_tabPreviewUrlPure) is now exported from
tab-preview.js and imported here, so the tabview contract lives in one place
and the two surfaces can't drift.

New src/gp5-export.js owns the download plus its own pure, export-worded
helpers: a fretted/saved guard (keys/drums have no tab; the converter reads
the saved pack), the download filename (pack-extension drop + track name +
cross-OS sanitise), and the honest 404/501 failure messages. Wired as a File
command (shortcuts.js registry + _editorRunEofCommand case + menu-bar item),
mirroring the sibling Tab preview command; also exposed on window.

Tests: tests/gp5_export.test.mjs (11) — guard truth table, filename cases
(extension drop, no-part, missing-filename, illegal-char sanitise), failure
messages, and that the export fetches the SAME tabview endpoint the preview
does. Live-verified on AC/DC with the Tab View plugin loaded: File▸Export
downloads a real 17.8KB "…— Lead.gp5" (valid GP5 header), status confirms.

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

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Adds Guitar Pro .gp5 export for saved fretted tracks, reusing the Tab View conversion endpoint. The feature includes validation, filename sanitization, error handling, browser downloads, File-menu and command wiring, and pure-helper tests.

Changes

Guitar Pro export

Layer / File(s) Summary
GP5 export implementation
src/gp5-export.js, src/tab-preview.js, CHANGELOG.md
Adds validated GP5 conversion downloads, sanitized filenames, mapped HTTP errors, and shared Tab View endpoint usage.
Menu and command wiring
src/menu-bar.js, src/shortcuts.js, src/input.js, src/main.js
Registers exportGp5 in the File menu and command system, dispatches it, and exposes the handler globally.
Export helper validation
tests/gp5_export.test.mjs
Tests guard decisions, filename formatting, HTTP messages, endpoint construction, session safety, and failure reporting.

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

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant FileMenu
  participant CommandDispatcher
  participant editorExportGp5
  participant TabViewEndpoint
  participant Browser
  Editor->>FileMenu: Select Export GP5
  FileMenu->>CommandDispatcher: Dispatch exportGp5
  CommandDispatcher->>editorExportGp5: Invoke export
  editorExportGp5->>TabViewEndpoint: Request GP5 conversion
  TabViewEndpoint-->>editorExportGp5: Return GP5 bytes
  editorExportGp5->>Browser: Download .gp5 file
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 summarizes the main change: exporting a track to Guitar Pro (.gp5).
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-gp5-export

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.

🧹 Nitpick comments (1)
src/gp5-export.js (1)

86-92: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a fetch timeout for the conversion request.

If the Tab View plugin backend hangs, editorExportGp5 has no way to recover — the "Exporting …" status persists indefinitely with no user-facing timeout or cancel path.

♻️ Proposed fix: bound the request with an AbortController
-        const resp = await fetch(_tabPreviewUrlPure(S.filename, S.currentArr, Date.now()));
+        const ctrl = new AbortController();
+        const timeoutId = setTimeout(() => ctrl.abort(), 15000);
+        let resp;
+        try {
+            resp = await fetch(_tabPreviewUrlPure(S.filename, S.currentArr, Date.now()), { signal: ctrl.signal });
+        } finally {
+            clearTimeout(timeoutId);
+        }
🤖 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/gp5-export.js` around lines 86 - 92, Update the conversion request in
editorExportGp5 to use an AbortController with a finite timeout, pass its signal
to fetch, and clear the timeout when the request settles. Handle an abort as a
user-facing export failure/status update rather than leaving “Exporting …”
indefinitely, while preserving the existing non-OK response handling.
🤖 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.

Nitpick comments:
In `@src/gp5-export.js`:
- Around line 86-92: Update the conversion request in editorExportGp5 to use an
AbortController with a finite timeout, pass its signal to fetch, and clear the
timeout when the request settles. Handle an abort as a user-facing export
failure/status update rather than leaving “Exporting …” indefinitely, while
preserving the existing non-OK response handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 90c39143-8048-4d81-b367-035987c012d0

📥 Commits

Reviewing files that changed from the base of the PR and between 4968f8b and 66febf1.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • src/gp5-export.js
  • src/input.js
  • src/main.js
  • src/menu-bar.js
  • src/shortcuts.js
  • src/tab-preview.js
  • tests/gp5_export.test.mjs

The tabview converter reads the SAVED pack and indexes it by S.currentArr,
clamping that index into the saved track list. So exporting mid-edit didn't
just drop unsaved notes: with an added or reordered track the clamp handed
back a DIFFERENT track's bytes under the requested track's name.

Route the export through the session's own guardSessionTransition prompt
(Save / Don't Save / Cancel) and re-check the guards after that await — the
song can close while the dialog is up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/gp5-export.js (1)

89-99: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include the transition guard in the error boundary.

The try block begins after await guardSessionTransition(...). If the prompt or host.saveSession() rejects, editorExportGp5() produces an unhandled rejection and no failure status. Move the try around the entire orchestration or catch this await separately.

🤖 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/gp5-export.js` around lines 89 - 99, Update editorExportGp5 so the await
guardSessionTransition call and subsequent session-transition orchestration are
covered by the existing error boundary. Ensure rejections from the prompt or
host.saveSession are caught and produce the same failure status handling as
export errors, while preserving the cancellation return path.
🧹 Nitpick comments (1)
tests/gp5_export.test.mjs (1)

109-172: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a test for the pre-prompt short-circuit.

The three orchestrator tests cover cancel-aborts, prompt-precedes-fetch, and guard-recheck-after-prompt, but none verify that when the initial guard already fails (e.g. a never-saved song with no filename, S.filename === ''), guardSessionTransition is never invoked at all. Per the src control flow shown in src/gp5-export.js:19-115, the pre-check runs and returns before the prompt is ever awaited: if (!guard.ok) { setStatus(guard.reason); return; }. This orchestration-level short-circuit isn't protected by the pure _gp5ExportGuardPure truth table (lines 31-57), which only tests the guard function in isolation — a future refactor could reorder the checks to always prompt first without any test catching it.

✅ Suggested additional test
await ta('never-saved session: the guard fails before the save prompt is ever shown', async () => {
    const env = exportEnv({ S: { arrangements: [], currentArr: 0, filename: '' } });
    env.guardSessionTransition = async () => { throw new Error('prompt must not run when the initial guard already fails'); };
    await env.run();
    assert.deepStrictEqual(env.fetched, []);
    assert.deepStrictEqual(env.downloads, []);
    assert.match(env.statuses.join(' '), /Load a song first/);
});
🤖 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 `@tests/gp5_export.test.mjs` around lines 109 - 172, Add an orchestrator-level
test alongside the existing editorExportGp5 tests for an unsaved session with
empty arrangements and filename. Make guardSessionTransition throw if invoked,
run the export, and assert no fetches or downloads occur while statuses report
“Load a song first,” verifying the initial guard short-circuits before
prompting.
🤖 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/gp5-export.js`:
- Around line 89-96: Update the export flow around guardSessionTransition so it
exposes the transition choice and continues only when the result is save; treat
discard as export cancellation. Ensure arr, guard, and subsequent conversion
logic execute only after a successful save, preserving the existing cancellation
status behavior.

---

Outside diff comments:
In `@src/gp5-export.js`:
- Around line 89-99: Update editorExportGp5 so the await guardSessionTransition
call and subsequent session-transition orchestration are covered by the existing
error boundary. Ensure rejections from the prompt or host.saveSession are caught
and produce the same failure status handling as export errors, while preserving
the cancellation return path.

---

Nitpick comments:
In `@tests/gp5_export.test.mjs`:
- Around line 109-172: Add an orchestrator-level test alongside the existing
editorExportGp5 tests for an unsaved session with empty arrangements and
filename. Make guardSessionTransition throw if invoked, run the export, and
assert no fetches or downloads occur while statuses report “Load a song first,”
verifying the initial guard short-circuits before prompting.
🪄 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: 5d0b1833-a8d7-4024-9637-4af2b295420f

📥 Commits

Reviewing files that changed from the base of the PR and between 66febf1 and 73f0ecc.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • node_modules
  • src/gp5-export.js
  • tests/gp5_export.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment thread src/gp5-export.js
Comment on lines +89 to +96
if (!(await guardSessionTransition('exporting to Guitar Pro'))) {
setStatus('Export cancelled.');
return;
}
// The prompt awaited: the song (and the current part) may have moved.
arr = cur();
guard = _gp5ExportGuardPure(S.filename, arr && arr.name, !!S.arrangements.length);
if (!guard.ok) { setStatus(guard.reason); return; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not continue after “Don’t Save” for this export.

guardSessionTransition returns true for both discard and successful save. The export can therefore use unsaved S metadata/current index while the converter reads the saved pack, exporting stale or even another track’s bytes under the current filename. Expose the transition choice and require save, or cancel export after discard.

🧰 Tools
🪛 ast-grep (0.44.1)

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

(usestate-direct-usage)


[error] 95-95: React's useState should not be directly called
Context: setStatus(guard.reason)
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/gp5-export.js` around lines 89 - 96, Update the export flow around
guardSessionTransition so it exposes the transition choice and continues only
when the result is save; treat discard as export cancellation. Ensure arr,
guard, and subsequent conversion logic execute only after a successful save,
preserving the existing cancellation status behavior.

@byrongamatos
byrongamatos merged commit 895ac00 into main Jul 13, 2026
3 of 4 checks passed
@byrongamatos
byrongamatos deleted the feat/editor-gp5-export branch July 13, 2026 21:48
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