Skip to content

feat(incott): macro card in the Macro tab - #1382

Draft
vladidraganov wants to merge 2 commits into
OpenMouse-Project:mainfrom
vladidraganov:incott-macros-v2
Draft

vladidraganov wants to merge 2 commits into
OpenMouse-Project:mainfrom
vladidraganov:incott-macros-v2

Conversation

@vladidraganov

@vladidraganov vladidraganov commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Adds an Incott macro card to the Macro tab: build or record a macro, then give it to a button. Hardware-tested on a G23V2 Pro over the 8K dongle.

Draft until OpenMouse-Project/mouse-protocol#288 is released. This uses incottMacroSlotForButton, INCOTT_MACRO_MOUSE_KEYS and incottMacroKeyLabel, which 0.52.0 does not have, so typecheck fails until @openmouse/protocol is bumped. I'll bump it here once the release is out.

What it does

The mouse accepts a macro buffer and has no command to read one back. Macros are therefore kept in this browser (src/device/incott-macros.ts), and the card says so. What the mouse does report wins: a button only shows a macro while its read-back mapping is Macro. A button remapped elsewhere stops showing a stale assignment, and one set by the vendor tool shows "A macro from elsewhere".

  • Library and assignment. Named macros, each assignable to any of the six buttons. A button runs only the buffer with its own index, so assigning uploads the macro into that button's buffer and binds the button to Macro. One macro on two buttons is uploaded twice. Unassigning puts the button's factory action back, since there is no command to empty a buffer.
  • Recorder. Keys and the three mouse buttons, with Escape to stop. Timing is fast, recorded or fixed, and kept beside the steps, so switching back to recorded restores the recording.
  • Repeat. While held, until a key is pressed, or a set number of times.
  • Step editor. Press or release, key (grouped mouse and keyboard), delay, reorder, delete.
  • Every save uploads and then re-binds the button, as the vendor does. Hardware-confirmed: the loop mode only takes effect on the binding write. The separate mapping change is dropped as "already matches" once a button is on Macro, so without this a re-saved macro kept its old loop mode.

The card shows only when the driver offers the per-button Macro action (availability.incottMacros), so it stays hidden on older protocol builds. Uploads go through the pending-changes bar like other settings. They are refused while a game profile is being edited, because a profile cannot restore a macro.

