schemes: update for scripts refactor

This commit is contained in:
2 * r + 2 * t 2025-03-05 17:37:43 +11:00
parent 9d1ee02d2e
commit d46b368606
6 changed files with 171 additions and 85 deletions

View file

@ -1,7 +1,6 @@
import { Apps } from "@/services/apps";
import type { IPalette } from "@/services/palette";
import Palette from "@/services/palette";
import Schemes from "@/services/schemes";
import Schemes, { type Colours } from "@/services/schemes";
import Wallpapers from "@/services/wallpapers";
import { basename } from "@/utils/strings";
import { notify } from "@/utils/system";
@ -30,6 +29,12 @@ const autocomplete = (entry: Widget.Entry, action: string) => {
entry.set_position(-1);
};
const hasMode = (mode: "light" | "dark") => {
const scheme = Schemes.get_default().map[Palette.get_default().scheme];
if (scheme.colours?.[mode]) return true;
return scheme.flavours?.[Palette.get_default().flavour ?? ""]?.colours?.[mode] !== undefined;
};
const actions = (mode: Variable<Mode>, entry: Widget.Entry): ActionMap => ({
apps: {
icon: "apps",
@ -70,22 +75,22 @@ const actions = (mode: Variable<Mode>, entry: Widget.Entry): ActionMap => ({
light: {
icon: "light_mode",
name: "Light",
description: "Change dynamic scheme to light mode",
description: "Change scheme to light mode",
action: () => {
execAsync(`caelestia wallpaper -T light -f ${STATE}/wallpaper/current`).catch(console.error);
close();
},
available: () => Palette.get_default().name === "dynamic",
available: () => hasMode("light"),
},
dark: {
icon: "dark_mode",
name: "Dark",
description: "Change dynamic scheme to dark mode",
description: "Change scheme to dark mode",
action: () => {
execAsync(`caelestia wallpaper -T dark -f ${STATE}/wallpaper/current`).catch(console.error);
close();
},
available: () => Palette.get_default().name === "dynamic",
available: () => hasMode("dark"),
},
scheme: {
icon: "palette",
@ -244,44 +249,47 @@ const Action = ({ args, icon, name, description, action }: IAction & { args: str
const Swatch = ({ colour }: { colour: string }) => <box className="swatch" css={"background-color: " + colour + ";"} />;
const Scheme = ({ name, colours }: { name: string; colours: IPalette }) => (
<Gtk.FlowBoxChild visible canFocus={false}>
<button
className="result"
cursor="pointer"
onClicked={() => {
execAsync(`caelestia scheme ${name}`).catch(console.error);
close();
}}
>
<box>
<box valign={Gtk.Align.CENTER}>
<box className="swatch big left" css={"background-color: " + colours.base + ";"} />
<box className="swatch big right" css={"background-color: " + colours.accent + ";"} />
</box>
<box vertical className="has-sublabel">
<label truncate xalign={0} label={name} />
<box className="swatches">
<Swatch colour={colours.rosewater} />
<Swatch colour={colours.flamingo} />
<Swatch colour={colours.pink} />
<Swatch colour={colours.mauve} />
<Swatch colour={colours.red} />
<Swatch colour={colours.maroon} />
<Swatch colour={colours.peach} />
<Swatch colour={colours.yellow} />
<Swatch colour={colours.green} />
<Swatch colour={colours.teal} />
<Swatch colour={colours.sky} />
<Swatch colour={colours.sapphire} />
<Swatch colour={colours.blue} />
<Swatch colour={colours.lavender} />
const Scheme = ({ scheme, name, colours }: { scheme?: string; name: string; colours?: Colours }) => {
const palette = colours![Palette.get_default().mode] ?? colours!.light ?? colours!.dark!;
return (
<Gtk.FlowBoxChild visible canFocus={false}>
<button
className="result"
cursor="pointer"
onClicked={() => {
execAsync(`caelestia scheme ${scheme ?? ""} ${name}`).catch(console.error);
close();
}}
>
<box>
<box valign={Gtk.Align.CENTER}>
<box className="swatch big left" css={"background-color: " + palette.base + ";"} />
<box className="swatch big right" css={"background-color: " + palette.accent + ";"} />
</box>
<box vertical className="has-sublabel">
<label truncate xalign={0} label={scheme ? `${scheme} (${name})` : name} />
<box className="swatches">
<Swatch colour={palette.rosewater} />
<Swatch colour={palette.flamingo} />
<Swatch colour={palette.pink} />
<Swatch colour={palette.mauve} />
<Swatch colour={palette.red} />
<Swatch colour={palette.maroon} />
<Swatch colour={palette.peach} />
<Swatch colour={palette.yellow} />
<Swatch colour={palette.green} />
<Swatch colour={palette.teal} />
<Swatch colour={palette.sky} />
<Swatch colour={palette.sapphire} />
<Swatch colour={palette.blue} />
<Swatch colour={palette.lavender} />
</box>
</box>
</box>
</box>
</button>
</Gtk.FlowBoxChild>
);
</button>
</Gtk.FlowBoxChild>
);
};
const Wallpaper = ({ path, thumbnail }: { path: string; thumbnail?: string }) => (
<Gtk.FlowBoxChild visible canFocus={false}>
@ -334,8 +342,17 @@ export default class Actions extends Widget.Box implements LauncherContent {
if (action === "scheme") {
const scheme = args[1] ?? "";
for (const { target } of fuzzysort.go(scheme, Object.keys(Schemes.get_default().map), { all: true }))
this.#content.add(<Scheme name={target} colours={Schemes.get_default().map[target]} />);
const schemes = Object.values(Schemes.get_default().map)
.flatMap(s => (s.colours ? s.name : Object.values(s.flavours!).map(f => `${f.scheme}-${f.name}`)))
.filter(s => s !== undefined);
for (const { target } of fuzzysort.go(scheme, schemes, { all: true })) {
if (Schemes.get_default().map.hasOwnProperty(target))
this.#content.add(<Scheme {...Schemes.get_default().map[target]} />);
else {
const [scheme, flavour] = target.split("-");
this.#content.add(<Scheme {...Schemes.get_default().map[scheme].flavours![flavour]} />);
}
}
} else if (action === "wallpaper") {
const wallpaper = args[1] ?? "";
for (const { obj } of fuzzysort.go(wallpaper, Wallpapers.get_default().list, { all: true, key: "path" }))

View file

@ -41,18 +41,24 @@ export default class Palette extends GObject.Object {
return this.instance;
}
#isLight: boolean;
#name: string;
#mode: "light" | "dark";
#scheme: string;
#flavour?: string;
#colours!: IPalette;
@property(Boolean)
get isLight() {
return this.#isLight;
get mode() {
return this.#mode;
}
@property(String)
get name() {
return this.#name;
get scheme() {
return this.#scheme;
}
@property(String)
get flavour() {
return this.#flavour;
}
@property(Object)
@ -246,16 +252,17 @@ export default class Palette extends GObject.Object {
constructor() {
super();
this.#isLight = readFile(`${STATE}/scheme/current-mode.txt`) === "light";
this.#mode = readFile(`${STATE}/scheme/current-mode.txt`) === "light" ? "light" : "dark";
monitorFile(`${STATE}/scheme/current-mode.txt`, async file => {
this.#isLight = (await readFileAsync(file)) === "light";
this.notify("is-light");
this.#mode = (await readFileAsync(file)) === "light" ? "light" : "dark";
this.notify("mode");
});
this.#name = readFile(`${STATE}/scheme/current-name.txt`);
[this.#scheme, this.#flavour] = readFile(`${STATE}/scheme/current-name.txt`).split("-");
monitorFile(`${STATE}/scheme/current-name.txt`, async file => {
this.#name = await readFileAsync(file);
this.notify("name");
[this.#scheme, this.#flavour] = (await readFileAsync(file)).split("-");
this.notify("scheme");
this.notify("flavour");
});
this.update();

View file

@ -1,7 +1,25 @@
import { basename } from "@/utils/strings";
import { execAsync, GLib, GObject, monitorFile, property, readFileAsync, register } from "astal";
import { monitorDirectory } from "@/utils/system";
import { execAsync, GLib, GObject, property, readFileAsync, register } from "astal";
import type { IPalette } from "./palette";
export interface Colours {
light?: IPalette;
dark?: IPalette;
}
export interface Flavour {
name: string;
scheme: string;
colours: Colours;
}
export interface Scheme {
name: string;
flavours?: { [k: string]: Flavour };
colours?: Colours;
}
const DATA = `${GLib.get_user_data_dir()}/caelestia`;
@register({ GTypeName: "Schemes" })
@ -13,21 +31,58 @@ export default class Schemes extends GObject.Object {
return this.instance;
}
#map: { [k: string]: IPalette } = {};
readonly #schemeDir: string = `${DATA}/scripts/data/schemes`;
#map: { [k: string]: Scheme } = {};
@property(Object)
get map() {
return this.#map;
}
async parseScheme(path: string) {
async parseMode(path: string): Promise<IPalette> {
const schemeColours = (await readFileAsync(path)).split("\n").map(l => l.split(" "));
return schemeColours.reduce((acc, [name, hex]) => ({ ...acc, [name]: `#${hex}` }), {} as IPalette);
}
async parseFlavour(scheme: string, name: string): Promise<Flavour> {
const path = `${this.#schemeDir}/${scheme}/${name}`;
let light = undefined;
let dark = undefined;
if (GLib.file_test(`${path}/light.txt`, GLib.FileTest.EXISTS))
light = await this.parseMode(`${path}/light.txt`);
if (GLib.file_test(`${path}/dark.txt`, GLib.FileTest.EXISTS)) dark = await this.parseMode(`${path}/dark.txt`);
return { name, scheme, colours: { light, dark } };
}
async parseScheme(name: string): Promise<Scheme> {
const path = `${this.#schemeDir}/${name}`;
const flavours = await execAsync(`find ${path}/ -mindepth 1 -maxdepth 1 -type d`);
if (flavours.trim())
return {
name,
flavours: (
await Promise.all(flavours.split("\n").map(f => this.parseFlavour(name, basename(f))))
).reduce((acc, f) => ({ ...acc, [f.name]: f }), {} as { [k: string]: Flavour }),
};
let light = undefined;
let dark = undefined;
if (GLib.file_test(`${path}/light.txt`, GLib.FileTest.EXISTS))
light = await this.parseMode(`${path}/light.txt`);
if (GLib.file_test(`${path}/dark.txt`, GLib.FileTest.EXISTS)) dark = await this.parseMode(`${path}/dark.txt`);
return { name, colours: { light, dark } };
}
async update() {
const schemes = await execAsync(`find ${DATA}/scripts/data/schemes/ -type f`);
for (const scheme of schemes.split("\n")) this.#map[basename(scheme)] = await this.parseScheme(scheme);
const schemes = await execAsync(`find ${this.#schemeDir}/ -mindepth 1 -maxdepth 1 -type d`);
for (const scheme of schemes.split("\n")) {
const name = basename(scheme);
this.#map[name] = await this.parseScheme(name);
}
this.notify("map");
}
@ -35,6 +90,6 @@ export default class Schemes extends GObject.Object {
super();
this.update().catch(console.error);
monitorFile(`${DATA}/scripts/data/schemes`, () => this.update().catch(console.error));
monitorDirectory(this.#schemeDir, () => this.update().catch(console.error), true);
}
}

View file

@ -1,5 +1,6 @@
import { basename } from "@/utils/strings";
import { execAsync, Gio, GLib, GObject, property, register } from "astal";
import { monitorDirectory } from "@/utils/system";
import { execAsync, GLib, GObject, property, register } from "astal";
import { wallpapers as config } from "config";
export interface Wallpaper {
@ -55,28 +56,12 @@ export default class Wallpapers extends GObject.Object {
this.update().catch(console.error);
const monitorDir = ({ path, recursive }: { path: string; recursive: boolean }) => {
const file = Gio.file_new_for_path(path.replace("~", HOME));
const monitor = file.monitor_directory(null, null);
monitor.connect("changed", () => this.update().catch(console.error));
const monitors = [monitor];
if (recursive) {
const enumerator = file.enumerate_children("standard::*", null, null);
let child;
while ((child = enumerator.next_file(null)))
if (child.get_file_type() === Gio.FileType.DIRECTORY)
monitors.push(...monitorDir({ path: `${path}/${child.get_name()}`, recursive }));
}
return monitors;
};
let monitors = config.paths.get().flatMap(monitorDir);
let monitors = config.paths
.get()
.flatMap(p => monitorDirectory(p.path, () => this.update().catch(console.error), p.recursive));
config.paths.subscribe(v => {
for (const m of monitors) m.cancel();
monitors = v.flatMap(monitorDir);
monitors = v.flatMap(p => monitorDirectory(p.path, () => this.update().catch(console.error), p.recursive));
});
}
}

View file

@ -1,3 +1,7 @@
export const ellipsize = (str: string, len: number) => (str.length > len ? `${str.slice(0, len - 1)}` : str);
export const basename = (path: string) => path.slice(path.lastIndexOf("/") + 1, path.lastIndexOf("."));
export const basename = (path: string) => {
const lastSlash = path.lastIndexOf("/");
const lastDot = path.lastIndexOf(".");
return path.slice(lastSlash + 1, lastDot > lastSlash ? lastDot : undefined);
};

View file

@ -1,4 +1,4 @@
import { bind, execAsync, GLib, Variable, type Binding, type Gio } from "astal";
import { bind, execAsync, Gio, GLib, Variable, type Binding } from "astal";
import type AstalApps from "gi://AstalApps";
import { osIcons } from "./icons";
@ -75,3 +75,21 @@ export const bindCurrentTime = (
self?.connect("destroy", () => time.drop());
return bind(time);
};
export const monitorDirectory = (path: string, callback: (path: string) => void, recursive?: boolean) => {
const file = Gio.file_new_for_path(path.replace("~", HOME));
const monitor = file.monitor_directory(null, null);
monitor.connect("changed", callback);
const monitors = [monitor];
if (recursive) {
const enumerator = file.enumerate_children("standard::*", null, null);
let child;
while ((child = enumerator.next_file(null)))
if (child.get_file_type() === Gio.FileType.DIRECTORY)
monitors.push(...monitorDirectory(`${path}/${child.get_name()}`, callback, recursive));
}
return monitors;
};