From fa81dca9507b7fa0f82099b75f2ab89c865626ac Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 28 Jun 2024 14:16:23 +0100 Subject: [PATCH] feat(backend): Add tab completion to server console command arguments A command can optionally define a `completer(args)` function. It takes an array of strings for the current arguments, and returns an array of completion results for the last argument. Includes a few completers: - alarm:info/clear/inspect completes the first argument as an alarm id or short_id - script:run completes the first argument as a script name - params:get/set completes the first argument as a parameter name --- .../backend/src/services/CommandService.js | 11 ++++++++ .../backend/src/services/DevConsoleService.js | 10 ++++--- .../backend/src/services/ParameterService.js | 17 ++++++++++-- .../backend/src/services/ScriptService.js | 10 +++++++ .../services/runtime-analysis/AlarmService.js | 27 ++++++++++++++++--- 5 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/services/CommandService.js b/packages/backend/src/services/CommandService.js index c4824f18de..b944f7a55e 100644 --- a/packages/backend/src/services/CommandService.js +++ b/packages/backend/src/services/CommandService.js @@ -37,6 +37,13 @@ class Command { log.error(err.stack); } } + + completeArgument(args) { + const completer = this.spec_.completer; + if ( completer ) + return completer(args); + return []; + } } class CommandService extends BaseService { @@ -87,6 +94,10 @@ class CommandService extends BaseService { get commandNames() { return this.commands_.map(command => command.id); } + + getCommand(id) { + return this.commands_.find(command => command.id === id); + } } module.exports = { diff --git a/packages/backend/src/services/DevConsoleService.js b/packages/backend/src/services/DevConsoleService.js index f14fcc9494..1556c902a3 100644 --- a/packages/backend/src/services/DevConsoleService.js +++ b/packages/backend/src/services/DevConsoleService.js @@ -132,9 +132,13 @@ class DevConsoleService extends BaseService { prompt: 'puter> ', terminal: true, completer: line => { - // We only complete service and command names - if ( line.includes(' ') ) - return; + if ( line.includes(' ') ) { + const [ commandName, ...args ] = line.split(/\s+/); + const command = commands.getCommand(commandName); + if (!command) + return; + return [ command.completeArgument(args), args[args.length - 1] ]; + } const results = commands.commandNames .filter(name => name.startsWith(line)) diff --git a/packages/backend/src/services/ParameterService.js b/packages/backend/src/services/ParameterService.js index fb17fc5e4f..ed08bae4b5 100644 --- a/packages/backend/src/services/ParameterService.js +++ b/packages/backend/src/services/ParameterService.js @@ -68,6 +68,17 @@ class ParameterService extends BaseService { } _registerCommands (commands) { + const completeParameterName = (args) => { + // The parameter name is the first argument, so return no results if we're on the second or later. + if (args.length > 1) + return; + const lastArg = args[args.length - 1]; + + return this.parameters_ + .map(parameter => parameter.spec_.id) + .filter(parameterName => parameterName.startsWith(lastArg)); + }; + commands.registerCommands('params', [ { id: 'get', @@ -76,7 +87,8 @@ class ParameterService extends BaseService { const [name] = args; const value = await this.get(name); log.log(value); - } + }, + completer: completeParameterName, }, { id: 'set', @@ -86,7 +98,8 @@ class ParameterService extends BaseService { const parameter = this._get_param(name); parameter.set(value); log.log(value); - } + }, + completer: completeParameterName, }, { id: 'list', diff --git a/packages/backend/src/services/ScriptService.js b/packages/backend/src/services/ScriptService.js index f0576c89ad..800268b857 100644 --- a/packages/backend/src/services/ScriptService.js +++ b/packages/backend/src/services/ScriptService.js @@ -31,6 +31,16 @@ class ScriptService extends BaseService { return; } await script.run(ctx, args); + }, + completer: (args) => { + // The script name is the first argument, so return no results if we're on the second or later. + if (args.length > 1) + return; + const scriptName = args[args.length - 1]; + + return this.scripts + .filter(script => scriptName.startsWith(scriptName)) + .map(script => script.name); } } ]); diff --git a/packages/backend/src/services/runtime-analysis/AlarmService.js b/packages/backend/src/services/runtime-analysis/AlarmService.js index d3c3eb78e2..69711b96b5 100644 --- a/packages/backend/src/services/runtime-analysis/AlarmService.js +++ b/packages/backend/src/services/runtime-analysis/AlarmService.js @@ -308,6 +308,24 @@ class AlarmService extends BaseService { } _register_commands (commands) { + const completeAlarmID = (args) => { + // The alarm ID is the first argument, so return no results if we're on the second or later. + if (args.length > 1) + return; + const lastArg = args[args.length - 1]; + + const results = []; + for ( const alarm of Object.values(this.alarms) ) { + if ( alarm.id.startsWith(lastArg) ) { + results.push(alarm.id); + } + if ( alarm.short_id?.startsWith(lastArg) ) { + results.push(alarm.short_id); + } + } + return results; + }; + commands.registerCommands('alarm', [ { id: 'list', @@ -341,7 +359,8 @@ class AlarmService extends BaseService { for ( const [key, value] of Object.entries(alarm.fields) ) { log.log(`- ${key}: ${util.inspect(value)}`); } - } + }, + completer: completeAlarmID, }, { id: 'clear', @@ -356,7 +375,8 @@ class AlarmService extends BaseService { ); } this.clear(id); - } + }, + completer: completeAlarmID, }, { id: 'clear-all', @@ -397,7 +417,8 @@ class AlarmService extends BaseService { log.log("┃ " + stringify_log_entry(lg)); } log.log(`┗━━ Logs before: ${alarm.id_string} ━━━━`); - } + }, + completer: completeAlarmID, }, ]); }