feat(picker): Visual HID/behavior picker built on upstream PR #159#172
Closed
numachang wants to merge 16 commits into
Closed
feat(picker): Visual HID/behavior picker built on upstream PR #159#172numachang wants to merge 16 commits into
numachang wants to merge 16 commits into
Conversation
Adds export (download) and import (upload) buttons to the app header toolbar, allowing users to save their keymap as a .keymap devicetree file and load one back onto the device. Export generates a formatted .keymap file with column-aligned bindings. Import parses .keymap files, resolves ZMK keycode names to HID usage codes, and applies bindings to the connected device via RPC.
Replace icon-only Download and Upload buttons with text labels for better clarity and accessibility in the keymap import/export toolbar. Co-Authored-By: Claude <noreply@anthropic.com>
Rewrite the upstream-derived README to describe this as a personal, work-in-progress fork of zmkfirmware/zmk-studio, with explicit credit to the upstream project and a list of planned tweaks (keymap import/export, type-to-search key picker, host-layout localization). Add CLAUDE.md documenting the working stance for this fork: upstream-respectful, minimal-diff, Apache-2.0 obligations preserved. Co-Authored-By: Claude <noreply@anthropic.com>
This fork does not have the signing / release secrets configured in the upstream repo (Apple, Azure Trusted Signing, NETLIFY_DEPLOY_HOOK, ZMK_STUDIO_RELEASE_TOKEN), so the inherited workflows would fail on every push and clutter the Actions tab without producing useful artifacts. - Remove release-please.yml entirely: depends on a custom release token and pushes to a 'prod' branch that does not exist here. - Restrict tauri-build.yml to workflow_dispatch only, so desktop builds can still be triggered manually when needed. Co-Authored-By: Claude <noreply@anthropic.com>
Pulls in zmkfirmware#171 by @max-hill-4 (max-hill-4) as the starting point for this fork's keymap import/export feature. The PR has been open since 2026-04-16 without maintainer review; merging here to iterate while remaining ready to send refinements back upstream. Original work © max-hill-4, licensed Apache-2.0, retained intact in this merge commit. Co-Authored-By: max-hill-4 <max-hill-4@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
Add src/Toast.tsx exposing a ToastProvider + useToast hook with
kind-aware styling (success / error / warning / info), an optional
`action` field rendered as a bold-red follow-up line for messages
that require the user to take a next step (e.g. press Save), and
auto-dismiss durations tuned per kind.
Wrap App with the provider in main.tsx so all downstream components
can call notify() without prop-drilling. Foundation for replacing
the upstream `window.alert("Failed to connect…")` TODO and for
giving import/export observable outcomes instead of silent failure.
Co-Authored-By: Claude <noreply@anthropic.com>
…er drop PR zmkfirmware#171's keymap parser had three independent issues: 1. The layer-block regex was non-greedy across `[\s\S]*?bindings`, which caused its very first match to start at `keymap { …` and walk through `compatible = "zmk,keymap"; Base { …` before stopping on Base's `bindings = <…>;`. Match #1's layerName came out as "keymap" — correctly skipped — but the Base block had already been consumed, so the next match started at Windows and Base was silently dropped. On a Cornix .keymap the parser produced 7 layers instead of 8 and Base never made it through import. Constrain what's allowed between `{` and `bindings = <` to just an optional `display-name = "…";`. 2. The keycode lookup zmkAliases iteration unconditionally overwrote any keycodeLookup entry, including ones that the `hid-usage-name-overrides.json` short aliases bound to keypad-page usages (KP_N5 etc.). Common ZMK names like N5 / COMMA / FSLH then mapped to the wrong codes and could not be reverse- looked up, so exports for those keys fell back to integers like `&kp 458786`. Replace with a single canonical `ZMK_KEYCODES` table — `[name, page, usage]` tuples covering A-Z, N0-N9, punctuation, F1-F24, system keys, arrows, keypad, modifiers, and the consumer-page media / brightness keycodes. Forward (keycodeLookup) and reverse (codeToZmkName) maps are both derived from it, so the canonical name wins every conflict. 3. Bindings whose keycode parameter carried implicit-modifier flags in the upper byte (e.g. LS(N1) = 0x0207001E for "!") came out as bare integers because the reverse lookup only knew the unshifted forms. Add MODIFIER_FLAGS, fold the bits out of formatBindingParam into LS()/LC()/LA()/LG() (and right-hand variants) wrappers, and teach parseBindingParam to undo the same wrap. Additionally: - Behavior reference name table extended to ZMK's full built-in set (mkp, mmv, msc, bt, out, ext_power, bl, rgb_ug, bootloader, reset, soft_off, caps_word, key_repeat, gresc, studio_unlock, …). - Export `dtsRefForDisplayName(displayName)` so callers can translate RPC `displayName` ("Key Press") into the DTS reference ("kp"), passing user-defined behavior names through unchanged. - Export `formatBindingParam` and `parseBindingParam` as the one-way and two-way primitives for binding serialization. - HID-page names and ZMK punctuation shortcuts (",", "[", "!") are added as parse-only secondary aliases via a `has`-gated insert so they never displace canonical ZMK names. Co-Authored-By: Claude <noreply@anthropic.com>
…eliable PR zmkfirmware#171 shipped a starting point for keymap import/export but it was not usable on a non-trivial keymap: the export emitted invalid DTS (`&Key Press 458795` style), the import silently failed on trans/none entries, both flows were entirely silent on success and failure, and a handful of behaviors got rejected by firmware with no explanation. Rework the App-side wiring on top of the new keymap-parser helpers to make import / export round-trip a real keymap. Export: - Use `dtsRefForDisplayName` and `formatBindingParam` to emit valid ZMK reference syntax: `&kp EQUAL`, `&mo 4`, `&mkp 1`, `&bt 3 1`, `&kp LS(N1)`, etc. instead of the previous `&Key Press 458795` / `&Momentary Layer 4` / `&kp 34013214`. - Stamp the filename with an ISO timestamp so repeated exports don't collide as `Cornix (1).keymap`, `Cornix (2).keymap`, etc. - Notify the user on success and on failure. Import: - Build behaviorNameToId from the device's full behavior list, including DTS reference aliases (`kp`, `mo`, …) alongside the RPC displayName variants. - Drop PR zmkfirmware#171's hardcoded skip for trans/none — they have valid behavior IDs and accept setLayerBinding with (0, 0). The previous skip prevented importing a file that wanted a position to revert to `&trans` if the user had manually changed it. - Check the setLayerBinding response code instead of ignoring it. An OK response counts as applied. A failure with `metadata: []` on the behavior is classified as "preserved" (Studio API can't edit this behavior; the device keeps its saved value). A failure with non-empty metadata is a real parameter mismatch and surfaces to both the toast and the console with the offending behavior's metadata attached, so the next debugging round has data. - Surface counts in the toast: "Imported X / preserved Y / skipped Z / rejected W", with names of read-only behaviors listed for the preserved set (mouse_move, mouse_scroll, ext_power on the tested Cornix build). User-facing copy: - Reword Save / Discard / Import toasts so the model is honest: `setLayerBinding` writes the working keymap in device RAM (which is what the keyboard uses live), `saveChanges` persists it to flash, `discardChanges` reverts the working state to flash. The import success message now reads "Changes are live on the device. Press Save to keep them after restart, or Discard to revert.", Save shows "Committing keymap to device flash…" → "Keymap saved to flash. It will persist across restarts.", Discard shows "Reverted to last saved keymap." - Replace the upstream `window.alert("Failed to connect…")` with a notify("error", …) call routed through the Toast provider. - The action sentence on toasts that demand a follow-up is shown on its own line in bold red so users don't skim past it. Lint: - Sweep through the touched code with `eslint --fix` so PR zmkfirmware#171's prefer-const errors don't carry into the fork's lint baseline. Co-Authored-By: Claude <noreply@anthropic.com>
…eady matches PR zmkfirmware#171's import unconditionally called setLayerBinding for every binding parsed out of the file. Even when the file value was identical to the device's current value (e.g. the user re-imported their own export with no edits), the firmware still flipped its "unsaved changes" flag because *some* setLayerBinding had been called. Save then appeared armed for a no-op flash write, which is both confusing and bad for flash longevity. Diff before calling: fetch keymap.layers once, then per position compare {behaviorId, param1, param2} against the parsed binding. Skip the RPC when they match and count it as `unchanged`. Reword the toast accordingly: - Everything matched: "<file> already matches the device. No changes needed." (info) - Some real updates: "Updated N binding(s) from <file> (M already matched)." with the persist reminder. - Partial / rejected: "Updated N (M already matched), skipped X, rejected Y. …" The `applied` counter is renamed `updated` to match the new semantics. preserved / skipped / failed handling is unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
feat: Reliable keymap import/export (refines upstream PR zmkfirmware#171)
…picker Squashes upstream zmkfirmware#159 by @awkannan into a single commit. Replaces the HID usage dropdown with a categorized tab grid (Letters / Numbers + Punctuation / Function + Navigation / Numpad / Apps/Media/Special / International / Other) using react-aria-components Tabs, and keeps the implicit modifier checkboxes above the grid. - Renames src/hid-usage-name-overrides.json to src/hid-usage-metadata.json and extends entries with a "category" field plus richer labels (incl. Japanese/Korean IME-related keys). - Adds hid_usage_get_metadata() that falls back to the canonical name stripped of the "Keyboard " prefix when no override is present. - Updates HidUsageLabel to consume the new metadata accessor. Credit: zmkfirmware#159 (Andrew Kannan, @awkannan). Apache-2.0 license preserved. Co-Authored-By: Andrew Kannan <andrew.kannan@gmail.com> Co-Authored-By: Claude <noreply@anthropic.com>
Layered on top of upstream PR zmkfirmware#159's category-tabbed grid: - HidUsagePicker: a global filter input above the tabs. While the field has content, the tab list is replaced by a flat grid of all matching keys (matched against canonical HID name, short/med/long override labels, and category). The per-tab "Other" ComboBox is kept untouched so the upstream layout still works when the filter is empty. - ParameterValuePicker: the layer-id <select> is replaced with a wrap-friendly row of buttons styled like the keycode grid, so picking a target layer for `mo` / `lt` / `to` matches the visual feel of the rest of the picker. - keymap-parser: re-point the override JSON import at the renamed hid-usage-metadata.json (PR zmkfirmware#159 deleted the old name). Co-Authored-By: Claude <noreply@anthropic.com>
Replace the single behavior dropdown with a flat chip grid grouped by
tier ("ZMK Standard" / "Firmware Extension") and category
("Basic / Layer / Hold-Tap / Mouse / System / Custom / Other"). Chips
within a category are reordered so the most common behaviors land first
("Key Press" first, "Transparent / None" last). The block label uses a
shared uppercase-tracked style so it lines up with the keycode picker.
- BehaviorParametersPicker: show Hold/Tap tabs only for 2-param bindings
with a HID-typed param2, fall through to the picker directly for
1-param, and disable the field for 0-param behaviors. Defaults to the
first metadata set so param2 is visible even when param1 = 0.
- ParameterValuePicker: drop the native <select> for layer ids in favor
of a wrap-friendly button row styled to match the keycode grid.
Co-Authored-By: Claude <noreply@anthropic.com>
Layer ANSI 60% / TKL function+nav cluster / numpad layouts on top of upstream PR zmkfirmware#159's tab grid, so picking a keycode reads like a real keyboard. Modifiers and CapsLk fall back to short labels ("L Shft", "L Ctrl") and paired short labels ("1 !", "[ {") are stacked top-small / bottom-large to match the physical legend. - HidUsagePicker: split into "+ MODIFIER" (eight modifier column) and "KEY" (search + tabs + visual grid). Tabs: Basic (ANSI 60%), Function + Nav (TKL cluster), Numpad, Apps/Media/Special, International, Other. All tabs share a fixed min/max height so the panel doesn't jump when switching. Auto-jumps to the tab that holds the current value (or Basic when value is 0). - Search bar above the tabs filters across every category and renders a flat result grid while populated; clears restore the per-tab view. - hid-usage-metadata.json: drop JIS-only sub-labels from Intl1-6 and Lang1-5. Locale-specific glyphs were misleading on non-JP keyboards (e.g. Lang1/Lang2 type Hangul per spec but trigger Henkan/Muhenkan on JIS layouts), so labels stay neutral until a proper i18n overlay is introduced. Co-Authored-By: Claude <noreply@anthropic.com>
Make the keymap preview faithful to the bound behavior at a glance and
keep the rendering stable across zoom, rotation, and key width.
- Key + Keymap: render the behavior name in a rounded badge at the top
of each cap (clears with pt-3.5; no pad when no badge). Hold-tap
bindings stack the hold value small/faded above the bold tap value;
layer-type params render the layer name directly; constants and raw
values get type-aware text sizes. Drop the hover scale/3D translate
so popped keys don't overlap their neighbors.
- HidUsageLabel: render paired short labels ("1 !" → top-small "!" /
base "1") and collapse to the shifted glyph alone when an implicit
Shift modifier is set ("LS(N2)" → "@"). Auto-shrink labels longer
than 4 glyphs so values like "Lang1" fit in 1U keys without ellipsis.
Add a `compact` mode used by hold-tap previews where vertical room is
scarce.
- PhysicalLayout: bbox the layout with rotation taken into account so
rotated thumb keys no longer clip, and switch the scale wrapper to
observe only the parent (element observation caused a feedback loop
on zoom-to-fit).
- Keyboard: pin the bottom panel's grid row to a fixed width with
overflow auto so picker tab changes don't jump the layout. Surface
setLayerBinding rejections via a toast that names the behavior and
hints that it may not be writable from Studio in this firmware build.
Co-Authored-By: Claude <noreply@anthropic.com>
Two follow-ups from a self-review of the visual-picker branch. - Keyboard: the undo path of setLayerBinding had an empty else, so a firmware-rejected Ctrl+Z would silently leave the UI out of sync with the device. Mirror the do-path notify() with an undo-specific message. - BehaviorParametersPicker: when the user switches from a 2-param behavior to a 1-param one while the Tap tab is active, activeParam stays at "p2" and feeds <Tabs> a key for a tab that no longer exists. Snap it back to "p1" via useEffect when hasParam2 flips false. Co-Authored-By: Claude <noreply@anthropic.com>
✅ Deploy Preview for zmk-studio ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Author
|
Closing immediately — this PR was opened against the wrong base. It was intended as an internal PR on my personal fork (numachang/zmk-studio-tweaks), not against upstream. Apologies for the noise. The individual changes will be proposed upstream later as separate, smaller PRs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Personal-fork visual rework of the binding picker, layered on top of upstream zmkfirmware/zmk-studio#159 (Andrew Kannan,
@awkannan).453980d) with attribution preserved.<select>with a wrap-friendly button row (1d7de0e).931edf0).Intl1-6/Lang1-5(the per-region glyphs were misleading on non-JP keyboards — left for a future locale overlay) (0e226bf).1 !→ stacked), shrink-to-fit for 5+ glyph labels, rotation-aware bounding box (rotated thumb keys no longer clip), and anotify()toast when the firmware rejectssetLayerBinding(c3b5cad).activeParamresets top1when the active tab disappears after a behavior swap (50c47a6).Upstream stance
This is a personal fork, intentionally minimal-diff and Apache-2.0. Apache notices and
@awkannan's authorship for PR #159 are preserved. Individual pieces will be proposed upstream as separate PRs (rotation bbox fix, undo-rejection toast, etc. as small standalone bug fixes; the chip grid and physical layouts as opinionated UX proposals).Test plan
Open
npm run devin real Chrome/Edge (Web Serial / BLE will not work in VSCode's embedded browser):&kp→ HID grid auto-jumps to the Basic tab and renders the ANSI 60% layoutShft) → paired keys collapse to the shifted glyph (1 !→!)&mt LSHFT A→ keymap preview shows hold faded above bold tap, Hold/Tap tabs appear in the param pickerselectedKeywarningcapsin the search bar → flat results grid filters across all tabs; clearing restores per-tab viewautozoom no longer clips the rotated keysmouse_moveon a build without it) → error toast appears with hint text