Skip to content

[shortcuts] Add import and export backups for Keyboard shortcuts#4226

Open
jensenpat wants to merge 1 commit into
aethersdr:mainfrom
jensenpat:aether/shortcut-import-export
Open

[shortcuts] Add import and export backups for Keyboard shortcuts#4226
jensenpat wants to merge 1 commit into
aethersdr:mainfrom
jensenpat:aether/shortcut-import-export

Conversation

@jensenpat

@jensenpat jensenpat commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds portable shortcut backup and cloning to the Shortcuts Editor. Users can now export their complete keyboard mapping to CSV and import it on another AetherSDR installation, including across older and newer releases.

The editor footer now presents the controls in this order:

Import... → Export... → Reset All to Defaults → Close

What changed

  • Added Import... and Export... controls with accessible names and descriptions.
  • Added a versioned CSV format with these additive columns:
    • FORMAT_VERSION
    • ACTION_ID
    • ACTION_NAME
    • CATEGORY
    • SHORTCUT
    • CUSTOMIZED
  • Serializes key combinations with QKeySequence::PortableText, keeping modifiers portable across platforms (Alt for macOS Option, for example).
  • Uses the stable action ID as the primary identity and the exact, unique readable action name as a conservative fallback if an internal ID changes.
  • Skips and reports actions that are unavailable in the importing release instead of rejecting the complete backup.
  • Leaves local actions absent from an older backup unchanged.
  • Restores customized and explicitly cleared mappings exactly, while non-customized rows adopt the importing release's current default.
  • Resolves shortcut conflicts as one batch so customized mappings win over colliding defaults regardless of CSV row order.
  • Writes exports atomically with QSaveFile.
  • Rejects invalid UTF-8, malformed quoting, invalid fields, duplicate action rows, duplicate custom mappings, oversized files, and oversized fields before changing any shortcut.
  • Updated the in-app controls help with backup, cloning, and cross-release behavior.

Cross-release behavior

The exported file remains useful when moving both forward and backward through AetherSDR releases:

  • Importing a newer backup into an older release applies every recognized action and reports unknown future actions as skipped.
  • Importing an older backup into a newer release applies its known rows while leaving newer local actions alone.
  • Default mappings track the defaults of the release receiving the import rather than pinning an old release's default values.
  • The original backup retains skipped rows, so returning to a newer release restores those capabilities normally.

User impact

Operators can back up carefully customized keyboard layouts, clone them between shack computers, and move between macOS, Windows, and Linux without manually recreating every mapping. Import errors are non-destructive: no binding is changed unless the complete file parses and resolves successfully.

Screenshot

Shortcuts Editor showing Import, Export, Reset All to Defaults, and Close buttons

Validation

  • cmake --build build --target AetherSDR shortcut_manager_test -j8
  • ctest --test-dir build -R '^shortcut_manager_test$' --output-on-failure
  • python3 tools/check_engine_boundary.py --strict — 0 blockers; existing tracked warnings only
  • python3 tools/check_a11y.py — existing warning-only findings; no new finding in the changed dialog
  • GUI verification with the radio disconnected:
    • opened and captured the Shortcuts Editor
    • confirmed the final button order and layout
  • Production file-transfer tests verify export, edited-file import, persistence, and malformed-file rejection without changing live bindings.

Test coverage

The shortcut manager test now covers:

  • schema and RFC-style CSV quoting
  • portable modifier/key serialization
  • customized bindings and explicit clears
  • importing-release default semantics
  • unknown future-action handling
  • action-name fallback after an internal ID rename
  • batch conflict normalization
  • malformed-file atomicity
  • atomic filesystem export and production-path import

👨🏼‍💻 Generated with OpenAI Codex (GPT-5.6 Sol) and tested by @jensenpat

