-
Notifications
You must be signed in to change notification settings - Fork 212
feat(macros): add text/wait steps and added export and import #815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
// 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"` |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
002857b
to
25e476e
Compare
for (const char of step.text) { | ||
const keyprops = selectedKeyboard.chars[char]; | ||
if (!keyprops) continue; | ||
const { key, shift, altRight, deadKey, accentKey } = keyprops; |
There was a problem hiding this comment.
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 }); |
There was a problem hiding this comment.
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 })); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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++; } |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)
?
Demo:
https://www.youtube.com/watch?v=cD0neh8rioA