🔧 chore(scripts/config): stop using singleton instance, add type declarations(for completion)

this will make easier to work with multiple configuration files at the same time
This commit is contained in:
retrozinndev
2025-08-03 15:02:36 -03:00
parent 7265a9ec1f
commit 1d5fe06f8a
7 changed files with 105 additions and 111 deletions
+64 -3
View File
@@ -19,6 +19,70 @@ import { Scope } from "/usr/share/ags/js/gnim/src/jsx/scope";
import App from "ags/gtk4/app" import App from "ags/gtk4/app"
import GObject from "ags/gobject"; import GObject from "ags/gobject";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
import GLib from "gi://GLib?version=2.0";
type ConfigEntries = {
workspaces?: Partial<{
/** this is the function that shows the Workspace's IDs
* around the current workspace if one breaks the crescent order.
* It basically helps keyboard navigation between workspaces.
* ---
* Example: 1(empty, current, shows ID), 2(empty, does not appear(makes
* the previous not to be in a crescent order)), 3(not empty, shows ID) */
enable_helper: boolean;
/** breaks `enable_helper`, makes all workspaces show their respective ID
* by default */
always_show_id: boolean;
}>;
clock?: Partial<{
/** use the same format as gnu's `date` command */
date_format: string;
}>;
notifications?: Partial<{
timeout_low: number;
timeout_normal: number;
timeout_critical: number;
}>;
night_light?: Partial<{
/** whether to save night light values to disk */
save_on_shutdown: boolean;
}>;
misc?: Partial<{
play_bell_on_volume_change: boolean;
}>;
};
export const generalConfig = new Config<keyof ConfigEntries, ConfigEntries[keyof ConfigEntries]>(
`${GLib.get_user_config_dir()}/colorshell/config.json`, {
notifications: {
timeout_low: 4000,
timeout_normal: 6000,
timeout_critical: 0
},
night_light: {
save_on_shutdown: true
},
workspaces: {
always_show_id: false,
enable_helper: true
},
clock: {
date_format: "%A %d, %H:%M"
},
misc: {
play_bell_on_volume_change: true
}
}
);
export const appScope: Scope = new Scope(null); export const appScope: Scope = new Scope(null);
@@ -46,9 +110,6 @@ App.start({
console.log(`Colorshell: initialized instance as: "${ App.instanceName || "astal" }"`); console.log(`Colorshell: initialized instance as: "${ App.instanceName || "astal" }"`);
connections.set(App, App.connect("shutdown", () => appScope.dispose())); connections.set(App, App.connect("shutdown", () => appScope.dispose()));
console.log("Config: initializing configuration file");
Config.getDefault();
Stylesheet.getDefault().compileApply(); Stylesheet.getDefault().compileApply();
// Init clipboard module // Init clipboard module
+3 -3
View File
@@ -5,8 +5,8 @@ import { timeout } from "ags/time";
import { Runner } from "../runner/Runner"; import { Runner } from "../runner/Runner";
import { showWorkspaceNumber } from "../widget/bar/Workspaces"; import { showWorkspaceNumber } from "../widget/bar/Workspaces";
import { playSystemBell } from "./utils"; import { playSystemBell } from "./utils";
import { Config } from "./config";
import { player, setPlayer } from "../widget/bar/Media"; import { player, setPlayer } from "../widget/bar/Media";
import { generalConfig } from "../app";
import AstalIO from "gi://AstalIO"; import AstalIO from "gi://AstalIO";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
@@ -236,7 +236,7 @@ function handleVolumeArgs(args: Array<string>) {
Wireplumber.getDefault().increaseSinkVolume(Number.parseInt(args[2])) Wireplumber.getDefault().increaseSinkVolume(Number.parseInt(args[2]))
: Wireplumber.getDefault().increaseSourceVolume(Number.parseInt(args[2])) : Wireplumber.getDefault().increaseSourceVolume(Number.parseInt(args[2]))
Config.getDefault().getProperty("misc.play_bell_on_volume_change", "boolean") === true && generalConfig.getProperty("misc.play_bell_on_volume_change", "boolean") === true &&
playSystemBell(); playSystemBell();
return `Done increasing volume by ${args[2]}`; return `Done increasing volume by ${args[2]}`;
@@ -246,7 +246,7 @@ function handleVolumeArgs(args: Array<string>) {
Wireplumber.getDefault().decreaseSinkVolume(Number.parseInt(args[2])) Wireplumber.getDefault().decreaseSinkVolume(Number.parseInt(args[2]))
: Wireplumber.getDefault().decreaseSourceVolume(Number.parseInt(args[2])) : Wireplumber.getDefault().decreaseSourceVolume(Number.parseInt(args[2]))
Config.getDefault().getProperty("misc.play_bell_on_volume_change", "boolean") === true && generalConfig.getProperty("misc.play_bell_on_volume_change", "boolean") === true &&
playSystemBell(); playSystemBell();
return `Done decreasing volume to ${args[2]}`; return `Done decreasing volume to ${args[2]}`;
+19 -89
View File
@@ -12,99 +12,36 @@ import { Accessor } from "ags";
export { Config }; export { Config };
export type ConfigEntries = Partial<{
workspaces: Partial<{
/** this is the function that shows the Workspace's IDs
* around the current workspace if one breaks the crescent order.
* It basically helps keyboard navigation between workspaces.
* ---
* Example: 1(empty, current, shows ID), 2(empty, does not appear(makes
* the previous not to be in a crescent order)), 3(not empty, shows ID) */
enable_helper: boolean;
/** breaks `enable_helper`, makes all workspaces show their respective ID
* by default */
always_show_id: boolean;
}>;
clock: Partial<{
/** use the same format as gnu's `date` command */
date_format: string;
}>;
notifications: Partial<{
timeout_low: number;
timeout_normal: number;
timeout_critical: number;
}>;
night_light: Partial<{
/** whether to save night light values to disk */
save_on_shutdown: boolean;
}>;
misc: Partial<{
play_bell_on_volume_change: boolean;
}>;
}>;
type ValueTypes = "string" | "boolean" | "object" | "number" | "undefined" | "any"; type ValueTypes = "string" | "boolean" | "object" | "number" | "undefined" | "any";
interface ConfigSignals extends GObject.Object.SignalSignatures {
"notify::entries": (entries: ConfigEntries) => void;
}
@register({ GTypeName: "Config" }) @register({ GTypeName: "Config" })
class Config extends GObject.Object { class Config<K extends NonNullable<string|number|symbol>, V extends string|object|any> extends GObject.Object {
private static instance: Config; declare $signals: GObject.Object.SignalSignatures & {
"notify::entries": (entries: Record<K, V>) => void;
declare $signals: ConfigSignals; };
private readonly defaultFile = Gio.File.new_for_path(
`${GLib.get_user_config_dir()}/colorshell/config.json`);
/** unmodified object with default entries. User-values are stored /** unmodified object with default entries. User-values are stored
* in the `entries` field */ * in the `entries` field */
public readonly defaults: ConfigEntries = { public readonly defaults: Record<K, V>;
notifications: {
timeout_low: 4000,
timeout_normal: 6000,
timeout_critical: 0
},
night_light: {
save_on_shutdown: true
},
workspaces: {
always_show_id: false,
enable_helper: true
},
clock: {
date_format: "%A %d, %H:%M"
},
misc: {
play_bell_on_volume_change: true
}
};
@getter(Object) @getter(Object)
public get entries() { return this.#entries; } public get entries(): object { return this.#entries; }
#file: Gio.File; #file: Gio.File;
#entries: ConfigEntries = this.defaults; #entries: Record<K, V>;
private timeout: (AstalIO.Time|boolean|undefined); private timeout: (AstalIO.Time|boolean|undefined);
public get file() { return this.#file; }; public get file() { return this.#file; };
constructor(filePath?: (Gio.File|string)) { constructor(filePath: Gio.File|string, defaults?: Record<K, V>) {
super(); super();
this.defaults = (defaults ?? {}) as Record<K, V>;
this.#entries = { ...defaults } as Record<K, V>;
this.#file = (typeof filePath === "string") ? this.#file = (typeof filePath === "string") ?
Gio.File.new_for_path(filePath) Gio.File.new_for_path(filePath)
: (filePath ?? this.defaultFile); : filePath;
if(!this.#file.query_exists(null)) { if(!this.#file.query_exists(null)) {
this.#file.make_directory_with_parents(null); this.#file.make_directory_with_parents(null);
@@ -156,19 +93,12 @@ class Config extends GObject.Object {
); );
} }
public static getDefault(): Config {
if(!this.instance)
this.instance = new Config();
return this.instance;
}
private async readFile(): Promise<void> { private async readFile(): Promise<void> {
await readFileAsync(this.#file.get_path()!).then((content) => { await readFileAsync(this.#file.get_path()!).then((content) => {
let config: (ConfigEntries|undefined); let config: (Record<K, V>|undefined);
try { try {
config = JSON.parse(content) as ConfigEntries; config = JSON.parse(content) as Record<K, V>;
} catch(e) { } catch(e) {
Notifications.getDefault().sendNotification({ Notifications.getDefault().sendNotification({
urgency: AstalNotifd.Urgency.NORMAL, urgency: AstalNotifd.Urgency.NORMAL,
@@ -189,7 +119,7 @@ class Config extends GObject.Object {
return; return;
// TODO needs more work, like object-recursive(infinite depth) entry attributions // TODO needs more work, like object-recursive(infinite depth) entry attributions
this.entries[k as keyof typeof this.entries] = config[k as keyof typeof config]; this.#entries[k as keyof Record<K, V>] = config[k as keyof typeof config];
} }
this.notify("entries"); this.notify("entries");
@@ -204,22 +134,22 @@ class Config extends GObject.Object {
}); });
} }
public bindProperty(propertyPath: (keyof ConfigEntries|string), expectType?: ValueTypes): Accessor<any|undefined> { public bindProperty(propertyPath: string, expectType?: ValueTypes): Accessor<any|undefined> {
return new Accessor<ConfigEntries>(() => this.getProperty(propertyPath, expectType), (callback: () => void) => { return new Accessor<Record<K, V>>(() => this.getProperty(propertyPath, expectType), (callback: () => void) => {
const id = this.connect("notify::entries", () => callback()); const id = this.connect("notify::entries", () => callback());
return () => this.disconnect(id); return () => this.disconnect(id);
}); });
} }
public getProperty(path: string, expectType?: ValueTypes): (any|undefined) { public getProperty(path: string, expectType?: ValueTypes): (any|undefined) {
return this._getProperty(path, this.entries, expectType); return this._getProperty(path, this.#entries, expectType);
} }
public getPropertyDefault(path: string, expectType?: ValueTypes): (any|undefined) { public getPropertyDefault(path: string, expectType?: ValueTypes): (any|undefined) {
return this._getProperty(path, this.defaults, expectType); return this._getProperty(path, this.defaults, expectType);
} }
private _getProperty(path: string, entries: ConfigEntries, expectType?: ValueTypes): (any|undefined) { private _getProperty(path: string, entries: Record<K, V>, expectType?: ValueTypes): (any|undefined) {
let property: any = entries; let property: any = entries;
const pathArray = path.split('.').filter(str => str); const pathArray = path.split('.').filter(str => str);
+5 -5
View File
@@ -1,14 +1,14 @@
import { Config } from "./config";
import { timeout } from "ags/time"; import { timeout } from "ags/time";
import { execAsync } from "ags/process"; import { execAsync } from "ags/process";
import { readFile } from "ags/file";
import { generalConfig } from "../app";
import { onCleanup } from "ags";
import GObject, { getter, property, register, signal } from "ags/gobject"; import GObject, { getter, property, register, signal } from "ags/gobject";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
import AstalIO from "gi://AstalIO"; import AstalIO from "gi://AstalIO";
import { onCleanup } from "ags";
import Gio from "gi://Gio?version=2.0"; import Gio from "gi://Gio?version=2.0";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
import { readFile } from "ags/file";
export interface HistoryNotification { export interface HistoryNotification {
@@ -53,7 +53,7 @@ class Notifications extends GObject.Object {
this.#connections.push( this.#connections.push(
AstalNotifd.get_default().connect("notified", (notifd, id) => { AstalNotifd.get_default().connect("notified", (notifd, id) => {
const notification = notifd.get_notification(id); const notification = notifd.get_notification(id);
const notifTimeout = Config.getDefault().getProperty( const notifTimeout = generalConfig.getProperty(
`notifications.timeout_${this.getUrgencyString(notification.urgency).toLowerCase()}`, `notifications.timeout_${this.getUrgencyString(notification.urgency).toLowerCase()}`,
"number") as number; "number") as number;
+3 -2
View File
@@ -2,7 +2,8 @@ import { Gtk } from "ags/gtk4";
import { Windows } from "../../windows"; import { Windows } from "../../windows";
import { createBinding } from "ags"; import { createBinding } from "ags";
import { time } from "../../scripts/utils"; import { time } from "../../scripts/utils";
import { Config } from "../../scripts/config"; import { generalConfig } from "../../app";
export const Clock = () => export const Clock = () =>
<Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((wins) => <Gtk.Button class={createBinding(Windows.getDefault(), "openWindows").as((wins) =>
@@ -14,7 +15,7 @@ export const Clock = () =>
]; ];
}} }}
label={time((dt) => dt.format( label={time((dt) => dt.format(
Config.getDefault().getProperty("clock.date_format", "string")) generalConfig.getProperty("clock.date_format", "string"))
?? "An error occurred" ?? "An error occurred"
)} )}
/>; />;
+6 -4
View File
@@ -1,12 +1,14 @@
import { Gtk } from "ags/gtk4"; import { Gtk } from "ags/gtk4";
import AstalHyprland from "gi://AstalHyprland"; import AstalHyprland from "gi://AstalHyprland";
import { getAppIcon, getSymbolicIcon } from "../../scripts/apps"; import { getAppIcon, getSymbolicIcon } from "../../scripts/apps";
import { Config } from "../../scripts/config";
import { Separator } from "../Separator"; import { Separator } from "../Separator";
import { generalConfig } from "../../app";
import { createBinding, createComputed, createState, For, With } from "ags"; import { createBinding, createComputed, createState, For, With } from "ags";
import GObject from "gi://GObject?version=2.0";
import { variableToBoolean } from "../../scripts/utils"; import { variableToBoolean } from "../../scripts/utils";
import GObject from "ags/gobject";
const [showNumbers, setShowNumbers] = createState(false); const [showNumbers, setShowNumbers] = createState(false);
export const showWorkspaceNumber = (show: boolean) => export const showWorkspaceNumber = (show: boolean) =>
setShowNumbers(show); setShowNumbers(show);
@@ -83,8 +85,8 @@ export const Workspaces = () => {
<For each={defaultWorkspaces}> <For each={defaultWorkspaces}>
{(ws: AstalHyprland.Workspace, i) => { {(ws: AstalHyprland.Workspace, i) => {
const showId = createComputed([ const showId = createComputed([
Config.getDefault().bindProperty("workspaces.always_show_id", "boolean").as(Boolean), generalConfig.bindProperty("workspaces.always_show_id", "boolean").as(Boolean),
Config.getDefault().bindProperty("workspaces.enable_helper", "boolean").as(Boolean), generalConfig.bindProperty("workspaces.enable_helper", "boolean").as(Boolean),
showNumbers, showNumbers,
i i
], (alwaysShowIds, enableHelper, showIds, i) => { ], (alwaysShowIds, enableHelper, showIds, i) => {
+5 -5
View File
@@ -1,14 +1,14 @@
import { Astal, Gdk, Gtk } from "ags/gtk4"; import { Astal, Gdk, Gtk } from "ags/gtk4";
import { execAsync } from "ags/process"; import { execAsync } from "ags/process";
import { generalConfig } from "../app";
import { AskPopup } from "../widget/AskPopup"; import { AskPopup } from "../widget/AskPopup";
import { Notifications } from "../scripts/notifications"; import { Notifications } from "../scripts/notifications";
import { NightLight } from "../scripts/nightlight"; import { NightLight } from "../scripts/nightlight";
import { Config } from "../scripts/config";
import { time } from "../scripts/utils"; import { time } from "../scripts/utils";
import GObject from "ags/gobject";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
import Gio from "gi://Gio?version=2.0"; import Gio from "gi://Gio?version=2.0";
import GObject from "gi://GObject?version=2.0";
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
@@ -64,7 +64,7 @@ export const LogoutMenu = (mon: number) =>
title: "Power Off", title: "Power Off",
text: "Are you sure you want to power off? Unsaved work will be lost.", text: "Are you sure you want to power off? Unsaved work will be lost.",
onAccept: () => { onAccept: () => {
Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") && generalConfig.getProperty("night_light.save_on_shutdown", "boolean") &&
NightLight.getDefault().saveData(); NightLight.getDefault().saveData();
execAsync("systemctl poweroff"); execAsync("systemctl poweroff");
@@ -76,7 +76,7 @@ export const LogoutMenu = (mon: number) =>
title: "Reboot", title: "Reboot",
text: "Are you sure you want to Reboot? Unsaved work will be lost.", text: "Are you sure you want to Reboot? Unsaved work will be lost.",
onAccept: () => { onAccept: () => {
Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") && generalConfig.getProperty("night_light.save_on_shutdown", "boolean") &&
NightLight.getDefault().saveData(); NightLight.getDefault().saveData();
execAsync("systemctl reboot"); execAsync("systemctl reboot");
@@ -95,7 +95,7 @@ export const LogoutMenu = (mon: number) =>
title: "Log out", title: "Log out",
text: "Are you sure you want to log out? Your session will be ended.", text: "Are you sure you want to log out? Your session will be ended.",
onAccept: () => { onAccept: () => {
Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") && generalConfig.getProperty("night_light.save_on_shutdown", "boolean") &&
NightLight.getDefault().saveData(); NightLight.getDefault().saveData();
execAsync(`hyprctl dispatch exit`).catch((err: Gio.IOErrorEnum) => execAsync(`hyprctl dispatch exit`).catch((err: Gio.IOErrorEnum) =>