* 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>
201 lines
6.8 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|