diff --git a/hidrpc.go b/hidrpc.go index 7475b358..ebe03daa 100644 --- a/hidrpc.go +++ b/hidrpc.go @@ -34,7 +34,7 @@ func handleHidRPCMessage(message hidrpc.Message, session *Session) { logger.Warn().Err(err).Msg("failed to get keyboard macro report") return } - _, rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps) + rpcErr = rpcExecuteKeyboardMacro(keyboardMacroReport.Steps) case hidrpc.TypeCancelKeyboardMacroReport: rpcCancelKeyboardMacro() return diff --git a/jsonrpc.go b/jsonrpc.go index 759325b4..a6114ba5 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -1083,7 +1083,7 @@ func setKeyboardMacroCancel(cancel context.CancelFunc) { keyboardMacroCancel = cancel } -func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) (usbgadget.KeysDownState, error) { +func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) error { cancelKeyboardMacro() ctx, cancel := context.WithCancel(context.Background()) @@ -1098,7 +1098,7 @@ func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) (usbgadget.KeysDo currentSession.reportHidRPCKeyboardMacroState(s) } - result, err := rpcDoExecuteKeyboardMacro(ctx, macro) + err := rpcDoExecuteKeyboardMacro(ctx, macro) setKeyboardMacroCancel(nil) @@ -1107,7 +1107,7 @@ func rpcExecuteKeyboardMacro(macro []hidrpc.KeyboardMacroStep) (usbgadget.KeysDo currentSession.reportHidRPCKeyboardMacroState(s) } - return result, err + return err } func rpcCancelKeyboardMacro() { @@ -1120,19 +1120,16 @@ func isClearKeyStep(step hidrpc.KeyboardMacroStep) bool { return step.Modifier == 0 && bytes.Equal(step.Keys, keyboardClearStateKeys) } -func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacroStep) (usbgadget.KeysDownState, error) { - var last usbgadget.KeysDownState - var err error - +func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacroStep) error { logger.Debug().Interface("macro", macro).Msg("Executing keyboard macro") for i, step := range macro { delay := time.Duration(step.Delay) * time.Millisecond - last, err = rpcKeyboardReport(step.Modifier, step.Keys) + err := rpcKeyboardReport(step.Modifier, step.Keys) if err != nil { logger.Warn().Err(err).Msg("failed to execute keyboard macro") - return last, err + return err } // notify the device that the keyboard state is being cleared @@ -1146,17 +1143,17 @@ func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacro // Sleep completed normally case <-ctx.Done(): // make sure keyboard state is reset - _, err := rpcKeyboardReport(0, keyboardClearStateKeys) + err := rpcKeyboardReport(0, keyboardClearStateKeys) if err != nil { logger.Warn().Err(err).Msg("failed to reset keyboard state") } logger.Debug().Int("step", i).Msg("Keyboard macro cancelled during sleep") - return last, ctx.Err() + return ctx.Err() } } - return last, nil + return nil } var rpcHandlers = map[string]RPCHandler{ diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index 67637987..8d101b3b 100644 --- a/ui/src/hooks/useKeyboard.ts +++ b/ui/src/hooks/useKeyboard.ts @@ -115,102 +115,6 @@ export default function useKeyboard() { }); }, [send]); - - // executeMacro is used to execute a macro consisting of multiple steps. - // Each step can have multiple keys, multiple modifiers and a delay. - // The keys and modifiers are pressed together and held for the delay duration. - // 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 executeMacroRemote = useCallback(async ( - steps: MacroSteps, - ) => { - const macro: KeyboardMacroStep[] = []; - - for (const [_, step] of steps.entries()) { - const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean); - const modifierMask: number = (step.modifiers || []) - - .map(mod => modifiers[mod]) - - .reduce((acc, val) => acc + val, 0); - - // If the step has keys and/or modifiers, press them and hold for the delay - if (keyValues.length > 0 || modifierMask > 0) { - macro.push({ keys: keyValues, modifier: modifierMask, delay: 20 }); - macro.push({ ...MACRO_RESET_KEYBOARD_STATE, delay: step.delay || 100 }); - } - } - - sendKeyboardMacroEventHidRpc(macro); - }, [sendKeyboardMacroEventHidRpc]); - const executeMacroClientSide = useCallback(async (steps: MacroSteps) => { - const promises: (() => Promise)[] = []; - - const ac = new AbortController(); - setAbortController(ac); - - for (const [_, step] of steps.entries()) { - const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean); - const modifierMask: number = (step.modifiers || []) - .map(mod => modifiers[mod]) - .reduce((acc, val) => acc + val, 0); - - // If the step has keys and/or modifiers, press them and hold for the delay - if (keyValues.length > 0 || modifierMask > 0) { - promises.push(() => sendKeystrokeLegacy(keyValues, modifierMask, ac)); - promises.push(() => resetKeyboardState()); - promises.push(() => sleep(step.delay || 100)); - } - } - - const runAll = async () => { - for (const promise of promises) { - // Check if we've been aborted before executing each promise - if (ac.signal.aborted) { - throw new Error("Macro execution aborted"); - } - await promise(); - } - } - - return await new Promise((resolve, reject) => { - // Set up abort listener - const abortListener = () => { - reject(new Error("Macro execution aborted")); - }; - - ac.signal.addEventListener("abort", abortListener); - - runAll() - .then(() => { - ac.signal.removeEventListener("abort", abortListener); - resolve(); - }) - .catch((error) => { - ac.signal.removeEventListener("abort", abortListener); - reject(error); - }); - }); - }, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]); - const executeMacro = useCallback(async (steps: MacroSteps) => { - if (rpcHidReady) { - return executeMacroRemote(steps); - } - return executeMacroClientSide(steps); - }, [rpcHidReady, executeMacroRemote, executeMacroClientSide]); - - const cancelExecuteMacro = useCallback(async () => { - if (abortController.current) { - abortController.current.abort(); - } - if (!rpcHidReady) return; - // older versions don't support this API, - // and all paste actions are pure-frontend, - // we don't need to cancel it actually - cancelOngoingKeyboardMacroHidRpc(); - }, [rpcHidReady, cancelOngoingKeyboardMacroHidRpc, abortController]); - const KEEPALIVE_INTERVAL = 50; const cancelKeepAlive = useCallback(() => { @@ -373,5 +277,101 @@ export default function useKeyboard() { cancelKeepAlive(); }, [cancelKeepAlive]); + + // executeMacro is used to execute a macro consisting of multiple steps. + // Each step can have multiple keys, multiple modifiers and a delay. + // The keys and modifiers are pressed together and held for the delay duration. + // 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 executeMacroRemote = useCallback(async ( + steps: MacroSteps, + ) => { + const macro: KeyboardMacroStep[] = []; + + for (const [_, step] of steps.entries()) { + const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean); + const modifierMask: number = (step.modifiers || []) + + .map(mod => modifiers[mod]) + + .reduce((acc, val) => acc + val, 0); + + // If the step has keys and/or modifiers, press them and hold for the delay + if (keyValues.length > 0 || modifierMask > 0) { + macro.push({ keys: keyValues, modifier: modifierMask, delay: 20 }); + macro.push({ ...MACRO_RESET_KEYBOARD_STATE, delay: step.delay || 100 }); + } + } + + sendKeyboardMacroEventHidRpc(macro); + }, [sendKeyboardMacroEventHidRpc]); + const executeMacroClientSide = useCallback(async (steps: MacroSteps) => { + const promises: (() => Promise)[] = []; + + const ac = new AbortController(); + setAbortController(ac); + + for (const [_, step] of steps.entries()) { + const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean); + const modifierMask: number = (step.modifiers || []) + .map(mod => modifiers[mod]) + .reduce((acc, val) => acc + val, 0); + + // If the step has keys and/or modifiers, press them and hold for the delay + if (keyValues.length > 0 || modifierMask > 0) { + promises.push(() => sendKeystrokeLegacy(keyValues, modifierMask, ac)); + promises.push(() => resetKeyboardState()); + promises.push(() => sleep(step.delay || 100)); + } + } + + const runAll = async () => { + for (const promise of promises) { + // Check if we've been aborted before executing each promise + if (ac.signal.aborted) { + throw new Error("Macro execution aborted"); + } + await promise(); + } + } + + return await new Promise((resolve, reject) => { + // Set up abort listener + const abortListener = () => { + reject(new Error("Macro execution aborted")); + }; + + ac.signal.addEventListener("abort", abortListener); + + runAll() + .then(() => { + ac.signal.removeEventListener("abort", abortListener); + resolve(); + }) + .catch((error) => { + ac.signal.removeEventListener("abort", abortListener); + reject(error); + }); + }); + }, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]); + const executeMacro = useCallback(async (steps: MacroSteps) => { + if (rpcHidReady) { + return executeMacroRemote(steps); + } + return executeMacroClientSide(steps); + }, [rpcHidReady, executeMacroRemote, executeMacroClientSide]); + + const cancelExecuteMacro = useCallback(async () => { + if (abortController.current) { + abortController.current.abort(); + } + if (!rpcHidReady) return; + // older versions don't support this API, + // and all paste actions are pure-frontend, + // we don't need to cancel it actually + cancelOngoingKeyboardMacroHidRpc(); + }, [rpcHidReady, cancelOngoingKeyboardMacroHidRpc, abortController]); + return { handleKeyPress, resetKeyboardState, executeMacro, cleanup, cancelExecuteMacro }; }