diff --git a/companion/lib/Controls/ActionRunner.ts b/companion/lib/Controls/ActionRunner.ts index 3832592569..a9cd040312 100644 --- a/companion/lib/Controls/ActionRunner.ts +++ b/companion/lib/Controls/ActionRunner.ts @@ -5,6 +5,7 @@ import type { ControlEntityInstance } from './Entities/EntityInstance.js' import LogController from '../Log/Controller.js' import type { InternalController } from '../Internal/Controller.js' import type { InstanceController } from '../Instance/Controller.js' +import type { VariableValue } from '@companion-app/shared/Model/Variables.js' /** * Class to handle execution of actions. @@ -33,24 +34,26 @@ export class ActionRunner { } /** - * Run a single action + * Run a single action and return its result */ - async #runAction(action: ControlEntityInstance, extras: RunActionExtras): Promise { + async #runAction(action: ControlEntityInstance, extras: RunActionExtras): Promise { this.#logger.silly('Running action', action) if (action.connectionId === 'internal') { await this.#internalModule.executeAction(action, extras) - } else { - const instance = this.#instanceController.processManager.getConnectionChild(action.connectionId) - if (instance) { - const entityModel = action.asEntityModel(false) - if (entityModel.type !== EntityModelType.Action) - throw new Error(`Cannot execute entity of type "${entityModel.type}" as an action`) - await instance.actionRun(entityModel, extras) - } else { - this.#logger.silly('trying to run action on a missing instance.', action) - } + return undefined } + + const instance = this.#instanceController.processManager.getConnectionChild(action.connectionId) + if (instance) { + const entityModel = action.asEntityModel(false) + if (entityModel.type !== EntityModelType.Action) + throw new Error(`Cannot execute entity of type "${entityModel.type}" as an action`) + return instance.actionRun(entityModel, extras) + } + + this.#logger.silly('trying to run action on a missing instance.', action) + return undefined } /** @@ -60,25 +63,28 @@ export class ActionRunner { actions0: ControlEntityInstance[], extras: RunActionExtras, executeSequential = false - ): Promise { + ): Promise { const actions = actions0.filter((act) => act.type === EntityModelType.Action && !act.disabled) - if (actions.length === 0) return + if (actions.length === 0) return undefined - if (extras.abortDelayed.aborted) return + if (extras.abortDelayed.aborted) return undefined if (executeSequential) { // Future: abort on error? for (const action of actions) { if (extras.abortDelayed.aborted) break - await this.#runAction(action, extras).catch((e) => { + extras.previousResult = await this.#runAction(action, extras).catch((e) => { this.#logger.silly(`Error executing action for ${action.connectionId}: ${e.message ?? e}`) + return undefined }) } + + return extras.previousResult } else { const groupedActions = this.#splitActionsAroundWaits(actions) - const ps: Promise[] = [] + const ps: Promise[] = [] for (const { waitAction, actions } of groupedActions) { if (extras.abortDelayed.aborted) break @@ -97,6 +103,7 @@ export class ActionRunner { ps.push( this.#runAction(action, extras).catch((e) => { this.#logger.silly(`Error executing action for ${action.connectionId}: ${e.message ?? e}`) + return undefined }) ) } @@ -104,6 +111,7 @@ export class ActionRunner { // Await all the actions, so that the abort signal is respected and the promise is pending until all actions are done await Promise.all(ps) + return undefined } } @@ -155,7 +163,7 @@ export class ControlActionRunner { async runActions( actions: ControlEntityInstance[], extras: Omit - ): Promise { + ): Promise { const controller = new AbortController() const chainId = nanoid() diff --git a/companion/lib/Controls/ControlStore.ts b/companion/lib/Controls/ControlStore.ts index 1804c9b143..b1b8b26e87 100644 --- a/companion/lib/Controls/ControlStore.ts +++ b/companion/lib/Controls/ControlStore.ts @@ -1,7 +1,7 @@ import { TriggerEvents } from './TriggerEvents.js' import type { IControlStore } from './IControlStore.js' import type { SomeControl } from './IControlFragments.js' -import type { VariableValues } from '@companion-app/shared/Model/Variables.js' +import type { VariableValue, VariableValues } from '@companion-app/shared/Model/Variables.js' import type { VariablesAndExpressionParser } from '../Variables/VariablesAndExpressionParser.js' import type { NewFeedbackValue } from './Entities/Types.js' import type { VariablesValues } from '../Variables/Values.js' @@ -166,15 +166,16 @@ export class ControlStore implements IControlStore { createVariablesAndExpressionParser( controlId: string | null | undefined, - overrideVariableValues: VariableValues | null + overrideVariableValues: VariableValues | null, + previousResult: VariableValue ): VariablesAndExpressionParser { const control = controlId && this.getControl(controlId) // If the control exists and supports entities, use its parser for local variables if (control && control.supportsEntities) - return control.entities.createVariablesAndExpressionParser(overrideVariableValues) + return control.entities.createVariablesAndExpressionParser(overrideVariableValues, previousResult) // Otherwise create a generic one - return this.#variablesValues.createVariablesAndExpressionParser(null, null, overrideVariableValues) + return this.#variablesValues.createVariablesAndExpressionParser(null, null, overrideVariableValues, previousResult) } } diff --git a/companion/lib/Controls/ControlTypes/Button/Base.ts b/companion/lib/Controls/ControlTypes/Button/Base.ts index 1172bbcf5c..7ecaacaf8e 100644 --- a/companion/lib/Controls/ControlTypes/Button/Base.ts +++ b/companion/lib/Controls/ControlTypes/Button/Base.ts @@ -87,7 +87,8 @@ export abstract class ButtonControlBase { this.logger.error(`action execution failed: ${e}`) @@ -360,6 +362,7 @@ export abstract class ButtonControlBase { this.logger.error(`action execution failed: ${e}`) diff --git a/companion/lib/Controls/ControlTypes/Button/Preset.ts b/companion/lib/Controls/ControlTypes/Button/Preset.ts index 798a678733..a3beaec207 100644 --- a/companion/lib/Controls/ControlTypes/Button/Preset.ts +++ b/companion/lib/Controls/ControlTypes/Button/Preset.ts @@ -125,7 +125,8 @@ export class ControlButtonPreset .createVariablesAndExpressionParser( deps.pageStore.getLocationOfControlId(this.controlId), null, // This doesn't support local variables - injectedVariableValues ?? null + injectedVariableValues ?? null, + undefined ) .executeExpression(expression, requiredType) ) diff --git a/companion/lib/Controls/ControlTypes/Button/Util.ts b/companion/lib/Controls/ControlTypes/Button/Util.ts index c7b6e88f0c..3b6b04cb37 100644 --- a/companion/lib/Controls/ControlTypes/Button/Util.ts +++ b/companion/lib/Controls/ControlTypes/Button/Util.ts @@ -25,7 +25,8 @@ export function parseVariablesInButtonStyle( const parser = deps.variableValues.createVariablesAndExpressionParser( location, entities.getLocalVariableEntities(), - overrideVariableValues + overrideVariableValues, + undefined ) if (style.textExpression) { diff --git a/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts b/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts index 95ae6290e5..5ef1ada1cc 100644 --- a/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts +++ b/companion/lib/Controls/ControlTypes/Triggers/Trigger.ts @@ -249,6 +249,7 @@ export class ControlTrigger .runActions(actions, { surfaceId: this.controlId, location: undefined, + previousResult: undefined, }) .catch((e) => { this.logger.error(`Failed to run actions: ${e.message}`) diff --git a/companion/lib/Controls/Controller.ts b/companion/lib/Controls/Controller.ts index 4ee7ae5aee..c837aa0f86 100644 --- a/companion/lib/Controls/Controller.ts +++ b/companion/lib/Controls/Controller.ts @@ -30,7 +30,7 @@ import { createActionSetsTrpcRouter } from './ActionSetsTrpcRouter.js' import { createControlsTrpcRouter } from './ControlsTrpcRouter.js' import z from 'zod' import type { SomeControlModel, UIControlUpdate } from '@companion-app/shared/Model/Controls.js' -import type { VariableValues } from '@companion-app/shared/Model/Variables.js' +import type { VariableValue, VariableValues } from '@companion-app/shared/Model/Variables.js' import type { VariablesAndExpressionParser } from '../Variables/VariablesAndExpressionParser.js' import { ControlExpressionVariable } from './ControlTypes/ExpressionVariable.js' import type { @@ -662,8 +662,9 @@ export class ControlsController { createVariablesAndExpressionParser( controlId: string | null | undefined, - overrideVariableValues: VariableValues | null + overrideVariableValues: VariableValues | null, + previousResult: VariableValue ): VariablesAndExpressionParser { - return this.#store.createVariablesAndExpressionParser(controlId, overrideVariableValues) + return this.#store.createVariablesAndExpressionParser(controlId, overrideVariableValues, previousResult) } } diff --git a/companion/lib/Controls/Entities/EntityIsInvertedManager.ts b/companion/lib/Controls/Entities/EntityIsInvertedManager.ts index f832c9013d..b9ffed2b68 100644 --- a/companion/lib/Controls/Entities/EntityIsInvertedManager.ts +++ b/companion/lib/Controls/Entities/EntityIsInvertedManager.ts @@ -3,7 +3,7 @@ import type { ControlEntityInstance } from '../../Controls/Entities/EntityInstan import LogController, { type Logger } from '../../Log/Controller.js' import type { NewIsInvertedValue } from './Types.js' import { isExpressionOrValue } from '@companion-app/shared/Model/Options.js' -import type { VariableValues } from '@companion-app/shared/Model/Variables.js' +import type { VariableValue, VariableValues } from '@companion-app/shared/Model/Variables.js' import type { VariablesAndExpressionParser } from '../../Variables/VariablesAndExpressionParser.js' interface EntityWrapper { @@ -16,7 +16,8 @@ interface EntityWrapper { export type UpdateIsInvertedValuesFn = (newValues: ReadonlyMap) => void export type CreateVariablesAndExpressionParser = ( - overrideVariableValues: VariableValues | null + overrideVariableValues: VariableValues | null, + previousResult: VariableValue ) => VariablesAndExpressionParser /** @@ -52,7 +53,7 @@ export class EntityPoolIsInvertedManager { const updatedValues = new Map() - const parser = this.#createVariablesAndExpressionParser(null) + const parser = this.#createVariablesAndExpressionParser(null, undefined) for (const [entityId, wrapper] of this.#entities) { // Resolve the entity, and make sure it still exists diff --git a/companion/lib/Controls/Entities/EntityListPoolBase.ts b/companion/lib/Controls/Entities/EntityListPoolBase.ts index e73217f3da..9cf9dcc3e2 100644 --- a/companion/lib/Controls/Entities/EntityListPoolBase.ts +++ b/companion/lib/Controls/Entities/EntityListPoolBase.ts @@ -13,7 +13,7 @@ import type { InternalController } from '../../Internal/Controller.js' import isEqual from 'fast-deep-equal' import type { InstanceDefinitionsForEntity, NewFeedbackValue, NewIsInvertedValue } from './Types.js' import type { ButtonStyleProperties } from '@companion-app/shared/Model/StyleModel.js' -import type { VariableValues } from '@companion-app/shared/Model/Variables.js' +import type { VariableValue, VariableValues } from '@companion-app/shared/Model/Variables.js' import debounceFn from 'debounce-fn' import type { VariablesValues } from '../../Variables/Values.js' import { isLabelValid } from '@companion-app/shared/Label.js' @@ -159,14 +159,18 @@ export abstract class ControlEntityListPoolBase { if (changed) this.invalidateControl() } - createVariablesAndExpressionParser(overrideVariableValues: VariableValues | null): VariablesAndExpressionParser { + createVariablesAndExpressionParser( + overrideVariableValues: VariableValues | null, + previousResult: VariableValue + ): VariablesAndExpressionParser { const controlLocation = this.#pageStore.getLocationOfControlId(this.controlId) const variableEntities = this.getLocalVariableEntities() return this.#variableValues.createVariablesAndExpressionParser( controlLocation, variableEntities, - overrideVariableValues + overrideVariableValues, + previousResult ) } diff --git a/companion/lib/Controls/IControlStore.ts b/companion/lib/Controls/IControlStore.ts index 309438b48b..4f1253800a 100644 --- a/companion/lib/Controls/IControlStore.ts +++ b/companion/lib/Controls/IControlStore.ts @@ -1,4 +1,4 @@ -import type { VariableValues } from '@companion-app/shared/Model/Variables.js' +import type { VariableValue, VariableValues } from '@companion-app/shared/Model/Variables.js' import type { SomeControl } from './IControlFragments.js' import type { VariablesAndExpressionParser } from '../Variables/VariablesAndExpressionParser.js' import type { NewFeedbackValue } from './Entities/Types.js' @@ -47,7 +47,8 @@ export interface IControlStore { createVariablesAndExpressionParser( controlId: string | null | undefined, - overrideVariableValues: VariableValues | null + overrideVariableValues: VariableValues | null, + previousResult: VariableValue ): VariablesAndExpressionParser /** diff --git a/companion/lib/ImportExport/Backups.ts b/companion/lib/ImportExport/Backups.ts index e35b5a544b..72ba9833b7 100644 --- a/companion/lib/ImportExport/Backups.ts +++ b/companion/lib/ImportExport/Backups.ts @@ -383,7 +383,7 @@ export class BackupController { await fs.mkdir(backupDir, { recursive: true }) // Generate backup filename - const parser = this.#variableValuesController.createVariablesAndExpressionParser(null, null, null) + const parser = this.#variableValuesController.createVariablesAndExpressionParser(null, null, null, undefined) const backupName = parser.parseVariables(rule.backupNamePattern).text if (!backupName) { logger.info('No backup name generated, skipping backup') diff --git a/companion/lib/ImportExport/Export.ts b/companion/lib/ImportExport/Export.ts index 5068b18b28..e746ba6fd3 100644 --- a/companion/lib/ImportExport/Export.ts +++ b/companion/lib/ImportExport/Export.ts @@ -310,7 +310,7 @@ export class ExportController { #generateFilename(filename: string, exportType: string, fileExt: string): string { //If the user isn't using their default file name, don't append any extra info in file name since it was a manual choice const useDefault = filename == this.#userConfigController.getKey('default_export_filename') - const parser = this.#variablesController.values.createVariablesAndExpressionParser(null, null, null) + const parser = this.#variablesController.values.createVariablesAndExpressionParser(null, null, null, undefined) const parsedName = parser.parseVariables(filename).text return parsedName && parsedName !== 'undefined' diff --git a/companion/lib/Instance/Connection/ChildHandlerApi.ts b/companion/lib/Instance/Connection/ChildHandlerApi.ts index dd181b2a95..746cf135b5 100644 --- a/companion/lib/Instance/Connection/ChildHandlerApi.ts +++ b/companion/lib/Instance/Connection/ChildHandlerApi.ts @@ -13,6 +13,7 @@ import type { ActionEntityModel, SomeEntityModel } from '@companion-app/shared/M import type { ControlEntityInstance } from '../../Controls/Entities/EntityInstance.js' import type { ExpressionableOptionsObject, SomeCompanionInputField } from '@companion-app/shared/Model/Options.js' import type { ChildProcessHandlerBase } from '../ProcessManager.js' +import type { VariableValue } from '@companion-app/shared/Model/Variables.js' export interface ConnectionChildHandlerDependencies { readonly controls: IControlStore @@ -87,7 +88,7 @@ export interface ConnectionChildHandlerApi extends ChildProcessHandlerBase { /** * Tell the child instance class to execute an action */ - actionRun(action: ActionEntityModel, extras: RunActionExtras): Promise + actionRun(action: ActionEntityModel, extras: RunActionExtras): Promise /** * @@ -106,4 +107,5 @@ export interface RunActionExtras { location: ControlLocation | undefined abortDelayed: AbortSignal executionMode: 'sequential' | 'concurrent' + previousResult: VariableValue } diff --git a/companion/lib/Instance/Connection/ChildHandlerLegacy.ts b/companion/lib/Instance/Connection/ChildHandlerLegacy.ts index 5e076b12bb..af7ca94f03 100644 --- a/companion/lib/Instance/Connection/ChildHandlerLegacy.ts +++ b/companion/lib/Instance/Connection/ChildHandlerLegacy.ts @@ -569,7 +569,7 @@ export class ConnectionChildHandlerLegacy implements ChildProcessHandlerBase, Co /** * Tell the child instance class to execute an action */ - async actionRun(action: ActionEntityModel, extras: RunActionExtras): Promise { + async actionRun(action: ActionEntityModel, extras: RunActionExtras): Promise { if (action.connectionId !== this.connectionId) throw new Error(`Action is for a different connection`) try { @@ -584,7 +584,11 @@ export class ConnectionChildHandlerLegacy implements ChildProcessHandlerBase, Co if (!actionDefinition) throw new Error(`Failed to find action definition for ${action.definitionId}`) // Note: for actions, this doesn't need to be reactive - const parser = this.#deps.controls.createVariablesAndExpressionParser(extras.controlId, null) + const parser = this.#deps.controls.createVariablesAndExpressionParser( + extras.controlId, + null, + extras.previousResult + ) const parseRes = parser.parseEntityOptions(actionDefinition, action.options) if (!parseRes.ok) { this.logger.warn( @@ -621,6 +625,9 @@ export class ConnectionChildHandlerLegacy implements ChildProcessHandlerBase, Co throw e } + + // Legacy instance actions can't return values. + return undefined } /** @@ -931,7 +938,7 @@ export class ConnectionChildHandlerLegacy implements ChildProcessHandlerBase, Co msg: ParseVariablesInStringMessage ): Promise { try { - const parser = this.#deps.controls.createVariablesAndExpressionParser(msg.controlId, null) + const parser = this.#deps.controls.createVariablesAndExpressionParser(msg.controlId, null, undefined) const result = parser.parseVariables(msg.text) return { diff --git a/companion/lib/Instance/Connection/ChildHandlerNew.ts b/companion/lib/Instance/Connection/ChildHandlerNew.ts index aa9a9b311e..40e066e0a6 100644 --- a/companion/lib/Instance/Connection/ChildHandlerNew.ts +++ b/companion/lib/Instance/Connection/ChildHandlerNew.ts @@ -44,6 +44,7 @@ import type { ConnectionChildHandlerDependencies, RunActionExtras, } from './ChildHandlerApi.js' +import type { VariableValue } from '@companion-app/shared/Model/Variables.js' import type { SharedUdpSocketMessageJoin, SharedUdpSocketMessageLeave } from '@companion-module/base/host-api' import { exprVal, @@ -277,7 +278,7 @@ export class ConnectionChildHandlerNew implements ChildProcessHandlerBase, Conne } const learnTimeout = entityDefinition.learnTimeout - const parser = this.#deps.controls.createVariablesAndExpressionParser(controlId, null) + const parser = this.#deps.controls.createVariablesAndExpressionParser(controlId, null, undefined) const parseRes = parser.parseEntityOptions(entityDefinition, entity.options) if (!parseRes.ok) { this.logger.warn( @@ -355,9 +356,9 @@ export class ConnectionChildHandlerNew implements ChildProcessHandlerBase, Conne /** * Tell the child instance class to execute an action */ - async actionRun(action: ActionEntityModel, extras: RunActionExtras): Promise { + async actionRun(action: ActionEntityModel, extras: RunActionExtras): Promise { if (action.connectionId !== this.connectionId) throw new Error(`Action is for a different connection`) - if (action.disabled) return + if (action.disabled) return undefined try { // This means the new flow is being done, and the options must be parsed at this stage @@ -369,7 +370,11 @@ export class ConnectionChildHandlerNew implements ChildProcessHandlerBase, Conne if (!actionDefinition) throw new Error(`Failed to find action definition for ${action.definitionId}`) // Note: for actions, this doesn't need to be reactive - const parser = this.#deps.controls.createVariablesAndExpressionParser(extras.controlId, null) + const parser = this.#deps.controls.createVariablesAndExpressionParser( + extras.controlId, + null, + extras.previousResult + ) const parseRes = parser.parseEntityOptions(actionDefinition, action.options) if (!parseRes.ok) { this.logger.warn( @@ -388,11 +393,15 @@ export class ConnectionChildHandlerNew implements ChildProcessHandlerBase, Conne surfaceId: extras?.surfaceId, }) - if (result && !result.success) { - const message = result.errorMessage || 'Unknown error' - this.logger.warn(`Error executing action: ${message}`) - this.#sendToModuleLog('error', `Error executing action: ${message}`) + + if (result && result.success) { + return result.result } + + const message = result?.errorMessage || 'Unknown error' + this.logger.warn(`Error executing action: ${message}`) + this.#sendToModuleLog('error', `Error executing action: ${message}`) + return undefined } catch (e) { this.logger.warn(`Error executing action: ${stringifyError(e)}`) this.#sendToModuleLog('error', `Error executing action: ${stringifyError(e)}`) diff --git a/companion/lib/Instance/Connection/EntityManager.ts b/companion/lib/Instance/Connection/EntityManager.ts index e90615a70d..d2ef390456 100644 --- a/companion/lib/Instance/Connection/EntityManager.ts +++ b/companion/lib/Instance/Connection/EntityManager.ts @@ -183,7 +183,7 @@ export class ConnectionEntityManager { let updateOptions: CompanionOptionValues | undefined try { // Parse the options and track the variables referenced - const parser = this.controlsStore.createVariablesAndExpressionParser(wrapper.controlId, null) + const parser = this.controlsStore.createVariablesAndExpressionParser(wrapper.controlId, null, undefined) const parseRes = parser.parseEntityOptions(entityDefinition, entityModel.options) if (!parseRes.ok) { this.#logger.warn( diff --git a/companion/lib/Instance/Connection/IpcTypesNew.ts b/companion/lib/Instance/Connection/IpcTypesNew.ts index 664b98009e..591b3bfa39 100644 --- a/companion/lib/Instance/Connection/IpcTypesNew.ts +++ b/companion/lib/Instance/Connection/IpcTypesNew.ts @@ -185,12 +185,19 @@ export interface ExecuteActionMessage { surfaceId: string | undefined } -export interface ExecuteActionResponseMessage { - success: boolean - /** If success=false, a reason for the failure */ - errorMessage: string | undefined +export interface ExecuteActionSuccess { + success: true + result: CompanionVariableValue } +export interface ExecuteActionFailure { + success: false + /** A reason for the failure */ + errorMessage: string +} + +export type ExecuteActionResponseMessage = ExecuteActionSuccess | ExecuteActionFailure + export interface UpdateFeedbackValuesMessage { values: HostFeedbackValue[] } diff --git a/companion/lib/Instance/Connection/Thread/Entrypoint.ts b/companion/lib/Instance/Connection/Thread/Entrypoint.ts index ced662b5c4..47078eb13a 100644 --- a/companion/lib/Instance/Connection/Thread/Entrypoint.ts +++ b/companion/lib/Instance/Connection/Thread/Entrypoint.ts @@ -103,10 +103,15 @@ const ipcWrapper = new IpcWrapper( if (!instance || !instanceInitialized) throw new Error('Not initialized') const res = await instance.executeAction(msg.action, msg.surfaceId) - return { - success: res.success, - errorMessage: res.errorMessage, - } + return res.success + ? { + success: true, + result: res.result, + } + : { + success: false, + errorMessage: res.errorMessage, + } }, getConfigFields: async (): Promise => { if (!instance || !instanceInitialized) throw new Error('Not initialized') diff --git a/companion/lib/Internal/Controller.ts b/companion/lib/Internal/Controller.ts index 9de7fc24b0..7bade5edbb 100644 --- a/companion/lib/Internal/Controller.ts +++ b/companion/lib/Internal/Controller.ts @@ -312,7 +312,7 @@ export class InternalController { return undefined } - const parser = this.#controlsStore.createVariablesAndExpressionParser(feedbackState.controlId, null) + const parser = this.#controlsStore.createVariablesAndExpressionParser(feedbackState.controlId, null, undefined) // Parse the options if enabled let parsedOptions: CompanionOptionValues @@ -461,8 +461,13 @@ export class InternalController { const overrideVariableValues: VariableValues = { '$(this:surface_id)': extras.surfaceId, + '$(this:result)': extras.previousResult, } - const parser = this.#controlsStore.createVariablesAndExpressionParser(extras.controlId, overrideVariableValues) + const parser = this.#controlsStore.createVariablesAndExpressionParser( + extras.controlId, + overrideVariableValues, + extras.previousResult + ) let parsedOptions: CompanionOptionValues if (entityDefinition.optionsSupportExpressions) { diff --git a/companion/lib/Preview/ExpressionStream.ts b/companion/lib/Preview/ExpressionStream.ts index d021354255..49bb7e635a 100644 --- a/companion/lib/Preview/ExpressionStream.ts +++ b/companion/lib/Preview/ExpressionStream.ts @@ -104,14 +104,14 @@ export class PreviewExpressionStream { controlId: string | null, requiredType: string | undefined ): ExecuteExpressionResult => { - const parser = this.#controlsController.createVariablesAndExpressionParser(controlId, null) + const parser = this.#controlsController.createVariablesAndExpressionParser(controlId, null, undefined) // TODO - make reactive to control moving? return parser.executeExpression(expression, requiredType) } #parseVariables = (str: string, controlId: string | null): ExecuteExpressionResult => { - const parser = this.#controlsController.createVariablesAndExpressionParser(controlId, null) + const parser = this.#controlsController.createVariablesAndExpressionParser(controlId, null, undefined) // TODO - make reactive to control moving? const res = parser.parseVariables(str) diff --git a/companion/lib/Preview/Graphics.ts b/companion/lib/Preview/Graphics.ts index 136fefb1e2..1be529916f 100644 --- a/companion/lib/Preview/Graphics.ts +++ b/companion/lib/Preview/Graphics.ts @@ -175,7 +175,7 @@ export class PreviewGraphics { const changes = toIterable(self.#renderEvents, `reference:${id}`, signal) const location = self.#pageStore.getLocationOfControlId(controlId) - const parser = self.#controlsController.createVariablesAndExpressionParser(controlId, null) + const parser = self.#controlsController.createVariablesAndExpressionParser(controlId, null, undefined) // Do a resolve of the reference for the starting image const locationValue = parser.parseEntityOption(options.location, { @@ -265,7 +265,11 @@ export class PreviewGraphics { #triggerRecheck(previewSession: PreviewSession): void { try { const location = this.#pageStore.getLocationOfControlId(previewSession.controlId) - const parser = this.#controlsController.createVariablesAndExpressionParser(previewSession.controlId, null) + const parser = this.#controlsController.createVariablesAndExpressionParser( + previewSession.controlId, + null, + undefined + ) // Resolve the new location const locationValue = parser.parseEntityOption(previewSession.options.location, { diff --git a/companion/lib/Surface/Controller.ts b/companion/lib/Surface/Controller.ts index d1814b1261..cdbabdec1a 100644 --- a/companion/lib/Surface/Controller.ts +++ b/companion/lib/Surface/Controller.ts @@ -1429,10 +1429,15 @@ export class SurfaceController extends EventEmitter { surfaceId: string, injectedVariableValues: VariableValues | undefined ): ExecuteExpressionResult { - const parser = this.#handlerDependencies.variables.values.createVariablesAndExpressionParser(null, null, { - ...injectedVariableValues, - ...this.#getInjectedVariablesForSurfaceId(surfaceId), - }) + const parser = this.#handlerDependencies.variables.values.createVariablesAndExpressionParser( + null, + null, + { + ...injectedVariableValues, + ...this.#getInjectedVariablesForSurfaceId(surfaceId), + }, + undefined + ) return parser.executeExpression(str, undefined) } diff --git a/companion/lib/Variables/Values.ts b/companion/lib/Variables/Values.ts index 4db2420977..ce9f5a8d99 100644 --- a/companion/lib/Variables/Values.ts +++ b/companion/lib/Variables/Values.ts @@ -62,10 +62,11 @@ export class VariablesValues extends EventEmitter { createVariablesAndExpressionParser( controlLocation: ControlLocation | null | undefined, localValues: ControlEntityInstance[] | null, - overrideVariableValues: VariableValues | null + overrideVariableValues: VariableValues | null, + previousResult: VariableValue ): VariablesAndExpressionParser { const thisValues: VariablesCache = new Map() - this.addInjectedVariablesForLocation(thisValues, controlLocation) + this.#addInjectedVariablesForLocation(thisValues, controlLocation, previousResult) return new VariablesAndExpressionParser( this.#blinker, @@ -174,7 +175,11 @@ export class VariablesValues extends EventEmitter { /** * Variables to inject based on location */ - addInjectedVariablesForLocation(values: VariablesCache, location: ControlLocation | null | undefined): void { + #addInjectedVariablesForLocation( + values: VariablesCache, + location: ControlLocation | null | undefined, + previousResult: VariableValue + ): void { values.set('$(this:page)', location?.pageNumber) values.set('$(this:column)', location?.column) values.set('$(this:row)', location?.row) @@ -214,6 +219,8 @@ export class VariablesValues extends EventEmitter { // ? `$(internal:b_status_${location.pageNumber}_${location.row}_${location.column})` // : VARIABLE_UNKNOWN_VALUE // ) + + values.set('$(this:result)', previousResult) } } diff --git a/companion/package.json b/companion/package.json index b1a918d7aa..87286ec35b 100644 --- a/companion/package.json +++ b/companion/package.json @@ -1,104 +1,104 @@ { - "name": "companion", - "version": "4.3.0", - "description": "Companion", - "main": "main.js", - "type": "module", - "private": true, - "scripts": { - "dev": "run dev:inner --admin-address 127.0.0.1 --log-level debug", - "dev:inner": "run -TB tsx --env-file-if-exists=../.env ../tools/dev.mts --extra-module-path=../module-local-dev", - "dev:debug": "run dev:inner --admin-address 127.0.0.1 --log-level silly", - "build": "tsc && webpack" - }, - "repository": "https://github.com/bitfocus/companion", - "keywords": [ - "bitfocus", - "companion" - ], - "engines": { - "npm": "please-use-yarn", - "yarn": "^4.5", - "node": ">=22.22.2 <23" - }, - "author": "Bitfocus AS", - "license": "MIT", - "devDependencies": { - "@sentry/webpack-plugin": "^5.1.1", - "@types/better-sqlite3": "^7.6.13", - "@types/compression": "^1.8.1", - "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", - "@types/on-headers": "^1.0.4", - "@types/semver": "^7.7.1", - "@types/socketcluster-client": "^20.0.0", - "@types/supertest": "^7.2.0", - "@types/tar-fs": "^2.0.4", - "@types/tar-stream": "^3.1.4", - "@types/winston-syslog": "^2.4.4", - "@types/ws": "^8.18.1", - "@types/yazl": "^3.3.0", - "supertest": "^7.2.2", - "typescript": "~5.9.3", - "webpack": "5.104.1", - "webpack-cli": "^7.0.2" - }, - "dependencies": { - "@companion-app/shared": "*", - "@companion-module/base-old": "npm:@companion-module/base@~1.14.1", - "@companion-module/host": "1.0.2", - "@companion-surface/host": "~1.1.5", - "@julusian/bonjour-service": "^1.4.2", - "@julusian/image-rs": "^2.1.1", - "@julusian/segfault-raub": "^2.3.2", - "@napi-rs/canvas": "0.1.97", - "@sentry/node": "^10.45.0", - "@trpc/server": "^11.14.1", - "better-sqlite3": "^12.8.0", - "bufferutil": "^4.1.0", - "colord": "^2.9.3", - "commander": "^14.0.3", - "compression": "^1.8.1", - "cors": "^2.8.6", - "csv-stringify": "^6.7.0", - "dayjs": "^1.11.20", - "debounce-fn": "^6.0.0", - "emberplus-connection": "^0.2.4", - "env-paths": "^4.0.0", - "express": "^5.2.1", - "express-serve-zip": "^2.0.1", - "fast-deep-equal": "^3.1.3", - "fast-json-patch": "patch:fast-json-patch@npm%3A3.1.1#~/.yarn/patches/fast-json-patch-npm-3.1.1-7e8bb70a45.patch", - "fs-extra": "^11.3.4", - "get-port": "^7.2.0", - "nanoid": "^5.1.7", - "node-cron": "^4.2.1", - "node-hid": "^3.3.0", - "node-machine-id": "^1.1.12", - "on-headers": "^1.1.0", - "openapi-fetch": "^0.17.0", - "osc": "2.4.5", - "p-debounce": "^5.1.0", - "p-queue": "^9.1.0", - "path-to-regexp": "^8.3.0", - "quick-lru": "^7.3.0", - "selfsigned": "^5.5.0", - "semver": "^7.7.4", - "socketcluster-client": "^20.0.1", - "supports-color": "^10.2.2", - "systeminformation": "^5.31.5", - "tar-fs": "^3.1.2", - "tar-stream": "^3.1.8", - "type-fest": "^5.5.0", - "udev-generator": "^1.0.2", - "usb": "^2.17.0", - "uuid": "^13.0.0", - "winston": "^3.19.0", - "winston-syslog": "patch:winston-syslog@npm%3A2.7.1#~/.yarn/patches/winston-syslog-npm-2.7.1-c8df046508.patch", - "workerpool": "^10.0.1", - "ws": "^8.20.0", - "yaml": "^2.8.3", - "yazl": "^3.3.1", - "zod": "^4.3.6" - } + "name": "companion", + "version": "4.3.0", + "description": "Companion", + "main": "main.js", + "type": "module", + "private": true, + "scripts": { + "dev": "run dev:inner --admin-address 127.0.0.1 --log-level debug", + "dev:inner": "run -TB tsx --env-file-if-exists=../.env ../tools/dev.mts --extra-module-path=../module-local-dev", + "dev:debug": "run dev:inner --admin-address 127.0.0.1 --log-level silly", + "build": "tsc && webpack" + }, + "repository": "https://github.com/bitfocus/companion", + "keywords": [ + "bitfocus", + "companion" + ], + "engines": { + "npm": "please-use-yarn", + "yarn": "^4.5", + "node": ">=22.22.2 <23" + }, + "author": "Bitfocus AS", + "license": "MIT", + "devDependencies": { + "@sentry/webpack-plugin": "^5.1.1", + "@types/better-sqlite3": "^7.6.13", + "@types/compression": "^1.8.1", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/on-headers": "^1.0.4", + "@types/semver": "^7.7.1", + "@types/socketcluster-client": "^20.0.0", + "@types/supertest": "^7.2.0", + "@types/tar-fs": "^2.0.4", + "@types/tar-stream": "^3.1.4", + "@types/winston-syslog": "^2.4.4", + "@types/ws": "^8.18.1", + "@types/yazl": "^3.3.0", + "supertest": "^7.2.2", + "typescript": "~5.9.3", + "webpack": "5.104.1", + "webpack-cli": "^7.0.2" + }, + "dependencies": { + "@companion-app/shared": "*", + "@companion-module/base-old": "npm:@companion-module/base@~1.14.1", + "@companion-module/host": "1.0.2", + "@companion-surface/host": "~1.1.5", + "@julusian/bonjour-service": "^1.4.2", + "@julusian/image-rs": "^2.1.1", + "@julusian/segfault-raub": "^2.3.2", + "@napi-rs/canvas": "0.1.97", + "@sentry/node": "^10.45.0", + "@trpc/server": "^11.14.1", + "better-sqlite3": "^12.8.0", + "bufferutil": "^4.1.0", + "colord": "^2.9.3", + "commander": "^14.0.3", + "compression": "^1.8.1", + "cors": "^2.8.6", + "csv-stringify": "^6.7.0", + "dayjs": "^1.11.20", + "debounce-fn": "^6.0.0", + "emberplus-connection": "^0.2.4", + "env-paths": "^4.0.0", + "express": "^5.2.1", + "express-serve-zip": "^2.0.1", + "fast-deep-equal": "^3.1.3", + "fast-json-patch": "patch:fast-json-patch@npm%3A3.1.1#~/.yarn/patches/fast-json-patch-npm-3.1.1-7e8bb70a45.patch", + "fs-extra": "^11.3.4", + "get-port": "^7.2.0", + "nanoid": "^5.1.7", + "node-cron": "^4.2.1", + "node-hid": "^3.3.0", + "node-machine-id": "^1.1.12", + "on-headers": "^1.1.0", + "openapi-fetch": "^0.17.0", + "osc": "2.4.5", + "p-debounce": "^5.1.0", + "p-queue": "^9.1.0", + "path-to-regexp": "^8.3.0", + "quick-lru": "^7.3.0", + "selfsigned": "^5.5.0", + "semver": "^7.7.4", + "socketcluster-client": "^20.0.1", + "supports-color": "^10.2.2", + "systeminformation": "^5.31.5", + "tar-fs": "^3.1.2", + "tar-stream": "^3.1.8", + "type-fest": "^5.5.0", + "udev-generator": "^1.0.2", + "usb": "^2.17.0", + "uuid": "^13.0.0", + "winston": "^3.19.0", + "winston-syslog": "patch:winston-syslog@npm%3A2.7.1#~/.yarn/patches/winston-syslog-npm-2.7.1-c8df046508.patch", + "workerpool": "^10.0.1", + "ws": "^8.20.0", + "yaml": "^2.8.3", + "yazl": "^3.3.1", + "zod": "^4.3.6" + } } diff --git a/docs/user-guide/3_config/variables.md b/docs/user-guide/3_config/variables.md index f963af9d0e..fd2382f9bc 100644 --- a/docs/user-guide/3_config/variables.md +++ b/docs/user-guide/3_config/variables.md @@ -50,19 +50,19 @@ For each custom variable, you can see and set: - **Startup value** The value to use for the variable upon restarting Companion - **Persist value** Whether to persist the current value to be used upon startup. This will increase disk IO. -Just like connection variables a custom variable can hold data in different formats, called types. +Just like connection variables a custom variable can hold data in different formats, called types. The available types are: - string: a UTF-8 encoded text, e.g. `Hello "world" 😀` - number: a IEEE754 floating-point number e.g. `2.4`, `-8`. Internally 64bits are used to store the number. That means that there is a limit in size and precision. When used as integer the range is -2^53 + 1 to 2^53 - 1 - boolean: the smallest digital information, `true` or `false` -- object: a collection of objects or other data types. Basically there are two forms, a keyed collection, often referred as a JSON (JavaScript object notation), and a indexed collection, called an Array. - JSON is enclosed in curly brackets and holds a comma delimited list of properties with a key name and a value, e.g. `{"key1":"value1", "key2":42, "third_key":{"description":"objects can be nested"}}` +- object: a collection of objects or other data types. Basically there are two forms, a keyed collection, often referred as a JSON (JavaScript object notation), and a indexed collection, called an Array. + JSON is enclosed in curly brackets and holds a comma delimited list of properties with a key name and a value, e.g. `{"key1":"value1", "key2":42, "third_key":{"description":"objects can be nested"}}` Arrays are enclosed in square brackets and hold a comma delimited list of values without keys, the values are referenced by their position with the first position having the index 0, e.g. `["value1", 42, {"description":"arrays can hold different data types"}]` - null: this is a special value that can't be called a string or a number or something else, so it has its own data type. The value and data type is called `null`. It is often used to express invalidity. You can enter a value or startup value in two different ways: Text and JSON. -When the button on the left of a text entry field shows a T, you can enter text in the field and the variable will be updated with that text. The data type of the variable will be string. +When the button on the left of a text entry field shows a T, you can enter text in the field and the variable will be updated with that text. The data type of the variable will be string. When the button on the left is switched to {}, you can enter JSON and the variable will be updated with the corresponding value. If you want to enter a number, just type it in JSON entry. If you want to enter a boolean just type true or false in JSON entry. If you want to enter a text in JSON entry you would need to enclose it in double quotation marks and escape all quotation marks inside the text, so it is easier to enter text with text entry. Objects are very useful when you want to store multiple values in _one_ variable. Let's assume you want to store the names of 3 persons. You could add three custom variables with the names `name1`, `name2` and `name3`. But you can also create one custom variable for it with the name `names` and set it to the array `["Peter","Paul","Mary"]`. When you want to show a name on a button, you have to access it with an expression. E.g. the expression for getting the second name (index 1) is `$(custom:names)[1]`. If you want to change a single name in that array from an action, you have to use an expression to. Set for example the variable using the expression `arr=$(custom:names); arr[1] = 'John'; arr`. This expression will read the current array, set the second element to John and finally return the new array. This may sound more complicated than just updating a dedicated variable for name2, but now think of having to do this for 100 names. @@ -121,3 +121,5 @@ Additionally, in some places there are some builtin local variables under the `$ - Variable: `this:page_name` - The id of the surface triggering this action - Variable: `this:surface_id` +- The result of the action sequentially preceding this one + - Variable: `this:result` diff --git a/webui/src/Controls/LocalVariablesStore.tsx b/webui/src/Controls/LocalVariablesStore.tsx index 2510c3985e..15587b26db 100644 --- a/webui/src/Controls/LocalVariablesStore.tsx +++ b/webui/src/Controls/LocalVariablesStore.tsx @@ -131,6 +131,10 @@ export const ControlLocalVariables: DropdownChoiceInt[] = [ value: 'this:page_name', label: 'This page name', }, + { + value: 'this:result', + label: 'The result of the action sequentially preceding this one', + }, ] export const ControlWithInternalLocalVariables: DropdownChoiceInt[] = [ diff --git a/yarn.lock b/yarn.lock index a58ae4b775..c77de2418f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1926,25 +1926,25 @@ __metadata: languageName: node linkType: hard -"@companion-module/base@npm:~2.0.3": +"@companion-module/base@file:/home/jwalden/Christianity/Sound/github/module-base/packages/base/::locator=%40companion-module%2Fhost%40file%3A%2Fhome%2Fjwalden%2FChristianity%2FSound%2Fgithub%2Fmodule-base%2Fpackages%2Fhost%2F%23%2Fhome%2Fjwalden%2FChristianity%2FSound%2Fgithub%2Fmodule-base%2Fpackages%2Fhost%2F%3A%3Ahash%3D95c8d1%26locator%3Dcompanion%2540workspace%253Acompanion": version: 2.0.3 - resolution: "@companion-module/base@npm:2.0.3" + resolution: "@companion-module/base@file:/home/jwalden/Christianity/Sound/github/module-base/packages/base/#/home/jwalden/Christianity/Sound/github/module-base/packages/base/::hash=9897ad&locator=%40companion-module%2Fhost%40file%3A%2Fhome%2Fjwalden%2FChristianity%2FSound%2Fgithub%2Fmodule-base%2Fpackages%2Fhost%2F%23%2Fhome%2Fjwalden%2FChristianity%2FSound%2Fgithub%2Fmodule-base%2Fpackages%2Fhost%2F%3A%3Ahash%3D95c8d1%26locator%3Dcompanion%2540workspace%253Acompanion" dependencies: colord: "npm:^2.9.3" tslib: "npm:^2.8.1" - checksum: 10c0/a6b3be873fbc52e9a842cebe9201e670df1c79655975678aefb045d6b83e76ec8be77a507136e28153aca0cc5dab19dcf6c138c0eeeb365c1ada609c28b00ab8 + checksum: 10c0/8b2e26a1708a44132c5b7bd4cc7aad9ae1f4e946963d3b56c92beee3295d6ebf53d3e8f2d7dc5c021fd10646cc62bc396a588f275668c65b62713fe8de44c3fc languageName: node linkType: hard -"@companion-module/host@npm:1.0.2": +"@companion-module/host@file:/home/jwalden/Christianity/Sound/github/module-base/packages/host/::locator=companion%40workspace%3Acompanion": version: 1.0.2 - resolution: "@companion-module/host@npm:1.0.2" + resolution: "@companion-module/host@file:/home/jwalden/Christianity/Sound/github/module-base/packages/host/#/home/jwalden/Christianity/Sound/github/module-base/packages/host/::hash=95c8d1&locator=companion%40workspace%3Acompanion" dependencies: - "@companion-module/base": "npm:~2.0.3" + "@companion-module/base": "file:/home/jwalden/Christianity/Sound/github/module-base/packages/base/" debounce-fn: "npm:^6.0.0" p-queue: "npm:^9.1.0" tslib: "npm:^2.8.1" - checksum: 10c0/5ca7f9db0dfd14c9a183bd63fe26854a79db1182207144336b43db82d3095f3be5b7942e4d32335a12c976036e4c9d54799b025d781eca8a51b3fd38d4a9be4c + checksum: 10c0/57043df7abc9ef715288f1a15d00ed4c3e5254a43dd294c49d7c213135c2f61b31d5a5cce99d431ef021a2c8308599d2250008682d2dbb0176eee6419a2179ec languageName: node linkType: hard @@ -11257,7 +11257,7 @@ __metadata: dependencies: "@companion-app/shared": "npm:*" "@companion-module/base-old": "npm:@companion-module/base@~1.14.1" - "@companion-module/host": "npm:1.0.2" + "@companion-module/host": "file:/home/jwalden/Christianity/Sound/github/module-base/packages/host/" "@companion-surface/host": "npm:~1.1.5" "@julusian/bonjour-service": "npm:^1.4.2" "@julusian/image-rs": "npm:^2.1.1"