Files
custum-hyprpanel/src/services/cli/commander/Parser.ts
Jas Singh 8cf5806766 Minor: Refactor the code-base for better organization and compartmentalization. (#934)
* Clean up unused code

* Fix media player formatting issue for labels with new line characteres.

* Refactor the media player handlers into a class.

* More code cleanup and organize shared weather utils into distinct classes.

* Flatten some nesting.

* Move weather manager in dedicated class and build HTTP Utility class for Rest API calling.

* Remove logs

* Rebase master merge

* Reorg code (WIP)

* More reorg

* Delete utility scripts

* Reorg options

* Finish moving all options over

* Fix typescript issues

* Update options imports to default

* missed update

* Screw barrel files honestly, work of the devil.

* Only initialize power profiles if power-profiles-daemon is running.

* Fix window positioning and weather service naming

* style dir

* More organization

* Restructure types to be closer to their source

* Remove lib types and constants

* Update basic weather object to be saner with extensibility.

* Service updates

* Fix initialization strategy for services.

* Fix Config Manager to only emit changed objects and added missing temp converters.

* Update storage service to handle unit changes.

* Added cpu temp sensor auto-discovery

* Added missing JSDocs to services

* remove unused

* Migrate to network service.

* Fix network password issue.

* Move out password input into helper

* Rename password mask constant to be less double-negativey.

* Dropdown menu rename

* Added a component to edit JSON in the settings dialog (rough/WIP)

* Align settings

* Add and style JSON Editor.

* Adjust padding

* perf(shortcuts):  avoid unnecessary polling when shortcuts are disabled

Stops the recording poller when shortcuts are disabled, preventing redundant polling and reducing resource usage.

* Fix types and return value if shortcut not enabled.

* Move the swww daemon checking process outside of the wallpaper service into a dedicated deamon lifecyle processor.

* Add more string formatters and use title case for weather status (as it was).

* Fix startup errors.

* Rgba fix

* Remove zod from dependencies

---------

Co-authored-by: KernelDiego <gonzalezdiego.contact@gmail.com>
2025-05-26 19:45:11 -07:00

201 lines
6.8 KiB
TypeScript

import { CommandRegistry } from './Registry';
import { Command, ParsedCommand } from './types';
/**
* Parses an input string into a command and its positional arguments.
*
* Expected format:
* astal <commandName> arg1 arg2 arg3...
*
* 1. Tokenizes the input.
* 2. Identifies the command by the first token.
* 3. Parses positional arguments based on the command definition.
* 4. Converts arguments to their specified types.
* 5. Validates required arguments.
*/
export class CommandParser {
private _registry: CommandRegistry;
/**
* Constructs a CommandParser with the provided command registry.
*
* @param registry - The command registry containing available commands.
*/
constructor(registry: CommandRegistry) {
this._registry = registry;
}
/**
* Parses the entire input string, returning the matching command and its arguments.
*
* @param input - The raw input string to parse.
* @returns A parsed command object, including the command and its arguments.
* @throws If no command token is found.
* @throws If the command token is not registered.
*/
public parse(input: string): ParsedCommand {
const tokens = this._tokenize(input);
if (tokens.length === 0) {
throw new Error('No command provided.');
}
const commandName = tokens.shift() ?? 'non-existent-command';
const command = this._registry.get(commandName);
if (!command) {
throw new Error(
`Unknown command: "${commandName}". Use "hyprpanel explain" for available commands.`,
);
}
const args = this._parseArgs(command, tokens);
return { command, args };
}
/**
* Splits the input string into tokens, respecting quotes.
*
* @param input - The raw input string to break into tokens.
* @returns An array of tokens.
*/
private _tokenize(input: string): string[] {
const regex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g;
const matches = input.match(regex);
return matches ? matches.map((token) => this._stripQuotes(token)) : [];
}
/**
* Removes surrounding quotes from a single token, if they exist.
*
* @param str - The token from which to strip leading or trailing quotes.
* @returns The token without its outer quotes.
*/
private _stripQuotes(str: string): string {
return str.replace(/^["'](.+(?=["']$))["']$/, '$1');
}
/**
* Parses the array of tokens into arguments based on the command's argument definitions.
*
* @param command - The command whose arguments are being parsed.
* @param tokens - The list of tokens extracted from the input.
* @returns An object mapping argument names to their parsed values.
* @throws If required arguments are missing.
* @throws If there are too many tokens for the command definition.
*/
private _parseArgs(command: Command, tokens: string[]): Record<string, unknown> {
const args: Record<string, unknown> = {};
let currentIndex = 0;
for (const argDef of command.args) {
if (currentIndex >= tokens.length) {
if (argDef.required === true) {
throw new Error(`Missing required argument: "${argDef.name}".`);
}
if (argDef.default !== undefined) {
args[argDef.name] = argDef.default;
}
continue;
}
if (argDef.type === 'object') {
const { objectValue, nextIndex } = this._parseObjectTokens(tokens, currentIndex);
args[argDef.name] = objectValue;
currentIndex = nextIndex;
} else {
const value = tokens[currentIndex];
currentIndex++;
args[argDef.name] = this._convertType(value, argDef.type);
}
}
if (currentIndex < tokens.length) {
throw new Error(
`Too many arguments for command "${command.name}". Expected at most ${command.args.length}.`,
);
}
return args;
}
/**
* Accumulates tokens until braces are balanced to form a valid JSON string,
* then parses the result.
*
* @param tokens - The list of tokens extracted from the input.
* @param startIndex - The token index from which to begin JSON parsing.
* @returns An object containing the parsed JSON object and the next token index.
* @throws If the reconstructed JSON is invalid.
*/
private _parseObjectTokens(
tokens: string[],
startIndex: number,
): { objectValue: unknown; nextIndex: number } {
let braceCount = 0;
let started = false;
const objectTokens: string[] = [];
let currentIndex = startIndex;
while (currentIndex < tokens.length) {
const token = tokens[currentIndex];
currentIndex++;
for (const char of token) {
if (char === '{') braceCount++;
if (char === '}') braceCount--;
}
objectTokens.push(token);
if (started && braceCount === 0) break;
if (token.includes('{')) started = true;
}
const objectString = objectTokens.join(' ');
let parsed: unknown;
try {
parsed = JSON.parse(objectString);
} catch {
throw new Error(`Invalid JSON object: "${objectString}".`);
}
return { objectValue: parsed, nextIndex: currentIndex };
}
/**
* Converts a single token to the specified argument type.
*
* @param value - The raw token to be converted.
* @param type - The expected argument type.
* @returns The converted value.
* @throws If the token cannot be converted to the expected type.
*/
private _convertType(value: string, type: 'string' | 'number' | 'boolean' | 'object'): unknown {
switch (type) {
case 'number': {
const num = Number(value);
if (isNaN(num)) {
throw new Error(`Expected a number but got "${value}".`);
}
return num;
}
case 'boolean': {
const lower = value.toLowerCase();
if (lower === 'true') return true;
if (lower === 'false') return false;
throw new Error(`Expected a boolean (true/false) but got "${value}".`);
}
case 'object': {
try {
return JSON.parse(value);
} catch {
throw new Error(`Invalid JSON object: "${value}".`);
}
}
case 'string':
default:
return value;
}
}
}