Design and size

  • It reuses the K-snake macro card's classes for the recorder, rows, notices and actions. New CSS covers only a library row, a button grid and a wrapping step row (styles/areas/library.css). The heading carries the Command glyph via CardSlot.
  • Checked in Emerald, Light and NieR.
  • Strings are new macro.incott* keys in every locale, and existing macro.* keys are reused where they fit.
  • Bundle budget: JS raised from 3,850,000 to 3,950,000 bytes, with a measured note in build/check-bundle-size.ts. The card and its strings add about 33 kB (3,892,361 to 3,925,511 bytes against the protocol build it needs; the rest of that build's growth is protocol drivers released since 0.52.0). Main is 1,431,450 bytes and CSS 169,383, both under their caps.

Preview

?preview=incott opens an Incott G23 Pro fixture based on the hardware-test read in mouse-protocol captures/incott-g23-pro/. Back is shown running a macro.

Testing

  • npm run build, npm test (650 pass) and npm run size, run against the protocol PR linked locally.
  • Tests for the store (malformed data, the step limit, LE16 clamping, recorded gaps closing the previous step), assignment read-back, recorder key mapping, availability, the Macro tab and the fixture.
  • On hardware: recorded and hand-built macros, assignment and unassignment, and each repeat mode, including "3 times" firing three times from one press.

🤖 Generated with Claude Code

Note

Add Incott macro editor card and recorder to the Macro tab

  • Adds IncottMacroCard, a browser-local macro editor that lets users create, name, edit, and delete macros and assign them to device buttons (IncottMacroCard.tsx)
  • Recording uses capture-phase keyboard and mouse listeners. It records supported presses and releases with elapsed gaps, caps gaps at the unsigned 16-bit maximum, and enforces the protocol step limit (incott-macros.ts)
  • Macros are saved to local storage. Assigning or saving a macro uploads it to the device buffer and maps the button to the macro action via applyIncottMacro in controller.ts
  • Card shows only for Incott devices whose button mappings include the Macro option, and it gates the macro workspace tab. Adds an Incott G23 Pro preview fixture and translated strings for 12 locales
  • Raises the JS bundle-size budget from 3,850,000 to 3,950,000 bytes in check-bundle-size.ts
📊 Macroscope summarized 493a6d4. 22 files reviewed, 7 issues evaluated, 2 issues filtered, 5 comments posted

🗂️ Filtered Issues

src/device/controller.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 7228: The unconditional binding write survives cancellation of the button assignment. With instant flash off and a button initially on its factory action, assign a macro and then unassign it before Apply. IncottMacroCard.upload stages this incott-macro-${button} entry plus button-${button}; unassigning calls applyDeviceButtonMapping with the original action, so stageChange sees that the device already matches and drops only button-${button}. The macro entry remains and this line binds the button to Macro on Apply, despite the UI showing it unassigned. Coordinate cancellation/overrides of the macro entry with button remaps so reverting an assignment cannot activate the macro. [ Cross-file consolidated ]
src/device/incott-macros.ts — 1 comment posted, 2 evaluated, 1 filtered
  • line 109: At the step limit, appendRecordedStep discards every subsequent event, including releases for keys pressed in the retained steps. The recorder in IncottMacroCard continues recording and saves this truncated array without balancing outstanding presses. For example, reaching the limit with a key-down and then releasing that key produces a saved macro with no corresponding release, rather than the recorded tap. Reserve room for pending releases or stop at a balanced boundary instead of silently dropping them. [ Cross-file consolidated ]

vladidraganov and others added 2 commits November 6, 2026 21:05
Incott macros can be built, recorded and assigned to buttons from the app.
The mouse accepts a macro buffer and never returns one, so macros are kept in
this browser (`device/incott-macros.ts`) and a button only shows a macro while
the mouse's own button mapping says it runs one.

- library of named macros; a button runs the buffer carrying its own index, so
  assigning a macro uploads it into that button's buffer
- recorder for keys and the three mouse buttons, timing (fast, recorded,
  fixed) kept beside the steps so switching back restores the recording
- repeat: while held, until a key is pressed, or a set number of times
- step editor: press or release, key, delay, reorder, delete
- every save uploads the buffer and then re-binds the button, as the vendor
  does; hardware-confirmed that the loop mode only takes effect on the binding
  write, so skipping it kept the old mode

Reuses the K-snake macro card's styles; the new CSS is a library row, a button
grid and a wrapping step row. Strings in every locale. JS budget raised with a
measured note: the card and its strings add about 33 kB.

Needs the @openmouse/protocol release carrying incottMacroSlotForButton,
INCOTT_MACRO_MOUSE_KEYS and incottMacroKeyLabel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shaped on the G23 Pro hardware test read in mouse-protocol
`captures/incott-g23-pro/`, with Back shown running a macro. Opens with
`?preview=incott`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions github-actions Bot added type: enhancement PR title category: enhancement area: ui Interface and styling area: devices Device integration and HID support area: translations Localized interface strings area: ci Automation and build configuration labels Nov 6, 2026
uid: slot + 1,
steps: stepsForDevice(macro),
});
control.applyDeviceButtonMapping(button, "Macro");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High cards/IncottMacroCard.tsx:186

In game-profile mode, assigning a macro stages the button mapping as Macro even though applyIncottMacro rejects the buffer write, so the profile runs an old or uninitialized buffer instead of the selected macro. Since this mapping is staged independently, gate the whole assignment flow in game-profile mode rather than rejecting only the buffer write.

🤖 Copy this AI Prompt to have your agent fix this:
In file @src/features/devices/cards/IncottMacroCard.tsx around line 186:

In game-profile mode, assigning a macro stages the button mapping as `Macro` even though `applyIncottMacro` rejects the buffer write, so the profile runs an old or uninitialized buffer instead of the selected macro. Since this mapping is staged independently, gate the whole assignment flow in game-profile mode rather than rejecting only the buffer write.

assignments: Partial<Record<IncottButtonName, string>>;
}

const KEY = "openmouse.incott-macros";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High device/incott-macros.ts:60

A saved assignment for Back on one Incott mouse is reused for every other mouse, so incottMacroAssignment identifies the second mouse's unrelated Macro buffer as the saved macro and edits can overwrite that buffer. KEY stores all assignments under one global key, with no device identity; keep the shared macro library if desired, but scope assignments to the connected device.

🤖 Copy this AI Prompt to have your agent fix this:
In file @src/device/incott-macros.ts around line 60:

