config: restore to default if not specified

Restore configs to default if not given in the config file instead of not changing them
Also sideleft configs
This commit is contained in:
2 * r + 2 * t 2025-02-22 21:25:42 +11:00
parent 2d97968445
commit 6a043ce106
2 changed files with 109 additions and 76 deletions

167
config.ts
View file

@ -2,165 +2,196 @@ import { GLib, monitorFile, readFileAsync, Variable } from "astal";
import { Astal } from "astal/gtk3"; import { Astal } from "astal/gtk3";
import { loadStyleAsync } from "./app"; import { loadStyleAsync } from "./app";
type Settings<T> = { [P in keyof T]: T[P] extends object ? 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 s = <T>(v: T): Variable<T> => Variable(v); const isObject = (o: any) => typeof o === "object" && o !== null && !Array.isArray(o);
const warn = (e: Error) => console.warn(`Invalid config: ${e}`); 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;
}
} else merged[k] = v;
}
return merged as any;
};
const updateSection = (from: { [k: string]: any }, to: { [k: string]: any }, path: string) => { const convertSettings = <T extends object>(obj: T): Settings<T> =>
Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, isObject(v) ? convertSettings(v) : Variable(v)])) as any;
const updateSection = (from: { [k: string]: any }, to: { [k: string]: any }, path = "") => {
for (const [k, v] of Object.entries(from)) { for (const [k, v] of Object.entries(from)) {
print(`${path}${k}:`, to.hasOwnProperty(k));
if (to.hasOwnProperty(k)) { if (to.hasOwnProperty(k)) {
if (typeof v === "object" && v !== null && !Array.isArray(v)) updateSection(v, to[k], `${path}.${k}`); if (isObject(v)) updateSection(v, to[k], `${path}${k}.`);
else if (typeof v === typeof to[k].get()) to[k].set(v); else to[k].set(v);
else console.warn(`Invalid type for ${path}.${k}: ${typeof v} != ${typeof to[k].get()}`); } else console.warn(`Unknown config key: ${path}${k}`);
} else console.warn(`Unknown config key: ${path}.${k}`);
} }
}; };
export const updateConfig = async () => { export const updateConfig = async () => {
const conf: { [k: string]: any } = JSON.parse(await readFileAsync(CONFIG)); updateSection(deepMerge(DEFAULTS, JSON.parse(await readFileAsync(CONFIG))), config);
for (const [k, v] of Object.entries(conf)) {
if (config.hasOwnProperty(k)) updateSection(v, config[k as keyof typeof config], k);
else console.warn(`Unknown config key: ${k}`);
}
loadStyleAsync().catch(console.error); loadStyleAsync().catch(console.error);
}; };
export const initConfig = () => { export const initConfig = () => {
monitorFile(CONFIG, () => updateConfig().catch(warn)); monitorFile(CONFIG, () => updateConfig().catch(e => console.warn(`Invalid config: ${e}`)));
updateConfig().catch(warn); updateConfig().catch(e => console.warn(`Invalid config: ${e}`));
}; };
const config = { const DEFAULTS = {
// Modules // Modules
bar: { bar: {
vertical: s(true), vertical: true,
modules: { modules: {
osIcon: { osIcon: {
enabled: s(true), enabled: true,
}, },
activeWindow: { activeWindow: {
enabled: s(true), enabled: true,
}, },
mediaPlaying: { mediaPlaying: {
enabled: s(true), enabled: true,
}, },
workspaces: { workspaces: {
enabled: s(true), enabled: true,
shown: s(5), shown: 5,
}, },
tray: { tray: {
enabled: s(true), enabled: true,
}, },
statusIcons: { statusIcons: {
enabled: s(true), enabled: true,
}, },
pkgUpdates: { pkgUpdates: {
enabled: s(true), enabled: true,
}, },
notifCount: { notifCount: {
enabled: s(true), enabled: true,
}, },
battery: { battery: {
enabled: s(true), enabled: true,
}, },
dateTime: { dateTime: {
enabled: s(true), enabled: true,
format: s("%d/%m/%y %R"), format: "%d/%m/%y %R",
detailedFormat: s("%c"), detailedFormat: "%c",
}, },
power: { power: {
enabled: s(true), enabled: true,
}, },
}, },
}, },
launcher: { launcher: {
maxResults: s(15), // Max shown results at one time (i.e. max height of the launcher) maxResults: 15, // Max shown results at one time (i.e. max height of the launcher)
apps: { apps: {
maxResults: s(30), // Actual max results, -1 for infinite maxResults: 30, // Actual max results, -1 for infinite
pins: s([ pins: [
["zen", "firefox", "waterfox", "google-chrome", "chromium", "brave-browser"], ["zen", "firefox", "waterfox", "google-chrome", "chromium", "brave-browser"],
["foot", "alacritty", "kitty", "wezterm"], ["foot", "alacritty", "kitty", "wezterm"],
["thunar", "nemo", "nautilus"], ["thunar", "nemo", "nautilus"],
["codium", "code", "clion", "intellij-idea-ultimate-edition"], ["codium", "code", "clion", "intellij-idea-ultimate-edition"],
["spotify-adblock", "spotify", "audacious", "elisa"], ["spotify-adblock", "spotify", "audacious", "elisa"],
]), ],
}, },
files: { files: {
maxResults: s(40), // Actual max results, -1 for infinite maxResults: 40, // Actual max results, -1 for infinite
fdOpts: s(["-a", "-t", "f"]), // Options to pass to `fd` fdOpts: ["-a", "-t", "f"], // Options to pass to `fd`
}, },
math: { math: {
maxResults: s(40), // Actual max results, -1 for infinite maxResults: 40, // Actual max results, -1 for infinite
}, },
windows: { windows: {
maxResults: s(-1), // Actual max results, -1 for infinite maxResults: -1, // Actual max results, -1 for infinite
weights: { weights: {
// Weights for fuzzy sort // Weights for fuzzy sort
title: s(1), title: 1,
class: s(1), class: 1,
initialTitle: s(0.5), initialTitle: 0.5,
initialClass: s(0.5), initialClass: 0.5,
}, },
}, },
todo: { todo: {
notify: s(true), notify: true,
}, },
}, },
notifpopups: { notifpopups: {
maxPopups: s(-1), maxPopups: -1,
expire: s(false), expire: false,
agoTime: s(true), // Whether to show time in ago format, e.g. 10 mins ago, or raw time, e.g. 10:42 agoTime: true, // Whether to show time in ago format, e.g. 10 mins ago, or raw time, e.g. 10:42
}, },
osds: { osds: {
volume: { volume: {
position: s(Astal.WindowAnchor.RIGHT), position: Astal.WindowAnchor.RIGHT,
margin: s(20), margin: 20,
hideDelay: s(1500), hideDelay: 1500,
showValue: s(true), showValue: true,
}, },
brightness: { brightness: {
position: s(Astal.WindowAnchor.LEFT), position: Astal.WindowAnchor.LEFT,
margin: s(20), margin: 20,
hideDelay: s(1500), hideDelay: 1500,
showValue: s(true), showValue: true,
}, },
lock: { lock: {
spacing: s(5), spacing: 5,
caps: { caps: {
hideDelay: s(1000), hideDelay: 1000,
}, },
num: { num: {
hideDelay: s(1000), hideDelay: 1000,
},
},
},
sideleft: {
directories: {
left: {
top: "󰉍 Downloads",
middle: "󱧶 Documents",
bottom: "󱍙 Music",
},
right: {
top: "󰉏 Pictures",
middle: "󰉏 Videos",
bottom: "󱂵 Home",
}, },
}, },
}, },
// Services // Services
math: { math: {
maxHistory: s(100), maxHistory: 100,
}, },
updates: { updates: {
interval: s(900000), interval: 900000,
}, },
weather: { weather: {
interval: s(600000), interval: 600000,
key: s("assets/weather-api-key.txt"), // Path to file containing api key relative to the base directory. To get a key, visit https://weatherapi.com/ key: "assets/weather-api-key.txt", // Path to file containing api key relative to the base directory. To get a key, visit https://weatherapi.com/
location: s(""), // Location as a string or empty to autodetect location: "", // Location as a string or empty to autodetect
imperial: s(false), imperial: false,
}, },
cpu: { cpu: {
interval: s(2000), interval: 2000,
}, },
gpu: { gpu: {
interval: s(2000), interval: 2000,
}, },
memory: { memory: {
interval: s(5000), interval: 5000,
}, },
storage: { storage: {
interval: s(5000), interval: 5000,
}, },
}; };
export const { bar, launcher, notifpopups, osds, math, updates, weather, cpu, gpu, memory, storage } = config; const config = convertSettings(DEFAULTS);
export const { bar, launcher, notifpopups, osds, sideleft, math, updates, weather, cpu, gpu, memory, storage } = config;

