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:
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
5
src/lib/types/bar.d.ts
vendored
5
src/lib/types/bar.d.ts
vendored
@@ -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>;
|
||||
|
||||
2
src/lib/types/utils.d.ts
vendored
2
src/lib/types/utils.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user