diff options
| -rw-r--r-- | app.tsx | 2 | ||||
| -rw-r--r-- | config.ts | 27 | ||||
| -rw-r--r-- | scss/launcher.scss | 32 | ||||
| -rw-r--r-- | src/modules/launcher/actions.tsx | 28 | ||||
| -rw-r--r-- | src/services/schemes.ts | 8 | ||||
| -rw-r--r-- | src/services/wallpapers.ts | 81 | ||||
| -rw-r--r-- | src/utils/strings.ts | 2 |
7 files changed, 173 insertions, 7 deletions
@@ -8,6 +8,7 @@ import Monitors from "@/services/monitors"; import Palette from "@/services/palette"; import Players from "@/services/players"; import Schemes from "@/services/schemes"; +import Wallpapers from "@/services/wallpapers"; import type PopupWindow from "@/widgets/popupwindow"; import { execAsync, idle, writeFileAsync } from "astal"; import { App } from "astal/gtk3"; @@ -41,6 +42,7 @@ App.start({ // Init services idle(() => Schemes.get_default()); + idle(() => Wallpapers.get_default()); console.log(`Caelestia started in ${Date.now() - now}ms`); }, @@ -121,6 +121,9 @@ const DEFAULTS = { todo: { notify: true, }, + wallpaper: { + style: "compact", // One of "compact", "medium", "large" + }, }, notifpopups: { maxPopups: -1, @@ -190,8 +193,30 @@ const DEFAULTS = { storage: { interval: 5000, }, + wallpapers: { + paths: [ + { + recursive: true, // Whether to search recursively + path: "~/Pictures/Wallpapers", // Path to search + }, + ], + }, }; const config = convertSettings(DEFAULTS); -export const { bar, launcher, notifpopups, osds, sideleft, math, updates, weather, cpu, gpu, memory, storage } = config; +export const { + bar, + launcher, + notifpopups, + osds, + sideleft, + math, + updates, + weather, + cpu, + gpu, + memory, + storage, + wallpapers, +} = config; diff --git a/scss/launcher.scss b/scss/launcher.scss index 49f09c8..983f362 100644 --- a/scss/launcher.scss +++ b/scss/launcher.scss @@ -153,4 +153,36 @@ } } } + + .wallpaper { + margin: lib.s(3) 0; + + .thumbnail { + background-size: cover; + background-position: center; + } + + &.compact .thumbnail { + @include lib.rounded(100); + + min-width: lib.s(32); + min-height: lib.s(32); + } + + &:not(.compact) { + @include lib.spacing(3, true); + + .thumbnail { + @include lib.rounded(10); + } + } + + &.medium .thumbnail { + min-height: lib.s(96); + } + + &.large .thumbnail { + min-height: lib.s(160); + } + } } diff --git a/src/modules/launcher/actions.tsx b/src/modules/launcher/actions.tsx index 75f1092..a8c5d0a 100644 --- a/src/modules/launcher/actions.tsx +++ b/src/modules/launcher/actions.tsx @@ -1,6 +1,8 @@ import { Apps } from "@/services/apps"; import type { IPalette } from "@/services/palette"; import Schemes from "@/services/schemes"; +import Wallpapers from "@/services/wallpapers"; +import { basename } from "@/utils/strings"; import { notify } from "@/utils/system"; import { setupCustomTooltip, type FlowBox } from "@/utils/widgets"; import { execAsync, GLib, readFile, register, type Variable } from "astal"; @@ -218,6 +220,28 @@ const Scheme = ({ name, colours }: { name: string; colours: IPalette }) => ( </Gtk.FlowBoxChild> ); +const Wallpaper = ({ path, thumbnail }: { path: string; thumbnail?: string }) => ( + <Gtk.FlowBoxChild visible canFocus={false}> + <button + className="result" + cursor="pointer" + onClicked={() => { + execAsync(`caelestia wallpaper -f ${path}`).catch(console.error); + close(); + }} + setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))} + > + <box + vertical={config.wallpaper.style.get() !== "compact"} + className={`wallpaper ${config.wallpaper.style.get()}`} + > + <box className="thumbnail" css={"background-image: url('" + (thumbnail ?? path) + "');"} /> + <label truncate label={basename(path)} /> + </box> + </button> + </Gtk.FlowBoxChild> +); + @register() export default class Actions extends Widget.Box implements LauncherContent { #map: ActionMap; @@ -249,6 +273,10 @@ export default class Actions extends Widget.Box implements LauncherContent { 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]} />); + } else if (action === "wallpaper") { + const wallpaper = args[1] ?? ""; + for (const { obj } of fuzzysort.go(wallpaper, Wallpapers.get_default().list, { all: true, key: "path" })) + this.#content.add(<Wallpaper {...obj} />); } else { for (const { target } of fuzzysort.go(action, this.#list, { all: true })) this.#content.add(<Action {...this.#map[target]} args={args.slice(1)} />); diff --git a/src/services/schemes.ts b/src/services/schemes.ts index 07d6ded..4c51744 100644 --- a/src/services/schemes.ts +++ b/src/services/schemes.ts @@ -1,3 +1,4 @@ +import { basename } from "@/utils/strings"; import { execAsync, GLib, GObject, monitorFile, property, readFileAsync, register } from "astal"; import type { IPalette } from "./palette"; @@ -19,10 +20,6 @@ export default class Schemes extends GObject.Object { return this.#map; } - #schemePathToName(path: string) { - return path.slice(path.lastIndexOf("/") + 1, path.lastIndexOf(".")); - } - async parseScheme(path: string) { const schemeColours = (await readFileAsync(path)).split("\n").map(l => l.split(" ")); return schemeColours.reduce((acc, [name, hex]) => ({ ...acc, [name]: `#${hex}` }), {} as IPalette); @@ -30,8 +27,7 @@ export default class Schemes extends GObject.Object { async update() { const schemes = await execAsync(`find ${DATA}/scripts/data/schemes/ -type f`); - for (const scheme of schemes.split("\n")) - this.#map[this.#schemePathToName(scheme)] = await this.parseScheme(scheme); + for (const scheme of schemes.split("\n")) this.#map[basename(scheme)] = await this.parseScheme(scheme); this.notify("map"); } diff --git a/src/services/wallpapers.ts b/src/services/wallpapers.ts new file mode 100644 index 0000000..e7a8742 --- /dev/null +++ b/src/services/wallpapers.ts @@ -0,0 +1,81 @@ +import { basename } from "@/utils/strings"; +import { execAsync, Gio, GLib, GObject, property, register } from "astal"; +import { wallpapers as config } from "config"; + +export interface Wallpaper { + path: string; + thumbnail?: string; +} + +@register({ GTypeName: "Wallpapers" }) +export default class Wallpapers extends GObject.Object { + static instance: Wallpapers; + static get_default() { + if (!this.instance) this.instance = new Wallpapers(); + + return this.instance; + } + + #thumbnailDir = `${CACHE}/thumbnails`; + + #list: Wallpaper[] = []; + + @property(Object) + get list() { + return this.#list; + } + + async #thumbnail(path: string) { + const thumbPath = `${this.#thumbnailDir}/${basename(path)}.jpg`; + await execAsync(`magick -define jpeg:size=1000x500 ${path} -thumbnail 500x250 -unsharp 0x.5 ${thumbPath}`); + return thumbPath; + } + + async update() { + const results = await Promise.allSettled( + config.paths + .get() + .map(p => execAsync(`find ${p.path.replace("~", HOME)}/ ${p.recursive ? "" : "-maxdepth 1"} -type f`)) + ); + const files = results + .filter(r => r.status === "fulfilled") + .map(r => r.value.replaceAll("\n", " ")) + .join(" "); + const list = (await execAsync(["fish", "-c", `identify -ping -format '%i\n' ${files} ; true`])).split("\n"); + + this.#list = await Promise.all(list.map(async p => ({ path: p, thumbnail: await this.#thumbnail(p) }))); + this.notify("list"); + } + + constructor() { + super(); + + GLib.mkdir_with_parents(this.#thumbnailDir, 0o755); + + 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); + config.paths.subscribe(v => { + for (const m of monitors) m.cancel(); + monitors = v.flatMap(monitorDir); + }); + } +} diff --git a/src/utils/strings.ts b/src/utils/strings.ts index df2f781..2fd4c76 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -1 +1,3 @@ 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(".")); |