151 lines
4.5 KiB
TypeScript
151 lines
4.5 KiB
TypeScript
import GObject, { register, signal } from "astal/gobject";
|
|
import { readFileAsync, monitorFile } from "astal/file";
|
|
import GLib from "gi://GLib";
|
|
import Gio from "gi://Gio";
|
|
import yaml from "yaml";
|
|
import { ConfigFile, Vars } from "./types";
|
|
|
|
@register({
|
|
GTypeName: "ConfigService",
|
|
})
|
|
export class ConfigService extends GObject.Object {
|
|
private get configPath(): string {
|
|
const envVar = GLib.getenv("VOIDSHELL_CONFIG");
|
|
if (envVar !== null) {
|
|
return envVar;
|
|
}
|
|
const defaultPath = `${GLib.getenv("XDG_CONFIG_HOME") ?? GLib.getenv("HOME") + "/.config"}/voidshell/config.yml`;
|
|
return defaultPath;
|
|
}
|
|
|
|
private fileMonitors: [path: string, mon: Gio.FileMonitor][] = [];
|
|
|
|
/**
|
|
* Normalizes a path to be relative to the config directory, unless it is absolute.
|
|
*/
|
|
private normalizePath(path: string) {
|
|
if (path.startsWith("/")) {
|
|
return path;
|
|
}
|
|
if (path.startsWith("~")) {
|
|
return `${GLib.getenv("HOME")}/${path.slice(2)}`;
|
|
}
|
|
const basedir = this.configPath.split("/").slice(0, -1).join("/");
|
|
return `${basedir}/${path}`;
|
|
}
|
|
|
|
/**
|
|
* Recursively parse config files
|
|
*/
|
|
private async parseConfig(path: string): Promise<ConfigFile> {
|
|
try {
|
|
const contents = await readFileAsync(this.normalizePath(path));
|
|
const parsed = yaml.parse(contents) as ConfigFile;
|
|
const importedFiles =
|
|
(await parsed.import?.amap(this.parseConfig.bind(this))) ?? [];
|
|
|
|
// Merge config files
|
|
return importedFiles.reduce((config, imported) => {
|
|
// Merge output definitions
|
|
if (config.outputs !== undefined) {
|
|
Object.entries(imported.outputs ?? {}).forEach(([output, def]) => {
|
|
config.outputs![output] = { ...config.outputs?.[output], ...def };
|
|
});
|
|
} else {
|
|
config.outputs = imported.outputs;
|
|
}
|
|
|
|
// Merge workspace definitions
|
|
config.workspaces = { ...config.workspaces, ...imported.workspaces };
|
|
|
|
// Merge layout definitions
|
|
config.layouts = { ...config.layouts, ...imported.layouts };
|
|
|
|
// Merge context definitions
|
|
config.contexts = config.contexts ?? {};
|
|
Object.entries(imported.contexts ?? {}).forEach(([context, def]) => {
|
|
if (context in config.contexts!) {
|
|
config.contexts![context].vars = {
|
|
...config.contexts![context].vars,
|
|
...def.vars,
|
|
};
|
|
config.contexts![context].groups =
|
|
def.groups ?? config.contexts![context].groups;
|
|
} else {
|
|
config.contexts![context] = def;
|
|
}
|
|
});
|
|
|
|
// Merge variables
|
|
config.vars = { ...config.vars, ...imported.vars };
|
|
|
|
return config;
|
|
}, parsed);
|
|
} catch (e) {
|
|
console.warn(e);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format a string, usually taken from the config, using the variables set in the config and extra variables passed in
|
|
*/
|
|
public formatConfigString(value: string, extraArgs?: Vars): string {
|
|
const args = { ...this.config.vars, ...extraArgs };
|
|
return value.replace(
|
|
/{([a-zA-Z_][a-zA-Z0-9_\-\.]*)}/g,
|
|
(match, identifier: string) => {
|
|
// @ts-expect-error
|
|
const replacer = identifier.split(".").reduce((val, id) => {
|
|
if (val === undefined) return undefined;
|
|
if (typeof val !== "object") return undefined;
|
|
if (id in val) return val[id];
|
|
return undefined;
|
|
}, args);
|
|
if (replacer === undefined) return match;
|
|
if (typeof replacer === "object") return match;
|
|
return replacer;
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Pass the given string to formatConfigString and then evaluate it as a mathematical expression. Only addition and subtraction are supported.
|
|
*/
|
|
public doConfigMath(value: string, extraArgs?: Vars): number {
|
|
const str = this.formatConfigString(value, extraArgs);
|
|
let total = 0;
|
|
const matches = str.match(/[+\-]*(\.\d+|\d+(\.\d+)?)/g) ?? [];
|
|
while (matches.length) {
|
|
total += parseFloat(matches.shift() ?? "0");
|
|
}
|
|
return total;
|
|
}
|
|
|
|
public get config(): Readonly<ConfigFile> {
|
|
return this._config;
|
|
}
|
|
|
|
private _config: ConfigFile = {};
|
|
|
|
/**
|
|
* Fully refresh the config file
|
|
*/
|
|
async refreshConfig() {
|
|
this.fileMonitors.forEach(([_, mon]) => {
|
|
mon.cancel();
|
|
});
|
|
this.fileMonitors = [];
|
|
this._config = await this.parseConfig(this.configPath);
|
|
this.configReloaded();
|
|
}
|
|
|
|
@signal()
|
|
declare configReloaded: () => void;
|
|
|
|
constructor() {
|
|
super();
|
|
this.refreshConfig();
|
|
}
|
|
}
|