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

@@ -100,7 +100,7 @@ export class Opt<T = unknown> extends Variable<T> {
super.set(value);
if (writeDisk) {
const raw = readFile(CONFIG);
const raw = readFile(CONFIG_FILE);
let config: Record<string, unknown> = {};
if (raw && raw.trim() !== '') {
try {
@@ -119,7 +119,7 @@ export class Opt<T = unknown> extends Variable<T> {
}
}
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.
*/
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> = {};
if (rawConfig && rawConfig.trim() !== '') {
@@ -254,7 +254,7 @@ export function mkOptions<T extends object>(optionsObj: T): T & MkOptionsResult
// the config menu
const debounceTimeMs = 200;
let lastEventTime = Date.now();
monitorFile(CONFIG, () => {
monitorFile(CONFIG_FILE, () => {
if (Date.now() - lastEventTime < debounceTimeMs) {
return;
}
@@ -262,7 +262,7 @@ export function mkOptions<T extends object>(optionsObj: T): T & MkOptionsResult
let newConfig: Record<string, unknown> = {};
const rawConfig = readFile(CONFIG);
const rawConfig = readFile(CONFIG_FILE);
if (rawConfig && rawConfig.trim() !== '') {
try {
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.
*/
export class Poller {
private intervalInstance: AstalIO.Time | null = null;
private isExecuting: boolean = false;
private pollingFunction: () => Promise<void>;
private _intervalInstance: AstalIO.Time | null = null;
private _isExecuting: boolean = false;
private _pollingFunction: () => Promise<void>;
/**
* Creates an instance of Poller.
* @param pollingInterval - The interval at which polling occurs.
* @param trackers - An array of trackers to monitor.
* @param _pollingInterval - The interval at which polling occurs.
* @param _trackers - An array of trackers to monitor.
* @param pollingFunction - The function to execute during each poll.
*/
constructor(
private pollingInterval: Bind,
private trackers: Bind[],
private _pollingInterval: Bind,
private _trackers: Bind[],
pollingFunction: () => Promise<void>,
) {
this.pollingFunction = pollingFunction;
this._pollingFunction = pollingFunction;
}
/**
* Starts the polling process by setting up the interval.
*/
public start(): void {
Variable.derive([this.pollingInterval, ...this.trackers], (intervalMs: number) => {
this.executePolling(intervalMs);
Variable.derive([this._pollingInterval, ...this._trackers], (intervalMs: number) => {
this._executePolling(intervalMs);
})();
}
@@ -40,9 +40,9 @@ export class Poller {
* Stops the polling process and cleans up resources.
*/
public stop(): void {
if (this.intervalInstance !== null) {
this.intervalInstance.cancel();
this.intervalInstance = null;
if (this._intervalInstance !== null) {
this._intervalInstance.cancel();
this._intervalInstance = null;
}
}
@@ -83,25 +83,47 @@ export class Poller {
*
* @param intervalMs - The polling interval in milliseconds.
*/
private executePolling(intervalMs: number): void {
if (this.intervalInstance !== null) {
this.intervalInstance.cancel();
private _executePolling(intervalMs: number): void {
if (this._intervalInstance !== null) {
this._intervalInstance.cancel();
}
this.intervalInstance = interval(intervalMs, async () => {
if (this.isExecuting) {
return;
}
if (intervalMs === 0) {
this._executeSinglePoll();
return;
}
this.isExecuting = true;
this._intervalInstance = interval(intervalMs, () => this._executePollingCycle());
}
try {
await this.pollingFunction();
} catch (error) {
console.error('Error during polling execution:', error);
} finally {
this.isExecuting = false;
}
});
/**
* Executes a single polling operation synchronously.
*/
private _executeSinglePoll(): void {
try {
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';
declare global {
const CONFIG: string;
const CONFIG_DIR: string;
const CONFIG_FILE: string;
const TMP: string;
const USER: 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 {
const file = Gio.File.new_for_path(path);
const parent = file.get_parent();
@@ -31,12 +46,15 @@ export function ensureFile(path: string): void {
const dataDir = typeof DATADIR !== 'undefined' ? DATADIR : SRC;
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`,
USER: GLib.get_user_name(),
SRC_DIR: dataDir,
});
ensureDirectory(TMP);
ensureFile(CONFIG);
ensureFile(CONFIG_FILE);
ensureJsonFile(`${CONFIG_DIR}/modules.json`);
ensureFile(`${CONFIG_DIR}/modules.scss`);
App.add_icons(`${SRC_DIR}/assets`);

View File

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

View File

@@ -9,3 +9,5 @@ export type ThrottleFn = (
) => void;
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 { exec, execAsync } from 'astal/process';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import { Primitive } from './types/utils';
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.
*
@@ -102,7 +113,8 @@ export function icon(name: string | null, fallback = icons.missing): string {
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;
}
@@ -154,9 +166,10 @@ export async function sh(cmd: string | string[]): Promise<string> {
*
* @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;
return range(n, 0).flatMap(widget);
return Promise.all(range(n, 0).map(widget));
}
/**