A saved assignment for `Back` on one Incott mouse is reused for every other mouse, so `incottMacroAssignment` identifies the second mouse's unrelated `Macro` buffer as the saved macro and edits can overwrite that buffer. `KEY` stores all assignments under one global key, with no device identity; keep the shared macro library if desired, but scope assignments to the connected device.

}
const macro = store.macros.find((entry) => entry.id === id);
if (!macro) return;
persist({ ...store, assignments: { ...assignments, [button]: id } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High cards/IncottMacroCard.tsx:205

Discarding a staged assignment leaves the mouse running macro A while the card reports macro B, and the mismatch survives reload. assign persists store.assignments[button] before the upload is committed, while incottMacroAssignment trusts that mapping; keep the assignment pending and persist it only after a successful flash.

🤖 Copy this AI Prompt to have your agent fix this:
In file @src/features/devices/cards/IncottMacroCard.tsx around line 205:

Discarding a staged assignment leaves the mouse running macro A while the card reports macro B, and the mismatch survives reload. `assign` persists `store.assignments[button]` before the upload is committed, while `incottMacroAssignment` trusts that mapping; keep the assignment pending and persist it only after a successful flash.

const controlsDisabled = disabled || isRecording;

const finishRecording = (): void => {
const recorded = recordedRef.current;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High cards/IncottMacroCard.tsx:77

Recording can save a macro with a key press but no matching release, leaving that key held during playback. finishRecording copies recordedRef.current verbatim, and when Escape stops recording the keyup listener is removed before a held key is released; likewise, events dropped at the step limit can include those releases. Close held keys before saving, and stop or reject recordings at the limit unless they can remain balanced.

Also found in 1 other location(s)

src/device/incott-macros.ts:109

At the step limit, appendRecordedStep discards every subsequent event, including releases for keys pressed in the retained steps. The recorder in IncottMacroCard continues recording and saves this truncated array without balancing outstanding presses. For example, reaching the limit with a key-down and then releasing that key produces a saved macro with no corresponding release, rather than the recorded tap. Reserve room for pending releases or stop at a balanced boundary instead of silently dropping them.

🤖 Copy this AI Prompt to have your agent fix this:
In file @src/features/devices/cards/IncottMacroCard.tsx around line 77:

Recording can save a macro with a key press but no matching release, leaving that key held during playback. `finishRecording` copies `recordedRef.current` verbatim, and when Escape stops recording the `keyup` listener is removed before a held key is released; likewise, events dropped at the step limit can include those releases. Close held keys before saving, and stop or reject recordings at the limit unless they can remain balanced.

Also found in 1 other location(s):
- src/device/incott-macros.ts:109 -- At the step limit, `appendRecordedStep` discards every subsequent event, including releases for keys pressed in the retained steps. The recorder in `IncottMacroCard` continues recording and saves this truncated array without balancing outstanding presses. For example, reaching the limit with a key-down and then releasing that key produces a saved macro with no corresponding release, rather than the recorded tap. Reserve room for pending releases or stop at a balanced boundary instead of silently dropping them.

const unbind = (button: IncottButtonName): void => {
// There is no command to empty a buffer; re-pointing the button is all
// unassigning can do.
if (assignment(button) !== null) control.applyDeviceButtonMapping(button, INCOTT_MACRO_DEFAULT_ACTION[button]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High cards/IncottMacroCard.tsx:192

Unassigning a button can still bind it to Macro when the user clicks Apply, even though the UI shows it as unassigned. unbind stages the factory mapping, but because that mapping already matches the device, the staged mapping is discarded while the incott-macro-${button} upload from upload remains; cancel that pending macro upload when unbinding.

Also found in 1 other location(s)

src/device/controller.ts:7228

The unconditional binding write survives cancellation of the button assignment. With instant flash off and a button initially on its factory action, assign a macro and then unassign it before Apply. IncottMacroCard.upload stages this incott-macro-${button} entry plus button-${button}; unassigning calls applyDeviceButtonMapping with the original action, so stageChange sees that the device already matches and drops only button-${button}. The macro entry remains and this line binds the button to Macro on Apply, despite the UI showing it unassigned. Coordinate cancellation/overrides of the macro entry with button remaps so reverting an assignment cannot activate the macro.

🤖 Copy this AI Prompt to have your agent fix this:
In file @src/features/devices/cards/IncottMacroCard.tsx around line 192:

Unassigning a button can still bind it to `Macro` when the user clicks Apply, even though the UI shows it as unassigned. `unbind` stages the factory mapping, but because that mapping already matches the device, the staged mapping is discarded while the `incott-macro-${button}` upload from `upload` remains; cancel that pending macro upload when unbinding.

Also found in 1 other location(s):
- src/device/controller.ts:7228 -- The unconditional binding write survives cancellation of the button assignment. With instant flash off and a button initially on its factory action, assign a macro and then unassign it before Apply. `IncottMacroCard.upload` stages this `incott-macro-${button}` entry plus `button-${button}`; unassigning calls `applyDeviceButtonMapping` with the original action, so `stageChange` sees that the device already matches and drops only `button-${button}`. The macro entry remains and this line binds the button to `Macro` on Apply, despite the UI showing it unassigned. Coordinate cancellation/overrides of the macro entry with button remaps so reverting an assignment cannot activate the macro.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ci Automation and build configuration area: devices Device integration and HID support area: translations Localized interface strings area: ui Interface and styling type: enhancement PR title category: enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant