Upgrade to Agsv2 + Astal (#533)
* migrate to astal * Reorganize project structure. * progress * Migrate Dashboard and Window Title modules. * Migrate clock and notification bar modules. * Remove unused code * Media menu * Rework network and volume modules * Finish custom modules. * Migrate battery bar module. * Update battery module and organize helpers. * Migrate workspace module. * Wrap up bar modules. * Checkpoint before I inevitbly blow something up. * Updates * Fix event propagation logic. * Type fixes * More type fixes * Fix padding for event boxes. * Migrate volume menu and refactor scroll event handlers. * network module WIP * Migrate network service. * Migrate bluetooth menu * Updates * Migrate notifications * Update scrolling behavior for custom modules. * Improve popup notifications and add timer functionality. * Migration notifications menu header/controls. * Migrate notifications menu and consolidate notifications menu code. * Migrate power menu. * Dashboard progress * Migrate dashboard * Migrate media menu. * Reduce media menu nesting. * Finish updating media menu bindings to navigate active player. * Migrate battery menu * Consolidate code * Migrate calendar menu * Fix workspace logic to update on client add/change/remove and consolidate code. * Migrate osd * Consolidate hyprland service connections. * Implement startup dropdown menu position allocation. * Migrate settings menu (WIP) * Settings dialo menu fixes * Finish Dashboard menu * Type updates * update submoldule for types * update github ci * ci * Submodule update * Ci updates * Remove type checking for now. * ci fix * Fix a bunch of stuff, losing track... need rest. Brb coffee * Validate dropdown menu before render. * Consolidate code and add auto-hide functionality. * Improve auto-hide behavior. * Consolidate audio menu code * Organize bluetooth code * Improve active player logic * Properly dismiss a notification on action button resolution. * Implement CLI command engine and migrate CLI commands. * Handle variable disposal * Bar component fixes and add hyprland startup rules. * Handle potentially null bindings network and bluetooth bindings. * Handle potentially null wired adapter. * Fix GPU stats * Handle poller for GPU * Fix gpu bar logic. * Clean up logic for stat bars. * Handle wifi and wired bar icon bindings. * Fix battery percentages * Fix switch behavior * Wifi staging fixes * Reduce redundant hyprland service calls. * Code cleanup * Document the option code and reduce redundant calls to optimize performance. * Remove outdated comment. * Add JSDocs * Add meson to build hyprpanel * Consistency updates * Organize commands * Fix images not showing up on notifications. * Remove todo * Move hyprpanel configuration to the ~/.config/hyprpanel directory and add utility commands. * Handle SRC directory for the bundled/built hyprpanel. * Add namespaces to all windows * Migrate systray * systray updates * Update meson to include ts, tsx and scss files. * Remove log from meson * Fix file choose path and make it float. * Added a command to check the dependency status * Update dep names. * Get scale directly from env * Add todo
This commit is contained in:
19
src/cli/commander/InitializeCommand.ts
Normal file
19
src/cli/commander/InitializeCommand.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CommandRegistry } from './Registry';
|
||||
import { Command } from './types';
|
||||
import { createExplainCommand } from './helpers';
|
||||
import { appearanceCommands } from './commands/appearance';
|
||||
import { utilityCommands } from './commands/utility';
|
||||
import { windowManagementCommands } from './commands/windowManagement';
|
||||
|
||||
/**
|
||||
* Initializes and registers commands in the provided CommandRegistry.
|
||||
*
|
||||
* @param registry - The command registry to register commands in.
|
||||
*/
|
||||
export function initializeCommands(registry: CommandRegistry): void {
|
||||
const commandList: Command[] = [...appearanceCommands, ...utilityCommands, ...windowManagementCommands];
|
||||
|
||||
commandList.forEach((command) => registry.register(command));
|
||||
|
||||
registry.register(createExplainCommand(registry));
|
||||
}
|
||||
143
src/cli/commander/Parser.ts
Normal file
143
src/cli/commander/Parser.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { CommandRegistry } from './Registry';
|
||||
import { Command, ParsedCommand } from './types';
|
||||
|
||||
/**
|
||||
* The CommandParser is responsible for parsing the input string into a command and its positional arguments.
|
||||
* It does not handle flags, only positional arguments.
|
||||
*
|
||||
* Expected command format:
|
||||
* astal <commandName> arg1 arg2 arg3...
|
||||
*
|
||||
* The parser:
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Creates an instance of CommandParser.
|
||||
*
|
||||
* @param registry - The command registry to use.
|
||||
*/
|
||||
constructor(registry: CommandRegistry) {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the input string into a ParsedCommand object.
|
||||
*
|
||||
* @param input - The input string to parse.
|
||||
* @returns The parsed command and its arguments.
|
||||
* @throws If no command is provided or the command is unknown.
|
||||
*/
|
||||
parse(input: string): ParsedCommand {
|
||||
const tokens = this.tokenize(input);
|
||||
if (tokens.length === 0) {
|
||||
throw new Error('No command provided.');
|
||||
}
|
||||
|
||||
const commandName = tokens.shift()!;
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizes the input string into an array of tokens.
|
||||
*
|
||||
* @param input - The input string to tokenize.
|
||||
* @returns The 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)) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips quotes from the beginning and end of a string.
|
||||
*
|
||||
* @param str - The string to strip quotes from.
|
||||
* @returns The string without quotes.
|
||||
*/
|
||||
private stripQuotes(str: string): string {
|
||||
return str.replace(/^["'](.+(?=["']$))["']$/, '$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the positional arguments for a command.
|
||||
*
|
||||
* @param command - The command definition.
|
||||
* @param tokens - The array of argument tokens.
|
||||
* @returns The parsed arguments.
|
||||
* @throws If there are too many arguments or a required argument is missing.
|
||||
*/
|
||||
private parseArgs(command: Command, tokens: string[]): Record<string, unknown> {
|
||||
const args: Record<string, unknown> = {};
|
||||
const argDefs = command.args;
|
||||
|
||||
if (tokens.length > argDefs.length) {
|
||||
throw new Error(`Too many arguments for command "${command.name}". Expected at most ${argDefs.length}.`);
|
||||
}
|
||||
|
||||
argDefs.forEach((argDef, index) => {
|
||||
const value = tokens[index];
|
||||
if (value === undefined) {
|
||||
if (argDef.required) {
|
||||
throw new Error(`Missing required argument: "${argDef.name}".`);
|
||||
}
|
||||
if (argDef.default !== undefined) {
|
||||
args[argDef.name] = argDef.default;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
args[argDef.name] = this.convertType(value, argDef.type);
|
||||
});
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string value to the specified type.
|
||||
*
|
||||
* @param value - The value to convert.
|
||||
* @param type - The type to convert to.
|
||||
* @returns The converted value.
|
||||
* @throws If the value cannot be converted to the specified type.
|
||||
*/
|
||||
private convertType(
|
||||
value: string,
|
||||
type: 'string' | 'number' | 'boolean' | 'object',
|
||||
): string | number | boolean | Record<string, 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':
|
||||
if (value.toLowerCase() === 'true') return true;
|
||||
if (value.toLowerCase() === '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;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/cli/commander/Registry.ts
Normal file
53
src/cli/commander/Registry.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Command } from './types';
|
||||
|
||||
/**
|
||||
* The CommandRegistry manages the storage and retrieval of commands.
|
||||
* It supports registration of multiple commands, lookup by name or alias,
|
||||
* and retrieval of all commands for listing and help functionalities.
|
||||
*/
|
||||
export class CommandRegistry {
|
||||
private commands: Map<string, Command> = new Map();
|
||||
|
||||
/**
|
||||
* Registers a command. If a command with the same name or alias already exists,
|
||||
* it will throw an error.
|
||||
*
|
||||
* @param command - The command to register.
|
||||
* @throws If a command with the same name or alias already exists.
|
||||
*/
|
||||
register(command: Command): void {
|
||||
if (this.commands.has(command.name)) {
|
||||
throw new Error(`Command "${command.name}" is already registered.`);
|
||||
}
|
||||
this.commands.set(command.name, command);
|
||||
|
||||
if (command.aliases) {
|
||||
for (const alias of command.aliases) {
|
||||
if (this.commands.has(alias)) {
|
||||
throw new Error(`Alias "${alias}" is already in use.`);
|
||||
}
|
||||
this.commands.set(alias, command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a command by its name or alias. Returns undefined if not found.
|
||||
*
|
||||
* @param commandName - The name or alias of the command to retrieve.
|
||||
* @returns The command if found, otherwise undefined.
|
||||
*/
|
||||
get(commandName: string): Command | undefined {
|
||||
return this.commands.get(commandName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all registered commands, ensuring each command is returned once even if it has aliases.
|
||||
*
|
||||
* @returns An array of all registered commands.
|
||||
*/
|
||||
getAll(): Command[] {
|
||||
const unique = new Set<Command>(this.commands.values());
|
||||
return Array.from(unique);
|
||||
}
|
||||
}
|
||||
89
src/cli/commander/RequestHandler.ts
Normal file
89
src/cli/commander/RequestHandler.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { CommandParser } from './Parser';
|
||||
import { ResponseCallback } from './types';
|
||||
|
||||
/**
|
||||
* The RequestHandler orchestrates the parsing and execution of commands:
|
||||
* 1. Uses the CommandParser to parse the input into a command and args.
|
||||
* 2. Invokes the command handler with the parsed arguments.
|
||||
* 3. Handles any errors and passes the result back via the response callback.
|
||||
*/
|
||||
export class RequestHandler {
|
||||
private parser: CommandParser;
|
||||
|
||||
/**
|
||||
* Creates an instance of RequestHandler.
|
||||
*
|
||||
* @param parser - The CommandParser instance to use.
|
||||
*/
|
||||
constructor(parser: CommandParser) {
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the request handler with the given input and response callback.
|
||||
*
|
||||
* @param input - The input string to process.
|
||||
* @param response - The callback to handle the response.
|
||||
* @returns A promise that resolves when the request is handled.
|
||||
*/
|
||||
async initializeRequestHandler(input: string, response: ResponseCallback): Promise<void> {
|
||||
try {
|
||||
const parsed = this.parser.parse(input);
|
||||
const { command, args } = parsed;
|
||||
|
||||
const result = command.handler(args);
|
||||
if (result instanceof Promise) {
|
||||
const resolved = await result;
|
||||
response(this.formatOutput(resolved));
|
||||
} else {
|
||||
response(this.formatOutput(result));
|
||||
}
|
||||
} catch (error) {
|
||||
response(this.formatError(error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the output based on its type.
|
||||
*
|
||||
* @param output - The output to format.
|
||||
* @returns A string representation of the output.
|
||||
*/
|
||||
private formatOutput(output: unknown): string {
|
||||
if (typeof output === 'string') {
|
||||
return output;
|
||||
} else if (typeof output === 'number' || typeof output === 'boolean') {
|
||||
return output.toString();
|
||||
} else if (typeof output === 'object' && output !== null) {
|
||||
try {
|
||||
return JSON.stringify(output, null, 2);
|
||||
} catch {
|
||||
return 'Unable to display object.';
|
||||
}
|
||||
} else {
|
||||
return String(output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the error based on its type.
|
||||
*
|
||||
* @param error - The error to format.
|
||||
* @returns A string representation of the error.
|
||||
*/
|
||||
private formatError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return `Error: ${error.message}`;
|
||||
} else if (typeof error === 'string') {
|
||||
return `Error: ${error}`;
|
||||
} else if (typeof error === 'object' && error !== null) {
|
||||
try {
|
||||
return `Error: ${JSON.stringify(error, null, 2)}`;
|
||||
} catch {
|
||||
return 'An unknown error occurred.';
|
||||
}
|
||||
} else {
|
||||
return `Error: ${String(error)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/cli/commander/commands/appearance/index.ts
Normal file
75
src/cli/commander/commands/appearance/index.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { errorHandler } from 'src/lib/utils';
|
||||
import { Command } from '../types';
|
||||
import { BarLayouts } from 'src/lib/types/options';
|
||||
|
||||
export const appearanceCommands: Command[] = [
|
||||
{
|
||||
name: 'setWallpaper',
|
||||
aliases: ['sw'],
|
||||
description: 'Sets the wallpaper based on the provided input.',
|
||||
category: 'Appearance',
|
||||
args: [
|
||||
{
|
||||
name: 'path',
|
||||
description: 'Path to the wallpaper image.',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: (args: Record<string, unknown>): string => {
|
||||
try {
|
||||
setWallpaper(args['path'] as string);
|
||||
return 'Wallpaper set successfully.';
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return `Error setting wallpaper: ${error.message}`;
|
||||
}
|
||||
return `Error setting wallpaper: ${error}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'useTheme',
|
||||
aliases: ['ut'],
|
||||
description: 'Sets the theme based on the provided input.',
|
||||
category: 'Appearance',
|
||||
args: [
|
||||
{
|
||||
name: 'path',
|
||||
description: 'Path to the JSON file of the HyprPanel theme.',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: (args: Record<string, unknown>): string => {
|
||||
try {
|
||||
useTheme(args['path'] as string);
|
||||
return 'Theme set successfully.';
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'setLayout',
|
||||
aliases: ['slo'],
|
||||
description: 'Sets the layout of the modules on the bar.',
|
||||
category: 'Appearance',
|
||||
args: [
|
||||
{
|
||||
name: 'layout',
|
||||
description: 'Bar layout to apply. Wiki: https://hyprpanel.com/configuration/panel.html#layouts',
|
||||
type: 'object',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: (args: Record<string, unknown>): string => {
|
||||
try {
|
||||
setLayout(args['layout'] as BarLayouts);
|
||||
return 'Layout applied successfully.';
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
367
src/cli/commander/commands/utility/checkDependencies.ts
Normal file
367
src/cli/commander/commands/utility/checkDependencies.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import { GLib } from 'astal';
|
||||
import { errorHandler } from 'src/lib/utils';
|
||||
|
||||
const RED = '\x1b[31m';
|
||||
const GREEN = '\x1b[32m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const RESET = '\x1b[0m';
|
||||
const BOLD = '\x1b[1m';
|
||||
|
||||
const STATUS_INSTALLED = '(INSTALLED)';
|
||||
const STATUS_ACTIVE = '(ACTIVE)';
|
||||
const STATUS_DISABLED = '(DISABLED)';
|
||||
const STATUS_MISSING = '(MISSING)';
|
||||
|
||||
/**
|
||||
* Decodes a Uint8Array output into a trimmed UTF-8 string.
|
||||
*
|
||||
* @description Converts a Uint8Array output from a command execution into a human-readable string.
|
||||
*
|
||||
* @param output - The Uint8Array output to decode.
|
||||
*/
|
||||
function decodeOutput(output: Uint8Array): string {
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(output).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a command line synchronously and returns the exit code and output.
|
||||
*
|
||||
* @description Executes a shell command using GLib.spawn_command_line_sync and extracts the exit code, stdout, and stderr.
|
||||
*
|
||||
* @param command - The command to execute.
|
||||
*/
|
||||
function runCommand(command: string): CommandResult {
|
||||
const [, out, err, exitCode] = GLib.spawn_command_line_sync(command);
|
||||
const stdout = out ? decodeOutput(out) : '';
|
||||
const stderr = err ? decodeOutput(err) : '';
|
||||
return {
|
||||
exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Colors a given text using ANSI color codes.
|
||||
*
|
||||
* @description Wraps the provided text with ANSI color codes.
|
||||
*
|
||||
* @param text - The text to color.
|
||||
* @param color - The ANSI color code to use.
|
||||
*/
|
||||
function colorText(text: string, color: string): string {
|
||||
return `${color}${text}${RESET}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any of the given executables is installed by using `which`.
|
||||
*
|
||||
* @description Iterates through a list of executables and returns true if any are found.
|
||||
*
|
||||
* @param executables - The list of executables to check.
|
||||
*/
|
||||
function checkExecutable(executables: string[]): boolean {
|
||||
for (const exe of executables) {
|
||||
const { exitCode } = runCommand(`which ${exe}`);
|
||||
|
||||
if (exitCode === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any of the given libraries is installed using `pkg-config`.
|
||||
*
|
||||
* @description Uses `pkg-config --exists <lib>` to determine if a library is installed.
|
||||
*
|
||||
* @param libraries - The list of libraries to check.
|
||||
*/
|
||||
function checkLibrary(libraries: string[]): boolean {
|
||||
for (const lib of libraries) {
|
||||
const { exitCode, stdout } = runCommand(`sh -c "ldconfig -p | grep ${lib}"`);
|
||||
|
||||
if (exitCode === 0 && stdout.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the status of a service.
|
||||
*
|
||||
* @description Determines if a service is ACTIVE, INSTALLED (but not running), DISABLED, or MISSING.
|
||||
*
|
||||
* @param services - The list of services to check.
|
||||
*/
|
||||
function checkServiceStatus(services: string[]): ServiceStatus {
|
||||
for (const svc of services) {
|
||||
const activeResult = runCommand(`systemctl is-active ${svc}`);
|
||||
const activeStatus = activeResult.stdout;
|
||||
|
||||
if (activeStatus === 'active') {
|
||||
return 'ACTIVE';
|
||||
}
|
||||
|
||||
if (activeStatus === 'inactive' || activeStatus === 'failed') {
|
||||
const enabledResult = runCommand(`systemctl is-enabled ${svc}`);
|
||||
const enabledStatus = enabledResult.stdout;
|
||||
|
||||
if (enabledResult && (enabledStatus === 'enabled' || enabledStatus === 'static')) {
|
||||
return 'INSTALLED';
|
||||
} else if (enabledResult && enabledStatus === 'disabled') {
|
||||
return 'DISABLED';
|
||||
} else {
|
||||
return 'MISSING';
|
||||
}
|
||||
}
|
||||
|
||||
if (activeStatus === 'unknown' || activeResult.exitCode !== 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return 'MISSING';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the status string and color for a dependency based on its type and checks.
|
||||
*
|
||||
* @description Returns the formatted line indicating the status of the given dependency.
|
||||
*
|
||||
* @param dep - The dependency to check.
|
||||
*/
|
||||
function getDependencyStatus(dep: Dependency): string {
|
||||
let status: ServiceStatus | 'INSTALLED' | 'MISSING';
|
||||
|
||||
switch (dep.type) {
|
||||
case 'executable':
|
||||
status = checkExecutable(dep.check) ? 'INSTALLED' : 'MISSING';
|
||||
break;
|
||||
case 'library':
|
||||
status = checkLibrary(dep.check) ? 'INSTALLED' : 'MISSING';
|
||||
break;
|
||||
case 'service':
|
||||
status = checkServiceStatus(dep.check);
|
||||
break;
|
||||
default:
|
||||
status = 'MISSING';
|
||||
}
|
||||
|
||||
let color: string;
|
||||
let textStatus: string;
|
||||
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
textStatus = STATUS_ACTIVE;
|
||||
color = GREEN;
|
||||
break;
|
||||
case 'INSTALLED':
|
||||
textStatus = STATUS_INSTALLED;
|
||||
color = GREEN;
|
||||
break;
|
||||
case 'DISABLED':
|
||||
textStatus = STATUS_DISABLED;
|
||||
color = YELLOW;
|
||||
break;
|
||||
case 'MISSING':
|
||||
default:
|
||||
textStatus = STATUS_MISSING;
|
||||
color = RED;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!dep.description) {
|
||||
return ` ${colorText(textStatus, color)} ${dep.package}`;
|
||||
}
|
||||
|
||||
return ` ${colorText(textStatus, color)} ${dep.package}: ${dep.description ?? ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks all dependencies and returns a formatted output.
|
||||
*
|
||||
* @description Gathers the status of both required and optional dependencies and formats the result.
|
||||
*/
|
||||
export function checkDependencies(): string {
|
||||
try {
|
||||
const dependencies: Dependency[] = [
|
||||
{
|
||||
package: 'wireplumber',
|
||||
required: true,
|
||||
type: 'executable',
|
||||
check: ['wireplumber'],
|
||||
},
|
||||
{
|
||||
package: 'libgtop',
|
||||
required: true,
|
||||
type: 'library',
|
||||
check: ['gtop-2.0'],
|
||||
},
|
||||
{
|
||||
package: 'bluez',
|
||||
required: true,
|
||||
type: 'service',
|
||||
check: ['bluetooth.service'],
|
||||
},
|
||||
{
|
||||
package: 'bluez-utils',
|
||||
required: true,
|
||||
type: 'executable',
|
||||
check: ['bluetoothctl'],
|
||||
},
|
||||
{
|
||||
package: 'networkmanager',
|
||||
required: true,
|
||||
type: 'service',
|
||||
check: ['NetworkManager.service'],
|
||||
},
|
||||
{
|
||||
package: 'dart-sass',
|
||||
required: true,
|
||||
type: 'executable',
|
||||
check: ['sass'],
|
||||
},
|
||||
{
|
||||
package: 'wl-clipboard',
|
||||
required: true,
|
||||
type: 'executable',
|
||||
check: ['wl-copy', 'wl-paste'],
|
||||
},
|
||||
{
|
||||
package: 'upower',
|
||||
required: true,
|
||||
type: 'service',
|
||||
check: ['upower.service'],
|
||||
},
|
||||
{
|
||||
package: 'aylurs-gtk-shell',
|
||||
required: true,
|
||||
type: 'executable',
|
||||
check: ['ags'],
|
||||
},
|
||||
|
||||
{
|
||||
package: 'python',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['python', 'python3'],
|
||||
description: 'GPU usage tracking (NVidia only)',
|
||||
},
|
||||
{
|
||||
package: 'python-gpustat',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['gpustat'],
|
||||
description: 'GPU usage tracking (NVidia only)',
|
||||
},
|
||||
{
|
||||
package: 'pywal',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['wal'],
|
||||
description: 'Pywal hook for wallpapers',
|
||||
},
|
||||
{
|
||||
package: 'pacman-contrib',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['paccache', 'rankmirrors'],
|
||||
description: 'Checking for pacman updates',
|
||||
},
|
||||
{
|
||||
package: 'power-profiles-daemon',
|
||||
required: false,
|
||||
type: 'service',
|
||||
check: ['power-profiles-daemon.service'],
|
||||
description: 'Switch power profiles',
|
||||
},
|
||||
{
|
||||
package: 'swww',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['swww'],
|
||||
description: 'Setting wallpapers',
|
||||
},
|
||||
{
|
||||
package: 'grimblast',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['grimblast'],
|
||||
description: 'For the snapshot shortcut',
|
||||
},
|
||||
{
|
||||
package: 'brightnessctl',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['brightnessctl'],
|
||||
description: 'To control keyboard and screen brightness',
|
||||
},
|
||||
{
|
||||
package: 'btop',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['btop'],
|
||||
description: 'To view system resource usage',
|
||||
},
|
||||
{
|
||||
package: 'gpu-screen-recorder',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['gpu-screen-recorder'],
|
||||
description: 'To use the built-in screen recorder',
|
||||
},
|
||||
{
|
||||
package: 'hyprpicker',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['hyprpicker'],
|
||||
description: 'To use the preset color picker shortcut',
|
||||
},
|
||||
{
|
||||
package: 'matugen',
|
||||
required: false,
|
||||
type: 'executable',
|
||||
check: ['matugen'],
|
||||
description: 'To use wallpaper-based color schemes',
|
||||
},
|
||||
];
|
||||
|
||||
let output = `${BOLD}Required Dependencies:${RESET}\n`;
|
||||
|
||||
for (const dep of dependencies.filter((d) => d.required)) {
|
||||
output += getDependencyStatus(dep) + '\n';
|
||||
}
|
||||
|
||||
output += `\n${BOLD}Optional Dependencies:${RESET}\n`;
|
||||
|
||||
for (const dep of dependencies.filter((d) => !d.required)) {
|
||||
output += getDependencyStatus(dep) + '\n';
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
type CommandResult = {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
type DependencyType = 'executable' | 'library' | 'service';
|
||||
|
||||
type ServiceStatus = 'ACTIVE' | 'INSTALLED' | 'DISABLED' | 'MISSING';
|
||||
|
||||
type Dependency = {
|
||||
package: string;
|
||||
required: boolean;
|
||||
type: DependencyType;
|
||||
check: string[];
|
||||
description?: string;
|
||||
};
|
||||
104
src/cli/commander/commands/utility/index.ts
Normal file
104
src/cli/commander/commands/utility/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { errorHandler } from 'src/lib/utils';
|
||||
import { Command } from '../../types';
|
||||
import { execAsync, Gio, GLib } from 'astal';
|
||||
import { checkDependencies } from './checkDependencies';
|
||||
|
||||
export const utilityCommands: Command[] = [
|
||||
{
|
||||
name: 'systrayItems',
|
||||
aliases: ['sti'],
|
||||
description: 'Gets a list of IDs for the current applications in the system tray.',
|
||||
category: 'Utility',
|
||||
args: [],
|
||||
handler: (): string => {
|
||||
try {
|
||||
return getSystrayItems();
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'clearNotifications',
|
||||
aliases: ['cno'],
|
||||
description: 'Clears all of the notifications that currently exist.',
|
||||
category: 'Utility',
|
||||
args: [],
|
||||
handler: (): string => {
|
||||
try {
|
||||
clearAllNotifications();
|
||||
return 'Notifications cleared successfully.';
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'migrateConfig',
|
||||
aliases: ['mcfg'],
|
||||
description: 'Migrates the configuration file from the old location to the new one.',
|
||||
category: 'Utility',
|
||||
args: [],
|
||||
handler: (): string => {
|
||||
const oldPath = `${GLib.get_user_cache_dir()}/ags/hyprpanel/options.json`;
|
||||
|
||||
try {
|
||||
const oldFile = Gio.File.new_for_path(oldPath);
|
||||
const newFile = Gio.File.new_for_path(CONFIG);
|
||||
|
||||
if (oldFile.query_exists(null)) {
|
||||
oldFile.move(newFile, Gio.FileCopyFlags.OVERWRITE, null, null);
|
||||
return `Configuration file moved to ${CONFIG}`;
|
||||
} else {
|
||||
return `Old configuration file does not exist at ${oldPath}`;
|
||||
}
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'checkDependencies',
|
||||
aliases: ['chd'],
|
||||
description: 'Checks the status of required and optional dependencies.',
|
||||
category: 'Utility',
|
||||
args: [],
|
||||
handler: (): string => {
|
||||
try {
|
||||
return checkDependencies();
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'restart',
|
||||
aliases: ['r'],
|
||||
description: 'Restarts HyprPanel.',
|
||||
category: 'Utility',
|
||||
args: [],
|
||||
handler: (): string => {
|
||||
try {
|
||||
execAsync('bash -c "hyprpanel -q; hyprpanel"');
|
||||
return '';
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'quit',
|
||||
aliases: ['q'],
|
||||
description: 'Quits HyprPanel.',
|
||||
category: 'Utility',
|
||||
args: [],
|
||||
handler: (): string => {
|
||||
try {
|
||||
execAsync('bash -c "hyprpanel -q"');
|
||||
return '';
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
70
src/cli/commander/commands/windowManagement/index.ts
Normal file
70
src/cli/commander/commands/windowManagement/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { errorHandler } from 'src/lib/utils';
|
||||
import { Command } from '../types';
|
||||
import { App } from 'astal/gtk3';
|
||||
|
||||
export const windowManagementCommands: Command[] = [
|
||||
{
|
||||
name: 'isWindowVisible',
|
||||
aliases: ['iwv'],
|
||||
description: 'Checks if a specified window is visible.',
|
||||
category: 'Window Management',
|
||||
args: [
|
||||
{
|
||||
name: 'window',
|
||||
description: 'Name of the window to check.',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: (args: Record<string, unknown>): boolean => {
|
||||
return isWindowVisible(args['window'] as string);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toggleWindow',
|
||||
aliases: ['t'],
|
||||
description: 'Toggles the visibility of a specified window.',
|
||||
category: 'Window Management',
|
||||
args: [
|
||||
{
|
||||
name: 'window',
|
||||
description: 'The name of the window to toggle.',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: (args: Record<string, unknown>): string => {
|
||||
try {
|
||||
const windowName = args['window'] as string;
|
||||
const foundWindow = App.get_window(windowName);
|
||||
|
||||
if (!foundWindow) {
|
||||
throw new Error(`Window ${args['window']} not found.`);
|
||||
}
|
||||
|
||||
const windowStatus = foundWindow.visible ? 'hidden' : 'visible';
|
||||
|
||||
App.toggle_window(windowName);
|
||||
|
||||
return windowStatus;
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'listWindows',
|
||||
aliases: ['lw'],
|
||||
description: 'Gets a list of all HyprPanel windows.',
|
||||
category: 'Window Management',
|
||||
args: [],
|
||||
handler: (): string => {
|
||||
try {
|
||||
const windowList = App.get_windows().map((window) => window.name);
|
||||
return windowList.join('\n');
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
195
src/cli/commander/helpers/index.ts
Normal file
195
src/cli/commander/helpers/index.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { CommandRegistry } from '../Registry';
|
||||
import { CategoryMap, Command, PositionalArg } from '../types';
|
||||
|
||||
const ANSI_RESET = '\x1b[0m';
|
||||
const ANSI_BOLD = '\x1b[1m';
|
||||
const ANSI_UNDERLINE = '\x1b[4m';
|
||||
|
||||
// Foreground Colors
|
||||
const ANSI_FG_RED = '\x1b[31m';
|
||||
const ANSI_FG_GREEN = '\x1b[32m';
|
||||
const ANSI_FG_YELLOW = '\x1b[33m';
|
||||
const ANSI_FG_BLUE = '\x1b[34m';
|
||||
const ANSI_FG_MAGENTA = '\x1b[35m';
|
||||
const ANSI_FG_CYAN = '\x1b[36m';
|
||||
const ANSI_FG_WHITE = '\x1b[37m';
|
||||
|
||||
// Background Colors
|
||||
const ANSI_BG_RED = '\x1b[41m';
|
||||
const ANSI_BG_GREEN = '\x1b[42m';
|
||||
const ANSI_BG_YELLOW = '\x1b[43m';
|
||||
const ANSI_BG_BLUE = '\x1b[44m';
|
||||
const ANSI_BG_MAGENTA = '\x1b[45m';
|
||||
const ANSI_BG_CYAN = '\x1b[46m';
|
||||
const ANSI_BG_WHITE = '\x1b[47m';
|
||||
|
||||
/**
|
||||
* Creates the explain command.
|
||||
*
|
||||
* This command displays all available commands categorized by their respective
|
||||
* categories. If a specific command name is provided as an argument, it displays
|
||||
* detailed information about that command, including its positional parameters and aliases.
|
||||
*
|
||||
* @param registry - The command registry to use.
|
||||
* @returns The explain command.
|
||||
*/
|
||||
export function createExplainCommand(registry: CommandRegistry): Command {
|
||||
return {
|
||||
name: 'explain',
|
||||
aliases: ['e'],
|
||||
description: 'Displays explain information for all commands or a specific command.',
|
||||
category: 'General',
|
||||
args: [
|
||||
{
|
||||
name: 'commandName',
|
||||
description: 'Optional name of a command to get detailed info.',
|
||||
type: 'string',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
/**
|
||||
* Handler for the explain command.
|
||||
*
|
||||
* @param args - The arguments passed to the command.
|
||||
* @returns The formatted explain message.
|
||||
*/
|
||||
handler: (args: Record<string, unknown>): string => {
|
||||
const commandName = args['commandName'] as string | undefined;
|
||||
|
||||
if (commandName) {
|
||||
return formatCommandExplain(registry, commandName);
|
||||
}
|
||||
|
||||
return formatGlobalExplain(registry);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the detailed explain message for a specific command.
|
||||
*
|
||||
* @param registry - The command registry to retrieve the command.
|
||||
* @param commandName - The name of the command to get detailed explain for.
|
||||
* @returns The formatted detailed explain message.
|
||||
*/
|
||||
function formatCommandExplain(registry: CommandRegistry, commandName: string): string {
|
||||
const cmd = registry.get(commandName);
|
||||
if (!cmd) {
|
||||
return `${ANSI_FG_RED}✖ No such command: "${commandName}". Use "explain" to see all commands.${ANSI_RESET}\n`;
|
||||
}
|
||||
|
||||
let message = `${ANSI_BOLD}${ANSI_FG_YELLOW}Command: ${cmd.name}${ANSI_RESET}\n`;
|
||||
|
||||
if (cmd.aliases && cmd.aliases.length > 0) {
|
||||
const aliases = formatAliases(cmd.aliases);
|
||||
message += `${ANSI_FG_GREEN}Aliases:${ANSI_RESET} ${aliases}\n`;
|
||||
}
|
||||
|
||||
message += `${ANSI_FG_GREEN}Description:${ANSI_RESET} ${cmd.description}\n`;
|
||||
message += `${ANSI_FG_GREEN}Category:${ANSI_RESET} ${cmd.category}\n`;
|
||||
|
||||
if (cmd.args.length > 0) {
|
||||
message += `${ANSI_FG_GREEN}Arguments:${ANSI_RESET}\n`;
|
||||
const formattedArgs = formatArguments(cmd.args);
|
||||
message += formattedArgs;
|
||||
} else {
|
||||
message += `${ANSI_FG_GREEN}No positional arguments.${ANSI_RESET}`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the global explain message listing all available commands categorized by their categories.
|
||||
*
|
||||
* @param registry - The command registry to retrieve all commands.
|
||||
* @returns The formatted global explain message.
|
||||
*/
|
||||
function formatGlobalExplain(registry: CommandRegistry): string {
|
||||
const allCommands = registry.getAll();
|
||||
const categoryMap: CategoryMap = organizeCommandsByCategory(allCommands);
|
||||
|
||||
let explainMessage = `${ANSI_BOLD}${ANSI_FG_CYAN}Available HyprPanel Commands:${ANSI_RESET}\n`;
|
||||
|
||||
for (const [category, cmds] of Object.entries(categoryMap)) {
|
||||
explainMessage += `\n${ANSI_BOLD}${ANSI_FG_BLUE}${category}${ANSI_RESET}\n`;
|
||||
const formattedCommands = formatCommandList(cmds);
|
||||
explainMessage += formattedCommands;
|
||||
}
|
||||
|
||||
explainMessage += `\n${ANSI_FG_MAGENTA}Use "hyprpanel explain <commandName>" to get detailed information about a specific hyprpanel command.${ANSI_RESET}\n`;
|
||||
|
||||
return explainMessage.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Organizes commands into their respective categories.
|
||||
*
|
||||
* @param commands - The list of all commands.
|
||||
* @returns A mapping of category names to arrays of commands.
|
||||
*/
|
||||
function organizeCommandsByCategory(commands: Command[]): CategoryMap {
|
||||
const categoryMap: CategoryMap = {};
|
||||
|
||||
commands.forEach((cmd) => {
|
||||
if (!categoryMap[cmd.category]) {
|
||||
categoryMap[cmd.category] = [];
|
||||
}
|
||||
categoryMap[cmd.category].push(cmd);
|
||||
});
|
||||
|
||||
return categoryMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the list of commands under a specific category.
|
||||
*
|
||||
* @param commands - The list of commands in a category.
|
||||
* @returns A formatted string of commands.
|
||||
*/
|
||||
function formatCommandList(commands: Command[]): string {
|
||||
return (
|
||||
commands
|
||||
.map((cmd) => {
|
||||
const aliasesText =
|
||||
cmd.aliases && cmd.aliases.length > 0
|
||||
? ` (${cmd.aliases.map((alias) => `${ANSI_FG_CYAN}${alias}${ANSI_RESET}`).join(', ')})`
|
||||
: '';
|
||||
return ` - ${ANSI_FG_YELLOW}${cmd.name}${ANSI_RESET}${aliasesText}: ${cmd.description}`;
|
||||
})
|
||||
.join('\n') + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the aliases array into a readable string with appropriate coloring.
|
||||
*
|
||||
* @param aliases - The array of alias strings.
|
||||
* @returns The formatted aliases string.
|
||||
*/
|
||||
function formatAliases(aliases: string[]): string {
|
||||
return aliases.map((alias) => `${ANSI_FG_CYAN}${alias}${ANSI_RESET}`).join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the arguments array into a readable string with appropriate coloring.
|
||||
*
|
||||
* @param args - The array of positional arguments.
|
||||
* @returns The formatted arguments string.
|
||||
*/
|
||||
function formatArguments(args: PositionalArg[]): string {
|
||||
return (
|
||||
args
|
||||
.map((arg) => {
|
||||
const requirement = arg.required ? `${ANSI_FG_RED}(required)` : `${ANSI_FG_CYAN}(optional)`;
|
||||
const defaultValue =
|
||||
arg.default !== undefined
|
||||
? ` ${ANSI_FG_MAGENTA}[default: ${JSON.stringify(arg.default)}]${ANSI_RESET}`
|
||||
: '';
|
||||
return ` ${ANSI_FG_YELLOW}${arg.name}${ANSI_RESET}: ${arg.description} ${requirement}${defaultValue}`;
|
||||
})
|
||||
.join('\n') + '\n'
|
||||
);
|
||||
}
|
||||
35
src/cli/commander/index.ts
Normal file
35
src/cli/commander/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { CommandRegistry } from './Registry';
|
||||
import { CommandParser } from './Parser';
|
||||
import { RequestHandler } from './RequestHandler';
|
||||
import { initializeCommands } from './InitializeCommand';
|
||||
import { ResponseCallback } from './types';
|
||||
|
||||
/**
|
||||
* This is the entry point for the CLI. It:
|
||||
* 1. Creates a CommandRegistry
|
||||
* 2. Initializes all commands
|
||||
* 3. Creates a CommandParser
|
||||
* 4. Creates a RequestHandler
|
||||
* 5. Provides a function `runCLI` to process an input string and respond with a callback.
|
||||
*/
|
||||
|
||||
const registry = new CommandRegistry();
|
||||
|
||||
initializeCommands(registry);
|
||||
|
||||
const parser = new CommandParser(registry);
|
||||
const handler = new RequestHandler(parser);
|
||||
|
||||
/**
|
||||
* Run the CLI with a given input and a response callback.
|
||||
*
|
||||
* @param input - The input string to process.
|
||||
* @param response - The callback to handle the response.
|
||||
*/
|
||||
export function runCLI(input: string, response: ResponseCallback): void {
|
||||
handler.initializeRequestHandler(input, response).catch((err) => {
|
||||
response({ error: err instanceof Error ? err.message : String(err) });
|
||||
});
|
||||
}
|
||||
|
||||
export { registry };
|
||||
31
src/cli/commander/types.ts
Normal file
31
src/cli/commander/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface PositionalArg {
|
||||
name: string;
|
||||
description: string;
|
||||
type: 'string' | 'number' | 'boolean' | 'object';
|
||||
required?: boolean;
|
||||
default?: string | number | boolean | Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type HandlerReturn = unknown | Promise<unknown>;
|
||||
|
||||
export interface Command {
|
||||
name: string;
|
||||
aliases?: string[];
|
||||
description: string;
|
||||
category: string;
|
||||
args: PositionalArg[];
|
||||
handler: (args: Record<string, unknown>) => HandlerReturn;
|
||||
}
|
||||
|
||||
export interface ParsedCommand {
|
||||
command: Command;
|
||||
args: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ResponseCallback {
|
||||
(res: unknown): void;
|
||||
}
|
||||
|
||||
export interface CategoryMap {
|
||||
[category: string]: Command[];
|
||||
}
|
||||
Reference in New Issue
Block a user