Skip to content

Commit

Permalink
Merge branch 'master' into fix-nerdfonts
Browse files Browse the repository at this point in the history
  • Loading branch information
orangci authored Dec 25, 2024
2 parents c92c6b2 + c5e7a9b commit 15a02fc
Show file tree
Hide file tree
Showing 19 changed files with 518 additions and 284 deletions.
4 changes: 2 additions & 2 deletions nix/module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ in

theme = mkOption {
type = types.str;
default = null;
default = "";
example = "catppuccin_mocha";
description = "Theme to import (see ./themes/*.json)";
};
Expand Down Expand Up @@ -235,7 +235,7 @@ in
};

xdg.configFile.hyprpanel = let
theme = if cfg.theme != null then builtins.fromJSON (builtins.readFile ../themes/${cfg.theme}.json) else {};
theme = if cfg.theme != "" then builtins.fromJSON (builtins.readFile ../themes/${cfg.theme}.json) else {};
flatSet = flattenAttrs (lib.attrsets.recursiveUpdate cfg.settings theme) "";
mergeSet = if cfg.layout == null then flatSet else flatSet // cfg.layout;
in {
Expand Down
2 changes: 1 addition & 1 deletion scripts/hyprpanel_launcher.sh.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export HYPRPANEL_DATADIR="@DATADIR@"
if [ "$#" -eq 0 ]; then
exec gjs -m "@DATADIR@/hyprpanel.js"
else
exec astal -i hyprpanel "$@"
exec astal -i hyprpanel "$*"
fi
145 changes: 99 additions & 46 deletions src/cli/commander/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ 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.
* Parses an input string into a command and its positional arguments.
*
* Expected command format:
* Expected 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.
Expand All @@ -19,23 +17,25 @@ export class CommandParser {
private registry: CommandRegistry;

/**
* Creates an instance of CommandParser.
* Constructs a CommandParser with the provided command registry.
*
* @param registry - The command registry to use.
* @param registry - The command registry containing available commands.
*/
constructor(registry: CommandRegistry) {
this.registry = registry;
}

/**
* Parses the input string into a ParsedCommand object.
* Parses the entire input string, returning the matching command and its arguments.
*
* @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.
* @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.
*/
parse(input: string): ParsedCommand {
const tokens = this.tokenize(input);

if (tokens.length === 0) {
throw new Error('No command provided.');
}
Expand All @@ -51,10 +51,10 @@ export class CommandParser {
}

/**
* Tokenizes the input string into an array of tokens.
* Splits the input string into tokens, respecting quotes.
*
* @param input - The input string to tokenize.
* @returns The array of tokens.
* @param input - The raw input string to break into tokens.
* @returns An array of tokens.
*/
private tokenize(input: string): string[] {
const regex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g;
Expand All @@ -63,78 +63,131 @@ export class CommandParser {
}

/**
* Strips quotes from the beginning and end of a string.
* Removes surrounding quotes from a single token, if they exist.
*
* @param str - The string to strip quotes from.
* @returns The string without quotes.
* @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 positional arguments for a command.
* Parses the array of tokens into arguments based on the command's argument definitions.
*
* @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.
* @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> = {};
const argDefs = command.args;

if (tokens.length > argDefs.length) {
throw new Error(`Too many arguments for command "${command.name}". Expected at most ${argDefs.length}.`);
}
let currentIndex = 0;

argDefs.forEach((argDef, index) => {
const value = tokens[index];
if (value === undefined) {
for (const argDef of command.args) {
if (currentIndex >= tokens.length) {
if (argDef.required) {
throw new Error(`Missing required argument: "${argDef.name}".`);
}
if (argDef.default !== undefined) {
args[argDef.name] = argDef.default;
}
return;
continue;
}

args[argDef.name] = this.convertType(value, argDef.type);
});
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;
}

/**
* Converts a string value to the specified type.
* 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);

// Once we've started and braceCount returns to 0, we assume the object is complete
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 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.
* @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',
): string | number | boolean | Record<string, unknown> {
private convertType(value: string, type: 'string' | 'number' | 'boolean' | 'object'): unknown {
switch (type) {
case 'number':
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;
}
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':
}
case 'object': {
try {
return JSON.parse(value);
} catch {
throw new Error(`Invalid JSON object: "${value}".`);
}
}
case 'string':
default:
return value;
Expand Down
31 changes: 31 additions & 0 deletions src/cli/commander/commands/utility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { errorHandler } from 'src/lib/utils';
import { Command } from '../../types';
import { execAsync, Gio, GLib } from 'astal';
import { checkDependencies } from './checkDependencies';
import { audioService } from 'src/lib/constants/services';

export const utilityCommands: Command[] = [
{
Expand Down Expand Up @@ -33,6 +34,36 @@ export const utilityCommands: Command[] = [
}
},
},
{
name: 'adjustVolume',
aliases: ['vol'],
description: 'Adjusts the volume of the default audio output device.',
category: 'Utility',
args: [
{
name: 'volume',
description: 'A positive or negative number to adjust the volume by.',
type: 'number',
required: true,
},
],
handler: (args: Record<string, unknown>): number => {
try {
const speaker = audioService.defaultSpeaker;
const volumeInput = Number(args['volume']) / 100;

if (options.menus.volume.raiseMaximumVolume.get()) {
speaker.set_volume(Math.min(speaker.volume + volumeInput, 1.5));
} else {
speaker.set_volume(Math.min(speaker.volume + volumeInput, 1));
}

return Math.round((speaker.volume + volumeInput) * 100);
} catch (error) {
errorHandler(error);
}
},
},
{
name: 'migrateConfig',
aliases: ['mcfg'],
Expand Down
61 changes: 37 additions & 24 deletions src/components/bar/modules/battery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/util
import Variable from 'astal/variable';
import { bind } from 'astal/binding.js';
import AstalBattery from 'gi://AstalBattery?version=0.1';
import { useHook } from 'src/lib/shared/hookHandler';
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers';
import { getBatteryIcon } from './helpers';

Expand Down Expand Up @@ -102,29 +101,43 @@ const BatteryLabel = (): BarBoxChild => {
boxClass: 'battery',
props: {
setup: (self: Astal.Button): void => {
useHook(self, options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.get());

const disconnectPrimary = onPrimaryClick(self, (clicked, event) => {
openMenu(clicked, event, 'energymenu');
});

const disconnectSecondary = onSecondaryClick(self, (clicked, event) => {
runAsyncCommand(rightClick.get(), { clicked, event });
});

const disconnectMiddle = onMiddleClick(self, (clicked, event) => {
runAsyncCommand(middleClick.get(), { clicked, event });
});

const disconnectScroll = onScroll(self, throttledHandler, scrollUp.get(), scrollDown.get());
return (): void => {
disconnectPrimary();
disconnectSecondary();
disconnectMiddle();
disconnectScroll();
};
});
let disconnectFunctions: (() => void)[] = [];

Variable.derive(
[
bind(rightClick),
bind(middleClick),
bind(scrollUp),
bind(scrollDown),
bind(options.bar.scrollSpeed),
],
() => {
disconnectFunctions.forEach((disconnect) => disconnect());
disconnectFunctions = [];

const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.get());

disconnectFunctions.push(
onPrimaryClick(self, (clicked, event) => {
openMenu(clicked, event, 'energymenu');
}),
);

disconnectFunctions.push(
onSecondaryClick(self, (clicked, event) => {
runAsyncCommand(rightClick.get(), { clicked, event });
}),
);

disconnectFunctions.push(
onMiddleClick(self, (clicked, event) => {
runAsyncCommand(middleClick.get(), { clicked, event });
}),
);

disconnectFunctions.push(onScroll(self, throttledHandler, scrollUp.get(), scrollDown.get()));
},
);
},
},
};
Expand Down
Loading

0 comments on commit 15a02fc

Please sign in to comment.