Feat: Custom modules can now be created through a JSON file. (#887)
* Feat: Custom modules can now be created through a JSON file. * Added the ability to consume labels and icons. * Add all properties but styling. * Wrap up implementation. * Rename custom modules to basic modules to make way for new actually custom modules.
This commit is contained in:
64
src/components/bar/custom_modules/CustomModules.ts
Normal file
64
src/components/bar/custom_modules/CustomModules.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Gio, readFileAsync } from 'astal';
|
||||
import { CustomBarModule } from './types';
|
||||
import { ModuleContainer } from './module_container';
|
||||
import { WidgetContainer } from '../shared/WidgetContainer';
|
||||
import { WidgetMap } from '..';
|
||||
|
||||
export class CustomModules {
|
||||
constructor() {}
|
||||
|
||||
public static async build(): Promise<WidgetMap> {
|
||||
const customModuleMap = await this._getCustomModules();
|
||||
const customModuleComponents: WidgetMap = {};
|
||||
|
||||
try {
|
||||
Object.entries(customModuleMap).map(([moduleName, moduleMetadata]) => {
|
||||
if (!moduleName.startsWith('custom/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
customModuleComponents[moduleName] = (): JSX.Element =>
|
||||
WidgetContainer(ModuleContainer(moduleName, moduleMetadata));
|
||||
});
|
||||
|
||||
return customModuleComponents;
|
||||
} catch (error) {
|
||||
console.log(`Failed to build custom modules in ${CONFIG_DIR}: ${error}`);
|
||||
throw new Error(`Failed to build custom modules in ${CONFIG_DIR}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async _getCustomModules(): Promise<Record<string, CustomBarModule>> {
|
||||
try {
|
||||
const filesInConfigDir = await this._getFilesInConfigDir();
|
||||
const modulesFile = filesInConfigDir.find((file) => file.match(/^modules(\.json)?$/));
|
||||
const pathToModulesFile = `${CONFIG_DIR}/${modulesFile}`;
|
||||
|
||||
const customModulesFileContent = await readFileAsync(pathToModulesFile);
|
||||
|
||||
const modulesObject = JSON.parse(customModulesFileContent);
|
||||
|
||||
return modulesObject;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse modules file in ${CONFIG_DIR}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async _getFilesInConfigDir(): Promise<string[]> {
|
||||
const file = Gio.File.new_for_path(CONFIG_DIR);
|
||||
const enumerator = file.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
|
||||
const fileNames = [];
|
||||
|
||||
for (const info of enumerator) {
|
||||
const fileType = info.get_file_type();
|
||||
const fileName = info.get_name();
|
||||
|
||||
if (fileType === Gio.FileType.REGULAR) {
|
||||
fileNames.push(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
enumerator.close(null);
|
||||
return fileNames;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { isPrimitive } from 'src/lib/utils';
|
||||
import { CustomBarModuleIcon } from '../../types';
|
||||
import { parseCommandOutputJson } from './utils';
|
||||
|
||||
const ERROR_ICON = '';
|
||||
|
||||
/**
|
||||
* Resolves the appropriate icon for a custom bar module based on its configuration and command output
|
||||
*
|
||||
* @param moduleName - The name of the module requesting the icon
|
||||
* @param commandOutput - The raw output string from the module's command execution
|
||||
* @param moduleIcon - The module's configuration metadata containing icon settings
|
||||
* @returns The resolved icon string based on the configuration, or ERROR_ICON if resolution fails
|
||||
*
|
||||
* @example
|
||||
* // Using a static icon
|
||||
* getIcon('myModule', '', { icon: '🚀' }) // returns '🚀'
|
||||
*
|
||||
* // Using an array of icons based on percentage
|
||||
* getIcon('myModule', '{"percentage": 50}', { icon: ['😡', '😐', '😊'] })
|
||||
*
|
||||
* // Using an object mapping for specific states
|
||||
* getIcon('myModule', '{"alt": "success"}', { icon: { success: '✅', error: '❌' } })
|
||||
*/
|
||||
export function getIcon(moduleName: string, commandOutput: string, moduleIcon: CustomBarModuleIcon): string {
|
||||
if (Array.isArray(moduleIcon)) {
|
||||
return getIconFromArray(moduleName, commandOutput, moduleIcon);
|
||||
}
|
||||
|
||||
if (typeof moduleIcon === 'object') {
|
||||
return getIconFromObject(moduleName, commandOutput, moduleIcon);
|
||||
}
|
||||
|
||||
return moduleIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an icon from an object configuration based on the 'alt' value in command output
|
||||
*
|
||||
* @param moduleName - The name of the module requesting the icon
|
||||
* @param commandOutput - The raw output string from the module's command execution
|
||||
* @param iconObject - Object mapping alternate text to corresponding icons
|
||||
* @returns The matched icon string or ERROR_ICON if resolution fails
|
||||
*
|
||||
* @throws Logs error and returns ERROR_ICON if:
|
||||
* - Command output cannot be parsed
|
||||
* - 'alt' value is not a string
|
||||
* - No matching icon is found for the alt text
|
||||
* - Corresponding icon value is not a string
|
||||
*/
|
||||
function getIconFromObject(moduleName: string, commandOutput: string, iconObject: Record<string, unknown>): string {
|
||||
try {
|
||||
const commandResults: CommandResults = parseCommandOutputJson(moduleName, commandOutput);
|
||||
|
||||
if (!isPrimitive(commandResults?.alt) || commandResults?.alt === undefined) {
|
||||
console.error(`Expected 'alt' to be a primitive for module: ${moduleName}`);
|
||||
return ERROR_ICON;
|
||||
}
|
||||
|
||||
const resultsAltText = String(commandResults?.alt);
|
||||
|
||||
const correspondingAltIcon = iconObject[resultsAltText];
|
||||
|
||||
if (correspondingAltIcon === undefined) {
|
||||
console.error(`Corresponding icon ${resultsAltText} not found for module: ${moduleName}`);
|
||||
return typeof iconObject.default === 'string' ? iconObject.default : ERROR_ICON;
|
||||
}
|
||||
|
||||
if (typeof correspondingAltIcon !== 'string') {
|
||||
console.error(`Corresponding icon ${resultsAltText} is not a string for module: ${moduleName}`);
|
||||
return ERROR_ICON;
|
||||
}
|
||||
|
||||
return correspondingAltIcon;
|
||||
} catch {
|
||||
return ERROR_ICON;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an icon from an array configuration based on the percentage value in command output
|
||||
*
|
||||
* @param moduleName - The name of the module requesting the icon
|
||||
* @param commandOutput - The raw output string from the module's command execution
|
||||
* @param iconArray - Array of icons to select from based on percentage ranges
|
||||
* @returns The appropriate icon string based on the percentage or ERROR_ICON if resolution fails
|
||||
*
|
||||
* @example
|
||||
* // With iconArray ['😡', '😐', '😊']
|
||||
* // 0-33%: returns '😡'
|
||||
* // 34-66%: returns '😐'
|
||||
* // 67-100%: returns '😊'
|
||||
*
|
||||
* @throws Logs error and returns ERROR_ICON if:
|
||||
* - Command output cannot be parsed
|
||||
* - Percentage value is not a number
|
||||
* - Percentage is NaN or exceeds 100
|
||||
*/
|
||||
function getIconFromArray(moduleName: string, commandOutput: string, iconArray: string[]): string {
|
||||
try {
|
||||
const commandResults: CommandResults = parseCommandOutputJson(moduleName, commandOutput);
|
||||
const resultsPercentage = commandResults?.percentage;
|
||||
|
||||
if (typeof resultsPercentage !== 'number') {
|
||||
console.error(`Expected percentage to be a number for module: ${moduleName}`);
|
||||
return ERROR_ICON;
|
||||
}
|
||||
|
||||
if (isNaN(resultsPercentage) || resultsPercentage > 100) {
|
||||
console.error(`Expected percentage to be between 1-100 for module: ${moduleName}`);
|
||||
return ERROR_ICON;
|
||||
}
|
||||
|
||||
const step = 100 / iconArray.length;
|
||||
|
||||
const iconForStep = iconArray.find((_, index) => resultsPercentage <= step * (index + 1));
|
||||
|
||||
return iconForStep || ERROR_ICON;
|
||||
} catch {
|
||||
return ERROR_ICON;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the expected structure of parsed command output
|
||||
*/
|
||||
type CommandResults = {
|
||||
/** Alternate text identifier for object-based icon configuration */
|
||||
alt?: string;
|
||||
/** Percentage value for array-based icon configuration (0-100) */
|
||||
percentage?: number;
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import { isPrimitive } from 'src/lib/utils';
|
||||
|
||||
/**
|
||||
* Generates a label based on module command output and a template configuration.
|
||||
*
|
||||
* @param moduleName - The name of the module (used for error reporting)
|
||||
* @param commandOutput - The raw output from a module command, expected to be a JSON string or plain text
|
||||
* @param labelConfig - A template string containing variables in the format {path.to.value}
|
||||
* @returns A formatted label with template variables replaced with actual values
|
||||
*
|
||||
* @example
|
||||
* // For a JSON command output: {"user": {"name": "Jim Halpert"}}
|
||||
* // And labelConfig: "Hello, {user.name}!"
|
||||
* // Returns: "Hello, Jim Halpert!"
|
||||
*/
|
||||
export function getLabel(moduleName: string, commandOutput: string, labelConfig: string): string {
|
||||
const processedCommandOutput = tryParseJson(moduleName, commandOutput);
|
||||
const regexForTemplateVariables = /\{([^{}]*)\}/g;
|
||||
|
||||
return labelConfig.replace(regexForTemplateVariables, (_, path) => {
|
||||
return getValueForTemplateVariable(path, processedCommandOutput);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a value from command output based on a template variable path.
|
||||
*
|
||||
* @param templatePath - The dot-notation path to extract (e.g., "user.name")
|
||||
* @param commandOutput - The processed command output (either a string or object)
|
||||
* @returns The extracted value as a string, or empty string if not found
|
||||
*/
|
||||
function getValueForTemplateVariable(templatePath: string, commandOutput: string | Record<string, unknown>): string {
|
||||
if (typeof commandOutput === 'string') {
|
||||
return getTemplateValueForStringOutput(templatePath, commandOutput);
|
||||
}
|
||||
|
||||
if (typeof commandOutput === 'object' && commandOutput !== null) {
|
||||
return getTemplateValueForObjectOutput(templatePath, commandOutput);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a template value from string command output.
|
||||
*
|
||||
* @param templatePath - The path to extract value from
|
||||
* @param commandOutput - The string command output
|
||||
* @returns The entire string if path is empty, otherwise empty string
|
||||
*/
|
||||
function getTemplateValueForStringOutput(templatePath: string, commandOutput: string): string {
|
||||
if (templatePath === '') {
|
||||
return commandOutput;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a template value from object command output using dot notation.
|
||||
*
|
||||
* @param templatePath - The dot-notation path to extract (e.g., "user.name")
|
||||
* @param commandOutput - The object representing parsed command output
|
||||
* @returns The extracted value as a string, or empty string if path is invalid or value is not primitive
|
||||
*/
|
||||
function getTemplateValueForObjectOutput(templatePath: string, commandOutput: Record<string, unknown>): string {
|
||||
const pathParts = templatePath.split('.');
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && !Array.isArray(value) && typeof value === 'object';
|
||||
}
|
||||
|
||||
try {
|
||||
const result = pathParts.reduce<unknown>((acc, part) => {
|
||||
if (!isRecord(acc)) {
|
||||
throw new Error('Path unreachable');
|
||||
}
|
||||
|
||||
return acc[part];
|
||||
}, commandOutput);
|
||||
|
||||
return isPrimitive(result) && result !== undefined ? String(result) : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse a JSON string, with fallback to the original string.
|
||||
*
|
||||
* @param moduleName - The name of the module (used for error reporting)
|
||||
* @param commandOutput - The raw string output to parse as JSON
|
||||
* @returns A parsed object if valid JSON and an object, otherwise the original string
|
||||
*/
|
||||
function tryParseJson(moduleName: string, commandOutput: string): string | Record<string, unknown> {
|
||||
try {
|
||||
if (typeof commandOutput !== 'string') {
|
||||
console.error(
|
||||
`Expected command output to be a string but found ${typeof commandOutput} for module: ${moduleName}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
|
||||
const parsedCommand = JSON.parse(commandOutput);
|
||||
|
||||
if (typeof parsedCommand === 'object' && parsedCommand !== null && !Array.isArray(parsedCommand)) {
|
||||
return parsedCommand as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return commandOutput;
|
||||
} catch {
|
||||
return commandOutput;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function parseCommandOutputJson(moduleName: string, cmdOutput: unknown): Record<string, unknown> {
|
||||
try {
|
||||
if (typeof cmdOutput !== 'string') {
|
||||
throw new Error('Input must be a string');
|
||||
}
|
||||
|
||||
return JSON.parse(cmdOutput);
|
||||
} catch {
|
||||
throw new Error(`The command output for the following module is not valid JSON: ${moduleName}`);
|
||||
}
|
||||
}
|
||||
48
src/components/bar/custom_modules/module_container/index.tsx
Normal file
48
src/components/bar/custom_modules/module_container/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { BarBoxChild } from 'src/lib/types/bar.js';
|
||||
import { CustomBarModule } from '../types';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { getIcon } from './helpers/icon';
|
||||
import { getLabel } from './helpers/label';
|
||||
import { initActionListener, initCommandPoller, setupModuleInteractions } from './setup';
|
||||
|
||||
export const ModuleContainer = (moduleName: string, moduleMetadata: CustomBarModule): BarBoxChild => {
|
||||
const {
|
||||
icon: moduleIcon = '',
|
||||
label: moduleLabel = '',
|
||||
tooltip: moduleTooltip = '',
|
||||
truncationSize: moduleTruncation = -1,
|
||||
execute: moduleExecute = '',
|
||||
executeOnAction: moduleExecuteOnAction = '',
|
||||
interval: moduleInterval = -1,
|
||||
hideOnEmpty: moduleHideOnEmpty = false,
|
||||
scrollThreshold: moduleScrollThreshold = 4,
|
||||
actions: moduleActions = {},
|
||||
} = moduleMetadata;
|
||||
|
||||
const pollingInterval: Variable<number> = Variable(moduleInterval);
|
||||
const actionExecutionListener: Variable<boolean> = Variable(true);
|
||||
const commandOutput: Variable<string> = Variable('');
|
||||
|
||||
const commandPoller = initCommandPoller(commandOutput, pollingInterval, moduleExecute, moduleInterval);
|
||||
initActionListener(actionExecutionListener, moduleExecuteOnAction, commandOutput);
|
||||
|
||||
const module = Module({
|
||||
textIcon: bind(commandOutput).as((cmdOutput) => getIcon(moduleName, cmdOutput, moduleIcon)),
|
||||
tooltipText: bind(commandOutput).as((cmdOutput) => getLabel(moduleName, cmdOutput, moduleTooltip)),
|
||||
boxClass: `cmodule-${moduleName.replace(/custom\//, '')}`,
|
||||
label: bind(commandOutput).as((cmdOutput) => getLabel(moduleName, cmdOutput, moduleLabel)),
|
||||
truncationSize: bind(Variable(typeof moduleTruncation === 'number' ? moduleTruncation : -1)),
|
||||
props: {
|
||||
setup: (self: Astal.Button) =>
|
||||
setupModuleInteractions(self, moduleActions, actionExecutionListener, moduleScrollThreshold),
|
||||
onDestroy: () => {
|
||||
commandPoller.stop();
|
||||
},
|
||||
},
|
||||
isVis: bind(commandOutput).as((cmdOutput) => (moduleHideOnEmpty ? cmdOutput.length > 0 : true)),
|
||||
});
|
||||
|
||||
return module;
|
||||
};
|
||||
76
src/components/bar/custom_modules/module_container/setup.ts
Normal file
76
src/components/bar/custom_modules/module_container/setup.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Variable, bind, execAsync } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BashPoller } from 'src/lib/poller/BashPoller';
|
||||
import { CustomBarModule } from '../types';
|
||||
import { inputHandler } from '../../utils/helpers';
|
||||
|
||||
export function initCommandPoller(
|
||||
commandOutput: Variable<string>,
|
||||
pollingInterval: Variable<number>,
|
||||
moduleExecute: string,
|
||||
moduleInterval: number,
|
||||
): BashPoller<string, []> {
|
||||
const commandPoller = new BashPoller<string, []>(
|
||||
commandOutput,
|
||||
[],
|
||||
bind(pollingInterval),
|
||||
moduleExecute || '',
|
||||
(commandResult: string) => commandResult,
|
||||
);
|
||||
|
||||
if (moduleInterval >= 0) {
|
||||
commandPoller.initialize();
|
||||
}
|
||||
|
||||
return commandPoller;
|
||||
}
|
||||
|
||||
export function initActionListener(
|
||||
actionExecutionListener: Variable<boolean>,
|
||||
moduleExecuteOnAction: string,
|
||||
commandOutput: Variable<string>,
|
||||
): void {
|
||||
actionExecutionListener.subscribe(() => {
|
||||
if (typeof moduleExecuteOnAction !== 'string' || !moduleExecuteOnAction.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
execAsync(moduleExecuteOnAction).then((cmdOutput) => {
|
||||
commandOutput.set(cmdOutput);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up user interaction handlers for the module
|
||||
*/
|
||||
export function setupModuleInteractions(
|
||||
element: Astal.Button,
|
||||
moduleActions: CustomBarModule['actions'],
|
||||
actionListener: Variable<boolean>,
|
||||
moduleScrollThreshold: number,
|
||||
): void {
|
||||
const scrollThreshold = moduleScrollThreshold >= 0 ? moduleScrollThreshold : 1;
|
||||
inputHandler(
|
||||
element,
|
||||
{
|
||||
onPrimaryClick: {
|
||||
cmd: Variable(moduleActions?.onLeftClick ?? ''),
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: Variable(moduleActions?.onRightClick ?? ''),
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: Variable(moduleActions?.onMiddleClick ?? ''),
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: Variable(moduleActions?.onScrollUp ?? ''),
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: Variable(moduleActions?.onScrollDown ?? ''),
|
||||
},
|
||||
},
|
||||
actionListener,
|
||||
scrollThreshold,
|
||||
);
|
||||
}
|
||||
20
src/components/bar/custom_modules/types.ts
Normal file
20
src/components/bar/custom_modules/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export type CustomBarModuleActions = {
|
||||
onLeftClick?: string;
|
||||
onRightClick?: string;
|
||||
onMiddleClick?: string;
|
||||
onScrollUp?: string;
|
||||
onScrollDown?: string;
|
||||
};
|
||||
export type CustomBarModule = {
|
||||
icon?: CustomBarModuleIcon;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
truncationSize?: number;
|
||||
execute?: string;
|
||||
executeOnAction?: string;
|
||||
interval?: number;
|
||||
hideOnEmpty?: boolean;
|
||||
scrollThreshold?: number;
|
||||
actions?: CustomBarModuleActions;
|
||||
};
|
||||
export type CustomBarModuleIcon = string | string[] | Record<string, string>;
|
||||
@@ -10,7 +10,7 @@ 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
|
||||
// Basic Modules
|
||||
import { Microphone } from '../../components/bar/modules/microphone/index';
|
||||
import { Ram } from '../../components/bar/modules/ram/index';
|
||||
import { Cpu } from '../../components/bar/modules/cpu/index';
|
||||
@@ -40,7 +40,7 @@ export {
|
||||
Clock,
|
||||
SysTray,
|
||||
|
||||
// Custom Modules
|
||||
// Basic Modules
|
||||
Microphone,
|
||||
Ram,
|
||||
Cpu,
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
BatteryLabel,
|
||||
Clock,
|
||||
SysTray,
|
||||
|
||||
// Custom Modules
|
||||
Microphone,
|
||||
Ram,
|
||||
Cpu,
|
||||
@@ -37,12 +35,13 @@ import Astal from 'gi://Astal?version=3.0';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { getLayoutForMonitor, isLayoutEmpty } from './utils/monitors';
|
||||
import { GdkMonitorMapper } from './utils/GdkMonitorMapper';
|
||||
import { CustomModules } from './custom_modules/CustomModules';
|
||||
|
||||
const { layouts } = options.bar;
|
||||
const { location } = options.theme.bar;
|
||||
const { location: borderLocation } = options.theme.bar.border;
|
||||
|
||||
const widget = {
|
||||
let widgets: WidgetMap = {
|
||||
battery: (): JSX.Element => WidgetContainer(BatteryLabel()),
|
||||
dashboard: (): JSX.Element => WidgetContainer(Menu()),
|
||||
workspaces: (monitor: number): JSX.Element => WidgetContainer(Workspaces(monitor)),
|
||||
@@ -73,7 +72,16 @@ const widget = {
|
||||
|
||||
const gdkMonitorMapper = new GdkMonitorMapper();
|
||||
|
||||
export const Bar = (monitor: number): JSX.Element => {
|
||||
export const Bar = async (monitor: number): Promise<JSX.Element> => {
|
||||
try {
|
||||
const customWidgets = await CustomModules.build();
|
||||
widgets = {
|
||||
...widgets,
|
||||
...customWidgets,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
const hyprlandMonitor = gdkMonitorMapper.mapGdkToHyprland(monitor);
|
||||
|
||||
const computeVisibility = bind(layouts).as(() => {
|
||||
@@ -116,22 +124,22 @@ export const Bar = (monitor: number): JSX.Element => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||
|
||||
return foundLayout.left
|
||||
.filter((mod) => Object.keys(widget).includes(mod))
|
||||
.map((w) => widget[w](hyprlandMonitor));
|
||||
.filter((mod) => Object.keys(widgets).includes(mod))
|
||||
.map((w) => widgets[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));
|
||||
.filter((mod) => Object.keys(widgets).includes(mod))
|
||||
.map((w) => widgets[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));
|
||||
.filter((mod) => Object.keys(widgets).includes(mod))
|
||||
.map((w) => widgets[w](hyprlandMonitor));
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -178,3 +186,7 @@ export const Bar = (monitor: number): JSX.Element => {
|
||||
</window>
|
||||
);
|
||||
};
|
||||
|
||||
export type WidgetMap = {
|
||||
[K in string]: (monitor: number) => JSX.Element;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Gtk } from 'astal/gtk3';
|
||||
export const CustomModuleSettings = (): JSX.Element => {
|
||||
return (
|
||||
<scrollable
|
||||
name={'Custom Modules'}
|
||||
name={'Basic Modules'}
|
||||
className="menu-theme-page customModules paged-container"
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
hscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Gtk } from 'astal/gtk3';
|
||||
export const CustomModuleTheme = (): JSX.Element => {
|
||||
return (
|
||||
<scrollable
|
||||
name={'Custom Modules'}
|
||||
name={'Basic Modules'}
|
||||
className="menu-theme-page customModules paged-container"
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
hscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const Module = ({
|
||||
textIcon,
|
||||
useTextIcon = bind(Variable(false)),
|
||||
label,
|
||||
truncationSize = bind(Variable(-1)),
|
||||
tooltipText = '',
|
||||
boxClass,
|
||||
isVis,
|
||||
@@ -21,15 +22,17 @@ export const Module = ({
|
||||
hook,
|
||||
}: BarModule): BarBoxChild => {
|
||||
const getIconWidget = (useTxtIcn: boolean): JSX.Element | undefined => {
|
||||
let iconWidget: JSX.Element | undefined;
|
||||
const className = `txt-icon bar-button-icon module-icon ${boxClass}`;
|
||||
|
||||
if (icon !== undefined && icon.get() != '' && !useTxtIcn) {
|
||||
iconWidget = <icon className={`txt-icon bar-button-icon module-icon ${boxClass}`} icon={icon} />;
|
||||
} else if (textIcon !== undefined && textIcon.get() != '') {
|
||||
iconWidget = <label className={`txt-icon bar-button-icon module-icon ${boxClass}`} label={textIcon} />;
|
||||
const icn = typeof icon === 'string' ? icon : icon?.get();
|
||||
if (!useTxtIcn && icn?.length) {
|
||||
return <icon className={className} icon={icon} />;
|
||||
}
|
||||
|
||||
return iconWidget;
|
||||
const textIcn = typeof textIcon === 'string' ? textIcon : textIcon?.get();
|
||||
if (textIcn?.length) {
|
||||
return <label className={className} label={textIcon} />;
|
||||
}
|
||||
};
|
||||
|
||||
const componentClass = Variable.derive(
|
||||
@@ -60,6 +63,8 @@ export const Module = ({
|
||||
childrenArray.push(
|
||||
<label
|
||||
className={`bar-button-label module-label ${boxClass}`}
|
||||
truncate={truncationSize.as((truncSize) => truncSize > 0)}
|
||||
maxWidthChars={truncationSize.as((truncSize) => truncSize)}
|
||||
label={label ?? ''}
|
||||
setup={labelHook}
|
||||
/>,
|
||||
|
||||
@@ -42,7 +42,7 @@ export const runAsyncCommand: RunAsyncCommand = (cmd, events, fn, postInputUpdat
|
||||
if (cmd.startsWith('menu:')) {
|
||||
const menuName = cmd.split(':')[1].trim().toLowerCase();
|
||||
openMenu(events.clicked, events.event, `${menuName}menu`);
|
||||
|
||||
handlePostInputUpdater(postInputUpdater);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,8 +122,9 @@ export const inputHandler = (
|
||||
onScrollDown: onScrollDownInput,
|
||||
}: InputHandlerEvents,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
customScrollThreshold?: number,
|
||||
): void => {
|
||||
const sanitizeInput = (input?: Variable<string> | Variable<string>): string => {
|
||||
const sanitizeInput = (input?: Variable<string>): string => {
|
||||
if (input === undefined) {
|
||||
return '';
|
||||
}
|
||||
@@ -131,7 +132,7 @@ export const inputHandler = (
|
||||
};
|
||||
|
||||
const updateHandlers = (): UpdateHandlers => {
|
||||
const interval = scrollSpeed.get();
|
||||
const interval = customScrollThreshold ?? scrollSpeed.get();
|
||||
const throttledHandler = throttledScrollHandler(interval);
|
||||
|
||||
const disconnectPrimaryClick = onPrimaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const themePages = [
|
||||
'System Tray',
|
||||
'Volume Menu',
|
||||
'Power Menu',
|
||||
'Custom Modules',
|
||||
'Basic Modules',
|
||||
] as const;
|
||||
|
||||
export const configPages = [
|
||||
@@ -28,7 +28,7 @@ export const configPages = [
|
||||
'Volume',
|
||||
'Clock Menu',
|
||||
'Dashboard Menu',
|
||||
'Custom Modules',
|
||||
'Basic Modules',
|
||||
'Power Menu',
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export const BarGeneral = (): JSX.Element => {
|
||||
title="Config"
|
||||
subtitle="WARNING: Importing a configuration will replace your current configuration settings."
|
||||
type="config_import"
|
||||
exportData={{ filePath: CONFIG, themeOnly: false }}
|
||||
exportData={{ filePath: CONFIG_FILE, themeOnly: false }}
|
||||
/>
|
||||
<Option
|
||||
opt={options.hyprpanel.restartAgs}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const MenuTheme = (): JSX.Element => {
|
||||
title="Theme"
|
||||
subtitle="WARNING: Importing a theme will replace your current theme color settings."
|
||||
type="config_import"
|
||||
exportData={{ filePath: CONFIG, themeOnly: true }}
|
||||
exportData={{ filePath: CONFIG_FILE, themeOnly: true }}
|
||||
/>
|
||||
<Option
|
||||
opt={options.theme.bar.menus.monochrome}
|
||||
|
||||
@@ -272,7 +272,7 @@ export const importFiles = (themeOnly: boolean = false): void => {
|
||||
iconName: icons.ui.info,
|
||||
});
|
||||
|
||||
const optionsConfigFile = Gio.File.new_for_path(CONFIG);
|
||||
const optionsConfigFile = Gio.File.new_for_path(CONFIG_FILE);
|
||||
|
||||
const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null);
|
||||
|
||||
@@ -289,7 +289,7 @@ export const importFiles = (themeOnly: boolean = false): void => {
|
||||
: filterConfigForNonTheme(importedConfig);
|
||||
optionsConfig = { ...optionsConfig, ...filteredConfig };
|
||||
|
||||
saveConfigToFile(optionsConfig, CONFIG);
|
||||
saveConfigToFile(optionsConfig, CONFIG_FILE);
|
||||
}
|
||||
dialog.destroy();
|
||||
bash(restartCommand.get());
|
||||
|
||||
Reference in New Issue
Block a user