View file

@ -7,6 +7,7 @@ import PopupWindow from "@/widgets/popupwindow";
import { bind, execAsync, GLib, type Binding } from "astal"; import { bind, execAsync, GLib, type Binding } from "astal";
import { App, Gtk, type Widget } from "astal/gtk3"; import { App, Gtk, type Widget } from "astal/gtk3";
import type cairo from "cairo"; import type cairo from "cairo";
import { sideleft } from "config";
const fmt = (bytes: number, pow: number) => +(bytes / 1024 ** pow).toFixed(2); const fmt = (bytes: number, pow: number) => +(bytes / 1024 ** pow).toFixed(2);
const format = ({ total, used }: { total: number; used: number }) => { const format = ({ total, used }: { total: number; used: number }) => {
@ -62,13 +63,14 @@ const QuickLaunch = () => (
</box> </box>
); );
const Location = ({ home, label, num }: { home?: boolean; label: string; num: number }) => ( const Location = ({ label, num }: { label: Binding<string>; num: number }) => (
<button <button
className={"loc" + num} className={"loc" + num}
cursor="pointer" cursor="pointer"
onClicked={self => { onClicked={self => {
self.get_toplevel().hide(); self.get_toplevel().hide();
execAsync(`xdg-open ${HOME}/${home ? "" : label.split(" ").at(-1) + "/"}`).catch(console.error); const dir = label.get().split(" ").at(-1);
execAsync(`xdg-open ${HOME}/${dir?.toLowerCase() === "home" ? "" : `${dir}/`}`).catch(console.error);
}} }}
> >
<label xalign={0} label={label} /> <label xalign={0} label={label} />
@ -78,14 +80,14 @@ const Location = ({ home, label, num }: { home?: boolean; label: string; num: nu
const Locations = () => ( const Locations = () => (
<box className="locations"> <box className="locations">
<box vertical> <box vertical>
<Location label="󰉍 Downloads" num={1} /> <Location label={bind(sideleft.directories.left.top)} num={1} />
<Location label="󱧶 Documents" num={2} /> <Location label={bind(sideleft.directories.left.middle)} num={2} />
<Location label="󱍙 Music" num={3} /> <Location label={bind(sideleft.directories.left.bottom)} num={3} />
</box> </box>
<box vertical> <box vertical>
<Location label="󰉏 Pictures" num={4} /> <Location label={bind(sideleft.directories.right.top)} num={4} />
<Location label="󰉏 Videos" num={5} /> <Location label={bind(sideleft.directories.right.middle)} num={5} />
<Location label="󱂵 Home" num={6} home /> <Location label={bind(sideleft.directories.right.bottom)} num={6} />
</box> </box>
</box> </box>
); );