Upgrade to Agsv2 + Astal (#533)
* migrate to astal * Reorganize project structure. * progress * Migrate Dashboard and Window Title modules. * Migrate clock and notification bar modules. * Remove unused code * Media menu * Rework network and volume modules * Finish custom modules. * Migrate battery bar module. * Update battery module and organize helpers. * Migrate workspace module. * Wrap up bar modules. * Checkpoint before I inevitbly blow something up. * Updates * Fix event propagation logic. * Type fixes * More type fixes * Fix padding for event boxes. * Migrate volume menu and refactor scroll event handlers. * network module WIP * Migrate network service. * Migrate bluetooth menu * Updates * Migrate notifications * Update scrolling behavior for custom modules. * Improve popup notifications and add timer functionality. * Migration notifications menu header/controls. * Migrate notifications menu and consolidate notifications menu code. * Migrate power menu. * Dashboard progress * Migrate dashboard * Migrate media menu. * Reduce media menu nesting. * Finish updating media menu bindings to navigate active player. * Migrate battery menu * Consolidate code * Migrate calendar menu * Fix workspace logic to update on client add/change/remove and consolidate code. * Migrate osd * Consolidate hyprland service connections. * Implement startup dropdown menu position allocation. * Migrate settings menu (WIP) * Settings dialo menu fixes * Finish Dashboard menu * Type updates * update submoldule for types * update github ci * ci * Submodule update * Ci updates * Remove type checking for now. * ci fix * Fix a bunch of stuff, losing track... need rest. Brb coffee * Validate dropdown menu before render. * Consolidate code and add auto-hide functionality. * Improve auto-hide behavior. * Consolidate audio menu code * Organize bluetooth code * Improve active player logic * Properly dismiss a notification on action button resolution. * Implement CLI command engine and migrate CLI commands. * Handle variable disposal * Bar component fixes and add hyprland startup rules. * Handle potentially null bindings network and bluetooth bindings. * Handle potentially null wired adapter. * Fix GPU stats * Handle poller for GPU * Fix gpu bar logic. * Clean up logic for stat bars. * Handle wifi and wired bar icon bindings. * Fix battery percentages * Fix switch behavior * Wifi staging fixes * Reduce redundant hyprland service calls. * Code cleanup * Document the option code and reduce redundant calls to optimize performance. * Remove outdated comment. * Add JSDocs * Add meson to build hyprpanel * Consistency updates * Organize commands * Fix images not showing up on notifications. * Remove todo * Move hyprpanel configuration to the ~/.config/hyprpanel directory and add utility commands. * Handle SRC directory for the bundled/built hyprpanel. * Add namespaces to all windows * Migrate systray * systray updates * Update meson to include ts, tsx and scss files. * Remove log from meson * Fix file choose path and make it float. * Added a command to check the dependency status * Update dep names. * Get scale directly from env * Add todo
This commit is contained in:
321
src/lib/option.ts
Normal file
321
src/lib/option.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { isHexColor } from '../globals/variables';
|
||||
import { MkOptionsResult } from './types/options';
|
||||
import { ensureDirectory } from './session';
|
||||
import Variable from 'astal/variable';
|
||||
import { monitorFile, readFile, writeFile } from 'astal/file';
|
||||
import GLib from 'gi://GLib?version=2.0';
|
||||
|
||||
type OptProps = {
|
||||
persistent?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A file to store default configurations. Placed inside the cache directory.
|
||||
* NOTE: We need to move this out into the .config directory instead.
|
||||
*/
|
||||
export const defaultFile = `${GLib.get_tmp_dir()}/ags/hyprpanel/default.json`;
|
||||
|
||||
export class Opt<T = unknown> extends Variable<T> {
|
||||
/**
|
||||
* The initial value set when the `Opt` is created.
|
||||
*/
|
||||
public readonly initial: T;
|
||||
|
||||
/**
|
||||
* Indicates whether this option should remain unchanged even when reset operations occur.
|
||||
*/
|
||||
public readonly persistent: boolean;
|
||||
|
||||
private _id = '';
|
||||
|
||||
/**
|
||||
* Creates an instance of `Opt`.
|
||||
*
|
||||
* @param {T} initial - The initial value of the option.
|
||||
* @param {OptProps} [props={}] - Additional properties for the option.
|
||||
*/
|
||||
constructor(initial: T, { persistent = false }: OptProps = {}) {
|
||||
super(initial);
|
||||
this.initial = initial;
|
||||
this.persistent = persistent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the current value to a JSON-compatible string.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
toJSON(): string {
|
||||
return `opt:${JSON.stringify(this.get())}`;
|
||||
}
|
||||
|
||||
public get value(): T {
|
||||
return this.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the current value of the option.
|
||||
*/
|
||||
public set value(val: T) {
|
||||
this.set(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the unique ID of the option.
|
||||
*/
|
||||
public get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the unique ID of the option.
|
||||
*/
|
||||
public set id(newId: string) {
|
||||
this._id = newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this option by attempting to read its value from a cache file.
|
||||
* If found, sets the current value. Also sets up a subscription to write updates back.
|
||||
*
|
||||
* @param cacheFile - The path to the cache file.
|
||||
*/
|
||||
public init(cacheFile: string): void {
|
||||
const rawData = readFile(cacheFile);
|
||||
|
||||
let cacheData: Record<string, unknown> = {};
|
||||
|
||||
if (rawData && rawData.trim() !== '') {
|
||||
try {
|
||||
cacheData = JSON.parse(rawData) as Record<string, unknown>;
|
||||
} catch {
|
||||
// do nuffin
|
||||
}
|
||||
}
|
||||
|
||||
const cachedVariable = cacheData[this._id];
|
||||
|
||||
if (cachedVariable !== undefined) {
|
||||
this.set(cachedVariable as T);
|
||||
}
|
||||
|
||||
this.subscribe((newVal) => {
|
||||
const reRaw = readFile(cacheFile);
|
||||
let currentCache: Record<string, unknown> = {};
|
||||
if (reRaw && reRaw.trim() !== '') {
|
||||
try {
|
||||
currentCache = JSON.parse(reRaw) as Record<string, unknown>;
|
||||
} catch {
|
||||
// Do nuffin
|
||||
}
|
||||
}
|
||||
currentCache[this._id] = newVal;
|
||||
writeFile(cacheFile, JSON.stringify(currentCache, null, 2));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this option by attempting to read its default value from the default file.
|
||||
* If found, sets the current value.
|
||||
*/
|
||||
public createDefault(): void {
|
||||
const rawData = readFile(defaultFile);
|
||||
|
||||
let defaultData: Record<string, unknown> = {};
|
||||
|
||||
if (rawData && rawData.trim() !== '') {
|
||||
try {
|
||||
defaultData = JSON.parse(rawData) as Record<string, unknown>;
|
||||
} catch {
|
||||
// do nuffin
|
||||
}
|
||||
}
|
||||
|
||||
const defaultVal = defaultData[this._id];
|
||||
|
||||
if (defaultVal !== undefined) {
|
||||
this.set(defaultVal as T);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the value of this option to its initial value if not persistent and if it differs from the current value.
|
||||
*
|
||||
* @returns Returns the option's ID if reset occurred, otherwise undefined.
|
||||
*/
|
||||
public reset(): string | undefined {
|
||||
if (this.persistent) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const current = this.get();
|
||||
|
||||
if (JSON.stringify(current) !== JSON.stringify(this.initial)) {
|
||||
this.set(this.initial);
|
||||
return this._id;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an `Opt` instance with the given initial value and properties.
|
||||
* @template T
|
||||
* @param initial - The initial value.
|
||||
* @param [props] - Additional properties.
|
||||
*/
|
||||
export function opt<T>(initial: T, props?: OptProps): Opt<T> {
|
||||
return new Opt(initial, props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses the provided object to extract all `Opt` instances, assigning IDs to each.
|
||||
*
|
||||
* @param object - The object containing `Opt` instances.
|
||||
* @param [path=''] - The current path (used internally).
|
||||
* @param [arr=[]] - The accumulator array for found `Opt` instances.
|
||||
* @returns An array of all found `Opt` instances.
|
||||
*/
|
||||
function getOptions(object: Record<string, unknown>, path = '', arr: Opt[] = []): Opt[] {
|
||||
for (const key in object) {
|
||||
const value = object[key];
|
||||
const id = path ? `${path}.${key}` : key;
|
||||
|
||||
if (value instanceof Variable) {
|
||||
const optValue = value as Opt;
|
||||
optValue.id = id;
|
||||
arr.push(optValue);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
getOptions(value as Record<string, unknown>, id, arr);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes options from a given object structure. The returned object
|
||||
* includes methods to reset values, reset theme colors, and handle dependencies.
|
||||
*
|
||||
* @template T extends object
|
||||
* @param cacheFile - The file path to store cached values.
|
||||
* @param object - The object containing nested `Opt` instances.
|
||||
* @param [confFile='config.json'] - The configuration file name stored in TMP.
|
||||
* @returns The original object extended with additional methods for handling options.
|
||||
*/
|
||||
export function mkOptions<T extends object>(
|
||||
cacheFile: string,
|
||||
object: T,
|
||||
confFile: string = 'config.json',
|
||||
): T & MkOptionsResult {
|
||||
const allOptions = getOptions(object as Record<string, unknown>);
|
||||
|
||||
for (let i = 0; i < allOptions.length; i++) {
|
||||
allOptions[i].init(cacheFile);
|
||||
}
|
||||
|
||||
ensureDirectory(cacheFile.split('/').slice(0, -1).join('/'));
|
||||
ensureDirectory(defaultFile.split('/').slice(0, -1).join('/'));
|
||||
|
||||
const configFile = `${TMP}/${confFile}`;
|
||||
|
||||
const values: Record<string, unknown> = {};
|
||||
const defaultValues: Record<string, unknown> = {};
|
||||
|
||||
for (let i = 0; i < allOptions.length; i++) {
|
||||
const option = allOptions[i];
|
||||
const val = option.value;
|
||||
|
||||
values[option.id] = val;
|
||||
|
||||
if (isHexColor(val as string)) {
|
||||
defaultValues[option.id] = option.initial;
|
||||
} else {
|
||||
defaultValues[option.id] = val;
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(defaultFile, JSON.stringify(defaultValues, null, 2));
|
||||
writeFile(configFile, JSON.stringify(values, null, 2));
|
||||
|
||||
monitorFile(configFile, () => {
|
||||
const raw = readFile(configFile);
|
||||
|
||||
if (!raw || raw.trim() === '') return;
|
||||
|
||||
let cache: Record<string, unknown>;
|
||||
|
||||
try {
|
||||
cache = JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < allOptions.length; i++) {
|
||||
const opt = allOptions[i];
|
||||
const newVal = cache[opt.id];
|
||||
const oldVal = opt.get();
|
||||
|
||||
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
||||
opt.set(newVal as T);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A simple sleep utility.
|
||||
*
|
||||
* @param [ms=0] - Milliseconds to sleep.
|
||||
*/
|
||||
function sleep(ms = 0): Promise<T> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all options to their initial values if possible.
|
||||
*
|
||||
* @param opts - Array of all option instances.
|
||||
* @returns IDs of all reset options.
|
||||
*/
|
||||
async function resetAll(opts: Opt[]): Promise<string[]> {
|
||||
const results: string[] = [];
|
||||
for (let i = 0; i < opts.length; i++) {
|
||||
const id = opts[i].reset();
|
||||
|
||||
if (id) {
|
||||
results.push(id);
|
||||
await sleep(50);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
return Object.assign(object, {
|
||||
configFile,
|
||||
array: (): Opt[] => allOptions,
|
||||
async reset(): Promise<string> {
|
||||
const ids = await resetAll(allOptions);
|
||||
|
||||
return ids.join('\n');
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a callback that fires when any option whose ID starts with any of the given dependencies changes.
|
||||
*
|
||||
* @param deps - An array of dependency prefixes.
|
||||
* @param callback - The callback function to execute on changes.
|
||||
*/
|
||||
handler(deps: string[], callback: () => void): void {
|
||||
for (let i = 0; i < allOptions.length; i++) {
|
||||
const opt = allOptions[i];
|
||||
|
||||
for (let j = 0; j < deps.length; j++) {
|
||||
if (opt.id.startsWith(deps[j])) {
|
||||
opt.subscribe(callback);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user