config: validate values
Validate values for keys with literal types, also better validation for array types
This commit is contained in:
parent
1ffd7e1d54
commit
9f6e227d7b
2 changed files with 124 additions and 6 deletions
|
|
@ -2,23 +2,70 @@ import { GLib, monitorFile, readFileAsync, Variable } from "astal";
|
||||||
import config from ".";
|
import config from ".";
|
||||||
import { loadStyleAsync } from "../../app";
|
import { loadStyleAsync } from "../../app";
|
||||||
import defaults from "./defaults";
|
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]> };
|
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 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 deepMerge = <T extends object, U extends object>(a: T, b: U, path = ""): T & U => {
|
||||||
const merged: { [k: string]: any } = { ...b };
|
const merged: { [k: string]: any } = { ...b };
|
||||||
for (const [k, v] of Object.entries(a)) {
|
for (const [k, v] of Object.entries(a)) {
|
||||||
if (b.hasOwnProperty(k)) {
|
if (b.hasOwnProperty(k)) {
|
||||||
const bv = b[k as keyof U];
|
const bv = b[k as keyof U];
|
||||||
if (isObject(v) && isObject(bv)) merged[k] = deepMerge(v, bv as object, `${path}${k}.`);
|
if (isObject(v) && isObject(bv)) merged[k] = deepMerge(v, bv, `${path}${k}.`);
|
||||||
else if (typeof v !== typeof bv) {
|
else if (!isCorrectType(bv, types[path + k], path + k)) merged[k] = v;
|
||||||
console.warn(`Invalid type for ${path}${k}: ${typeof v} != ${typeof bv}`);
|
|
||||||
merged[k] = v;
|
|
||||||
}
|
|
||||||
} else merged[k] = v;
|
} else merged[k] = v;
|
||||||
}
|
}
|
||||||
return merged as any;
|
return merged as any;
|
||||||
|
|
|
||||||
71
src/config/types.ts
Normal file
71
src/config/types.ts
Normal 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[] };
|
||||||
Loading…
Add table
Reference in a new issue