diff --git a/doc-dev/reference-manual.md b/doc-dev/reference-manual.md index f6ae84b22..af70db928 100644 --- a/doc-dev/reference-manual.md +++ b/doc-dev/reference-manual.md @@ -41,7 +41,7 @@ $onJoin $onSplit ``` -i.e., if you want to customize the acceleration driver for your trackball module on keymap QWR, create a macro named `$onKeymapChange QWR`, with content e.g.: +For example, if you want to customize the acceleration driver for your trackball module on keymap QWR, create a macro named `$onKeymapChange QWR` containing: ``` set module.trackball.baseSpeed 0.5 @@ -195,6 +195,8 @@ COMMAND = set bluetooth.enabled BOOL COMMAND = set bluetooth.alwaysAdvertiseHid BOOL COMMAND = set modifierLayerTriggers.{shift|alt|super|ctrl} {left|right|both} COMMAND = ¯oArg. +COMMAND = macroArg [: MACROARG_TYPE] +MACROARG_TYPE = { int | float | bool | string | keyid | scancode | any } CONDITION = CONDITION = if (EXPRESSION) CONDITION = else @@ -233,7 +235,7 @@ KEYMAPID = |last|current MACROID = last | | OPERATOR = + | - | * | / | % | < | > | <= | >= | == | != | && | || VARIABLE_EXPANSION = $ | $ -VARIABLE_EXPANSION = $currentAddress | $currentTime | $thisKeyId | $queuedKeyId. | $keyId.KEYID_ABBREV | $uhk.name | $macroArg. +VARIABLE_EXPANSION = $currentAddress | $currentTime | $thisKeyId | $queuedKeyId. | $keyId.KEYID_ABBREV | $uhk.name | $macroArg. | $macroArg. EXPRESSION = | (EXPRESSION) | INT | BOOL | FLOAT | VARIABLE_EXPANSION | EXPRESSION OPERATOR EXPRESSION | !EXPRESSION | min(EXPRESSION [, EXPRESSION]+) | max(EXPRESSION [, EXPRESSION]+) EXPRESSION = STRING == STRING | STRING != STRING PARENTHESSED_EXPRESSION = (EXPRESSION) @@ -609,6 +611,12 @@ Key actions can be parametrized with macro arguments. These arguments can be exp - the argument bounds must correspond to token bounds in the fully expanded string - the argument cannot span multiple lines +### Named Arguments + +Macro arguments can also be named and typed by declaring them using the `macroArg` command at the beginning of the macro. + +Such named arguments can also be accessed using `$macroArg.`, and the value provided by Agent will be parsed according to the argument type. + ### Configuration options - `set stickyModifiers {never|smart|always}` globally turns on or off sticky modifiers. This affects only standard scancode actions. Macro actions (both gui and command ones) are always nonsticky, unless `sticky` flag is included in `tapKey|holdKey|pressKey` commands. Default value is `smart`, which is the official behaviour - i.e., ` + ` are sticky. diff --git a/doc-dev/user-guide.md b/doc-dev/user-guide.md index 482142f1e..347eed7fe 100644 --- a/doc-dev/user-guide.md +++ b/doc-dev/user-guide.md @@ -55,9 +55,9 @@ ifDoubletap tapKey capsLock - Provided value bounds are informational only - they denote values that seem to make sense. Sometimes default values are marked. - If you are still not sure about some feature or syntax, do not hesitate to ask. -3) If `ERR` appears on the display, you can retrieve the description by using `printStatus` over a focused text editor. Or, using the above point, just search the [reference manual](reference-manual.md) for `ERR`. +3) If `ERR` appears on the display (UHK60), or a triangle warning icon `⚠️` is shown on the display (UHK80), you can retrieve the error description by using `printStatus` over a focused text editor. In addition, a yellow error pane will automatically appear in Agent, showing the error description. You can also search the [reference manual](reference-manual.md) for `ERR`. -4) If you encounter a bug, let me know. There are lots of features and quite few users around this codebase - if you do not report problems you find, chances are that no one else will (since most likely no one else has noticed). +4) If you encounter a bug, let us know (ideally, by reporting a GitHub issue). There are lots of features and quite few users around this codebase - if you do not report problems you find, chances are that no one else will (since most likely no one else has noticed). ## Known software limitations and oddities @@ -114,6 +114,15 @@ replaceLayer mod QTY mod replaceLayer mouse QTY mouse ``` +Alternatively, you can also replace all of the keymap first, and the just load the base layer of your keymap again. For this approach, your macro would be: + +``` +replaceKeymap QTY +overlayKeymap current +``` + +This second method also allows you to only define some keys on the COL base layer that need to change from QTY. Any key mapped to "None" inherits its action from QTY, as `overlayKeymap` will not overload it. + ## Examples Every nonempty line is considered as one command. Empty line, or commented line too. Empty lines are skipped. Exception is an empty command action, which counts for one command. Even `{` and `}` are treated as commands, and have to be on separate lines. diff --git a/right/src/macros/command_hash.gperf b/right/src/macros/command_hash.gperf index 9148cbb5c..3c58a722d 100644 --- a/right/src/macros/command_hash.gperf +++ b/right/src/macros/command_hash.gperf @@ -88,6 +88,7 @@ ifRegEq, CommandId_ifRegEq ifNotRegEq, CommandId_ifNotRegEq ifRegGt, CommandId_ifRegGt ifRegLt, CommandId_ifRegLt +macroArg, CommandId_macroArg mulReg, CommandId_mulReg noOp, CommandId_noOp notify, CommandId_notify diff --git a/right/src/macros/command_ids.h b/right/src/macros/command_ids.h index 038eb7cc5..37ef03908 100644 --- a/right/src/macros/command_ids.h +++ b/right/src/macros/command_ids.h @@ -107,6 +107,7 @@ typedef enum { CommandId_ifRegLt, // deprecated // 'm' commands + CommandId_macroArg, CommandId_mulReg, // deprecated // 'n' commands diff --git a/right/src/macros/commands.c b/right/src/macros/commands.c index d3416c6ec..7950c7ff7 100644 --- a/right/src/macros/commands.c +++ b/right/src/macros/commands.c @@ -941,6 +941,96 @@ static macro_result_t processPlayMacroCommand(parser_context_t* ctx) return res ? MacroResult_Blocking : MacroResult_Finished; } +static macro_result_t processMacroArgCommand(parser_context_t* ctx) +{ + if (S->ms.macroHeadersProcessed) { + Macros_ReportErrorPos(ctx, "macroArg commands must be placed before any other commands in the macro"); + return MacroResult_Finished; + } + + // parse the argument name (identifier) + const char *idStart = ctx->at; + const char *idEnd = IdentifierEnd(ctx); // possibly use ConsumeIdentifier here + + if (idEnd == idStart) { + Macros_ReportErrorPos(ctx, "Expected identifier"); + return MacroResult_Header; + } + ctx->at = idEnd; + + // see if the argument has a type + macro_argument_type_t argType; + + if (ConsumeToken(ctx, ":")) { + if (ConsumeToken(ctx, "int")) { + argType = MacroArgType_Int; + } + else if (ConsumeToken(ctx, "float")) { + argType = MacroArgType_Float; + } + else if (ConsumeToken(ctx, "bool")) { + argType = MacroArgType_Bool; + } + else if (ConsumeToken(ctx, "string")) { + argType = MacroArgType_String; + } + else if (ConsumeToken(ctx, "keyid") || ConsumeToken(ctx, "keyId")) { + argType = MacroArgType_KeyId; + } + else if (ConsumeToken(ctx, "scancode") || ConsumeToken(ctx, "scanCode") || + ConsumeToken(ctx, "moddedScanCode") || ConsumeToken(ctx, "moddedScancode") || + ConsumeToken(ctx, "shortcut")) { + argType = MacroArgType_ScanCode; + } + else if (ConsumeToken(ctx, "any")) { + argType = MacroArgType_Any; + } + else { + Macros_ReportErrorTok(ctx, "Unrecognized macroArg argument type:"); + return MacroResult_Header; + } + } + else { + if (!IsWhite(ctx)) { + Macros_ReportErrorTok(ctx, "Superfluous non-identifier characters:"); + return MacroResult_Header; + } + ConsumeWhite(ctx); + argType = MacroArgType_Any; + } + + // The following two blocks (counting arguments, then allocating argument) + // could be optimised to walk the macro arg pool only once. + // The current implementation is straightforward to read, and the slight performance + // decrease at the start of a macro (when macroArg commands are processed) seems negligible. + + // check whether we are exceeding the arguments for this macro + uint8_t argNumber = Macros_CountMacroArgumentsByOwner(MACRO_STATE_SLOT(S)); + if (argNumber >= MAX_MACRO_ARGUMENT_COUNT) { + Macros_ReportErrorPos(ctx, "Maximum number of macro arguments exceeded"); + return MacroResult_Header; + } + + // allocate a pool slot for the argument and store its metadata (name and type) there + macro_argument_alloc_result_t res = Macros_AllocateMacroArgument(MACRO_STATE_SLOT(S), idStart, idEnd, argType, argNumber+1); + switch (res) { + case MacroArgAllocResult_Success: + // macro argument successfully allocated + break; + case MacroArgAllocResult_PoolLimitExceeded: + Macros_ReportErrorPos(ctx, "Too many arguments across simultaneously active macros (argument pool exhausted)"); + return MacroResult_Header; + case MacroArgAllocResult_DuplicateArgumentName: + Macros_ReportErrorPos(ctx, "Duplicate argument name"); + return MacroResult_Header; + } + + // rest of command is descriptive label, ignore. + Macros_ConsumeStringToken(ctx); + + return MacroResult_Header; +} + static macro_result_t processWriteCommand(parser_context_t* ctx) { if (Macros_DryRun) { @@ -1182,7 +1272,7 @@ static macro_result_t processIfShortcutCommand(parser_context_t* ctx, bool negat CTX_COPY(ctx2, *ctx); uint8_t totalArgs = 0; uint8_t argKeyId = 255; - while((argKeyId = Macros_TryConsumeKeyId(&ctx2)) != 255 && ctx2.at < ctx2.end) { + while(ctx2.at < ctx2.end && (argKeyId = Macros_TryConsumeKeyId(&ctx2)) != 255) { totalArgs++; } if (totalArgs > PostponerQuery_PendingKeypressCount()) { @@ -1195,7 +1285,7 @@ static macro_result_t processIfShortcutCommand(parser_context_t* ctx, bool negat uint8_t numArgs = 0; bool someoneNotReleased = false; uint8_t argKeyId = 255; - while((argKeyId = Macros_TryConsumeKeyId(ctx)) != 255 && ctx->at < ctx->end) { + while(ctx->at < ctx->end && (argKeyId = Macros_TryConsumeKeyId(ctx)) != 255) { numArgs++; if (pendingCount < numArgs || insufficientNumberForAnyOrder) { uint32_t referenceTime = transitive && pendingCount > 0 ? PostponerExtended_LastPressTime() : S->ms.currentMacroStartTime; @@ -1228,14 +1318,14 @@ static macro_result_t processIfShortcutCommand(parser_context_t* ctx, bool negat } } else if (orGate) { - // go through all canidates all at once + // go through all candidates all at once while (true) { // first keyid had already been processed. if (PostponerQuery_ContainsKeyId(argKeyId)) { numArgs = 1; goto matched; } - if ((argKeyId = Macros_TryConsumeKeyId(ctx)) == 255 || ctx->at == ctx->end) { + if (ctx->at >= ctx->end || (argKeyId = Macros_TryConsumeKeyId(ctx)) == 255) { break; } } @@ -1283,7 +1373,9 @@ uint8_t Macros_TryConsumeKeyId(parser_context_t* ctx) uint8_t keyId = MacroKeyIdParser_TryConsumeKeyId(ctx); if (keyId == 255 && isNUM(ctx)) { - uint8_t num = Macros_ConsumeInt(ctx); + // through the consumeValue() chain, this will parse plain integers, + // numeric variables, and $macroArg of type :keyId (in both text or numeric form). + uint8_t num = Macros_ConsumeInt(ctx); if (Macros_ParserError) { return 255; } else { @@ -1998,13 +2090,404 @@ static macro_result_t processZephyrCommand(parser_context_t* ctx) { } \ break; +static macro_result_t dispatchCommand(parser_context_t* ctx, command_id_t commandId) { + // Dispatch based on command ID + switch (commandId) { + // 'a' commands + case CommandId_activateKeyPostponed: + return processActivateKeyPostponedCommand(ctx); + case CommandId_autoRepeat: + return processAutoRepeatCommand(ctx); + case CommandId_addReg: + Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `setVar varName ($varName+1)`."); + return MacroResult_Finished; + + // 'b' commands + case CommandId_break: + return processBreakCommand(ctx); + case CommandId_bluetooth: + return processBluetoothCommand(ctx); + + // 'c' commands + case CommandId_consumePending: + return processConsumePendingCommand(ctx); + case CommandId_clearStatus: + return Macros_ProcessClearStatusCommand(true); + case CommandId_call: + return processCallCommand(ctx); + + // 'd' commands + case CommandId_delayUntilRelease: + return processDelayUntilReleaseCommand(); + case CommandId_delayUntilReleaseMax: + return processDelayUntilReleaseMaxCommand(ctx); + case CommandId_delayUntil: + return processDelayUntilCommand(ctx); + case CommandId_diagnose: + return Macros_ProcessDiagnoseCommand(); + + // 'e' commands + case CommandId_exec: + return processExecCommand(ctx); + case CommandId_else: + if (!Macros_DryRun && S->ls->ms.lastIfSucceeded) { + return MacroResult_Finished; + } + break; + case CommandId_exit: + return processExitCommand(ctx); + + // 'f' commands + case CommandId_final: + return processFinalCommand(ctx); + case CommandId_fork: + return processForkCommand(ctx); + case CommandId_freeze: + return processFreezeCommand(ctx); + + // 'g' commands + case CommandId_goTo: + return processGoToCommand(ctx); + + // 'h' commands + case CommandId_holdLayer: + return processHoldLayerCommand(ctx); + case CommandId_holdLayerMax: + return processHoldLayerMaxCommand(ctx); + case CommandId_holdKeymapLayer: + return processHoldKeymapLayerCommand(ctx); + case CommandId_holdKeymapLayerMax: + return processHoldKeymapLayerMaxCommand(ctx); + case CommandId_holdKey: + return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Hold, &S->ms.reports); + + // 'i' commands - conditionals + case CommandId_if: + PROCESS_CONDITION(processIfCommand(ctx)) + case CommandId_ifDoubletap: + PROCESS_CONDITION(processIfDoubletapCommand(false)) + case CommandId_ifNotDoubletap: + PROCESS_CONDITION(processIfDoubletapCommand(true)) + case CommandId_ifInterrupted: + PROCESS_CONDITION(processIfInterruptedCommand(false)) + case CommandId_ifNotInterrupted: + PROCESS_CONDITION(processIfInterruptedCommand(true)) + case CommandId_ifReleased: + PROCESS_CONDITION(processIfReleasedCommand(false)) + case CommandId_ifNotReleased: + PROCESS_CONDITION(processIfReleasedCommand(true)) + case CommandId_ifKeymap: + PROCESS_CONDITION(processIfKeymapCommand(ctx, false)) + case CommandId_ifNotKeymap: + PROCESS_CONDITION(processIfKeymapCommand(ctx, true)) + case CommandId_ifLayer: + PROCESS_CONDITION(processIfLayerCommand(ctx, false)) + case CommandId_ifNotLayer: + PROCESS_CONDITION(processIfLayerCommand(ctx, true)) + case CommandId_ifLayerToggled: + PROCESS_CONDITION(processIfLayerToggledCommand(ctx, false)) + case CommandId_ifNotLayerToggled: + PROCESS_CONDITION(processIfLayerToggledCommand(ctx, true)) + case CommandId_ifPlaytime: + PROCESS_CONDITION(processIfPlaytimeCommand(ctx, false)) + case CommandId_ifNotPlaytime: + PROCESS_CONDITION(processIfPlaytimeCommand(ctx, true)) + case CommandId_ifAnyMod: + PROCESS_CONDITION(processIfModifierCommand(false, 0xFF)) + case CommandId_ifNotAnyMod: + PROCESS_CONDITION(processIfModifierCommand(true, 0xFF)) + case CommandId_ifShift: + PROCESS_CONDITION(processIfModifierCommand(false, SHIFTMASK)) + case CommandId_ifNotShift: + PROCESS_CONDITION(processIfModifierCommand(true, SHIFTMASK)) + case CommandId_ifCtrl: + PROCESS_CONDITION(processIfModifierCommand(false, CTRLMASK)) + case CommandId_ifNotCtrl: + PROCESS_CONDITION(processIfModifierCommand(true, CTRLMASK)) + case CommandId_ifAlt: + PROCESS_CONDITION(processIfModifierCommand(false, ALTMASK)) + case CommandId_ifNotAlt: + PROCESS_CONDITION(processIfModifierCommand(true, ALTMASK)) + case CommandId_ifGui: + PROCESS_CONDITION(processIfModifierCommand(false, GUIMASK)) + case CommandId_ifNotGui: + PROCESS_CONDITION(processIfModifierCommand(true, GUIMASK)) + case CommandId_ifCapsLockOn: + PROCESS_CONDITION(processIfStateKeyCommand(false, &UsbBasicKeyboard_CapsLockOn)) + case CommandId_ifNotCapsLockOn: + PROCESS_CONDITION(processIfStateKeyCommand(true, &UsbBasicKeyboard_CapsLockOn)) + case CommandId_ifNumLockOn: + PROCESS_CONDITION(processIfStateKeyCommand(false, &UsbBasicKeyboard_NumLockOn)) + case CommandId_ifNotNumLockOn: + PROCESS_CONDITION(processIfStateKeyCommand(true, &UsbBasicKeyboard_NumLockOn)) + case CommandId_ifScrollLockOn: + PROCESS_CONDITION(processIfStateKeyCommand(false, &UsbBasicKeyboard_ScrollLockOn)) + case CommandId_ifNotScrollLockOn: + PROCESS_CONDITION(processIfStateKeyCommand(true, &UsbBasicKeyboard_ScrollLockOn)) + case CommandId_ifRecording: + PROCESS_CONDITION(processIfRecordingCommand(false)) + case CommandId_ifNotRecording: + PROCESS_CONDITION(processIfRecordingCommand(true)) + case CommandId_ifRecordingId: + PROCESS_CONDITION(processIfRecordingIdCommand(ctx, false)) + case CommandId_ifNotRecordingId: + PROCESS_CONDITION(processIfRecordingIdCommand(ctx, true)) + case CommandId_ifNotPending: + PROCESS_CONDITION(processIfPendingCommand(ctx, true)) + case CommandId_ifPending: + PROCESS_CONDITION(processIfPendingCommand(ctx, false)) + case CommandId_ifKeyPendingAt: + PROCESS_CONDITION(processIfKeyPendingAtCommand(ctx, false)) + case CommandId_ifNotKeyPendingAt: + PROCESS_CONDITION(processIfKeyPendingAtCommand(ctx, true)) + case CommandId_ifKeyActive: + PROCESS_CONDITION(processIfKeyActiveCommand(ctx, false)) + case CommandId_ifNotKeyActive: + PROCESS_CONDITION(processIfKeyActiveCommand(ctx, true)) + case CommandId_ifPendingKeyReleased: + PROCESS_CONDITION(processIfPendingKeyReleasedCommand(ctx, false)) + case CommandId_ifNotPendingKeyReleased: + PROCESS_CONDITION(processIfPendingKeyReleasedCommand(ctx, true)) + case CommandId_ifKeyDefined: + PROCESS_CONDITION(processIfKeyDefinedCommand(ctx, false)) + case CommandId_ifNotKeyDefined: + PROCESS_CONDITION(processIfKeyDefinedCommand(ctx, true)) + case CommandId_ifModuleConnected: + PROCESS_CONDITION(processIfModuleConnected(ctx, false)) + case CommandId_ifNotModuleConnected: + PROCESS_CONDITION(processIfModuleConnected(ctx, true)) + case CommandId_ifHold: + return processIfHoldCommand(ctx, false); + case CommandId_ifTap: + return processIfHoldCommand(ctx, true); + case CommandId_ifSecondary: + return processIfSecondaryCommand(ctx, false); + case CommandId_ifPrimary: + return processIfSecondaryCommand(ctx, true); + case CommandId_ifShortcut: + return processIfShortcutCommand(ctx, false, true); + case CommandId_ifNotShortcut: + return processIfShortcutCommand(ctx, true, true); + case CommandId_ifGesture: + return processIfShortcutCommand(ctx, false, false); + case CommandId_ifNotGesture: + return processIfShortcutCommand(ctx, true, false); + case CommandId_ifRegEq: + case CommandId_ifNotRegEq: + Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `if ($varName == 1)`."); + return MacroResult_Finished; + case CommandId_ifRegGt: + case CommandId_ifRegLt: + Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `if ($varName >= 1)`."); + return MacroResult_Finished; + + // 'm' commands + case CommandId_macroArg: + return processMacroArgCommand(ctx); + case CommandId_mulReg: + Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `setVar varName ($varName*2)`."); + return MacroResult_Finished; + + // 'n' commands + case CommandId_noOp: + return processNoOpCommand(); + case CommandId_notify: + return Macros_ProcessNotifyCommand(ctx); + + // 'o' commands + case CommandId_oneShot: + return processOneShotCommand(ctx); + case CommandId_overlayLayer: + return processOverlayLayerCommand(ctx); + case CommandId_overlayKeymap: + return processOverlayKeymapCommand(ctx); + + // 'p' commands + case CommandId_printStatus: + return Macros_ProcessPrintStatusCommand(); + case CommandId_playMacro: + return processPlayMacroCommand(ctx); + case CommandId_pressKey: + return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Press, &S->ms.reports); + case CommandId_postponeKeys: + processPostponeKeysCommand(); + break; + case CommandId_postponeNext: + return processPostponeNextNCommand(ctx); + case CommandId_progressHue: + return processProgressHueCommand(); + case CommandId_powerMode: + return processPowerModeCommand(ctx); + case CommandId_panic: + return processPanicCommand(ctx); + + // 'r' commands + case CommandId_recordMacro: + return processRecordMacroCommand(ctx, false); + case CommandId_recordMacroBlind: + return processRecordMacroCommand(ctx, true); + case CommandId_recordMacroDelay: + return processRecordMacroDelayCommand(); + case CommandId_resolveNextKeyId: + return processResolveNextKeyIdCommand(); + case CommandId_releaseKey: + return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Release, &S->ms.reports); + case CommandId_repeatFor: + return processRepeatForCommand(ctx); + case CommandId_resetTrackpoint: + return processResetTrackpointCommand(); + case CommandId_replaceLayer: + return processReplaceLayerCommand(ctx); + case CommandId_replaceKeymap: + return processReplaceKeymapCommand(ctx); + case CommandId_resolveNextKeyEq: + Macros_ReportErrorPos(ctx, "Command deprecated. Please, replace resolveNextKeyEq by ifShortcut or ifGesture, or complain at github that you actually need this."); + return MacroResult_Finished; + case CommandId_resolveSecondary: + Macros_ReportErrorPos(ctx, "Command deprecated. Please, replace resolveSecondary by `ifPrimary advancedStrategy goTo ...` or `ifSecondary advancedStrategy goTo ...`."); + return MacroResult_Finished; + case CommandId_resetConfiguration: + return processResetConfigurationCommand(ctx); + case CommandId_reboot: + return processRebootCommand(); + case CommandId_reconnect: + return processReconnectCommand(); + + // 's' commands + case CommandId_set: + return Macro_ProcessSetCommand(ctx); + case CommandId_setVar: + return Macros_ProcessSetVarCommand(ctx); + case CommandId_setStatus: + return Macros_ProcessSetStatusCommand(ctx, true); + case CommandId_startRecording: + return processStartRecordingCommand(ctx, false); + case CommandId_startRecordingBlind: + return processStartRecordingCommand(ctx, true); + case CommandId_setLedTxt: + return Macros_ProcessSetLedTxtCommand(ctx); + case CommandId_statsRuntime: + return Macros_ProcessStatsRuntimeCommand(); + case CommandId_statsRecordKeyTiming: + return Macros_ProcessStatsRecordKeyTimingCommand(); + case CommandId_statsLayerStack: + return Macros_ProcessStatsLayerStackCommand(); + case CommandId_statsActiveKeys: + return Macros_ProcessStatsActiveKeysCommand(); + case CommandId_statsActiveMacros: + return Macros_ProcessStatsActiveMacrosCommand(); + case CommandId_statsPostponerStack: + return Macros_ProcessStatsPostponerStackCommand(); + case CommandId_statsVariables: + return Macros_ProcessStatsVariablesCommand(); + case CommandId_statsBattery: + return Macros_ProcessStatsBatteryCommand(); + case CommandId_switchKeymap: + return processSwitchKeymapCommand(ctx); + case CommandId_startMouse: + return processMouseCommand(ctx, true); + case CommandId_stopMouse: + return processMouseCommand(ctx, false); + case CommandId_stopRecording: + case CommandId_stopRecordingBlind: + return processStopRecordingCommand(); + case CommandId_stopAllMacros: + return processStopAllMacrosCommand(); + case CommandId_suppressMods: + processSuppressModsCommand(); + break; + case CommandId_setReg: + Macros_ReportErrorPos(ctx, "Command was removed, please use named variables. E.g., `setVar myVar 1` and `write \"$myVar\"`"); + return MacroResult_Finished; + case CommandId_subReg: + Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `setVar varName ($varName+1)`."); + return MacroResult_Finished; + case CommandId_setStatusPart: + Macros_ReportErrorPos(ctx, "Command was removed, please use string interpolated setStatus."); + return MacroResult_Finished; + case CommandId_switchKeymapLayer: + case CommandId_switchLayer: + Macros_ReportErrorPos(ctx, "Command deprecated. Please, replace switchKeymapLayer by toggleKeymapLayer or holdKeymapLayer. Or complain on github that you actually need this command."); + return MacroResult_Finished; + case CommandId_switchHost: + return processSwitchHostCommand(ctx); + + // 't' commands + case CommandId_toggleKeymapLayer: + return processToggleKeymapLayerCommand(ctx); + case CommandId_toggleLayer: + return processToggleLayerCommand(ctx); + case CommandId_tapKey: + return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Tap, &S->ms.reports); + case CommandId_tapKeySeq: + return Macros_ProcessTapKeySeqCommand(ctx); + case CommandId_toggleKey: + return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Toggle, &S->ms.reports); + case CommandId_trackpoint: + return processTrackpointCommand(ctx); + case CommandId_trace: + if (!Macros_DryRun) { + Trace_Print(LogTarget_ErrorBuffer, "Triggered by macro command"); + } + return MacroResult_Finished; + case CommandId_testLeakage: + return processTestLeakageCommand(ctx); + case CommandId_testSuite: + return processTestSuiteCommand(ctx); + + // 'u' commands + case CommandId_unToggleLayer: + case CommandId_untoggleLayer: + return processUnToggleLayerCommand(); + case CommandId_unpairHost: + return Macros_ProcessUnpairHostCommand(ctx); + + // 'v' commands + case CommandId_validateUserConfig: + case CommandId_validateMacros: + return processValidateMacrosCommand(ctx); + + // 'w' commands + case CommandId_write: + return processWriteCommand(ctx); + case CommandId_while: + return processWhileCommand(ctx); + case CommandId_writeExpr: + Macros_ReportErrorPos(ctx, "writeExpr is now deprecated, please migrate to interpolated strings"); + return MacroResult_Finished; + + // 'y' commands + case CommandId_yield: + return processYieldCommand(ctx); + + // 'z' commands + case CommandId_zephyr: + return processZephyrCommand(ctx); + + // brace commands + case CommandId_openBrace: + return processOpeningBraceCommand(ctx); + case CommandId_closeBrace: + return processClosingBraceCommand(ctx); + + default: + Macros_ReportErrorTok(ctx, "Unrecognized command:"); + return MacroResult_Finished; + } + + // this is reachable when 'ifXxx' conditions pass; processCommand() should continue with further commands. + return MacroResult_None; +} + static macro_result_t processCommand(parser_context_t* ctx) { const char* cmdTokEnd = TokEnd(ctx->at, ctx->end); if (cmdTokEnd > ctx->at && cmdTokEnd[-1] == ':') { //skip labels ConsumeAnyToken(ctx); - if (ctx->at == ctx->end && IsEnd(ctx)) { + // TODO: why not just IsEnd() in this condition? + // The ctx->at check is done inside IsEnd() anyway. + if (/* ctx->at >= ctx->end && */ IsEnd(ctx)) { return MacroResult_Finished; } } @@ -2019,388 +2502,18 @@ static macro_result_t processCommand(parser_context_t* ctx) return MacroResult_Finished; } - // Dispatch based on command ID - switch (entry->id) { - // 'a' commands - case CommandId_activateKeyPostponed: - return processActivateKeyPostponedCommand(ctx); - case CommandId_autoRepeat: - return processAutoRepeatCommand(ctx); - case CommandId_addReg: - Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `setVar varName ($varName+1)`."); - return MacroResult_Finished; + macro_result_t res = dispatchCommand(ctx, entry->id); - // 'b' commands - case CommandId_break: - return processBreakCommand(ctx); - case CommandId_bluetooth: - return processBluetoothCommand(ctx); - - // 'c' commands - case CommandId_consumePending: - return processConsumePendingCommand(ctx); - case CommandId_clearStatus: - return Macros_ProcessClearStatusCommand(true); - case CommandId_call: - return processCallCommand(ctx); - - // 'd' commands - case CommandId_delayUntilRelease: - return processDelayUntilReleaseCommand(); - case CommandId_delayUntilReleaseMax: - return processDelayUntilReleaseMaxCommand(ctx); - case CommandId_delayUntil: - return processDelayUntilCommand(ctx); - case CommandId_diagnose: - return Macros_ProcessDiagnoseCommand(); - - // 'e' commands - case CommandId_exec: - return processExecCommand(ctx); - case CommandId_else: - if (!Macros_DryRun && S->ls->ms.lastIfSucceeded) { - return MacroResult_Finished; - } - break; - case CommandId_exit: - return processExitCommand(ctx); - - // 'f' commands - case CommandId_final: - return processFinalCommand(ctx); - case CommandId_fork: - return processForkCommand(ctx); - case CommandId_freeze: - return processFreezeCommand(ctx); - - // 'g' commands - case CommandId_goTo: - return processGoToCommand(ctx); - - // 'h' commands - case CommandId_holdLayer: - return processHoldLayerCommand(ctx); - case CommandId_holdLayerMax: - return processHoldLayerMaxCommand(ctx); - case CommandId_holdKeymapLayer: - return processHoldKeymapLayerCommand(ctx); - case CommandId_holdKeymapLayerMax: - return processHoldKeymapLayerMaxCommand(ctx); - case CommandId_holdKey: - return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Hold, &S->ms.reports); - - // 'i' commands - conditionals - case CommandId_if: - PROCESS_CONDITION(processIfCommand(ctx)) - case CommandId_ifDoubletap: - PROCESS_CONDITION(processIfDoubletapCommand(false)) - case CommandId_ifNotDoubletap: - PROCESS_CONDITION(processIfDoubletapCommand(true)) - case CommandId_ifInterrupted: - PROCESS_CONDITION(processIfInterruptedCommand(false)) - case CommandId_ifNotInterrupted: - PROCESS_CONDITION(processIfInterruptedCommand(true)) - case CommandId_ifReleased: - PROCESS_CONDITION(processIfReleasedCommand(false)) - case CommandId_ifNotReleased: - PROCESS_CONDITION(processIfReleasedCommand(true)) - case CommandId_ifKeymap: - PROCESS_CONDITION(processIfKeymapCommand(ctx, false)) - case CommandId_ifNotKeymap: - PROCESS_CONDITION(processIfKeymapCommand(ctx, true)) - case CommandId_ifLayer: - PROCESS_CONDITION(processIfLayerCommand(ctx, false)) - case CommandId_ifNotLayer: - PROCESS_CONDITION(processIfLayerCommand(ctx, true)) - case CommandId_ifLayerToggled: - PROCESS_CONDITION(processIfLayerToggledCommand(ctx, false)) - case CommandId_ifNotLayerToggled: - PROCESS_CONDITION(processIfLayerToggledCommand(ctx, true)) - case CommandId_ifPlaytime: - PROCESS_CONDITION(processIfPlaytimeCommand(ctx, false)) - case CommandId_ifNotPlaytime: - PROCESS_CONDITION(processIfPlaytimeCommand(ctx, true)) - case CommandId_ifAnyMod: - PROCESS_CONDITION(processIfModifierCommand(false, 0xFF)) - case CommandId_ifNotAnyMod: - PROCESS_CONDITION(processIfModifierCommand(true, 0xFF)) - case CommandId_ifShift: - PROCESS_CONDITION(processIfModifierCommand(false, SHIFTMASK)) - case CommandId_ifNotShift: - PROCESS_CONDITION(processIfModifierCommand(true, SHIFTMASK)) - case CommandId_ifCtrl: - PROCESS_CONDITION(processIfModifierCommand(false, CTRLMASK)) - case CommandId_ifNotCtrl: - PROCESS_CONDITION(processIfModifierCommand(true, CTRLMASK)) - case CommandId_ifAlt: - PROCESS_CONDITION(processIfModifierCommand(false, ALTMASK)) - case CommandId_ifNotAlt: - PROCESS_CONDITION(processIfModifierCommand(true, ALTMASK)) - case CommandId_ifGui: - PROCESS_CONDITION(processIfModifierCommand(false, GUIMASK)) - case CommandId_ifNotGui: - PROCESS_CONDITION(processIfModifierCommand(true, GUIMASK)) - case CommandId_ifCapsLockOn: - PROCESS_CONDITION(processIfStateKeyCommand(false, &UsbBasicKeyboard_CapsLockOn)) - case CommandId_ifNotCapsLockOn: - PROCESS_CONDITION(processIfStateKeyCommand(true, &UsbBasicKeyboard_CapsLockOn)) - case CommandId_ifNumLockOn: - PROCESS_CONDITION(processIfStateKeyCommand(false, &UsbBasicKeyboard_NumLockOn)) - case CommandId_ifNotNumLockOn: - PROCESS_CONDITION(processIfStateKeyCommand(true, &UsbBasicKeyboard_NumLockOn)) - case CommandId_ifScrollLockOn: - PROCESS_CONDITION(processIfStateKeyCommand(false, &UsbBasicKeyboard_ScrollLockOn)) - case CommandId_ifNotScrollLockOn: - PROCESS_CONDITION(processIfStateKeyCommand(true, &UsbBasicKeyboard_ScrollLockOn)) - case CommandId_ifRecording: - PROCESS_CONDITION(processIfRecordingCommand(false)) - case CommandId_ifNotRecording: - PROCESS_CONDITION(processIfRecordingCommand(true)) - case CommandId_ifRecordingId: - PROCESS_CONDITION(processIfRecordingIdCommand(ctx, false)) - case CommandId_ifNotRecordingId: - PROCESS_CONDITION(processIfRecordingIdCommand(ctx, true)) - case CommandId_ifNotPending: - PROCESS_CONDITION(processIfPendingCommand(ctx, true)) - case CommandId_ifPending: - PROCESS_CONDITION(processIfPendingCommand(ctx, false)) - case CommandId_ifKeyPendingAt: - PROCESS_CONDITION(processIfKeyPendingAtCommand(ctx, false)) - case CommandId_ifNotKeyPendingAt: - PROCESS_CONDITION(processIfKeyPendingAtCommand(ctx, true)) - case CommandId_ifKeyActive: - PROCESS_CONDITION(processIfKeyActiveCommand(ctx, false)) - case CommandId_ifNotKeyActive: - PROCESS_CONDITION(processIfKeyActiveCommand(ctx, true)) - case CommandId_ifPendingKeyReleased: - PROCESS_CONDITION(processIfPendingKeyReleasedCommand(ctx, false)) - case CommandId_ifNotPendingKeyReleased: - PROCESS_CONDITION(processIfPendingKeyReleasedCommand(ctx, true)) - case CommandId_ifKeyDefined: - PROCESS_CONDITION(processIfKeyDefinedCommand(ctx, false)) - case CommandId_ifNotKeyDefined: - PROCESS_CONDITION(processIfKeyDefinedCommand(ctx, true)) - case CommandId_ifModuleConnected: - PROCESS_CONDITION(processIfModuleConnected(ctx, false)) - case CommandId_ifNotModuleConnected: - PROCESS_CONDITION(processIfModuleConnected(ctx, true)) - case CommandId_ifHold: - return processIfHoldCommand(ctx, false); - case CommandId_ifTap: - return processIfHoldCommand(ctx, true); - case CommandId_ifSecondary: - return processIfSecondaryCommand(ctx, false); - case CommandId_ifPrimary: - return processIfSecondaryCommand(ctx, true); - case CommandId_ifShortcut: - return processIfShortcutCommand(ctx, false, true); - case CommandId_ifNotShortcut: - return processIfShortcutCommand(ctx, true, true); - case CommandId_ifGesture: - return processIfShortcutCommand(ctx, false, false); - case CommandId_ifNotGesture: - return processIfShortcutCommand(ctx, true, false); - case CommandId_ifRegEq: - case CommandId_ifNotRegEq: - Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `if ($varName == 1)`."); - return MacroResult_Finished; - case CommandId_ifRegGt: - case CommandId_ifRegLt: - Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `if ($varName >= 1)`."); - return MacroResult_Finished; - - // 'm' commands - case CommandId_mulReg: - Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `setVar varName ($varName*2)`."); - return MacroResult_Finished; - - // 'n' commands - case CommandId_noOp: - return processNoOpCommand(); - case CommandId_notify: - return Macros_ProcessNotifyCommand(ctx); - - // 'o' commands - case CommandId_oneShot: - return processOneShotCommand(ctx); - case CommandId_overlayLayer: - return processOverlayLayerCommand(ctx); - case CommandId_overlayKeymap: - return processOverlayKeymapCommand(ctx); - - // 'p' commands - case CommandId_printStatus: - return Macros_ProcessPrintStatusCommand(); - case CommandId_playMacro: - return processPlayMacroCommand(ctx); - case CommandId_pressKey: - return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Press, &S->ms.reports); - case CommandId_postponeKeys: - processPostponeKeysCommand(); - break; - case CommandId_postponeNext: - return processPostponeNextNCommand(ctx); - case CommandId_progressHue: - return processProgressHueCommand(); - case CommandId_powerMode: - return processPowerModeCommand(ctx); - case CommandId_panic: - return processPanicCommand(ctx); - - // 'r' commands - case CommandId_recordMacro: - return processRecordMacroCommand(ctx, false); - case CommandId_recordMacroBlind: - return processRecordMacroCommand(ctx, true); - case CommandId_recordMacroDelay: - return processRecordMacroDelayCommand(); - case CommandId_resolveNextKeyId: - return processResolveNextKeyIdCommand(); - case CommandId_releaseKey: - return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Release, &S->ms.reports); - case CommandId_repeatFor: - return processRepeatForCommand(ctx); - case CommandId_resetTrackpoint: - return processResetTrackpointCommand(); - case CommandId_replaceLayer: - return processReplaceLayerCommand(ctx); - case CommandId_replaceKeymap: - return processReplaceKeymapCommand(ctx); - case CommandId_resolveNextKeyEq: - Macros_ReportErrorPos(ctx, "Command deprecated. Please, replace resolveNextKeyEq by ifShortcut or ifGesture, or complain at github that you actually need this."); - return MacroResult_Finished; - case CommandId_resolveSecondary: - Macros_ReportErrorPos(ctx, "Command deprecated. Please, replace resolveSecondary by `ifPrimary advancedStrategy goTo ...` or `ifSecondary advancedStrategy goTo ...`."); - return MacroResult_Finished; - case CommandId_resetConfiguration: - return processResetConfigurationCommand(ctx); - case CommandId_reboot: - return processRebootCommand(); - case CommandId_reconnect: - return processReconnectCommand(); - - // 's' commands - case CommandId_set: - return Macro_ProcessSetCommand(ctx); - case CommandId_setVar: - return Macros_ProcessSetVarCommand(ctx); - case CommandId_setStatus: - return Macros_ProcessSetStatusCommand(ctx, true); - case CommandId_startRecording: - return processStartRecordingCommand(ctx, false); - case CommandId_startRecordingBlind: - return processStartRecordingCommand(ctx, true); - case CommandId_setLedTxt: - return Macros_ProcessSetLedTxtCommand(ctx); - case CommandId_statsRuntime: - return Macros_ProcessStatsRuntimeCommand(); - case CommandId_statsRecordKeyTiming: - return Macros_ProcessStatsRecordKeyTimingCommand(); - case CommandId_statsLayerStack: - return Macros_ProcessStatsLayerStackCommand(); - case CommandId_statsActiveKeys: - return Macros_ProcessStatsActiveKeysCommand(); - case CommandId_statsActiveMacros: - return Macros_ProcessStatsActiveMacrosCommand(); - case CommandId_statsPostponerStack: - return Macros_ProcessStatsPostponerStackCommand(); - case CommandId_statsVariables: - return Macros_ProcessStatsVariablesCommand(); - case CommandId_statsBattery: - return Macros_ProcessStatsBatteryCommand(); - case CommandId_switchKeymap: - return processSwitchKeymapCommand(ctx); - case CommandId_startMouse: - return processMouseCommand(ctx, true); - case CommandId_stopMouse: - return processMouseCommand(ctx, false); - case CommandId_stopRecording: - case CommandId_stopRecordingBlind: - return processStopRecordingCommand(); - case CommandId_stopAllMacros: - return processStopAllMacrosCommand(); - case CommandId_suppressMods: - processSuppressModsCommand(); - break; - case CommandId_setReg: - Macros_ReportErrorPos(ctx, "Command was removed, please use named variables. E.g., `setVar myVar 1` and `write \"$myVar\"`"); - return MacroResult_Finished; - case CommandId_subReg: - Macros_ReportErrorPos(ctx, "Command was removed, please use command similar to `setVar varName ($varName+1)`."); - return MacroResult_Finished; - case CommandId_setStatusPart: - Macros_ReportErrorPos(ctx, "Command was removed, please use string interpolated setStatus."); - return MacroResult_Finished; - case CommandId_switchKeymapLayer: - case CommandId_switchLayer: - Macros_ReportErrorPos(ctx, "Command deprecated. Please, replace switchKeymapLayer by toggleKeymapLayer or holdKeymapLayer. Or complain on github that you actually need this command."); - return MacroResult_Finished; - case CommandId_switchHost: - return processSwitchHostCommand(ctx); - - // 't' commands - case CommandId_toggleKeymapLayer: - return processToggleKeymapLayerCommand(ctx); - case CommandId_toggleLayer: - return processToggleLayerCommand(ctx); - case CommandId_tapKey: - return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Tap, &S->ms.reports); - case CommandId_tapKeySeq: - return Macros_ProcessTapKeySeqCommand(ctx); - case CommandId_toggleKey: - return Macros_ProcessKeyCommandAndConsume(ctx, MacroSubAction_Toggle, &S->ms.reports); - case CommandId_trackpoint: - return processTrackpointCommand(ctx); - case CommandId_trace: - if (!Macros_DryRun) { - Trace_Print(LogTarget_ErrorBuffer, "Triggered by macro command"); - } - return MacroResult_Finished; - case CommandId_testLeakage: - return processTestLeakageCommand(ctx); - case CommandId_testSuite: - return processTestSuiteCommand(ctx); - - // 'u' commands - case CommandId_unToggleLayer: - case CommandId_untoggleLayer: - return processUnToggleLayerCommand(); - case CommandId_unpairHost: - return Macros_ProcessUnpairHostCommand(ctx); - - // 'v' commands - case CommandId_validateUserConfig: - case CommandId_validateMacros: - return processValidateMacrosCommand(ctx); - - // 'w' commands - case CommandId_write: - return processWriteCommand(ctx); - case CommandId_while: - return processWhileCommand(ctx); - case CommandId_writeExpr: - Macros_ReportErrorPos(ctx, "writeExpr is now deprecated, please migrate to interpolated strings"); - return MacroResult_Finished; - - // 'y' commands - case CommandId_yield: - return processYieldCommand(ctx); - - // 'z' commands - case CommandId_zephyr: - return processZephyrCommand(ctx); - - // brace commands - case CommandId_openBrace: - return processOpeningBraceCommand(ctx); - case CommandId_closeBrace: - return processClosingBraceCommand(ctx); - - default: - Macros_ReportErrorTok(ctx, "Unrecognized command:"); + if (res == MacroResult_Header) { return MacroResult_Finished; + } else { + S->ms.macroHeadersProcessed = true; // non-header commands mark the header as finished } + if (res != MacroResult_None) { + return res; + } } + //this is reachable if there is a train of conditions/modifiers/labels without any command return MacroResult_Finished; } diff --git a/right/src/macros/core.c b/right/src/macros/core.c index d06c088e6..152b36a48 100644 --- a/right/src/macros/core.c +++ b/right/src/macros/core.c @@ -11,6 +11,7 @@ #include "macros/scancode_commands.h" #include "macros/status_buffer.h" #include "macros/typedefs.h" +#include "macros/vars.h" #include "module.h" #include "postponer.h" #include @@ -32,7 +33,6 @@ macro_reference_t AllMacros[MacroIndex_MaxCount] = { }; uint8_t AllMacrosCount; - bool Macros_WakedBecauseOfOneShot = false; bool Macros_WakedBecauseOfTime = false; bool Macros_WakedBecauseOfKeystateChange = false; @@ -297,6 +297,9 @@ static macro_result_t endMacro(void) EventVector_Set(EventVector_SendUsbReports); } + // Deallocate all macro arguments owned by this macro + Macros_DeallocateMacroArgumentsByOwner(MACRO_STATE_SLOT(S)); + freeLocalScopes(); S->ms.macroSleeping = false; diff --git a/right/src/macros/core.h b/right/src/macros/core.h index 30e545194..877bb2306 100644 --- a/right/src/macros/core.h +++ b/right/src/macros/core.h @@ -11,17 +11,22 @@ #include "key_states.h" #include "str_utils.h" #include "macros/typedefs.h" + #include "macros/vars.h" #include "event_scheduler.h" // Macros: #define MACRO_CYCLES_TO_POSTPONE 4 + #define MACRO_STATE_SLOT(S) ((S) - MacroState) + #define MAX_MACRO_NUM 255 #define MACRO_STATE_POOL_SIZE 16 #define MACRO_HISTORY_POOL_SIZE 16 #define MACRO_SCOPE_STATE_POOL_SIZE (MACRO_STATE_POOL_SIZE*2) #define MAX_REG_COUNT 32 + #define MAX_MACRO_ARGUMENT_COUNT 8 + #define ALTMASK (HID_KEYBOARD_MODIFIER_LEFTALT | HID_KEYBOARD_MODIFIER_RIGHTALT) #define CTRLMASK (HID_KEYBOARD_MODIFIER_LEFTCTRL | HID_KEYBOARD_MODIFIER_RIGHTCTRL) #define SHIFTMASK (HID_KEYBOARD_MODIFIER_LEFTSHIFT | HID_KEYBOARD_MODIFIER_RIGHTSHIFT) @@ -168,8 +173,8 @@ macro_autorepeat_state_t autoRepeatPhase: 1; bool isDoubletap: 1; secondary_role_state_t secondaryRoleState: 2; + bool macroHeadersProcessed : 1; // ---- 4-aligned ---- - macro_usb_keyboard_reports_t reports; } ms; diff --git a/right/src/macros/display.c b/right/src/macros/display.c index c911a888e..721232ef5 100644 --- a/right/src/macros/display.c +++ b/right/src/macros/display.c @@ -37,6 +37,10 @@ static uint8_t consumeDisplayString(parser_context_t* ctx, char* str, uint8_t le if (Macros_IsNUM(ctx)) { #ifndef __ZEPHYR__ macro_variable_t value = Macros_ConsumeAnyValue(ctx); + if (value.type == MacroVariableType_None) { + Macros_ReportErrorTok(ctx, "Could not resolve:"); + return 0; + } SegmentDisplay_SerializeVar(str, value); textLen = 3; #else @@ -44,9 +48,12 @@ static uint8_t consumeDisplayString(parser_context_t* ctx, char* str, uint8_t le #endif return textLen; } else if (ctx->at != ctx->end) { - uint16_t stringOffset = 0, textIndex = 0, textSubIndex = 0; + string_reader_context_t stringCtx; + + StrRead_InitContext(ctx, &stringCtx, StrReadMode_Literal); + for (uint8_t i = 0; true; i++) { - char c = Macros_ConsumeCharOfString(ctx, &stringOffset, &textIndex, &textSubIndex); + char c = StrRead_ConsumeCharOfString(ctx, &stringCtx); if (c == '\0') { break; } @@ -211,6 +218,7 @@ void processList(parser_context_t* ctx, bool show, uint16_t time) { macro_result_t Macros_ProcessSetLedTxtCommand(parser_context_t* ctx) { + // TODO: I guess ATTR_UNUSED is not correct here? ATTR_UNUSED int16_t time = Macros_ConsumeInt(ctx); macro_result_t res = MacroResult_Finished; diff --git a/right/src/macros/keyid_parser.c b/right/src/macros/keyid_parser.c index ba53fcf21..516ffc0c6 100644 --- a/right/src/macros/keyid_parser.c +++ b/right/src/macros/keyid_parser.c @@ -166,12 +166,15 @@ static const lookup_record_t* lookup(uint8_t begin, uint8_t end, const char* str } } +// this attempts to consume a KEYID_ABBREV (textual key id)). +// returns the key id, or 255 if not found (invalid key id). uint8_t MacroKeyIdParser_TryConsumeKeyId(parser_context_t* ctx) { // this gets identifier till the next dot only const char* end1 = IdentifierEnd(ctx); const lookup_record_t* record = lookup(0, lookup_size-1, ctx->at, end1); + // TODO: WHY this second attempt with dot?? // if failed, try consume with dot if (record == NULL && *end1 == '.' && end1+1 < ctx->end) { CTX_COPY(ctx2, *ctx); diff --git a/right/src/macros/scancode_commands.c b/right/src/macros/scancode_commands.c index 353305459..4ff777489 100644 --- a/right/src/macros/scancode_commands.c +++ b/right/src/macros/scancode_commands.c @@ -489,6 +489,18 @@ static macro_action_t decodeKeyAndConsume(parser_context_t* ctx, macro_sub_actio return action; } +void dequoteContext(parser_context_t* ctx) +{ + if (ctx->at < ctx->end && (*ctx->at == '\'' || *ctx->at == '"')) { + char limiter = *ctx->at++; // remember starting quote and skip it + if (ctx->end > ctx->at && *(ctx->end - 1) == limiter) { // check if ending quote matches starting quote + ctx->end--; // if yes, skip the ending quote as well + } else { + ctx->at--; // if not, step back and keep quotes + } + } +} + macro_result_t Macros_ProcessKeyCommandAndConsume(parser_context_t* ctx, macro_sub_action_t type, macro_usb_keyboard_reports_t* reports) { if (reports == NULL) { @@ -498,7 +510,38 @@ macro_result_t Macros_ProcessKeyCommandAndConsume(parser_context_t* ctx, macro_s reports = &Macros_PersistentReports; } - macro_action_t action = decodeKeyAndConsume(ctx, type); + macro_action_t action; + + // Allow $macroArg.xxx for type scancode ("modded scancode") here as well. + // - check for $ + // - if found, call Macros_ConsumeString() to get a string segment (uses consumeValue()) + // - Macros_ConsumeString() is new (in vars.c) and should coalesceType to string + // - parse that string segment as a shortcut (with MacroShortcutParser_Parse) to get the scancode and modifiers + + if (*ctx->at == '$') { + string_segment_t segment = Macros_ConsumeString(ctx); + if (segment.start == NULL) { + Macros_ReportErrorTok(ctx, "Expected shortcut string but found:"); + return MacroResult_Finished; + } + parser_context_t stringCtx = (parser_context_t) { + .begin = segment.start, + .at = segment.start, + .end = segment.end, + .macroState = ctx->macroState, + .nestingLevel = ctx->nestingLevel, + .nestingBound = ctx->nestingBound, + }; + dequoteContext(&stringCtx); // remove enclosing quotes if they exist (hack to allow simple strings) + action = decodeKeyAndConsume(&stringCtx, type); + // the next part should not be necessary, because dequoteContext should have already removed the quotes. + if (stringCtx.at < stringCtx.end && (*stringCtx.at == '\'' || *stringCtx.at == '"')) { + stringCtx.at++; + } + } + else { + action = decodeKeyAndConsume(ctx, type); + } if (Macros_DryRun) { return MacroResult_Finished; diff --git a/right/src/macros/set_command.c b/right/src/macros/set_command.c index ffd6ad358..d78aeff27 100644 --- a/right/src/macros/set_command.c +++ b/right/src/macros/set_command.c @@ -266,14 +266,14 @@ static macro_variable_t module(parser_context_t* ctx, set_command_action_t actio module_id_t moduleId = ConsumeModuleId(ctx); module_configuration_t* module = GetModuleConfiguration(moduleId); - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); if (Macros_ParserError) { return noneVar(); } if (ConsumeToken(ctx, "navigationMode")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return moduleNavigationMode(ctx, action, module); } else if (ConsumeToken(ctx, "holdContinuationTimeout") && moduleId == ModuleId_TouchpadRight) { @@ -348,7 +348,7 @@ static macro_variable_t secondaryRoles(parser_context_t* ctx, set_command_action ASSIGN_CUSTOM(int32_t, intVar, Cfg.SecondaryRoles_Strategy, ConsumeSecondaryRoleStrategy(ctx)); } else if (ConsumeToken(ctx, "advanced")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return secondaryRoleAdvanced(ctx, action); } else { @@ -414,7 +414,7 @@ static macro_variable_t mouseKeys(parser_context_t* ctx, set_command_action_t ac return noneVar(); } - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); if (ConsumeToken(ctx, "initialSpeed")) { DEFINE_INT_LIMITS(0, 255); @@ -540,7 +540,7 @@ static macro_variable_t keyRgb(parser_context_t* ctx, set_command_action_t actio { layer_id_t layerId = Macros_ConsumeLayerId(ctx); - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); uint16_t keyId = Macros_TryConsumeKeyId(ctx); @@ -703,11 +703,11 @@ static macro_variable_t backlight(parser_context_t* ctx, set_command_action_t ac return backlightStrategy(ctx, action); } else if (ConsumeToken(ctx, "constantRgb")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return constantRgb(ctx, action); } else if (ConsumeToken(ctx, "keyRgb")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return keyRgb(ctx, action); } else { @@ -756,7 +756,7 @@ static macro_variable_t navigationModeAction(parser_context_t* ctx, set_command_ navigationMode = ConsumeNavigationModeId(ctx); - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); if (action == SetCommandAction_Read) { Macros_ReportErrorPos(ctx, "Reading actions is not supported!"); @@ -810,7 +810,7 @@ static macro_variable_t keymapAction(parser_context_t* ctx, set_command_action_t uint8_t layerId = Macros_ConsumeLayerId(ctx); CTX_COPY(keyIdPos, *ctx); - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); uint16_t keyId = Macros_TryConsumeKeyId(ctx); @@ -960,47 +960,47 @@ static macro_variable_t setMaxVoltage(parser_context_t* ctx, set_command_action_ static macro_variable_t root(parser_context_t* ctx, set_command_action_t action) { if (ConsumeToken(ctx, "module")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return module(ctx, action); } else if (ConsumeToken(ctx, "secondaryRole")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return secondaryRoles(ctx, action); } else if (ConsumeToken(ctx, "bluetooth")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return bluetooth(ctx, action); } else if (ConsumeToken(ctx, "mouseKeys")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return mouseKeys(ctx, action); } else if (ConsumeToken(ctx, "keymapAction")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return keymapAction(ctx, action); } else if (ConsumeToken(ctx, "navigationModeAction")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return navigationModeAction(ctx, action); } else if (ConsumeToken(ctx, "macroEngine")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return macroEngine(ctx, action); } else if (ConsumeToken(ctx, "backlight")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return backlight(ctx, action); } else if (ConsumeToken(ctx, "battery")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return battery(ctx, action); } else if (ConsumeToken(ctx, "leds")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return leds(ctx, action); } else if (ConsumeToken(ctx, "modifierLayerTriggers")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); return modLayerTriggers(ctx, action); } else if (ConsumeToken(ctx, "maxVoltage")) { diff --git a/right/src/macros/status_buffer.c b/right/src/macros/status_buffer.c index 326d9f6b4..65029757d 100644 --- a/right/src/macros/status_buffer.c +++ b/right/src/macros/status_buffer.c @@ -224,7 +224,7 @@ static uint16_t findPosition(const char* arg) const char* startOfLine = S->ms.currentMacroAction.cmd.text + S->ls->ms.commandBegin; const char* endOfLine = S->ms.currentMacroAction.cmd.text + S->ls->ms.commandEnd; - if (arg < startOfLine || endOfLine < arg) { + if (arg < startOfLine || arg > endOfLine) { return 1; } return arg - startOfLine + 1; @@ -240,7 +240,7 @@ static uint16_t findPositionCtx(const parser_context_t* ctx) ctx = ViewContext(0); } - if (ctx->at < ctx->begin || ctx->end < ctx->at) { + if (ctx->at < ctx->begin || ctx->at > ctx->end) { return 1; } @@ -293,9 +293,9 @@ static void reportCommandLocation(uint16_t line, uint16_t pos, const char* begin } static void reportLocationStackLevel(const parser_context_t* ctx, uint16_t line, uint8_t indent) { - uint16_t pos = ctx->at - ctx->begin; - bool positionIsValid = ctx->begin <= ctx->at && ctx->at <= ctx->end; + bool positionIsValid = ctx->at >= ctx->begin && ctx->at <= ctx->end; if (positionIsValid) { + uint16_t pos = ctx->at - ctx->begin; reportCommandLocation(line, pos, ctx->begin, ctx->end, positionIsValid, indent); } else { Macros_SetStatusString("> Position not available here.\n", NULL); @@ -320,7 +320,6 @@ static void reportError( Macros_SetStatusString(err, NULL); if (S != NULL) { - bool argIsCommand = ValidatedUserConfigBuffer.buffer <= (uint8_t*)arg && (uint8_t*)arg < ValidatedUserConfigBuffer.buffer + USER_CONFIG_SIZE; if (arg != NULL && arg != argEnd) { Macros_SetStatusString(" ", NULL); Macros_SetStatusString(arg, TokEnd(arg, argEnd)); @@ -329,7 +328,8 @@ static void reportError( const char* startOfLine = S->ms.currentMacroAction.cmd.text + S->ls->ms.commandBegin; const char* endOfLine = S->ms.currentMacroAction.cmd.text + S->ls->ms.commandEnd; uint16_t line = findCurrentCommandLine(); - if (startOfLine <= arg && arg <= endOfLine) { + if (arg != NULL && argEnd != NULL && argEnd >= startOfLine && arg <= endOfLine) { + bool argIsCommand = (uint8_t*)arg >= ValidatedUserConfigBuffer.buffer && (uint8_t*)arg < ValidatedUserConfigBuffer.buffer + USER_CONFIG_SIZE; reportCommandLocation(line, arg - startOfLine, startOfLine, endOfLine, argIsCommand, 0); } else if (ctx != NULL) { reportLocationStack(ctx, line); diff --git a/right/src/macros/string_reader.c b/right/src/macros/string_reader.c index 509e72ff2..a16ac239f 100644 --- a/right/src/macros/string_reader.c +++ b/right/src/macros/string_reader.c @@ -12,12 +12,287 @@ #define MAX(a, b) ((a) > (b) ? (a) : (b)) #endif -typedef enum { - StringType_Raw, - StringType_DoubleQuote, - StringType_SingleQuote, -} string_type_t; +// new code: +static char consumeExpressionCharOfInt(const macro_variable_t* variable, uint16_t* idx); +static char consumeExpressionCharOfFloat(const macro_variable_t* variable, uint16_t* idx); +static char consumeExpressionCharOfBool(const macro_variable_t* variable, uint16_t* idx); +static char consumeExpressionCharOfString(const macro_variable_t* variable, uint16_t* idx); +static char consumeCharOfTemplate(parser_context_t* ctx, string_type_t stringType, uint16_t* index); +static char consumeExpressionChar(parser_context_t* ctx, string_type_t stringType, uint16_t* index); + +static char StrRead_consumeExpressionChar(parser_context_t* ctx, string_reader_context_t* stringCtx); +static char StrRead_consumeExpressionCharOfString(const macro_variable_t* variable, uint16_t* idx); + +void StrRead_InitContext(parser_context_t* ctx, string_reader_context_t* stringCtx, string_reader_mode_t mode) +{ + stringCtx->at = ctx->at; + stringCtx->stringOffset = 0; + stringCtx->index = 0; + stringCtx->subIndex = 0; + if (mode == StrReadMode_Verbatim) { + stringCtx->stringType = StringType_Verbatim; + } else { + stringCtx->stringType = StringType_Undetermined; + } +} + +static char StrRead_tryConsumeAnotherStringLiteral(parser_context_t *ctx, string_reader_context_t *stringCtx) +{ + const char* at = ctx->at + stringCtx->stringOffset + stringCtx->index; + + if (at >= ctx->end) { + ctx->at = ctx->end; + return '\0'; + } + + switch (*at) { + case '\'': + case '"': + // advance the string reader context to the beginning quote of the next literal + stringCtx->stringOffset += stringCtx->index; + stringCtx->index = 0; + return StrRead_ConsumeCharOfString(ctx, stringCtx); + default: + // advance the main context to the end of the current string + ctx->at = at; + ConsumeWhite(ctx); + // we probably don't need to reset the string reader context here, + // any new string reading should call InitContext() again. + stringCtx->stringOffset = 0; + stringCtx->index = 0; + stringCtx->subIndex = 0; + return '\0'; + } +} + +static char StrRead_ConsumeCharInString(parser_context_t* ctx, string_reader_context_t* stringCtx) +{ + const char* at = stringCtx->at; + if (at >= ctx->end) { + return '\0'; + } + + if(stringCtx->stringType == StringType_Verbatim) { + char res = *at; + stringCtx->index++; + return res; + } + + switch(*at) { + case '\\': + if (stringCtx->stringType == StringType_SingleQuote || at+1 >= ctx->end) { + goto normalChar; + } else { + stringCtx->index++; + at++; + switch (*at) { + case 'n': + stringCtx->index++; + return '\n'; + default: + stringCtx->index++; + return *at; + } + } + case '"': + if (stringCtx->stringType == StringType_DoubleQuote) { + at++; + stringCtx->index++; + return '\0'; + } else { + goto normalChar; + } + case '\'': + if (stringCtx->stringType == StringType_SingleQuote) { + at++; + stringCtx->index++; + return '\0'; + } else { + goto normalChar; + } + case '\n': + return '\0'; + case '$': + if (stringCtx->stringType == StringType_SingleQuote) { + goto normalChar; + } else { + // new context at $ (e.g. $macroArg.1 blah blah) + parser_context_t ctx2 = { + .macroState = ctx->macroState, + .begin = ctx->begin, + .at = at, + .end = ctx->end, + .nestingLevel = ctx->nestingLevel, + .nestingBound = ctx->nestingLevel, + }; + + ConsumeCommentsAsWhite(false); // a bit of a hack, turn off comments processing + char res = StrRead_consumeExpressionChar(&ctx2, stringCtx); + ConsumeCommentsAsWhite(true); // turn it back on + + if (ctx2.nestingLevel != ctx->nestingLevel) { + Macros_ReportError("Macro template has overflown expression boundary! Undefined behavior coming!", ctx2.at, ctx2.end); + while (ctx2.nestingLevel > ctx->nestingLevel && PopParserContext(&ctx2)) { + } + stringCtx->subIndex += 1; + return '$'; + } + + if (stringCtx->subIndex == 0) { + stringCtx->index += ctx2.at - at; + } + return res; + } + default: + normalChar: + stringCtx->index++; + return *at; + } +} + +// A string can be either a verbatim string (without quotes, with no support for escapes and expansions), +// a raw string (without quotes, with support for escapes and expansions), or a series of string literals. +// Each literal is either a double-quoted string (with support for escapes and expansions), or +// a single-quoted string (with support for only a single-quote-escape but no expansions). +// Literals are concatenated without any interventing characters. + +// For example, the following are all valid strings: +// hello $worldname world => raw string with expansions, so $worldname is expanded. +// "hello"' $worldname '"world" => three literals: double-quoted string, single-quoted string, double-quoted string +// the single-quoted part prevents expansions in that part, so $worldname is not expanded. +// "hello \"$worldname\" world" => double-quoted string with escapes and expansions, so $worldname is expanded, and remains double-quoted due to the escapes. + +// If any of these examples are read as a verbatim string, all the characters will be read +// verbatim (as-is) until the end of the context, including all $ signs and quotes (no expansions, no escapes). + +char StrRead_ConsumeCharOfString(parser_context_t* ctx, string_reader_context_t* stringCtx) +{ + const char* at = ctx->at; + + at += stringCtx->stringOffset; // point to the current literal of the string. + + if (stringCtx->stringType == StringType_Verbatim) { + stringCtx->at = at + stringCtx->index; + char res = StrRead_ConsumeCharInString(ctx, stringCtx); + + if (res == '\0') { + ctx->at += stringCtx->stringOffset + stringCtx->index; + stringCtx->stringOffset += stringCtx->index; + stringCtx->index = 0; + } + return res; + } + + switch (*at) { + case '\'': + stringCtx->stringType = StringType_SingleQuote; + break; + case '"': + stringCtx->stringType = StringType_DoubleQuote; + break; + default: + stringCtx->stringType = StringType_Raw; + break; + } + + if (stringCtx->index == 0 && stringCtx->stringType != StringType_Raw) { + stringCtx->index++; + } + + at += stringCtx->index; + + // (This is correct, we don't want a context pop here.) + if (at >= ctx->end) { + ctx->at = ctx->end; + return '\0'; + } + + stringCtx->at = at; + char res = StrRead_ConsumeCharInString(ctx, stringCtx); + + if (res == '\0' && stringCtx->stringType != StringType_Verbatim) { + return StrRead_tryConsumeAnotherStringLiteral(ctx, stringCtx); + } else { + return res; + } +} + +extern string_segment_t StringRefToSegment(string_ref_t ref); + +static char StrRead_consumeExpressionCharOfString(const macro_variable_t* variable, uint16_t* idx) +{ + // read the nth character of the string variable, where n is the value of *idx. + // If n exceeds the string length, return '\0' and reset *idx to 0. + + string_segment_t str = StringRefToSegment(variable->asStringRef); + uint8_t len = str.end - str.start; + + if (*idx < len) { + char c = str.start[*idx]; + if (c != '\0') { + (*idx)++; + } else { + *idx = 0; + } + return c; + } else { + *idx = 0; + return '\0'; + } +} + +static char StrRead_consumeExpressionChar(parser_context_t* ctx, string_reader_context_t* stringCtx) +{ + char c; + + // TODO: this TRY_EXPAND_TEMPLATE won't be needed if we expand $macroArg:any correctly. + // It will be handled automatically in Macros_ConsumeAnyValue() below. + if (TRY_EXPAND_TEMPLATE(ctx)) { + // Call tree of this never expands or unexpands this context, so we can safely perform a pop after. + // (If there is an expansion, it is handled within a new context copy.) + c = consumeCharOfTemplate(ctx, stringCtx->stringType, &stringCtx->subIndex); + PopParserContext(ctx); + + if (stringCtx->subIndex == 0) { + UnconsumeWhite(ctx); + } + return c; + } else { + macro_variable_t res = Macros_ConsumeAnyValue(ctx); + UnconsumeWhite(ctx); + + switch (res.type) { + case MacroVariableType_Int: + c = consumeExpressionCharOfInt(&res, &stringCtx->subIndex); + break; + case MacroVariableType_Float: + c = consumeExpressionCharOfFloat(&res, &stringCtx->subIndex); + break; + case MacroVariableType_Bool: + c = consumeExpressionCharOfBool(&res, &stringCtx->subIndex); + break; + case MacroVariableType_String: + c = StrRead_consumeExpressionCharOfString(&res, &stringCtx->subIndex); + break; + case MacroVariableType_None: + c = '?'; + break; + default: + Macros_ReportErrorNum("Unrecognized variable type", res.type, ctx->at); + return '\0'; + } + } + + if (Macros_ParserError) { + ctx->at++; + stringCtx->subIndex = 0; + } + return c; +} + + +// existing code:a static char Macros_ConsumeCharInString(parser_context_t* ctx, string_type_t stringType, const char* at, uint16_t* index, uint16_t* subIndex); @@ -148,6 +423,9 @@ static char consumeExpressionCharOfString(const macro_variable_t* variable, uint static char consumeExpressionChar(parser_context_t* ctx, string_type_t stringType, uint16_t* index) { char c; + + // TODO: this TRY_EXPAND_TEMPLATE won't be needed if we expand $macroArg:any correctly. + // It will be handled automatically in Macros_ConsumeAnyValue() below. if (TRY_EXPAND_TEMPLATE(ctx)) { // Call tree of this never expands or unexpands this context, so we can safely perform a pop after. // (If there is an expansion, it is handled within a new context copy.) @@ -331,7 +609,6 @@ char Macros_ConsumeCharOfString(parser_context_t* ctx, uint16_t* stringOffset, u } } - static char Macros_ConsumeCharInString(parser_context_t* ctx, string_type_t stringType, const char* at, uint16_t* index, uint16_t* subIndex) { if (at >= ctx->end) { @@ -407,4 +684,3 @@ static char Macros_ConsumeCharInString(parser_context_t* ctx, string_type_t stri return *at; } } - diff --git a/right/src/macros/string_reader.h b/right/src/macros/string_reader.h index e25c072dc..9d63a364b 100644 --- a/right/src/macros/string_reader.h +++ b/right/src/macros/string_reader.h @@ -19,7 +19,26 @@ // Typedefs: - +typedef enum { + StrReadMode_Verbatim, // do not expand anything in the string. Reads until end of context. + StrReadMode_Literal, // read a string literal, with support for quotes, escapes and $-expansions. +} string_reader_mode_t; + +typedef enum { + StringType_Undetermined = 0, + StringType_Raw, + StringType_DoubleQuote, + StringType_SingleQuote, + StringType_Verbatim, +} string_type_t; + +typedef struct { + const char* at; + uint16_t stringOffset; + uint16_t index; + uint16_t subIndex; + string_type_t stringType; +} string_reader_context_t; // Variables: @@ -30,5 +49,8 @@ bool Macros_CompareStringToken(parser_context_t* ctx, string_segment_t str); void Macros_ConsumeStringToken(parser_context_t* ctx); + void StrRead_InitContext(parser_context_t* ctx, string_reader_context_t* stringCtx, string_reader_mode_t mode); + char StrRead_ConsumeCharOfString(parser_context_t* ctx, string_reader_context_t* stringCtx); + #endif diff --git a/right/src/macros/typedefs.h b/right/src/macros/typedefs.h index 67854c16f..6a493806b 100644 --- a/right/src/macros/typedefs.h +++ b/right/src/macros/typedefs.h @@ -14,7 +14,6 @@ // Typedefs: - typedef enum { MacroSubAction_Tap, MacroSubAction_Press, @@ -24,6 +23,7 @@ } macro_sub_action_t; typedef enum { + MacroResult_None = 0, MacroResult_InProgressFlag = 1, MacroResult_ActionFinishedFlag = 2, MacroResult_DoneFlag = 4, @@ -36,6 +36,7 @@ MacroResult_Waiting = MacroResult_InProgressFlag | MacroResult_YieldFlag, MacroResult_Sleeping = MacroResult_InProgressFlag | MacroResult_YieldFlag, MacroResult_Finished = MacroResult_ActionFinishedFlag, + MacroResult_Header = MacroResult_ActionFinishedFlag | MacroResult_InProgressFlag, MacroResult_JumpedForward = MacroResult_DoneFlag, MacroResult_JumpedBackward = MacroResult_DoneFlag | MacroResult_YieldFlag, } macro_result_t; diff --git a/right/src/macros/vars.c b/right/src/macros/vars.c index 727bf8889..11176e75e 100644 --- a/right/src/macros/vars.c +++ b/right/src/macros/vars.c @@ -50,12 +50,19 @@ typedef enum { macro_variable_t macroVariables[MACRO_VARIABLE_COUNT_MAX]; uint8_t macroVariableCount = 0; +macro_argument_t macroArguments[MACRO_ARGUMENT_POOL_SIZE]; +// uint8_t macroArgumentCount = 0; + static macro_variable_t consumeArgumentAsValue(parser_context_t* ctx); static macro_variable_t consumeParenthessExpression(parser_context_t* ctx); static macro_variable_t consumeValue(parser_context_t* ctx); static macro_variable_t negate(parser_context_t *ctx, macro_variable_t res); static macro_variable_t consumeMinMaxOperation(parser_context_t* ctx, operator_t op); static macro_variable_t negateBool(parser_context_t *ctx, macro_variable_t res); +static macro_variable_t coalesceType(parser_context_t* ctx, macro_variable_t value, macro_variable_type_t dstType); + +static string_ref_t createStringRef(const char *start, const char *end); +static string_segment_t stringRefToSegment(string_ref_t ref); macro_result_t Macros_ProcessStatsVariablesCommand(void) { if (Macros_DryRun) { @@ -77,6 +84,11 @@ static macro_variable_t intVar(int32_t value) return (macro_variable_t) { .asInt = value, .type = MacroVariableType_Int }; } +//static macro_variable_t floatVar(float value) +//{ +// return (macro_variable_t) { .asFloat = value, .type = MacroVariableType_Float }; +//} + static macro_variable_t boolVar(bool value) { return (macro_variable_t) { .asBool = value, .type = MacroVariableType_Bool }; @@ -92,38 +104,103 @@ static macro_variable_t noneVar() return (macro_variable_t) { .asInt = 1, .type = MacroVariableType_None }; } -static macro_variable_t consumeNumericValue(parser_context_t* ctx) +static macro_variable_t consumeNumericValueOfType(parser_context_t* ctx, macro_numericalvalue_type_t expectedType) { macro_variable_t res = { .type = MacroVariableType_Int, .asInt = 0 }; bool numFound = false; - while(*ctx->at > 47 && *ctx->at < 58 && ctx->at < ctx->end) { - res.asInt = res.asInt*10 + ((uint8_t)(*ctx->at))-48; + while(*ctx->at >= '0' && *ctx->at <= '9' && ctx->at < ctx->end) { + res.asInt = res.asInt*10 + ((uint8_t)(*ctx->at))-'0'; ctx->at++; numFound = true; } if (*ctx->at == '.') { + if (expectedType == MacroNumericalValueType_Int) { + Macros_ReportErrorTok(ctx, "Integer value expected but found:"); + return noneVar(); + } res.type = MacroVariableType_Float; res.asFloat = (float) res.asInt; ctx->at++; float b = 0.1; - while(*ctx->at > 47 && *ctx->at < 58 && ctx->at < ctx->end) { - res.asFloat += (((uint8_t)(*ctx->at))-48)*b; + while(*ctx->at >= '0' && *ctx->at <= '9' && ctx->at < ctx->end) { + res.asFloat += (((uint8_t)(*ctx->at))-'0')*b; b = b*0.1f; ctx->at++; numFound = true; } } if (!numFound) { - Macros_ReportErrorTok(ctx, "Numeric value expected"); + Macros_ReportErrorTok(ctx, "Numeric value expected but found:"); return noneVar(); } + if (expectedType == MacroNumericalValueType_Float && res.type == MacroVariableType_Int) { + res.asFloat = (float) res.asInt; + res.type = MacroVariableType_Float; + } + ConsumeWhite(ctx); return res; } +static macro_variable_t consumeIntValue(parser_context_t* ctx) +{ + return consumeNumericValueOfType(ctx, MacroNumericalValueType_Int); +} + +static macro_variable_t consumeFloatValue(parser_context_t* ctx) +{ + return consumeNumericValueOfType(ctx, MacroNumericalValueType_Float); +} + +static macro_variable_t consumeNumericValue(parser_context_t* ctx) +{ + return consumeNumericValueOfType(ctx, MacroNumericalValueType_Any); +} + +static macro_variable_t consumeBool(parser_context_t* ctx) +{ + if (ConsumeToken(ctx, "false")) { + return boolVar(false); + } + else if (ConsumeToken(ctx, "true")) { + return boolVar(true); + } + + Macros_ReportErrorTok(ctx, "Boolean value (true/false) expected but found:"); + return noneVar(); +} + +static macro_variable_t consumeKeyIdValue(parser_context_t* ctx) +{ + uint8_t keyId = MacroKeyIdParser_TryConsumeKeyId(ctx); + if (keyId == 255) { + return consumeIntValue(ctx); + } + return intVar(keyId); +} + +static macro_variable_t consumeScancodeValue(parser_context_t* ctx) +// consume "scancode" = modded scancode = "shortcut", return as string variable. +{ + // this function could preparse the Mods+Scancode for validity, e.g. + // macro_action_t action = decodeKeyAndConsume(ctx, MacroSubAction_None); + // but for now we return the raw string. + const char* atStart = ctx->at; + const char* atEnd = TokEnd(ctx->at, ctx->end); + ConsumeWhiteAt(ctx, atEnd); + + return stringVar(createStringRef(atStart, atEnd)); +} + +static macro_variable_t consumeStringVerbatim(parser_context_t* ctx) +{ + // the remaining context is the string. No expansions. + return stringVar(createStringRef(ctx->at, ctx->end)); +} + static macro_variable_t consumeStringLiteral(parser_context_t* ctx) { const char* stringStart = ctx->at; @@ -136,13 +213,9 @@ static macro_variable_t consumeStringLiteral(parser_context_t* ctx) return noneVar(); } - uint16_t offset = stringStart - (const char*)ValidatedUserConfigBuffer.buffer; - uint8_t len = ctx->at - stringStart; - - return stringVar((string_ref_t){ .offset = offset, .len = len }); + return stringVar(createStringRef(stringStart, ctx->at)); } - macro_variable_t* Macros_ConsumeExistingWritableVariable(parser_context_t* ctx) { if (Macros_DryRun) { @@ -185,7 +258,7 @@ static macro_variable_t consumeVariable(parser_context_t* ctx) } ConsumeAnyIdentifier(ctx); - return (macro_variable_t){}; + return (macro_variable_t){}; // TODO: shouldn't this be noneVar()? } // Expects @@ -198,7 +271,8 @@ static macro_variable_t* consumeVarAndAllocate(parser_context_t* ctx) } } - CTX_COPY(bakCtx, *ctx); + // TODO: Is this needed at all? Looks like something left over + // CTX_COPY(bakCtx, *ctx); macro_variable_t configVal = Macro_TryReadConfigVal(ctx); if (configVal.type != MacroVariableType_None) { @@ -236,6 +310,14 @@ static macro_variable_t* consumeVarAndAllocate(parser_context_t* ctx) return res; } +static macro_variable_t reportUnexpectedVariableType(parser_context_t* ctx, macro_variable_type_t type) +{ + // TODO: would there be any way to trace the current position down the + // context stack and report it here? + Macros_ReportErrorNum("Unexpected variable type:", type, NULL); + return noneVar(); +} + static macro_variable_t coalesceType(parser_context_t* ctx, macro_variable_t value, macro_variable_type_t dstType) { if (value.type == dstType) { @@ -260,8 +342,7 @@ static macro_variable_t coalesceType(parser_context_t* ctx, macro_variable_t val case MacroVariableType_None: break; default: - Macros_ReportErrorNum("Unexpected variable type:", value.type, NULL); - break; + return reportUnexpectedVariableType(ctx, value.type); } break; case MacroVariableType_Float: @@ -281,8 +362,7 @@ static macro_variable_t coalesceType(parser_context_t* ctx, macro_variable_t val case MacroVariableType_None: break; default: - Macros_ReportErrorNum("Unexpected variable type:", value.type, NULL); - break; + return reportUnexpectedVariableType(ctx, value.type); } break; case MacroVariableType_Bool: @@ -302,8 +382,7 @@ static macro_variable_t coalesceType(parser_context_t* ctx, macro_variable_t val case MacroVariableType_None: break; default: - Macros_ReportErrorNum("Unexpected variable type:", value.type, NULL); - break; + return reportUnexpectedVariableType(ctx, value.type); } break; case MacroVariableType_String: @@ -319,15 +398,13 @@ static macro_variable_t coalesceType(parser_context_t* ctx, macro_variable_t val case MacroVariableType_None: break; default: - Macros_ReportErrorNum("Unexpected variable type:", value.type, NULL); - break; + return reportUnexpectedVariableType(ctx, value.type); } break; case MacroVariableType_None: break; default: - Macros_ReportErrorNum("Unexpected variable type:", dstType, NULL); - break; + return reportUnexpectedVariableType(ctx, value.type);; } value.type = dstType; return value; @@ -349,7 +426,7 @@ static macro_variable_t consumeDollarExpression(parser_context_t* ctx) return intVar(Timer_GetCurrentTime() & 0x7FFFFFFF); } else if (ConsumeToken(ctx, "queuedKeyId")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); int8_t queueIdx = Macros_ConsumeInt(ctx); if (queueIdx >= PostponerQuery_PendingKeypressCount()) { if (!Macros_DryRun) { @@ -361,16 +438,16 @@ static macro_variable_t consumeDollarExpression(parser_context_t* ctx) return intVar(PostponerExtended_PendingId(queueIdx)); } else if (ConsumeToken(ctx, "keyId")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); uint8_t keyId = MacroKeyIdParser_TryConsumeKeyId(ctx); if (keyId == 255) { - Macros_ReportErrorTok(ctx, "KeyId abbreviation expected"); + Macros_ReportErrorTok(ctx, "KeyId abbreviation expected:"); return noneVar(); } return intVar(keyId); } else if (ConsumeToken(ctx, "uhk")) { - ConsumeUntilDot(ctx); + ConsumeOneDot(ctx); if (ConsumeToken(ctx, "name")) { return stringVar(Cfg.DeviceName); } else { @@ -390,6 +467,7 @@ static macro_variable_t consumeDollarExpression(parser_context_t* ctx) static macro_variable_t consumeValue(parser_context_t* ctx) { + // TODO: this shouldn't be here, when properly handled by $macroArg :any type. if (*ctx->at == '&') { TryExpandMacroTemplateOnce(ctx); if (Macros_ParserError) { @@ -430,7 +508,6 @@ static macro_variable_t consumeValue(parser_context_t* ctx) else { goto failed; } - case 't': if (ConsumeToken(ctx, "true")) { return (macro_variable_t){ .type = MacroVariableType_Bool, .asBool = true }; @@ -438,7 +515,6 @@ static macro_variable_t consumeValue(parser_context_t* ctx) else { goto failed; } - case '$': ctx->at++; return consumeDollarExpression(ctx); @@ -458,12 +534,16 @@ static macro_variable_t consumeValue(parser_context_t* ctx) } failed: + return consumeStringLiteral(ctx); // try reading as a raw string literal + +#if 0 if (IsIdentifierChar(*ctx->at)) { - Macros_ReportErrorPrintf(ctx->at, "Parsing failed, did you mean '$%s'?", OneWord(ctx)); + Macros_ReportErrorPrintf(ctx->at, "Parsing failed, did you mean '\"%s\"'?", OneWord(ctx)); } else { Macros_ReportErrorTok(ctx, "Could not parse"); } return noneVar(); +#endif } static macro_variable_t negate(parser_context_t *ctx, macro_variable_t res) @@ -912,6 +992,17 @@ bool Macros_ConsumeBool(parser_context_t* ctx) return coalesceType(ctx, res, MacroVariableType_Bool).asBool; } +string_segment_t Macros_ConsumeString(parser_context_t* ctx) +{ + macro_variable_t res = consumeValue(ctx); + if (res.type != MacroVariableType_String) { + // Do not report error directly here, just return a "null" string. + // Macros_ReportError("String value expected but found:", NULL, NULL); + return (string_segment_t){ .start = NULL, .end = NULL }; + } + return stringRefToSegment(res.asStringRef); +} + macro_variable_t Macros_ConsumeAnyValue(parser_context_t *ctx) { return consumeValue(ctx); @@ -928,7 +1019,23 @@ macro_result_t Macros_ProcessSetVarCommand(parser_context_t* ctx) if (dst != NULL) { dst->type = src.type; - dst->asInt = src.asInt; + switch (src.type) { + case MacroVariableType_Int: + dst->asInt = src.asInt; + break; + case MacroVariableType_Float: + dst->asFloat = src.asFloat; + break; + case MacroVariableType_Bool: + dst->asBool = src.asBool; + break; + case MacroVariableType_String: + dst->asStringRef = src.asStringRef; + break; + default: + Macros_ReportErrorNum("Unexpected variable type:", src.type, NULL); + break; + } } return MacroResult_Finished; @@ -1022,32 +1129,117 @@ void MacroVariables_RunTests(void) { } static macro_variable_t consumeArgumentAsValue(parser_context_t* ctx) { - ConsumeUntilDot(ctx); - uint8_t argId = Macros_ConsumeInt(ctx); + uint8_t argIdx; + macro_argument_type_t argType; - if (S->ms.currentMacroArgumentOffset == 0) { - Macros_ReportErrorPrintf(ctx->at, "Failed to retrieve argument %d, because this macro doesn't seem to have arguments assigned!", argId); + if (!ConsumeOneDot(ctx)) { + Macros_ReportErrorPos(ctx, "Expected '.' after '$macroArg'"); + return noneVar(); + }; + + if (!IsDigit(ctx)) { + // argument accessed by name, e.g., $macroArg.my_param + + const char *idStart = ctx->at; + const char *idEnd = IdentifierEnd(ctx); + if (idStart == idEnd) { + Macros_ReportErrorPos(ctx, "Expected identifier after '$macroArg.'"); + return noneVar(); + } + + // parse macro argument name and convert to number; error if not found. + // if found, consume the name and retrieve argument number and argument type. + macro_argument_t *arg = Macros_FindMacroArgumentByName(MACRO_STATE_SLOT(S), idStart, idEnd); + if (arg == NULL) { + Macros_ReportErrorPrintf(ctx->at, "Argument with name '$macroArg.%s' not found!", OneWord(ctx)); + return noneVar(); + } + argIdx = arg->idx; + argType = arg->type; + ConsumeWhiteAt(ctx, idEnd); + } else { + // argument accessed by number, e.g., $macroArg.1 + + argIdx = Macros_ConsumeInt(ctx); + macro_argument_t *arg = Macros_FindMacroArgumentByIndex(MACRO_STATE_SLOT(S), argIdx); + if (arg == NULL) { + // if not found (= undeclared), assume type 'any' for this argument + // (backwards compatibility to macro arguments without macroArg declaration). + argType = MacroArgType_Any; + } else { + argType = arg->type; + } } - string_segment_t str = ParseMacroArgument(S->ms.currentMacroArgumentOffset, argId); + // at this point, we have argument index and argument type. - if (str.start == NULL) { - Macros_ReportErrorPrintf(ctx->at, "Failed to retrieve argument %d. Argument not found!", argId); + if (S->ms.currentMacroArgumentOffset == 0) { + Macros_ReportErrorPrintf(ctx->at, "Failed to retrieve argument %d, because this macro doesn't seem to have arguments assigned!", argIdx); return noneVar(); } - parser_context_t varCtx = (parser_context_t) { - .at = str.start, - .begin = str.start, - .end = str.end, - .macroState = ctx->macroState, - .nestingLevel = ctx->nestingLevel, - .nestingBound = ctx->nestingBound, - }; + string_segment_t str = ParseMacroArgument(S->ms.currentMacroArgumentOffset, argIdx); - macro_variable_t res = consumeValue(&varCtx); + if (str.start == NULL) { + Macros_ReportErrorPrintf(ctx->at, "Failed to retrieve argument %d. Argument not found!", argIdx); + return noneVar(); + } - return res; + if (argType == MacroArgType_Any) { + // for type 'any', consume the value as a template expansion (i.e. like ¯oArg) + // for compatibility with existing macros that don't declare their argument types. + +#if 0 + // TODO: This doesn't work; it will cause firmware crashes. + // I don't understand why. + PushParserContext(ctx, str.start, str.start, str.end); + return consumeValue(ctx); +#else + parser_context_t varCtx = (parser_context_t) { + .at = str.start, + .begin = str.start, + .end = str.end, + .macroState = ctx->macroState, + .nestingLevel = ctx->nestingLevel, + .nestingBound = ctx->nestingBound, + }; + + return consumeValue(&varCtx); +#endif + } else { + // for declared types, consume the value according to type. + parser_context_t varCtx = (parser_context_t) { + .at = str.start, + .begin = str.start, + .end = str.end, + .macroState = ctx->macroState, + .nestingLevel = ctx->nestingLevel, + .nestingBound = ctx->nestingBound, + }; + + switch (argType) { + case MacroArgType_Int: + return consumeIntValue(&varCtx); + case MacroArgType_Float: + return consumeFloatValue(&varCtx); + case MacroArgType_Bool: + return consumeBool(&varCtx); + case MacroArgType_String: + // this used to be consumeStringLiteral, but that leads to $-expansions + // within the string, even if not enclosed in double-quotes. + // Values configured for arguments of type string should be interpreted + // as verbatim strings without expansions. + // Use type 'any' if you want $-expansions in your arguments. + return consumeStringVerbatim(&varCtx); + case MacroArgType_KeyId: + return consumeKeyIdValue(&varCtx); + case MacroArgType_ScanCode: + return consumeScancodeValue(&varCtx); + default: + Macros_ReportErrorNum("Unexpected argument type:", argType, NULL); + return noneVar(); + } + } } static bool expandArgumentInplace(parser_context_t* ctx, uint8_t argNumber) { @@ -1069,17 +1261,160 @@ static bool expandArgumentInplace(parser_context_t* ctx, uint8_t argNumber) { bool TryExpandMacroTemplateOnce(parser_context_t* ctx) { ASSERT(*ctx->at == '&'); + // save context position to restore if the "try" fails + const char *savedAt = ctx->at; + ctx->at++; Trace_Printc("e1"); if (ConsumeToken(ctx, "macroArg")) { - ConsumeUntilDot(ctx); - uint8_t argId = Macros_ConsumeInt(ctx); - expandArgumentInplace(ctx, argId); - return true; + if(ConsumeOneDot(ctx)) { + uint8_t argId = Macros_ConsumeInt(ctx); + expandArgumentInplace(ctx, argId); + return true; + } } - ctx->at--; + // restore parser context if no expansion was performed + ctx->at = savedAt; + return false; } + +// ---------------------------------------- +// macroArguments allocation and processing +// ---------------------------------------- + +// helper functions to convert to and from StringRefs and StringSegments + +static string_ref_t createStringRef(const char *start, const char *end) { + return (string_ref_t) { + .offset = start - (const char*)ValidatedUserConfigBuffer.buffer, + .len = (uint8_t)(end - start), + }; +} + +static string_segment_t stringRefToSegment(string_ref_t ref) { + return (string_segment_t) { + .start = (const char*)(ValidatedUserConfigBuffer.buffer + ref.offset), + .end = (const char*)(ValidatedUserConfigBuffer.buffer + ref.offset + ref.len), + }; +} + +string_segment_t StringRefToSegment(string_ref_t ref) { + return stringRefToSegment(ref); +} + +// currently unused: +//static const char *stringRefStart(string_ref_t ref) { +// return (const char *)(ValidatedUserConfigBuffer.buffer + ref.offset); +//} + +// currently unused: +//static const char *stringRefEnd(string_ref_t ref) { +// return (const char *)(ValidatedUserConfigBuffer.buffer + ref.offset + ref.len); +//} + +// Allocates a macro argument in the pool and returns a reference to it. +// Fails if an argument with the same name already exists for this owner, +// or if the pool limit is exceeded. +// +// Returns: +// MacroArgAllocResult_Success and sets *outArgRef to new allocated argument on success. +// MacroArgAllocResult_DuplicateArgumentName if an argument with the same name already exists for this owner. +// MacroArgAllocResult_PoolLimitExceeded if there are no free slots in the pool. + +macro_argument_alloc_result_t Macros_AllocateMacroArgument( + uint8_t owner, + const char *idStart, + const char *idEnd, + macro_argument_type_t type, + uint8_t argNumber +) { + // search for existing argument of same owner with the same identifier, error if found + if (Macros_FindMacroArgumentByName(owner, idStart, idEnd)) { + return MacroArgAllocResult_DuplicateArgumentName; + } + + // search for an unused slot in the pool + for (uint8_t i = 0; i < MACRO_ARGUMENT_POOL_SIZE; i++) { + if (macroArguments[i].type == MacroArgType_Unused) { + macroArguments[i].owner = owner; + macroArguments[i].type = type; + macroArguments[i].idx = argNumber; + macroArguments[i].name = createStringRef(idStart, idEnd); + return MacroArgAllocResult_Success; + } + } + + return MacroArgAllocResult_PoolLimitExceeded; +} + +// Deallocates all macro arguments for the given owner. Used when a macro ends. + +void Macros_DeallocateMacroArgumentsByOwner(uint8_t owner) { + for (uint8_t i = 0; i < MACRO_ARGUMENT_POOL_SIZE; i++) { + if (macroArguments[i].type != MacroArgType_Unused && macroArguments[i].owner == owner) { + macroArguments[i].type = MacroArgType_Unused; + } + } +} + +// Retrieve the number of arguments allocated for the given owner. + +uint8_t Macros_CountMacroArgumentsByOwner(uint8_t owner) { + uint8_t count = 0; + + for (uint8_t i = 0; i < MACRO_ARGUMENT_POOL_SIZE; i++) { + if (macroArguments[i].type != MacroArgType_Unused && macroArguments[i].owner == owner) { + count++; + } + } + return count; +} + +// Finds a macro argument by name for the given owner. Returns NULL if not found. + +macro_argument_t *Macros_FindMacroArgumentByName(uint8_t owner, const char *nameStart, const char *nameEnd) { + for (uint8_t i = 0; i < MACRO_ARGUMENT_POOL_SIZE; i++) { + if (macroArguments[i].type != MacroArgType_Unused && macroArguments[i].owner == owner && + SegmentEqual(stringRefToSegment(macroArguments[i].name), (string_segment_t){ .start = nameStart, .end = nameEnd })) { + return ¯oArguments[i]; + } + } + return NULL; +} + +// Finds a macro argument index by name for the given owner. Returns 0 if not found. +// Returns the argument index (1-based) if found, or 0 if not found. +// The index is determined by the order of arguments for the same owner in the pool. + +uint8_t Macros_FindMacroArgumentIndexByName(uint8_t owner, const char *nameStart, const char *nameEnd) { + uint8_t idx = 0; // argument index for this owner + + for (uint8_t i = 0; i < MACRO_ARGUMENT_POOL_SIZE; i++) { + if (macroArguments[i].type != MacroArgType_Unused && macroArguments[i].owner == owner) { + idx++; + if (SegmentEqual(stringRefToSegment(macroArguments[i].name), (string_segment_t){ .start = nameStart, .end = nameEnd })) { + return idx; + } + } + } + return 0; // return 0 if not found; argument indices are 1-based +} + +// Finds a macro argument by index for the given owner. + +macro_argument_t *Macros_FindMacroArgumentByIndex(uint8_t owner, uint8_t argIndex) { + uint8_t idx = 0; // argument index for this owner + + for (uint8_t i = 0; i < MACRO_ARGUMENT_POOL_SIZE; i++) { + idx++; + if (macroArguments[i].type != MacroArgType_Unused && macroArguments[i].owner == owner && + idx == argIndex) { + return ¯oArguments[i]; + } + } + return NULL; +} diff --git a/right/src/macros/vars.h b/right/src/macros/vars.h index 2fee6fb7f..f6fd8127a 100644 --- a/right/src/macros/vars.h +++ b/right/src/macros/vars.h @@ -1,7 +1,6 @@ #ifndef __MACROS_VARS_H__ #define __MACROS_VARS_H__ - // Includes: #include @@ -18,14 +17,22 @@ #define MACRO_VARIABLE_COUNT_MAX 32 #define TRY_EXPAND_TEMPLATE(CTX) (*ctx->at == '&' && TryExpandMacroTemplateOnce(CTX)) + #define MACRO_ARGUMENT_POOL_SIZE 32 + // Typedefs: typedef enum { + MacroNumericalValueType_Any = 0, + MacroNumericalValueType_Int, + MacroNumericalValueType_Float, + } macro_numericalvalue_type_t; + + typedef enum { + MacroVariableType_None = 0, MacroVariableType_Int, MacroVariableType_Float, MacroVariableType_Bool, MacroVariableType_String, - MacroVariableType_None, } macro_variable_type_t; typedef struct { @@ -39,6 +46,32 @@ macro_variable_type_t type; } ATTR_PACKED macro_variable_t; + typedef enum { + MacroArgType_Unused = 0, + MacroArgType_Any, + MacroArgType_Int, + MacroArgType_Float, + MacroArgType_Bool, + MacroArgType_String, + MacroArgType_KeyId, + MacroArgType_ScanCode + } macro_argument_type_t; + + typedef struct { + uint8_t owner; // MACRO_STATE_SLOT() of the macro that owns this argument + uint8_t idx; // index of the argument in the macro's argument list (1-based) + // (we could always calculate idx by looping through the pool, + // but returning argument+index separately everywhere becomes + // a nightmare...) + string_ref_t name; // macro argument name (identifier) + macro_argument_type_t type; + } macro_argument_t; + + typedef enum { + MacroArgAllocResult_Success, + MacroArgAllocResult_PoolLimitExceeded, + MacroArgAllocResult_DuplicateArgumentName, + } macro_argument_alloc_result_t; // Variables: @@ -51,10 +84,18 @@ int32_t Macros_ConsumeInt(parser_context_t* ctx); float Macros_ConsumeFloat(parser_context_t* ctx); bool Macros_ConsumeBool(parser_context_t* ctx); + string_segment_t Macros_ConsumeString(parser_context_t* ctx); macro_variable_t Macros_ConsumeAnyValue(parser_context_t* ctx); void MacroVariables_RunTests(void); void Macros_SerializeVar(char* buffer, uint8_t len, macro_variable_t var); bool TryExpandMacroTemplateOnce(parser_context_t* ctx); -#endif + string_segment_t StringRefToSegment(string_ref_t ref); + macro_argument_alloc_result_t Macros_AllocateMacroArgument(uint8_t owner, const char *idStart, const char *idEnd, macro_argument_type_t type, uint8_t argNumber); + void Macros_DeallocateMacroArgumentsByOwner(uint8_t owner); + uint8_t Macros_CountMacroArgumentsByOwner(uint8_t owner); + macro_argument_t *Macros_FindMacroArgumentByName(uint8_t owner, const char *nameStart, const char *nameEnd); + uint8_t Macros_FindMacroArgumentIndexByName(uint8_t owner, const char *nameStart, const char *nameEnd); + macro_argument_t *Macros_FindMacroArgumentByIndex(uint8_t owner, uint8_t argIndex); +#endif diff --git a/right/src/segment_display.c b/right/src/segment_display.c index 0cf13e2a6..b92bc60ba 100644 --- a/right/src/segment_display.c +++ b/right/src/segment_display.c @@ -124,12 +124,30 @@ void SegmentDisplay_SerializeVar(char* buffer, macro_variable_t var) case MacroVariableType_Bool: SegmentDisplay_SerializeInt(buffer, var.asBool); break; + case MacroVariableType_String: + SegmentDisplay_SerializeString(buffer, var.asStringRef); + break; default: Macros_ReportErrorNum("Unexpected variable type:", var.type, NULL); break; } } +void SegmentDisplay_SerializeString(char* buffer, string_ref_t strRef) +{ + RETURN_IF_SEGMENT_NOT_PRESENT; + string_segment_t strSeg = StringRefToSegment(strRef); + + // copy the first 3 chars of the string, pad with spaces if shorter. + for (uint8_t i = 0; i < 3; i++) { + if (i < strRef.len) { + buffer[i] = strSeg.start[i]; + } else { + buffer[i] = ' '; + } + } +} + void SegmentDisplay_SerializeFloat(char* buffer, float num) { RETURN_IF_SEGMENT_NOT_PRESENT; diff --git a/right/src/segment_display.h b/right/src/segment_display.h index 54e128502..0a7fdf8af 100644 --- a/right/src/segment_display.h +++ b/right/src/segment_display.h @@ -34,6 +34,7 @@ void SegmentDisplay_SetText(uint8_t len, const char* text, segment_display_slot_t slot); void SegmentDisplay_SetInt(int32_t a, segment_display_slot_t slot); void SegmentDisplay_SetFloat(float a, segment_display_slot_t slot); + void SegmentDisplay_SerializeString(char* buffer, string_ref_t strRef); void SegmentDisplay_SerializeInt(char* buffer, int32_t a); void SegmentDisplay_SerializeFloat(char* buffer, float f); void SegmentDisplay_SerializeVar(char* buffer, macro_variable_t var); diff --git a/right/src/str_utils.c b/right/src/str_utils.c index 93ccf9d36..09c0022c2 100644 --- a/right/src/str_utils.c +++ b/right/src/str_utils.c @@ -13,7 +13,7 @@ #define MIN(a, b) ((a) < (b) ? (a) : (b)) #endif -ATTR_UNUSED static parser_context_t parserContextStack[PARSER_CONTEXT_STACK_SIZE]; +static parser_context_t parserContextStack[PARSER_CONTEXT_STACK_SIZE]; static bool consumeCommentsAsWhite = true; @@ -124,7 +124,7 @@ static bool isEnd(parser_context_t* ctx) { return false; } while (ctx->nestingLevel > 0 && ctx->at >= ctx->end && PopParserContext(ctx)) { - /* everything was don in PopParserContext */ + /* everything was done in PopParserContext */ }; return ctx->at >= ctx->end; } @@ -133,17 +133,46 @@ bool IsEnd(parser_context_t* ctx) { return isEnd(ctx); } +static bool isDigit(parser_context_t* ctx) { + return *ctx->at >= '0' && *ctx->at <= '9'; +} + +bool IsDigit(parser_context_t* ctx) { + return isDigit(ctx); +} + +static bool isCommentLeader(parser_context_t* ctx) { + return ctx->at + 1 < ctx->end && ctx->at[0] == '/' && ctx->at[1] == '/'; +} + +static bool isWhite(parser_context_t* ctx) { + if (*ctx->at <= 32) { + return true; + } + if (isCommentLeader(ctx)) { + return true; + } + return false; +} + +bool IsWhite(parser_context_t* ctx) { + return isWhite(ctx); +} + static void consumeWhite(parser_context_t* ctx) { while (!isEnd(ctx)) { - while (*ctx->at <= 32 && !isEnd(ctx)) { + while (!isEnd(ctx) && *ctx->at <= 32) { ctx->at++; } - if (ctx->at[0] == '/' && ctx->at[1] == '/' && consumeCommentsAsWhite) { - while (*ctx->at != '\n' && !isEnd(ctx)) { + if (consumeCommentsAsWhite && isCommentLeader(ctx)) { + while (!isEnd(ctx) && *ctx->at != '\n') { ctx->at++; } } + // TODO: this TRY_EXPAND_TEMPLATE needs to be replaced with expansion of $macroArg:any here. + // Note: possible command injection vulnerability if we allow template expansion in white space. + // Do we want to allow this at all? if (TRY_EXPAND_TEMPLATE(ctx)) { continue; } else { @@ -152,7 +181,6 @@ static void consumeWhite(parser_context_t* ctx) } } - void ConsumeCommentsAsWhite(bool consume) { consumeCommentsAsWhite = consume; @@ -176,6 +204,7 @@ void UnconsumeWhite(parser_context_t* ctx) } } +// dangerous due to static return buffer; only use for error messages! const char* OneWord(parser_context_t* ctx) { static char buffer[20]; @@ -189,12 +218,14 @@ const char* ConsumedToken(parser_context_t* ctx) { const char* at = ctx->at; - at--; + if(at > ctx->begin) { + at--; + } - while (*at <= 32) { + while (at > ctx->begin && *at <= 32) { at--; } - while (*at > 32 && *at != '.') { + while (at > ctx->begin && *at > 32 && *at != '.') { at--; } @@ -205,8 +236,8 @@ bool ConsumeToken(parser_context_t* ctx, const char *b) { const char* at = ctx->at; while(at < ctx->end && *b) { - if (*at <= 32 || at == ctx->end || *b <= 32) { - bool res = (*at <= 32 || at == ctx->end) && *b <= 32; + if (at >= ctx->end || *at <= 32 || *b <= 32) { + bool res = (at >= ctx->end || *at <= 32) && *b <= 32; if (res) { ctx->at = at; consumeWhite(ctx); @@ -217,7 +248,7 @@ bool ConsumeToken(parser_context_t* ctx, const char *b) return false; } } - bool res = (*at <= 32 || at == ctx->end || !isIdentifierChar(*at) || !isIdentifierChar(*(at-1))) && *b <= 32; + bool res = (at >= ctx->end || *at <= 32 || !isIdentifierChar(*at) || !isIdentifierChar(*(at-1))) && *b <= 32; if (res) { ctx->at = at; consumeWhite(ctx); @@ -238,8 +269,8 @@ bool ConsumeTokenByRef(parser_context_t* ctx, string_ref_t ref) const char* b = (const char*)(ValidatedUserConfigBuffer.buffer + ref.offset); const char* bEnd = (const char*)(ValidatedUserConfigBuffer.buffer + ref.offset + ref.len); while(at < ctx->end && b < bEnd) { - if (*at <= 32 || at == ctx->end || *b <= 32 || b == bEnd) { - bool res = (*at <= 32 || at == ctx->end) && *b <= 32; + if (at >= ctx->end || *at <= 32 || b >= bEnd || *b <= 32) { + bool res = (at >= ctx->end || *at <= 32) && *b <= 32; if (res) { ctx->at = at; consumeWhite(ctx); @@ -250,7 +281,7 @@ bool ConsumeTokenByRef(parser_context_t* ctx, string_ref_t ref) return false; } } - bool res = (*at <= 32 || at == ctx->end || *at == '.') && (*b <= 32 || b == bEnd); + bool res = (at >= ctx->end || *at <= 32 || *at == '.') && (b >= bEnd || *b <= 32); if (res) { ctx->at = at; consumeWhite(ctx); @@ -271,21 +302,19 @@ static bool isIdentifierChar(char c) } } - bool IsIdentifierChar(char c) { return isIdentifierChar(c); } - bool ConsumeIdentifierByRef(parser_context_t* ctx, string_ref_t ref) { const char* at = ctx->at; const char* b = (const char*)(ValidatedUserConfigBuffer.buffer + ref.offset); const char* bEnd = (const char*)(ValidatedUserConfigBuffer.buffer + ref.offset + ref.len); while(at < ctx->end && b < bEnd) { - if (!isIdentifierChar(*at) || at == ctx->end || !isIdentifierChar(*b) || b == bEnd) { - bool res = (!isIdentifierChar(*at) || at == ctx->end) && (!isIdentifierChar(*b) || b == bEnd); + if (at >= ctx->end || !isIdentifierChar(*at) || b >= bEnd || !isIdentifierChar(*b)) { + bool res = (at >= ctx->end || !isIdentifierChar(*at)) && (b >= bEnd || !isIdentifierChar(*b)); if (res) { ctx->at = at; consumeWhite(ctx); @@ -297,7 +326,7 @@ bool ConsumeIdentifierByRef(parser_context_t* ctx, string_ref_t ref) } } - bool res = (!isIdentifierChar(*at) || at == ctx->end) && (!isIdentifierChar(*b) || b == bEnd); + bool res = (at >= ctx->end || !isIdentifierChar(*at)) && (b >= bEnd || !isIdentifierChar(*b)); if (res) { ctx->at = at; consumeWhite(ctx); @@ -308,7 +337,7 @@ bool ConsumeIdentifierByRef(parser_context_t* ctx, string_ref_t ref) const char* IdentifierEnd(parser_context_t* ctx) { const char* at = ctx->at; - while (isIdentifierChar(*at) && at < ctx->end) { + while (at < ctx->end && isIdentifierChar(*at)) { at++; } return at; @@ -320,22 +349,60 @@ void ConsumeAnyIdentifier(parser_context_t* ctx) consumeWhite(ctx); } -void ConsumeUntilDot(parser_context_t* ctx) +#if 0 +// Consume characters until a specific character is found or whitespace is hit. +// If end of context is reached, report an error. +// If the character is found, consume it. +// If whitespace is found, and failOnWhite is true, report an error. +void consumeUntilCharOrWhite(parser_context_t* ctx, char c, bool failOnWhite) { - while(*ctx->at > 32 && *ctx->at != '.' && !isEnd(ctx)) { + while(!isEnd(ctx) && *ctx->at > 32 && *ctx->at != c) { ctx->at++; } - if (*ctx->at != '.') { - Macros_ReportError("'.' expected", ctx->at, ctx->at); + if (IsEnd(ctx)) { + Macros_ReportError("unexpected end of statement", ctx->at, ctx->at); + return; } - ctx->at++; + if (*ctx->at == c) { + ctx->at++; + return; + } + if (failOnWhite) { + Macros_ReportErrorPrintf(ctx->at, "'%c' expected", c); + return; + } +} + +void ConsumeUntilDot(parser_context_t* ctx) +{ + consumeUntilCharOrWhite(ctx, '.', true); +} +#endif + +// will consume exactly one character. +// will not consume whitespace after the character. +// returns true if the character was found, false otherwise. +bool ConsumeOneChar(parser_context_t* ctx, char c) +{ + if (!isEnd(ctx) && *ctx->at == c) { + ctx->at++; + return true; + } + return false; +} + +// will not consume whitespace after the dot. +// returns true if the dot was found, false otherwise. +bool ConsumeOneDot(parser_context_t* ctx) +{ + return ConsumeOneChar(ctx, '.'); } bool TokenMatches(const char *a, const char *aEnd, const char *b) { while(a < aEnd && *b) { - if (*a <= 32 || a == aEnd || *b <= 32) { - return (*a <= 32 || a == aEnd) && *b <= 32; + if (a >= aEnd || *a <= 32 || *b <= 32) { + return (a >= aEnd || *a <= 32) && *b <= 32; } if (*a++ != *b++) { return false; @@ -347,20 +414,20 @@ bool TokenMatches(const char *a, const char *aEnd, const char *b) bool TokenMatches2(const char *a, const char *aEnd, const char *b, const char *bEnd) { while(a < aEnd && b < bEnd) { - if (*a <= 32 || a == aEnd || *b <= 32 || b == bEnd) { - return (*a <= 32 || a == aEnd) && *b <= 32; + if (a >= aEnd || *a <= 32 || b >= bEnd || *b <= 32) { + return (a >= aEnd || *a <= 32) && *b <= 32; } if (*a++ != *b++) { return false; } } - return (*a <= 32 || a == aEnd || *a == '.') && (*b <= 32 || b == bEnd); + return (a >= aEnd || *a <= 32 || *a == '.') && (b >= bEnd || *b <= 32); } uint8_t TokLen(const char *a, const char *aEnd) { uint8_t l = 0; - while(*a > 32 && a < aEnd) { + while(a < aEnd && *a > 32) { l++; a++; } @@ -369,7 +436,7 @@ uint8_t TokLen(const char *a, const char *aEnd) const char* TokEnd(const char* cmd, const char *cmdEnd) { - while(*cmd > 32 && cmd < cmdEnd) { + while(cmd < cmdEnd && *cmd > 32) { cmd++; } return cmd; @@ -378,10 +445,10 @@ const char* TokEnd(const char* cmd, const char *cmdEnd) // This doesn't handle expansions. Don't use it in actual macro context. const char* NextTok(const char* cmd, const char *cmdEnd) { - while(*cmd > 32 && cmd < cmdEnd) { + while(cmd < cmdEnd && *cmd > 32) { cmd++; } - while(*cmd <= 32 && cmd < cmdEnd) { + while(cmd < cmdEnd && *cmd <= 32) { cmd++; } if (cmd < cmdEnd - 1 && cmd[0] == '/' && cmd[1] == '/') { @@ -392,7 +459,7 @@ const char* NextTok(const char* cmd, const char *cmdEnd) void ConsumeAnyToken(parser_context_t* ctx) { - while (*ctx->at > 32 && ctx->at < ctx->end) { + while (ctx->at < ctx->end && *ctx->at > 32) { ctx->at++; } consumeWhite(ctx); @@ -403,12 +470,12 @@ struct command_entry* ConsumeGperfToken(parser_context_t* ctx) const char* start = ctx->at; // parse an identifier token - while (isIdentifierChar(*ctx->at) && ctx->at < ctx->end) { + while (ctx->at < ctx->end && isIdentifierChar(*ctx->at)) { ctx->at++; } // parse a single char operator if token wasn't matched. - if (ctx->at == start && !isIdentifierChar(*ctx->at) && *ctx->at > 32 && ctx->at < ctx->end) { + if (ctx->at < ctx->end && ctx->at == start && !isIdentifierChar(*ctx->at) && *ctx->at > 32) { ctx->at++; } @@ -419,11 +486,11 @@ struct command_entry* ConsumeGperfToken(parser_context_t* ctx) const char* NextCmd(const char* cmd, const char *cmdEnd) { - while(*cmd != '\n' && cmd < cmdEnd) { + while(cmd < cmdEnd && *cmd != '\n') { cmd++; } const char* lastNewline = cmd; - while(*cmd <= 32 && cmd < cmdEnd) { + while(cmd < cmdEnd && *cmd <= 32) { if (*cmd == '\n') { lastNewline = cmd; } @@ -439,7 +506,7 @@ const char* NextCmd(const char* cmd, const char *cmdEnd) const char* CmdEnd(const char* cmd, const char *cmdEnd) { - while(*cmd != '\n' && cmd < cmdEnd) { + while(cmd < cmdEnd && *cmd != '\n') { cmd++; } return cmd; @@ -447,7 +514,7 @@ const char* CmdEnd(const char* cmd, const char *cmdEnd) const char* SkipWhite(const char* cmd, const char *cmdEnd) { - while(*cmd <= 32 && cmd < cmdEnd) { + while(cmd < cmdEnd && *cmd <= 32) { cmd++; } return cmd; @@ -602,7 +669,7 @@ uint8_t CountCommands(const char* text, uint16_t textLen) uint8_t count = 1; const char* textEnd = text + textLen; - while ( *text <= 32 && text < textEnd) { + while (text < textEnd && *text <= 32) { text++; } @@ -690,4 +757,3 @@ const char* DeviceModelName(device_id_t device) { return "Unknown device"; } } - diff --git a/right/src/str_utils.h b/right/src/str_utils.h index 0341d18da..7da321c42 100644 --- a/right/src/str_utils.h +++ b/right/src/str_utils.h @@ -30,7 +30,6 @@ // Typedefs: - typedef struct macro_state_t macro_state_t; typedef struct { @@ -39,7 +38,7 @@ const char* at; const char* end; uint8_t nestingLevel; - uint8_t nestingBound; // This context can't be popped bellow this bound, because it is a copy. + uint8_t nestingBound; // This context can't be popped below this bound, because it is a copy. } parser_context_t; typedef struct { @@ -57,6 +56,8 @@ uint8_t SegmentLen(string_segment_t str); bool IsEnd(parser_context_t* ctx); + bool IsDigit(parser_context_t* ctx); + bool IsWhite(parser_context_t* ctx); bool SegmentEqual(string_segment_t str1, string_segment_t str2); bool StrLessOrEqual(const char* a, const char* aEnd, const char* b, const char* bEnd); bool StrEqual(const char* a, const char* aEnd, const char* b, const char* bEnd); @@ -82,6 +83,8 @@ const char* NextTok(const char* cmd, const char *cmdEnd); const char* NextCmd(const char* cmd, const char *cmdEnd); const char* CmdEnd(const char* cmd, const char *cmdEnd); + bool ConsumeOneChar(parser_context_t* ctx, char c); + bool ConsumeOneDot(parser_context_t* ctx); void ConsumeUntilDot(parser_context_t* ctx); void ConsumeWhiteAt(parser_context_t* ctx, const char* at); const char* SkipWhite(const char* cmd, const char *cmdEnd);