@jensenpat jensenpat self-assigned this Jul 13, 2026
@jensenpat
jensenpat marked this pull request as ready for review July 13, 2026 05:19
@jensenpat
jensenpat requested review from a team as code owners July 13, 2026 05:19

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thorough, well-tested work — thanks for this, @jensenpat. The RFC-style CSV parser, the batch-atomic import (nothing changes unless the whole file parses and resolves), and the id-primary / name-fallback identity model are all handled carefully, and the cross-release default semantics (persisted → CUSTOMIZED, non-customized rows adopting the importing release's default) round-trip correctly. Conflict resolution — clear colliding non-imported keys, apply, then reuse the startup normalizeDuplicateBindings() so customized wins over a default restored by a later row — is consistent with the existing setBinding() behavior. Both entry points rebuild live QShortcuts (GUI via the post-exec() rebuild in MainWindow_Menus.cpp, automation inline), export is atomic via QSaveFile, and conventions look right (AppSettings, no QSettings, RAII). All seven checks pass, and nothing is out of the stated scope.

I have no blocking findings.

Non-blocking notes

  • importFromFile() reads the whole file into memory before the 1 MiB guard inside importBindingsCsv() runs — a pre-read file.size() check at the boundary would be a touch more defensive. See inline. Since the path is operator/user-selected and the byte guard still catches it, this is purely polish.

🤖 aethersdr-agent · cost: $8.1291 · model: claude-opus-4-8

.arg(QDir::toNativeSeparators(path));
return result;
}
return manager.importBindingsCsv(file.readAll());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor / non-blocking: readAll() pulls the entire file into memory before the 1 MiB guard in parseCsvRows() gets a chance to reject it, so pointing import at a multi-GB file loads it all first. Since this is a user/operator-selected local path and the byte-size check still fires post-read, it's low severity — but a file.size() check here (mirroring kMaxShortcutCsvBytes) would reject oversized files at the boundary without the allocation. Left as a plain note rather than a suggestion to avoid duplicating the limit constant across the file boundary.

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Really nicely done — this is a clean, self-contained feature with a hand-rolled RFC-4180 CSV parser that handles quoting/UTF-8/size limits, batch-atomic imports that touch no binding unless the whole file resolves, and thoughtful cross-release semantics (stable-id primary key with an exact-name fallback, importing-release defaults for non-customized rows, explicit-clear preservation). The ShortcutFileTransfer seam that lets the Shortcuts Editor buttons and the shortcutConfig bridge verb share one parser/writer/persistence/rebuild path is the right factoring, and it's backed by real tests. All 7 CI checks are green and the commit is signed. Conventions look good — AppSettings (not QSettings), QSaveFile for atomic writes, RAII throughout, no leaks.

I found nothing blocking. A couple of small notes below.

Polish

  • parseShortcutCsv treats any non-empty SHORTCUT that QKeySequence::fromString can't decode as valid because unrecognized tokens decode to Qt::Key_unknown rather than an empty sequence — see inline. Hand-edited-CSV path only; exports never produce it.

Non-blocking notes

  • docs/automation-bridge.md — the shortcutConfig examples show "exported":146/"imported":146, but the registry is ~92 actions (per your own validation). Worth aligning the placeholder so the doc doesn't imply a specific count.
  • Not this PR, but a heads-up: shortcutConfig import mutates persisted state + rebuilds live shortcuts, so when the observe-only bridge mode (#4208) lands it'll want to be outside that allowlist.

🤖 aethersdr-agent · cost: $7.9034 · model: claude-opus-4-8


const QKeySequence shortcut = QKeySequence::fromString(
shortcutText, QKeySequence::PortableText);
if (!shortcutText.isEmpty() && shortcut.isEmpty()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

QKeySequence::fromString("Frobnicate", PortableText) doesn't return an empty sequence for un-decodable text — Qt maps unknown tokens to Qt::Key_unknown, so shortcut.isEmpty() is false and a garbage SHORTCUT value in a hand-edited CSV slips through as an unusable binding rather than being rejected. Since the code already reads currentKey[0].key() elsewhere, catching the unknown key here closes the gap:

Suggested change
if (!shortcutText.isEmpty() && shortcut.isEmpty()) {
if (!shortcutText.isEmpty()
&& (shortcut.isEmpty() || shortcut[0].key() == Qt::Key_unknown)) {

@jensenpat
jensenpat force-pushed the aether/shortcut-import-export branch from 3dca302 to 90c2ae4 Compare July 14, 2026 05:27
@jensenpat jensenpat changed the title Add portable Shortcuts Editor import/export and automation coverage Add portable Shortcuts Editor import and export Jul 14, 2026
@jensenpat jensenpat changed the title Add portable Shortcuts Editor import and export [shortcuts] Add portable Shortcuts Editor import and export Jul 14, 2026
@jensenpat jensenpat changed the title [shortcuts] Add portable Shortcuts Editor import and export [shortcuts] Add import and export backups for Keyboard shortcuts Jul 14, 2026
@jensenpat

Copy link
Copy Markdown
Collaborator Author

Holding for next weeks release.

@jensenpat
jensenpat marked this pull request as draft July 17, 2026 16:24
@jensenpat
jensenpat marked this pull request as ready for review July 19, 2026 03:23
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.

Move keyboard shortcuts to dedicated settings file with import/export

1 participant