Skip to content

Conversation

SilkePilon
Copy link
Contributor

@SilkePilon SilkePilon commented Sep 20, 2025

  • Lower minimum step delay to 10ms for finer-grained macro timing
  • Add optional "text" field on macro steps and client-side expansion into per-character key events (handles shift, AltRight, dead/accent keys, trailing space)
  • Add optional "wait" field on macro steps to represent explicit wait-only pauses (treated as no-op delays)
  • Wire text expansion into both remote and client-side macro execution
  • Ensure typed keys send press then explicit release delay
  • Update Go/TypeScript types and JSON-RPC parsing for new step fields
  • added the option to export and import macros

Demo:
https://www.youtube.com/watch?v=cD0neh8rioA

@SilkePilon SilkePilon marked this pull request as draft September 20, 2025 18:59
@SilkePilon SilkePilon marked this pull request as ready for review September 20, 2025 20:16
// Optional: when set, this step types the given text using the configured keyboard layout.
// The delay value is treated as the per-character delay.
Text string `json:"text,omitempty"`
Wait bool `json:"wait,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we already do that with no keys and delay?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The "Wait" makes a pause explicit, always clears state first, and gives us a future hook; an empty keys+delay is ambiguous and less robust.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll defer to @adamshiervani or @ym on this... I don't think empty-keys+delay is any less robust... but if there's value in wait's implementation, the by all means. Otherwise this seems to be pretty solid

Lower minimum step delay to 10ms to allow finer-grained macro timing.
Introduce optional "text" and "wait" fields on macro steps (Go and
TypeScript types, JSON-RPC parsing) so steps can either type text using
the selected keyboard layout or act as explicit wait-only pauses.

Implement client-side expansion of text steps into per-character key
events (handling shift, AltRight, dead/accent keys and trailing space)
and wire expansion into both remote and client-side macro execution.
Adjust macro execution logic to treat wait steps as no-op delays and
ensure key press followed by explicit release delay is sent for typed
keys.

These changes enable richer macro semantics (text composition and
explicit waits) and more responsive timing control.
- add buildDownloadFilename and pad2 helpers to consistently
  generate safe timestamped filenames for macro downloads
- extract macro download logic into handleDownloadMacro and wire up
  Download button to use it
- refactor normalizeSortOrders to a concise one-liner
- introduce sanitizeImportedStep and sanitizeImportedMacro to validate
  imported JSON, enforce types, default values, and limit name length,
  preventing malformed data from corrupting store
- generate new IDs for imported macros and ensure correct sortOrder
- update Memo dependencies to include handleDownloadMacro

These changes enable reliable macro export/import with sanitized
inputs, improve code clarity by extracting utilities, and prevent
issues from malformed external files.
@SilkePilon SilkePilon force-pushed the feat-mac-text-wait-delay branch from 002857b to 25e476e Compare September 21, 2025 13:50
for (const char of step.text) {
const keyprops = selectedKeyboard.chars[char];
if (!keyprops) continue;
const { key, shift, altRight, deadKey, accentKey } = keyprops;
Copy link
Contributor

Choose a reason for hiding this comment

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

This code is very similar to what is done in VirtualKeyboard.tsx. Is there any way we could refactor both or put a comment in here to note that they should be kept in sync.

if (keyValues.length > 0 || modifierMask > 0) {
if (step.wait) {
// pure wait: send a no-op clear state with desired delay
macro.push({ ...MACRO_RESET_KEYBOARD_STATE, delay: step.delay || 100 });
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use DEFAULT_DELAY here (and line 338 and line 341) for the delay fallback? (it's 50ms, which really shouldn't be a problem)

...macro,
sortOrder: index + 1,
}));
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => macros.map((m, i) => ({ ...m, sortOrder: i + 1 }));
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we leave the original normalizeSortOrders here, this change is less readable and seems identical in behavior

// If the step has keys and/or modifiers, press them and hold for the delay
if (keyValues.length > 0 || modifierMask > 0) {
if (step.wait) {
promises.push(() => sleep(step.delay || 100));
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use DEFAULT_DELAY here (and line 365 and line 367) for the delay fallback? (it's 50ms, which really shouldn't be a problem)

// After the delay, the keys and modifiers are released and the next step is executed.
// If a step has no keys or modifiers, it is treated as a delay-only step.
// A small pause is added between steps to ensure that the device can process the events.
const expandTextSteps = useCallback((steps: MacroSteps): MacroSteps => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be coded as a generator function so-as to not need to reallocate/copy the entire array.

// Generator function to process steps, expanding text type ones into keystrokes, yielding steps
function* expandTextSteps(steps: Iterable<MacroStep>, selectedKeyboard: KeyboardLayout): Generator<MacroStep> {
  const stepIterator = steps[Symbol.iterator]();
  let currentStep = stepIterator.next(); 

  while (!currentStep.done) {
     const step = currentStep.value;

     if (step.text && step.text.length > 0) {
        for (const char of step.text) {
           const keyprops = selectedKeyboard.chars[char];
           if (!keyprops) continue;
           const { key, shift, altRight, deadKey, accentKey } = keyprops;

           if (!key) continue;

           if (accentKey) {
              const accentModifiers: string[] = [];
              if (accentKey.shift) accentModifiers.push("ShiftLeft");
              if (accentKey.altRight) accentModifiers.push("AltRight");
              accentStep: MacroStep = { keys: [String(accentKey.key)], modifiers: accentModifiers, delay: step.delay };
              yield accentStep;
         }

         const mods: string[] = [];
         if (shift) mods.push("ShiftLeft");
         if (altRight) mods.push("AltRight");

         characterStep: MacroStep = { keys: [String(key)], modifiers: mods, delay: step.delay };
         yield characterStep;

         if (deadKey) {
            deadStep: MacroStep = { keys: ["Space"], modifiers: null, delay: step.delay };
            yield deadStep;
         }
     } else {
        yield step;
     }

     currentStep = stepIterator.next();
}

the above was composed in GitHub review editor and easily could be wrong, but it's close :)

working.push(sanitized);
imported.push(sanitized.name);
}
} catch { errors++; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be nice to capture the error and what the raw line was for an error message modal?

/>
<div className="ml-2 flex items-center gap-2">
<input ref={fileInputRef} type="file" accept="application/json" multiple className="hidden" onChange={async e => {
const fl = e.target.files;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we extract this code out to a function onFileUpload(files)?

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