config: validate values

Validate values for keys with literal types, also better validation for array types
This commit is contained in:
2 * r + 2 * t 2025-03-23 17:47:33 +11:00
parent 1ffd7e1d54
commit 9f6e227d7b
2 changed files with 124 additions and 6 deletions

View file

@ -2,23 +2,70 @@ import { GLib, monitorFile, readFileAsync, Variable } from "astal";
import config from ".";
import { loadStyleAsync } from "../../app";
import defaults from "./defaults";
import types from "./types";
type Settings<T> = { [P in keyof T]: T[P] extends object & { length?: never } ? Settings<T[P]> : Variable<T[P]> };
const CONFIG = `${GLib.get_user_config_dir()}/caelestia/shell.json`;
const isObject = (o: any) => typeof o === "object" && o !== null && !Array.isArray(o);
const isObject = (o: any): o is object => typeof o === "object" && o !== null && !Array.isArray(o);
const isCorrectType = (v: any, type: string | string[] | number[], path: string) => {
if (Array.isArray(type)) {
// type is array of valid values
if (!type.includes(v as never)) {
console.warn(`Invalid value for ${path}: ${v} != ${type.map(v => `"${v}"`).join(" | ")}`);
return false;
}
} else if (type.startsWith("array of ")) {
// Array of ...
if (Array.isArray(v)) {
// Remove invalid items but always return true
const arrType = type.slice(9);
try {
// Recursively check type
const type = JSON.parse(arrType);
const valid = v.filter((item, i) =>
Object.entries(type).some(([k, t]) => {
if (!item[k]) {
console.warn(`Invalid shape for ${path}[${i}]: ${JSON.stringify(item)} != ${arrType}`);
return false;
}
return !isCorrectType(item[k], t as any, `${path}[${i}].${k}`);
})
);
v.splice(0, v.length, ...valid); // In-place filter
} catch {
const valid = v.filter((item, i) => {
if (typeof item !== arrType) {
console.warn(`Invalid type for ${path}[${i}]: ${typeof item} != ${arrType}`);
return false;
}
return true;
});
v.splice(0, v.length, ...valid); // In-place filter
}
} else {
// Type is array but value is not
console.warn(`Invalid type for ${path}: ${typeof v} != ${type}`);
return false;
}
} else if (typeof v !== type) {
// Value is not correct type
console.warn(`Invalid type for ${path}: ${typeof v} != ${type}`);
return false;
}
return true;
};
const deepMerge = <T extends object, U extends object>(a: T, b: U, path = ""): T & U => {
const merged: { [k: string]: any } = { ...b };
for (const [k, v] of Object.entries(a)) {
if (b.hasOwnProperty(k)) {
const bv = b[k as keyof U];
if (isObject(v) && isObject(bv)) merged[k] = deepMerge(v, bv as object, `${path}${k}.`);
else if (typeof v !== typeof bv) {
console.warn(`Invalid type for ${path}${k}: ${typeof v} != ${typeof bv}`);
merged[k] = v;
}
if (isObject(v) && isObject(bv)) merged[k] = deepMerge(v, bv, `${path}${k}.`);
else if (!isCorrectType(bv, types[path + k], path + k)) merged[k] = v;
} else merged[k] = v;
}
return merged as any;

71
src/config/types.ts Normal file
View file

@ -0,0 +1,71 @@
const BOOL = "boolean";
const STR = "string";
const NUM = "number";
const ARR = (type: string) => `array of ${type}`;
const OBJ_ARR = (shape: object) => ARR(JSON.stringify(shape));
export default {
"style.transparency": ["off", "normal", "high"],
"style.vibrant": BOOL,
// Bar
"bar.vertical": BOOL,
"bar.modules.osIcon.enabled": BOOL,
"bar.modules.activeWindow.enabled": BOOL,
"bar.modules.mediaPlaying.enabled": BOOL,
"bar.modules.workspaces.enabled": BOOL,
"bar.modules.workspaces.shown": NUM,
"bar.modules.tray.enabled": BOOL,
"bar.modules.statusIcons.enabled": BOOL,
"bar.modules.pkgUpdates.enabled": BOOL,
"bar.modules.notifCount.enabled": BOOL,
"bar.modules.battery.enabled": BOOL,
"bar.modules.dateTime.enabled": BOOL,
"bar.modules.dateTime.format": STR,
"bar.modules.dateTime.detailedFormat": STR,
"bar.modules.power.enabled": BOOL,
// Launcher
"launcher.actionPrefix": STR,
"launcher.apps.maxResults": NUM,
"launcher.files.maxResults": NUM,
"launcher.files.fdOpts": ARR(STR),
"launcher.files.shortenThreshold": NUM,
"launcher.math.maxResults": NUM,
"launcher.todo.notify": BOOL,
"launcher.wallpaper.style": ["compact", "medium", "large"],
"launcher.disabledActions": ARR(STR),
// Notif popups
"notifpopups.maxPopups": NUM,
"notifpopups.expire": BOOL,
"notifpopups.agoTime": BOOL,
// OSDs
"osds.volume.position": [2, 4, 8, 16],
"osds.volume.margin": NUM,
"osds.volume.hideDelay": NUM,
"osds.volume.showValue": BOOL,
"osds.brightness.position": [2, 4, 8, 16],
"osds.brightness.margin": NUM,
"osds.brightness.hideDelay": NUM,
"osds.brightness.showValue": BOOL,
"osds.lock.spacing": NUM,
"osds.lock.caps.hideDelay": NUM,
"osds.lock.num.hideDelay": NUM,
// Sideleft
"sideleft.directories.left.top": STR,
"sideleft.directories.left.middle": STR,
"sideleft.directories.left.bottom": STR,
"sideleft.directories.right.top": STR,
"sideleft.directories.right.middle": STR,
"sideleft.directories.right.bottom": STR,
// Services
"math.maxHistory": NUM,
"updates.interval": NUM,
"weather.interval": NUM,
"weather.key": STR,
"weather.location": STR,
"weather.imperial": BOOL,
"cpu.interval": NUM,
"gpu.interval": NUM,
"memory.interval": NUM,
"storage.interval": NUM,
"wallpapers.paths": OBJ_ARR({ recursive: BOOL, path: STR }),
} as { [k: string]: string | string[] | number[] };