Skip to content

Keyboard Shortcuts: Import… and File › Open read text shortcut sets (.kys) - #1223

Merged
bflatastic merged 7 commits into
storytold:mainfrom
dotCooCoo:feat/shortcut-sets
Oct 14, 2026
Merged

bflatastic merged 7 commits into
storytold:mainfrom
dotCooCoo:feat/shortcut-sets

Conversation

@dotCooCoo

@dotCooCoo dotCooCoo commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What and why

A shortcut set another app saved as slash-name text (a .kys file whose text starts with a slash name and {, such
as /Menus {) opens in a new Import Shortcut Set dialog from File › Open, from Edit › Keyboard Shortcuts › Import… in
the desktop app, or from a drop on the window, whether or not a document is open.

  • The dialog proposes a target for an entry when an earlier import kept one for that name, or else when one tool,
    command or panel matches the name clearly by its id or by one of its labels (English or the interface language).
    The file's key and modifier codes are decoded from the codes earlier imports kept, from the README example cited
    below (/Key 48 for the 0 key, with /Modifiers 0 taken as no modifiers), and from VectorCraft's default shortcuts
    where at least two entries vote and four votes in five agree.
  • The user picks the remaining targets, presses the key for each unknown key code or marks it No equivalent, and
    chooses the modifiers for each unknown modifier code. Apply assigns the ready rows with the Keyboard Shortcuts
    rules, and Keyboard Shortcuts shows the result as unsaved changes until the user clicks OK. Copy Report copies the
    rows left out.
  • shortcutSet.read and importMatches.list, .set and .reset are the engine commands, and docs/mcp.md describes
    them. With Remember My Choices on (the default), Apply keeps the user's choices with the preferences, and for the
    rows it assigns, it also keeps the matched targets and the codes decoded by agreement. A later import proposes
    them, and resetting the preferences keeps them. With --automation-read-root (mcp: add --automation-read-root / --automation-write-root so the server can be confined to a folder #832), shortcutSet.read {path} and
    ui.shortcutImportDialog {path} return an error for a path outside the read root.
  • When Import Shortcut Set is opened from Keyboard Shortcuts, Cancel, Escape and ui.dialog.cancel close it and
    return to Keyboard Shortcuts with its unsaved changes kept (dialogs::PARENT). A document's queued Missing Fonts
    dialog (Fonts: missing fonts are listed when a document opens, with Find Fonts and a folder search #792) opens after Keyboard Shortcuts closes.

The reader follows the signature and keywords at https://filext.com/file-extension/KYS and the one example in the
MIT-licensed AiCommandPalette README (https://github.com/joshbduncan/AiCommandPalette), an entry from a set saved with
the reference app. Apart from the signature, the keywords and that example, nothing in the change comes from the
reference app, and nothing comes from its SDK. The change does not include a table of the reference app's command
names. The tests write their sets from scratch with invented names; their codes are made up or follow the README
example.

How it was tested

This change adds engine, UI and fuzz tests. One UI test drops a set on the window while a document is open and
checks that it opens in Import Shortcut Set. On macOS 27.0.1 with rustc 1.99.0, on top of main at 5a120e3:

I ran this change's tests on macOS only.

Checklist

  • cargo xtask ci passes (fmt, clippy, tests, assets, brands, layers, wasm).
  • No panics in shipped code: no unwrap(), expect(), panic!, unreachable!, todo! or
    unimplemented! outside tests, and no unchecked indexing on data from files, parameters or
    messages. Errors are returned as Result and shown to the user
    (why and how).
  • Clean-room: nothing copied from Adobe products or GPL/AGPL code (AGENTS.md).
  • Every new asset has a row in ASSETS.md.

@bflatastic

Copy link
Copy Markdown
Member

Thanks @dotCooCoo, this is a big and careful piece of work.

I'm holding it for the owner rather than merging it tonight. It adds a new product feature rather than matching an existing Illustrator behaviour, so a few calls are his to make:

  • whether VectorCraft should import shortcut sets saved by other apps at all, and through File › Open and drops as well as Keyboard Shortcuts › Import…;
  • the key-code guessing by agreement with our default shortcuts (two votes, four in five agreeing);
  • keeping the import choices with the preferences, on by default, and keeping them across a preferences reset.

I read the diff and found no security concerns. It isn't rebased or gated yet; I'll do that once the owner has decided.

@dotCooCoo

Copy link
Copy Markdown
Contributor Author

Added the cs message rows that a59d8533 now requires. The branch now conflicts with main in crates/ui-egui/src/dialogs/mod.rs and docs/roadmap.md (#1169, #866); keeping both sides resolves each.

@dotCooCoo

Copy link
Copy Markdown
Contributor Author

I merged main at 2ac3ba2 into the branch. I resolved the two conflicts, in crates/ui-egui/src/dialogs/mod.rs and the M14 row of docs/roadmap.md, by keeping both sides. On the merged tree, CRAFT_FONTS_DIR=<craft-fonts checkout> cargo xtask ci passes its 7 steps: 5,417 tests passed, 0 failed.

@dotCooCoo

Copy link
Copy Markdown
Contributor Author

I merged main at b94c13b into the branch and added the hu rows that b9716366 now requires. On the merged tree, CRAFT_FONTS_DIR=<craft-fonts checkout> cargo xtask ci passes its 7 steps: 5,628 tests passed, 0 failed.

@bflatastic

Copy link
Copy Markdown
Member

Thanks @dotCooCoo for keeping this current with main and adding the Czech and Hungarian rows. It still waits on the owner's product decisions listed above. Nothing more is needed from you for now.

@dotCooCoo

Copy link
Copy Markdown
Contributor Author

I merged main at 5a120e3 into the branch and resolved the conflicts in crates/engine/src/cmd/fileio/mod.rs, crates/engine/src/cmd/fileio/tests.rs, crates/ui-egui/src/io.rs, crates/ui-egui/src/place.rs, docs/mcp.md and docs/roadmap.md by keeping both sides; in io.rs the .kys route now sits in open_file, next to the script route. Two more commits remove 26 catalog rows that main now has too, and fix importMatches.list's description so that it parses under main's new check, with a test. I also added the ko, pl and vi rows that main now requires. On the merged tree, with the Mac fix from #1761 merged in, CRAFT_FONTS_DIR=<craft-fonts checkout> cargo xtask ci passes its 7 steps: 6,590 tests passed, 0 failed.

dotCooCoo and others added 7 commits October 14, 2026 13:22
….kys)

A shortcut set another app saved as slash-name text (a .kys file whose
text starts with a slash name and "{", such as "/Menus {") opens in
Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts ›
Import… in the desktop app, or from a drop on the window, whether or
not a document is open. The dialog proposes a target for an entry when
an earlier import kept one for that name, or else when one tool,
command or panel matches the name clearly by its id or by one of its
labels. It decodes the file's key and modifier codes from the codes
earlier imports kept, from the example in the AiCommandPalette README
(/Key 48 for the 0 key, with /Modifiers 0 taken as no modifiers), and
from VectorCraft's default shortcuts where at least two entries vote and
four votes in five agree. The user picks the remaining targets, presses
the key for each unknown key code or marks it No equivalent, and chooses
the modifiers for each unknown modifier code. Apply assigns the ready
rows with the Keyboard Shortcuts rules: a tool takes a single key,
optionally with Shift. It goes through the rows from the last one back
and leaves a row out when a later row it applies has the same shortcut
or the same tool, command or panel. Keyboard Shortcuts then shows the
result as unsaved changes until the user clicks OK. Copy Report copies
the set's name, the rows Apply leaves out and the parts of the file that
weren't read.

- shortcutSet.read is a query command: it reads a set and does not
  modify the document or the preferences. It returns each entry with
  its section, codes and line, and up to 1,000 skipped tokens, each cut
  to 80 characters. It reads at most 4 MiB of text and 20,000 entries,
  skips blocks nested more than 16 deep and cuts names at 1,024 bytes.
  With a path it reads at most 4 MiB and 4 bytes of the file, and with
  --automation-read-root (storytold#832) the path must lie inside the read root.
  A file whose text starts with "<", such as an XML shortcut file saved
  as UTF-8, fails with "This shortcut file is in a form VectorCraft
  can't read."
- importMatches.list, importMatches.set and importMatches.reset read
  and change the matches kept with the preferences; resetting the
  preferences keeps them. With Remember My Choices on (the default),
  Apply keeps the targets and codes the user chose, and for the rows it
  assigns, it also keeps the matched targets and the codes decoded by
  agreement. A later import proposes them.
- The name matcher compares a name with ids by exact match and shared
  words. It compares the name with labels, in English and in the
  interface language, by exact match, shared words and character
  pairs. It proposes a match only when one tool, command or panel
  scores at least 0.6 and at least 0.1 more than the next one.
- When Import Shortcut Set is opened from Keyboard Shortcuts, Cancel,
  Escape and ui.dialog.cancel close it and return to Keyboard Shortcuts
  as it was (dialogs::PARENT). A document's queued Missing Fonts dialog
  (storytold#792) opens after Keyboard Shortcuts closes.
- File › Open has a "Shortcut sets" filter, and in the desktop app
  Keyboard Shortcuts' Import… shows .json and .kys files.

Docs: docs/mcp.md, docs/control-protocol.md, docs/development.md,
docs/target-app-parity.md, docs/roadmap.md.
Since storytold#1189, Czech is a complete catalog: every menu string, tl! literal
and status or error message has a cs row, and the i18n tests check it.
cs.tsv gets the rows that the other complete catalogs have for Import
Shortcut Set and its commands: 35 strings, the two plurals with one, few
and other forms, and the two error messages. Shortcut and the Imported
message already have cs rows on main, so they are not repeated. The rows
are placed after the Keyboard Shortcuts… row.
Since b971636 (storytold#1302), Hungarian is a complete catalog: every menu
string, tl! literal and status or error message has a hu row, and the
i18n tests check it. hu.tsv gets the rows that the other complete
catalogs have for Import Shortcut Set and its commands: 35 strings, the
two plurals in the single form hu.tsv uses (a count takes the
singular), and the two error messages. Shortcut and the Imported
message already have hu rows on main, so they are not repeated. The
rows are placed after the Keyboard Shortcuts… row.
Check Script (3374d07) added Copy Report and Not read to every catalog,
and es and fr gained Shortcut and the Imported message in a747daa and
9e39a6d. The rows that Import Shortcut Set added for the same strings
became second entries, which bundled_catalogs_are_consistent rejects.
They are removed, 26 rows in cs, de, es, fr, hu, it, ja, ru, uk, zh-hans
and zh-hant, and the existing rows stay as they are.
…its commands

Since storytold#1352, storytold#1498 and storytold#1507, Korean, Polish and Vietnamese are complete
catalogs: every menu string, tl! literal and status or error message has
a row, and the i18n tests check it. ko.tsv, pl.tsv and vi.tsv get the
rows that the other complete catalogs have for Import Shortcut Set and
its commands: the dialog's strings, the labels Import Shortcut Set…,
Read Shortcut Set File, Import Matches, Remember Import Matches and
Forget Import Matches, the two plurals and the two error messages. Copy
Report and Not read are already in all three, and Shortcut and the
Imported message in pl, so they are not repeated. The dialog's rows go
after Import Print Presets in ko and vi and after the Keyboard Shortcuts
dialog's rows in pl, and the two messages after "not a VectorCraft
shortcut set".
…ey with an error

Since storytold#1442 (3acc29f), a caller outside the app gets an error for a
parameter key that the command's params description doesn't name, and
every_description_parses requires each top-level entry of the params
object to start with a key. The note on query in importMatches.list had
a comma at the top level ("text in the name, target or label"), so the
description didn't parse: the test failed, and the command took any key.
The note is now in parentheses, and the command takes kind and query.
A test checks that shortcutSet.read and the three importMatches
commands take every key they read and refuse a misspelled one.
… hostile files

A set whose entries sit in long, deeply nested sections kept every section's names again for each entry (up to hundreds of megabytes from a 4 MiB file); the names kept over all entries now stop at 8 MiB. Name matching compares character pairs in linear time, so a 20,000-entry set of long names is matched in about 2 s instead of 17 s.
@bflatastic
bflatastic merged commit a067177 into storytold:main Oct 14, 2026
1 check passed
@bflatastic

Copy link
Copy Markdown
Member

Thanks @dotCooCoo, merged.

The owner decided to have the feature as you designed it, so the three questions I held it on are settled.

What I changed:

  • Rebased your six feature commits onto current main without the merge commits. Your merges had already resolved the conflicts, so your diff came through unchanged.
  • Added one commit on top. A set whose entries sit in long, deeply nested sections copied every section name again for each entry. A 4 MiB file could fill hundreds of megabytes that way, so the reader now stops at 8 MiB of names over all the entries, with a test. Name matching now compares character pairs in linear time. Before, a hostile set of 20,000 long names held the UI for about 17 s while the dialog opened; now it takes about 2 s, and a 500-entry set opens in 45 ms.

What I checked:

  • Only this feature is on the branch: no files from fix(mcp): discard malformed JSON replies before reusing connections #1761 or anything else.
  • The reader has no recursion, caps on depth, entries, names and unread tokens, and fuzz coverage. Remembered matches are capped at 20,000 and only go into the preferences. Fixtures are hand-written with invented names.
  • The full gate passes on the rebased branch (fmt, clippy, tests, assets, brands, layers, wasm).
  • Live in a release build, with File › Open and with Keyboard Shortcuts › Import…: the dialog reads the set and matches names. Agreement decoded the modifier code. I recorded keys for the unknown codes and marked one as No equivalent. Apply with Add to the Current Shortcuts kept an earlier unsaved change, and after OK both Cmd+F6 (Group) and an imported tool key worked. A second import proposed the remembered matches. Cancel and Escape go back to Keyboard Shortcuts with its unsaved changes kept.
  • With a read root set, paths outside it are refused, including through .. and a directory junction, both in the app and over vectorcraft-cli mcp --headless. A .kys that is really a PDF is refused. Unknown parameter keys are refused for all four commands. I didn't test drops live because the control channel can't drop files; your UI test covers them.

bobtherobot added a commit to bobtherobot/vectorcraft that referenced this pull request Oct 14, 2026
No conflicts. Brings in Keyboard Shortcuts › Import… and File › Open
for text shortcut sets (.kys, storytold#1223) and the 0.11.0 version bump.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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