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:
Jas Singh
2025-04-07 01:52:39 -07:00
committed by GitHub
parent 483facfa56
commit 93235f0fb1
31 changed files with 820 additions and 377 deletions

View File

@@ -4,5 +4,13 @@
"trailingComma": "all", "trailingComma": "all",
"printWidth": 120, "printWidth": 120,
"tabWidth": 4, "tabWidth": 4,
"useTabs": false "useTabs": false,
"overrides": [
{
"files": ["**/*.jsonc"],
"options": {
"parser": "json"
}
}
]
} }

7
app.ts
View File

@@ -53,12 +53,15 @@ App.start({
requestHandler(request: string, res: (response: unknown) => void) { requestHandler(request: string, res: (response: unknown) => void) {
runCLI(request, res); runCLI(request, res);
}, },
main() { async main() {
initializeStartupScripts(); initializeStartupScripts();
Notifications(); Notifications();
OSD(); OSD();
forMonitors(Bar).forEach((bar: JSX.Element) => bar);
const barsForMonitors = await forMonitors(Bar);
barsForMonitors.forEach((bar: JSX.Element) => bar);
SettingsDialog(); SettingsDialog();
initializeMenus(); initializeMenus();

7
package-lock.json generated
View File

@@ -24,13 +24,8 @@
"typescript": "^5.6.2" "typescript": "^5.6.2"
} }
}, },
"../../../../../usr/share/astal/gjs": {
"name": "astal",
"license": "LGPL-2.1"
},
"../../../../usr/share/astal/gjs": { "../../../../usr/share/astal/gjs": {
"name": "astal", "name": "astal",
"extraneous": true,
"license": "LGPL-2.1" "license": "LGPL-2.1"
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
@@ -606,7 +601,7 @@
} }
}, },
"node_modules/astal": { "node_modules/astal": {
"resolved": "../../../../../usr/share/astal/gjs", "resolved": "../../../../usr/share/astal/gjs",
"link": true "link": true
}, },
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {

View File

@@ -120,11 +120,11 @@ export const utilityCommands: Command[] = [
try { try {
const oldFile = Gio.File.new_for_path(oldPath); const oldFile = Gio.File.new_for_path(oldPath);
const newFile = Gio.File.new_for_path(CONFIG); const newFile = Gio.File.new_for_path(CONFIG_FILE);
if (oldFile.query_exists(null)) { if (oldFile.query_exists(null)) {
oldFile.move(newFile, Gio.FileCopyFlags.OVERWRITE, null, null); oldFile.move(newFile, Gio.FileCopyFlags.OVERWRITE, null, null);
return `Configuration file moved to ${CONFIG}`; return `Configuration file moved to ${CONFIG_FILE}`;
} else { } else {
return `Old configuration file does not exist at ${oldPath}`; return `Old configuration file does not exist at ${oldPath}`;
} }

View 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;
}
}

View File

@@ -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;
};

View File

@@ -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;
}
}

View File

