mirror of
https://github.com/zephrynis/nix-flake.git
synced 2026-02-18 20:21:53 +00:00
feat: make AGS colorshell configuration fully declarative
- Add complete colorshell v2.0.3 configuration to home/ags-config/ - Disable runner plugin and NightLight tile (incompatible with NixOS) - Customize SCSS with full opacity (no transparency) - Add dark pale blue color scheme in home/pywal-colors/ - Configure Papirus-Dark icon theme via home-manager - Make ~/.config/ags/ immutable and managed by Nix store - Auto-deploy pywal colors to ~/.cache/wal/colors.json All AGS configuration is now reproducible and version controlled.
This commit is contained in:
219
home/ags-config/modules/config.ts
Normal file
219
home/ags-config/modules/config.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { timeout } from "ags/time";
|
||||
import { monitorFile, readFileAsync, writeFileAsync } from "ags/file";
|
||||
import { Notifications } from "./notifications";
|
||||
import { Accessor } from "ags";
|
||||
import GObject, { getter, gtype, register } from "ags/gobject";
|
||||
|
||||
import Gio from "gi://Gio?version=2.0";
|
||||
import AstalIO from "gi://AstalIO";
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
|
||||
|
||||
export { Config };
|
||||
type ValueTypes = "string" | "boolean" | "object" | "number" | "any";
|
||||
|
||||
@register({ GTypeName: "Config" })
|
||||
class Config<K extends string, V = any> extends GObject.Object {
|
||||
declare $signals: GObject.Object.SignalSignatures & {
|
||||
"notify::entries": (entries: Record<K, V>) => void;
|
||||
};
|
||||
|
||||
/** unmodified object with default entries. User-values are stored
|
||||
* in the `entries` field */
|
||||
public readonly defaults: Record<K, V>;
|
||||
|
||||
@getter(gtype<Record<K, V>>(Object))
|
||||
public get entries() { return this.#entries; }
|
||||
|
||||
#file: Gio.File;
|
||||
#entries: Record<K, V>;
|
||||
|
||||
private timeout: (AstalIO.Time|boolean|undefined);
|
||||
public get file() { return this.#file; };
|
||||
|
||||
constructor(filePath: Gio.File|string, defaults?: Record<K, V>) {
|
||||
super();
|
||||
|
||||
this.defaults = (defaults ?? {}) as Record<K, V>;
|
||||
this.#entries = { ...defaults } as Record<K, V>;
|
||||
|
||||
this.#file = (typeof filePath === "string") ?
|
||||
Gio.File.new_for_path(filePath)
|
||||
: filePath;
|
||||
|
||||
if(!this.#file.query_exists(null)) {
|
||||
this.#file.make_directory_with_parents(null);
|
||||
this.#file.delete(null);
|
||||
|
||||
this.writeFile().catch(e => Notifications.getDefault().sendNotification({
|
||||
appName: "colorshell",
|
||||
summary: "Write error",
|
||||
body: `Couldn't write default configuration file to "${this.#file.get_path()!
|
||||
}".\nStderr: ${e}`
|
||||
}));
|
||||
}
|
||||
|
||||
monitorFile(this.#file.get_path()!,
|
||||
() => {
|
||||
if(this.timeout) return;
|
||||
this.timeout = timeout(500, () => this.timeout = undefined);
|
||||
|
||||
if(this.#file.query_exists(null)) {
|
||||
this.timeout?.cancel();
|
||||
this.timeout = true;
|
||||
|
||||
this.readFile().finally(() =>
|
||||
this.timeout = undefined);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Notifications.getDefault().sendNotification({
|
||||
appName: "colorshell",
|
||||
summary: "Config error",
|
||||
body: `Could not hot-reload configuration: config file not found in \`${this.#file.get_path()!}\`, last valid configuration is being used. Maybe it got deleted?`
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.readFile().catch(e => console.error(
|
||||
`Config: An error occurred while read the configuration file. Stderr: ${e}`
|
||||
));
|
||||
}
|
||||
|
||||
private async writeFile(): Promise<void> {
|
||||
this.timeout = true;
|
||||
await writeFileAsync(
|
||||
this.#file.get_path()!, JSON.stringify(this.entries, undefined, 4)
|
||||
).finally(() => this.timeout = false);
|
||||
}
|
||||
|
||||
private async readFile(): Promise<void> {
|
||||
await readFileAsync(this.#file.get_path()!).then((content) => {
|
||||
let config: (Record<K, V>|undefined);
|
||||
|
||||
try {
|
||||
config = JSON.parse(content) as Record<K, V>;
|
||||
} catch(e) {
|
||||
Notifications.getDefault().sendNotification({
|
||||
urgency: AstalNotifd.Urgency.NORMAL,
|
||||
appName: "colorshell",
|
||||
summary: "Config parsing error",
|
||||
body: `An error occurred while parsing colorshell's config file: \nFile: ${
|
||||
this.#file.get_path()!}\n${
|
||||
(e as SyntaxError).message}`
|
||||
});
|
||||
}
|
||||
|
||||
if(!config) return;
|
||||
|
||||
|
||||
// only change valid entries that are available in the defaults (with 1 of depth)
|
||||
for(const k of Object.keys(this.entries)) {
|
||||
if(config[k as keyof typeof config] === undefined)
|
||||
return;
|
||||
|
||||
// TODO needs more work, like object-recursive(infinite depth) entry attributions
|
||||
this.#entries[k as keyof Record<K, V>] = config[k as keyof typeof config];
|
||||
}
|
||||
|
||||
this.notify("entries");
|
||||
}).catch((e: Gio.IOErrorEnum) => {
|
||||
Notifications.getDefault().sendNotification({
|
||||
urgency: AstalNotifd.Urgency.NORMAL,
|
||||
appName: "colorshell",
|
||||
summary: "Config read error",
|
||||
body: `An error occurred while reading colorshell's config file: ${this.#file.get_path()!
|
||||
}\n${e.message}`.replace(/[<>]/g, "\\&")
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public bindProperty(path: string, expectType: "boolean"): Accessor<boolean>;
|
||||
public bindProperty(path: string, expectType: "number"): Accessor<number>;
|
||||
public bindProperty(path: string, expectType: "string"): Accessor<string>;
|
||||
public bindProperty(path: string, expectType: "object"): Accessor<object>;
|
||||
public bindProperty(path: string, expectType: "any"): Accessor<any>;
|
||||
public bindProperty(path: string, expectType: undefined): Accessor<any>;
|
||||
|
||||
public bindProperty(propertyPath: string, expectType?: ValueTypes): Accessor<boolean|number|string|object|any> {
|
||||
return new Accessor(() => this.getProperty(propertyPath, expectType as never), (callback: () => void) => {
|
||||
const id = this.connect("notify::entries", () => callback());
|
||||
return () => this.disconnect(id);
|
||||
});
|
||||
}
|
||||
|
||||
public getProperty(path: string, expectType: "boolean"): boolean;
|
||||
public getProperty(path: string, expectType: "number"): number;
|
||||
public getProperty(path: string, expectType: "string"): string;
|
||||
public getProperty(path: string, expectType: "object"): object;
|
||||
public getProperty(path: string, expectType: "any"): any;
|
||||
public getProperty(path: string, expectType: undefined): any;
|
||||
|
||||
public getProperty(path: string, expectType?: ValueTypes): boolean|number|string|object|any {
|
||||
return this._getProperty(path, this.#entries, expectType);
|
||||
}
|
||||
|
||||
public getPropertyDefault(path: string, expectType: "boolean"): boolean;
|
||||
public getPropertyDefault(path: string, expectType: "number"): number;
|
||||
public getPropertyDefault(path: string, expectType: "string"): string;
|
||||
public getPropertyDefault(path: string, expectType: "object"): object;
|
||||
public getPropertyDefault(path: string, expectType: "any"): any;
|
||||
public getPropertyDefault(path: string, expectType: undefined): any;
|
||||
|
||||
public getPropertyDefault(path: string, expectType?: ValueTypes): boolean|number|string|object|any {
|
||||
return this._getProperty(path, this.defaults, expectType);
|
||||
}
|
||||
|
||||
public setProperty(path: string, value: any, write?: boolean): void {
|
||||
let property: any = this.#entries,
|
||||
obj: typeof this.entries = property;
|
||||
const pathArray = path.split('.').filter(str => str);
|
||||
|
||||
for(let i = 0; i < pathArray.length; i++) {
|
||||
const currentPath = pathArray[i];
|
||||
|
||||
property = property[currentPath as keyof typeof property];
|
||||
if(typeof property === "object") {
|
||||
obj = property;
|
||||
} else {
|
||||
obj[pathArray[pathArray.length - 1] as keyof typeof obj] = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.notify("entries");
|
||||
write && this.writeFile().catch(e => console.error(
|
||||
`Config: Couldn't save file. Stderr: ${e}`
|
||||
));
|
||||
}
|
||||
|
||||
private _getProperty(path: string, entries: Record<K, V>, expectType?: ValueTypes): (any|undefined) {
|
||||
let property: any = entries;
|
||||
const pathArray = path.split('.').filter(str => str);
|
||||
|
||||
for(let i = 0; i < pathArray.length; i++) {
|
||||
const currentPath = pathArray[i];
|
||||
|
||||
property = property[currentPath as keyof typeof property];
|
||||
}
|
||||
|
||||
if(expectType !== "any" && typeof property !== expectType) {
|
||||
// return default value if not defined by user
|
||||
property = this.defaults;
|
||||
|
||||
for(let i = 0; i < pathArray.length; i++) {
|
||||
const currentPath = pathArray[i];
|
||||
|
||||
property = property[currentPath as keyof typeof property];
|
||||
}
|
||||
}
|
||||
|
||||
if(expectType !== "any" && typeof property !== expectType) {
|
||||
console.error(`Config: property with path \`${path}\` not found in defaults/user-entries, returning \`undefined\``);
|
||||
property = undefined;
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user