diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/modules/launcher/actions.tsx | 105 | ||||
| -rw-r--r-- | src/services/palette.ts | 31 | ||||
| -rw-r--r-- | src/services/schemes.ts | 67 | ||||
| -rw-r--r-- | src/services/wallpapers.ts | 27 | ||||
| -rw-r--r-- | src/utils/strings.ts | 6 | ||||
| -rw-r--r-- | src/utils/system.ts | 20 |
6 files changed, 171 insertions, 85 deletions
diff --git a/src/modules/launcher/actions.tsx b/src/modules/launcher/actions.tsx index 0ded45f..c45273d 100644 --- a/src/modules/launcher/actions.tsx +++ b/src/modules/launcher/actions.tsx @@ -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" })) diff --git a/src/services/palette.ts b/src/services/palette.ts index 7f6dc0c..da7ef09 100644 --- a/src/services/palette.ts +++ b/src/services/palette.ts @@ -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(); diff --git a/src/services/schemes.ts b/src/services/schemes.ts index 4c51744..3f248bc 100644 --- a/src/services/schemes.ts +++ b/src/services/schemes.ts @@ -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); } } diff --git a/src/services/wallpapers.ts b/src/services/wallpapers.ts index 7d29a14..39bced4 100644 --- a/src/services/wallpapers.ts +++ b/src/services/wallpapers.ts @@ -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)); }); } } diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 2fd4c76..c2bf71a 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -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); +}; diff --git a/src/utils/system.ts b/src/utils/system.ts index 7ae23dd..d0470c7 100644 --- a/src/utils/system.ts +++ b/src/utils/system.ts @@ -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; +}; |