@@ -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}`);
}
}

View 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;
};

View 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,
);
}

View 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>;

View File

@@ -10,7 +10,7 @@ import { BatteryLabel } from '../../components/bar/modules/battery/index';
import { Clock } from '../../components/bar/modules/clock/index'; import { Clock } from '../../components/bar/modules/clock/index';
import { SysTray } from '../../components/bar/modules/systray/index'; import { SysTray } from '../../components/bar/modules/systray/index';
// Custom Modules // Basic Modules
import { Microphone } from '../../components/bar/modules/microphone/index'; import { Microphone } from '../../components/bar/modules/microphone/index';
import { Ram } from '../../components/bar/modules/ram/index'; import { Ram } from '../../components/bar/modules/ram/index';
import { Cpu } from '../../components/bar/modules/cpu/index'; import { Cpu } from '../../components/bar/modules/cpu/index';
@@ -40,7 +40,7 @@ export {
Clock, Clock,
SysTray, SysTray,
// Custom Modules // Basic Modules
Microphone, Microphone,
Ram, Ram,
Cpu, Cpu,

View File

@@ -10,8 +10,6 @@ import {
BatteryLabel, BatteryLabel,
Clock, Clock,
SysTray, SysTray,
// Custom Modules
Microphone, Microphone,
Ram, Ram,
Cpu, Cpu,
@@ -37,12 +35,13 @@ import Astal from 'gi://Astal?version=3.0';
import { bind, Variable } from 'astal'; import { bind, Variable } from 'astal';
import { getLayoutForMonitor, isLayoutEmpty } from './utils/monitors'; import { getLayoutForMonitor, isLayoutEmpty } from './utils/monitors';
import { GdkMonitorMapper } from './utils/GdkMonitorMapper'; import { GdkMonitorMapper } from './utils/GdkMonitorMapper';
import { CustomModules } from './custom_modules/CustomModules';
const { layouts } = options.bar; const { layouts } = options.bar;
const { location } = options.theme.bar; const { location } = options.theme.bar;
const { location: borderLocation } = options.theme.bar.border; const { location: borderLocation } = options.theme.bar.border;
const widget = { let widgets: WidgetMap = {
battery: (): JSX.Element => WidgetContainer(BatteryLabel()), battery: (): JSX.Element => WidgetContainer(BatteryLabel()),
dashboard: (): JSX.Element => WidgetContainer(Menu()), dashboard: (): JSX.Element => WidgetContainer(Menu()),
workspaces: (monitor: number): JSX.Element => WidgetContainer(Workspaces(monitor)), workspaces: (monitor: number): JSX.Element => WidgetContainer(Workspaces(monitor)),
@@ -73,7 +72,16 @@ const widget = {
const gdkMonitorMapper = new GdkMonitorMapper(); 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 hyprlandMonitor = gdkMonitorMapper.mapGdkToHyprland(monitor);
const computeVisibility = bind(layouts).as(() => { const computeVisibility = bind(layouts).as(() => {
@@ -116,22 +124,22 @@ export const Bar = (monitor: number): JSX.Element => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
return foundLayout.left return foundLayout.left
.filter((mod) => Object.keys(widget).includes(mod)) .filter((mod) => Object.keys(widgets).includes(mod))
.map((w) => widget[w](hyprlandMonitor)); .map((w) => widgets[w](hyprlandMonitor));
}); });
const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => { const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
return foundLayout.middle return foundLayout.middle
.filter((mod) => Object.keys(widget).includes(mod)) .filter((mod) => Object.keys(widgets).includes(mod))
.map((w) => widget[w](hyprlandMonitor)); .map((w) => widgets[w](hyprlandMonitor));
}); });
const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => { const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
return foundLayout.right return foundLayout.right
.filter((mod) => Object.keys(widget).includes(mod)) .filter((mod) => Object.keys(widgets).includes(mod))
.map((w) => widget[w](hyprlandMonitor)); .map((w) => widgets[w](hyprlandMonitor));
}); });
return ( return (
@@ -178,3 +186,7 @@ export const Bar = (monitor: number): JSX.Element => {
</window> </window>
); );
}; };
export type WidgetMap = {
[K in string]: (monitor: number) => JSX.Element;
};

View File

@@ -6,7 +6,7 @@ import { Gtk } from 'astal/gtk3';
export const CustomModuleSettings = (): JSX.Element => { export const CustomModuleSettings = (): JSX.Element => {
return ( return (
<scrollable <scrollable
name={'Custom Modules'} name={'Basic Modules'}
className="menu-theme-page customModules paged-container" className="menu-theme-page customModules paged-container"
vscroll={Gtk.PolicyType.AUTOMATIC} vscroll={Gtk.PolicyType.AUTOMATIC}
hscroll={Gtk.PolicyType.AUTOMATIC} hscroll={Gtk.PolicyType.AUTOMATIC}

View File

@@ -7,7 +7,7 @@ import { Gtk } from 'astal/gtk3';
export const CustomModuleTheme = (): JSX.Element => { export const CustomModuleTheme = (): JSX.Element => {
return ( return (
<scrollable <scrollable
name={'Custom Modules'} name={'Basic Modules'}
className="menu-theme-page customModules paged-container" className="menu-theme-page customModules paged-container"
vscroll={Gtk.PolicyType.AUTOMATIC} vscroll={Gtk.PolicyType.AUTOMATIC}
hscroll={Gtk.PolicyType.AUTOMATIC} hscroll={Gtk.PolicyType.AUTOMATIC}

View File

@@ -10,6 +10,7 @@ export const Module = ({
textIcon, textIcon,
useTextIcon = bind(Variable(false)), useTextIcon = bind(Variable(false)),
label, label,
truncationSize = bind(Variable(-1)),
tooltipText = '', tooltipText = '',
boxClass, boxClass,
isVis, isVis,
@@ -21,15 +22,17 @@ export const Module = ({
hook, hook,
}: BarModule): BarBoxChild => { }: BarModule): BarBoxChild => {
const getIconWidget = (useTxtIcn: boolean): JSX.Element | undefined => { 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) { const icn = typeof icon === 'string' ? icon : icon?.get();
iconWidget = <icon className={`txt-icon bar-button-icon module-icon ${boxClass}`} icon={icon} />; if (!useTxtIcn && icn?.length) {
} else if (textIcon !== undefined && textIcon.get() != '') { return <icon className={className} icon={icon} />;
iconWidget = <label className={`txt-icon bar-button-icon module-icon ${boxClass}`} label={textIcon} />;
} }
return iconWidget; const textIcn = typeof textIcon === 'string' ? textIcon : textIcon?.get();
if (textIcn?.length) {
return <label className={className} label={textIcon} />;
}
}; };
const componentClass = Variable.derive( const componentClass = Variable.derive(
@@ -60,6 +63,8 @@ export const Module = ({
childrenArray.push( childrenArray.push(
<label <label
className={`bar-button-label module-label ${boxClass}`} className={`bar-button-label module-label ${boxClass}`}
truncate={truncationSize.as((truncSize) => truncSize > 0)}
maxWidthChars={truncationSize.as((truncSize) => truncSize)}
label={label ?? ''} label={label ?? ''}
setup={labelHook} setup={labelHook}
/>, />,

View File

@@ -42,7 +42,7 @@ export const runAsyncCommand: RunAsyncCommand = (cmd, events, fn, postInputUpdat
if (cmd.startsWith('menu:')) { if (cmd.startsWith('menu:')) {
const menuName = cmd.split(':')[1].trim().toLowerCase(); const menuName = cmd.split(':')[1].trim().toLowerCase();
openMenu(events.clicked, events.event, `${menuName}menu`); openMenu(events.clicked, events.event, `${menuName}menu`);
handlePostInputUpdater(postInputUpdater);
return; return;
} }
@@ -122,8 +122,9 @@ export const inputHandler = (
onScrollDown: onScrollDownInput, onScrollDown: onScrollDownInput,
}: InputHandlerEvents, }: InputHandlerEvents,
postInputUpdater?: Variable<boolean>, postInputUpdater?: Variable<boolean>,
customScrollThreshold?: number,
): void => { ): void => {
const sanitizeInput = (input?: Variable<string> | Variable<string>): string => { const sanitizeInput = (input?: Variable<string>): string => {
if (input === undefined) { if (input === undefined) {
return ''; return '';
} }
@@ -131,7 +132,7 @@ export const inputHandler = (
}; };
const updateHandlers = (): UpdateHandlers => { const updateHandlers = (): UpdateHandlers => {
const interval = scrollSpeed.get(); const interval = customScrollThreshold ?? scrollSpeed.get();
const throttledHandler = throttledScrollHandler(interval); const throttledHandler = throttledScrollHandler(interval);
const disconnectPrimaryClick = onPrimaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => { const disconnectPrimaryClick = onPrimaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => {

View File

@@ -16,7 +16,7 @@ export const themePages = [
'System Tray', 'System Tray',
'Volume Menu', 'Volume Menu',
'Power Menu', 'Power Menu',
'Custom Modules', 'Basic Modules',
] as const; ] as const;
export const configPages = [ export const configPages = [
@@ -28,7 +28,7 @@ export const configPages = [
'Volume', 'Volume',
'Clock Menu', 'Clock Menu',
'Dashboard Menu', 'Dashboard Menu',
'Custom Modules', 'Basic Modules',
'Power Menu', 'Power Menu',
] as const; ] as const;

View File

@@ -30,7 +30,7 @@ export const BarGeneral = (): JSX.Element => {
title="Config" title="Config"
subtitle="WARNING: Importing a configuration will replace your current configuration settings." subtitle="WARNING: Importing a configuration will replace your current configuration settings."
type="config_import" type="config_import"
exportData={{ filePath: CONFIG, themeOnly: false }} exportData={{ filePath: CONFIG_FILE, themeOnly: false }}
/> />
<Option <Option
opt={options.hyprpanel.restartAgs} opt={options.hyprpanel.restartAgs}

View File

@@ -22,7 +22,7 @@ export const MenuTheme = (): JSX.Element => {
title="Theme" title="Theme"
subtitle="WARNING: Importing a theme will replace your current theme color settings." subtitle="WARNING: Importing a theme will replace your current theme color settings."
type="config_import" type="config_import"
exportData={{ filePath: CONFIG, themeOnly: true }} exportData={{ filePath: CONFIG_FILE, themeOnly: true }}
/> />
<Option <Option
opt={options.theme.bar.menus.monochrome} opt={options.theme.bar.menus.monochrome}

View File

@@ -272,7 +272,7 @@ export const importFiles = (themeOnly: boolean = false): void => {
iconName: icons.ui.info, 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); const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null);
@@ -289,7 +289,7 @@ export const importFiles = (themeOnly: boolean = false): void => {
: filterConfigForNonTheme(importedConfig); : filterConfigForNonTheme(importedConfig);
optionsConfig = { ...optionsConfig, ...filteredConfig }; optionsConfig = { ...optionsConfig, ...filteredConfig };
saveConfigToFile(optionsConfig, CONFIG); saveConfigToFile(optionsConfig, CONFIG_FILE);
} }
dialog.destroy(); dialog.destroy();
bash(restartCommand.get()); bash(restartCommand.get());

View File

@@ -14,7 +14,7 @@ globalThis.useTheme = (filePath: string): void => {
return; return;
} }
const optionsConfigFile = Gio.File.new_for_path(CONFIG); const optionsConfigFile = Gio.File.new_for_path(CONFIG_FILE);
const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null); const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null);
@@ -27,7 +27,7 @@ globalThis.useTheme = (filePath: string): void => {
const filteredConfig = filterConfigForThemeOnly(importedConfig); const filteredConfig = filterConfigForThemeOnly(importedConfig);
optionsConfig = { ...optionsConfig, ...filteredConfig }; optionsConfig = { ...optionsConfig, ...filteredConfig };
saveConfigToFile(optionsConfig, CONFIG); saveConfigToFile(optionsConfig, CONFIG_FILE);
bash(restartCommand.get()); bash(restartCommand.get());
} catch (error) { } catch (error) {
errorHandler(error); errorHandler(error);

View File

@@ -100,7 +100,7 @@ export class Opt<T = unknown> extends Variable<T> {
super.set(value); super.set(value);
if (writeDisk) { if (writeDisk) {
const raw = readFile(CONFIG); const raw = readFile(CONFIG_FILE);
let config: Record<string, unknown> = {}; let config: Record<string, unknown> = {};
if (raw && raw.trim() !== '') { if (raw && raw.trim() !== '') {
try { try {
@@ -119,7 +119,7 @@ export class Opt<T = unknown> extends Variable<T> {
} }
} }
config[this._id] = value; config[this._id] = value;
writeFile(CONFIG, JSON.stringify(config, null, 2)); writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
} }
}; };
@@ -225,9 +225,9 @@ function getOptions(optionsObj: Record<string, unknown>, path = '', arr: Opt[] =
* @returns The original object extended with additional methods for handling options. * @returns The original object extended with additional methods for handling options.
*/ */
export function mkOptions<T extends object>(optionsObj: T): T & MkOptionsResult { export function mkOptions<T extends object>(optionsObj: T): T & MkOptionsResult {
ensureDirectory(CONFIG.split('/').slice(0, -1).join('/')); ensureDirectory(CONFIG_FILE.split('/').slice(0, -1).join('/'));
const rawConfig = readFile(CONFIG); const rawConfig = readFile(CONFIG_FILE);
let config: Record<string, unknown> = {}; let config: Record<string, unknown> = {};
if (rawConfig && rawConfig.trim() !== '') { if (rawConfig && rawConfig.trim() !== '') {
@@ -254,7 +254,7 @@ export function mkOptions<T extends object>(optionsObj: T): T & MkOptionsResult
// the config menu // the config menu
const debounceTimeMs = 200; const debounceTimeMs = 200;
let lastEventTime = Date.now(); let lastEventTime = Date.now();
monitorFile(CONFIG, () => { monitorFile(CONFIG_FILE, () => {
if (Date.now() - lastEventTime < debounceTimeMs) { if (Date.now() - lastEventTime < debounceTimeMs) {
return; return;
} }
@@ -262,7 +262,7 @@ export function mkOptions<T extends object>(optionsObj: T): T & MkOptionsResult
let newConfig: Record<string, unknown> = {}; let newConfig: Record<string, unknown> = {};
const rawConfig = readFile(CONFIG); const rawConfig = readFile(CONFIG_FILE);
if (rawConfig && rawConfig.trim() !== '') { if (rawConfig && rawConfig.trim() !== '') {
try { try {
newConfig = JSON.parse(rawConfig) as Record<string, unknown>; newConfig = JSON.parse(rawConfig) as Record<string, unknown>;

View File

@@ -9,30 +9,30 @@ const { layouts } = options.bar;
* A class that manages the polling lifecycle, including interval management and execution state. * A class that manages the polling lifecycle, including interval management and execution state.
*/ */
export class Poller { export class Poller {
private intervalInstance: AstalIO.Time | null = null; private _intervalInstance: AstalIO.Time | null = null;
private isExecuting: boolean = false; private _isExecuting: boolean = false;
private pollingFunction: () => Promise<void>; private _pollingFunction: () => Promise<void>;
/** /**
* Creates an instance of Poller. * Creates an instance of Poller.
* @param pollingInterval - The interval at which polling occurs. * @param _pollingInterval - The interval at which polling occurs.
* @param trackers - An array of trackers to monitor. * @param _trackers - An array of trackers to monitor.
* @param pollingFunction - The function to execute during each poll. * @param pollingFunction - The function to execute during each poll.
*/ */
constructor( constructor(
private pollingInterval: Bind, private _pollingInterval: Bind,
private trackers: Bind[], private _trackers: Bind[],
pollingFunction: () => Promise<void>, pollingFunction: () => Promise<void>,
) { ) {
this.pollingFunction = pollingFunction; this._pollingFunction = pollingFunction;
} }
/** /**
* Starts the polling process by setting up the interval. * Starts the polling process by setting up the interval.
*/ */
public start(): void { public start(): void {
Variable.derive([this.pollingInterval, ...this.trackers], (intervalMs: number) => { Variable.derive([this._pollingInterval, ...this._trackers], (intervalMs: number) => {
this.executePolling(intervalMs); this._executePolling(intervalMs);
})(); })();
} }
@@ -40,9 +40,9 @@ export class Poller {
* Stops the polling process and cleans up resources. * Stops the polling process and cleans up resources.
*/ */
public stop(): void { public stop(): void {
if (this.intervalInstance !== null) { if (this._intervalInstance !== null) {
this.intervalInstance.cancel(); this._intervalInstance.cancel();
this.intervalInstance = null; this._intervalInstance = null;
} }
} }
@@ -83,25 +83,47 @@ export class Poller {
* *
* @param intervalMs - The polling interval in milliseconds. * @param intervalMs - The polling interval in milliseconds.
*/ */
private executePolling(intervalMs: number): void { private _executePolling(intervalMs: number): void {
if (this.intervalInstance !== null) { if (this._intervalInstance !== null) {
this.intervalInstance.cancel(); this._intervalInstance.cancel();
} }
this.intervalInstance = interval(intervalMs, async () => { if (intervalMs === 0) {
if (this.isExecuting) { this._executeSinglePoll();
return; return;
} }
this.isExecuting = true; this._intervalInstance = interval(intervalMs, () => this._executePollingCycle());
}
try { /**
await this.pollingFunction(); * Executes a single polling operation synchronously.
} catch (error) { */
console.error('Error during polling execution:', error); private _executeSinglePoll(): void {
} finally { try {
this.isExecuting = false; this._pollingFunction();
} } catch (error) {
}); console.error('Error during polling execution:', error);
}
}
/**
* Executes an asynchronous polling cycle with execution guard.
* Ensures only one polling cycle runs at a time using the isExecuting flag.
*/
private async _executePollingCycle(): Promise<void> {
if (this._isExecuting) {
return;
}
this._isExecuting = true;
try {
await this._pollingFunction();
} catch (error) {
console.error('Error during polling execution:', error);
} finally {
this._isExecuting = false;
}
} }
} }

View File

@@ -3,7 +3,8 @@ import { Gio } from 'astal/file';
import { GLib } from 'astal/gobject'; import { GLib } from 'astal/gobject';
declare global { declare global {
const CONFIG: string; const CONFIG_DIR: string;
const CONFIG_FILE: string;
const TMP: string; const TMP: string;
const USER: string; const USER: string;
const SRC_DIR: string; const SRC_DIR: string;
@@ -15,6 +16,20 @@ export function ensureDirectory(path: string): void {
} }
} }
export function ensureJsonFile(path: string): void {
const file = Gio.File.new_for_path(path);
const parent = file.get_parent();
if (parent && !parent.query_exists(null)) {
parent.make_directory_with_parents(null);
}
if (!file.query_exists(null)) {
const stream = file.create(Gio.FileCreateFlags.NONE, null);
stream.write_all('{}', null);
}
}
export function ensureFile(path: string): void { export function ensureFile(path: string): void {
const file = Gio.File.new_for_path(path); const file = Gio.File.new_for_path(path);
const parent = file.get_parent(); const parent = file.get_parent();
@@ -31,12 +46,15 @@ export function ensureFile(path: string): void {
const dataDir = typeof DATADIR !== 'undefined' ? DATADIR : SRC; const dataDir = typeof DATADIR !== 'undefined' ? DATADIR : SRC;
Object.assign(globalThis, { Object.assign(globalThis, {
CONFIG: `${GLib.get_user_config_dir()}/hyprpanel/config.json`, CONFIG_DIR: `${GLib.get_user_config_dir()}/hyprpanel`,
CONFIG_FILE: `${GLib.get_user_config_dir()}/hyprpanel/config.json`,
TMP: `${GLib.get_tmp_dir()}/hyprpanel`, TMP: `${GLib.get_tmp_dir()}/hyprpanel`,
USER: GLib.get_user_name(), USER: GLib.get_user_name(),
SRC_DIR: dataDir, SRC_DIR: dataDir,
}); });
ensureDirectory(TMP); ensureDirectory(TMP);
ensureFile(CONFIG); ensureFile(CONFIG_FILE);
ensureJsonFile(`${CONFIG_DIR}/modules.json`);
ensureFile(`${CONFIG_DIR}/modules.scss`);
App.add_icons(`${SRC_DIR}/assets`); App.add_icons(`${SRC_DIR}/assets`);

View File

@@ -1,10 +1,12 @@
import { Binding, Connectable } from 'types/service';
import { Variable } from 'types/variable'; import { Variable } from 'types/variable';
import Box from 'types/widgets/box'; import Box from 'types/widgets/box';
import Button, { ButtonProps } from 'types/widgets/button'; import Button, { ButtonProps } from 'types/widgets/button';
import Label from 'types/widgets/label'; import Label from 'types/widgets/label';
import { Attribute, Child } from './widget'; import { Attribute, Child } from './widget';
import { Widget } from 'astal/gtk3'; import { Widget } from 'astal/gtk3';
import { Binding } from 'astal';
import { Connectable } from 'astal/binding';
import { CustomBarModuleStyle } from 'src/components/bar/custom_modules/types';
export type BarBoxChild = { export type BarBoxChild = {
component: JSX.Element; component: JSX.Element;
@@ -25,6 +27,7 @@ export type BarModule = {
textIcon?: string | Binding<string>; textIcon?: string | Binding<string>;
useTextIcon?: Binding<boolean>; useTextIcon?: Binding<boolean>;
label?: string | Binding<string>; label?: string | Binding<string>;
truncationSize?: Binding<number>;
labelHook?: LabelHook; labelHook?: LabelHook;
boundLabel?: string; boundLabel?: string;
tooltipText?: string | Binding<string>; tooltipText?: string | Binding<string>;

View File

@@ -9,3 +9,5 @@ export type ThrottleFn = (
) => void; ) => void;
export type ThrottleFnCallback = ((output: string) => void) | undefined; export type ThrottleFnCallback = ((output: string) => void) | undefined;
export type Primitive = string | number | boolean | symbol | null | undefined | bigint;

View File

@@ -12,9 +12,20 @@ import { Astal, Gdk, Gtk } from 'astal/gtk3';
import AstalApps from 'gi://AstalApps?version=0.1'; import AstalApps from 'gi://AstalApps?version=0.1';
import { exec, execAsync } from 'astal/process'; import { exec, execAsync } from 'astal/process';
import AstalNotifd from 'gi://AstalNotifd?version=0.1'; import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import { Primitive } from './types/utils';
const notifdService = AstalNotifd.get_default(); const notifdService = AstalNotifd.get_default();
/**
* Checks if a value is a primitive type.
*
* @param value - The value to check
* @returns True if the value is a primitive (null, undefined, string, number, boolean, symbol, or bigint)
*/
export function isPrimitive(value: unknown): value is Primitive {
return value === null || (typeof value !== 'object' && typeof value !== 'function');
}
/** /**
* Handles errors by throwing a new Error with a message. * Handles errors by throwing a new Error with a message.
* *
@@ -102,7 +113,8 @@ export function icon(name: string | null, fallback = icons.missing): string {
if (lookUpIcon(icon)) return icon; if (lookUpIcon(icon)) return icon;
console.log(`no icon substitute "${icon}" for "${name}", fallback: "${fallback}"`); console.log(`No icon substitute "${icon}" for "${name}", fallback: "${fallback}"`);
return fallback; return fallback;
} }
@@ -154,9 +166,10 @@ export async function sh(cmd: string | string[]): Promise<string> {
* *
* @returns An array of JSX elements, one for each monitor. * @returns An array of JSX elements, one for each monitor.
*/ */
export function forMonitors(widget: (monitor: number) => JSX.Element): JSX.Element[] { export async function forMonitors(widget: (monitor: number) => Promise<JSX.Element>): Promise<JSX.Element[]> {
const n = Gdk.Display.get_default()?.get_n_monitors() || 1; const n = Gdk.Display.get_default()?.get_n_monitors() || 1;
return range(n, 0).flatMap(widget);
return Promise.all(range(n, 0).map(widget));
} }
/** /**

View File

@@ -91,6 +91,7 @@ export const resetCss = async (): Promise<void> => {
const css = `${TMP}/main.css`; const css = `${TMP}/main.css`;
const scss = `${TMP}/entry.scss`; const scss = `${TMP}/entry.scss`;
const localScss = `${SRC_DIR}/src/scss/main.scss`; const localScss = `${SRC_DIR}/src/scss/main.scss`;
const moduleScss = `${CONFIG_DIR}/modules.scss`;
const themeVariables = variables; const themeVariables = variables;
const integratedVariables = themeVariables; const integratedVariables = themeVariables;
@@ -102,6 +103,9 @@ export const resetCss = async (): Promise<void> => {
let mainScss = readFile(localScss); let mainScss = readFile(localScss);
mainScss = `${imports}\n${mainScss}`; mainScss = `${imports}\n${mainScss}`;
const moduleScssFile = readFile(moduleScss);
mainScss = `${mainScss}\n${moduleScssFile}`;
writeFile(scss, mainScss); writeFile(scss, mainScss);
await bash(`sass --load-path=${SRC_DIR}/src/scss ${scss} ${css}`); await bash(`sass --load-path=${SRC_DIR}/src/scss ${scss} ${css}`);

View File

@@ -32,22 +32,33 @@
} }
} }
$style-module-defaults: (
'label-color': $bar-buttons-text,
'icon-color': $bar-buttons-icon,
'icon-background': $bar-buttons-icon_background,
'label-background': $bar-buttons-background,
'border-enabled': false,
'border-color': $text,
'icon-size': 1em,
'inner-spacing': 0.5em,
);
/* /*
* ################################# * #################################
* # Styling Function # * # Styling Function #
* ################################# * #################################
*/ */
@mixin styleModule( @mixin styleModule($class, $config: ()) {
$class, $config: map-merge($style-module-defaults, $config);
$textColor, $text-color: map-get($config, 'text-color');
$iconColor, $icon-color: map-get($config, 'icon-color');
$iconBackground, $icon-background: map-get($config, 'icon-background');
$labelBackground, $label-background: map-get($config, 'label-background');
$spacing, $spacing: map-get($config, 'inner-spacing');
$borderEnabled, $border-enabled: map-get($config, 'border-enabled');
$borderColor, $border-color: map-get($config, 'border-color');
$fontSize: 1em $icon-size: map-get($config, 'icon-size');
) {
$bar-button-background-opacity-ratio: $bar-buttons-background_opacity * 0.01; $bar-button-background-opacity-ratio: $bar-buttons-background_opacity * 0.01;
$transparency-value: 1 - $bar-button-background-opacity-ratio; $transparency-value: 1 - $bar-button-background-opacity-ratio;
@@ -56,12 +67,12 @@
.bar_item_box_visible { .bar_item_box_visible {
&.#{$class} { &.#{$class} {
background: transparentize( background: transparentize(
if($bar-buttons-monochrome, $bar-buttons-background, $labelBackground), if($bar-buttons-monochrome, $bar-buttons-background, $label-background),
$transparency-value $transparency-value
); );
border: if( border: if(
$borderEnabled or $bar-buttons-enableBorders, $border-enabled or $bar-buttons-enableBorders,
$bar-buttons-borderSize solid if($bar-buttons-monochrome, $bar-buttons-borderColor, $borderColor), $bar-buttons-borderSize solid if($bar-buttons-monochrome, $bar-buttons-borderColor, $border-color),
0em 0em
); );
@@ -76,35 +87,35 @@
} }
.module-label.#{$class} { .module-label.#{$class} {
color: if($bar-buttons-monochrome, $bar-buttons-text, $textColor); color: if($bar-buttons-monochrome, $bar-buttons-text, $text-color);
margin-left: $spacing; margin-left: $spacing;
border-radius: $bar-buttons-radius; border-radius: $bar-buttons-radius;
} }
.module-icon.#{$class} { .module-icon.#{$class} {
color: if($bar-buttons-monochrome, $bar-buttons-icon, $iconColor); color: if($bar-buttons-monochrome, $bar-buttons-icon, $icon-color);
font-size: if($fontSize, $fontSize, 1em); font-size: if($icon-size, $icon-size, 1em);
} }
.style2 { .style2 {
.module-icon.#{$class} { .module-icon.#{$class} {
background: if($bar-buttons-monochrome, $bar-buttons-icon_background, $iconBackground); background: if($bar-buttons-monochrome, $bar-buttons-icon_background, $icon-background);
padding-right: $spacing; padding-right: $spacing;
color: if($bar-buttons-monochrome, $bar-buttons-icon, $iconColor); color: if($bar-buttons-monochrome, $bar-buttons-icon, $icon-color);
border-top-left-radius: if( border-top-left-radius: if(
$borderEnabled or $bar-buttons-enableBorders, $border-enabled or $bar-buttons-enableBorders,
$bar-buttons-radius * $bar-buttons-innerRadiusMultiplier, $bar-buttons-radius * $bar-buttons-innerRadiusMultiplier,
$bar-buttons-radius $bar-buttons-radius
); );
border-bottom-left-radius: if( border-bottom-left-radius: if(
$borderEnabled or $bar-buttons-enableBorders, $border-enabled or $bar-buttons-enableBorders,
$bar-buttons-radius * $bar-buttons-innerRadiusMultiplier, $bar-buttons-radius * $bar-buttons-innerRadiusMultiplier,
$bar-buttons-radius $bar-buttons-radius
); );
} }
.module-label.#{$class} { .module-label.#{$class} {
background: transparentize($labelBackground, $transparency-value); background: transparentize($label-background, $transparency-value);
padding-left: $spacing * 1.5; padding-left: $spacing * 1.5;
margin-left: 0em; margin-left: 0em;
border-top-left-radius: 0em; border-top-left-radius: 0em;
@@ -113,12 +124,12 @@
&.no-label.#{$class} { &.no-label.#{$class} {
.module-icon { .module-icon {
border-top-right-radius: if( border-top-right-radius: if(
$borderEnabled or $bar-buttons-enableBorders, $border-enabled or $bar-buttons-enableBorders,
$bar-buttons-radius * $bar-buttons-innerRadiusMultiplier, $bar-buttons-radius * $bar-buttons-innerRadiusMultiplier,
$bar-buttons-radius $bar-buttons-radius
); );
border-bottom-right-radius: if( border-bottom-right-radius: if(
$borderEnabled or $bar-buttons-enableBorders, $border-enabled or $bar-buttons-enableBorders,
$bar-buttons-radius * $bar-buttons-innerRadiusMultiplier, $bar-buttons-radius * $bar-buttons-innerRadiusMultiplier,
$bar-buttons-radius $bar-buttons-radius
); );
@@ -133,25 +144,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'mic', 'mic',
// label color (
$bar-buttons-modules-microphone-text, 'text-color': $bar-buttons-modules-microphone-text,
// icon color 'icon-color': $bar-buttons-modules-microphone-icon,
$bar-buttons-modules-microphone-icon, 'icon-background': $bar-buttons-modules-microphone-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-microphone-background,
$bar-buttons-modules-microphone-icon_background, 'inner-spacing': $bar-buttons-modules-microphone-spacing,
// label background 'border-enabled': $bar-buttons-modules-microphone-enableBorder,
$bar-buttons-modules-microphone-background, 'border-color': $bar-buttons-modules-microphone-border,
// inner spacing 'icon-size': 1.3em,
$bar-buttons-modules-microphone-spacing, )
//
// if border enabled
$bar-buttons-modules-microphone-enableBorder,
// border color
$bar-buttons-modules-microphone-border,
1.3em
); );
/* /*
@@ -160,24 +163,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'ram', 'ram',
// label color (
$bar-buttons-modules-ram-text, 'text-color': $bar-buttons-modules-ram-text,
// icon color 'icon-color': $bar-buttons-modules-ram-icon,
$bar-buttons-modules-ram-icon, 'icon-background': $bar-buttons-modules-ram-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-ram-background,
$bar-buttons-modules-ram-icon_background, 'inner-spacing': $bar-buttons-modules-ram-spacing,
// label background 'border-enabled': $bar-buttons-modules-ram-enableBorder,
$bar-buttons-modules-ram-background, 'border-color': $bar-buttons-modules-ram-border,
// inner spacing 'icon-size': 1em,
$bar-buttons-modules-ram-spacing, )
//
// if border enabled
$bar-buttons-modules-ram-enableBorder,
// border color
$bar-buttons-modules-ram-border
); );
/* /*
@@ -186,25 +182,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'cpu', 'cpu',
// label color (
$bar-buttons-modules-cpu-text, 'text-color': $bar-buttons-modules-cpu-text,
// icon color 'icon-color': $bar-buttons-modules-cpu-icon,
$bar-buttons-modules-cpu-icon, 'icon-background': $bar-buttons-modules-cpu-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-cpu-background,
$bar-buttons-modules-cpu-icon_background, 'inner-spacing': $bar-buttons-modules-cpu-spacing,
// label background 'border-enabled': $bar-buttons-modules-cpu-enableBorder,
$bar-buttons-modules-cpu-background, 'border-color': $bar-buttons-modules-cpu-border,
// inner spacing 'icon-size': 1.05em,
$bar-buttons-modules-cpu-spacing, )
// if border enabled
$bar-buttons-modules-cpu-enableBorder,
// border color
$bar-buttons-modules-cpu-border,
// custom font size
1.05em //
); );
/* /*
@@ -213,25 +201,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'cpu-temp', 'cpu-temp',
// label color (
$bar-buttons-modules-cpuTemp-text, 'text-color': $bar-buttons-modules-cpuTemp-text,
// icon color 'icon-color': $bar-buttons-modules-cpuTemp-icon,
$bar-buttons-modules-cpuTemp-icon, 'icon-background': $bar-buttons-modules-cpuTemp-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-cpuTemp-background,
$bar-buttons-modules-cpuTemp-icon_background, 'inner-spacing': $bar-buttons-modules-cpuTemp-spacing,
// label background 'border-enabled': $bar-buttons-modules-cpuTemp-enableBorder,
$bar-buttons-modules-cpuTemp-background, 'border-color': $bar-buttons-modules-cpuTemp-border,
// inner spacing 'icon-size': 1.05em,
$bar-buttons-modules-cpuTemp-spacing, )
// if border enabled
$bar-buttons-modules-cpuTemp-enableBorder,
// border color
$bar-buttons-modules-cpuTemp-border,
// custom font size
1.05em //
); );
/* /*
@@ -240,25 +220,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'storage', 'storage',
// label color (
$bar-buttons-modules-storage-text, 'text-color': $bar-buttons-modules-storage-text,
// icon color 'icon-color': $bar-buttons-modules-storage-icon,
$bar-buttons-modules-storage-icon, 'icon-background': $bar-buttons-modules-storage-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-storage-background,
$bar-buttons-modules-storage-icon_background, 'inner-spacing': $bar-buttons-modules-storage-spacing,
// label background 'border-enabled': $bar-buttons-modules-storage-enableBorder,
$bar-buttons-modules-storage-background, 'border-color': $bar-buttons-modules-storage-border,
// inner spacing 'icon-size': 1.3em,
$bar-buttons-modules-storage-spacing, )
// if border enabled
$bar-buttons-modules-storage-enableBorder,
// border color
$bar-buttons-modules-storage-border,
// custom font size
1.3em //
); );
/* /*
@@ -267,25 +239,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'netstat', 'netstat',
// label color (
$bar-buttons-modules-netstat-text, 'text-color': $bar-buttons-modules-netstat-text,
// icon color 'icon-color': $bar-buttons-modules-netstat-icon,
$bar-buttons-modules-netstat-icon, 'icon-background': $bar-buttons-modules-netstat-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-netstat-background,
$bar-buttons-modules-netstat-icon_background, 'inner-spacing': $bar-buttons-modules-netstat-spacing,
// label background 'border-enabled': $bar-buttons-modules-netstat-enableBorder,
$bar-buttons-modules-netstat-background, 'border-color': $bar-buttons-modules-netstat-border,
// inner spacing 'icon-size': 1.2em,
$bar-buttons-modules-netstat-spacing, )
// if border enabled
$bar-buttons-modules-netstat-enableBorder,
// border color
$bar-buttons-modules-netstat-border,
// custom font size
1.2em //
); );
/* /*
@@ -294,25 +258,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'kblayout', 'kblayout',
// label color (
$bar-buttons-modules-kbLayout-text, 'text-color': $bar-buttons-modules-kbLayout-text,
// icon color 'icon-color': $bar-buttons-modules-kbLayout-icon,
$bar-buttons-modules-kbLayout-icon, 'icon-background': $bar-buttons-modules-kbLayout-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-kbLayout-background,
$bar-buttons-modules-kbLayout-icon_background, 'inner-spacing': $bar-buttons-modules-kbLayout-spacing,
// label background 'border-enabled': $bar-buttons-modules-kbLayout-enableBorder,
$bar-buttons-modules-kbLayout-background, 'border-color': $bar-buttons-modules-kbLayout-border,
// inner spacing 'icon-size': 1.2em,
$bar-buttons-modules-kbLayout-spacing, )
// if border enabled
$bar-buttons-modules-kbLayout-enableBorder,
// border color
$bar-buttons-modules-kbLayout-border,
// custom font size
1.2em //
); );
/* /*
@@ -321,25 +277,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'updates', 'updates',
// label color (
$bar-buttons-modules-updates-text, 'text-color': $bar-buttons-modules-updates-text,
// icon color 'icon-color': $bar-buttons-modules-updates-icon,
$bar-buttons-modules-updates-icon, 'icon-background': $bar-buttons-modules-updates-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-updates-background,
$bar-buttons-modules-updates-icon_background, 'inner-spacing': $bar-buttons-modules-updates-spacing,
// label background 'border-enabled': $bar-buttons-modules-updates-enableBorder,
$bar-buttons-modules-updates-background, 'border-color': $bar-buttons-modules-updates-border,
// inner spacing 'icon-size': 1.2em,
$bar-buttons-modules-updates-spacing, )
// if border enabled
$bar-buttons-modules-updates-enableBorder,
// border color
$bar-buttons-modules-updates-border,
// custom font size
1.2em //
); );
/* /*
@@ -348,25 +296,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'submap', 'submap',
// label color (
$bar-buttons-modules-submap-text, 'text-color': $bar-buttons-modules-submap-text,
// icon color 'icon-color': $bar-buttons-modules-submap-icon,
$bar-buttons-modules-submap-icon, 'icon-background': $bar-buttons-modules-submap-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-submap-background,
$bar-buttons-modules-submap-icon_background, 'inner-spacing': $bar-buttons-modules-submap-spacing,
// label background 'border-enabled': $bar-buttons-modules-submap-enableBorder,
$bar-buttons-modules-submap-background, 'border-color': $bar-buttons-modules-submap-border,
// inner spacing 'icon-size': 1.2em,
$bar-buttons-modules-submap-spacing, )
// if border enabled
$bar-buttons-modules-submap-enableBorder,
// border color
$bar-buttons-modules-submap-border,
// custom font size
1.2em //
); );
/* /*
@@ -375,25 +315,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'weather-custom', 'weather-custom',
// label color (
$bar-buttons-modules-weather-text, 'text-color': $bar-buttons-modules-weather-text,
// icon color 'icon-color': $bar-buttons-modules-weather-icon,
$bar-buttons-modules-weather-icon, 'icon-background': $bar-buttons-modules-weather-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-weather-background,
$bar-buttons-modules-weather-icon_background, 'inner-spacing': $bar-buttons-modules-weather-spacing,
// label background 'border-enabled': $bar-buttons-modules-weather-enableBorder,
$bar-buttons-modules-weather-background, 'border-color': $bar-buttons-modules-weather-border,
// inner spacing 'icon-size': 1.2em,
$bar-buttons-modules-weather-spacing, )
// if border enabled
$bar-buttons-modules-weather-enableBorder,
// border color
$bar-buttons-modules-weather-border,
// custom font size
1.2em //
); );
/* /*
@@ -402,25 +334,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'powermodule', 'powermodule',
// label color (
$red, 'text-color': $red,
// icon color 'icon-color': $bar-buttons-modules-power-icon,
$bar-buttons-modules-power-icon, 'icon-background': $bar-buttons-modules-power-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-power-background,
$bar-buttons-modules-power-icon_background, 'inner-spacing': $bar-buttons-modules-power-spacing,
// label background 'border-enabled': $bar-buttons-modules-power-enableBorder,
$bar-buttons-modules-power-background, 'border-color': $bar-buttons-modules-power-border,
// inner spacing 'icon-size': 1.3em,
$bar-buttons-modules-power-spacing, )
// if border enabled
$bar-buttons-modules-power-enableBorder,
// border color
$bar-buttons-modules-power-border,
// custom font size
1.3em //
); );
/* /*
@@ -429,25 +353,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'hyprsunset', 'hyprsunset',
// label color (
$bar-buttons-modules-hyprsunset-text, 'text-color': $bar-buttons-modules-hyprsunset-text,
// icon color 'icon-color': $bar-buttons-modules-hyprsunset-icon,
$bar-buttons-modules-hyprsunset-icon, 'icon-background': $bar-buttons-modules-hyprsunset-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-hyprsunset-background,
$bar-buttons-modules-hyprsunset-icon_background, 'inner-spacing': $bar-buttons-modules-hyprsunset-spacing,
// label background 'border-enabled': $bar-buttons-modules-hyprsunset-enableBorder,
$bar-buttons-modules-hyprsunset-background, 'border-color': $bar-buttons-modules-hyprsunset-border,
// inner spacing 'icon-size': 1.3em,
$bar-buttons-modules-hyprsunset-spacing, )
// if border enabled
$bar-buttons-modules-hyprsunset-enableBorder,
// border color
$bar-buttons-modules-hyprsunset-border,
// custom font size
1.3em //
); );
/* /*
@@ -456,25 +372,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'hypridle', 'hypridle',
// label color (
$bar-buttons-modules-hypridle-text, 'text-color': $bar-buttons-modules-hypridle-text,
// icon color 'icon-color': $bar-buttons-modules-hypridle-icon,
$bar-buttons-modules-hypridle-icon, 'icon-background': $bar-buttons-modules-hypridle-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-hypridle-background,
$bar-buttons-modules-hypridle-icon_background, 'inner-spacing': $bar-buttons-modules-hypridle-spacing,
// label background 'border-enabled': $bar-buttons-modules-hypridle-enableBorder,
$bar-buttons-modules-hypridle-background, 'border-color': $bar-buttons-modules-hypridle-border,
// inner spacing 'icon-size': 1.075em,
$bar-buttons-modules-hypridle-spacing, )
// if border enabled
$bar-buttons-modules-hypridle-enableBorder,
// border color
$bar-buttons-modules-hypridle-border,
// custom font size
1.075em //
); );
/* /*
@@ -483,25 +391,17 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'cava', 'cava',
// label color (
$bar-buttons-modules-cava-text, 'text-color': $bar-buttons-modules-cava-text,
// icon color 'icon-color': $bar-buttons-modules-cava-icon,
$bar-buttons-modules-cava-icon, 'icon-background': $bar-buttons-modules-cava-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-cava-background,
$bar-buttons-modules-cava-icon_background, 'inner-spacing': $bar-buttons-modules-cava-spacing,
// label background 'border-enabled': $bar-buttons-modules-cava-enableBorder,
$bar-buttons-modules-cava-background, 'border-color': $bar-buttons-modules-cava-border,
// inner spacing 'icon-size': 1.2em,
$bar-buttons-modules-cava-spacing, )
// if border enabled
$bar-buttons-modules-cava-enableBorder,
// border color
$bar-buttons-modules-cava-border,
// custom font size
1.2em //
); );
/* /*
@@ -510,23 +410,15 @@
* ################################# * #################################
*/ */
@include styleModule( @include styleModule(
//
// class name
'worldclock', 'worldclock',
// label color (
$bar-buttons-modules-worldclock-text, 'text-color': $bar-buttons-modules-worldclock-text,
// icon color 'icon-color': $bar-buttons-modules-worldclock-icon,
$bar-buttons-modules-worldclock-icon, 'icon-background': $bar-buttons-modules-worldclock-icon_background,
// icon background if split style is used 'label-background': $bar-buttons-modules-worldclock-background,
$bar-buttons-modules-worldclock-icon_background, 'inner-spacing': $bar-buttons-modules-worldclock-spacing,
// label background 'border-enabled': $bar-buttons-modules-worldclock-enableBorder,
$bar-buttons-modules-worldclock-background, 'border-color': $bar-buttons-modules-worldclock-border,
// inner spacing 'icon-size': 1.2em,
$bar-buttons-modules-worldclock-spacing, )
// if border enabled
$bar-buttons-modules-worldclock-enableBorder,
// border color
$bar-buttons-modules-worldclock-border,
// custom font size
1.2em //
); );

View File

@@ -12,6 +12,7 @@ export const initializeHotReload = async (): Promise<void> => {
`${SRC_DIR}/src/scss/style/settings`, `${SRC_DIR}/src/scss/style/settings`,
`${SRC_DIR}/src/scss/style/colors.scss`, `${SRC_DIR}/src/scss/style/colors.scss`,
`${SRC_DIR}/src/scss/style/highlights.scss`, `${SRC_DIR}/src/scss/style/highlights.scss`,
`${CONFIG_DIR}/modules.scss`,
]; ];
monitorList.forEach((file) => monitorFile(file, resetCss)); monitorList.forEach((file) => monitorFile(file, resetCss));