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[];
|
||||
}
|
||||
53
src/components/bar/exports.ts
Normal file
53
src/components/bar/exports.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Menu } from './modules/menu';
|
||||
import { Workspaces } from '../../components/bar/modules/workspaces/index';
|
||||
import { ClientTitle } from '../../components/bar/modules/window_title/index';
|
||||
import { Media } from '../../components/bar/modules/media/index';
|
||||
import { Notifications } from '../../components/bar/modules/notifications/index';
|
||||
import { Volume } from '../../components/bar/modules/volume/index';
|
||||
import { Network } from '../../components/bar/modules/network/index';
|
||||
import { Bluetooth } from '../../components/bar/modules/bluetooth/index';
|
||||
import { BatteryLabel } from '../../components/bar/modules/battery/index';
|
||||
import { Clock } from '../../components/bar/modules/clock/index';
|
||||
import { SysTray } from '../../components/bar/modules/systray/index';
|
||||
|
||||
// Custom Modules
|
||||
import { Ram } from '../../components/bar/modules/ram/index';
|
||||
import { Cpu } from '../../components/bar/modules/cpu/index';
|
||||
import { CpuTemp } from '../../components/bar/modules/cputemp/index';
|
||||
import { Storage } from '../../components/bar/modules/storage/index';
|
||||
import { Netstat } from '../../components/bar/modules/netstat/index';
|
||||
import { KbInput } from '../../components/bar/modules/kblayout/index';
|
||||
import { Updates } from '../../components/bar/modules/updates/index';
|
||||
import { Submap } from '../../components/bar/modules/submap/index';
|
||||
import { Weather } from '../../components/bar/modules/weather/index';
|
||||
import { Power } from '../../components/bar/modules/power/index';
|
||||
import { Hyprsunset } from '../../components/bar/modules/hyprsunset/index';
|
||||
import { Hypridle } from '../../components/bar/modules/hypridle/index';
|
||||
|
||||
export {
|
||||
Menu,
|
||||
Workspaces,
|
||||
ClientTitle,
|
||||
Media,
|
||||
Notifications,
|
||||
Volume,
|
||||
Network,
|
||||
Bluetooth,
|
||||
BatteryLabel,
|
||||
Clock,
|
||||
SysTray,
|
||||
|
||||
// Custom Modules
|
||||
Ram,
|
||||
Cpu,
|
||||
CpuTemp,
|
||||
Storage,
|
||||
Netstat,
|
||||
KbInput,
|
||||
Updates,
|
||||
Submap,
|
||||
Weather,
|
||||
Power,
|
||||
Hyprsunset,
|
||||
Hypridle,
|
||||
};
|
||||
169
src/components/bar/index.tsx
Normal file
169
src/components/bar/index.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
Menu,
|
||||
Workspaces,
|
||||
ClientTitle,
|
||||
Media,
|
||||
Notifications,
|
||||
Volume,
|
||||
Network,
|
||||
Bluetooth,
|
||||
BatteryLabel,
|
||||
Clock,
|
||||
SysTray,
|
||||
|
||||
// Custom Modules
|
||||
Ram,
|
||||
Cpu,
|
||||
CpuTemp,
|
||||
Storage,
|
||||
Netstat,
|
||||
KbInput,
|
||||
Updates,
|
||||
Submap,
|
||||
Weather,
|
||||
Power,
|
||||
Hyprsunset,
|
||||
Hypridle,
|
||||
} from './exports';
|
||||
|
||||
import { WidgetContainer } from './shared/WidgetContainer';
|
||||
import options from 'src/options';
|
||||
import { App, Gtk } from 'astal/gtk3/index';
|
||||
|
||||
import Astal from 'gi://Astal?version=3.0';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { gdkMonitorIdToHyprlandId, getLayoutForMonitor, isLayoutEmpty } from './utils/monitors';
|
||||
|
||||
const { layouts } = options.bar;
|
||||
const { location } = options.theme.bar;
|
||||
const { location: borderLocation } = options.theme.bar.border;
|
||||
|
||||
const widget = {
|
||||
battery: (): JSX.Element => WidgetContainer(BatteryLabel()),
|
||||
dashboard: (): JSX.Element => WidgetContainer(Menu()),
|
||||
workspaces: (monitor: number): JSX.Element => WidgetContainer(Workspaces(monitor)),
|
||||
windowtitle: (): JSX.Element => WidgetContainer(ClientTitle()),
|
||||
media: (): JSX.Element => WidgetContainer(Media()),
|
||||
notifications: (): JSX.Element => WidgetContainer(Notifications()),
|
||||
volume: (): JSX.Element => WidgetContainer(Volume()),
|
||||
network: (): JSX.Element => WidgetContainer(Network()),
|
||||
bluetooth: (): JSX.Element => WidgetContainer(Bluetooth()),
|
||||
clock: (): JSX.Element => WidgetContainer(Clock()),
|
||||
systray: (): JSX.Element => WidgetContainer(SysTray()),
|
||||
ram: (): JSX.Element => WidgetContainer(Ram()),
|
||||
cpu: (): JSX.Element => WidgetContainer(Cpu()),
|
||||
cputemp: (): JSX.Element => WidgetContainer(CpuTemp()),
|
||||
storage: (): JSX.Element => WidgetContainer(Storage()),
|
||||
netstat: (): JSX.Element => WidgetContainer(Netstat()),
|
||||
kbinput: (): JSX.Element => WidgetContainer(KbInput()),
|
||||
updates: (): JSX.Element => WidgetContainer(Updates()),
|
||||
submap: (): JSX.Element => WidgetContainer(Submap()),
|
||||
weather: (): JSX.Element => WidgetContainer(Weather()),
|
||||
power: (): JSX.Element => WidgetContainer(Power()),
|
||||
hyprsunset: (): JSX.Element => WidgetContainer(Hyprsunset()),
|
||||
hypridle: (): JSX.Element => WidgetContainer(Hypridle()),
|
||||
};
|
||||
|
||||
export const Bar = (() => {
|
||||
const usedHyprlandMonitors = new Set<number>();
|
||||
|
||||
return (monitor: number): JSX.Element => {
|
||||
const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors);
|
||||
|
||||
const computeVisibility = bind(layouts).as(() => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get());
|
||||
return !isLayoutEmpty(foundLayout);
|
||||
});
|
||||
|
||||
const computeAnchor = bind(location).as((loc) => {
|
||||
if (loc === 'bottom') {
|
||||
return Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
||||
}
|
||||
|
||||
return Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
||||
});
|
||||
|
||||
const computeLayer = Variable.derive([bind(options.theme.bar.layer), bind(options.tear)], (barLayer, tear) => {
|
||||
if (tear && barLayer === 'overlay') {
|
||||
return Astal.Layer.TOP;
|
||||
}
|
||||
const layerMap = {
|
||||
overlay: Astal.Layer.OVERLAY,
|
||||
top: Astal.Layer.TOP,
|
||||
bottom: Astal.Layer.BOTTOM,
|
||||
background: Astal.Layer.BACKGROUND,
|
||||
};
|
||||
|
||||
return layerMap[barLayer];
|
||||
});
|
||||
|
||||
const computeBorderLocation = bind(borderLocation).as((brdrLcn) =>
|
||||
brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel',
|
||||
);
|
||||
|
||||
const leftBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||
|
||||
return foundLayout.left
|
||||
.filter((mod) => Object.keys(widget).includes(mod))
|
||||
.map((w) => widget[w](hyprlandMonitor));
|
||||
});
|
||||
const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||
|
||||
return foundLayout.middle
|
||||
.filter((mod) => Object.keys(widget).includes(mod))
|
||||
.map((w) => widget[w](hyprlandMonitor));
|
||||
});
|
||||
const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||
|
||||
return foundLayout.right
|
||||
.filter((mod) => Object.keys(widget).includes(mod))
|
||||
.map((w) => widget[w](hyprlandMonitor));
|
||||
});
|
||||
|
||||
return (
|
||||
<window
|
||||
name={`bar-${hyprlandMonitor}`}
|
||||
namespace={`bar-${hyprlandMonitor}`}
|
||||
className={'bar'}
|
||||
application={App}
|
||||
monitor={monitor}
|
||||
visible={computeVisibility}
|
||||
anchor={computeAnchor}
|
||||
layer={computeLayer()}
|
||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||
onDestroy={() => {
|
||||
computeLayer.drop();
|
||||
leftBinding.drop();
|
||||
middleBinding.drop();
|
||||
rightBinding.drop();
|
||||
}}
|
||||
>
|
||||
<box className={'bar-panel-container'}>
|
||||
<centerbox
|
||||
css={'padding: 1px;'}
|
||||
hexpand
|
||||
className={computeBorderLocation}
|
||||
startWidget={
|
||||
<box className={'box-left'} hexpand>
|
||||
{leftBinding()}
|
||||
</box>
|
||||
}
|
||||
centerWidget={
|
||||
<box className={'box-center'} halign={Gtk.Align.CENTER}>
|
||||
{middleBinding()}
|
||||
</box>
|
||||
}
|
||||
endWidget={
|
||||
<box className={'box-right'} halign={Gtk.Align.END}>
|
||||
{rightBinding()}
|
||||
</box>
|
||||
}
|
||||
/>
|
||||
</box>
|
||||
</window>
|
||||
);
|
||||
};
|
||||
})();
|
||||
51
src/components/bar/modules/battery/helpers/index.ts
Normal file
51
src/components/bar/modules/battery/helpers/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { BatteryIconKeys, BatteryIcons } from 'src/lib/types/battery';
|
||||
|
||||
const batteryIcons: BatteryIcons = {
|
||||
0: '',
|
||||
10: '',
|
||||
20: '',
|
||||
30: '',
|
||||
40: '',
|
||||
50: '',
|
||||
60: '',
|
||||
70: '',
|
||||
80: '',
|
||||
90: '',
|
||||
100: '',
|
||||
};
|
||||
|
||||
const batteryIconsCharging: BatteryIcons = {
|
||||
0: '',
|
||||
10: '',
|
||||
20: '',
|
||||
30: '',
|
||||
40: '',
|
||||
50: '',
|
||||
60: '',
|
||||
70: '',
|
||||
80: '',
|
||||
90: '',
|
||||
100: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the appropriate battery icon based on the battery percentage and charging status.
|
||||
*
|
||||
* This function returns the corresponding battery icon based on the provided battery percentage, charging status, and whether the battery is fully charged.
|
||||
* It uses predefined mappings for battery icons and charging battery icons.
|
||||
*
|
||||
* @param percentage The current battery percentage.
|
||||
* @param charging A boolean indicating whether the battery is currently charging.
|
||||
* @param isCharged A boolean indicating whether the battery is fully charged.
|
||||
*
|
||||
* @returns The corresponding battery icon as a string.
|
||||
*/
|
||||
export const getBatteryIcon = (percentage: number, charging: boolean, isCharged: boolean): string => {
|
||||
if (isCharged) {
|
||||
return '';
|
||||
}
|
||||
const percentages: BatteryIconKeys[] = [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0];
|
||||
const foundPercentage = percentages.find((threshold) => threshold <= percentage) ?? 100;
|
||||
|
||||
return charging ? batteryIconsCharging[foundPercentage] : batteryIcons[foundPercentage];
|
||||
};
|
||||
133
src/components/bar/modules/battery/index.tsx
Normal file
133
src/components/bar/modules/battery/index.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { batteryService } from 'src/lib/constants/services.js';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { openMenu } from '../../utils/menu';
|
||||
import options from 'src/options';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
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';
|
||||
|
||||
const { label: show_label, rightClick, middleClick, scrollUp, scrollDown, hideLabelWhenFull } = options.bar.battery;
|
||||
|
||||
const BatteryLabel = (): BarBoxChild => {
|
||||
const batIcon = Variable.derive(
|
||||
[bind(batteryService, 'percentage'), bind(batteryService, 'charging'), bind(batteryService, 'state')],
|
||||
(batPercent: number, batCharging: boolean, state: AstalBattery.State) => {
|
||||
const batCharged = state === AstalBattery.State.FULLY_CHARGED;
|
||||
|
||||
return getBatteryIcon(Math.floor(batPercent * 100), batCharging, batCharged);
|
||||
},
|
||||
);
|
||||
|
||||
const formatTime = (seconds: number): Record<string, number> => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return { hours, minutes };
|
||||
};
|
||||
|
||||
const generateTooltip = (timeSeconds: number, isCharging: boolean, isCharged: boolean): string => {
|
||||
if (isCharged === true) {
|
||||
return 'Full';
|
||||
}
|
||||
|
||||
const { hours, minutes } = formatTime(timeSeconds);
|
||||
if (isCharging) {
|
||||
return `Time to full: ${hours} h ${minutes} min`;
|
||||
} else {
|
||||
return `Time to empty: ${hours} h ${minutes} min`;
|
||||
}
|
||||
};
|
||||
|
||||
const componentClassName = Variable.derive(
|
||||
[bind(options.theme.bar.buttons.style), bind(show_label)],
|
||||
(style, showLabel) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `battery-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
);
|
||||
|
||||
const componentTooltip = Variable.derive(
|
||||
[bind(batteryService, 'charging'), bind(batteryService, 'timeToFull'), bind(batteryService, 'timeToEmpty')],
|
||||
(isCharging, timeToFull, timeToEmpty) => {
|
||||
const timeRemaining = isCharging ? timeToFull : timeToEmpty;
|
||||
return generateTooltip(timeRemaining, isCharging, Math.floor(batteryService.percentage * 100) === 100);
|
||||
},
|
||||
);
|
||||
|
||||
const componentChildren = Variable.derive(
|
||||
[bind(show_label), bind(batteryService, 'percentage'), bind(hideLabelWhenFull)],
|
||||
(showLabel, percentage, hideLabelWhenFull) => {
|
||||
const isCharged = Math.round(percentage) === 100;
|
||||
|
||||
const icon = <label className={'bar-button-icon battery txt-icon'} label={batIcon()} />;
|
||||
const label = <label className={'bar-button-label battery'} label={`${Math.floor(percentage * 100)}%`} />;
|
||||
|
||||
const children = [icon];
|
||||
|
||||
if (showLabel && !(isCharged && hideLabelWhenFull)) {
|
||||
children.push(label);
|
||||
}
|
||||
|
||||
return children;
|
||||
},
|
||||
);
|
||||
|
||||
const component = (
|
||||
<box
|
||||
className={componentClassName()}
|
||||
tooltipText={componentTooltip()}
|
||||
onDestroy={() => {
|
||||
batIcon.drop();
|
||||
componentClassName.drop();
|
||||
componentTooltip.drop();
|
||||
componentChildren.drop();
|
||||
}}
|
||||
>
|
||||
{componentChildren()}
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
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();
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { BatteryLabel };
|
||||
93
src/components/bar/modules/bluetooth/index.tsx
Normal file
93
src/components/bar/modules/bluetooth/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { bluetoothService } from 'src/lib/constants/services.js';
|
||||
import options from 'src/options.js';
|
||||
import { openMenu } from '../../utils/menu.js';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import Variable from 'astal/variable.js';
|
||||
import { useHook } from 'src/lib/shared/hookHandler.js';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { rightClick, middleClick, scrollDown, scrollUp } = options.bar.bluetooth;
|
||||
|
||||
const Bluetooth = (): BarBoxChild => {
|
||||
const btIcon = (isPowered: boolean): JSX.Element => (
|
||||
<label className={'bar-button-icon bluetooth txt-icon bar'} label={isPowered ? '' : ''} />
|
||||
);
|
||||
|
||||
const btText = (isPowered: boolean, devices: AstalBluetooth.Device[]): JSX.Element => {
|
||||
const connectDevices = devices.filter((device) => device.connected);
|
||||
|
||||
const label =
|
||||
isPowered && connectDevices.length ? ` Connected (${connectDevices.length})` : isPowered ? 'On' : 'Off';
|
||||
|
||||
return <label label={label} className={'bar-button-label bluetooth'} />;
|
||||
};
|
||||
|
||||
const componentClassName = Variable.derive(
|
||||
[options.theme.bar.buttons.style, options.bar.volume.label],
|
||||
(style, showLabel) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `bluetooth-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
);
|
||||
|
||||
const componentBinding = Variable.derive(
|
||||
[bind(options.bar.volume.label), bind(bluetoothService, 'isPowered'), bind(bluetoothService, 'devices')],
|
||||
(showLabel: boolean, isPowered: boolean, devices: AstalBluetooth.Device[]): JSX.Element[] => {
|
||||
if (showLabel) {
|
||||
return [btIcon(isPowered), btText(isPowered, devices)];
|
||||
}
|
||||
return [btIcon(isPowered)];
|
||||
},
|
||||
);
|
||||
|
||||
const component = <box className={componentClassName()}>{componentBinding()}</box>;
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
boxClass: 'bluetooth',
|
||||
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, 'bluetoothmenu');
|
||||
});
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
},
|
||||
onDestroy: (): void => {
|
||||
componentClassName.drop();
|
||||
componentBinding.drop();
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Bluetooth };
|
||||
89
src/components/bar/modules/clock/index.tsx
Normal file
89
src/components/bar/modules/clock/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { openMenu } from '../../utils/menu';
|
||||
import options from 'src/options';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { useHook } from 'src/lib/shared/hookHandler';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { systemTime } from 'src/globals/time';
|
||||
|
||||
const { format, icon, showIcon, showTime, rightClick, middleClick, scrollUp, scrollDown } = options.bar.clock;
|
||||
const { style } = options.theme.bar.buttons;
|
||||
|
||||
const time = Variable.derive([systemTime, format], (c, f) => c.format(f) || '');
|
||||
|
||||
const Clock = (): BarBoxChild => {
|
||||
const clockTime = <label className={'bar-button-label clock bar'} label={bind(time)} />;
|
||||
const clockIcon = <label className={'bar-button-icon clock txt-icon bar'} label={bind(icon)} />;
|
||||
|
||||
const componentClassName = Variable.derive(
|
||||
[bind(style), bind(showIcon), bind(showTime)],
|
||||
(btnStyle, shwIcn, shwLbl) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `clock-container ${styleMap[btnStyle]} ${!shwLbl ? 'no-label' : ''} ${!shwIcn ? 'no-icon' : ''}`;
|
||||
},
|
||||
);
|
||||
|
||||
const componentChildren = Variable.derive([bind(showIcon), bind(showTime)], (shIcn, shTm) => {
|
||||
if (shIcn && !shTm) {
|
||||
return [clockIcon];
|
||||
} else if (shTm && !shIcn) {
|
||||
return [clockTime];
|
||||
}
|
||||
return [clockIcon, clockTime];
|
||||
});
|
||||
|
||||
const component = (
|
||||
<box
|
||||
className={componentClassName()}
|
||||
onDestroy={() => {
|
||||
componentClassName.drop();
|
||||
componentChildren.drop();
|
||||
}}
|
||||
>
|
||||
{componentChildren()}
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
boxClass: 'clock',
|
||||
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, 'calendarmenu');
|
||||
});
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Clock };
|
||||
27
src/components/bar/modules/cpu/helpers/index.ts
Normal file
27
src/components/bar/modules/cpu/helpers/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import GTop from 'gi://GTop';
|
||||
|
||||
let previousCpuData = new GTop.glibtop_cpu();
|
||||
GTop.glibtop_get_cpu(previousCpuData);
|
||||
|
||||
/**
|
||||
* Computes the CPU usage percentage.
|
||||
*
|
||||
* This function calculates the CPU usage percentage by comparing the current CPU data with the previous CPU data.
|
||||
* It calculates the differences in total and idle CPU times and uses these differences to compute the usage percentage.
|
||||
*
|
||||
* @returns The CPU usage percentage as a number.
|
||||
*/
|
||||
export const computeCPU = (): number => {
|
||||
const currentCpuData = new GTop.glibtop_cpu();
|
||||
GTop.glibtop_get_cpu(currentCpuData);
|
||||
|
||||
// Calculate the differences from the previous to current data
|
||||
const totalDiff = currentCpuData.total - previousCpuData.total;
|
||||
const idleDiff = currentCpuData.idle - previousCpuData.idle;
|
||||
|
||||
const cpuUsagePercentage = totalDiff > 0 ? ((totalDiff - idleDiff) / totalDiff) * 100 : 0;
|
||||
|
||||
previousCpuData = currentCpuData;
|
||||
|
||||
return cpuUsagePercentage;
|
||||
};
|
||||
61
src/components/bar/modules/cpu/index.tsx
Normal file
61
src/components/bar/modules/cpu/index.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Module } from '../../shared/Module';
|
||||
import options from 'src/options';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { computeCPU } from './helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { label, round, leftClick, rightClick, middleClick, scrollUp, scrollDown, pollingInterval, icon } =
|
||||
options.bar.customModules.cpu;
|
||||
|
||||
export const cpuUsage = Variable(0);
|
||||
|
||||
const cpuPoller = new FunctionPoller<number, []>(cpuUsage, [bind(round)], bind(pollingInterval), computeCPU);
|
||||
|
||||
cpuPoller.initialize('cpu');
|
||||
|
||||
export const Cpu = (): BarBoxChild => {
|
||||
const renderLabel = (cpuUsg: number, rnd: boolean): string => {
|
||||
return rnd ? `${Math.round(cpuUsg)}%` : `${cpuUsg.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
const labelBinding = Variable.derive([bind(cpuUsage), bind(round)], (cpuUsg, rnd) => {
|
||||
return renderLabel(cpuUsg, rnd);
|
||||
});
|
||||
|
||||
const cpuModule = Module({
|
||||
textIcon: bind(icon),
|
||||
label: labelBinding(),
|
||||
tooltipText: 'CPU',
|
||||
boxClass: 'cpu',
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
labelBinding.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return cpuModule;
|
||||
};
|
||||
44
src/components/bar/modules/cputemp/helpers/index.ts
Normal file
44
src/components/bar/modules/cputemp/helpers/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Variable } from 'astal';
|
||||
import GLib from 'gi://GLib?version=2.0';
|
||||
import { convertCelsiusToFahrenheit } from 'src/globals/weather';
|
||||
import { UnitType } from 'src/lib/types/weather';
|
||||
import options from 'src/options';
|
||||
const { sensor } = options.bar.customModules.cpuTemp;
|
||||
|
||||
/**
|
||||
* Retrieves the current CPU temperature.
|
||||
*
|
||||
* This function reads the CPU temperature from the specified sensor file and converts it to the desired unit (Celsius or Fahrenheit).
|
||||
* It also handles rounding the temperature value based on the provided `round` variable.
|
||||
*
|
||||
* @param round A Variable<boolean> indicating whether to round the temperature value.
|
||||
* @param unit A Variable<UnitType> indicating the desired unit for the temperature (Celsius or Fahrenheit).
|
||||
*
|
||||
* @returns The current CPU temperature as a number. Returns 0 if an error occurs or the sensor file is empty.
|
||||
*/
|
||||
export const getCPUTemperature = (round: Variable<boolean>, unit: Variable<UnitType>): number => {
|
||||
try {
|
||||
if (sensor.get().length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const [success, tempInfoBytes] = GLib.file_get_contents(sensor.get());
|
||||
const tempInfo = new TextDecoder('utf-8').decode(tempInfoBytes);
|
||||
|
||||
if (!success || !tempInfoBytes) {
|
||||
console.error(`Failed to read ${sensor.get()} or file content is null.`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
let decimalTemp = parseInt(tempInfo, 10) / 1000;
|
||||
|
||||
if (unit.get() === 'imperial') {
|
||||
decimalTemp = convertCelsiusToFahrenheit(decimalTemp);
|
||||
}
|
||||
|
||||
return round.get() ? Math.round(decimalTemp) : parseFloat(decimalTemp.toFixed(2));
|
||||
} catch (error) {
|
||||
console.error('Error calculating CPU Temp:', error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
81
src/components/bar/modules/cputemp/index.tsx
Normal file
81
src/components/bar/modules/cputemp/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { getCPUTemperature } from './helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { UnitType } from 'src/lib/types/weather';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const {
|
||||
label,
|
||||
sensor,
|
||||
round,
|
||||
showUnit,
|
||||
unit,
|
||||
leftClick,
|
||||
rightClick,
|
||||
middleClick,
|
||||
scrollUp,
|
||||
scrollDown,
|
||||
pollingInterval,
|
||||
icon,
|
||||
} = options.bar.customModules.cpuTemp;
|
||||
|
||||
export const cpuTemp = Variable(0);
|
||||
|
||||
const cpuTempPoller = new FunctionPoller<number, [Variable<boolean>, Variable<UnitType>]>(
|
||||
cpuTemp,
|
||||
[bind(sensor), bind(round), bind(unit)],
|
||||
bind(pollingInterval),
|
||||
getCPUTemperature,
|
||||
round,
|
||||
unit,
|
||||
);
|
||||
|
||||
cpuTempPoller.initialize('cputemp');
|
||||
|
||||
export const CpuTemp = (): BarBoxChild => {
|
||||
const labelBinding = Variable.derive(
|
||||
[bind(cpuTemp), bind(unit), bind(showUnit), bind(round)],
|
||||
(cpuTmp, tempUnit, shwUnit) => {
|
||||
const unitLabel = tempUnit === 'imperial' ? 'F' : 'C';
|
||||
const unit = shwUnit ? ` ${unitLabel}` : '';
|
||||
return `${cpuTmp.toString()}°${unit}`;
|
||||
},
|
||||
);
|
||||
const cpuTempModule = Module({
|
||||
textIcon: bind(icon),
|
||||
label: labelBinding(),
|
||||
tooltipText: 'CPU Temperature',
|
||||
boxClass: 'cpu-temp',
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
labelBinding.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return cpuTempModule;
|
||||
};
|
||||
55
src/components/bar/modules/hypridle/helpers/index.ts
Normal file
55
src/components/bar/modules/hypridle/helpers/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { execAsync, Variable } from 'astal';
|
||||
|
||||
/**
|
||||
* Checks if the hypridle process is active.
|
||||
*
|
||||
* This command checks if the hypridle process is currently running by using the `pgrep` command.
|
||||
* It returns 'yes' if the process is found and 'no' otherwise.
|
||||
*/
|
||||
export const isActiveCommand = `bash -c "pgrep -x 'hypridle' &>/dev/null && echo 'yes' || echo 'no'"`;
|
||||
|
||||
/**
|
||||
* A variable to track the active state of the hypridle process.
|
||||
*/
|
||||
export const isActive = Variable(false);
|
||||
|
||||
/**
|
||||
* Updates the active state of the hypridle process.
|
||||
*
|
||||
* This function checks if the hypridle process is currently running and updates the `isActive` variable accordingly.
|
||||
*
|
||||
* @param isActive A Variable<boolean> that tracks the active state of the hypridle process.
|
||||
*/
|
||||
const updateIsActive = (isActive: Variable<boolean>): void => {
|
||||
execAsync(isActiveCommand).then((res) => {
|
||||
isActive.set(res === 'yes');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the hypridle process on or off based on its current state.
|
||||
*
|
||||
* This function checks if the hypridle process is currently running. If it is not running, it starts the process.
|
||||
* If it is running, it stops the process. The active state is updated accordingly.
|
||||
*
|
||||
* @param isActive A Variable<boolean> that tracks the active state of the hypridle process.
|
||||
*/
|
||||
export const toggleIdle = (isActive: Variable<boolean>): void => {
|
||||
execAsync(isActiveCommand).then((res) => {
|
||||
const toggleIdleCommand =
|
||||
res === 'no' ? `bash -c "nohup hypridle > /dev/null 2>&1 &"` : `bash -c "pkill hypridle"`;
|
||||
|
||||
execAsync(toggleIdleCommand).then(() => updateIsActive(isActive));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the current status of the hypridle process and updates the active state.
|
||||
*
|
||||
* This function checks if the hypridle process is currently running and updates the `isActive` variable accordingly.
|
||||
*/
|
||||
export const checkIdleStatus = (): undefined => {
|
||||
execAsync(isActiveCommand).then((res) => {
|
||||
isActive.set(res === 'yes');
|
||||
});
|
||||
};
|
||||
68
src/components/bar/modules/hypridle/index.tsx
Normal file
68
src/components/bar/modules/hypridle/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler, throttleInput } from '../../utils/helpers';
|
||||
import { checkIdleStatus, isActive, toggleIdle } from './helpers';
|
||||
import { FunctionPoller } from '../../../../lib/poller/FunctionPoller';
|
||||
import Variable from 'astal/variable';
|
||||
import { bind } from 'astal';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { label, pollingInterval, onIcon, offIcon, onLabel, offLabel, rightClick, middleClick, scrollUp, scrollDown } =
|
||||
options.bar.customModules.hypridle;
|
||||
|
||||
const dummyVar = Variable(undefined);
|
||||
|
||||
checkIdleStatus();
|
||||
|
||||
const idleStatusPoller = new FunctionPoller<undefined, []>(dummyVar, [], bind(pollingInterval), checkIdleStatus);
|
||||
|
||||
idleStatusPoller.initialize('hypridle');
|
||||
|
||||
const throttledToggleIdle = throttleInput(() => toggleIdle(isActive), 1000);
|
||||
|
||||
export const Hypridle = (): BarBoxChild => {
|
||||
const iconBinding = Variable.derive([bind(isActive), bind(onIcon), bind(offIcon)], (active, onIcn, offIcn) => {
|
||||
return active ? onIcn : offIcn;
|
||||
});
|
||||
const labelBinding = Variable.derive([bind(isActive), bind(onLabel), bind(offLabel)], (active, onLbl, offLbl) => {
|
||||
return active ? onLbl : offLbl;
|
||||
});
|
||||
|
||||
const hypridleModule = Module({
|
||||
textIcon: iconBinding(),
|
||||
tooltipText: bind(isActive).as((active) => `Hypridle ${active ? 'enabled' : 'disabled'}`),
|
||||
boxClass: 'hypridle',
|
||||
label: labelBinding(),
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
fn: () => {
|
||||
throttledToggleIdle();
|
||||
},
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
iconBinding.drop();
|
||||
labelBinding.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return hypridleModule;
|
||||
};
|
||||
54
src/components/bar/modules/hyprsunset/helpers/index.ts
Normal file
54
src/components/bar/modules/hyprsunset/helpers/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { execAsync, Variable } from 'astal';
|
||||
import options from 'src/options';
|
||||
|
||||
const { temperature } = options.bar.customModules.hyprsunset;
|
||||
|
||||
/**
|
||||
* Checks if the hyprsunset process is active.
|
||||
*
|
||||
* This command checks if the hyprsunset process is currently running by using the `pgrep` command.
|
||||
* It returns 'yes' if the process is found and 'no' otherwise.
|
||||
*/
|
||||
export const isActiveCommand = `bash -c "pgrep -x 'hyprsunset' > /dev/null && echo 'yes' || echo 'no'"`;
|
||||
|
||||
/**
|
||||
* A variable to track the active state of the hyprsunset process.
|
||||
*/
|
||||
export const isActive = Variable(false);
|
||||
|
||||
/**
|
||||
* Toggles the hyprsunset process on or off based on its current state.
|
||||
*
|
||||
* This function checks if the hyprsunset process is currently running. If it is not running, it starts the process with the specified temperature.
|
||||
* If it is running, it stops the process. The active state is updated accordingly.
|
||||
*
|
||||
* @param isActive A Variable<boolean> that tracks the active state of the hyprsunset process.
|
||||
*/
|
||||
export const toggleSunset = (isActive: Variable<boolean>): void => {
|
||||
execAsync(isActiveCommand).then((res) => {
|
||||
if (res === 'no') {
|
||||
execAsync(`bash -c "nohup hyprsunset -t ${temperature.get()} > /dev/null 2>&1 &"`).then(() => {
|
||||
execAsync(isActiveCommand).then((res) => {
|
||||
isActive.set(res === 'yes');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
execAsync(`bash -c "pkill hyprsunset "`).then(() => {
|
||||
execAsync(isActiveCommand).then((res) => {
|
||||
isActive.set(res === 'yes');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the current status of the hyprsunset process and updates the active state.
|
||||
*
|
||||
* This function checks if the hyprsunset process is currently running and updates the `isActive` variable accordingly.
|
||||
*/
|
||||
export const checkSunsetStatus = (): undefined => {
|
||||
execAsync(isActiveCommand).then((res) => {
|
||||
isActive.set(res === 'yes');
|
||||
});
|
||||
};
|
||||
84
src/components/bar/modules/hyprsunset/index.tsx
Normal file
84
src/components/bar/modules/hyprsunset/index.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler, throttleInput } from 'src/components/bar/utils/helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { checkSunsetStatus, isActive, toggleSunset } from './helpers';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const {
|
||||
label,
|
||||
pollingInterval,
|
||||
onIcon,
|
||||
offIcon,
|
||||
onLabel,
|
||||
offLabel,
|
||||
rightClick,
|
||||
middleClick,
|
||||
scrollUp,
|
||||
scrollDown,
|
||||
temperature,
|
||||
} = options.bar.customModules.hyprsunset;
|
||||
|
||||
const dummyVar = Variable(undefined);
|
||||
|
||||
checkSunsetStatus();
|
||||
|
||||
const sunsetPoller = new FunctionPoller<undefined, []>(dummyVar, [], bind(pollingInterval), checkSunsetStatus);
|
||||
|
||||
sunsetPoller.initialize('hyprsunset');
|
||||
|
||||
const throttledToggleSunset = throttleInput(() => toggleSunset(isActive), 1000);
|
||||
|
||||
export const Hyprsunset = (): BarBoxChild => {
|
||||
const iconBinding = Variable.derive([bind(isActive), bind(onIcon), bind(offIcon)], (active, onIcn, offIcn) => {
|
||||
return active ? onIcn : offIcn;
|
||||
});
|
||||
|
||||
const tooltipBinding = Variable.derive([isActive, temperature], (active, temp) => {
|
||||
return `Hyprsunset ${active ? 'enabled' : 'disabled'}\nTemperature: ${temp}`;
|
||||
});
|
||||
|
||||
const labelBinding = Variable.derive([bind(isActive), bind(onLabel), bind(offLabel)], (active, onLbl, offLbl) => {
|
||||
return active ? onLbl : offLbl;
|
||||
});
|
||||
|
||||
const hyprsunsetModule = Module({
|
||||
textIcon: iconBinding(),
|
||||
tooltipText: tooltipBinding(),
|
||||
boxClass: 'hyprsunset',
|
||||
label: labelBinding(),
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
fn: () => {
|
||||
throttledToggleSunset();
|
||||
},
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
iconBinding.drop();
|
||||
tooltipBinding.drop();
|
||||
labelBinding.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return hyprsunsetModule;
|
||||
};
|
||||
39
src/components/bar/modules/kblayout/helpers/index.ts
Normal file
39
src/components/bar/modules/kblayout/helpers/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
HyprctlDeviceLayout,
|
||||
HyprctlKeyboard,
|
||||
KbLabelType,
|
||||
LayoutKeys,
|
||||
LayoutValues,
|
||||
} from 'src/lib/types/customModules/kbLayout';
|
||||
import { layoutMap } from './layouts';
|
||||
|
||||
/**
|
||||
* Retrieves the keyboard layout from a given JSON string and format.
|
||||
*
|
||||
* This function parses the provided JSON string to extract the keyboard layout information.
|
||||
* It returns the layout in the specified format, either as a code or a human-readable string.
|
||||
*
|
||||
* @param obj The JSON string containing the keyboard layout information.
|
||||
* @param format The format in which to return the layout, either 'code' or 'label'.
|
||||
*
|
||||
* @returns The keyboard layout in the specified format. If no keyboards are found, returns 'Unknown' or 'Unknown Layout'.
|
||||
*/
|
||||
export const getKeyboardLayout = (obj: string, format: KbLabelType): LayoutKeys | LayoutValues => {
|
||||
const hyprctlDevices: HyprctlDeviceLayout = JSON.parse(obj);
|
||||
const keyboards = hyprctlDevices['keyboards'];
|
||||
|
||||
if (keyboards.length === 0) {
|
||||
return format === 'code' ? 'Unknown' : 'Unknown Layout';
|
||||
}
|
||||
|
||||
let mainKb = keyboards.find((kb: HyprctlKeyboard) => kb.main);
|
||||
|
||||
if (!mainKb) {
|
||||
mainKb = keyboards[keyboards.length - 1];
|
||||
}
|
||||
|
||||
const layout: LayoutKeys = mainKb['active_keymap'] as LayoutKeys;
|
||||
const foundLayout: LayoutValues = layoutMap[layout];
|
||||
|
||||
return format === 'code' ? foundLayout || layout : layout;
|
||||
};
|
||||
587
src/components/bar/modules/kblayout/helpers/layouts.ts
Normal file
587
src/components/bar/modules/kblayout/helpers/layouts.ts
Normal file
@@ -0,0 +1,587 @@
|
||||
import { LayoutKeys, LayoutValues } from 'src/lib/types/customModules/kbLayout';
|
||||
|
||||
export const layoutMap: Record<LayoutKeys, LayoutValues> = {
|
||||
'Abkhazian (Russia)': 'RU (Ab)',
|
||||
Akan: 'GH (Akan)',
|
||||
Albanian: 'AL',
|
||||
'Albanian (Plisi)': 'AL (Plisi)',
|
||||
'Albanian (Veqilharxhi)': 'AL (Veqilharxhi)',
|
||||
Amharic: 'ET',
|
||||
Arabic: 'ARA',
|
||||
'Arabic (Algeria)': 'DZ (Ar)',
|
||||
'Arabic (AZERTY, Eastern Arabic numerals)': 'ARA (Azerty Digits)',
|
||||
'Arabic (AZERTY)': 'ARA (Azerty)',
|
||||
'Arabic (Buckwalter)': 'ARA (Buckwalter)',
|
||||
'Arabic (Eastern Arabic numerals)': 'ARA (Digits)',
|
||||
'Arabic (Macintosh)': 'ARA (Mac)',
|
||||
'Arabic (Morocco)': 'MA',
|
||||
'Arabic (OLPC)': 'ARA (Olpc)',
|
||||
'Arabic (Pakistan)': 'PK (Ara)',
|
||||
'Arabic (QWERTY, Eastern Arabic numerals)': 'ARA (Qwerty Digits)',
|
||||
'Arabic (QWERTY)': 'ARA (Qwerty)',
|
||||
'Arabic (Syria)': 'SY',
|
||||
Armenian: 'AM',
|
||||
'Armenian (alt. eastern)': 'AM (Eastern-Alt)',
|
||||
'Armenian (alt. phonetic)': 'AM (Phonetic-Alt)',
|
||||
'Armenian (eastern)': 'AM (Eastern)',
|
||||
'Armenian (phonetic)': 'AM (Phonetic)',
|
||||
'Armenian (western)': 'AM (Western)',
|
||||
'Asturian (Spain, with bottom-dot H and L)': 'ES (Ast)',
|
||||
Avatime: 'GH (Avn)',
|
||||
Azerbaijani: 'AZ',
|
||||
'Azerbaijani (Cyrillic)': 'AZ (Cyrillic)',
|
||||
'Azerbaijani (Iran)': 'IR (Azb)',
|
||||
Bambara: 'ML',
|
||||
Bangla: 'BD',
|
||||
'Bangla (India, Baishakhi InScript)': 'IN (Ben Inscript)',
|
||||
'Bangla (India, Baishakhi)': 'IN (Ben Baishakhi)',
|
||||
'Bangla (India, Bornona)': 'IN (Ben Bornona)',
|
||||
'Bangla (India, Gitanjali)': 'IN (Ben Gitanjali)',
|
||||
'Bangla (India, Probhat)': 'IN (Ben Probhat)',
|
||||
'Bangla (India)': 'IN (Ben)',
|
||||
'Bangla (Probhat)': 'BD (Probhat)',
|
||||
Bashkirian: 'RU (Bak)',
|
||||
Belarusian: 'BY',
|
||||
'Belarusian (intl.)': 'BY (Intl)',
|
||||
'Belarusian (Latin)': 'BY (Latin)',
|
||||
'Belarusian (legacy)': 'BY (Legacy)',
|
||||
'Belarusian (phonetic)': 'BY (Phonetic)',
|
||||
Belgian: 'BE',
|
||||
'Belgian (alt.)': 'BE (Oss)',
|
||||
'Belgian (ISO, alt.)': 'BE (Iso-Alternate)',
|
||||
'Belgian (Latin-9 only, alt.)': 'BE (Oss Latin9)',
|
||||
'Belgian (no dead keys)': 'BE (Nodeadkeys)',
|
||||
'Belgian (Wang 724 AZERTY)': 'BE (Wang)',
|
||||
'Berber (Algeria, Latin)': 'DZ',
|
||||
'Berber (Algeria, Tifinagh)': 'DZ (Ber)',
|
||||
'Berber (Morocco, Tifinagh alt.)': 'MA (Tifinagh-Alt)',
|
||||
'Berber (Morocco, Tifinagh extended phonetic)': 'MA (Tifinagh-Extended-Phonetic)',
|
||||
'Berber (Morocco, Tifinagh extended)': 'MA (Tifinagh-Extended)',
|
||||
'Berber (Morocco, Tifinagh phonetic, alt.)': 'MA (Tifinagh-Alt-Phonetic)',
|
||||
'Berber (Morocco, Tifinagh phonetic)': 'MA (Tifinagh-Phonetic)',
|
||||
'Berber (Morocco, Tifinagh)': 'MA (Tifinagh)',
|
||||
Bosnian: 'BA',
|
||||
'Bosnian (US, with Bosnian digraphs)': 'BA (Unicodeus)',
|
||||
'Bosnian (US)': 'BA (Us)',
|
||||
'Bosnian (with Bosnian digraphs)': 'BA (Unicode)',
|
||||
'Bosnian (with guillemets)': 'BA (Alternatequotes)',
|
||||
Braille: 'BRAI',
|
||||
'Braille (left-handed inverted thumb)': 'BRAI (Left Hand Invert)',
|
||||
'Braille (left-handed)': 'BRAI (Left Hand)',
|
||||
'Braille (right-handed inverted thumb)': 'BRAI (Right Hand Invert)',
|
||||
'Braille (right-handed)': 'BRAI (Right Hand)',
|
||||
'Breton (France)': 'FR (Bre)',
|
||||
Bulgarian: 'BG',
|
||||
'Bulgarian (enhanced)': 'BG (Bekl)',
|
||||
'Bulgarian (new phonetic)': 'BG (Bas Phonetic)',
|
||||
'Bulgarian (traditional phonetic)': 'BG (Phonetic)',
|
||||
Burmese: 'MM',
|
||||
'Burmese Zawgyi': 'MM (Zawgyi)',
|
||||
'Cameroon (AZERTY, intl.)': 'CM (Azerty)',
|
||||
'Cameroon (Dvorak, intl.)': 'CM (Dvorak)',
|
||||
'Cameroon Multilingual (QWERTY, intl.)': 'CM (Qwerty)',
|
||||
'Canadian (CSA)': 'CA (Multix)',
|
||||
'Catalan (Spain, with middle-dot L)': 'ES (Cat)',
|
||||
Cherokee: 'US (Chr)',
|
||||
Chinese: 'CN',
|
||||
Chuvash: 'RU (Cv)',
|
||||
'Chuvash (Latin)': 'RU (Cv Latin)',
|
||||
CloGaelach: 'IE (CloGaelach)',
|
||||
'Crimean Tatar (Turkish Alt-Q)': 'UA (Crh Alt)',
|
||||
'Crimean Tatar (Turkish F)': 'UA (Crh F)',
|
||||
'Crimean Tatar (Turkish Q)': 'UA (Crh)',
|
||||
Croatian: 'HR',
|
||||
'Croatian (US, with Croatian digraphs)': 'HR (Unicodeus)',
|
||||
'Croatian (US)': 'HR (Us)',
|
||||
'Croatian (with Croatian digraphs)': 'HR (Unicode)',
|
||||
'Croatian (with guillemets)': 'HR (Alternatequotes)',
|
||||
Czech: 'CZ',
|
||||
'Czech (QWERTY, extended backslash)': 'CZ (Qwerty Bksl)',
|
||||
'Czech (QWERTY, Macintosh)': 'CZ (Qwerty-Mac)',
|
||||
'Czech (QWERTY)': 'CZ (Qwerty)',
|
||||
'Czech (UCW, only accented letters)': 'CZ (Ucw)',
|
||||
'Czech (US, Dvorak, UCW support)': 'CZ (Dvorak-Ucw)',
|
||||
'Czech (with <\\|> key)': 'CZ (Bksl)',
|
||||
Danish: 'DK',
|
||||
'Danish (Dvorak)': 'DK (Dvorak)',
|
||||
'Danish (Macintosh, no dead keys)': 'DK (Mac Nodeadkeys)',
|
||||
'Danish (Macintosh)': 'DK (Mac)',
|
||||
'Danish (no dead keys)': 'DK (Nodeadkeys)',
|
||||
'Danish (Windows)': 'DK (Winkeys)',
|
||||
Dari: 'AF',
|
||||
'Dari (Afghanistan, OLPC)': 'AF (Fa-Olpc)',
|
||||
Dhivehi: 'MV',
|
||||
Dutch: 'NL',
|
||||
'Dutch (Macintosh)': 'NL (Mac)',
|
||||
'Dutch (standard)': 'NL (Std)',
|
||||
'Dutch (US)': 'NL (Us)',
|
||||
Dzongkha: 'BT',
|
||||
'English (Australian)': 'AU',
|
||||
'English (Cameroon)': 'CM',
|
||||
'English (Canada)': 'CA (Eng)',
|
||||
'English (classic Dvorak)': 'US (Dvorak-Classic)',
|
||||
'English (Colemak-DH ISO)': 'US (Colemak Dh Iso)',
|
||||
'English (Colemak-DH)': 'US (Colemak Dh)',
|
||||
'English (Colemak)': 'US (Colemak)',
|
||||
'English (Dvorak, alt. intl.)': 'US (Dvorak-Alt-Intl)',
|
||||
'English (Dvorak, intl., with dead keys)': 'US (Dvorak-Intl)',
|
||||
'English (Dvorak, left-handed)': 'US (Dvorak-L)',
|
||||
'English (Dvorak, Macintosh)': 'US (Dvorak-Mac)',
|
||||
'English (Dvorak, right-handed)': 'US (Dvorak-R)',
|
||||
'English (Dvorak)': 'US (Dvorak)',
|
||||
'English (Ghana, GILLBT)': 'GH (Gillbt)',
|
||||
'English (Ghana, multilingual)': 'GH (Generic)',
|
||||
'English (Ghana)': 'GH',
|
||||
'English (India, with rupee)': 'IN (Eng)',
|
||||
'English (intl., with AltGr dead keys)': 'US (Altgr-Intl)',
|
||||
'English (Macintosh)': 'US (Mac)',
|
||||
'English (Mali, US, intl.)': 'ML (Us-Intl)',
|
||||
'English (Mali, US, Macintosh)': 'ML (Us-Mac)',
|
||||
'English (Nigeria)': 'NG',
|
||||
'English (Norman)': 'US (Norman)',
|
||||
'English (programmer Dvorak)': 'US (Dvp)',
|
||||
'English (South Africa)': 'ZA',
|
||||
'English (the divide/multiply toggle the layout)': 'US (Olpc2)',
|
||||
'English (UK, Colemak-DH)': 'GB (Colemak Dh)',
|
||||
'English (UK, Colemak)': 'GB (Colemak)',
|
||||
'English (UK, Dvorak, with UK punctuation)': 'GB (Dvorakukp)',
|
||||
'English (UK, Dvorak)': 'GB (Dvorak)',
|
||||
'English (UK, extended, Windows)': 'GB (Extd)',
|
||||
'English (UK, intl., with dead keys)': 'GB (Intl)',
|
||||
'English (UK, Macintosh, intl.)': 'GB (Mac Intl)',
|
||||
'English (UK, Macintosh)': 'GB (Mac)',
|
||||
'English (UK)': 'GB',
|
||||
'English (US, alt. intl.)': 'US (Alt-Intl)',
|
||||
'English (US, euro on 5)': 'US (Euro)',
|
||||
'English (US, intl., with dead keys)': 'US (Intl)',
|
||||
'English (US, Symbolic)': 'US (Symbolic)',
|
||||
'English (US)': 'US',
|
||||
'English (Workman, intl., with dead keys)': 'US (Workman-Intl)',
|
||||
'English (Workman)': 'US (Workman)',
|
||||
Esperanto: 'EPO',
|
||||
'Esperanto (Brazil, Nativo)': 'BR (Nativo-Epo)',
|
||||
'Esperanto (legacy)': 'EPO (Legacy)',
|
||||
'Esperanto (Portugal, Nativo)': 'PT (Nativo-Epo)',
|
||||
Estonian: 'EE',
|
||||
'Estonian (Dvorak)': 'EE (Dvorak)',
|
||||
'Estonian (no dead keys)': 'EE (Nodeadkeys)',
|
||||
'Estonian (US)': 'EE (Us)',
|
||||
Ewe: 'GH (Ewe)',
|
||||
Faroese: 'FO',
|
||||
'Faroese (no dead keys)': 'FO (Nodeadkeys)',
|
||||
Filipino: 'PH',
|
||||
'Filipino (Capewell-Dvorak, Baybayin)': 'PH (Capewell-Dvorak-Bay)',
|
||||
'Filipino (Capewell-Dvorak, Latin)': 'PH (Capewell-Dvorak)',
|
||||
'Filipino (Capewell-QWERF 2006, Baybayin)': 'PH (Capewell-Qwerf2k6-Bay)',
|
||||
'Filipino (Capewell-QWERF 2006, Latin)': 'PH (Capewell-Qwerf2k6)',
|
||||
'Filipino (Colemak, Baybayin)': 'PH (Colemak-Bay)',
|
||||
'Filipino (Colemak, Latin)': 'PH (Colemak)',
|
||||
'Filipino (Dvorak, Baybayin)': 'PH (Dvorak-Bay)',
|
||||
'Filipino (Dvorak, Latin)': 'PH (Dvorak)',
|
||||
'Filipino (QWERTY, Baybayin)': 'PH (Qwerty-Bay)',
|
||||
Finnish: 'FI',
|
||||
'Finnish (classic, no dead keys)': 'FI (Nodeadkeys)',
|
||||
'Finnish (classic)': 'FI (Classic)',
|
||||
'Finnish (Macintosh)': 'FI (Mac)',
|
||||
'Finnish (Windows)': 'FI (Winkeys)',
|
||||
French: 'FR',
|
||||
'French (alt., Latin-9 only)': 'FR (Oss Latin9)',
|
||||
'French (alt., no dead keys)': 'FR (Oss Nodeadkeys)',
|
||||
'French (alt.)': 'FR (Oss)',
|
||||
'French (AZERTY, AFNOR)': 'FR (Afnor)',
|
||||
'French (AZERTY)': 'FR (Azerty)',
|
||||
'French (BEPO, AFNOR)': 'FR (Bepo Afnor)',
|
||||
'French (BEPO, Latin-9 only)': 'FR (Bepo Latin9)',
|
||||
'French (BEPO)': 'FR (Bepo)',
|
||||
'French (Cameroon)': 'CM (French)',
|
||||
'French (Canada, Dvorak)': 'CA (Fr-Dvorak)',
|
||||
'French (Canada, legacy)': 'CA (Fr-Legacy)',
|
||||
'French (Canada)': 'CA',
|
||||
'French (Democratic Republic of the Congo)': 'CD',
|
||||
'French (Dvorak)': 'FR (Dvorak)',
|
||||
'French (legacy, alt., no dead keys)': 'FR (Latin9 Nodeadkeys)',
|
||||
'French (legacy, alt.)': 'FR (Latin9)',
|
||||
'French (Macintosh)': 'FR (Mac)',
|
||||
'French (Mali, alt.)': 'ML (Fr-Oss)',
|
||||
'French (Morocco)': 'MA (French)',
|
||||
'French (no dead keys)': 'FR (Nodeadkeys)',
|
||||
'French (Switzerland, Macintosh)': 'CH (Fr Mac)',
|
||||
'French (Switzerland, no dead keys)': 'CH (Fr Nodeadkeys)',
|
||||
'French (Switzerland)': 'CH (Fr)',
|
||||
'French (Togo)': 'TG',
|
||||
'French (US)': 'FR (Us)',
|
||||
'Friulian (Italy)': 'IT (Fur)',
|
||||
Fula: 'GH (Fula)',
|
||||
Ga: 'GH (Ga)',
|
||||
Georgian: 'GE',
|
||||
'Georgian (ergonomic)': 'GE (Ergonomic)',
|
||||
'Georgian (France, AZERTY Tskapo)': 'FR (Geo)',
|
||||
'Georgian (Italy)': 'IT (Geo)',
|
||||
'Georgian (MESS)': 'GE (Mess)',
|
||||
German: 'DE',
|
||||
'German (Austria, Macintosh)': 'AT (Mac)',
|
||||
'German (Austria, no dead keys)': 'AT (Nodeadkeys)',
|
||||
'German (Austria)': 'AT',
|
||||
'German (dead acute)': 'DE (Deadacute)',
|
||||
'German (dead grave acute)': 'DE (Deadgraveacute)',
|
||||
'German (dead tilde)': 'DE (Deadtilde)',
|
||||
'German (Dvorak)': 'DE (Dvorak)',
|
||||
'German (E1)': 'DE (E1)',
|
||||
'German (E2)': 'DE (E2)',
|
||||
'German (Macintosh, no dead keys)': 'DE (Mac Nodeadkeys)',
|
||||
'German (Macintosh)': 'DE (Mac)',
|
||||
'German (Neo 2)': 'DE (Neo)',
|
||||
'German (no dead keys)': 'DE (Nodeadkeys)',
|
||||
'German (QWERTY)': 'DE (Qwerty)',
|
||||
'German (Switzerland, legacy)': 'CH (Legacy)',
|
||||
'German (Switzerland, Macintosh)': 'CH (De Mac)',
|
||||
'German (Switzerland, no dead keys)': 'CH (De Nodeadkeys)',
|
||||
'German (Switzerland)': 'CH',
|
||||
'German (T3)': 'DE (T3)',
|
||||
'German (US)': 'DE (Us)',
|
||||
Greek: 'GR',
|
||||
'Greek (extended)': 'GR (Extended)',
|
||||
'Greek (no dead keys)': 'GR (Nodeadkeys)',
|
||||
'Greek (polytonic)': 'GR (Polytonic)',
|
||||
'Greek (simple)': 'GR (Simple)',
|
||||
Gujarati: 'IN (Guj)',
|
||||
'Hanyu Pinyin Letters (with AltGr dead keys)': 'CN (Altgr-Pinyin)',
|
||||
'Hausa (Ghana)': 'GH (Hausa)',
|
||||
'Hausa (Nigeria)': 'NG (Hausa)',
|
||||
Hawaiian: 'US (Haw)',
|
||||
Hebrew: 'IL',
|
||||
'Hebrew (Biblical, Tiro)': 'IL (Biblical)',
|
||||
'Hebrew (lyx)': 'IL (Lyx)',
|
||||
'Hebrew (phonetic)': 'IL (Phonetic)',
|
||||
'Hindi (Bolnagri)': 'IN (Bolnagri)',
|
||||
'Hindi (KaGaPa, phonetic)': 'IN (Hin-Kagapa)',
|
||||
'Hindi (Wx)': 'IN (Hin-Wx)',
|
||||
Hungarian: 'HU',
|
||||
'Hungarian (no dead keys)': 'HU (Nodeadkeys)',
|
||||
'Hungarian (QWERTY, 101-key, comma, dead keys)': 'HU (101 Qwerty Comma Dead)',
|
||||
'Hungarian (QWERTY, 101-key, comma, no dead keys)': 'HU (101 Qwerty Comma Nodead)',
|
||||
'Hungarian (QWERTY, 101-key, dot, dead keys)': 'HU (101 Qwerty Dot Dead)',
|
||||
'Hungarian (QWERTY, 101-key, dot, no dead keys)': 'HU (101 Qwerty Dot Nodead)',
|
||||
'Hungarian (QWERTY, 102-key, comma, dead keys)': 'HU (102 Qwerty Comma Dead)',
|
||||
'Hungarian (QWERTY, 102-key, comma, no dead keys)': 'HU (102 Qwerty Comma Nodead)',
|
||||
'Hungarian (QWERTY, 102-key, dot, dead keys)': 'HU (102 Qwerty Dot Dead)',
|
||||
'Hungarian (QWERTY, 102-key, dot, no dead keys)': 'HU (102 Qwerty Dot Nodead)',
|
||||
'Hungarian (QWERTY)': 'HU (Qwerty)',
|
||||
'Hungarian (QWERTZ, 101-key, comma, dead keys)': 'HU (101 Qwertz Comma Dead)',
|
||||
'Hungarian (QWERTZ, 101-key, comma, no dead keys)': 'HU (101 Qwertz Comma Nodead)',
|
||||
'Hungarian (QWERTZ, 101-key, dot, dead keys)': 'HU (101 Qwertz Dot Dead)',
|
||||
'Hungarian (QWERTZ, 101-key, dot, no dead keys)': 'HU (101 Qwertz Dot Nodead)',
|
||||
'Hungarian (QWERTZ, 102-key, comma, dead keys)': 'HU (102 Qwertz Comma Dead)',
|
||||
'Hungarian (QWERTZ, 102-key, comma, no dead keys)': 'HU (102 Qwertz Comma Nodead)',
|
||||
'Hungarian (QWERTZ, 102-key, dot, dead keys)': 'HU (102 Qwertz Dot Dead)',
|
||||
'Hungarian (QWERTZ, 102-key, dot, no dead keys)': 'HU (102 Qwertz Dot Nodead)',
|
||||
'Hungarian (standard)': 'HU (Standard)',
|
||||
Icelandic: 'IS',
|
||||
'Icelandic (Dvorak)': 'IS (Dvorak)',
|
||||
'Icelandic (Macintosh, legacy)': 'IS (Mac Legacy)',
|
||||
'Icelandic (Macintosh)': 'IS (Mac)',
|
||||
Igbo: 'NG (Igbo)',
|
||||
Indian: 'IN',
|
||||
'Indic IPA': 'IN (Iipa)',
|
||||
'Indonesian (Arab Melayu, extended phonetic)': 'ID (Melayu-Phoneticx)',
|
||||
'Indonesian (Arab Melayu, phonetic)': 'ID (Melayu-Phonetic)',
|
||||
'Indonesian (Arab Pegon, phonetic)': 'ID (Pegon-Phonetic)',
|
||||
'Indonesian (Latin)': 'ID',
|
||||
Inuktitut: 'CA (Ike)',
|
||||
Iraqi: 'IQ',
|
||||
Irish: 'IE',
|
||||
'Irish (UnicodeExpert)': 'IE (UnicodeExpert)',
|
||||
Italian: 'IT',
|
||||
'Italian (IBM 142)': 'IT (Ibm)',
|
||||
'Italian (intl., with dead keys)': 'IT (Intl)',
|
||||
'Italian (Macintosh)': 'IT (Mac)',
|
||||
'Italian (no dead keys)': 'IT (Nodeadkeys)',
|
||||
'Italian (US)': 'IT (Us)',
|
||||
'Italian (Windows)': 'IT (Winkeys)',
|
||||
Japanese: 'JP',
|
||||
'Japanese (Dvorak)': 'JP (Dvorak)',
|
||||
'Japanese (Kana 86)': 'JP (Kana86)',
|
||||
'Japanese (Kana)': 'JP (Kana)',
|
||||
'Japanese (Macintosh)': 'JP (Mac)',
|
||||
'Japanese (OADG 109A)': 'JP (OADG109A)',
|
||||
Javanese: 'ID (Javanese)',
|
||||
'Kabyle (AZERTY, with dead keys)': 'DZ (Azerty-Deadkeys)',
|
||||
'Kabyle (QWERTY, UK, with dead keys)': 'DZ (Qwerty-Gb-Deadkeys)',
|
||||
'Kabyle (QWERTY, US, with dead keys)': 'DZ (Qwerty-Us-Deadkeys)',
|
||||
Kalmyk: 'RU (Xal)',
|
||||
Kannada: 'IN (Kan)',
|
||||
'Kannada (KaGaPa, phonetic)': 'IN (Kan-Kagapa)',
|
||||
Kashubian: 'PL (Csb)',
|
||||
Kazakh: 'KZ',
|
||||
'Kazakh (extended)': 'KZ (Ext)',
|
||||
'Kazakh (Latin)': 'KZ (Latin)',
|
||||
'Kazakh (with Russian)': 'KZ (Kazrus)',
|
||||
'Khmer (Cambodia)': 'KH',
|
||||
Kikuyu: 'KE (Kik)',
|
||||
Komi: 'RU (Kom)',
|
||||
Korean: 'KR',
|
||||
'Korean (101/104-key compatible)': 'KR (Kr104)',
|
||||
'Kurdish (Iran, Arabic-Latin)': 'IR (Ku Ara)',
|
||||
'Kurdish (Iran, F)': 'IR (Ku F)',
|
||||
'Kurdish (Iran, Latin Alt-Q)': 'IR (Ku Alt)',
|
||||
'Kurdish (Iran, Latin Q)': 'IR (Ku)',
|
||||
'Kurdish (Iraq, Arabic-Latin)': 'IQ (Ku Ara)',
|
||||
'Kurdish (Iraq, F)': 'IQ (Ku F)',
|
||||
'Kurdish (Iraq, Latin Alt-Q)': 'IQ (Ku Alt)',
|
||||
'Kurdish (Iraq, Latin Q)': 'IQ (Ku)',
|
||||
'Kurdish (Syria, F)': 'SY (Ku F)',
|
||||
'Kurdish (Syria, Latin Alt-Q)': 'SY (Ku Alt)',
|
||||
'Kurdish (Syria, Latin Q)': 'SY (Ku)',
|
||||
'Kurdish (Turkey, F)': 'TR (Ku F)',
|
||||
'Kurdish (Turkey, Latin Alt-Q)': 'TR (Ku Alt)',
|
||||
'Kurdish (Turkey, Latin Q)': 'TR (Ku)',
|
||||
Kyrgyz: 'KG',
|
||||
'Kyrgyz (phonetic)': 'KG (Phonetic)',
|
||||
Lao: 'LA',
|
||||
'Lao (STEA)': 'LA (Stea)',
|
||||
Latvian: 'LV',
|
||||
'Latvian (adapted)': 'LV (Adapted)',
|
||||
'Latvian (apostrophe)': 'LV (Apostrophe)',
|
||||
'Latvian (ergonomic, ŪGJRMV)': 'LV (Ergonomic)',
|
||||
'Latvian (F)': 'LV (Fkey)',
|
||||
'Latvian (modern)': 'LV (Modern)',
|
||||
'Latvian (tilde)': 'LV (Tilde)',
|
||||
Lithuanian: 'LT',
|
||||
'Lithuanian (IBM LST 1205-92)': 'LT (Ibm)',
|
||||
'Lithuanian (LEKP)': 'LT (Lekp)',
|
||||
'Lithuanian (LEKPa)': 'LT (Lekpa)',
|
||||
'Lithuanian (Ratise)': 'LT (Ratise)',
|
||||
'Lithuanian (standard)': 'LT (Std)',
|
||||
'Lithuanian (US)': 'LT (Us)',
|
||||
'Lower Sorbian': 'DE (Dsb)',
|
||||
'Lower Sorbian (QWERTZ)': 'DE (Dsb Qwertz)',
|
||||
Macedonian: 'MK',
|
||||
'Macedonian (no dead keys)': 'MK (Nodeadkeys)',
|
||||
'Malay (Jawi, Arabic Keyboard)': 'MY',
|
||||
'Malay (Jawi, phonetic)': 'MY (Phonetic)',
|
||||
Malayalam: 'IN (Mal)',
|
||||
'Malayalam (enhanced InScript, with rupee)': 'IN (Mal Enhanced)',
|
||||
'Malayalam (Lalitha)': 'IN (Mal Lalitha)',
|
||||
Maltese: 'MT',
|
||||
'Maltese (UK, with AltGr overrides)': 'MT (Alt-Gb)',
|
||||
'Maltese (US, with AltGr overrides)': 'MT (Alt-Us)',
|
||||
'Maltese (US)': 'MT (Us)',
|
||||
'Manipuri (Eeyek)': 'IN (Eeyek)',
|
||||
Maori: 'MAO',
|
||||
'Marathi (enhanced InScript)': 'IN (Marathi)',
|
||||
'Marathi (KaGaPa, phonetic)': 'IN (Mar-Kagapa)',
|
||||
Mari: 'RU (Chm)',
|
||||
Mmuock: 'CM (Mmuock)',
|
||||
Moldavian: 'MD',
|
||||
'Moldavian (Gagauz)': 'MD (Gag)',
|
||||
Mon: 'MM (Mnw)',
|
||||
'Mon (A1)': 'MM (Mnw-A1)',
|
||||
Mongolian: 'MN',
|
||||
'Mongolian (Bichig)': 'CN (Mon Trad)',
|
||||
'Mongolian (Galik)': 'CN (Mon Trad Galik)',
|
||||
'Mongolian (Manchu Galik)': 'CN (Mon Manchu Galik)',
|
||||
'Mongolian (Manchu)': 'CN (Mon Trad Manchu)',
|
||||
'Mongolian (Todo Galik)': 'CN (Mon Todo Galik)',
|
||||
'Mongolian (Todo)': 'CN (Mon Trad Todo)',
|
||||
'Mongolian (Xibe)': 'CN (Mon Trad Xibe)',
|
||||
Montenegrin: 'ME',
|
||||
'Montenegrin (Cyrillic, with guillemets)': 'ME (Cyrillicalternatequotes)',
|
||||
'Montenegrin (Cyrillic, ZE and ZHE swapped)': 'ME (Cyrillicyz)',
|
||||
'Montenegrin (Cyrillic)': 'ME (Cyrillic)',
|
||||
'Montenegrin (Latin, QWERTY)': 'ME (Latinyz)',
|
||||
'Montenegrin (Latin, Unicode, QWERTY)': 'ME (Latinunicodeyz)',
|
||||
'Montenegrin (Latin, Unicode)': 'ME (Latinunicode)',
|
||||
'Montenegrin (Latin, with guillemets)': 'ME (Latinalternatequotes)',
|
||||
"N'Ko (AZERTY)": 'GN',
|
||||
Nepali: 'NP',
|
||||
'Northern Saami (Finland)': 'FI (Smi)',
|
||||
'Northern Saami (Norway, no dead keys)': 'NO (Smi Nodeadkeys)',
|
||||
'Northern Saami (Norway)': 'NO (Smi)',
|
||||
'Northern Saami (Sweden)': 'SE (Smi)',
|
||||
Norwegian: 'NO',
|
||||
'Norwegian (Colemak)': 'NO (Colemak)',
|
||||
'Norwegian (Dvorak)': 'NO (Dvorak)',
|
||||
'Norwegian (Macintosh, no dead keys)': 'NO (Mac Nodeadkeys)',
|
||||
'Norwegian (Macintosh)': 'NO (Mac)',
|
||||
'Norwegian (no dead keys)': 'NO (Nodeadkeys)',
|
||||
'Norwegian (Windows)': 'NO (Winkeys)',
|
||||
Occitan: 'FR (Oci)',
|
||||
Ogham: 'IE (Ogam)',
|
||||
'Ogham (IS434)': 'IE (Ogam Is434)',
|
||||
'Ol Chiki': 'IN (Olck)',
|
||||
'Old Turkic': 'TR (Otk)',
|
||||
'Old Turkic (F)': 'TR (Otkf)',
|
||||
Oriya: 'IN (Ori)',
|
||||
'Oriya (Bolnagri)': 'IN (Ori-Bolnagri)',
|
||||
'Oriya (Wx)': 'IN (Ori-Wx)',
|
||||
'Ossetian (Georgia)': 'GE (Os)',
|
||||
'Ossetian (legacy)': 'RU (Os Legacy)',
|
||||
'Ossetian (Windows)': 'RU (Os Winkeys)',
|
||||
'Ottoman (F)': 'TR (Otf)',
|
||||
'Ottoman (Q)': 'TR (Ot)',
|
||||
'Pannonian Rusyn': 'RS (Rue)',
|
||||
Pashto: 'AF (Ps)',
|
||||
'Pashto (Afghanistan, OLPC)': 'AF (Ps-Olpc)',
|
||||
Persian: 'IR',
|
||||
'Persian (with Persian keypad)': 'IR (Pes Keypad)',
|
||||
Polish: 'PL',
|
||||
'Polish (British keyboard)': 'GB (Pl)',
|
||||
'Polish (Dvorak, with Polish quotes on key 1)': 'PL (Dvorak Altquotes)',
|
||||
'Polish (Dvorak, with Polish quotes on quotemark key)': 'PL (Dvorak Quotes)',
|
||||
'Polish (Dvorak)': 'PL (Dvorak)',
|
||||
'Polish (legacy)': 'PL (Legacy)',
|
||||
'Polish (programmer Dvorak)': 'PL (Dvp)',
|
||||
'Polish (QWERTZ)': 'PL (Qwertz)',
|
||||
Portuguese: 'PT',
|
||||
'Portuguese (Brazil, Dvorak)': 'BR (Dvorak)',
|
||||
'Portuguese (Brazil, IBM/Lenovo ThinkPad)': 'BR (Thinkpad)',
|
||||
'Portuguese (Brazil, Nativo for US keyboards)': 'BR (Nativo-Us)',
|
||||
'Portuguese (Brazil, Nativo)': 'BR (Nativo)',
|
||||
'Portuguese (Brazil, no dead keys)': 'BR (Nodeadkeys)',
|
||||
'Portuguese (Brazil)': 'BR',
|
||||
'Portuguese (Macintosh, no dead keys)': 'PT (Mac Nodeadkeys)',
|
||||
'Portuguese (Macintosh)': 'PT (Mac)',
|
||||
'Portuguese (Nativo for US keyboards)': 'PT (Nativo-Us)',
|
||||
'Portuguese (Nativo)': 'PT (Nativo)',
|
||||
'Portuguese (no dead keys)': 'PT (Nodeadkeys)',
|
||||
'Punjabi (Gurmukhi Jhelum)': 'IN (Jhelum)',
|
||||
'Punjabi (Gurmukhi)': 'IN (Guru)',
|
||||
Romanian: 'RO',
|
||||
'Romanian (Germany, no dead keys)': 'DE (Ro Nodeadkeys)',
|
||||
'Romanian (Germany)': 'DE (Ro)',
|
||||
'Romanian (standard)': 'RO (Std)',
|
||||
'Romanian (Windows)': 'RO (Winkeys)',
|
||||
Russian: 'RU',
|
||||
'Russian (Belarus)': 'BY (Ru)',
|
||||
'Russian (Czech, phonetic)': 'CZ (Rus)',
|
||||
'Russian (DOS)': 'RU (Dos)',
|
||||
'Russian (engineering, EN)': 'RU (Ruchey En)',
|
||||
'Russian (engineering, RU)': 'RU (Ruchey Ru)',
|
||||
'Russian (Georgia)': 'GE (Ru)',
|
||||
'Russian (Germany, phonetic)': 'DE (Ru)',
|
||||
'Russian (Kazakhstan, with Kazakh)': 'KZ (Ruskaz)',
|
||||
'Russian (legacy)': 'RU (Legacy)',
|
||||
'Russian (Macintosh)': 'RU (Mac)',
|
||||
'Russian (phonetic, AZERTY)': 'RU (Phonetic Azerty)',
|
||||
'Russian (phonetic, Dvorak)': 'RU (Phonetic Dvorak)',
|
||||
'Russian (phonetic, French)': 'RU (Phonetic Fr)',
|
||||
'Russian (phonetic, Windows)': 'RU (Phonetic Winkeys)',
|
||||
'Russian (phonetic, YAZHERTY)': 'RU (Phonetic YAZHERTY)',
|
||||
'Russian (phonetic)': 'RU (Phonetic)',
|
||||
'Russian (Poland, phonetic Dvorak)': 'PL (Ru Phonetic Dvorak)',
|
||||
'Russian (Sweden, phonetic, no dead keys)': 'SE (Rus Nodeadkeys)',
|
||||
'Russian (Sweden, phonetic)': 'SE (Rus)',
|
||||
'Russian (typewriter, legacy)': 'RU (Typewriter-Legacy)',
|
||||
'Russian (typewriter)': 'RU (Typewriter)',
|
||||
'Russian (Ukraine, standard RSTU)': 'UA (Rstu Ru)',
|
||||
'Russian (US, phonetic)': 'US (Rus)',
|
||||
'Saisiyat (Taiwan)': 'TW (Saisiyat)',
|
||||
Samogitian: 'LT (Sgs)',
|
||||
'Sanskrit (KaGaPa, phonetic)': 'IN (San-Kagapa)',
|
||||
'Scottish Gaelic': 'GB (Gla)',
|
||||
Serbian: 'RS',
|
||||
'Serbian (Cyrillic, with guillemets)': 'RS (Alternatequotes)',
|
||||
'Serbian (Cyrillic, ZE and ZHE swapped)': 'RS (Yz)',
|
||||
'Serbian (Latin, QWERTY)': 'RS (Latinyz)',
|
||||
'Serbian (Latin, Unicode, QWERTY)': 'RS (Latinunicodeyz)',
|
||||
'Serbian (Latin, Unicode)': 'RS (Latinunicode)',
|
||||
'Serbian (Latin, with guillemets)': 'RS (Latinalternatequotes)',
|
||||
'Serbian (Latin)': 'RS (Latin)',
|
||||
'Serbian (Russia)': 'RU (Srp)',
|
||||
'Serbo-Croatian (US)': 'US (Hbs)',
|
||||
Shan: 'MM (Shn)',
|
||||
'Shan (Zawgyi Tai)': 'MM (Zgt)',
|
||||
Sicilian: 'IT (Scn)',
|
||||
Silesian: 'PL (Szl)',
|
||||
Sindhi: 'PK (Snd)',
|
||||
'Sinhala (phonetic)': 'LK',
|
||||
'Sinhala (US)': 'LK (Us)',
|
||||
Slovak: 'SK',
|
||||
'Slovak (extended backslash)': 'SK (Bksl)',
|
||||
'Slovak (QWERTY, extended backslash)': 'SK (Qwerty Bksl)',
|
||||
'Slovak (QWERTY)': 'SK (Qwerty)',
|
||||
Slovenian: 'SI',
|
||||
'Slovenian (US)': 'SI (Us)',
|
||||
'Slovenian (with guillemets)': 'SI (Alternatequotes)',
|
||||
Spanish: 'ES',
|
||||
'Spanish (dead tilde)': 'ES (Deadtilde)',
|
||||
'Spanish (Dvorak)': 'ES (Dvorak)',
|
||||
'Spanish (Latin American, Colemak)': 'LATAM (Colemak)',
|
||||
'Spanish (Latin American, dead tilde)': 'LATAM (Deadtilde)',
|
||||
'Spanish (Latin American, Dvorak)': 'LATAM (Dvorak)',
|
||||
'Spanish (Latin American, no dead keys)': 'LATAM (Nodeadkeys)',
|
||||
'Spanish (Latin American)': 'LATAM',
|
||||
'Spanish (Macintosh)': 'ES (Mac)',
|
||||
'Spanish (no dead keys)': 'ES (Nodeadkeys)',
|
||||
'Spanish (Windows)': 'ES (Winkeys)',
|
||||
'Swahili (Kenya)': 'KE',
|
||||
'Swahili (Tanzania)': 'TZ',
|
||||
Swedish: 'SE',
|
||||
'Swedish (Dvorak, intl.)': 'SE (Us Dvorak)',
|
||||
'Swedish (Dvorak)': 'SE (Dvorak)',
|
||||
'Swedish (Macintosh)': 'SE (Mac)',
|
||||
'Swedish (no dead keys)': 'SE (Nodeadkeys)',
|
||||
'Swedish (Svdvorak)': 'SE (Svdvorak)',
|
||||
'Swedish (US)': 'SE (Us)',
|
||||
'Swedish Sign Language': 'SE (Swl)',
|
||||
Syriac: 'SY (Syc)',
|
||||
'Syriac (phonetic)': 'SY (Syc Phonetic)',
|
||||
Taiwanese: 'TW',
|
||||
'Taiwanese (indigenous)': 'TW (Indigenous)',
|
||||
Tajik: 'TJ',
|
||||
'Tajik (legacy)': 'TJ (Legacy)',
|
||||
'Tamil (InScript, with Arabic numerals)': 'IN (Tam)',
|
||||
'Tamil (InScript, with Tamil numerals)': 'IN (Tam Tamilnumbers)',
|
||||
"Tamil (Sri Lanka, TamilNet '99, TAB encoding)": 'LK (Tam TAB)',
|
||||
"Tamil (Sri Lanka, TamilNet '99)": 'LK (Tam Unicode)',
|
||||
"Tamil (TamilNet '99 with Tamil numerals)": 'IN (Tamilnet Tamilnumbers)',
|
||||
"Tamil (TamilNet '99, TAB encoding)": 'IN (Tamilnet TAB)',
|
||||
"Tamil (TamilNet '99, TSCII encoding)": 'IN (Tamilnet TSCII)',
|
||||
"Tamil (TamilNet '99)": 'IN (Tamilnet)',
|
||||
Tarifit: 'MA (Rif)',
|
||||
Tatar: 'RU (Tt)',
|
||||
Telugu: 'IN (Tel)',
|
||||
'Telugu (KaGaPa, phonetic)': 'IN (Tel-Kagapa)',
|
||||
'Telugu (Sarala)': 'IN (Tel-Sarala)',
|
||||
Thai: 'TH',
|
||||
'Thai (Pattachote)': 'TH (Pat)',
|
||||
'Thai (TIS-820.2538)': 'TH (Tis)',
|
||||
Tibetan: 'CN (Tib)',
|
||||
'Tibetan (with ASCII numerals)': 'CN (Tib Asciinum)',
|
||||
Tswana: 'BW',
|
||||
Turkish: 'TR',
|
||||
'Turkish (Alt-Q)': 'TR (Alt)',
|
||||
'Turkish (E)': 'TR (E)',
|
||||
'Turkish (F)': 'TR (F)',
|
||||
'Turkish (Germany)': 'DE (Tr)',
|
||||
'Turkish (intl., with dead keys)': 'TR (Intl)',
|
||||
Turkmen: 'TM',
|
||||
'Turkmen (Alt-Q)': 'TM (Alt)',
|
||||
Udmurt: 'RU (Udm)',
|
||||
Ukrainian: 'UA',
|
||||
'Ukrainian (homophonic)': 'UA (Homophonic)',
|
||||
'Ukrainian (legacy)': 'UA (Legacy)',
|
||||
'Ukrainian (macOS)': 'UA (MacOS)',
|
||||
'Ukrainian (phonetic)': 'UA (Phonetic)',
|
||||
'Ukrainian (standard RSTU)': 'UA (Rstu)',
|
||||
'Ukrainian (typewriter)': 'UA (Typewriter)',
|
||||
'Ukrainian (Windows)': 'UA (Winkeys)',
|
||||
'Urdu (alt. phonetic)': 'IN (Urd-Phonetic3)',
|
||||
'Urdu (Pakistan, CRULP)': 'PK (Urd-Crulp)',
|
||||
'Urdu (Pakistan, NLA)': 'PK (Urd-Nla)',
|
||||
'Urdu (Pakistan)': 'PK',
|
||||
'Urdu (phonetic)': 'IN (Urd-Phonetic)',
|
||||
'Urdu (Windows)': 'IN (Urd-Winkeys)',
|
||||
Uyghur: 'CN (Ug)',
|
||||
Uzbek: 'UZ',
|
||||
'Uzbek (Afghanistan, OLPC)': 'AF (Uz-Olpc)',
|
||||
'Uzbek (Afghanistan)': 'AF (Uz)',
|
||||
'Uzbek (Latin)': 'UZ (Latin)',
|
||||
Vietnamese: 'VN',
|
||||
'Vietnamese (France)': 'VN (Fr)',
|
||||
'Vietnamese (US)': 'VN (Us)',
|
||||
Wolof: 'SN',
|
||||
Yakut: 'RU (Sah)',
|
||||
Yoruba: 'NG (Yoruba)',
|
||||
'Unknown Layout': 'Unknown',
|
||||
} as const;
|
||||
70
src/components/bar/modules/kblayout/index.tsx
Normal file
70
src/components/bar/modules/kblayout/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { hyprlandService } from 'src/lib/constants/services';
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { getKeyboardLayout } from './helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { bind, execAsync } from 'astal';
|
||||
import { useHook } from 'src/lib/shared/hookHandler';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { label, labelType, icon, leftClick, rightClick, middleClick, scrollUp, scrollDown } =
|
||||
options.bar.customModules.kbLayout;
|
||||
|
||||
export const KbInput = (): BarBoxChild => {
|
||||
const keyboardModule = Module({
|
||||
textIcon: bind(icon),
|
||||
tooltipText: '',
|
||||
labelHook: (self: Astal.Label): void => {
|
||||
useHook(
|
||||
self,
|
||||
hyprlandService,
|
||||
() => {
|
||||
execAsync('hyprctl devices -j')
|
||||
.then((obj) => {
|
||||
self.label = getKeyboardLayout(obj, labelType.get());
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
'keyboard-layout',
|
||||
);
|
||||
|
||||
useHook(self, labelType, () => {
|
||||
execAsync('hyprctl devices -j')
|
||||
.then((obj) => {
|
||||
self.label = getKeyboardLayout(obj, labelType.get());
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
boxClass: 'kblayout',
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return keyboardModule;
|
||||
};
|
||||
124
src/components/bar/modules/media/helpers/index.ts
Normal file
124
src/components/bar/modules/media/helpers/index.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { MediaTags } from 'src/lib/types/audio.js';
|
||||
import { Opt } from 'src/lib/option';
|
||||
import AstalMpris from 'gi://AstalMpris?version=0.1';
|
||||
import { Variable } from 'astal';
|
||||
|
||||
/**
|
||||
* Retrieves the icon for a given media player.
|
||||
*
|
||||
* This function returns the appropriate icon for the provided media player name based on a predefined mapping.
|
||||
* If no match is found, it returns a default icon.
|
||||
*
|
||||
* @param playerName The name of the media player.
|
||||
*
|
||||
* @returns The icon for the media player as a string.
|
||||
*/
|
||||
const getIconForPlayer = (playerName: string): string => {
|
||||
const windowTitleMap = [
|
||||
['Firefox', ''],
|
||||
['Microsoft Edge', ''],
|
||||
['Discord', ''],
|
||||
['Plex', ''],
|
||||
['Spotify', ''],
|
||||
['Vlc', ''],
|
||||
['Mpv', ''],
|
||||
['Rhythmbox', ''],
|
||||
['Google Chrome', ''],
|
||||
['Brave Browser', ''],
|
||||
['Chromium', ''],
|
||||
['Opera', ''],
|
||||
['Vivaldi', ''],
|
||||
['Waterfox', ''],
|
||||
['Thorium', ''],
|
||||
['Zen Browser', ''],
|
||||
['Floorp', ''],
|
||||
['(.*)', ''],
|
||||
];
|
||||
|
||||
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0], 'i').test(playerName));
|
||||
|
||||
return foundMatch ? foundMatch[1] : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a given tag is a valid media tag.
|
||||
*
|
||||
* This function determines whether the provided tag is a valid media tag by checking it against a predefined list of media tag keys.
|
||||
*
|
||||
* @param tag The tag to check.
|
||||
*
|
||||
* @returns True if the tag is a valid media tag, false otherwise.
|
||||
*/
|
||||
const isValidMediaTag = (tag: unknown): tag is keyof MediaTags => {
|
||||
if (typeof tag !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mediaTagKeys = ['title', 'artists', 'artist', 'album', 'name', 'identity'] as const;
|
||||
return (mediaTagKeys as readonly string[]).includes(tag);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a media label based on the provided options.
|
||||
*
|
||||
* This function creates a media label string by formatting the media tags according to the specified format.
|
||||
* It truncates the label if it exceeds the specified truncation size and returns a default label if no media is playing.
|
||||
*
|
||||
* @param truncation_size The maximum size of the label before truncation.
|
||||
* @param show_label A boolean indicating whether to show the label.
|
||||
* @param format The format string for the media label.
|
||||
* @param songIcon A variable to store the icon for the current song.
|
||||
* @param activePlayer A variable representing the active media player.
|
||||
*
|
||||
* @returns The generated media label as a string.
|
||||
*/
|
||||
export const generateMediaLabel = (
|
||||
truncation_size: Opt<number>,
|
||||
show_label: Opt<boolean>,
|
||||
format: Opt<string>,
|
||||
songIcon: Variable<string>,
|
||||
activePlayer: Variable<AstalMpris.Player | undefined>,
|
||||
): string => {
|
||||
const currentPlayer = activePlayer.get();
|
||||
|
||||
if (!currentPlayer || !show_label.get()) {
|
||||
songIcon.set(getIconForPlayer(activePlayer.get()?.identity || ''));
|
||||
return `Media`;
|
||||
}
|
||||
|
||||
const { title, identity, artist, album, busName } = currentPlayer;
|
||||
songIcon.set(getIconForPlayer(identity));
|
||||
|
||||
const mediaTags: MediaTags = {
|
||||
title: title,
|
||||
artists: artist,
|
||||
artist: artist,
|
||||
album: album,
|
||||
name: busName,
|
||||
identity: identity,
|
||||
};
|
||||
|
||||
const mediaFormat = format.get();
|
||||
|
||||
const truncatedLabel = mediaFormat.replace(
|
||||
/{(title|artists|artist|album|name|identity)(:[^}]*)?}/g,
|
||||
(_, p1: string | undefined, p2: string | undefined) => {
|
||||
if (!isValidMediaTag(p1)) {
|
||||
return '';
|
||||
}
|
||||
const value = p1 !== undefined ? mediaTags[p1] : '';
|
||||
const suffix = p2?.length ? p2.slice(1) : '';
|
||||
return value ? value + suffix : '';
|
||||
},
|
||||
);
|
||||
|
||||
const maxLabelSize = truncation_size.get();
|
||||
|
||||
let mediaLabel = truncatedLabel;
|
||||
|
||||
if (maxLabelSize > 0 && truncatedLabel.length > maxLabelSize) {
|
||||
mediaLabel = `${truncatedLabel.substring(0, maxLabelSize)}...`;
|
||||
}
|
||||
|
||||
return mediaLabel.length ? mediaLabel : 'Media';
|
||||
};
|
||||
97
src/components/bar/modules/media/index.tsx
Normal file
97
src/components/bar/modules/media/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { openMenu } from '../../utils/menu.js';
|
||||
import options from 'src/options.js';
|
||||
import { runAsyncCommand } from 'src/components/bar/utils/helpers.js';
|
||||
import { generateMediaLabel } from './helpers/index.js';
|
||||
import { useHook } from 'src/lib/shared/hookHandler.js';
|
||||
import { mprisService } from 'src/lib/constants/services.js';
|
||||
import Variable from 'astal/variable.js';
|
||||
import { onMiddleClick, onPrimaryClick, onSecondaryClick } from 'src/lib/shared/eventHandlers.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.js';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { activePlayer, mediaAlbum, mediaArtist, mediaTitle } from 'src/globals/media.js';
|
||||
|
||||
const { truncation, truncation_size, show_label, show_active_only, rightClick, middleClick, format } =
|
||||
options.bar.media;
|
||||
|
||||
const Media = (): BarBoxChild => {
|
||||
const isVis = Variable(!show_active_only.get());
|
||||
|
||||
show_active_only.subscribe(() => {
|
||||
isVis.set(!show_active_only.get() || mprisService.get_players().length > 0);
|
||||
});
|
||||
|
||||
const songIcon = Variable('');
|
||||
|
||||
const mediaLabel = Variable.derive(
|
||||
[
|
||||
bind(activePlayer),
|
||||
bind(truncation),
|
||||
bind(truncation_size),
|
||||
bind(show_label),
|
||||
bind(format),
|
||||
bind(mediaTitle),
|
||||
bind(mediaAlbum),
|
||||
bind(mediaArtist),
|
||||
],
|
||||
() => {
|
||||
return generateMediaLabel(truncation_size, show_label, format, songIcon, activePlayer);
|
||||
},
|
||||
);
|
||||
|
||||
const componentClassName = Variable.derive([options.theme.bar.buttons.style, show_label], (style: string) => {
|
||||
const styleMap: Record<string, string> = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `media-container ${styleMap[style]}`;
|
||||
});
|
||||
|
||||
const component = (
|
||||
<box
|
||||
className={componentClassName()}
|
||||
onDestroy={() => {
|
||||
isVis.drop();
|
||||
songIcon.drop();
|
||||
mediaLabel.drop();
|
||||
componentClassName.drop();
|
||||
}}
|
||||
>
|
||||
<label className={'bar-button-icon media txt-icon bar'} label={bind(songIcon).as((icn) => icn || '')} />
|
||||
<label className={'bar-button-label media'} label={mediaLabel()} />
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
isVis,
|
||||
boxClass: 'media',
|
||||
props: {
|
||||
setup: (self: Astal.Button): void => {
|
||||
useHook(self, options.bar.scrollSpeed, () => {
|
||||
const disconnectPrimary = onPrimaryClick(self, (clicked, event) => {
|
||||
openMenu(clicked, event, 'mediamenu');
|
||||
});
|
||||
|
||||
const disconnectSecondary = onSecondaryClick(self, (clicked, event) => {
|
||||
runAsyncCommand(rightClick.get(), { clicked, event });
|
||||
});
|
||||
|
||||
const disconnectMiddle = onMiddleClick(self, (clicked, event) => {
|
||||
runAsyncCommand(middleClick.get(), { clicked, event });
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
disconnectPrimary();
|
||||
disconnectSecondary();
|
||||
disconnectMiddle();
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Media };
|
||||
75
src/components/bar/modules/menu/index.tsx
Normal file
75
src/components/bar/modules/menu/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { runAsyncCommand, throttledScrollHandler } from '../../utils/helpers.js';
|
||||
import options from '../../../../options.js';
|
||||
import { openMenu } from '../../utils/menu.js';
|
||||
import { getDistroIcon } from '../../../../lib/utils.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import Variable from 'astal/variable.js';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js';
|
||||
import { useHook } from 'src/lib/shared/hookHandler.js'; // Ensure correct import
|
||||
import { BarBoxChild } from 'src/lib/types/bar.js';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { rightClick, middleClick, scrollUp, scrollDown, autoDetectIcon, icon } = options.bar.launcher;
|
||||
|
||||
const Menu = (): BarBoxChild => {
|
||||
const iconBinding = Variable.derive([autoDetectIcon, icon], (autoDetect: boolean, iconValue: string): string =>
|
||||
autoDetect ? getDistroIcon() : iconValue,
|
||||
);
|
||||
|
||||
const componentClassName = bind(options.theme.bar.buttons.style).as((style: string) => {
|
||||
const styleMap: Record<string, string> = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `dashboard ${styleMap[style]}`;
|
||||
});
|
||||
|
||||
const component = (
|
||||
<box
|
||||
className={componentClassName}
|
||||
onDestroy={() => {
|
||||
iconBinding.drop();
|
||||
}}
|
||||
>
|
||||
<label className={'bar-menu_label bar-button_icon txt-icon bar'} label={iconBinding()} />
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
boxClass: 'dashboard',
|
||||
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, 'dashboardmenu');
|
||||
});
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Menu };
|
||||
164
src/components/bar/modules/netstat/helpers/index.ts
Normal file
164
src/components/bar/modules/netstat/helpers/index.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import GLib from 'gi://GLib';
|
||||
import { Variable } from 'astal';
|
||||
import { NetworkResourceData } from 'src/lib/types/customModules/network';
|
||||
import { GET_DEFAULT_NETSTAT_DATA } from 'src/lib/types/defaults/netstat';
|
||||
import { RateUnit } from 'src/lib/types/bar';
|
||||
|
||||
let previousNetUsage = { rx: 0, tx: 0, time: 0 };
|
||||
|
||||
interface NetworkUsage {
|
||||
name: string;
|
||||
rx: number;
|
||||
tx: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the network rate based on the provided rate, type, and rounding option.
|
||||
*
|
||||
* This function converts the network rate to the appropriate unit (KiB/s, MiB/s, GiB/s, or bytes/s) based on the provided type.
|
||||
* It also rounds the rate to the specified number of decimal places.
|
||||
*
|
||||
* @param rate The network rate to format.
|
||||
* @param type The unit type for the rate (KiB, MiB, GiB).
|
||||
* @param round A boolean indicating whether to round the rate.
|
||||
*
|
||||
* @returns The formatted network rate as a string.
|
||||
*/
|
||||
const formatRate = (rate: number, type: string, round: boolean): string => {
|
||||
const fixed = round ? 0 : 2;
|
||||
|
||||
switch (true) {
|
||||
case type === 'KiB':
|
||||
return `${(rate / 1e3).toFixed(fixed)} KiB/s`;
|
||||
case type === 'MiB':
|
||||
return `${(rate / 1e6).toFixed(fixed)} MiB/s`;
|
||||
case type === 'GiB':
|
||||
return `${(rate / 1e9).toFixed(fixed)} GiB/s`;
|
||||
case rate >= 1e9:
|
||||
return `${(rate / 1e9).toFixed(fixed)} GiB/s`;
|
||||
case rate >= 1e6:
|
||||
return `${(rate / 1e6).toFixed(fixed)} MiB/s`;
|
||||
case rate >= 1e3:
|
||||
return `${(rate / 1e3).toFixed(fixed)} KiB/s`;
|
||||
default:
|
||||
return `${rate.toFixed(fixed)} bytes/s`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a line of network interface data.
|
||||
*
|
||||
* This function parses a line of network interface data from the /proc/net/dev file.
|
||||
* It extracts the interface name, received bytes, and transmitted bytes.
|
||||
*
|
||||
* @param line The line of network interface data to parse.
|
||||
*
|
||||
* @returns An object containing the interface name, received bytes, and transmitted bytes, or null if the line is invalid.
|
||||
*/
|
||||
const parseInterfaceData = (line: string): NetworkUsage | null => {
|
||||
const trimmedLine = line.trim();
|
||||
if (!trimmedLine || trimmedLine.startsWith('Inter-') || trimmedLine.startsWith('face')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [iface, rx, , , , , , , , tx] = trimmedLine.split(/\s+/);
|
||||
const rxValue = parseInt(rx, 10);
|
||||
const txValue = parseInt(tx, 10);
|
||||
const cleanedIface = iface.replace(':', '');
|
||||
|
||||
return { name: cleanedIface, rx: rxValue, tx: txValue };
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates a network interface.
|
||||
*
|
||||
* This function checks if the provided network interface is valid based on the interface name and received/transmitted bytes.
|
||||
*
|
||||
* @param iface The network interface to validate.
|
||||
* @param interfaceName The name of the interface to check.
|
||||
*
|
||||
* @returns True if the interface is valid, false otherwise.
|
||||
*/
|
||||
const isValidInterface = (iface: NetworkUsage | null, interfaceName: string): boolean => {
|
||||
if (!iface) return false;
|
||||
if (interfaceName) return iface.name === interfaceName;
|
||||
return iface.name !== 'lo' && iface.rx > 0 && iface.tx > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the network usage for a specified interface.
|
||||
*
|
||||
* This function reads the /proc/net/dev file to get the network usage data for the specified interface.
|
||||
* If no interface name is provided, it returns the usage data for the first valid interface found.
|
||||
*
|
||||
* @param interfaceName The name of the interface to get the usage data for. Defaults to an empty string.
|
||||
*
|
||||
* @returns An object containing the interface name, received bytes, and transmitted bytes.
|
||||
*/
|
||||
const getNetworkUsage = (interfaceName: string = ''): NetworkUsage => {
|
||||
const [success, data] = GLib.file_get_contents('/proc/net/dev');
|
||||
if (!success) {
|
||||
console.error('Failed to read /proc/net/dev');
|
||||
return { name: '', rx: 0, tx: 0 };
|
||||
}
|
||||
|
||||
const lines = new TextDecoder('utf-8').decode(data).split('\n');
|
||||
for (const line of lines) {
|
||||
const iface = parseInterfaceData(line);
|
||||
if (isValidInterface(iface, interfaceName)) {
|
||||
return iface!;
|
||||
}
|
||||
}
|
||||
|
||||
return { name: '', rx: 0, tx: 0 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the network usage data.
|
||||
*
|
||||
* This function calculates the network usage data based on the provided rounding option, interface name, and data type.
|
||||
* It returns an object containing the formatted received and transmitted rates.
|
||||
*
|
||||
* @param round A Variable<boolean> indicating whether to round the rates.
|
||||
* @param interfaceNameVar A Variable<string> containing the name of the interface to get the usage data for.
|
||||
* @param dataType A Variable<RateUnit> containing the unit type for the rates.
|
||||
*
|
||||
* @returns An object containing the formatted received and transmitted rates.
|
||||
*/
|
||||
export const computeNetwork = (
|
||||
round: Variable<boolean>,
|
||||
interfaceNameVar: Variable<string>,
|
||||
dataType: Variable<RateUnit>,
|
||||
): NetworkResourceData => {
|
||||
const rateUnit = dataType.get();
|
||||
const interfaceName = interfaceNameVar ? interfaceNameVar.get() : '';
|
||||
|
||||
const DEFAULT_NETSTAT_DATA = GET_DEFAULT_NETSTAT_DATA(rateUnit);
|
||||
try {
|
||||
const { rx, tx, name } = getNetworkUsage(interfaceName);
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (!name) {
|
||||
return DEFAULT_NETSTAT_DATA;
|
||||
}
|
||||
|
||||
if (previousNetUsage.time === 0) {
|
||||
previousNetUsage = { rx, tx, time: currentTime };
|
||||
return DEFAULT_NETSTAT_DATA;
|
||||
}
|
||||
|
||||
const timeDiff = Math.max((currentTime - previousNetUsage.time) / 1000, 1);
|
||||
const rxRate = (rx - previousNetUsage.rx) / timeDiff;
|
||||
const txRate = (tx - previousNetUsage.tx) / timeDiff;
|
||||
|
||||
previousNetUsage = { rx, tx, time: currentTime };
|
||||
|
||||
return {
|
||||
in: formatRate(rxRate, rateUnit, round.get()),
|
||||
out: formatRate(txRate, rateUnit, round.get()),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calculating network usage:', error);
|
||||
return DEFAULT_NETSTAT_DATA;
|
||||
}
|
||||
};
|
||||
124
src/components/bar/modules/netstat/index.tsx
Normal file
124
src/components/bar/modules/netstat/index.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { networkService } from 'src/lib/constants/services';
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { computeNetwork } from './helpers';
|
||||
import { BarBoxChild, NetstatLabelType, RateUnit } from 'src/lib/types/bar';
|
||||
import { NetworkResourceData } from 'src/lib/types/customModules/network';
|
||||
import { NETWORK_LABEL_TYPES } from 'src/lib/types/defaults/bar';
|
||||
import { GET_DEFAULT_NETSTAT_DATA } from 'src/lib/types/defaults/netstat';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { bind, Variable } from 'astal';
|
||||
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const {
|
||||
label,
|
||||
labelType,
|
||||
networkInterface,
|
||||
rateUnit,
|
||||
dynamicIcon,
|
||||
icon,
|
||||
round,
|
||||
leftClick,
|
||||
rightClick,
|
||||
middleClick,
|
||||
pollingInterval,
|
||||
} = options.bar.customModules.netstat;
|
||||
|
||||
export const networkUsage = Variable<NetworkResourceData>(GET_DEFAULT_NETSTAT_DATA(rateUnit.get()));
|
||||
|
||||
const netstatPoller = new FunctionPoller<
|
||||
NetworkResourceData,
|
||||
[round: Variable<boolean>, interfaceNameVar: Variable<string>, dataType: Variable<RateUnit>]
|
||||
>(
|
||||
networkUsage,
|
||||
[bind(rateUnit), bind(networkInterface), bind(round)],
|
||||
bind(pollingInterval),
|
||||
computeNetwork,
|
||||
round,
|
||||
networkInterface,
|
||||
rateUnit,
|
||||
);
|
||||
|
||||
netstatPoller.initialize('netstat');
|
||||
|
||||
export const Netstat = (): BarBoxChild => {
|
||||
const renderNetworkLabel = (lblType: NetstatLabelType, networkService: NetworkResourceData): string => {
|
||||
switch (lblType) {
|
||||
case 'in':
|
||||
return `↓ ${networkService.in}`;
|
||||
case 'out':
|
||||
return `↑ ${networkService.out}`;
|
||||
default:
|
||||
return `↓ ${networkService.in} ↑ ${networkService.out}`;
|
||||
}
|
||||
};
|
||||
|
||||
const iconBinding = Variable.derive(
|
||||
[bind(networkService, 'primary'), bind(networkService, 'wifi'), bind(networkService, 'wired')],
|
||||
(pmry, wfi, wrd) => {
|
||||
if (pmry === AstalNetwork.Primary.WIRED) {
|
||||
return wrd?.icon_name;
|
||||
}
|
||||
return wfi?.icon_name;
|
||||
},
|
||||
);
|
||||
|
||||
const labelBinding = Variable.derive(
|
||||
[bind(networkUsage), bind(labelType)],
|
||||
(networkService: NetworkResourceData, lblTyp: NetstatLabelType) => renderNetworkLabel(lblTyp, networkService),
|
||||
);
|
||||
|
||||
const netstatModule = Module({
|
||||
useTextIcon: bind(dynamicIcon).as((useDynamicIcon) => !useDynamicIcon),
|
||||
icon: iconBinding(),
|
||||
textIcon: bind(icon),
|
||||
label: labelBinding(),
|
||||
tooltipText: bind(labelType).as((lblTyp) => {
|
||||
return lblTyp === 'full' ? 'Ingress / Egress' : lblTyp === 'in' ? 'Ingress' : 'Egress';
|
||||
}),
|
||||
boxClass: 'netstat',
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
fn: () => {
|
||||
labelType.set(
|
||||
NETWORK_LABEL_TYPES[
|
||||
(NETWORK_LABEL_TYPES.indexOf(labelType.get()) + 1) % NETWORK_LABEL_TYPES.length
|
||||
] as NetstatLabelType,
|
||||
);
|
||||
},
|
||||
},
|
||||
onScrollDown: {
|
||||
fn: () => {
|
||||
labelType.set(
|
||||
NETWORK_LABEL_TYPES[
|
||||
(NETWORK_LABEL_TYPES.indexOf(labelType.get()) - 1 + NETWORK_LABEL_TYPES.length) %
|
||||
NETWORK_LABEL_TYPES.length
|
||||
] as NetstatLabelType,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
labelBinding.drop();
|
||||
iconBinding.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return netstatModule;
|
||||
};
|
||||
87
src/components/bar/modules/network/helpers.ts
Normal file
87
src/components/bar/modules/network/helpers.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { networkService } from 'src/lib/constants/services';
|
||||
|
||||
export const wiredIcon: Variable<string> = Variable('');
|
||||
export const wirelessIcon: Variable<string> = Variable('');
|
||||
|
||||
let wiredIconBinding: Variable<void>;
|
||||
let wirelessIconBinding: Variable<void>;
|
||||
|
||||
/**
|
||||
* Handles the wired network icon binding.
|
||||
*
|
||||
* This function sets up the binding for the wired network icon. It first drops any existing binding,
|
||||
* then checks if the wired network service is available. If available, it binds the icon name to the `wiredIcon` variable.
|
||||
*/
|
||||
const handleWiredIcon = (): void => {
|
||||
if (wiredIconBinding) {
|
||||
wiredIconBinding();
|
||||
wiredIconBinding.drop();
|
||||
}
|
||||
|
||||
if (!networkService.wired) {
|
||||
return;
|
||||
}
|
||||
|
||||
wiredIconBinding = Variable.derive([bind(networkService.wired, 'iconName')], (icon) => {
|
||||
wiredIcon.set(icon);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the wireless network icon binding.
|
||||
*
|
||||
* This function sets up the binding for the wireless network icon. It first drops any existing binding,
|
||||
* then checks if the wireless network service is available. If available, it binds the icon name to the `wirelessIcon` variable.
|
||||
*/
|
||||
const handleWirelessIcon = (): void => {
|
||||
if (wirelessIconBinding) {
|
||||
wirelessIconBinding();
|
||||
wirelessIconBinding.drop();
|
||||
}
|
||||
|
||||
if (!networkService.wifi) {
|
||||
return;
|
||||
}
|
||||
|
||||
wirelessIconBinding = Variable.derive([bind(networkService.wifi, 'iconName')], (icon) => {
|
||||
wirelessIcon.set(icon);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the frequency value to MHz.
|
||||
*
|
||||
* This function takes a frequency value in kHz and formats it to MHz with two decimal places.
|
||||
*
|
||||
* @param frequency The frequency value in kHz.
|
||||
*
|
||||
* @returns The formatted frequency value in MHz as a string.
|
||||
*/
|
||||
const formatFrequency = (frequency: number): string => {
|
||||
return `${(frequency / 1000).toFixed(2)}MHz`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the WiFi information for display.
|
||||
*
|
||||
* This function takes a WiFi object and formats its SSID, signal strength, and frequency for display.
|
||||
* If any of these values are not available, it provides default values.
|
||||
*
|
||||
* @param wifi The WiFi object containing SSID, signal strength, and frequency information.
|
||||
*
|
||||
* @returns A formatted string containing the WiFi information.
|
||||
*/
|
||||
export const formatWifiInfo = (wifi: AstalNetwork.Wifi | null): string => {
|
||||
const netSsid = wifi?.ssid ? wifi.ssid : 'None';
|
||||
const wifiStrength = wifi?.strength ? wifi.strength : '--';
|
||||
const wifiFreq = wifi?.frequency ? formatFrequency(wifi.frequency) : '--';
|
||||
|
||||
return `Network: ${netSsid} \nSignal Strength: ${wifiStrength}% \nFrequency: ${wifiFreq}`;
|
||||
};
|
||||
|
||||
Variable.derive([bind(networkService, 'wifi')], () => {
|
||||
handleWiredIcon();
|
||||
handleWirelessIcon();
|
||||
});
|
||||
121
src/components/bar/modules/network/index.tsx
Normal file
121
src/components/bar/modules/network/index.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { networkService } from 'src/lib/constants/services.js';
|
||||
import options from 'src/options';
|
||||
import { openMenu } from '../../utils/menu';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { onPrimaryClick, onSecondaryClick, onMiddleClick, onScroll } from 'src/lib/shared/eventHandlers';
|
||||
import { Astal, Gtk } from 'astal/gtk3';
|
||||
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
|
||||
import { useHook } from 'src/lib/shared/hookHandler';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.js';
|
||||
import { formatWifiInfo, wiredIcon, wirelessIcon } from './helpers';
|
||||
|
||||
const { label, truncation, truncation_size, rightClick, middleClick, scrollDown, scrollUp, showWifiInfo } =
|
||||
options.bar.network;
|
||||
|
||||
const Network = (): BarBoxChild => {
|
||||
const iconBinding = Variable.derive(
|
||||
[bind(networkService, 'primary'), bind(wiredIcon), bind(wirelessIcon)],
|
||||
(primaryNetwork, wiredIcon, wifiIcon) => {
|
||||
const isWired = primaryNetwork === AstalNetwork.Primary.WIRED;
|
||||
const iconName = isWired ? wiredIcon : wifiIcon;
|
||||
|
||||
return iconName;
|
||||
},
|
||||
);
|
||||
|
||||
const networkIcon = <icon className={'bar-button-icon network-icon'} icon={iconBinding()} />;
|
||||
|
||||
const networkLabel = Variable.derive(
|
||||
[
|
||||
bind(networkService, 'primary'),
|
||||
bind(networkService, 'wifi'),
|
||||
bind(label),
|
||||
bind(truncation),
|
||||
bind(truncation_size),
|
||||
bind(showWifiInfo),
|
||||
],
|
||||
(primaryNetwork, networkWifi, showLabel, trunc, tSize, showWifiInfo) => {
|
||||
if (!showLabel) {
|
||||
return <box />;
|
||||
}
|
||||
if (primaryNetwork === AstalNetwork.Primary.WIRED) {
|
||||
return <label className={'bar-button-label network-label'} label={'Wired'.substring(0, tSize)} />;
|
||||
}
|
||||
return (
|
||||
<label
|
||||
className={'bar-button-label network-label'}
|
||||
label={
|
||||
networkWifi?.ssid ? `${trunc ? networkWifi.ssid.substring(0, tSize) : networkWifi.ssid}` : '--'
|
||||
}
|
||||
tooltipText={showWifiInfo ? formatWifiInfo(networkWifi) : ''}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const componentClassName = Variable.derive(
|
||||
[bind(options.theme.bar.buttons.style), bind(options.bar.network.label)],
|
||||
(style, showLabel) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `network-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
);
|
||||
|
||||
const componentChildren = [networkIcon, networkLabel()];
|
||||
|
||||
const component = (
|
||||
<box
|
||||
vexpand
|
||||
valign={Gtk.Align.FILL}
|
||||
className={componentClassName()}
|
||||
onDestroy={() => {
|
||||
iconBinding.drop();
|
||||
networkLabel.drop();
|
||||
componentClassName.drop();
|
||||
}}
|
||||
>
|
||||
{componentChildren}
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
boxClass: 'network',
|
||||
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, 'networkmenu');
|
||||
});
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Network };
|
||||
115
src/components/bar/modules/notifications/index.tsx
Normal file
115
src/components/bar/modules/notifications/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import { Astal, Gtk } from 'astal/gtk3';
|
||||
import { openMenu } from '../../utils/menu';
|
||||
import options from 'src/options';
|
||||
import { filterNotifications } from 'src/lib/shared/notifications.js';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { useHook } from 'src/lib/shared/hookHandler';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
|
||||
const { show_total, rightClick, middleClick, scrollUp, scrollDown, hideCountWhenZero } = options.bar.notifications;
|
||||
const { ignore } = options.notifications;
|
||||
|
||||
const notifs = AstalNotifd.get_default();
|
||||
|
||||
export const Notifications = (): BarBoxChild => {
|
||||
const componentClassName = Variable.derive(
|
||||
[bind(options.theme.bar.buttons.style), bind(show_total)],
|
||||
(style: string, showTotal: boolean) => {
|
||||
const styleMap: Record<string, string> = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `notifications-container ${styleMap[style]} ${!showTotal ? 'no-label' : ''}`;
|
||||
},
|
||||
);
|
||||
|
||||
const boxChildren = Variable.derive(
|
||||
[
|
||||
bind(notifs, 'notifications'),
|
||||
bind(notifs, 'dontDisturb'),
|
||||
bind(show_total),
|
||||
bind(ignore),
|
||||
bind(hideCountWhenZero),
|
||||
],
|
||||
(
|
||||
notif: AstalNotifd.Notification[],
|
||||
dnd: boolean,
|
||||
showTotal: boolean,
|
||||
ignoredNotifs: string[],
|
||||
hideCountForZero: boolean,
|
||||
) => {
|
||||
const filteredNotifications = filterNotifications(notif, ignoredNotifs);
|
||||
|
||||
const notifIcon = (
|
||||
<label
|
||||
halign={Gtk.Align.CENTER}
|
||||
className={'bar-button-icon notifications txt-icon bar'}
|
||||
label={dnd ? '' : filteredNotifications.length > 0 ? '' : ''}
|
||||
/>
|
||||
);
|
||||
|
||||
const notifLabel = (
|
||||
<label
|
||||
halign={Gtk.Align.CENTER}
|
||||
className={'bar-button-label notifications'}
|
||||
label={filteredNotifications.length.toString()}
|
||||
/>
|
||||
);
|
||||
|
||||
if (showTotal) {
|
||||
if (hideCountForZero && filteredNotifications.length === 0) {
|
||||
return [notifIcon];
|
||||
}
|
||||
return [notifIcon, notifLabel];
|
||||
}
|
||||
return [notifIcon];
|
||||
},
|
||||
);
|
||||
|
||||
const component = (
|
||||
<box halign={Gtk.Align.START} className={componentClassName()}>
|
||||
<box halign={Gtk.Align.START} className={'bar-notifications'}>
|
||||
{boxChildren()}
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
boxClass: 'notifications',
|
||||
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, 'notificationsmenu');
|
||||
});
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
39
src/components/bar/modules/power/index.tsx
Normal file
39
src/components/bar/modules/power/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { bind } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { icon, leftClick, rightClick, middleClick, scrollUp, scrollDown } = options.bar.customModules.power;
|
||||
|
||||
export const Power = (): BarBoxChild => {
|
||||
const powerModule = Module({
|
||||
tooltipText: 'Power Menu',
|
||||
textIcon: bind(icon),
|
||||
boxClass: 'powermodule',
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return powerModule;
|
||||
};
|
||||
48
src/components/bar/modules/ram/helpers/index.ts
Normal file
48
src/components/bar/modules/ram/helpers/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { divide } from 'src/components/bar/utils/helpers';
|
||||
import { GenericResourceData } from 'src/lib/types/customModules/generic';
|
||||
import { GLib, Variable } from 'astal';
|
||||
|
||||
/**
|
||||
* Calculates the RAM usage.
|
||||
*
|
||||
* This function reads the memory information from the /proc/meminfo file and calculates the total, used, and available RAM.
|
||||
* It returns an object containing these values along with the percentage of used RAM.
|
||||
*
|
||||
* @param round A Variable<boolean> indicating whether to round the percentage value.
|
||||
*
|
||||
* @returns An object containing the total, used, free RAM in bytes, and the percentage of used RAM.
|
||||
*/
|
||||
export const calculateRamUsage = (round: Variable<boolean>): GenericResourceData => {
|
||||
try {
|
||||
const [success, meminfoBytes] = GLib.file_get_contents('/proc/meminfo');
|
||||
|
||||
if (!success || !meminfoBytes) {
|
||||
throw new Error('Failed to read /proc/meminfo or file content is null.');
|
||||
}
|
||||
|
||||
const meminfo = new TextDecoder('utf-8').decode(meminfoBytes);
|
||||
|
||||
const totalMatch = meminfo.match(/MemTotal:\s+(\d+)/);
|
||||
const availableMatch = meminfo.match(/MemAvailable:\s+(\d+)/);
|
||||
|
||||
if (!totalMatch || !availableMatch) {
|
||||
throw new Error('Failed to parse /proc/meminfo for memory values.');
|
||||
}
|
||||
|
||||
const totalRamInBytes = parseInt(totalMatch[1], 10) * 1024;
|
||||
const availableRamInBytes = parseInt(availableMatch[1], 10) * 1024;
|
||||
|
||||
let usedRam = totalRamInBytes - availableRamInBytes;
|
||||
usedRam = isNaN(usedRam) || usedRam < 0 ? 0 : usedRam;
|
||||
|
||||
return {
|
||||
percentage: divide([totalRamInBytes, usedRam], round.get()),
|
||||
total: totalRamInBytes,
|
||||
used: usedRam,
|
||||
free: availableRamInBytes,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calculating RAM usage:', error);
|
||||
return { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
}
|
||||
};
|
||||
85
src/components/bar/modules/ram/index.tsx
Normal file
85
src/components/bar/modules/ram/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { calculateRamUsage } from './helpers';
|
||||
import { formatTooltip, inputHandler, renderResourceLabel } from 'src/components/bar/utils/helpers';
|
||||
import { LABEL_TYPES } from 'src/lib/types/defaults/bar';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { GenericResourceData } from 'src/lib/types/customModules/generic';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { BarBoxChild, ResourceLabelType } from 'src/lib/types/bar';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { label, labelType, round, leftClick, rightClick, middleClick, pollingInterval, icon } =
|
||||
options.bar.customModules.ram;
|
||||
|
||||
const defaultRamData: GenericResourceData = { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
const ramUsage = Variable<GenericResourceData>(defaultRamData);
|
||||
|
||||
const ramPoller = new FunctionPoller<GenericResourceData, [Variable<boolean>]>(
|
||||
ramUsage,
|
||||
[bind(round)],
|
||||
bind(pollingInterval),
|
||||
calculateRamUsage,
|
||||
round,
|
||||
);
|
||||
|
||||
ramPoller.initialize('ram');
|
||||
|
||||
export const Ram = (): BarBoxChild => {
|
||||
const labelBinding = Variable.derive(
|
||||
[bind(ramUsage), bind(labelType), bind(round)],
|
||||
(rmUsg: GenericResourceData, lblTyp: ResourceLabelType, round: boolean) => {
|
||||
const returnValue = renderResourceLabel(lblTyp, rmUsg, round);
|
||||
|
||||
return returnValue;
|
||||
},
|
||||
);
|
||||
|
||||
const ramModule = Module({
|
||||
textIcon: bind(icon),
|
||||
label: labelBinding(),
|
||||
tooltipText: bind(labelType).as((lblTyp) => {
|
||||
return formatTooltip('RAM', lblTyp);
|
||||
}),
|
||||
boxClass: 'ram',
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
fn: () => {
|
||||
labelType.set(
|
||||
LABEL_TYPES[
|
||||
(LABEL_TYPES.indexOf(labelType.get()) + 1) % LABEL_TYPES.length
|
||||
] as ResourceLabelType,
|
||||
);
|
||||
},
|
||||
},
|
||||
onScrollDown: {
|
||||
fn: () => {
|
||||
labelType.set(
|
||||
LABEL_TYPES[
|
||||
(LABEL_TYPES.indexOf(labelType.get()) - 1 + LABEL_TYPES.length) % LABEL_TYPES.length
|
||||
] as ResourceLabelType,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
labelBinding.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return ramModule;
|
||||
};
|
||||
39
src/components/bar/modules/storage/helpers/index.ts
Normal file
39
src/components/bar/modules/storage/helpers/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import GTop from 'gi://GTop';
|
||||
|
||||
import { divide } from 'src/components/bar/utils/helpers';
|
||||
import { Variable } from 'astal';
|
||||
import { GenericResourceData } from 'src/lib/types/customModules/generic';
|
||||
|
||||
/**
|
||||
* Computes the storage usage for the root filesystem.
|
||||
*
|
||||
* This function calculates the total, used, and available storage for the root filesystem.
|
||||
* It returns an object containing these values along with the percentage of used storage.
|
||||
*
|
||||
* @param round A Variable<boolean> indicating whether to round the percentage value.
|
||||
*
|
||||
* @returns An object containing the total, used, free storage in bytes, and the percentage of used storage.
|
||||
*
|
||||
* FIX: Consolidate with Storage service class
|
||||
*/
|
||||
export const computeStorage = (round: Variable<boolean>): GenericResourceData => {
|
||||
try {
|
||||
const currentFsUsage = new GTop.glibtop_fsusage();
|
||||
|
||||
GTop.glibtop_get_fsusage(currentFsUsage, '/');
|
||||
|
||||
const total = currentFsUsage.blocks * currentFsUsage.block_size;
|
||||
const available = currentFsUsage.bavail * currentFsUsage.block_size;
|
||||
const used = total - available;
|
||||
|
||||
return {
|
||||
total,
|
||||
used,
|
||||
free: available,
|
||||
percentage: divide([total, used], round.get()),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calculating RAM usage:', error);
|
||||
return { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
}
|
||||
};
|
||||
83
src/components/bar/modules/storage/index.tsx
Normal file
83
src/components/bar/modules/storage/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { formatTooltip, inputHandler, renderResourceLabel } from 'src/components/bar/utils/helpers';
|
||||
import { computeStorage } from './helpers';
|
||||
import { BarBoxChild, ResourceLabelType } from 'src/lib/types/bar';
|
||||
import { GenericResourceData } from 'src/lib/types/customModules/generic';
|
||||
import { LABEL_TYPES } from 'src/lib/types/defaults/bar';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { label, labelType, icon, round, leftClick, rightClick, middleClick, pollingInterval } =
|
||||
options.bar.customModules.storage;
|
||||
|
||||
const defaultStorageData = { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
|
||||
const storageUsage = Variable<GenericResourceData>(defaultStorageData);
|
||||
|
||||
const storagePoller = new FunctionPoller<GenericResourceData, [Variable<boolean>]>(
|
||||
storageUsage,
|
||||
[bind(round)],
|
||||
bind(pollingInterval),
|
||||
computeStorage,
|
||||
round,
|
||||
);
|
||||
|
||||
storagePoller.initialize('storage');
|
||||
|
||||
export const Storage = (): BarBoxChild => {
|
||||
const labelBinding = Variable.derive(
|
||||
[bind(storageUsage), bind(labelType), bind(round)],
|
||||
(storage, lblTyp, round) => {
|
||||
return renderResourceLabel(lblTyp, storage, round);
|
||||
},
|
||||
);
|
||||
const storageModule = Module({
|
||||
textIcon: bind(icon),
|
||||
label: labelBinding(),
|
||||
tooltipText: bind(labelType).as((lblTyp) => {
|
||||
return formatTooltip('Storage', lblTyp);
|
||||
}),
|
||||
boxClass: 'storage',
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
fn: () => {
|
||||
labelType.set(
|
||||
LABEL_TYPES[
|
||||
(LABEL_TYPES.indexOf(labelType.get()) + 1) % LABEL_TYPES.length
|
||||
] as ResourceLabelType,
|
||||
);
|
||||
},
|
||||
},
|
||||
onScrollDown: {
|
||||
fn: () => {
|
||||
labelType.set(
|
||||
LABEL_TYPES[
|
||||
(LABEL_TYPES.indexOf(labelType.get()) - 1 + LABEL_TYPES.length) % LABEL_TYPES.length
|
||||
] as ResourceLabelType,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
labelBinding.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return storageModule;
|
||||
};
|
||||
38
src/components/bar/modules/submap/helpers/index.ts
Normal file
38
src/components/bar/modules/submap/helpers/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Variable } from 'astal';
|
||||
import { hyprlandService } from 'src/lib/constants/services';
|
||||
|
||||
/**
|
||||
* Determines if a submap is enabled based on the provided submap name.
|
||||
*
|
||||
* This function checks if the given submap name is not 'default' and returns the appropriate enabled or disabled string.
|
||||
*
|
||||
* @param submap The name of the submap to check.
|
||||
* @param enabled The string to return if the submap is enabled.
|
||||
* @param disabled The string to return if the submap is disabled.
|
||||
*
|
||||
* @returns The enabled string if the submap is not 'default', otherwise the disabled string.
|
||||
*/
|
||||
export const isSubmapEnabled = (submap: string, enabled: string, disabled: string): string => {
|
||||
return submap !== 'default' ? enabled : disabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the initial submap status and updates the provided variable.
|
||||
*
|
||||
* This function gets the initial submap status from the `hyprlandService` and updates the `submapStatus` variable.
|
||||
* It removes any newline characters from the submap status and sets it to 'default' if the status is 'unknown request'.
|
||||
*
|
||||
* @param submapStatus The variable to update with the initial submap status.
|
||||
*/
|
||||
export const getInitialSubmap = (submapStatus: Variable<string>): void => {
|
||||
let submap = hyprlandService.message('submap');
|
||||
|
||||
const newLineCarriage = /\n/g;
|
||||
submap = submap.replace(newLineCarriage, '');
|
||||
|
||||
if (submap === 'unknown request') {
|
||||
submap = 'default';
|
||||
}
|
||||
|
||||
submapStatus.set(submap);
|
||||
};
|
||||
88
src/components/bar/modules/submap/index.tsx
Normal file
88
src/components/bar/modules/submap/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { hyprlandService } from 'src/lib/constants/services';
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { capitalizeFirstLetter } from 'src/lib/utils';
|
||||
import { getInitialSubmap, isSubmapEnabled } from './helpers';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const {
|
||||
label,
|
||||
showSubmapName,
|
||||
enabledIcon,
|
||||
disabledIcon,
|
||||
enabledText,
|
||||
disabledText,
|
||||
leftClick,
|
||||
rightClick,
|
||||
middleClick,
|
||||
scrollUp,
|
||||
scrollDown,
|
||||
} = options.bar.customModules.submap;
|
||||
|
||||
const submapStatus: Variable<string> = Variable('default');
|
||||
|
||||
hyprlandService.connect('submap', (_, currentSubmap) => {
|
||||
if (currentSubmap.length === 0) {
|
||||
submapStatus.set('default');
|
||||
} else {
|
||||
submapStatus.set(currentSubmap);
|
||||
}
|
||||
});
|
||||
|
||||
getInitialSubmap(submapStatus);
|
||||
|
||||
export const Submap = (): BarBoxChild => {
|
||||
const submapLabel = Variable.derive(
|
||||
[bind(submapStatus), bind(enabledText), bind(disabledText), bind(showSubmapName)],
|
||||
(status, enabled, disabled, showSmName) => {
|
||||
if (showSmName) {
|
||||
return capitalizeFirstLetter(status);
|
||||
}
|
||||
return isSubmapEnabled(status, enabled, disabled);
|
||||
},
|
||||
);
|
||||
const submapIcon = Variable.derive(
|
||||
[bind(submapStatus), bind(enabledIcon), bind(disabledIcon)],
|
||||
(status, enabled, disabled) => {
|
||||
return isSubmapEnabled(status, enabled, disabled);
|
||||
},
|
||||
);
|
||||
|
||||
const submapModule = Module({
|
||||
textIcon: submapIcon(),
|
||||
tooltipText: submapLabel(),
|
||||
label: submapLabel(),
|
||||
showLabelBinding: bind(label),
|
||||
boxClass: 'submap',
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
submapLabel.drop();
|
||||
submapIcon.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return submapModule;
|
||||
};
|
||||
126
src/components/bar/modules/systray/index.tsx
Normal file
126
src/components/bar/modules/systray/index.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { isMiddleClick, isPrimaryClick, isSecondaryClick, Notify } from '../../../../lib/utils';
|
||||
import options from '../../../../options';
|
||||
import AstalTray from 'gi://AstalTray?version=0.1';
|
||||
import { bind, Gio, Variable } from 'astal';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { Gdk, Gtk } from 'astal/gtk3';
|
||||
import { BindableChild } from 'astal/gtk3/astalify';
|
||||
|
||||
const systemtray = AstalTray.get_default();
|
||||
const { ignore, customIcons } = options.bar.systray;
|
||||
|
||||
//TODO: Connect to `notify::menu-model` and `notify::action-group` to have up to date menu and action group
|
||||
const createMenu = (menuModel: Gio.MenuModel, actionGroup: Gio.ActionGroup): Gtk.Menu => {
|
||||
const menu = Gtk.Menu.new_from_model(menuModel);
|
||||
menu.insert_action_group('dbusmenu', actionGroup);
|
||||
|
||||
return menu;
|
||||
};
|
||||
|
||||
const MenuCustomIcon = ({ iconLabel, iconColor, item }: MenuCustomIconProps): JSX.Element => {
|
||||
return (
|
||||
<label
|
||||
className={'systray-icon txt-icon'}
|
||||
label={iconLabel}
|
||||
css={iconColor ? `color: ${iconColor}` : ''}
|
||||
tooltipMarkup={bind(item, 'tooltipMarkup')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuDefaultIcon = ({ item }: MenuEntryProps): JSX.Element => {
|
||||
return <icon className={'systray-icon'} gIcon={bind(item, 'gicon')} tooltipMarkup={bind(item, 'tooltipMarkup')} />;
|
||||
};
|
||||
|
||||
const MenuEntry = ({ item, child }: MenuEntryProps): JSX.Element => {
|
||||
const menu = createMenu(item.menuModel, item.actionGroup);
|
||||
|
||||
return (
|
||||
<button
|
||||
cursor={'pointer'}
|
||||
onClick={(self, event) => {
|
||||
if (isPrimaryClick(event)) {
|
||||
item.activate(0, 0);
|
||||
}
|
||||
|
||||
if (isSecondaryClick(event)) {
|
||||
menu?.popup_at_widget(self, Gdk.Gravity.NORTH, Gdk.Gravity.SOUTH, null);
|
||||
}
|
||||
|
||||
if (isMiddleClick(event)) {
|
||||
Notify({ summary: 'App Name', body: item.id });
|
||||
}
|
||||
}}
|
||||
onDestroy={() => menu?.destroy()}
|
||||
>
|
||||
{child}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const SysTray = (): BarBoxChild => {
|
||||
const isVis = Variable(false);
|
||||
|
||||
const componentChildren = Variable.derive(
|
||||
[bind(systemtray, 'items'), bind(ignore), bind(customIcons)],
|
||||
(items, ignored, custIcons) => {
|
||||
const filteredTray = items.filter(({ id }) => !ignored.includes(id) && id !== null);
|
||||
|
||||
isVis.set(filteredTray.length > 0);
|
||||
|
||||
return filteredTray.map((item) => {
|
||||
const matchedCustomIcon = Object.keys(custIcons).find((iconRegex) => item.id.match(iconRegex));
|
||||
|
||||
if (matchedCustomIcon !== undefined) {
|
||||
const iconLabel = custIcons[matchedCustomIcon].icon || '';
|
||||
const iconColor = custIcons[matchedCustomIcon].color;
|
||||
|
||||
return (
|
||||
<MenuEntry item={item}>
|
||||
<MenuCustomIcon iconLabel={iconLabel} iconColor={iconColor} item={item} />
|
||||
</MenuEntry>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MenuEntry item={item}>
|
||||
<MenuDefaultIcon item={item} />
|
||||
</MenuEntry>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const component = (
|
||||
<box
|
||||
className={'systray-container'}
|
||||
onDestroy={() => {
|
||||
isVis.drop();
|
||||
componentChildren.drop();
|
||||
}}
|
||||
>
|
||||
{componentChildren()}
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
boxClass: 'systray',
|
||||
isVis,
|
||||
isBox: true,
|
||||
props: {},
|
||||
};
|
||||
};
|
||||
|
||||
interface MenuCustomIconProps {
|
||||
iconLabel: string;
|
||||
iconColor: string;
|
||||
item: AstalTray.TrayItem;
|
||||
}
|
||||
|
||||
interface MenuEntryProps {
|
||||
item: AstalTray.TrayItem;
|
||||
child?: BindableChild;
|
||||
}
|
||||
|
||||
export { SysTray };
|
||||
81
src/components/bar/modules/updates/index.tsx
Normal file
81
src/components/bar/modules/updates/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { BashPoller } from 'src/lib/poller/BashPoller';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const {
|
||||
updateCommand,
|
||||
label,
|
||||
padZero,
|
||||
pollingInterval,
|
||||
icon,
|
||||
leftClick,
|
||||
rightClick,
|
||||
middleClick,
|
||||
scrollUp,
|
||||
scrollDown,
|
||||
} = options.bar.customModules.updates;
|
||||
|
||||
const pendingUpdates: Variable<string> = Variable('0');
|
||||
const postInputUpdater = Variable(true);
|
||||
|
||||
const processUpdateCount = (updateCount: string): string => {
|
||||
if (!padZero.get()) return updateCount;
|
||||
return `${updateCount.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const updatesPoller = new BashPoller<string, []>(
|
||||
pendingUpdates,
|
||||
[bind(padZero), bind(postInputUpdater)],
|
||||
bind(pollingInterval),
|
||||
updateCommand.get(),
|
||||
processUpdateCount,
|
||||
);
|
||||
|
||||
updatesPoller.initialize('updates');
|
||||
|
||||
const updatesIcon = Variable.derive(
|
||||
[bind(icon.pending), bind(icon.updated), bind(pendingUpdates)],
|
||||
(pendingIcon, updatedIcon, pUpdates) => {
|
||||
return pUpdates === '0' ? updatedIcon : pendingIcon;
|
||||
},
|
||||
);
|
||||
export const Updates = (): BarBoxChild => {
|
||||
const updatesModule = Module({
|
||||
textIcon: updatesIcon(),
|
||||
tooltipText: bind(pendingUpdates).as((v) => `${v} updates available`),
|
||||
boxClass: 'updates',
|
||||
label: bind(pendingUpdates),
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(
|
||||
self,
|
||||
{
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
},
|
||||
postInputUpdater,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return updatesModule;
|
||||
};
|
||||
32
src/components/bar/modules/volume/helpers/index.ts
Normal file
32
src/components/bar/modules/volume/helpers/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { VolumeIcons } from 'src/lib/types/volume';
|
||||
|
||||
const icons: VolumeIcons = {
|
||||
101: '',
|
||||
66: '',
|
||||
34: '',
|
||||
1: '',
|
||||
0: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the appropriate volume icon based on the volume level and mute status.
|
||||
*
|
||||
* This function returns the corresponding volume icon based on the provided volume level and mute status.
|
||||
* It uses predefined mappings for volume icons.
|
||||
*
|
||||
* @param isMuted A boolean indicating whether the volume is muted.
|
||||
* @param vol The current volume level as a number between 0 and 1.
|
||||
*
|
||||
* @returns The corresponding volume icon as a string.
|
||||
*/
|
||||
export const getIcon = (isMuted: boolean, vol: number): string => {
|
||||
if (isMuted) return icons[0];
|
||||
|
||||
const foundVol = [101, 66, 34, 1, 0].find((threshold) => threshold <= vol * 100);
|
||||
|
||||
if (foundVol !== undefined) {
|
||||
return icons[foundVol];
|
||||
}
|
||||
|
||||
return icons[101];
|
||||
};
|
||||
109
src/components/bar/modules/volume/index.tsx
Normal file
109
src/components/bar/modules/volume/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { audioService } from 'src/lib/constants/services.js';
|
||||
import { openMenu } from '../../utils/menu.js';
|
||||
import options from 'src/options';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import Variable from 'astal/variable.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import { useHook } from 'src/lib/shared/hookHandler.js';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js';
|
||||
import { getIcon } from './helpers/index.js';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.js';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { rightClick, middleClick, scrollUp, scrollDown } = options.bar.volume;
|
||||
|
||||
const Volume = (): BarBoxChild => {
|
||||
const volumeIcon = (isMuted: boolean, vol: number): JSX.Element => {
|
||||
return <label className={'bar-button-icon volume txt-icon bar'} label={getIcon(isMuted, vol)} />;
|
||||
};
|
||||
|
||||
const volumeLabel = (vol: number): JSX.Element => {
|
||||
return <label className={'bar-button-label volume'} label={`${Math.round(vol * 100)}%`} />;
|
||||
};
|
||||
|
||||
const componentTooltip = Variable.derive(
|
||||
[
|
||||
bind(audioService.defaultSpeaker, 'description'),
|
||||
bind(audioService.defaultSpeaker, 'volume'),
|
||||
bind(audioService.defaultSpeaker, 'mute'),
|
||||
],
|
||||
(desc, vol, isMuted) => {
|
||||
return `${getIcon(isMuted, vol)} ${desc}`;
|
||||
},
|
||||
);
|
||||
const componentClassName = Variable.derive(
|
||||
[options.theme.bar.buttons.style, options.bar.volume.label],
|
||||
(style, showLabel) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `volume-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
);
|
||||
const componentChildren = Variable.derive(
|
||||
[
|
||||
bind(options.bar.volume.label),
|
||||
bind(audioService.defaultSpeaker, 'volume'),
|
||||
bind(audioService.defaultSpeaker, 'mute'),
|
||||
],
|
||||
(showLabel, vol, isMuted) => {
|
||||
if (showLabel) {
|
||||
return [volumeIcon(isMuted, vol), volumeLabel(vol)];
|
||||
}
|
||||
return [volumeIcon(isMuted, vol)];
|
||||
},
|
||||
);
|
||||
const component = (
|
||||
<box
|
||||
vexpand
|
||||
tooltipText={componentTooltip()}
|
||||
className={componentClassName()}
|
||||
onDestroy={() => {
|
||||
componentTooltip.drop();
|
||||
componentClassName.drop();
|
||||
componentChildren.drop();
|
||||
}}
|
||||
>
|
||||
{componentChildren()}
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
boxClass: 'volume',
|
||||
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, 'audiomenu');
|
||||
});
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Volume };
|
||||
59
src/components/bar/modules/weather/index.tsx
Normal file
59
src/components/bar/modules/weather/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { getWeatherStatusTextIcon, globalWeatherVar } from 'src/globals/weather';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { label, unit, leftClick, rightClick, middleClick, scrollUp, scrollDown } = options.bar.customModules.weather;
|
||||
|
||||
export const Weather = (): BarBoxChild => {
|
||||
const iconBinding = Variable.derive([bind(globalWeatherVar)], (wthr) => {
|
||||
const weatherStatusIcon = getWeatherStatusTextIcon(wthr);
|
||||
return weatherStatusIcon;
|
||||
});
|
||||
|
||||
const labelBinding = Variable.derive([bind(globalWeatherVar), bind(unit)], (wthr, unt) => {
|
||||
if (unt === 'imperial') {
|
||||
return `${Math.ceil(wthr.current.temp_f)}° F`;
|
||||
} else {
|
||||
return `${Math.ceil(wthr.current.temp_c)}° C`;
|
||||
}
|
||||
});
|
||||
|
||||
const weatherModule = Module({
|
||||
textIcon: iconBinding(),
|
||||
tooltipText: bind(globalWeatherVar).as((v) => `Weather Status: ${v.current.condition.text}`),
|
||||
boxClass: 'weather-custom',
|
||||
label: labelBinding(),
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
iconBinding.drop();
|
||||
labelBinding.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return weatherModule;
|
||||
};
|
||||
189
src/components/bar/modules/window_title/helpers/title.ts
Normal file
189
src/components/bar/modules/window_title/helpers/title.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import options from 'src/options';
|
||||
import { capitalizeFirstLetter } from 'src/lib/utils';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
|
||||
/**
|
||||
* Retrieves the matching window title details for a given window.
|
||||
*
|
||||
* This function searches for a matching window title in the predefined `windowTitleMap` based on the class of the provided window.
|
||||
* If a match is found, it returns an object containing the icon and label for the window. If no match is found, it returns a default icon and label.
|
||||
*
|
||||
* @param windowtitle The window object containing the class information.
|
||||
*
|
||||
* @returns An object containing the icon and label for the window.
|
||||
*/
|
||||
export const getWindowMatch = (windowtitle: AstalHyprland.Client): Record<string, string> => {
|
||||
const windowTitleMap = [
|
||||
// user provided values
|
||||
...options.bar.windowtitle.title_map.get(),
|
||||
// Original Entries
|
||||
['kitty', '', 'Kitty Terminal'],
|
||||
['firefox', '', 'Firefox'],
|
||||
['microsoft-edge', '', 'Edge'],
|
||||
['discord', '', 'Discord'],
|
||||
['vesktop', '', 'Vesktop'],
|
||||
['org.kde.dolphin', '', 'Dolphin'],
|
||||
['plex', '', 'Plex'],
|
||||
['steam', '', 'Steam'],
|
||||
['spotify', '', 'Spotify'],
|
||||
['ristretto', '', 'Ristretto'],
|
||||
['obsidian', '', 'Obsidian'],
|
||||
|
||||
// Browsers
|
||||
['google-chrome', '', 'Google Chrome'],
|
||||
['brave-browser', '', 'Brave Browser'],
|
||||
['chromium', '', 'Chromium'],
|
||||
['opera', '', 'Opera'],
|
||||
['vivaldi', '', 'Vivaldi'],
|
||||
['waterfox', '', 'Waterfox'],
|
||||
['thorium', '', 'Waterfox'],
|
||||
['tor-browser', '', 'Tor Browser'],
|
||||
['floorp', '', 'Floorp'],
|
||||
|
||||
// Terminals
|
||||
['gnome-terminal', '', 'GNOME Terminal'],
|
||||
['konsole', '', 'Konsole'],
|
||||
['alacritty', '', 'Alacritty'],
|
||||
['wezterm', '', 'Wezterm'],
|
||||
['foot', '', 'Foot Terminal'],
|
||||
['tilix', '', 'Tilix'],
|
||||
['xterm', '', 'XTerm'],
|
||||
['urxvt', '', 'URxvt'],
|
||||
['st', '', 'st Terminal'],
|
||||
|
||||
// Development Tools
|
||||
['code', '', 'Visual Studio Code'],
|
||||
['vscode', '', 'VS Code'],
|
||||
['sublime-text', '', 'Sublime Text'],
|
||||
['atom', '', 'Atom'],
|
||||
['android-studio', '', 'Android Studio'],
|
||||
['intellij-idea', '', 'IntelliJ IDEA'],
|
||||
['pycharm', '', 'PyCharm'],
|
||||
['webstorm', '', 'WebStorm'],
|
||||
['phpstorm', '', 'PhpStorm'],
|
||||
['eclipse', '', 'Eclipse'],
|
||||
['netbeans', '', 'NetBeans'],
|
||||
['docker', '', 'Docker'],
|
||||
['vim', '', 'Vim'],
|
||||
['neovim', '', 'Neovim'],
|
||||
['neovide', '', 'Neovide'],
|
||||
['emacs', '', 'Emacs'],
|
||||
|
||||
// Communication Tools
|
||||
['slack', '', 'Slack'],
|
||||
['telegram-desktop', '', 'Telegram'],
|
||||
['org.telegram.desktop', '', 'Telegram'],
|
||||
['whatsapp', '', 'WhatsApp'],
|
||||
['teams', '', 'Microsoft Teams'],
|
||||
['skype', '', 'Skype'],
|
||||
['thunderbird', '', 'Thunderbird'],
|
||||
|
||||
// File Managers
|
||||
['nautilus', '', 'Files (Nautilus)'],
|
||||
['thunar', '', 'Thunar'],
|
||||
['pcmanfm', '', 'PCManFM'],
|
||||
['nemo', '', 'Nemo'],
|
||||
['ranger', '', 'Ranger'],
|
||||
['doublecmd', '', 'Double Commander'],
|
||||
['krusader', '', 'Krusader'],
|
||||
|
||||
// Media Players
|
||||
['vlc', '', 'VLC Media Player'],
|
||||
['mpv', '', 'MPV'],
|
||||
['rhythmbox', '', 'Rhythmbox'],
|
||||
|
||||
// Graphics Tools
|
||||
['gimp', '', 'GIMP'],
|
||||
['inkscape', '', 'Inkscape'],
|
||||
['krita', '', 'Krita'],
|
||||
['blender', '', 'Blender'],
|
||||
|
||||
// Video Editing
|
||||
['kdenlive', '', 'Kdenlive'],
|
||||
|
||||
// Games and Gaming Platforms
|
||||
['lutris', '', 'Lutris'],
|
||||
['heroic', '', 'Heroic Games Launcher'],
|
||||
['minecraft', '', 'Minecraft'],
|
||||
['csgo', '', 'CS:GO'],
|
||||
['dota2', '', 'Dota 2'],
|
||||
|
||||
// Office and Productivity
|
||||
['evernote', '', 'Evernote'],
|
||||
['sioyek', '', 'Sioyek'],
|
||||
|
||||
// Cloud Services and Sync
|
||||
['dropbox', '', 'Dropbox'],
|
||||
|
||||
// Desktop
|
||||
['^$', '', 'Desktop'],
|
||||
|
||||
// Fallback icon
|
||||
['(.+)', '', `${capitalizeFirstLetter(windowtitle?.class ?? 'Unknown')}`],
|
||||
];
|
||||
|
||||
if (!windowtitle?.class) {
|
||||
return {
|
||||
icon: '',
|
||||
label: 'Desktop',
|
||||
};
|
||||
}
|
||||
|
||||
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0]).test(windowtitle?.class.toLowerCase()));
|
||||
|
||||
if (!foundMatch || foundMatch.length !== 3) {
|
||||
return {
|
||||
icon: windowTitleMap[windowTitleMap.length - 1][1],
|
||||
label: windowTitleMap[windowTitleMap.length - 1][2],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
icon: foundMatch[1],
|
||||
label: foundMatch[2],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the title for a given window client.
|
||||
*
|
||||
* This function returns the title of the window based on the provided client object and options.
|
||||
* It can use a custom title, the class name, or the actual window title. If the title is empty, it falls back to the class name.
|
||||
*
|
||||
* @param client The window client object containing the title and class information.
|
||||
* @param useCustomTitle A boolean indicating whether to use a custom title.
|
||||
* @param useClassName A boolean indicating whether to use the class name as the title.
|
||||
*
|
||||
* @returns The title of the window as a string.
|
||||
*/
|
||||
export const getTitle = (client: AstalHyprland.Client, useCustomTitle: boolean, useClassName: boolean): string => {
|
||||
if (client === null) return getWindowMatch(client).label;
|
||||
|
||||
if (useCustomTitle) return getWindowMatch(client).label;
|
||||
if (useClassName) return client.class;
|
||||
|
||||
const title = client.title;
|
||||
// If the title is empty or only filled with spaces, fallback to the class name
|
||||
if (title.length === 0 || title.match(/^ *$/)) {
|
||||
return client.class;
|
||||
}
|
||||
return title;
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncates the given title to a specified maximum size.
|
||||
*
|
||||
* This function shortens the provided title string to the specified maximum size.
|
||||
* If the title exceeds the maximum size, it appends an ellipsis ('...') to the truncated title.
|
||||
*
|
||||
* @param title The title string to truncate.
|
||||
* @param max_size The maximum size of the truncated title.
|
||||
*
|
||||
* @returns The truncated title as a string. If the title is within the maximum size, returns the original title.
|
||||
*/
|
||||
export const truncateTitle = (title: string, max_size: number): string => {
|
||||
if (max_size > 0 && title.length > max_size) {
|
||||
return title.substring(0, max_size).trim() + '...';
|
||||
}
|
||||
return title;
|
||||
};
|
||||
113
src/components/bar/modules/window_title/index.tsx
Normal file
113
src/components/bar/modules/window_title/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import options from 'src/options';
|
||||
import { hyprlandService } from 'src/lib/constants/services';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { useHook } from 'src/lib/shared/hookHandler';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { getTitle, getWindowMatch, truncateTitle } from './helpers/title';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
|
||||
const { leftClick, rightClick, middleClick, scrollDown, scrollUp } = options.bar.windowtitle;
|
||||
|
||||
const ClientTitle = (): BarBoxChild => {
|
||||
const { custom_title, class_name, label, icon, truncation, truncation_size } = options.bar.windowtitle;
|
||||
|
||||
const componentClassName = Variable.derive(
|
||||
[bind(options.theme.bar.buttons.style), bind(label)],
|
||||
(style: string, showLabel: boolean) => {
|
||||
const styleMap: Record<string, string> = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `windowtitle-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
);
|
||||
|
||||
const componentChildren = Variable.derive(
|
||||
[
|
||||
bind(hyprlandService, 'focusedClient'),
|
||||
bind(custom_title),
|
||||
bind(class_name),
|
||||
bind(label),
|
||||
bind(icon),
|
||||
bind(truncation),
|
||||
bind(truncation_size),
|
||||
],
|
||||
(
|
||||
client: AstalHyprland.Client,
|
||||
useCustomTitle: boolean,
|
||||
useClassName: boolean,
|
||||
showLabel: boolean,
|
||||
showIcon: boolean,
|
||||
truncate: boolean,
|
||||
truncationSize: number,
|
||||
) => {
|
||||
const children: JSX.Element[] = [];
|
||||
|
||||
if (showIcon) {
|
||||
children.push(
|
||||
<label
|
||||
className={'bar-button-icon windowtitle txt-icon bar'}
|
||||
label={getWindowMatch(client).icon}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (showLabel) {
|
||||
children.push(
|
||||
<label
|
||||
className={`bar-button-label windowtitle ${showIcon ? '' : 'no-icon'}`}
|
||||
label={truncateTitle(
|
||||
getTitle(client, useCustomTitle, useClassName),
|
||||
truncate ? truncationSize : -1,
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
},
|
||||
);
|
||||
|
||||
const component = <box className={componentClassName()}>{componentChildren()}</box>;
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
boxClass: 'windowtitle',
|
||||
props: {
|
||||
setup: (self: Astal.Button): void => {
|
||||
useHook(self, options.bar.scrollSpeed, () => {
|
||||
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.get());
|
||||
|
||||
const disconnectPrimary = onPrimaryClick(self, (clicked, event) => {
|
||||
runAsyncCommand(leftClick.get(), { clicked, event });
|
||||
});
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { ClientTitle };
|
||||
361
src/components/bar/modules/workspaces/helpers/index.ts
Normal file
361
src/components/bar/modules/workspaces/helpers/index.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { exec, Variable } from 'astal';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { hyprlandService } from 'src/lib/constants/services';
|
||||
import { MonitorMap, WorkspaceMap, WorkspaceRule } from 'src/lib/types/workspace';
|
||||
import { range } from 'src/lib/utils';
|
||||
import options from 'src/options';
|
||||
|
||||
const { workspaces, reverse_scroll, ignored } = options.bar.workspaces;
|
||||
|
||||
/**
|
||||
* Retrieves the workspaces for a specific monitor.
|
||||
*
|
||||
* This function checks if a given workspace is valid for a specified monitor based on the workspace rules.
|
||||
*
|
||||
* @param curWs - The current workspace number.
|
||||
* @param wsRules - The workspace rules map.
|
||||
* @param monitor - The monitor ID.
|
||||
* @param workspaceList - The list of workspaces.
|
||||
* @param monitorList - The list of monitors.
|
||||
*
|
||||
* @returns Whether the workspace is valid for the monitor.
|
||||
*/
|
||||
export const getWorkspacesForMonitor = (
|
||||
curWs: number,
|
||||
wsRules: WorkspaceMap,
|
||||
monitor: number,
|
||||
workspaceList: AstalHyprland.Workspace[],
|
||||
monitorList: AstalHyprland.Monitor[],
|
||||
): boolean => {
|
||||
if (!wsRules || !Object.keys(wsRules).length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const monitorMap: MonitorMap = {};
|
||||
|
||||
const workspaceMonitorList = workspaceList.map((m) => {
|
||||
return { id: m.monitor.id, name: m.monitor.name };
|
||||
});
|
||||
|
||||
const monitors = [...new Map([...workspaceMonitorList, ...monitorList].map((item) => [item.id, item])).values()];
|
||||
|
||||
monitors.forEach((mon) => (monitorMap[mon.id] = mon.name));
|
||||
|
||||
const currentMonitorName = monitorMap[monitor];
|
||||
const monitorWSRules = wsRules[currentMonitorName];
|
||||
|
||||
if (monitorWSRules === undefined) {
|
||||
return true;
|
||||
}
|
||||
return monitorWSRules.includes(curWs);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the workspace rules.
|
||||
*
|
||||
* This function fetches and parses the workspace rules from the Hyprland service.
|
||||
*
|
||||
* @returns The workspace rules map.
|
||||
*/
|
||||
export const getWorkspaceRules = (): WorkspaceMap => {
|
||||
try {
|
||||
const rules = exec('hyprctl workspacerules -j');
|
||||
|
||||
const workspaceRules: WorkspaceMap = {};
|
||||
|
||||
JSON.parse(rules).forEach((rule: WorkspaceRule) => {
|
||||
const workspaceNum = parseInt(rule.workspaceString, 10);
|
||||
if (isNaN(workspaceNum)) {
|
||||
return;
|
||||
}
|
||||
if (Object.hasOwnProperty.call(workspaceRules, rule.monitor)) {
|
||||
workspaceRules[rule.monitor].push(workspaceNum);
|
||||
} else {
|
||||
workspaceRules[rule.monitor] = [workspaceNum];
|
||||
}
|
||||
});
|
||||
|
||||
return workspaceRules;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the current monitor's workspaces.
|
||||
*
|
||||
* This function returns a list of workspace numbers for the specified monitor.
|
||||
*
|
||||
* @param monitor - The monitor ID.
|
||||
*
|
||||
* @returns The list of workspace numbers.
|
||||
*/
|
||||
export const getCurrentMonitorWorkspaces = (monitor: number): number[] => {
|
||||
if (hyprlandService.get_monitors().length === 1) {
|
||||
return Array.from({ length: workspaces.get() }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
const monitorWorkspaces = getWorkspaceRules();
|
||||
const monitorMap: MonitorMap = {};
|
||||
hyprlandService.get_monitors().forEach((m) => (monitorMap[m.id] = m.name));
|
||||
|
||||
const currentMonitorName = monitorMap[monitor];
|
||||
|
||||
return monitorWorkspaces[currentMonitorName];
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a workspace is ignored.
|
||||
*
|
||||
* This function determines if a given workspace number is in the ignored workspaces list.
|
||||
*
|
||||
* @param ignoredWorkspaces - The ignored workspaces variable.
|
||||
* @param workspaceNumber - The workspace number.
|
||||
*
|
||||
* @returns Whether the workspace is ignored.
|
||||
*/
|
||||
export const isWorkspaceIgnored = (ignoredWorkspaces: Variable<string>, workspaceNumber: number): boolean => {
|
||||
if (ignoredWorkspaces.get() === '') return false;
|
||||
|
||||
const ignoredWsRegex = new RegExp(ignoredWorkspaces.get());
|
||||
|
||||
return ignoredWsRegex.test(workspaceNumber.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the next or previous workspace.
|
||||
*
|
||||
* This function changes the current workspace to the next or previous one, considering active and ignored workspaces.
|
||||
*
|
||||
* @param direction - The direction to navigate ('next' or 'prev').
|
||||
* @param currentMonitorWorkspaces - The current monitor's workspaces variable.
|
||||
* @param activeWorkspaces - Whether to consider only active workspaces.
|
||||
* @param ignoredWorkspaces - The ignored workspaces variable.
|
||||
*/
|
||||
const navigateWorkspace = (
|
||||
direction: 'next' | 'prev',
|
||||
currentMonitorWorkspaces: Variable<number[]>,
|
||||
activeWorkspaces: boolean,
|
||||
ignoredWorkspaces: Variable<string>,
|
||||
): void => {
|
||||
const hyprlandWorkspaces = hyprlandService.get_workspaces() || [];
|
||||
const occupiedWorkspaces = hyprlandWorkspaces
|
||||
.filter((ws) => hyprlandService.focusedMonitor.id === ws.monitor?.id)
|
||||
.map((ws) => ws.id);
|
||||
|
||||
const workspacesList = activeWorkspaces
|
||||
? occupiedWorkspaces
|
||||
: currentMonitorWorkspaces.get() || Array.from({ length: workspaces.get() }, (_, i) => i + 1);
|
||||
|
||||
if (workspacesList.length === 0) return;
|
||||
|
||||
const currentIndex = workspacesList.indexOf(hyprlandService.focusedWorkspace.id);
|
||||
const step = direction === 'next' ? 1 : -1;
|
||||
let newIndex = (currentIndex + step + workspacesList.length) % workspacesList.length;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < workspacesList.length) {
|
||||
const targetWS = workspacesList[newIndex];
|
||||
if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) {
|
||||
hyprlandService.message_async(`dispatch workspace ${targetWS}`);
|
||||
return;
|
||||
}
|
||||
newIndex = (newIndex + step + workspacesList.length) % workspacesList.length;
|
||||
attempts++;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the next workspace.
|
||||
*
|
||||
* This function changes the current workspace to the next one.
|
||||
*
|
||||
* @param currentMonitorWorkspaces - The current monitor's workspaces variable.
|
||||
* @param activeWorkspaces - Whether to consider only active workspaces.
|
||||
* @param ignoredWorkspaces - The ignored workspaces variable.
|
||||
*/
|
||||
export const goToNextWS = (
|
||||
currentMonitorWorkspaces: Variable<number[]>,
|
||||
activeWorkspaces: boolean,
|
||||
ignoredWorkspaces: Variable<string>,
|
||||
): void => {
|
||||
navigateWorkspace('next', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the previous workspace.
|
||||
*
|
||||
* This function changes the current workspace to the previous one.
|
||||
*
|
||||
* @param currentMonitorWorkspaces - The current monitor's workspaces variable.
|
||||
* @param activeWorkspaces - Whether to consider only active workspaces.
|
||||
* @param ignoredWorkspaces - The ignored workspaces variable.
|
||||
*/
|
||||
export const goToPrevWS = (
|
||||
currentMonitorWorkspaces: Variable<number[]>,
|
||||
activeWorkspaces: boolean,
|
||||
ignoredWorkspaces: Variable<string>,
|
||||
): void => {
|
||||
navigateWorkspace('prev', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
|
||||
};
|
||||
|
||||
/**
|
||||
* Throttles a function to limit its execution rate.
|
||||
*
|
||||
* This function ensures that the provided function is not called more often than the specified limit.
|
||||
*
|
||||
* @param func - The function to throttle.
|
||||
* @param limit - The time limit in milliseconds.
|
||||
*
|
||||
* @returns The throttled function.
|
||||
*/
|
||||
export function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number): T {
|
||||
let inThrottle: boolean;
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates throttled scroll handlers for navigating workspaces.
|
||||
*
|
||||
* This function returns handlers for scrolling up and down through workspaces, throttled by the specified scroll speed.
|
||||
*
|
||||
* @param scrollSpeed - The scroll speed.
|
||||
* @param currentMonitorWorkspaces - The current monitor's workspaces variable.
|
||||
* @param activeWorkspaces - Whether to consider only active workspaces.
|
||||
*
|
||||
* @returns The throttled scroll handlers.
|
||||
*/
|
||||
export const createThrottledScrollHandlers = (
|
||||
scrollSpeed: number,
|
||||
currentMonitorWorkspaces: Variable<number[]>,
|
||||
activeWorkspaces: boolean = true,
|
||||
): ThrottledScrollHandlers => {
|
||||
const throttledScrollUp = throttle(() => {
|
||||
if (reverse_scroll.get()) {
|
||||
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
|
||||
} else {
|
||||
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
|
||||
}
|
||||
}, 200 / scrollSpeed);
|
||||
|
||||
const throttledScrollDown = throttle(() => {
|
||||
if (reverse_scroll.get()) {
|
||||
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
|
||||
} else {
|
||||
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
|
||||
}
|
||||
}, 200 / scrollSpeed);
|
||||
|
||||
return { throttledScrollUp, throttledScrollDown };
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the workspaces to render.
|
||||
*
|
||||
* This function returns a list of workspace numbers to render based on the total workspaces, workspace list, rules, and monitor.
|
||||
*
|
||||
* @param totalWorkspaces - The total number of workspaces.
|
||||
* @param workspaceList - The list of workspaces.
|
||||
* @param workspaceRules - The workspace rules map.
|
||||
* @param monitor - The monitor ID.
|
||||
* @param isMonitorSpecific - Whether the workspaces are monitor-specific.
|
||||
* @param monitorList - The list of monitors.
|
||||
*
|
||||
* @returns The list of workspace numbers to render.
|
||||
*/
|
||||
export const getWorkspacesToRender = (
|
||||
totalWorkspaces: number,
|
||||
workspaceList: AstalHyprland.Workspace[],
|
||||
workspaceRules: WorkspaceMap,
|
||||
monitor: number,
|
||||
isMonitorSpecific: boolean,
|
||||
monitorList: AstalHyprland.Monitor[],
|
||||
): number[] => {
|
||||
let allWorkspaces = range(totalWorkspaces || 8);
|
||||
const activeWorkspaces = workspaceList.map((ws) => ws.id);
|
||||
|
||||
const workspaceMonitorList = workspaceList.map((ws) => {
|
||||
return {
|
||||
id: ws.monitor?.id || -1,
|
||||
name: ws.monitor?.name || '',
|
||||
};
|
||||
});
|
||||
|
||||
const curMonitor =
|
||||
monitorList.find((mon) => mon.id === monitor) || workspaceMonitorList.find((mon) => mon.id === monitor);
|
||||
|
||||
const workspacesWithRules = Object.keys(workspaceRules).reduce((acc: number[], k: string) => {
|
||||
return [...acc, ...workspaceRules[k]];
|
||||
}, []);
|
||||
|
||||
const activesForMonitor = activeWorkspaces.filter((w) => {
|
||||
if (
|
||||
curMonitor &&
|
||||
Object.hasOwnProperty.call(workspaceRules, curMonitor.name) &&
|
||||
workspacesWithRules.includes(w)
|
||||
) {
|
||||
return workspaceRules[curMonitor.name].includes(w);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (isMonitorSpecific) {
|
||||
const workspacesInRange = range(totalWorkspaces).filter((ws) => {
|
||||
return getWorkspacesForMonitor(ws, workspaceRules, monitor, workspaceList, monitorList);
|
||||
});
|
||||
|
||||
allWorkspaces = [...new Set([...activesForMonitor, ...workspacesInRange])];
|
||||
} else {
|
||||
allWorkspaces = [...new Set([...allWorkspaces, ...activeWorkspaces])];
|
||||
}
|
||||
|
||||
return allWorkspaces.sort((a, b) => a - b);
|
||||
};
|
||||
|
||||
/**
|
||||
* The workspace rules variable.
|
||||
* This variable holds the current workspace rules.
|
||||
*/
|
||||
export const workspaceRules = Variable(getWorkspaceRules());
|
||||
|
||||
/**
|
||||
* The force updater variable.
|
||||
* This variable is used to force updates when workspace events occur.
|
||||
*/
|
||||
export const forceUpdater = Variable(true);
|
||||
|
||||
/**
|
||||
* Sets up connections for workspace events.
|
||||
* This function sets up event listeners for various workspace-related events to update the workspace rules and force updates.
|
||||
*/
|
||||
export const setupConnections = (): void => {
|
||||
hyprlandService.connect('config-reloaded', () => {
|
||||
workspaceRules.set(getWorkspaceRules());
|
||||
});
|
||||
|
||||
hyprlandService.connect('client-moved', () => {
|
||||
forceUpdater.set(!forceUpdater.get());
|
||||
});
|
||||
|
||||
hyprlandService.connect('client-added', () => {
|
||||
forceUpdater.set(!forceUpdater.get());
|
||||
});
|
||||
|
||||
hyprlandService.connect('client-removed', () => {
|
||||
forceUpdater.set(!forceUpdater.get());
|
||||
});
|
||||
};
|
||||
|
||||
type ThrottledScrollHandlers = {
|
||||
throttledScrollUp: () => void;
|
||||
throttledScrollDown: () => void;
|
||||
};
|
||||
274
src/components/bar/modules/workspaces/helpers/utils.ts
Normal file
274
src/components/bar/modules/workspaces/helpers/utils.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { hyprlandService } from 'src/lib/constants/services';
|
||||
import { defaultApplicationIcons } from 'src/lib/constants/workspaces';
|
||||
import { AppIconOptions, WorkspaceIconMap } from 'src/lib/types/workspace';
|
||||
import { isValidGjsColor } from 'src/lib/utils';
|
||||
import options from 'src/options';
|
||||
|
||||
const { monochrome, background } = options.theme.bar.buttons;
|
||||
const { background: wsBackground, active } = options.theme.bar.buttons.workspaces;
|
||||
|
||||
const { showWsIcons, showAllActive, numbered_active_indicator: wsActiveIndicator } = options.bar.workspaces;
|
||||
|
||||
/**
|
||||
* Determines if a workspace is active on a given monitor.
|
||||
*
|
||||
* This function checks if the workspace with the specified index is currently active on the given monitor.
|
||||
* It uses the `showAllActive` setting and the `hyprlandService` to determine the active workspace on the monitor.
|
||||
*
|
||||
* @param monitor The index of the monitor to check.
|
||||
* @param i The index of the workspace to check.
|
||||
*
|
||||
* @returns True if the workspace is active on the monitor, false otherwise.
|
||||
*/
|
||||
const isWorkspaceActiveOnMonitor = (monitor: number, i: number): boolean => {
|
||||
return showAllActive.get() && hyprlandService.get_monitor(monitor).activeWorkspace.id === i;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the icon for a given workspace.
|
||||
*
|
||||
* This function returns the icon associated with a workspace from the provided workspace icon map.
|
||||
* If no icon is found, it returns the workspace index as a string.
|
||||
*
|
||||
* @param wsIconMap The map of workspace icons where keys are workspace indices and values are icons or icon objects.
|
||||
* @param i The index of the workspace for which to retrieve the icon.
|
||||
*
|
||||
* @returns The icon for the workspace as a string. If no icon is found, returns the workspace index as a string.
|
||||
*/
|
||||
const getWsIcon = (wsIconMap: WorkspaceIconMap, i: number): string => {
|
||||
const iconEntry = wsIconMap[i];
|
||||
|
||||
if (!iconEntry) {
|
||||
return `${i}`;
|
||||
}
|
||||
|
||||
const hasIcon = typeof iconEntry === 'object' && 'icon' in iconEntry && iconEntry.icon !== '';
|
||||
|
||||
if (typeof iconEntry === 'string' && iconEntry !== '') {
|
||||
return iconEntry;
|
||||
}
|
||||
|
||||
if (hasIcon) {
|
||||
return iconEntry.icon;
|
||||
}
|
||||
|
||||
return `${i}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the color for a given workspace.
|
||||
*
|
||||
* This function determines the color styling for a workspace based on the provided workspace icon map,
|
||||
* smart highlighting settings, and the monitor index. It returns a CSS string for the color and background.
|
||||
*
|
||||
* @param wsIconMap The map of workspace icons where keys are workspace indices and values are icon objects.
|
||||
* @param i The index of the workspace for which to retrieve the color.
|
||||
* @param smartHighlight A boolean indicating whether smart highlighting is enabled.
|
||||
* @param monitor The index of the monitor to check for active workspaces.
|
||||
*
|
||||
* @returns A CSS string representing the color and background for the workspace. If no color is found, returns an empty string.
|
||||
*/
|
||||
export const getWsColor = (
|
||||
wsIconMap: WorkspaceIconMap,
|
||||
i: number,
|
||||
smartHighlight: boolean,
|
||||
monitor: number,
|
||||
): string => {
|
||||
const iconEntry = wsIconMap[i];
|
||||
const hasColor = typeof iconEntry === 'object' && 'color' in iconEntry && isValidGjsColor(iconEntry.color);
|
||||
if (!iconEntry) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
showWsIcons.get() &&
|
||||
smartHighlight &&
|
||||
wsActiveIndicator.get() === 'highlight' &&
|
||||
(hyprlandService.focusedWorkspace.id === i || isWorkspaceActiveOnMonitor(monitor, i))
|
||||
) {
|
||||
const iconColor = monochrome.get() ? background.get() : wsBackground.get();
|
||||
const iconBackground = hasColor && isValidGjsColor(iconEntry.color) ? iconEntry.color : active.get();
|
||||
const colorCss = `color: ${iconColor};`;
|
||||
const backgroundCss = `background: ${iconBackground};`;
|
||||
|
||||
return colorCss + backgroundCss;
|
||||
}
|
||||
|
||||
if (hasColor && isValidGjsColor(iconEntry.color)) {
|
||||
return `color: ${iconEntry.color}; border-bottom-color: ${iconEntry.color};`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the application icon for a given workspace.
|
||||
*
|
||||
* This function returns the appropriate application icon for the specified workspace index.
|
||||
* It considers user-defined icons, default icons, and the option to remove duplicate icons.
|
||||
*
|
||||
* @param workspaceIndex The index of the workspace for which to retrieve the application icon.
|
||||
* @param removeDuplicateIcons A boolean indicating whether to remove duplicate icons.
|
||||
* @param options An object containing user-defined icon map, default icon, and empty icon.
|
||||
*
|
||||
* @returns The application icon for the workspace as a string. If no icons are found, returns the default or empty icon.
|
||||
*/
|
||||
export const getAppIcon = (
|
||||
workspaceIndex: number,
|
||||
removeDuplicateIcons: boolean,
|
||||
{ iconMap: userDefinedIconMap, defaultIcon, emptyIcon }: AppIconOptions,
|
||||
): string => {
|
||||
const iconMap = { ...userDefinedIconMap, ...defaultApplicationIcons };
|
||||
|
||||
const clients = hyprlandService
|
||||
.get_clients()
|
||||
.filter((client) => client.workspace.id === workspaceIndex)
|
||||
.map((client) => [client.class, client.title]);
|
||||
|
||||
if (!clients.length) {
|
||||
return emptyIcon;
|
||||
}
|
||||
|
||||
let icons = clients
|
||||
.map(([clientClass, clientTitle]) => {
|
||||
const maybeIcon = Object.entries(iconMap).find(([matcher]) => {
|
||||
try {
|
||||
if (matcher.startsWith('class:')) {
|
||||
const re = matcher.substring(6);
|
||||
return new RegExp(re).test(clientClass);
|
||||
}
|
||||
|
||||
if (matcher.startsWith('title:')) {
|
||||
const re = matcher.substring(6);
|
||||
|
||||
return new RegExp(re).test(clientTitle);
|
||||
}
|
||||
|
||||
return new RegExp(matcher, 'i').test(clientClass);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!maybeIcon) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return maybeIcon.at(1);
|
||||
})
|
||||
.filter((x) => x);
|
||||
|
||||
if (removeDuplicateIcons) {
|
||||
icons = [...new Set(icons)];
|
||||
}
|
||||
|
||||
if (icons.length) {
|
||||
return icons.join(' ');
|
||||
}
|
||||
|
||||
return defaultIcon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the class names for a workspace.
|
||||
*
|
||||
* This function generates the appropriate class names for a workspace based on various settings such as
|
||||
* whether to show icons, numbered workspaces, workspace icons, and smart highlighting.
|
||||
*
|
||||
* @param showIcons A boolean indicating whether to show icons.
|
||||
* @param showNumbered A boolean indicating whether to show numbered workspaces.
|
||||
* @param numberedActiveIndicator The indicator for active numbered workspaces.
|
||||
* @param showWsIcons A boolean indicating whether to show workspace icons.
|
||||
* @param smartHighlight A boolean indicating whether smart highlighting is enabled.
|
||||
* @param monitor The index of the monitor to check for active workspaces.
|
||||
* @param i The index of the workspace for which to render class names.
|
||||
*
|
||||
* @returns The class names for the workspace as a string.
|
||||
*/
|
||||
export const renderClassnames = (
|
||||
showIcons: boolean,
|
||||
showNumbered: boolean,
|
||||
numberedActiveIndicator: string,
|
||||
showWsIcons: boolean,
|
||||
smartHighlight: boolean,
|
||||
monitor: number,
|
||||
i: number,
|
||||
): string => {
|
||||
if (showIcons) {
|
||||
return 'workspace-icon txt-icon bar';
|
||||
}
|
||||
|
||||
if (showNumbered || showWsIcons) {
|
||||
const numActiveInd =
|
||||
hyprlandService.focusedWorkspace.id === i || isWorkspaceActiveOnMonitor(monitor, i)
|
||||
? numberedActiveIndicator
|
||||
: '';
|
||||
|
||||
const wsIconClass = showWsIcons ? 'txt-icon' : '';
|
||||
const smartHighlightClass = smartHighlight ? 'smart-highlight' : '';
|
||||
|
||||
const className = `workspace-number can_${numberedActiveIndicator} ${numActiveInd} ${wsIconClass} ${smartHighlightClass}`;
|
||||
|
||||
return className.trim();
|
||||
}
|
||||
|
||||
return 'default';
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the label for a workspace.
|
||||
*
|
||||
* This function generates the appropriate label for a workspace based on various settings such as
|
||||
* whether to show icons, application icons, workspace icons, and workspace indicators.
|
||||
*
|
||||
* @param showIcons A boolean indicating whether to show icons.
|
||||
* @param availableIndicator The indicator for available workspaces.
|
||||
* @param activeIndicator The indicator for active workspaces.
|
||||
* @param occupiedIndicator The indicator for occupied workspaces.
|
||||
* @param showAppIcons A boolean indicating whether to show application icons.
|
||||
* @param appIcons The application icons as a string.
|
||||
* @param workspaceMask A boolean indicating whether to mask the workspace.
|
||||
* @param showWorkspaceIcons A boolean indicating whether to show workspace icons.
|
||||
* @param wsIconMap The map of workspace icons where keys are workspace indices and values are icons or icon objects.
|
||||
* @param i The index of the workspace for which to render the label.
|
||||
* @param index The index of the workspace in the list.
|
||||
* @param monitor The index of the monitor to check for active workspaces.
|
||||
*
|
||||
* @returns The label for the workspace as a string.
|
||||
*/
|
||||
export const renderLabel = (
|
||||
showIcons: boolean,
|
||||
availableIndicator: string,
|
||||
activeIndicator: string,
|
||||
occupiedIndicator: string,
|
||||
showAppIcons: boolean,
|
||||
appIcons: string,
|
||||
workspaceMask: boolean,
|
||||
showWorkspaceIcons: boolean,
|
||||
wsIconMap: WorkspaceIconMap,
|
||||
i: number,
|
||||
index: number,
|
||||
monitor: number,
|
||||
): string => {
|
||||
if (showAppIcons) {
|
||||
return appIcons;
|
||||
}
|
||||
|
||||
if (showIcons) {
|
||||
if (hyprlandService.focusedWorkspace.id === i || isWorkspaceActiveOnMonitor(monitor, i)) {
|
||||
return activeIndicator;
|
||||
}
|
||||
if ((hyprlandService.get_workspace(i)?.clients.length || 0) > 0) {
|
||||
return occupiedIndicator;
|
||||
}
|
||||
if (monitor !== -1) {
|
||||
return availableIndicator;
|
||||
}
|
||||
}
|
||||
|
||||
if (showWorkspaceIcons) {
|
||||
return getWsIcon(wsIconMap, i);
|
||||
}
|
||||
|
||||
return workspaceMask ? `${index + 1}` : `${i}`;
|
||||
};
|
||||
53
src/components/bar/modules/workspaces/index.tsx
Normal file
53
src/components/bar/modules/workspaces/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import options from 'src/options';
|
||||
import { createThrottledScrollHandlers, getCurrentMonitorWorkspaces } from './helpers';
|
||||
import { BarBoxChild, SelfButton } from 'src/lib/types/bar';
|
||||
import { WorkspaceModule } from './workspaces';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { GtkWidget } from 'src/lib/types/widget';
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
|
||||
const { workspaces, scroll_speed } = options.bar.workspaces;
|
||||
|
||||
const Workspaces = (monitor = -1): BarBoxChild => {
|
||||
const currentMonitorWorkspaces = Variable(getCurrentMonitorWorkspaces(monitor));
|
||||
|
||||
workspaces.subscribe(() => {
|
||||
currentMonitorWorkspaces.set(getCurrentMonitorWorkspaces(monitor));
|
||||
});
|
||||
|
||||
const component = (
|
||||
<box className={'workspaces-box-container'}>
|
||||
<WorkspaceModule monitor={monitor} />
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
isVisible: true,
|
||||
boxClass: 'workspaces',
|
||||
isBox: true,
|
||||
props: {
|
||||
setup: (self: SelfButton): void => {
|
||||
Variable.derive([bind(scroll_speed)], (scroll_speed) => {
|
||||
const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers(
|
||||
scroll_speed,
|
||||
currentMonitorWorkspaces,
|
||||
);
|
||||
|
||||
const scrollHandlers = self.connect('scroll-event', (_: GtkWidget, event: Gdk.Event) => {
|
||||
const eventDirection = event.get_scroll_direction()[1];
|
||||
if (eventDirection === Gdk.ScrollDirection.UP) {
|
||||
throttledScrollUp();
|
||||
} else if (eventDirection === Gdk.ScrollDirection.DOWN) {
|
||||
throttledScrollDown();
|
||||
}
|
||||
});
|
||||
|
||||
self.disconnect(scrollHandlers);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Workspaces };
|
||||
178
src/components/bar/modules/workspaces/workspaces.tsx
Normal file
178
src/components/bar/modules/workspaces/workspaces.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { hyprlandService } from 'src/lib/constants/services';
|
||||
import options from 'src/options';
|
||||
import { forceUpdater, getWorkspacesToRender, isWorkspaceIgnored, setupConnections, workspaceRules } from './helpers';
|
||||
import { getAppIcon, getWsColor, renderClassnames, renderLabel } from './helpers/utils';
|
||||
import { ApplicationIcons, WorkspaceIconMap } from 'src/lib/types/workspace';
|
||||
import { bind, Variable } from 'astal';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
|
||||
const {
|
||||
workspaces,
|
||||
monitorSpecific,
|
||||
workspaceMask,
|
||||
spacing,
|
||||
ignored,
|
||||
showAllActive,
|
||||
show_icons,
|
||||
show_numbered,
|
||||
numbered_active_indicator,
|
||||
workspaceIconMap,
|
||||
showWsIcons,
|
||||
showApplicationIcons,
|
||||
applicationIconOncePerWorkspace,
|
||||
applicationIconMap,
|
||||
applicationIconEmptyWorkspace,
|
||||
applicationIconFallback,
|
||||
} = options.bar.workspaces;
|
||||
const { available, active, occupied } = options.bar.workspaces.icons;
|
||||
const { matugen } = options.theme;
|
||||
const { smartHighlight } = options.theme.bar.buttons.workspaces;
|
||||
|
||||
setupConnections();
|
||||
|
||||
export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element => {
|
||||
const boxChildren = Variable.derive(
|
||||
[
|
||||
bind(monitorSpecific),
|
||||
bind(hyprlandService, 'workspaces'),
|
||||
bind(workspaceMask),
|
||||
bind(workspaces),
|
||||
bind(show_icons),
|
||||
bind(available),
|
||||
bind(active),
|
||||
bind(occupied),
|
||||
bind(show_numbered),
|
||||
bind(numbered_active_indicator),
|
||||
bind(spacing),
|
||||
bind(workspaceIconMap),
|
||||
bind(showWsIcons),
|
||||
bind(showApplicationIcons),
|
||||
bind(applicationIconOncePerWorkspace),
|
||||
bind(applicationIconMap),
|
||||
bind(applicationIconEmptyWorkspace),
|
||||
bind(applicationIconFallback),
|
||||
bind(matugen),
|
||||
bind(smartHighlight),
|
||||
|
||||
bind(hyprlandService, 'monitors'),
|
||||
bind(ignored),
|
||||
bind(showAllActive),
|
||||
bind(hyprlandService, 'focusedWorkspace'),
|
||||
bind(workspaceRules),
|
||||
bind(forceUpdater),
|
||||
],
|
||||
(
|
||||
isMonitorSpecific: boolean,
|
||||
workspaceList: AstalHyprland.Workspace[],
|
||||
workspaceMaskFlag: boolean,
|
||||
totalWorkspaces: number,
|
||||
displayIcons: boolean,
|
||||
availableStatus: string,
|
||||
activeStatus: string,
|
||||
occupiedStatus: string,
|
||||
displayNumbered: boolean,
|
||||
numberedActiveIndicator: string,
|
||||
spacingValue: number,
|
||||
workspaceIconMapping: WorkspaceIconMap,
|
||||
displayWorkspaceIcons: boolean,
|
||||
displayApplicationIcons: boolean,
|
||||
appIconOncePerWorkspace: boolean,
|
||||
applicationIconMapping: ApplicationIcons,
|
||||
applicationIconEmptyWorkspace: string,
|
||||
applicationIconFallback: string,
|
||||
matugenEnabled: boolean,
|
||||
smartHighlightEnabled: boolean,
|
||||
monitorList: AstalHyprland.Monitor[],
|
||||
) => {
|
||||
const activeWorkspace = hyprlandService.focusedWorkspace.id;
|
||||
|
||||
const workspacesToRender = getWorkspacesToRender(
|
||||
totalWorkspaces,
|
||||
workspaceList,
|
||||
workspaceRules.get(),
|
||||
monitor,
|
||||
isMonitorSpecific,
|
||||
monitorList,
|
||||
);
|
||||
|
||||
return workspacesToRender.map((wsId, index) => {
|
||||
if (isWorkspaceIgnored(ignored, wsId)) {
|
||||
return <box />;
|
||||
}
|
||||
|
||||
const appIcons = displayApplicationIcons
|
||||
? getAppIcon(wsId, appIconOncePerWorkspace, {
|
||||
iconMap: applicationIconMapping,
|
||||
defaultIcon: applicationIconFallback,
|
||||
emptyIcon: applicationIconEmptyWorkspace,
|
||||
})
|
||||
: '';
|
||||
|
||||
return (
|
||||
<button
|
||||
className={'workspace-button'}
|
||||
onClick={(_, event) => {
|
||||
if (isPrimaryClick(event)) {
|
||||
hyprlandService.dispatch('workspace', wsId.toString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label
|
||||
valign={Gtk.Align.CENTER}
|
||||
css={
|
||||
`margin: 0rem ${0.375 * spacingValue}rem;` +
|
||||
`${displayWorkspaceIcons && !matugenEnabled ? getWsColor(workspaceIconMapping, wsId, smartHighlightEnabled, monitor) : ''}`
|
||||
}
|
||||
className={renderClassnames(
|
||||
displayIcons,
|
||||
displayNumbered,
|
||||
numberedActiveIndicator,
|
||||
displayWorkspaceIcons,
|
||||
smartHighlightEnabled,
|
||||
monitor,
|
||||
wsId,
|
||||
)}
|
||||
label={renderLabel(
|
||||
displayIcons,
|
||||
availableStatus,
|
||||
activeStatus,
|
||||
occupiedStatus,
|
||||
displayApplicationIcons,
|
||||
appIcons,
|
||||
workspaceMaskFlag,
|
||||
displayWorkspaceIcons,
|
||||
workspaceIconMapping,
|
||||
wsId,
|
||||
index,
|
||||
monitor,
|
||||
)}
|
||||
setup={(self) => {
|
||||
self.toggleClassName('active', activeWorkspace === wsId);
|
||||
self.toggleClassName(
|
||||
'occupied',
|
||||
(hyprlandService.get_workspace(wsId)?.get_clients().length || 0) > 0,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<box
|
||||
onDestroy={() => {
|
||||
boxChildren.drop();
|
||||
}}
|
||||
>
|
||||
{boxChildren()}
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceModuleProps {
|
||||
monitor: number;
|
||||
}
|
||||
286
src/components/bar/settings/config.tsx
Normal file
286
src/components/bar/settings/config.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import { Option } from 'src/components/settings/shared/Option';
|
||||
import { Header } from 'src/components/settings/shared/Header';
|
||||
import options from 'src/options';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
|
||||
export const CustomModuleSettings = (): JSX.Element => {
|
||||
return (
|
||||
<scrollable
|
||||
name={'Custom Modules'}
|
||||
className="menu-theme-page customModules paged-container"
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
hscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
>
|
||||
<box className="menu-theme-page paged-container" vertical>
|
||||
{/* General Section */}
|
||||
<Header title="General" />
|
||||
<Option opt={options.bar.customModules.scrollSpeed} title="Scrolling Speed" type="number" />
|
||||
|
||||
{/* RAM Section */}
|
||||
<Header title="RAM" />
|
||||
<Option opt={options.theme.bar.buttons.modules.ram.enableBorder} title="Button Border" type="boolean" />
|
||||
<Option opt={options.bar.customModules.ram.icon} title="Ram Icon" type="string" />
|
||||
<Option opt={options.bar.customModules.ram.label} title="Show Label" type="boolean" />
|
||||
<Option opt={options.theme.bar.buttons.modules.ram.spacing} title="Spacing" type="string" />
|
||||
<Option
|
||||
opt={options.bar.customModules.ram.labelType}
|
||||
title="Label Type"
|
||||
type="enum"
|
||||
enums={['used/total', 'used', 'free', 'percentage']}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.ram.round} title="Round" type="boolean" />
|
||||
<Option
|
||||
opt={options.bar.customModules.ram.pollingInterval}
|
||||
title="Polling Interval"
|
||||
type="number"
|
||||
min={100}
|
||||
max={60 * 24 * 1000}
|
||||
increment={1000}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.ram.leftClick} title="Left Click" type="string" />
|
||||
<Option opt={options.bar.customModules.ram.rightClick} title="Right Click" type="string" />
|
||||
<Option opt={options.bar.customModules.ram.middleClick} title="Middle Click" type="string" />
|
||||
|
||||
{/* CPU Section */}
|
||||
<Header title="CPU" />
|
||||
<Option opt={options.theme.bar.buttons.modules.cpu.enableBorder} title="Button Border" type="boolean" />
|
||||
<Option opt={options.bar.customModules.cpu.icon} title="Cpu Icon" type="string" />
|
||||
<Option opt={options.bar.customModules.cpu.label} title="Show Label" type="boolean" />
|
||||
<Option opt={options.theme.bar.buttons.modules.cpu.spacing} title="Spacing" type="string" />
|
||||
<Option opt={options.bar.customModules.cpu.round} title="Round" type="boolean" />
|
||||
<Option
|
||||
opt={options.bar.customModules.cpu.pollingInterval}
|
||||
title="Polling Interval"
|
||||
type="number"
|
||||
min={100}
|
||||
max={60 * 24 * 1000}
|
||||
increment={1000}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.cpu.leftClick} title="Left Click" type="string" />
|
||||
<Option opt={options.bar.customModules.cpu.rightClick} title="Right Click" type="string" />
|
||||
<Option opt={options.bar.customModules.cpu.middleClick} title="Middle Click" type="string" />
|
||||
<Option opt={options.bar.customModules.cpu.scrollUp} title="Scroll Up" type="string" />
|
||||
<Option opt={options.bar.customModules.cpu.scrollDown} title="Scroll Down" type="string" />
|
||||
|
||||
{/* CPU Temperature Section */}
|
||||
<Header title="CPU Temperature" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.cpuTemp.enableBorder}
|
||||
title="Button Border"
|
||||
type="boolean"
|
||||
/>
|
||||
<Option
|
||||
opt={options.bar.customModules.cpuTemp.sensor}
|
||||
title="CPU Temperature Sensor"
|
||||
subtitle="Wiki: https://hyprpanel.com/configuration/panel.html#custom-modules"
|
||||
subtitleLink="https://hyprpanel.com/configuration/panel.html#custom-modules"
|
||||
type="string"
|
||||
/>
|
||||
<Option
|
||||
opt={options.bar.customModules.cpuTemp.unit}
|
||||
title="CPU Temperature Unit"
|
||||
type="enum"
|
||||
enums={['imperial', 'metric']}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.cpuTemp.showUnit} title="Show Unit" type="boolean" />
|
||||
<Option opt={options.bar.customModules.cpuTemp.icon} title="Cpu Temperature Icon" type="string" />
|
||||
<Option opt={options.bar.customModules.cpuTemp.label} title="Show Label" type="boolean" />
|
||||
<Option opt={options.theme.bar.buttons.modules.cpuTemp.spacing} title="Spacing" type="string" />
|
||||
<Option opt={options.bar.customModules.cpuTemp.round} title="Round" type="boolean" />
|
||||
<Option
|
||||
opt={options.bar.customModules.cpuTemp.pollingInterval}
|
||||
title="Polling Interval"
|
||||
type="number"
|
||||
min={100}
|
||||
max={60 * 24 * 1000}
|
||||
increment={1000}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.cpuTemp.leftClick} title="Left Click" type="string" />
|
||||
<Option opt={options.bar.customModules.cpuTemp.rightClick} title="Right Click" type="string" />
|
||||
<Option opt={options.bar.customModules.cpuTemp.middleClick} title="Middle Click" type="string" />
|
||||
<Option opt={options.bar.customModules.cpuTemp.scrollUp} title="Scroll Up" type="string" />
|
||||
<Option opt={options.bar.customModules.cpuTemp.scrollDown} title="Scroll Down" type="string" />
|
||||
|
||||
{/* Storage Section */}
|
||||
<Header title="Storage" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.storage.enableBorder}
|
||||
title="Button Border"
|
||||
type="boolean"
|
||||
/>
|
||||
<Option opt={options.bar.customModules.storage.icon} title="Storage Icon" type="string" />
|
||||
<Option opt={options.bar.customModules.storage.label} title="Show Label" type="boolean" />
|
||||
<Option opt={options.theme.bar.buttons.modules.storage.spacing} title="Spacing" type="string" />
|
||||
<Option
|
||||
opt={options.bar.customModules.storage.labelType}
|
||||
title="Label Type"
|
||||
type="enum"
|
||||
enums={['used/total', 'used', 'free', 'percentage']}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.storage.round} title="Round" type="boolean" />
|
||||
<Option
|
||||
opt={options.bar.customModules.storage.pollingInterval}
|
||||
title="Polling Interval"
|
||||
type="number"
|
||||
min={100}
|
||||
max={60 * 24 * 1000}
|
||||
increment={1000}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.storage.leftClick} title="Left Click" type="string" />
|
||||
<Option opt={options.bar.customModules.storage.rightClick} title="Right Click" type="string" />
|
||||
<Option opt={options.bar.customModules.storage.middleClick} title="Middle Click" type="string" />
|
||||
|
||||
{/* Netstat Section */}
|
||||
<Header title="Netstat" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.netstat.enableBorder}
|
||||
title="Button Border"
|
||||
type="boolean"
|
||||
/>
|
||||
<Option
|
||||
opt={options.bar.customModules.netstat.networkInterface}
|
||||
title="Network Interface"
|
||||
subtitle="Wiki: https://hyprpanel.com/configuration/panel.html#custom-modules"
|
||||
type="string"
|
||||
/>
|
||||
<Option
|
||||
opt={options.bar.customModules.netstat.dynamicIcon}
|
||||
title="Use Network Icon"
|
||||
subtitle="If enabled, shows current network icon indicators instead of static icon"
|
||||
type="boolean"
|
||||
/>
|
||||
<Option opt={options.bar.customModules.netstat.icon} title="Netstat Icon" type="string" />
|
||||
<Option opt={options.bar.customModules.netstat.label} title="Show Label" type="boolean" />
|
||||
<Option
|
||||
opt={options.bar.customModules.netstat.rateUnit}
|
||||
title="Rate Unit"
|
||||
type="enum"
|
||||
enums={['GiB', 'MiB', 'KiB', 'auto']}
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.netstat.spacing} title="Spacing" type="string" />
|
||||
<Option
|
||||
opt={options.bar.customModules.netstat.labelType}
|
||||
title="Label Type"
|
||||
type="enum"
|
||||
enums={['full', 'in', 'out']}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.netstat.round} title="Round" type="boolean" />
|
||||
<Option
|
||||
opt={options.bar.customModules.netstat.pollingInterval}
|
||||
title="Polling Interval"
|
||||
type="number"
|
||||
min={100}
|
||||
max={60 * 24 * 1000}
|
||||
increment={1000}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.netstat.leftClick} title="Left Click" type="string" />
|
||||
<Option opt={options.bar.customModules.netstat.rightClick} title="Right Click" type="string" />
|
||||
<Option opt={options.bar.customModules.netstat.middleClick} title="Middle Click" type="string" />
|
||||
|
||||
{/* Keyboard Layout Section */}
|
||||
<Header title="Keyboard Layout" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.kbLayout.enableBorder}
|
||||
title="Button Border"
|
||||
type="boolean"
|
||||
/>
|
||||
<Option opt={options.bar.customModules.kbLayout.icon} title="Keyboard Layout Icon" type="string" />
|
||||
<Option opt={options.bar.customModules.kbLayout.label} title="Show Label" type="boolean" />
|
||||
<Option
|
||||
opt={options.bar.customModules.kbLayout.labelType}
|
||||
title="Label Type"
|
||||
type="enum"
|
||||
enums={['layout', 'code']}
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.kbLayout.spacing} title="Spacing" type="string" />
|
||||
<Option opt={options.bar.customModules.kbLayout.leftClick} title="Left Click" type="string" />
|
||||
<Option opt={options.bar.customModules.kbLayout.rightClick} title="Right Click" type="string" />
|
||||
<Option opt={options.bar.customModules.kbLayout.middleClick} title="Middle Click" type="string" />
|
||||
<Option opt={options.bar.customModules.kbLayout.scrollUp} title="Scroll Up" type="string" />
|
||||
<Option opt={options.bar.customModules.kbLayout.scrollDown} title="Scroll Down" type="string" />
|
||||
|
||||
{/* Updates Section */}
|
||||
<Header title="Updates" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.updates.enableBorder}
|
||||
title="Button Border"
|
||||
type="boolean"
|
||||
/>
|
||||
<Option
|
||||
opt={options.bar.customModules.updates.updateCommand}
|
||||
title="Check Updates Command"
|
||||
type="string"
|
||||
/>
|
||||
<Option
|
||||
opt={options.bar.customModules.updates.icon.pending}
|
||||
title="Updates Available Icon"
|
||||
type="string"
|
||||
/>
|
||||
<Option opt={options.bar.customModules.updates.icon.updated} title="No Updates Icon" type="string" />
|
||||
<Option opt={options.bar.customModules.updates.label} title="Show Label" type="boolean" />
|
||||
<Option opt={options.bar.customModules.updates.padZero} title="Pad with 0" type="boolean" />
|
||||
<Option opt={options.theme.bar.buttons.modules.updates.spacing} title="Spacing" type="string" />
|
||||
<Option
|
||||
opt={options.bar.customModules.updates.pollingInterval}
|
||||
title="Polling Interval"
|
||||
subtitle="WARNING: Be careful of your package manager's rate limit."
|
||||
type="number"
|
||||
min={100}
|
||||
max={60 * 24 * 1000}
|
||||
increment={1000}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.updates.leftClick} title="Left Click" type="string" />
|
||||
<Option opt={options.bar.customModules.updates.rightClick} title="Right Click" type="string" />
|
||||
<Option opt={options.bar.customModules.updates.middleClick} title="Middle Click" type="string" />
|
||||
<Option opt={options.bar.customModules.updates.scrollUp} title="Scroll Up" type="string" />
|
||||
<Option opt={options.bar.customModules.updates.scrollDown} title="Scroll Down" type="string" />
|
||||
|
||||
{/* Submap Section */}
|
||||
<Header title="Submap" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.submap.enableBorder}
|
||||
title="Button Border"
|
||||
type="boolean"
|
||||
/>
|
||||
<Option
|
||||
opt={options.bar.customModules.submap.showSubmapName}
|
||||
title="Show Submap Name"
|
||||
subtitle="Displays current submap name instead of Enabled/Disabled text."
|
||||
type="boolean"
|
||||
/>
|
||||
<Option opt={options.bar.customModules.submap.enabledIcon} title="Enabled Icon" type="string" />
|
||||
<Option opt={options.bar.customModules.submap.disabledIcon} title="Disabled Icon" type="string" />
|
||||
<Option opt={options.bar.customModules.submap.enabledText} title="Enabled Text" type="string" />
|
||||
<Option opt={options.bar.customModules.submap.disabledText} title="Disabled Text" type="string" />
|
||||
<Option opt={options.bar.customModules.submap.label} title="Show Label" type="boolean" />
|
||||
<Option opt={options.theme.bar.buttons.modules.submap.spacing} title="Spacing" type="string" />
|
||||
<Option opt={options.bar.customModules.submap.leftClick} title="Left Click" type="string" />
|
||||
<Option opt={options.bar.customModules.submap.rightClick} title="Right Click" type="string" />
|
||||
<Option opt={options.bar.customModules.submap.middleClick} title="Middle Click" type="string" />
|
||||
<Option opt={options.bar.customModules.submap.scrollUp} title="Scroll Up" type="string" />
|
||||
<Option opt={options.bar.customModules.submap.scrollDown} title="Scroll Down" type="string" />
|
||||
|
||||
{/* Weather Section */}
|
||||
<Header title="Weather" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.weather.enableBorder}
|
||||
title="Button Border"
|
||||
type="boolean"
|
||||
/>
|
||||
<Option opt={options.bar.customModules.weather.label} title="Show Label" type="boolean" />
|
||||
<Option
|
||||
opt={options.bar.customModules.weather.unit}
|
||||
title="Units"
|
||||
type="enum"
|
||||
enums={['imperial', 'metric']}
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.weather.spacing} title="Spacing" type="string" />
|
||||
<Option opt={options.bar.customModules.weather.leftClick} title="Left Click" type="string" />
|
||||
<Option opt={options.bar.customModules.weather.rightClick} title="Right Click" type="string" />
|
||||
<Option opt={options.bar.customModules.weather.middleClick} title="Middle Click" type="string" />
|
||||
<Option opt={options.bar.customModules.weather.scrollUp} title="Scroll Up" type="string" />
|
||||
<Option opt={options.bar.customModules.weather.scrollDown} title="Scroll Down" type="string" />
|
||||
</box>
|
||||
</scrollable>
|
||||
);
|
||||
};
|
||||
164
src/components/bar/settings/theme.tsx
Normal file
164
src/components/bar/settings/theme.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Option } from 'src/components/settings/shared/Option';
|
||||
import { Header } from 'src/components/settings/shared/Header';
|
||||
|
||||
import options from 'src/options';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
|
||||
export const CustomModuleTheme = (): JSX.Element => {
|
||||
return (
|
||||
<scrollable
|
||||
name={'Custom Modules'}
|
||||
className="menu-theme-page customModules paged-container"
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
hscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
vexpand={false}
|
||||
>
|
||||
<box vertical>
|
||||
{/* RAM Module Section */}
|
||||
<Header title="RAM" />
|
||||
<Option opt={options.theme.bar.buttons.modules.ram.text} title="Text" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.ram.icon} title="Icon" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.ram.background} title="Label Background" type="color" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.ram.icon_background}
|
||||
title="Icon Background"
|
||||
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
|
||||
type="color"
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.ram.border} title="Border" type="color" />
|
||||
|
||||
{/* CPU Module Section */}
|
||||
<Header title="CPU" />
|
||||
<Option opt={options.theme.bar.buttons.modules.cpu.text} title="Text" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.cpu.icon} title="Icon" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.cpu.background} title="Label Background" type="color" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.cpu.icon_background}
|
||||
title="Icon Background"
|
||||
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
|
||||
type="color"
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.cpu.border} title="Border" type="color" />
|
||||
|
||||
{/* CPU Temperature Module Section */}
|
||||
<Header title="CPU Temperature" />
|
||||
<Option opt={options.theme.bar.buttons.modules.cpuTemp.text} title="Text" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.cpuTemp.icon} title="Icon" type="color" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.cpuTemp.background}
|
||||
title="Label Background"
|
||||
type="color"
|
||||
/>
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.cpuTemp.icon_background}
|
||||
title="Icon Background"
|
||||
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
|
||||
type="color"
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.cpuTemp.border} title="Border" type="color" />
|
||||
|
||||
{/* Storage Module Section */}
|
||||
<Header title="Storage" />
|
||||
<Option opt={options.theme.bar.buttons.modules.storage.text} title="Text" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.storage.icon} title="Icon" type="color" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.storage.background}
|
||||
title="Label Background"
|
||||
type="color"
|
||||
/>
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.storage.icon_background}
|
||||
title="Icon Background"
|
||||
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
|
||||
type="color"
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.storage.border} title="Border" type="color" />
|
||||
|
||||
{/* Netstat Module Section */}
|
||||
<Header title="Netstat" />
|
||||
<Option opt={options.theme.bar.buttons.modules.netstat.text} title="Text" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.netstat.icon} title="Icon" type="color" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.netstat.background}
|
||||
title="Label Background"
|
||||
type="color"
|
||||
/>
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.netstat.icon_background}
|
||||
title="Icon Background"
|
||||
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
|
||||
type="color"
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.netstat.border} title="Border" type="color" />
|
||||
|
||||
{/* Keyboard Layout Module Section */}
|
||||
<Header title="Keyboard Layout" />
|
||||
<Option opt={options.theme.bar.buttons.modules.kbLayout.text} title="Text" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.kbLayout.icon} title="Icon" type="color" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.kbLayout.background}
|
||||
title="Label Background"
|
||||
type="color"
|
||||
/>
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.kbLayout.icon_background}
|
||||
title="Icon Background"
|
||||
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
|
||||
type="color"
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.kbLayout.border} title="Border" type="color" />
|
||||
|
||||
{/* Updates Module Section */}
|
||||
<Header title="Updates" />
|
||||
<Option opt={options.theme.bar.buttons.modules.updates.text} title="Text" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.updates.icon} title="Icon" type="color" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.updates.background}
|
||||
title="Label Background"
|
||||
type="color"
|
||||
/>
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.updates.icon_background}
|
||||
title="Icon Background"
|
||||
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
|
||||
type="color"
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.updates.border} title="Border" type="color" />
|
||||
|
||||
{/* Submap Module Section */}
|
||||
<Header title="Submap" />
|
||||
<Option opt={options.theme.bar.buttons.modules.submap.text} title="Text" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.submap.icon} title="Icon" type="color" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.submap.background}
|
||||
title="Label Background"
|
||||
type="color"
|
||||
/>
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.submap.icon_background}
|
||||
title="Icon Background"
|
||||
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
|
||||
type="color"
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.submap.border} title="Border" type="color" />
|
||||
|
||||
{/* Weather Module Section */}
|
||||
<Header title="Weather" />
|
||||
<Option opt={options.theme.bar.buttons.modules.weather.icon} title="Icon" type="color" />
|
||||
<Option opt={options.theme.bar.buttons.modules.weather.text} title="Text" type="color" />
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.weather.background}
|
||||
title="Label Background"
|
||||
type="color"
|
||||
/>
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.weather.icon_background}
|
||||
title="Icon Background"
|
||||
subtitle="Applies a background color to the icon section of the button.\nRequires 'split' button styling."
|
||||
type="color"
|
||||
/>
|
||||
<Option opt={options.theme.bar.buttons.modules.weather.border} title="Border" type="color" />
|
||||
</box>
|
||||
</scrollable>
|
||||
);
|
||||
};
|
||||
93
src/components/bar/shared/Module.tsx
Normal file
93
src/components/bar/shared/Module.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { bind, Variable } from 'astal';
|
||||
import { BarBoxChild, BarModule } from 'src/lib/types/bar';
|
||||
import { BarButtonStyles } from 'src/lib/types/options';
|
||||
import options from 'src/options';
|
||||
|
||||
const { style } = options.theme.bar.buttons;
|
||||
|
||||
const undefinedVar = Variable(undefined);
|
||||
|
||||
export const Module = ({
|
||||
icon,
|
||||
textIcon,
|
||||
useTextIcon = bind(Variable(false)),
|
||||
label,
|
||||
tooltipText,
|
||||
boxClass,
|
||||
props = {},
|
||||
showLabelBinding = bind(undefinedVar),
|
||||
showLabel,
|
||||
labelHook,
|
||||
hook,
|
||||
}: BarModule): BarBoxChild => {
|
||||
const getIconWidget = (useTxtIcn: boolean): JSX.Element | undefined => {
|
||||
let iconWidget: JSX.Element | undefined;
|
||||
|
||||
if (icon !== undefined && !useTxtIcn) {
|
||||
iconWidget = <icon className={`txt-icon bar-button-icon module-icon ${boxClass}`} icon={icon} />;
|
||||
} else if (textIcon !== undefined) {
|
||||
iconWidget = <label className={`txt-icon bar-button-icon module-icon ${boxClass}`} label={textIcon} />;
|
||||
}
|
||||
|
||||
return iconWidget;
|
||||
};
|
||||
|
||||
const componentClass = Variable.derive(
|
||||
[bind(style), showLabelBinding],
|
||||
(style: BarButtonStyles, shwLabel: boolean) => {
|
||||
const shouldShowLabel = shwLabel || showLabel;
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `${boxClass} ${styleMap[style]} ${!shouldShowLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
);
|
||||
|
||||
const componentChildren = Variable.derive(
|
||||
[showLabelBinding, useTextIcon],
|
||||
(showLabel: boolean, forceTextIcon: boolean): JSX.Element[] => {
|
||||
const childrenArray = [];
|
||||
const iconWidget = getIconWidget(forceTextIcon);
|
||||
|
||||
if (iconWidget !== undefined) {
|
||||
childrenArray.push(iconWidget);
|
||||
}
|
||||
|
||||
if (showLabel) {
|
||||
childrenArray.push(
|
||||
<label
|
||||
className={`bar-button-label module-label ${boxClass}`}
|
||||
label={label ?? ''}
|
||||
setup={labelHook}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return childrenArray;
|
||||
},
|
||||
);
|
||||
|
||||
const component: JSX.Element = (
|
||||
<box
|
||||
tooltipText={tooltipText}
|
||||
className={componentClass()}
|
||||
setup={hook}
|
||||
onDestroy={() => {
|
||||
componentChildren.drop();
|
||||
componentClass.drop();
|
||||
}}
|
||||
>
|
||||
{componentChildren()}
|
||||
</box>
|
||||
);
|
||||
|
||||
return {
|
||||
component,
|
||||
tooltip_text: tooltipText,
|
||||
isVisible: true,
|
||||
boxClass,
|
||||
props,
|
||||
};
|
||||
};
|
||||
40
src/components/bar/shared/WidgetContainer.tsx
Normal file
40
src/components/bar/shared/WidgetContainer.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { BarBoxChild } from 'src/lib/types/bar';
|
||||
import { Bind } from '../../../lib/types/variable';
|
||||
import options from '../../../options';
|
||||
import { bind } from 'astal';
|
||||
|
||||
const computeVisible = (child: BarBoxChild): Bind | boolean => {
|
||||
if (child.isVis !== undefined) {
|
||||
return bind(child.isVis);
|
||||
}
|
||||
return child.isVisible;
|
||||
};
|
||||
|
||||
export const WidgetContainer = (child: BarBoxChild): JSX.Element => {
|
||||
const buttonClassName = bind(options.theme.bar.buttons.style).as((style) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style4',
|
||||
};
|
||||
|
||||
const boxClassName = Object.hasOwnProperty.call(child, 'boxClass') ? child.boxClass : '';
|
||||
|
||||
return `bar_item_box_visible ${styleMap[style]} ${boxClassName}`;
|
||||
});
|
||||
|
||||
if (child.isBox) {
|
||||
return (
|
||||
<box className={buttonClassName} visible={computeVisible(child)}>
|
||||
{child.component}
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={buttonClassName} visible={computeVisible(child)} {...child.props}>
|
||||
{child.component}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
403
src/components/bar/utils/helpers.ts
Normal file
403
src/components/bar/utils/helpers.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import { ResourceLabelType } from 'src/lib/types/bar';
|
||||
import { GenericResourceData, Postfix, UpdateHandlers } from 'src/lib/types/customModules/generic';
|
||||
import { InputHandlerEvents, RunAsyncCommand } from 'src/lib/types/customModules/utils';
|
||||
import { ThrottleFn } from 'src/lib/types/utils';
|
||||
import { bind, Binding, execAsync, Variable } from 'astal';
|
||||
import { openMenu } from 'src/components/bar/utils/menu';
|
||||
import options from 'src/options';
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
import { GtkWidget } from 'src/lib/types/widget';
|
||||
import { onMiddleClick, onPrimaryClick, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
|
||||
const { scrollSpeed } = options.bar.customModules;
|
||||
|
||||
const dummyVar = Variable('');
|
||||
|
||||
/**
|
||||
* Handles the post input updater by toggling its value.
|
||||
*
|
||||
* This function checks if the `postInputUpdater` variable is defined. If it is, it toggles its value.
|
||||
*
|
||||
* @param postInputUpdater An optional Variable<boolean> that tracks the post input update state.
|
||||
*/
|
||||
const handlePostInputUpdater = (postInputUpdater?: Variable<boolean>): void => {
|
||||
if (postInputUpdater !== undefined) {
|
||||
postInputUpdater.set(!postInputUpdater.get());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes an asynchronous command and handles the result.
|
||||
*
|
||||
* This function runs a given command asynchronously using `execAsync`. If the command starts with 'menu:', it opens the specified menu.
|
||||
* Otherwise, it executes the command in a bash shell. After execution, it handles the post input updater and calls the provided callback function with the command output.
|
||||
*
|
||||
* @param cmd The command to execute.
|
||||
* @param events An object containing the clicked widget and event information.
|
||||
* @param fn An optional callback function to handle the command output.
|
||||
* @param postInputUpdater An optional Variable<boolean> that tracks the post input update state.
|
||||
*/
|
||||
export const runAsyncCommand: RunAsyncCommand = (cmd, events, fn, postInputUpdater?: Variable<boolean>): void => {
|
||||
if (cmd.startsWith('menu:')) {
|
||||
const menuName = cmd.split(':')[1].trim().toLowerCase();
|
||||
openMenu(events.clicked, events.event, `${menuName}menu`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
execAsync(`bash -c "${cmd}"`)
|
||||
.then((output) => {
|
||||
handlePostInputUpdater(postInputUpdater);
|
||||
if (fn !== undefined) {
|
||||
fn(output);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(`Error running command "${cmd}": ${err})`));
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic throttle function to limit the rate at which a function can be called.
|
||||
*
|
||||
* This function creates a throttled version of the provided function that can only be called once within the specified limit.
|
||||
*
|
||||
* @param func The function to throttle.
|
||||
* @param limit The time limit in milliseconds.
|
||||
*
|
||||
* @returns The throttled function.
|
||||
*/
|
||||
export function throttleInput<T extends ThrottleFn>(func: T, limit: number): T {
|
||||
let inThrottle = false;
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a throttled scroll handler with the given interval.
|
||||
*
|
||||
* This function returns a throttled version of the `runAsyncCommand` function that can be called with the specified interval.
|
||||
*
|
||||
* @param interval The interval in milliseconds.
|
||||
*
|
||||
* @returns The throttled scroll handler function.
|
||||
*/
|
||||
export const throttledScrollHandler = (interval: number): ThrottleFn =>
|
||||
throttleInput((cmd: string, args, fn, postInputUpdater) => {
|
||||
runAsyncCommand(cmd, args, fn, postInputUpdater);
|
||||
}, 200 / interval);
|
||||
|
||||
/**
|
||||
* Handles input events for a GtkWidget.
|
||||
*
|
||||
* This function sets up event handlers for primary, secondary, and middle clicks, as well as scroll events.
|
||||
* It uses the provided input handler events and post input updater to manage the input state.
|
||||
*
|
||||
* @param self The GtkWidget instance to handle input events for.
|
||||
* @param inputHandlerEvents An object containing the input handler events for primary, secondary, and middle clicks, as well as scroll up and down.
|
||||
* @param postInputUpdater An optional Variable<boolean> that tracks the post input update state.
|
||||
*/
|
||||
export const inputHandler = (
|
||||
self: GtkWidget,
|
||||
{
|
||||
onPrimaryClick: onPrimaryClickInput,
|
||||
onSecondaryClick: onSecondaryClickInput,
|
||||
onMiddleClick: onMiddleClickInput,
|
||||
onScrollUp: onScrollUpInput,
|
||||
onScrollDown: onScrollDownInput,
|
||||
}: InputHandlerEvents,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
): void => {
|
||||
const sanitizeInput = (input: Variable<string>): string => {
|
||||
if (input === undefined) {
|
||||
return '';
|
||||
}
|
||||
return input.get();
|
||||
};
|
||||
|
||||
const updateHandlers = (): UpdateHandlers => {
|
||||
const interval = scrollSpeed.get();
|
||||
const throttledHandler = throttledScrollHandler(interval);
|
||||
|
||||
const disconnectPrimaryClick = onPrimaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
|
||||
runAsyncCommand(
|
||||
sanitizeInput(onPrimaryClickInput?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onPrimaryClickInput.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
});
|
||||
|
||||
const disconnectSecondaryClick = onSecondaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
|
||||
runAsyncCommand(
|
||||
sanitizeInput(onSecondaryClickInput?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onSecondaryClickInput.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
});
|
||||
|
||||
const disconnectMiddleClick = onMiddleClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
|
||||
runAsyncCommand(
|
||||
sanitizeInput(onMiddleClickInput?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onMiddleClickInput.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
});
|
||||
|
||||
const id = self.connect('scroll-event', (self: GtkWidget, event: Gdk.Event) => {
|
||||
const [directionSuccess, direction] = event.get_scroll_direction();
|
||||
const [deltaSuccess, , yScroll] = event.get_scroll_deltas();
|
||||
|
||||
const handleScroll = (input?: { cmd: Variable<string>; fn: (output: string) => void }): void => {
|
||||
if (input) {
|
||||
throttledHandler(sanitizeInput(input.cmd), { clicked: self, event }, input.fn, postInputUpdater);
|
||||
}
|
||||
};
|
||||
|
||||
if (directionSuccess) {
|
||||
if (direction === Gdk.ScrollDirection.UP) {
|
||||
handleScroll(onScrollUpInput);
|
||||
} else if (direction === Gdk.ScrollDirection.DOWN) {
|
||||
handleScroll(onScrollDownInput);
|
||||
}
|
||||
}
|
||||
|
||||
if (deltaSuccess) {
|
||||
if (yScroll > 0) {
|
||||
handleScroll(onScrollUpInput);
|
||||
} else if (yScroll < 0) {
|
||||
handleScroll(onScrollDownInput);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
disconnectPrimary: disconnectPrimaryClick,
|
||||
disconnectSecondary: disconnectSecondaryClick,
|
||||
disconnectMiddle: disconnectMiddleClick,
|
||||
disconnectScroll: () => self.disconnect(id),
|
||||
};
|
||||
};
|
||||
|
||||
updateHandlers();
|
||||
|
||||
const sanitizeVariable = (someVar: Variable<string> | undefined): Binding<string> => {
|
||||
if (someVar === undefined || typeof someVar.bind !== 'function') {
|
||||
return bind(dummyVar);
|
||||
}
|
||||
return bind(someVar);
|
||||
};
|
||||
|
||||
Variable.derive(
|
||||
[
|
||||
bind(scrollSpeed),
|
||||
sanitizeVariable(onPrimaryClickInput),
|
||||
sanitizeVariable(onSecondaryClickInput),
|
||||
sanitizeVariable(onMiddleClickInput),
|
||||
sanitizeVariable(onScrollUpInput),
|
||||
sanitizeVariable(onScrollDownInput),
|
||||
],
|
||||
() => {
|
||||
const handlers = updateHandlers();
|
||||
|
||||
handlers.disconnectPrimary();
|
||||
handlers.disconnectSecondary();
|
||||
handlers.disconnectMiddle();
|
||||
handlers.disconnectScroll();
|
||||
},
|
||||
)();
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the percentage of used resources.
|
||||
*
|
||||
* This function calculates the percentage of used resources based on the total and used values.
|
||||
* It can optionally round the result to the nearest integer.
|
||||
*
|
||||
* @param totalUsed An array containing the total and used values.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The percentage of used resources as a number.
|
||||
*/
|
||||
export const divide = ([total, used]: number[], round: boolean): number => {
|
||||
const percentageTotal = (used / total) * 100;
|
||||
if (round) {
|
||||
return total > 0 ? Math.round(percentageTotal) : 0;
|
||||
}
|
||||
return total > 0 ? parseFloat(percentageTotal.toFixed(2)) : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a size in bytes to KiB.
|
||||
*
|
||||
* This function converts a size in bytes to kibibytes (KiB) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The size in KiB as a number.
|
||||
*/
|
||||
export const formatSizeInKiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 1;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a size in bytes to MiB.
|
||||
*
|
||||
* This function converts a size in bytes to mebibytes (MiB) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The size in MiB as a number.
|
||||
*/
|
||||
export const formatSizeInMiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 2;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a size in bytes to GiB.
|
||||
*
|
||||
* This function converts a size in bytes to gibibytes (GiB) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The size in GiB as a number.
|
||||
*/
|
||||
export const formatSizeInGiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 3;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a size in bytes to TiB.
|
||||
*
|
||||
* This function converts a size in bytes to tebibytes (TiB) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The size in TiB as a number.
|
||||
*/
|
||||
export const formatSizeInTiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 4;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically formats a size in bytes to the appropriate unit.
|
||||
*
|
||||
* This function converts a size in bytes to the most appropriate unit (TiB, GiB, MiB, KiB, or bytes) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The formatted size as a number.
|
||||
*/
|
||||
export const autoFormatSize = (sizeInBytes: number, round: boolean): number => {
|
||||
// auto convert to GiB, MiB, KiB, TiB, or bytes
|
||||
if (sizeInBytes >= 1024 ** 4) return formatSizeInTiB(sizeInBytes, round);
|
||||
if (sizeInBytes >= 1024 ** 3) return formatSizeInGiB(sizeInBytes, round);
|
||||
if (sizeInBytes >= 1024 ** 2) return formatSizeInMiB(sizeInBytes, round);
|
||||
if (sizeInBytes >= 1024 ** 1) return formatSizeInKiB(sizeInBytes, round);
|
||||
|
||||
return sizeInBytes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the appropriate postfix for a size in bytes.
|
||||
*
|
||||
* This function returns the appropriate postfix (TiB, GiB, MiB, KiB, or B) for a given size in bytes.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to determine the postfix for.
|
||||
*
|
||||
* @returns The postfix as a string.
|
||||
*/
|
||||
export const getPostfix = (sizeInBytes: number): Postfix => {
|
||||
if (sizeInBytes >= 1024 ** 4) return 'TiB';
|
||||
if (sizeInBytes >= 1024 ** 3) return 'GiB';
|
||||
if (sizeInBytes >= 1024 ** 2) return 'MiB';
|
||||
if (sizeInBytes >= 1024 ** 1) return 'KiB';
|
||||
|
||||
return 'B';
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a resource label based on the label type and resource data.
|
||||
*
|
||||
* This function generates a resource label string based on the provided label type, resource data, and rounding option.
|
||||
* It formats the used, total, and free resource values and calculates the percentage if needed.
|
||||
*
|
||||
* @param lblType The type of label to render (used/total, used, free, or percentage).
|
||||
* @param rmUsg An object containing the resource usage data (used, total, percentage, and free).
|
||||
* @param round A boolean indicating whether to round the values.
|
||||
*
|
||||
* @returns The rendered resource label as a string.
|
||||
*/
|
||||
export const renderResourceLabel = (lblType: ResourceLabelType, rmUsg: GenericResourceData, round: boolean): string => {
|
||||
const { used, total, percentage, free } = rmUsg;
|
||||
|
||||
const formatFunctions = {
|
||||
TiB: formatSizeInTiB,
|
||||
GiB: formatSizeInGiB,
|
||||
MiB: formatSizeInMiB,
|
||||
KiB: formatSizeInKiB,
|
||||
B: (size: number): number => size,
|
||||
};
|
||||
|
||||
// Get the data in proper GiB, MiB, KiB, TiB, or bytes
|
||||
const totalSizeFormatted = autoFormatSize(total, round);
|
||||
// get the postfix: one of [TiB, GiB, MiB, KiB, B]
|
||||
const postfix = getPostfix(total);
|
||||
|
||||
// Determine which format function to use
|
||||
const formatUsed = formatFunctions[postfix] || formatFunctions['B'];
|
||||
const usedSizeFormatted = formatUsed(used, round);
|
||||
|
||||
if (lblType === 'used/total') {
|
||||
return `${usedSizeFormatted}/${totalSizeFormatted} ${postfix}`;
|
||||
}
|
||||
if (lblType === 'used') {
|
||||
return `${autoFormatSize(used, round)} ${getPostfix(used)}`;
|
||||
}
|
||||
if (lblType === 'free') {
|
||||
return `${autoFormatSize(free, round)} ${getPostfix(free)}`;
|
||||
}
|
||||
|
||||
return `${percentage}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a tooltip based on the data type and label type.
|
||||
*
|
||||
* This function generates a tooltip string based on the provided data type and label type.
|
||||
*
|
||||
* @param dataType The type of data to include in the tooltip.
|
||||
* @param lblTyp The type of label to format the tooltip for (used, free, used/total, or percentage).
|
||||
*
|
||||
* @returns The formatted tooltip as a string.
|
||||
*/
|
||||
export const formatTooltip = (dataType: string, lblTyp: ResourceLabelType): string => {
|
||||
switch (lblTyp) {
|
||||
case 'used':
|
||||
return `Used ${dataType}`;
|
||||
case 'free':
|
||||
return `Free ${dataType}`;
|
||||
case 'used/total':
|
||||
return `Used/Total ${dataType}`;
|
||||
case 'percentage':
|
||||
return `Percentage ${dataType} Usage`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
59
src/components/bar/utils/menu.ts
Normal file
59
src/components/bar/utils/menu.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { App, Gdk } from 'astal/gtk3';
|
||||
import { GtkWidget } from 'src/lib/types/widget';
|
||||
import { calculateMenuPosition } from 'src/components/menus/shared/dropdown/locationHandler';
|
||||
|
||||
export const closeAllMenus = (): void => {
|
||||
const menuWindows = App.get_windows()
|
||||
.filter((w) => {
|
||||
if (w.name) {
|
||||
return /.*menu/.test(w.name);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((window) => window.name);
|
||||
|
||||
menuWindows.forEach((window) => {
|
||||
if (window) {
|
||||
App.get_window(window)?.set_visible(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const openMenu = async (clicked: GtkWidget, event: Gdk.Event, window: string): Promise<void> => {
|
||||
/*
|
||||
* NOTE: We have to make some adjustments so the menu pops up relatively
|
||||
* to the center of the button clicked. We don't want the menu to spawn
|
||||
* offcenter depending on which edge of the button you click on.
|
||||
* -------------
|
||||
* To fix this, we take the x coordinate of the click within the button's bounds.
|
||||
* If you click the left edge of a 100 width button, then the x axis will be 0
|
||||
* and if you click the right edge then the x axis will be 100.
|
||||
* -------------
|
||||
* Then we divide the width of the button by 2 to get the center of the button and then get
|
||||
* the offset by subtracting the clicked x coordinate. Then we can apply that offset
|
||||
* to the x coordinate of the click relative to the screen to get the center of the
|
||||
* icon click.
|
||||
*/
|
||||
|
||||
try {
|
||||
const middleOfButton = Math.floor(clicked.get_allocated_width() / 2);
|
||||
const xAxisOfButtonClick = clicked.get_pointer()[0];
|
||||
const middleOffset = middleOfButton - xAxisOfButtonClick;
|
||||
|
||||
const clickPos = event.get_root_coords();
|
||||
const adjustedXCoord = clickPos[1] + middleOffset;
|
||||
const coords = [adjustedXCoord, clickPos[2]];
|
||||
|
||||
await calculateMenuPosition(coords, window);
|
||||
|
||||
closeAllMenus();
|
||||
App.toggle_window(window);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`Error calculating menu position: ${error.stack}`);
|
||||
} else {
|
||||
console.error(`Unknown error occurred: ${error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
169
src/components/bar/utils/monitors.ts
Normal file
169
src/components/bar/utils/monitors.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { hyprlandService } from 'src/lib/constants/services';
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
import { BarLayout, BarLayouts } from 'src/lib/types/options';
|
||||
|
||||
type GdkMonitors = {
|
||||
[key: string]: {
|
||||
key: string;
|
||||
model: string;
|
||||
used: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const getLayoutForMonitor = (monitor: number, layouts: BarLayouts): BarLayout => {
|
||||
const matchingKey = Object.keys(layouts).find((key) => key === monitor.toString());
|
||||
const wildcard = Object.keys(layouts).find((key) => key === '*');
|
||||
|
||||
if (matchingKey) {
|
||||
return layouts[matchingKey];
|
||||
}
|
||||
|
||||
if (wildcard) {
|
||||
return layouts[wildcard];
|
||||
}
|
||||
|
||||
return {
|
||||
left: ['dashboard', 'workspaces', 'windowtitle'],
|
||||
middle: ['media'],
|
||||
right: ['volume', 'network', 'bluetooth', 'battery', 'systray', 'clock', 'notifications'],
|
||||
};
|
||||
};
|
||||
|
||||
export const isLayoutEmpty = (layout: BarLayout): boolean => {
|
||||
const isLeftSectionEmpty = !Array.isArray(layout.left) || layout.left.length === 0;
|
||||
const isRightSectionEmpty = !Array.isArray(layout.right) || layout.right.length === 0;
|
||||
const isMiddleSectionEmpty = !Array.isArray(layout.middle) || layout.middle.length === 0;
|
||||
|
||||
return isLeftSectionEmpty && isRightSectionEmpty && isMiddleSectionEmpty;
|
||||
};
|
||||
|
||||
export function getGdkMonitors(): GdkMonitors {
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
if (display === null) {
|
||||
console.error('Failed to get Gdk display.');
|
||||
return {};
|
||||
}
|
||||
|
||||
const numGdkMonitors = display.get_n_monitors();
|
||||
const gdkMonitors: GdkMonitors = {};
|
||||
|
||||
for (let i = 0; i < numGdkMonitors; i++) {
|
||||
const curMonitor = display.get_monitor(i);
|
||||
|
||||
if (curMonitor === null) {
|
||||
console.warn(`Monitor at index ${i} is null.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const model = curMonitor.get_model() || '';
|
||||
const geometry = curMonitor.get_geometry();
|
||||
const scaleFactor = curMonitor.get_scale_factor();
|
||||
|
||||
const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`;
|
||||
gdkMonitors[i] = { key, model, used: false };
|
||||
}
|
||||
|
||||
return gdkMonitors;
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Some more funky stuff being done by GDK.
|
||||
* We render windows/bar based on the monitor ID. So if you have 3 monitors, then your
|
||||
* monitor IDs will be [0, 1, 2]. Hyprland will NEVER change what ID belongs to what monitor.
|
||||
*
|
||||
* So if hyprland determines id 0 = DP-1, even after you unplug, shut off or restart your monitor,
|
||||
* the id 0 will ALWAYS be DP-1.
|
||||
*
|
||||
* However, GDK (the righteous genius that it is) will change the order of ID anytime your monitor
|
||||
* setup is changed. So if you unplug your monitor and plug it back it, it now becomes the last id.
|
||||
* So if DP-1 was id 0 and you unplugged it, it will reconfigure to id 2. This sucks because now
|
||||
* there's a mismtach between what GDK determines the monitor is at id 2 and what Hyprland determines
|
||||
* is at id 2.
|
||||
*
|
||||
* So for that reason, we need to redirect the input `monitor` that the Bar module takes in, to the
|
||||
* proper Hyprland monitor. So when monitor id 0 comes in, we need to find what the id of that monitor
|
||||
* is being determined as by Hyprland so the bars show up on the right monitors.
|
||||
*
|
||||
* Since GTK3 doesn't contain connection names and only monitor models, we have to make the best guess
|
||||
* in the case that there are multiple models in the same resolution with the same scale. We find the
|
||||
* 'right' monitor by checking if the model matches along with the resolution and scale. If monitor at
|
||||
* ID 0 for GDK is being reported as 'MSI MAG271CQR' we find the same model in the Hyprland monitor list
|
||||
* and check if the resolution and scaling is the same... if it is then we determine it's a match.
|
||||
*
|
||||
* The edge-case that we just can't handle is if you have the same monitors in the same resolution at the same
|
||||
* scale. So if you've got 2 'MSI MAG271CQR' monitors at 2560x1440 at scale 1, then we just match the first
|
||||
* monitor in the list as the first match and then the second 'MSI MAG271CQR' as a match in the 2nd iteration.
|
||||
* You may have the bar showing up on the wrong one in this case because we don't know what the connector id
|
||||
* is of either of these monitors (DP-1, DP-2) which are unique values - as these are only in GTK4.
|
||||
*
|
||||
* Keep in mind though, this is ONLY an issue if you change your monitor setup by plugging in a new one, restarting
|
||||
* an existing one or shutting it off.
|
||||
*
|
||||
* If your monitors aren't changed in the current session you're in then none of this safeguarding is relevant.
|
||||
*
|
||||
* Fun stuff really... :facepalm:
|
||||
*/
|
||||
|
||||
export const gdkMonitorIdToHyprlandId = (monitor: number, usedHyprlandMonitors: Set<number>): number => {
|
||||
const gdkMonitors = getGdkMonitors();
|
||||
|
||||
if (Object.keys(gdkMonitors).length === 0) {
|
||||
return monitor;
|
||||
}
|
||||
|
||||
// Get the GDK monitor for the given monitor index
|
||||
const gdkMonitor = gdkMonitors[monitor];
|
||||
|
||||
// First pass: Strict matching including the monitor index (i.e., hypMon.id === monitor + resolution+scale criteria)
|
||||
const directMatch = hyprlandService.get_monitors().find((hypMon) => {
|
||||
const isVertical = hypMon?.transform !== undefined ? hypMon.transform % 2 !== 0 : false;
|
||||
|
||||
const width = isVertical ? hypMon.height : hypMon.width;
|
||||
const height = isVertical ? hypMon.width : hypMon.height;
|
||||
|
||||
const hyprlandKey = `${hypMon.model}_${width}x${height}_${hypMon.scale}`;
|
||||
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id) && hypMon.id === monitor;
|
||||
});
|
||||
|
||||
if (directMatch) {
|
||||
usedHyprlandMonitors.add(directMatch.id);
|
||||
return directMatch.id;
|
||||
}
|
||||
|
||||
// Second pass: Relaxed matching without considering the monitor index
|
||||
const hyprlandMonitor = hyprlandService.get_monitors().find((hypMon) => {
|
||||
const isVertical = hypMon?.transform !== undefined ? hypMon.transform % 2 !== 0 : false;
|
||||
|
||||
const width = isVertical ? hypMon.height : hypMon.width;
|
||||
const height = isVertical ? hypMon.width : hypMon.height;
|
||||
|
||||
const hyprlandKey = `${hypMon.model}_${width}x${height}_${hypMon.scale}`;
|
||||
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id);
|
||||
});
|
||||
|
||||
if (hyprlandMonitor) {
|
||||
usedHyprlandMonitors.add(hyprlandMonitor.id);
|
||||
return hyprlandMonitor.id;
|
||||
}
|
||||
|
||||
// Fallback: Find the first available monitor ID that hasn't been used
|
||||
const fallbackMonitor = hyprlandService.get_monitors().find((hypMon) => !usedHyprlandMonitors.has(hypMon.id));
|
||||
|
||||
if (fallbackMonitor) {
|
||||
usedHyprlandMonitors.add(fallbackMonitor.id);
|
||||
return fallbackMonitor.id;
|
||||
}
|
||||
|
||||
// Ensure we return a valid monitor ID that actually exists
|
||||
for (let i = 0; i < hyprlandService.get_monitors().length; i++) {
|
||||
if (!usedHyprlandMonitors.has(i)) {
|
||||
usedHyprlandMonitors.add(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// As a last resort, return the original monitor index if no unique monitor can be found
|
||||
console.warn(`Returning original monitor index as a last resort: ${monitor}`);
|
||||
return monitor;
|
||||
};
|
||||
29
src/components/bar/utils/sideEffects.ts
Normal file
29
src/components/bar/utils/sideEffects.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import options from '../../../options';
|
||||
|
||||
const { showIcon, showTime } = options.bar.clock;
|
||||
|
||||
showIcon.subscribe(() => {
|
||||
if (!showTime.get() && !showIcon.get()) {
|
||||
showTime.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
showTime.subscribe(() => {
|
||||
if (!showTime.get() && !showIcon.get()) {
|
||||
showIcon.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
const { label, icon } = options.bar.windowtitle;
|
||||
|
||||
label.subscribe(() => {
|
||||
if (!label.get() && !icon.get()) {
|
||||
icon.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
icon.subscribe(() => {
|
||||
if (!label.get() && !icon.get()) {
|
||||
label.set(true);
|
||||
}
|
||||
});
|
||||
40
src/components/menus/audio/active/device/Slider.tsx
Normal file
40
src/components/menus/audio/active/device/Slider.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { bind } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import options from 'src/options';
|
||||
|
||||
const { raiseMaximumVolume } = options.menus.volume;
|
||||
|
||||
export const Slider = ({ device, type }: SliderProps): JSX.Element => {
|
||||
return (
|
||||
<box vertical>
|
||||
<label
|
||||
className={`menu-active ${type}`}
|
||||
halign={Gtk.Align.START}
|
||||
truncate
|
||||
expand
|
||||
wrap
|
||||
label={bind(device, 'description').as((description) => description ?? `Unknown ${type} Device`)}
|
||||
/>
|
||||
<slider
|
||||
value={bind(device, 'volume')}
|
||||
className={`menu-active-slider menu-slider ${type}`}
|
||||
drawValue={false}
|
||||
hexpand
|
||||
min={0}
|
||||
max={type === 'playback' ? bind(raiseMaximumVolume).as((raise) => (raise ? 1.5 : 1)) : 1}
|
||||
onDragged={({ value, dragging }) => {
|
||||
if (dragging) {
|
||||
device.volume = value;
|
||||
device.mute = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface SliderProps {
|
||||
device: AstalWp.Endpoint;
|
||||
type: 'playback' | 'input';
|
||||
}
|
||||
39
src/components/menus/audio/active/device/SliderIcon.tsx
Normal file
39
src/components/menus/audio/active/device/SliderIcon.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import { getIcon } from '../../utils';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
|
||||
export const SliderIcon = ({ type, device }: SliderIconProps): JSX.Element => {
|
||||
const iconBinding = Variable.derive([bind(device, 'volume'), bind(device, 'mute')], (volume, isMuted) => {
|
||||
const iconType = type === 'playback' ? 'spkr' : 'mic';
|
||||
|
||||
const effectiveVolume = volume > 0 ? volume : 100;
|
||||
const mutedState = volume > 0 ? isMuted : true;
|
||||
|
||||
return getIcon(effectiveVolume, mutedState)[iconType];
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
className={bind(device, 'mute').as((isMuted) => `menu-active-button ${type} ${isMuted ? 'muted' : ''}`)}
|
||||
vexpand={false}
|
||||
valign={Gtk.Align.END}
|
||||
onClick={(_, event) => {
|
||||
if (isPrimaryClick(event)) {
|
||||
device.mute = !device.mute;
|
||||
}
|
||||
}}
|
||||
onDestroy={() => {
|
||||
iconBinding.drop();
|
||||
}}
|
||||
>
|
||||
<icon className={`menu-active-icon ${type}`} icon={iconBinding()} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface SliderIconProps {
|
||||
type: 'playback' | 'input';
|
||||
device: AstalWp.Endpoint;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { bind } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
|
||||
export const SliderPercentage = ({ type, device }: SliderPercentageProps): JSX.Element => {
|
||||
return (
|
||||
<label
|
||||
className={`menu-active-percentage ${type}`}
|
||||
valign={Gtk.Align.END}
|
||||
label={bind(device, 'volume').as((vol) => `${Math.round(vol * 100)}%`)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface SliderPercentageProps {
|
||||
type: 'playback' | 'input';
|
||||
device: AstalWp.Endpoint;
|
||||
}
|
||||
21
src/components/menus/audio/active/device/index.tsx
Normal file
21
src/components/menus/audio/active/device/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import { SliderIcon } from './SliderIcon';
|
||||
import { Slider } from './Slider';
|
||||
import { SliderPercentage } from './SliderPercentage';
|
||||
|
||||
export const ActiveDevice = ({ type, device }: ActiveDeviceProps): JSX.Element => {
|
||||
return (
|
||||
<box className={`menu-active-container ${type}`} vertical>
|
||||
<box className={`menu-slider-container ${type}`}>
|
||||
<SliderIcon type={type} device={device} />
|
||||
<Slider type={type} device={device} />
|
||||
<SliderPercentage type={type} device={device} />
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ActiveDeviceProps {
|
||||
type: 'playback' | 'input';
|
||||
device: AstalWp.Endpoint;
|
||||
}
|
||||
34
src/components/menus/audio/active/index.tsx
Normal file
34
src/components/menus/audio/active/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { ActiveDevice } from './device/index.js';
|
||||
import { audioService } from 'src/lib/constants/services.js';
|
||||
import { BindableChild } from 'astal/gtk3/astalify.js';
|
||||
|
||||
export const SelectedDevices = (): JSX.Element => {
|
||||
const Header = (): JSX.Element => (
|
||||
<box className={'menu-label-container volume selected'} halign={Gtk.Align.FILL}>
|
||||
<label className={'menu-label audio volume'} halign={Gtk.Align.START} hexpand label={'Volume'} />
|
||||
</box>
|
||||
);
|
||||
|
||||
const ActiveDeviceContainer = ({ children }: ActiveDeviceContainerProps): JSX.Element => {
|
||||
return (
|
||||
<box className={'menu-items-section selected'} vertical>
|
||||
{children}
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<box className={'menu-section-container volume'} vertical>
|
||||
<Header />
|
||||
<ActiveDeviceContainer>
|
||||
<ActiveDevice type={'playback'} device={audioService.defaultSpeaker} />
|
||||
<ActiveDevice type={'input'} device={audioService.defaultMicrophone} />
|
||||
</ActiveDeviceContainer>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ActiveDeviceContainerProps {
|
||||
children?: BindableChild | BindableChild[];
|
||||
}
|
||||
52
src/components/menus/audio/available/Device.tsx
Normal file
52
src/components/menus/audio/available/Device.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import { bind } from 'astal';
|
||||
|
||||
const DeviceIcon = ({ device, type, icon }: AudioDeviceProps): JSX.Element => {
|
||||
return (
|
||||
<label
|
||||
className={bind(device, 'isDefault').as((isDefault) => {
|
||||
return `menu-button-icon ${isDefault ? 'active' : ''} ${type} txt-icon`;
|
||||
})}
|
||||
label={icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const DeviceName = ({ device, type }: Omit<AudioDeviceProps, 'icon'>): JSX.Element => {
|
||||
return (
|
||||
<label
|
||||
truncate
|
||||
wrap
|
||||
className={bind(device, 'description').as((currentDesc) =>
|
||||
device.description === currentDesc ? `menu-button-name active ${type}` : `menu-button-name ${type}`,
|
||||
)}
|
||||
label={device.description}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AudioDevice = ({ device, type, icon }: AudioDeviceProps): JSX.Element => {
|
||||
return (
|
||||
<button
|
||||
className={`menu-button audio ${type} ${device.id}`}
|
||||
onClick={(_, event) => {
|
||||
if (isPrimaryClick(event)) {
|
||||
device.set_is_default(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<box halign={Gtk.Align.START}>
|
||||
<DeviceIcon device={device} type={type} icon={icon} />
|
||||
<DeviceName device={device} type={type} />
|
||||
</box>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface AudioDeviceProps {
|
||||
device: AstalWp.Endpoint;
|
||||
type: 'playback' | 'input';
|
||||
icon: string;
|
||||
}
|
||||
14
src/components/menus/audio/available/Header.tsx
Normal file
14
src/components/menus/audio/available/Header.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
|
||||
export const Header = ({ type, label }: HeaderProps): JSX.Element => {
|
||||
return (
|
||||
<box className={`menu-label-container ${type}`} halign={Gtk.Align.FILL}>
|
||||
<label className={`menu-label audio ${type}`} halign={Gtk.Align.START} hexpand label={label} />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface HeaderProps {
|
||||
type: 'playback' | 'input';
|
||||
label: string;
|
||||
}
|
||||
24
src/components/menus/audio/available/InputDevices.tsx
Normal file
24
src/components/menus/audio/available/InputDevices.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { audioService } from 'src/lib/constants/services.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import { AudioDevice } from './Device';
|
||||
import { NotFoundButton } from './NotFoundButton';
|
||||
|
||||
export const InputDevices = (): JSX.Element => {
|
||||
const inputDevices = bind(audioService, 'microphones');
|
||||
|
||||
return (
|
||||
<box className={'menu-items-section input'} vertical>
|
||||
<box className={'menu-container input'} vertical>
|
||||
{inputDevices.as((devices) => {
|
||||
if (!devices || devices.length === 0) {
|
||||
return <NotFoundButton type={'input'} />;
|
||||
}
|
||||
|
||||
return devices.map((device) => {
|
||||
return <AudioDevice device={device} type={'input'} icon={''} />;
|
||||
});
|
||||
})}
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
13
src/components/menus/audio/available/NotFoundButton.tsx
Normal file
13
src/components/menus/audio/available/NotFoundButton.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
|
||||
export const NotFoundButton = ({ type }: { type: string }): JSX.Element => {
|
||||
return (
|
||||
<button className={`menu-unfound-button ${type}`} sensitive={false}>
|
||||
<box>
|
||||
<box halign={Gtk.Align.START}>
|
||||
<label className={`menu-button-name ${type}`} label={`No ${type} devices found...`} />
|
||||
</box>
|
||||
</box>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
24
src/components/menus/audio/available/PlaybackDevices.tsx
Normal file
24
src/components/menus/audio/available/PlaybackDevices.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { audioService } from 'src/lib/constants/services.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import { AudioDevice } from './Device';
|
||||
import { NotFoundButton } from './NotFoundButton';
|
||||
|
||||
export const PlaybackDevices = (): JSX.Element => {
|
||||
const playbackDevices = bind(audioService, 'speakers');
|
||||
|
||||
return (
|
||||
<box className={'menu-items-section playback'} vertical>
|
||||
<box className={'menu-container playback'} vertical>
|
||||
{playbackDevices.as((devices) => {
|
||||
if (!devices || devices.length === 0) {
|
||||
return <NotFoundButton type={'playback'} />;
|
||||
}
|
||||
|
||||
return devices.map((device) => {
|
||||
return <AudioDevice device={device} type={'playback'} icon={''} />;
|
||||
});
|
||||
})}
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
15
src/components/menus/audio/available/index.tsx
Normal file
15
src/components/menus/audio/available/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { PlaybackDevices } from './PlaybackDevices.js';
|
||||
import { InputDevices } from './InputDevices.js';
|
||||
import { Header } from './Header.js';
|
||||
|
||||
export const AvailableDevices = (): JSX.Element => {
|
||||
return (
|
||||
<box vertical className={'menu-section-container playback'}>
|
||||
<Header type={'playback'} label={'Playback Device'} />
|
||||
<PlaybackDevices />
|
||||
|
||||
<Header type={'input'} label={'Input Device'} />
|
||||
<InputDevices />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
23
src/components/menus/audio/index.tsx
Normal file
23
src/components/menus/audio/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { SelectedDevices } from './active/index.js';
|
||||
import options from 'src/options.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { AvailableDevices } from './available/index.js';
|
||||
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
|
||||
|
||||
export default (): JSX.Element => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
name="audiomenu"
|
||||
transition={bind(options.menus.transition).as((transition) => RevealerTransitionMap[transition])}
|
||||
>
|
||||
<box className={'menu-items audio'} halign={Gtk.Align.FILL} hexpand>
|
||||
<box className={'menu-items-container audio'} halign={Gtk.Align.FILL} vertical hexpand>
|
||||
<SelectedDevices />
|
||||
<AvailableDevices />
|
||||
</box>
|
||||
</box>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
40
src/components/menus/audio/utils.ts
Normal file
40
src/components/menus/audio/utils.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
const speakerIcons = {
|
||||
101: 'audio-volume-overamplified-symbolic',
|
||||
66: 'audio-volume-high-symbolic',
|
||||
34: 'audio-volume-medium-symbolic',
|
||||
1: 'audio-volume-low-symbolic',
|
||||
0: 'audio-volume-muted-symbolic',
|
||||
} as const;
|
||||
|
||||
const inputIcons = {
|
||||
101: 'microphone-sensitivity-high-symbolic',
|
||||
66: 'microphone-sensitivity-high-symbolic',
|
||||
34: 'microphone-sensitivity-medium-symbolic',
|
||||
1: 'microphone-sensitivity-low-symbolic',
|
||||
0: 'microphone-disabled-symbolic',
|
||||
};
|
||||
|
||||
type IconVolumes = keyof typeof speakerIcons;
|
||||
|
||||
/**
|
||||
* Retrieves the appropriate speaker and microphone icons based on the audio volume and mute status.
|
||||
*
|
||||
* This function determines the correct icons for both the speaker and microphone based on the provided audio volume and mute status.
|
||||
* It uses predefined thresholds to select the appropriate icons from the `speakerIcons` and `inputIcons` objects.
|
||||
*
|
||||
* @param audioVol The current audio volume as a number between 0 and 1.
|
||||
* @param isMuted A boolean indicating whether the audio is muted.
|
||||
*
|
||||
* @returns An object containing the speaker and microphone icons.
|
||||
*/
|
||||
const getIcon = (audioVol: number, isMuted: boolean): Record<string, string> => {
|
||||
const thresholds: IconVolumes[] = [101, 66, 34, 1, 0];
|
||||
const icon = isMuted ? 0 : thresholds.find((threshold) => threshold <= audioVol * 100) || 0;
|
||||
|
||||
return {
|
||||
spkr: speakerIcons[icon],
|
||||
mic: inputIcons[icon],
|
||||
};
|
||||
};
|
||||
|
||||
export { getIcon };
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
|
||||
export const BluetoothDisabled = (): JSX.Element => {
|
||||
return (
|
||||
<box className={'bluetooth-items'} vertical expand valign={Gtk.Align.CENTER} halign={Gtk.Align.CENTER}>
|
||||
<label className={'bluetooth-disabled dim'} hexpand label={'Bluetooth is disabled'} />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
17
src/components/menus/bluetooth/devices/DeviceListItem.tsx
Normal file
17
src/components/menus/bluetooth/devices/DeviceListItem.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { DeviceControls } from './controls';
|
||||
import { BluetoothDevice } from './device';
|
||||
|
||||
export const DeviceListItem = ({ btDevice, connectedDevices }: DeviceListItemProps): JSX.Element => {
|
||||
return (
|
||||
<box>
|
||||
<BluetoothDevice device={btDevice} connectedDevices={connectedDevices} />
|
||||
<DeviceControls device={btDevice} connectedDevices={connectedDevices} />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeviceListItemProps {
|
||||
btDevice: AstalBluetooth.Device;
|
||||
connectedDevices: string[];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
|
||||
export const NoBluetoothDevices = (): JSX.Element => {
|
||||
return (
|
||||
<box className={'bluetooth-items'} vertical expand valign={Gtk.Align.CENTER} halign={Gtk.Align.CENTER}>
|
||||
<label className={'no-bluetooth-devices dim'} hexpand label={'No devices currently found'} />
|
||||
<label className={'search-bluetooth-label dim'} hexpand label={"Press '' to search"} />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Binding } from 'astal';
|
||||
import { ButtonProps } from 'astal/gtk3/widget';
|
||||
|
||||
export const ActionButton = ({ name = '', tooltipText = '', label = '', ...props }: ActionButtonProps): JSX.Element => {
|
||||
return (
|
||||
<button className={`menu-icon-button ${name} bluetooth`} {...props}>
|
||||
<label
|
||||
className={`menu-icon-button-label ${name} bluetooth txt-icon`}
|
||||
tooltipText={tooltipText}
|
||||
label={label}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface ActionButtonProps extends ButtonProps {
|
||||
name: string;
|
||||
tooltipText: string | Binding<string>;
|
||||
label: string | Binding<string>;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { bind } from 'astal';
|
||||
import { ActionButton } from './ActionButton';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
|
||||
export const ConnectButton = ({ device }: ConnectButtonProps): JSX.Element => {
|
||||
return (
|
||||
<ActionButton
|
||||
name={'disconnect'}
|
||||
tooltipText={bind(device, 'connected').as((connected) => (connected ? 'Disconnect' : 'Connect'))}
|
||||
label={bind(device, 'connected').as((connected) => (connected ? '' : ''))}
|
||||
onClick={(_, self) => {
|
||||
if (isPrimaryClick(self) && device.connected) {
|
||||
device.disconnect_device((res) => {
|
||||
console.info(res);
|
||||
});
|
||||
} else {
|
||||
device.connect_device((res) => {
|
||||
console.info(res);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ConnectButtonProps {
|
||||
device: AstalBluetooth.Device;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ActionButton } from './ActionButton';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { forgetBluetoothDevice } from '../helpers';
|
||||
|
||||
export const ForgetButton = ({ device }: ForgetButtonProps): JSX.Element => {
|
||||
return (
|
||||
<ActionButton
|
||||
name={'delete'}
|
||||
tooltipText={'Forget'}
|
||||
label={''}
|
||||
onClick={(_, self) => {
|
||||
if (isPrimaryClick(self)) {
|
||||
forgetBluetoothDevice(device);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ForgetButtonProps {
|
||||
device: AstalBluetooth.Device;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { bind } from 'astal';
|
||||
import { ActionButton } from './ActionButton';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
|
||||
export const PairButton = ({ device }: PairButtonProps): JSX.Element => {
|
||||
return (
|
||||
<ActionButton
|
||||
name={'unpair'}
|
||||
tooltipText={bind(device, 'paired').as((paired) => (paired ? 'Unpair' : 'Pair'))}
|
||||
label={bind(device, 'paired').as((paired) => (paired ? '' : ''))}
|
||||
onClick={(_, self) => {
|
||||
if (!isPrimaryClick(self)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.paired) {
|
||||
device.pair();
|
||||
} else {
|
||||
device.cancel_pairing();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface PairButtonProps {
|
||||
device: AstalBluetooth.Device;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { bind } from 'astal';
|
||||
import { ActionButton } from './ActionButton';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
|
||||
export const TrustButton = ({ device }: TrustButtonProps): JSX.Element => {
|
||||
return (
|
||||
<ActionButton
|
||||
name={'untrust'}
|
||||
tooltipText={bind(device, 'trusted').as((trusted) => (trusted ? 'Untrust' : 'Trust'))}
|
||||
label={bind(device, 'trusted').as((trusted) => (trusted ? '' : ''))}
|
||||
onClick={(_, self) => {
|
||||
if (isPrimaryClick(self)) {
|
||||
device.set_trusted(!device.trusted);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface TrustButtonProps {
|
||||
device: AstalBluetooth.Device;
|
||||
}
|
||||
26
src/components/menus/bluetooth/devices/controls/index.tsx
Normal file
26
src/components/menus/bluetooth/devices/controls/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { PairButton } from './PairButton';
|
||||
import { ConnectButton } from './ConnectButton';
|
||||
import { TrustButton } from './TrustButton';
|
||||
import { ForgetButton } from './ForgetButton';
|
||||
|
||||
export const DeviceControls = ({ device, connectedDevices }: DeviceControlsProps): JSX.Element => {
|
||||
if (!connectedDevices.includes(device.address)) {
|
||||
return <box />;
|
||||
}
|
||||
|
||||
return (
|
||||
<box valign={Gtk.Align.START} className={'bluetooth-controls'}>
|
||||
<PairButton device={device} />
|
||||
<ConnectButton device={device} />
|
||||
<TrustButton device={device} />
|
||||
<ForgetButton device={device} />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeviceControlsProps {
|
||||
device: AstalBluetooth.Device;
|
||||
connectedDevices: string[];
|
||||
}
|
||||
22
src/components/menus/bluetooth/devices/device/DeviceIcon.tsx
Normal file
22
src/components/menus/bluetooth/devices/device/DeviceIcon.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { bind } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { getBluetoothIcon } from '../../utils';
|
||||
|
||||
export const DeviceIcon = ({ device, connectedDevices }: DeviceIconProps): JSX.Element => {
|
||||
return (
|
||||
<label
|
||||
valign={Gtk.Align.START}
|
||||
className={bind(device, 'address').as(
|
||||
(address) =>
|
||||
`menu-button-icon bluetooth ${connectedDevices.includes(address) ? 'active' : ''} txt-icon`,
|
||||
)}
|
||||
label={bind(device, 'icon').as((icon) => getBluetoothIcon(`${icon}-symbolic`))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeviceIconProps {
|
||||
device: AstalBluetooth.Device;
|
||||
connectedDevices: string[];
|
||||
}
|
||||
20
src/components/menus/bluetooth/devices/device/DeviceName.tsx
Normal file
20
src/components/menus/bluetooth/devices/device/DeviceName.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { bind } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
|
||||
export const DeviceName = ({ device }: DeviceNameProps): JSX.Element => {
|
||||
return (
|
||||
<label
|
||||
valign={Gtk.Align.CENTER}
|
||||
halign={Gtk.Align.START}
|
||||
className="menu-button-name bluetooth"
|
||||
truncate
|
||||
wrap
|
||||
label={bind(device, 'alias')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeviceNameProps {
|
||||
device: AstalBluetooth.Device;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
|
||||
export const DeviceStatus = ({ device }: DeviceStatusProps): JSX.Element => {
|
||||
const revealerBinding = Variable.derive(
|
||||
[bind(device, 'connected'), bind(device, 'paired')],
|
||||
(connected, paired) => {
|
||||
return connected || paired;
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<revealer
|
||||
halign={Gtk.Align.START}
|
||||
revealChild={revealerBinding()}
|
||||
onDestroy={() => {
|
||||
revealerBinding.drop();
|
||||
}}
|
||||
>
|
||||
<label
|
||||
halign={Gtk.Align.START}
|
||||
className={'connection-status dim'}
|
||||
label={bind(device, 'connected').as((connected) => (connected ? 'Connected' : 'Paired'))}
|
||||
/>
|
||||
</revealer>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeviceStatusProps {
|
||||
device: AstalBluetooth.Device;
|
||||
}
|
||||
50
src/components/menus/bluetooth/devices/device/index.tsx
Normal file
50
src/components/menus/bluetooth/devices/device/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import Spinner from 'src/components/shared/Spinner';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import { bind } from 'astal';
|
||||
import { DeviceIcon } from './DeviceIcon';
|
||||
import { DeviceName } from './DeviceName';
|
||||
import { DeviceStatus } from './DeviceStatus';
|
||||
|
||||
export const BluetoothDevice = ({ device, connectedDevices }: BluetoothDeviceProps): JSX.Element => {
|
||||
const IsConnectingSpinner = (): JSX.Element => {
|
||||
return (
|
||||
<revealer revealChild={bind(device, 'connecting')}>
|
||||
<Spinner valign={Gtk.Align.START} className="spinner bluetooth" />
|
||||
</revealer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
hexpand
|
||||
className={`bluetooth-element-item ${device}`}
|
||||
onClick={(_, event) => {
|
||||
if (!connectedDevices.includes(device.address) && isPrimaryClick(event)) {
|
||||
device.connect_device((res) => {
|
||||
console.info(res);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<box>
|
||||
<box hexpand halign={Gtk.Align.START} className="menu-button-container">
|
||||
<DeviceIcon device={device} connectedDevices={connectedDevices} />
|
||||
<box vertical valign={Gtk.Align.CENTER}>
|
||||
<DeviceName device={device} />
|
||||
<DeviceStatus device={device} />
|
||||
</box>
|
||||
</box>
|
||||
<box halign={Gtk.Align.END}>
|
||||
<IsConnectingSpinner />
|
||||
</box>
|
||||
</box>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface BluetoothDeviceProps {
|
||||
device: AstalBluetooth.Device;
|
||||
connectedDevices: string[];
|
||||
}
|
||||
60
src/components/menus/bluetooth/devices/helpers.ts
Normal file
60
src/components/menus/bluetooth/devices/helpers.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { execAsync } from 'astal';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { bluetoothService } from 'src/lib/constants/services';
|
||||
|
||||
/**
|
||||
* Retrieves the list of available Bluetooth devices.
|
||||
*
|
||||
* This function filters and sorts the list of Bluetooth devices from the `bluetoothService`.
|
||||
* It excludes devices with a null name and sorts the devices based on their connection and pairing status.
|
||||
*
|
||||
* @returns An array of available Bluetooth devices.
|
||||
*/
|
||||
export const getAvailableBluetoothDevices = (): AstalBluetooth.Device[] => {
|
||||
const availableDevices = bluetoothService.devices
|
||||
.filter((btDev) => btDev.name !== null)
|
||||
.sort((a, b) => {
|
||||
if (a.connected || a.paired) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.connected || b.paired) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return availableDevices;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the list of connected Bluetooth devices.
|
||||
*
|
||||
* This function filters the list of available Bluetooth devices to include only those that are connected or paired.
|
||||
* It returns an array of the addresses of the connected devices.
|
||||
*
|
||||
* @returns An array of addresses of connected Bluetooth devices.
|
||||
*/
|
||||
export const getConnectedBluetoothDevices = (): string[] => {
|
||||
const availableDevices = getAvailableBluetoothDevices();
|
||||
const connectedDeviceNames = availableDevices.filter((d) => d.connected || d.paired).map((d) => d.address);
|
||||
|
||||
return connectedDeviceNames;
|
||||
};
|
||||
|
||||
/**
|
||||
* Forgets a Bluetooth device.
|
||||
*
|
||||
* This function removes a Bluetooth device using the `bluetoothctl` command.
|
||||
* It executes the command asynchronously and emits a 'device-removed' event if the command is successful.
|
||||
*
|
||||
* @param device The Bluetooth device to forget.
|
||||
*/
|
||||
export const forgetBluetoothDevice = (device: AstalBluetooth.Device): void => {
|
||||
execAsync(['bash', '-c', `bluetoothctl remove ${device.address}`])
|
||||
.catch((err) => console.error('Bluetooth Remove', err))
|
||||
.then(() => {
|
||||
bluetoothService.emit('device-removed', device);
|
||||
});
|
||||
};
|
||||
41
src/components/menus/bluetooth/devices/index.tsx
Normal file
41
src/components/menus/bluetooth/devices/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import Variable from 'astal/variable.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import { bluetoothService } from 'src/lib/constants/services.js';
|
||||
import { getAvailableBluetoothDevices, getConnectedBluetoothDevices } from './helpers.js';
|
||||
import { NoBluetoothDevices } from './NoBluetoothDevices.js';
|
||||
import { BluetoothDisabled } from './BluetoothDisabled.js';
|
||||
import { DeviceListItem } from './DeviceListItem.js';
|
||||
|
||||
export const BluetoothDevices = (): JSX.Element => {
|
||||
const deviceListBinding = Variable.derive(
|
||||
[bind(bluetoothService, 'devices'), bind(bluetoothService, 'isPowered')],
|
||||
() => {
|
||||
const availableDevices = getAvailableBluetoothDevices();
|
||||
const connectedDevices = getConnectedBluetoothDevices();
|
||||
|
||||
if (availableDevices.length === 0) {
|
||||
return <NoBluetoothDevices />;
|
||||
}
|
||||
|
||||
if (!bluetoothService.adapter?.powered) {
|
||||
return <BluetoothDisabled />;
|
||||
}
|
||||
|
||||
return availableDevices.map((btDevice) => {
|
||||
return <DeviceListItem btDevice={btDevice} connectedDevices={connectedDevices} />;
|
||||
});
|
||||
},
|
||||
);
|
||||
return (
|
||||
<box
|
||||
className={'menu-items-section'}
|
||||
onDestroy={() => {
|
||||
deviceListBinding.drop();
|
||||
}}
|
||||
>
|
||||
<box className={'menu-content'} vertical>
|
||||
{deviceListBinding()}
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { bluetoothService } from 'src/lib/constants/services';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import { bind, timeout } from 'astal';
|
||||
import { isDiscovering } from './helper';
|
||||
|
||||
export const DiscoverButton = (): JSX.Element => (
|
||||
<button
|
||||
className="menu-icon-button search"
|
||||
valign={Gtk.Align.CENTER}
|
||||
onClick={(_, self) => {
|
||||
if (!isPrimaryClick(self)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothService.adapter?.discovering) {
|
||||
return bluetoothService.adapter.stop_discovery();
|
||||
}
|
||||
|
||||
bluetoothService.adapter?.start_discovery();
|
||||
|
||||
const discoveryTimeout = 12000;
|
||||
timeout(discoveryTimeout, () => {
|
||||
if (bluetoothService.adapter?.discovering) {
|
||||
bluetoothService.adapter.stop_discovery();
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<icon
|
||||
className={bind(isDiscovering).as((isDiscovering) => (isDiscovering ? 'spinning-icon' : ''))}
|
||||
icon="view-refresh-symbolic"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { bluetoothService } from 'src/lib/constants/services';
|
||||
|
||||
const isPowered = Variable(false);
|
||||
|
||||
Variable.derive([bind(bluetoothService, 'isPowered')], (isOn) => {
|
||||
return isPowered.set(isOn);
|
||||
});
|
||||
|
||||
export const ToggleSwitch = (): JSX.Element => (
|
||||
<switch
|
||||
className="menu-switch bluetooth"
|
||||
halign={Gtk.Align.END}
|
||||
hexpand
|
||||
active={bluetoothService.isPowered}
|
||||
setup={(self) => {
|
||||
self.connect('notify::active', () => {
|
||||
bluetoothService.adapter?.set_powered(self.active);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
20
src/components/menus/bluetooth/header/Controls/helper.ts
Normal file
20
src/components/menus/bluetooth/header/Controls/helper.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { bind, Variable } from 'astal';
|
||||
import { bluetoothService } from 'src/lib/constants/services';
|
||||
|
||||
export const isDiscovering: Variable<boolean> = Variable(false);
|
||||
let discoveringBinding: Variable<void>;
|
||||
|
||||
Variable.derive([bind(bluetoothService, 'adapter')], () => {
|
||||
if (discoveringBinding) {
|
||||
discoveringBinding();
|
||||
discoveringBinding.drop();
|
||||
}
|
||||
|
||||
if (!bluetoothService.adapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
discoveringBinding = Variable.derive([bind(bluetoothService.adapter, 'discovering')], (discovering) => {
|
||||
isDiscovering.set(discovering);
|
||||
});
|
||||
});
|
||||
14
src/components/menus/bluetooth/header/Controls/index.tsx
Normal file
14
src/components/menus/bluetooth/header/Controls/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import Separator from 'src/components/shared/Separator';
|
||||
import { ToggleSwitch } from './ToggleSwitch';
|
||||
import { DiscoverButton } from './DiscoverButton';
|
||||
|
||||
export const Controls = (): JSX.Element => {
|
||||
return (
|
||||
<box className="controls-container" valign={Gtk.Align.START}>
|
||||
<ToggleSwitch />
|
||||
<Separator className="menu-separator bluetooth" />
|
||||
<DiscoverButton />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
15
src/components/menus/bluetooth/header/index.tsx
Normal file
15
src/components/menus/bluetooth/header/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { Controls } from './Controls';
|
||||
|
||||
export const Header = (): JSX.Element => {
|
||||
const MenuLabel = (): JSX.Element => {
|
||||
return <label className="menu-label" valign={Gtk.Align.CENTER} halign={Gtk.Align.START} label="Bluetooth" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<box className="menu-label-container" halign={Gtk.Align.FILL} valign={Gtk.Align.START}>
|
||||
<MenuLabel />
|
||||
<Controls />
|
||||
</box>
|
||||
);
|
||||
};
|
||||
25
src/components/menus/bluetooth/index.tsx
Normal file
25
src/components/menus/bluetooth/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { BluetoothDevices } from './devices/index.js';
|
||||
import { Header } from './header/index.js';
|
||||
import options from 'src/options.js';
|
||||
import { bind } from 'astal/binding.js';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
|
||||
|
||||
export default (): JSX.Element => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
name={'bluetoothmenu'}
|
||||
transition={bind(options.menus.transition).as((transition) => RevealerTransitionMap[transition])}
|
||||
>
|
||||
<box className={'menu-items bluetooth'} halign={Gtk.Align.FILL} hexpand>
|
||||
<box className={'menu-items-container bluetooth'} halign={Gtk.Align.FILL} vertical hexpand>
|
||||
<box className={'menu-section-container bluetooth'} vertical>
|
||||
<Header />
|
||||
<BluetoothDevices />
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
39
src/components/menus/bluetooth/utils.ts
Normal file
39
src/components/menus/bluetooth/utils.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Retrieves the appropriate Bluetooth icon based on the provided icon name.
|
||||
*
|
||||
* This function returns a Bluetooth icon based on the given icon name. If no match is found,
|
||||
* it returns a default Bluetooth icon. It uses a predefined mapping of device icon names to Bluetooth icons.
|
||||
*
|
||||
* @param iconName The name of the icon to look up.
|
||||
*
|
||||
* @returns The corresponding Bluetooth icon as a string. If no match is found, returns the default Bluetooth icon.
|
||||
*/
|
||||
const getBluetoothIcon = (iconName: string): string => {
|
||||
const deviceIconMap = [
|
||||
['^audio-card*', ''],
|
||||
['^audio-headphones*', ''],
|
||||
['^audio-headset*', ''],
|
||||
['^audio-input*', ''],
|
||||
['^audio-speakers*', ''],
|
||||
['^bluetooth*', ''],
|
||||
['^camera*', ''],
|
||||
['^computer*', ''],
|
||||
['^input-gaming*', ''],
|
||||
['^input-keyboard*', ''],
|
||||
['^input-mouse*', ''],
|
||||
['^input-tablet*', ''],
|
||||
['^media*', ''],
|
||||
['^modem*', ''],
|
||||
['^network*', ''],
|
||||
['^phone*', ''],
|
||||
['^printer*', ''],
|
||||
['^scanner*', ''],
|
||||
['^video-camera*', ''],
|
||||
];
|
||||
|
||||
const foundMatch = deviceIconMap.find((icon) => RegExp(icon[0]).test(iconName.toLowerCase()));
|
||||
|
||||
return foundMatch ? foundMatch[1] : '';
|
||||
};
|
||||
|
||||
export { getBluetoothIcon };
|
||||
20
src/components/menus/calendar/CalendarWidget.tsx
Normal file
20
src/components/menus/calendar/CalendarWidget.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import Calendar from 'src/components/shared/Calendar';
|
||||
|
||||
export const CalendarWidget = (): JSX.Element => {
|
||||
return (
|
||||
<box className={'calendar-menu-item-container calendar'} halign={Gtk.Align.FILL} valign={Gtk.Align.FILL} expand>
|
||||
<box className={'calendar-container-box'}>
|
||||
<Calendar
|
||||
className={'calendar-menu-widget'}
|
||||
halign={Gtk.Align.FILL}
|
||||
valign={Gtk.Align.FILL}
|
||||
showDetails={false}
|
||||
expand
|
||||
showDayNames
|
||||
showHeading
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
35
src/components/menus/calendar/index.tsx
Normal file
35
src/components/menus/calendar/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { TimeWidget } from './time/index';
|
||||
import { CalendarWidget } from './CalendarWidget.js';
|
||||
import { WeatherWidget } from './weather/index';
|
||||
import options from 'src/options';
|
||||
import { bind } from 'astal';
|
||||
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
|
||||
|
||||
const { transition } = options.menus;
|
||||
const { enabled: weatherEnabled } = options.menus.clock.weather;
|
||||
|
||||
export default (): JSX.Element => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
name={'calendarmenu'}
|
||||
transition={bind(transition).as((transition) => RevealerTransitionMap[transition])}
|
||||
>
|
||||
<box css={'padding: 1px; margin: -1px;'}>
|
||||
{bind(weatherEnabled).as((isWeatherEnabled) => {
|
||||
return (
|
||||
<box className={'calendar-menu-content'} vexpand={false}>
|
||||
<box className={'calendar-content-container'} vertical>
|
||||
<box className={'calendar-content-items'} vertical>
|
||||
<TimeWidget />
|
||||
<CalendarWidget />
|
||||
<WeatherWidget isEnabled={isWeatherEnabled} />
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
})}
|
||||
</box>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
35
src/components/menus/calendar/time/MilitaryTime.tsx
Normal file
35
src/components/menus/calendar/time/MilitaryTime.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import options from 'src/options';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { systemTime } from 'src/globals/time';
|
||||
|
||||
const { military, hideSeconds } = options.menus.clock.time;
|
||||
|
||||
export const MilitaryTime = (): JSX.Element => {
|
||||
const timeBinding = Variable.derive([bind(military), bind(hideSeconds)], (is24hr, hideSeconds) => {
|
||||
if (!is24hr) {
|
||||
return <box />;
|
||||
}
|
||||
|
||||
return (
|
||||
<box halign={Gtk.Align.CENTER}>
|
||||
<label
|
||||
className={'clock-content-time'}
|
||||
label={bind(systemTime).as((time) => {
|
||||
return time?.format(hideSeconds ? '%H:%M' : '%H:%M:%S') || '';
|
||||
})}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<box
|
||||
onDestroy={() => {
|
||||
timeBinding.drop();
|
||||
}}
|
||||
>
|
||||
{timeBinding()}
|
||||
</box>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user