summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-04-26 22:36:23 +1000
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-04-26 22:36:23 +1000
commit3c579d0e275cdaf6f2c9589abade94bde7905c82 (patch)
tree4b825dc642cb6eb9a060e54bf8d69288fbee4904
parentschemes: fix (diff)
downloadcaelestia-shell-3c579d0e275cdaf6f2c9589abade94bde7905c82.tar.gz
caelestia-shell-3c579d0e275cdaf6f2c9589abade94bde7905c82.tar.bz2
caelestia-shell-3c579d0e275cdaf6f2c9589abade94bde7905c82.zip
clean
Remove everything
-rw-r--r--.gitignore3
-rw-r--r--.vscode/settings.json3
-rw-r--r--app.tsx132
-rw-r--r--assets/icons/caelestia-bluetooth-device-symbolic.svg6
-rw-r--r--assets/icons/caelestia-consecutive-symbolic.svg5
-rw-r--r--assets/icons/caelestia-feishin-symbolic.svg4
-rw-r--r--assets/icons/caelestia-media-generic-symbolic.svg2
-rw-r--r--assets/icons/caelestia-media-none-symbolic.svg19
-rw-r--r--assets/icons/caelestia-mozilla-firefox-symbolic.svg4
-rw-r--r--assets/icons/caelestia-no-repeat-symbolic.svg6
-rw-r--r--assets/icons/caelestia-pause-symbolic.svg8
-rw-r--r--assets/icons/caelestia-play-symbolic.svg5
-rw-r--r--assets/icons/caelestia-repeat-one-symbolic.svg5
-rw-r--r--assets/icons/caelestia-repeat-symbolic.svg5
-rw-r--r--assets/icons/caelestia-shuffle-symbolic.svg5
-rw-r--r--assets/icons/caelestia-skip-next-symbolic.svg8
-rw-r--r--assets/icons/caelestia-skip-previous-symbolic.svg8
-rw-r--r--assets/icons/caelestia-spotify-symbolic.svg21
-rw-r--r--env.d.ts4
-rw-r--r--package-lock.json616
-rw-r--r--package.json11
-rwxr-xr-xrun.fish17
-rw-r--r--scss/_font.scss21
-rw-r--r--scss/_lib.scss47
-rw-r--r--scss/bar.scss394
-rw-r--r--scss/common.scss68
-rw-r--r--scss/launcher.scss332
-rw-r--r--scss/mediadisplay.scss139
-rw-r--r--scss/navbar.scss65
-rw-r--r--scss/notifpopups.scss51
-rw-r--r--scss/osds.scss51
-rw-r--r--scss/scheme/_default.scss31
-rw-r--r--scss/session.scss58
-rw-r--r--scss/sidebar.scss1118
-rw-r--r--scss/widgets.scss136
-rw-r--r--src/config/defaults.ts183
-rw-r--r--src/config/funcs.ts124
-rw-r--r--src/config/index.ts26
-rw-r--r--src/config/literals.ts336
-rw-r--r--src/config/types.ts93
-rw-r--r--src/modules/bar.tsx703
-rw-r--r--src/modules/launcher/actions.tsx522
-rw-r--r--src/modules/launcher/index.tsx144
-rw-r--r--src/modules/launcher/modes.tsx225
-rw-r--r--src/modules/launcher/util.tsx19
-rw-r--r--src/modules/mediadisplay/index.tsx188
-rw-r--r--src/modules/mediadisplay/visualiser.tsx71
-rw-r--r--src/modules/navbar.tsx203
-rw-r--r--src/modules/notifpopups.tsx72
-rw-r--r--src/modules/osds.tsx327
-rw-r--r--src/modules/screencorners.tsx51
-rw-r--r--src/modules/session.tsx44
-rw-r--r--src/modules/sidebar/alerts.tsx11
-rw-r--r--src/modules/sidebar/audio.tsx13
-rw-r--r--src/modules/sidebar/connectivity.tsx10
-rw-r--r--src/modules/sidebar/dashboard.tsx132
-rw-r--r--src/modules/sidebar/index.tsx87
-rw-r--r--src/modules/sidebar/modules/bluetooth.tsx127
-rw-r--r--src/modules/sidebar/modules/calendar.tsx252
-rw-r--r--src/modules/sidebar/modules/deviceselector.tsx126
-rw-r--r--src/modules/sidebar/modules/headlines.tsx204
-rw-r--r--src/modules/sidebar/modules/hwresources.tsx67
-rw-r--r--src/modules/sidebar/modules/media.tsx168
-rw-r--r--src/modules/sidebar/modules/networks.tsx151
-rw-r--r--src/modules/sidebar/modules/news.tsx113
-rw-r--r--src/modules/sidebar/modules/notifications.tsx90
-rw-r--r--src/modules/sidebar/modules/streams.tsx110
-rw-r--r--src/modules/sidebar/modules/upcoming.tsx99
-rw-r--r--src/modules/sidebar/modules/updates.tsx109
-rw-r--r--src/modules/sidebar/packages.tsx11
-rw-r--r--src/modules/sidebar/time.tsx24
-rw-r--r--src/services/apps.ts3
-rw-r--r--src/services/calendar.ts228
-rw-r--r--src/services/cpu.ts49
-rw-r--r--src/services/gpu.ts63
-rw-r--r--src/services/math.ts155
-rw-r--r--src/services/memory.ts64
-rw-r--r--src/services/monitors.ts127
-rw-r--r--src/services/news.ts153
-rw-r--r--src/services/palette.ts298
-rw-r--r--src/services/players.ts148
-rw-r--r--src/services/schemes.ts109
-rw-r--r--src/services/storage.ts65
-rw-r--r--src/services/updates.ts191
-rw-r--r--src/services/wallpapers.ts127
-rw-r--r--src/services/weather.ts388
-rw-r--r--src/utils/icons.ts158
-rw-r--r--src/utils/mpris.ts16
-rw-r--r--src/utils/strings.ts18
-rw-r--r--src/utils/system.ts111
-rw-r--r--src/utils/thumbnailer.ts80
-rw-r--r--src/utils/types.ts35
-rw-r--r--src/utils/widgets.ts82
-rw-r--r--src/widgets/notification.tsx179
-rw-r--r--src/widgets/popupwindow.ts75
-rw-r--r--src/widgets/screencorner.tsx49
-rw-r--r--src/widgets/slider.tsx64
-rw-r--r--style.scss25
-rw-r--r--tsconfig.json21
99 files changed, 0 insertions, 11724 deletions
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 89341d4..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-@girs/
-node_modules/
-scss/scheme/_index.scss
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 72446f4..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "typescript.tsdk": "node_modules/typescript/lib"
-}
diff --git a/app.tsx b/app.tsx
deleted file mode 100644
index dcc93d8..0000000
--- a/app.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import Bar from "@/modules/bar";
-import Launcher from "@/modules/launcher";
-import MediaDisplay from "@/modules/mediadisplay";
-import NavBar from "@/modules/navbar";
-import NotifPopups from "@/modules/notifpopups";
-import Osds from "@/modules/osds";
-import ScreenCorners, { BarScreenCorners } from "@/modules/screencorners";
-import Session from "@/modules/session";
-import SideBar from "@/modules/sidebar";
-import Calendar from "@/services/calendar";
-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 { execAsync, idle, timeout, writeFileAsync } from "astal";
-import { App } from "astal/gtk3";
-import { style } from "config";
-import { initConfig, updateConfig } from "config/funcs";
-
-const isLayer = (name: string) =>
- ["base", "mantle", "crust"].includes(name) || name.startsWith("surface") || name.startsWith("overlay");
-
-const applyTransparency = (name: string, hex: string) => {
- const mode = style.transparency.get();
- if (mode === "off" || !isLayer(name)) return hex;
- let amount = 0.78;
- if (mode === "low") amount = 0.88;
- else if (mode === "high") amount = 0.58;
- return `color.change(${hex}, $alpha: ${amount})`;
-};
-
-const applyVibrancy = (hex: string) => (style.vibrant.get() ? `color.scale(${hex}, $saturation: 40%)` : hex);
-
-const getVars = () => {
- const vars = { light: Palette.get_default().mode === "light", borders: style.borders.get() };
- return Object.entries(vars)
- .map(([k, v]) => `$${k}: ${v}`)
- .join(";");
-};
-
-const styleLoader = new (class {
- #running = false;
- #dirty = false;
-
- async run() {
- this.#dirty = true;
- if (this.#running) return;
- this.#running = true;
- while (this.#dirty) {
- this.#dirty = false;
- await this.#run();
- }
- this.#running = false;
- }
-
- async #run() {
- const schemeColours = Object.entries(Palette.get_default().colours)
- .map(([name, hex]) => `$${name}: ${applyVibrancy(applyTransparency(name, hex))};`)
- .join("\n");
- await writeFileAsync(`${SRC}/scss/scheme/_index.scss`, `@use "sass:color";\n${getVars()};\n${schemeColours}`);
- App.apply_css(await execAsync(`sass ${SRC}/style.scss`), true);
- }
-})();
-
-export const loadStyleAsync = () => styleLoader.run();
-
-App.start({
- instanceName: "caelestia",
- icons: "assets/icons",
- async main() {
- try {
- const now = Date.now();
-
- await initConfig();
-
- loadStyleAsync().catch(console.error);
- Palette.get_default().connect("notify::colours", () => loadStyleAsync().catch(console.error));
- Palette.get_default().connect("notify::mode", () => loadStyleAsync().catch(console.error));
-
- <Launcher />;
- <Osds />;
- <Session />;
- Monitors.get_default().forEach(m => <NotifPopups monitor={m} />);
- Monitors.get_default().forEach(m => <MediaDisplay monitor={m} />);
- Monitors.get_default().forEach(m => <SideBar monitor={m} />);
- Monitors.get_default().forEach(m => <NavBar monitor={m} />);
- Monitors.get_default().forEach(m => <Bar monitor={m} />);
- Monitors.get_default().forEach(m => <ScreenCorners monitor={m} />);
- Monitors.get_default().forEach(m => <BarScreenCorners monitor={m} />);
-
- // Init services
- timeout(5000, () => {
- idle(() => Schemes.get_default());
- idle(() => Wallpapers.get_default());
- idle(() => Calendar.get_default());
- });
-
- console.log(`Caelestia started in ${Date.now() - now}ms`);
- } catch (e) {
- console.error(e);
- }
- },
- requestHandler(request, res) {
- if (request === "reload-css") loadStyleAsync().catch(console.error);
- else if (request === "reload-config") updateConfig();
- else if (request.startsWith("show")) App.get_window(request.split(" ")[1])?.show();
- else if (request.startsWith("toggle"))
- App.toggle_window(request.split(" ")[1] + Monitors.get_default().active.id);
- else if (request === "media play-pause") Players.get_default().lastPlayer?.play_pause();
- else if (request === "media next") Players.get_default().lastPlayer?.next();
- else if (request === "media previous") Players.get_default().lastPlayer?.previous();
- else if (request === "media stop") Players.get_default().lastPlayer?.stop();
- else if (request.startsWith("media")) {
- const player = Players.get_default().lastPlayer;
- const key = request.split(" ")[1];
- if (player === null) return res("No media");
- if (key in player) return res(player[key as keyof typeof player]);
- return res(`Invalid key: ${key}`);
- } else if (request.startsWith("brightness")) {
- const value = request.split(" ")[1];
- const num = parseFloat(value) / (value.includes("%") ? 100 : 1);
- if (isNaN(num)) return res("Syntax: brightness <value>[%][+ | -]");
- if (value.includes("+")) Monitors.get_default().active.brightness += num;
- else if (value.includes("-")) Monitors.get_default().active.brightness -= num;
- else Monitors.get_default().active.brightness = num;
- } else return res("Unknown command: " + request);
-
- console.log(`Request handled: ${request}`);
- res("OK");
- },
-});
diff --git a/assets/icons/caelestia-bluetooth-device-symbolic.svg b/assets/icons/caelestia-bluetooth-device-symbolic.svg
deleted file mode 100644
index 86b126f..0000000
--- a/assets/icons/caelestia-bluetooth-device-symbolic.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" standalone="no"?>
-<svg width="128" height="128" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <g stroke="none" stroke-width="1" fill="#000000" fill-rule="evenodd">
- <path d="M9.00553049,9.09762359 L9.00553049,4 C9.00553049,3.12761864 10.0317148,2.69004793 10.6606665,3.24382091 L10.7403339,3.3217199 L15.3111206,8.27340543 C15.6715217,8.66383994 15.6600247,9.26064253 15.3045564,9.6371766 L15.216432,9.71996479 L12.4826517,11.9976955 L15.2168235,14.2781149 C15.6246647,14.6182724 15.6909323,15.2111346 15.3877981,15.6306432 L15.3112739,15.7241817 L10.7404873,20.678114 C10.1488415,21.3193544 9.09779432,20.9449025 9.01123422,20.1112902 L9.00553049,20 L9.00553049,14.8947629 L8.36532009,15.4281734 C7.94101118,15.781699 7.31045167,15.7243178 6.95692602,15.3000089 C6.63059464,14.9083391 6.65438507,14.3409292 6.99370654,13.9778955 L7.08509045,13.8916148 L9.00553049,12.2915428 L9.00553049,11.7019502 L7.08469887,10.0998922 C6.66057024,9.74615033 6.60351053,9.11556165 6.95725244,8.69143301 C7.28378345,8.29992966 7.84620754,8.22119509 8.26439085,8.48962627 L8.36571168,8.56398659 L9.00553049,9.09762359 L9.00553049,4 L9.00553049,9.09762359 Z M11.0055305,13.3700381 L11.0055305,17.4414967 L13.1284383,15.1406365 L11.0055305,13.3700381 Z M18.8818126,7.23876677 C19.3022301,6.88062215 19.9333794,6.9311043 20.291524,7.3515218 C21.3872593,8.63777952 22,10.2703887 22,11.9949347 C22,13.7248622 21.3834157,15.3621939 20.2814798,16.6501128 C19.9224324,17.0697596 19.291176,17.1188851 18.8715292,16.7598377 C18.4518825,16.4007904 18.4027569,15.769534 18.7618043,15.3498872 C19.5566103,14.4209351 20,13.2435196 20,11.9949347 C20,10.7502267 19.5593778,9.57621638 18.7690576,8.6484782 C18.410913,8.2280607 18.4613951,7.59691139 18.8818126,7.23876677 Z M16.6166672,9.23876677 C17.0370847,8.88062215 17.668234,8.9311043 18.0263786,9.3515218 C18.6506,10.0842805 19,11.0152349 19,11.9974674 C19,12.9827668 18.6484092,13.9164117 18.0206587,14.6501128 C17.6616114,15.0697596 17.030355,15.1188851 16.6107082,14.7598377 C16.1910614,14.4007904 16.1419359,13.769534 16.5009833,13.3498872 C16.8216038,12.9751529 17,12.5014241 17,11.9974674 C17,11.4950729 16.8227185,11.0227174 16.5039122,10.6484782 C16.1457676,10.2280607 16.1962497,9.59691139 16.6166672,9.23876677 Z M11.0055305,6.5576505 L11.0055305,10.6251831 L13.1278913,8.85687474 L11.0055305,6.5576505 Z" />
- </g>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-consecutive-symbolic.svg b/assets/icons/caelestia-consecutive-symbolic.svg
deleted file mode 100644
index 5b18fd2..0000000
--- a/assets/icons/caelestia-consecutive-symbolic.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
- <path d="m 12 1 v 2 h -12 v 2 h 12 v 2 h 1 v -0.007812 c 0.265625 0.003906 0.519531 -0.101563 0.707031 -0.285157 l 2 -2 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 l -2 -2 c -0.1875 -0.183594 -0.441406 -0.289063 -0.707031 -0.285157 v -0.007812 z m 0 8 v 2 h -12 v 2 h 12 v 2 h 1 v -0.007812 c 0.265625 0.003906 0.519531 -0.101563 0.707031 -0.285157 l 2 -2 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 l -2 -2 c -0.1875 -0.183594 -0.441406 -0.289063 -0.707031 -0.285157 v -0.007812 z m 0 0" fill="#000000"/>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-feishin-symbolic.svg b/assets/icons/caelestia-feishin-symbolic.svg
deleted file mode 100644
index f1524cc..0000000
--- a/assets/icons/caelestia-feishin-symbolic.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg fill="#000000" version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512">
-<path d="M0 0 C0.9355925 -0.00974854 1.871185 -0.01949707 2.83512878 -0.02954102 C23.24416486 -0.1984297 42.96079539 -0.03089418 63.0078125 4.23828125 C63.93158691 4.42551758 64.85536133 4.61275391 65.80712891 4.80566406 C101.4803425 12.13655694 136.04867954 27.05718327 165.0078125 49.23828125 C165.5857959 49.67495117 166.1637793 50.11162109 166.75927734 50.56152344 C175.26586251 57.002323 183.31244993 63.84319145 191.0078125 71.23828125 C191.60239258 71.80562988 192.19697266 72.37297852 192.80957031 72.95751953 C201.15799553 80.96365501 208.80764092 89.18319073 215.69726562 98.47900391 C217.02578341 100.26240538 218.38141287 102.02237641 219.7421875 103.78125 C227.10148221 113.40694237 233.21551204 123.60687263 239.0078125 134.23828125 C239.3697168 134.89876465 239.73162109 135.55924805 240.10449219 136.23974609 C257.60985188 168.57859478 268.92626162 206.31643928 269.24609375 243.23046875 C269.25584229 244.16606125 269.26559082 245.10165375 269.27563477 246.06559753 C269.44452345 266.47463361 269.27698793 286.19126414 265.0078125 306.23828125 C264.82057617 307.16205566 264.63333984 308.08583008 264.44042969 309.03759766 C257.10953681 344.71081125 242.18891048 379.27914829 220.0078125 408.23828125 C219.57114258 408.81626465 219.13447266 409.39424805 218.68457031 409.98974609 C212.24377075 418.49633126 205.4029023 426.54291868 198.0078125 434.23828125 C197.44046387 434.83286133 196.87311523 435.42744141 196.28857422 436.04003906 C188.28243874 444.38846428 180.06290302 452.03810967 170.76708984 458.92773438 C168.98368837 460.25625216 167.22371734 461.61188162 165.46484375 462.97265625 C155.83915138 470.33195096 145.63922112 476.44598079 135.0078125 482.23828125 C134.3473291 482.60018555 133.6868457 482.96208984 133.00634766 483.33496094 C100.66749897 500.84032063 62.92965447 512.15673037 26.015625 512.4765625 C25.0800325 512.48631104 24.14444 512.49605957 23.18049622 512.50610352 C2.77146014 512.6749922 -16.94517039 512.50745668 -36.9921875 508.23828125 C-37.91596191 508.05104492 -38.83973633 507.86380859 -39.79150391 507.67089844 C-75.4647175 500.34000556 -110.03305454 485.41937923 -138.9921875 463.23828125 C-139.5701709 462.80161133 -140.1481543 462.36494141 -140.74365234 461.91503906 C-149.25023751 455.4742395 -157.29682493 448.63337105 -164.9921875 441.23828125 C-165.58676758 440.67093262 -166.18134766 440.10358398 -166.79394531 439.51904297 C-175.14237053 431.51290749 -182.79201592 423.29337177 -189.68164062 413.99755859 C-191.01015841 412.21415712 -192.36578787 410.45418609 -193.7265625 408.6953125 C-201.08585721 399.06962013 -207.19988704 388.86968987 -212.9921875 378.23828125 C-213.3540918 377.57779785 -213.71599609 376.91731445 -214.08886719 376.23681641 C-231.59422688 343.89796772 -242.91063662 306.16012322 -243.23046875 269.24609375 C-243.24021729 268.31050125 -243.24996582 267.37490875 -243.26000977 266.41096497 C-243.42889845 246.00192889 -243.26136293 226.28529836 -238.9921875 206.23828125 C-238.80495117 205.31450684 -238.61771484 204.39073242 -238.42480469 203.43896484 C-231.09391181 167.76575125 -216.17328548 133.19741421 -193.9921875 104.23828125 C-193.55551758 103.66029785 -193.11884766 103.08231445 -192.66894531 102.48681641 C-186.22814575 93.98023124 -179.3872773 85.93364382 -171.9921875 78.23828125 C-171.42483887 77.64370117 -170.85749023 77.04912109 -170.27294922 76.43652344 C-162.26681374 68.08809822 -154.04727802 60.43845283 -144.75146484 53.54882812 C-142.96806337 52.22031034 -141.20809234 50.86468088 -139.44921875 49.50390625 C-129.82352638 42.14461154 -119.62359612 36.03058171 -108.9921875 30.23828125 C-108.3317041 29.87637695 -107.6712207 29.51447266 -106.99072266 29.14160156 C-74.65187397 11.63624187 -36.91402947 0.31983213 0 0 Z M-10.77352905 158.40489197 C-11.48978915 159.11546881 -12.20604924 159.82604565 -12.94401419 160.55815512 C-15.32021467 162.92152652 -17.6782405 165.30220606 -20.03662109 167.68334961 C-21.74481229 169.3896754 -23.45409026 171.09491382 -25.16436768 172.79914856 C-29.3114501 176.93730878 -33.44689099 181.08687877 -37.57702129 185.24194043 C-42.3919506 190.08489761 -47.22024574 194.91443821 -52.0491211 199.74348325 C-60.66551912 208.36106656 -69.2699939 216.99045613 -77.86767578 225.62670898 C-86.20300208 233.99940477 -94.54635639 242.36398752 -102.89892578 250.71948242 C-103.67119836 251.49202921 -103.67119836 251.49202921 -104.4590724 252.28018301 C-104.9730499 252.79433948 -105.48702741 253.30849596 -106.01657999 253.83823293 C-107.05153519 254.87355357 -108.08648693 255.90887768 -109.12143517 256.94420528 C-109.63188608 257.45484172 -110.142337 257.96547815 -110.66825613 258.49158841 C-118.6404406 266.46734559 -126.60517996 274.45053484 -134.5691899 282.43445326 C-137.45148248 285.32375805 -140.33488531 288.21194951 -143.21910541 291.09933012 C-148.22096668 296.10672041 -153.21770726 301.11916328 -158.20883942 306.1372509 C-159.86637688 307.80222554 -161.52560039 309.46552362 -163.18662643 311.12701797 C-165.4533342 313.39498454 -167.71379497 315.66899985 -169.9727478 317.94468689 C-170.63130024 318.60082425 -171.28985268 319.2569616 -171.96836126 319.93298191 C-178.43122923 326.47096648 -182.09595002 331.58033645 -182.2421875 340.98828125 C-182.14253778 348.44389195 -180.92382883 353.46007612 -175.8046875 358.98828125 C-168.73024929 365.40224286 -163.10920551 366.682672 -153.5546875 366.76953125 C-143.81262434 365.31676745 -137.9890608 358.81635431 -131.33764648 352.1171875 C-130.29133681 351.07794514 -129.24418914 350.03954589 -128.1962738 349.00192261 C-125.369261 346.19730787 -122.55499795 343.38022563 -119.74362063 340.55994797 C-117.39365006 338.20485352 -115.0374853 335.85600565 -112.68131596 333.50711536 C-107.12704143 327.96980522 -101.58306856 322.42233115 -96.0456543 316.86816406 C-90.32986142 311.1354034 -84.59779826 305.41929303 -78.85742325 299.71115589 C-73.92593343 294.80631618 -69.00416015 289.89187858 -64.09022957 284.96944714 C-61.15644392 282.03072504 -58.2191729 279.09571763 -55.27270317 276.16970825 C-52.50762309 273.42291663 -49.75486572 270.6643532 -47.01128578 267.89609146 C-46.00093352 266.88071197 -44.98626773 265.86960446 -43.96710968 264.86306381 C-42.58065924 263.49243322 -41.21094587 262.10493277 -39.8422699 260.71655273 C-38.68395236 259.55735573 -38.68395236 259.55735573 -37.50223446 258.3747406 C-35.73537678 256.379465 -35.73537678 256.379465 -36.24049473 254.1078186 C-36.48855334 253.49087128 -36.73661196 252.87392395 -36.9921875 252.23828125 C-37.82946298 240.92871048 -38.73298424 228.36909903 -32.9921875 218.23828125 C-32.47140625 217.29984375 -31.950625 216.36140625 -31.4140625 215.39453125 C-24.3087843 203.48838941 -14.79120829 195.22723217 -1.2421875 191.30078125 C13.00734164 188.29793545 27.19294663 189.56718696 39.8203125 196.98828125 C51.30817947 204.56480304 60.16568435 215.4397491 63.0078125 229.23828125 C63.25398867 233.13889394 63.24220461 237.01892832 63.1953125 240.92578125 C63.1943457 242.47362305 63.1943457 242.47362305 63.19335938 244.05273438 C63.14759289 251.8889735 63.14759289 251.8889735 62.25810337 254.11178589 C61.75086722 256.37991233 61.75086722 256.37991233 63.51785946 258.3747406 C64.29007115 259.1475386 65.06228285 259.92033661 65.8578949 260.71655273 C66.73685928 261.60817108 67.61582367 262.49978943 68.52142334 263.41842651 C69.51330494 264.39899042 70.50544004 265.37929797 71.49780273 266.359375 C72.53004562 267.39675404 73.56121492 268.43520231 74.59138489 269.47463989 C77.39852098 272.3003472 80.22122514 275.11001521 83.04744983 277.91661453 C86.00368198 280.85648639 88.94732676 283.80891512 91.89283752 286.75952148 C96.84678652 291.71799756 101.810858 296.66616287 106.78100586 301.60839844 C112.51851321 307.31393068 118.23990934 313.03523297 123.95279926 318.76540661 C129.45213552 324.280644 134.96275903 329.78446966 140.47856331 335.28323364 C142.82295678 337.62077284 145.16337306 339.96218896 147.50063896 342.30685425 C150.26004568 345.07395841 153.02924554 347.83088473 155.80610466 350.58047104 C156.82049802 351.58801266 157.83156599 352.59891582 158.83891678 353.61349869 C167.79047826 362.65082045 167.79047826 362.65082045 179.5703125 366.76953125 C189.12483051 366.682672 194.74587429 365.40224286 201.8203125 358.98828125 C207.62025323 352.72487493 208.54838894 346.45435829 208.28515625 338.0625 C207.2395527 327.41501596 197.9470765 320.02926945 190.7727356 312.8493042 C190.07890081 312.15108716 189.38506603 311.45287013 188.67020595 310.733495 C186.36597914 308.4160114 184.05827344 306.10202569 181.75048828 303.78808594 C180.08902778 302.11871469 178.4277471 300.44916447 176.76663208 298.77944946 C171.33254632 293.31927664 165.89312414 287.86443662 160.453125 282.41015625 C158.57646874 280.5281366 156.69981955 278.64610989 154.82317734 276.76407623 C147.01890383 268.93800103 139.2130732 261.1134826 131.40345955 253.29273605 C129.38304841 251.26939577 127.36265359 249.24603918 125.34228516 247.22265625 C124.84019995 246.71982499 124.33811474 246.21699374 123.82081483 245.69892517 C115.67770083 237.54314224 107.54493449 229.37712033 99.41631974 221.206889 C91.05540968 212.80352675 82.68507062 204.40964921 74.30551898 196.02487493 C69.60691058 191.32285851 64.91280254 186.61649313 60.22873306 181.89998627 C55.8253644 177.46655729 51.41028372 173.04513782 46.98597336 168.63261032 C45.36634856 167.01320635 43.75098365 165.38952873 42.14027405 163.76125717 C39.93722211 161.53550152 37.71919958 159.32579452 35.49685669 157.11932373 C34.86638589 156.47497084 34.23591509 155.83061796 33.58633912 155.16673923 C27.89036457 149.57016963 21.25516459 145.66430179 13.0703125 145.67578125 C12.15378906 145.65386719 11.23726562 145.63195312 10.29296875 145.609375 C1.66711623 147.26069754 -4.76581141 152.27425802 -10.77352905 158.40489197 Z M-1.8671875 225.36328125 C-5.99654804 229.9439905 -7.86802597 234.66458848 -8.08203125 240.82421875 C-7.71666938 247.0634753 -6.10704737 251.16620791 -1.5703125 255.640625 C3.98339256 260.57884213 8.96449753 261.7637183 16.22265625 261.6171875 C22.0042663 260.83062905 26.04634666 257.52758518 29.8828125 253.30078125 C33.5244594 248.05252542 34.55607479 243.59812381 34.0078125 237.23828125 C32.63250421 231.60811293 30.47778313 228.05410984 26.0078125 224.23828125 C25.0796875 223.40296875 25.0796875 223.40296875 24.1328125 222.55078125 C15.53086484 217.23781358 5.42628423 218.68921264 -1.8671875 225.36328125 Z M-24.59939575 280.60498047 C-25.55203812 281.56428467 -25.55203812 281.56428467 -26.52392578 282.54296875 C-27.24452179 283.26371582 -27.9651178 283.98446289 -28.70755005 284.72705078 C-29.87325027 285.90823486 -29.87325027 285.90823486 -31.0625 287.11328125 C-31.85872772 287.91233887 -32.65495544 288.71139648 -33.47531128 289.53466797 C-36.02731502 292.09790244 -38.5722928 294.66799031 -41.1171875 297.23828125 C-42.84199092 298.97311072 -44.56724549 300.70749178 -46.29296875 302.44140625 C-50.53129798 306.70183421 -54.76387292 310.96791523 -58.9921875 315.23828125 C-54.87888879 320.14302026 -50.60676289 324.76023405 -46.0625 329.265625 C-45.42730438 329.89940567 -44.79210876 330.53318634 -44.13766479 331.18617249 C-42.13320034 333.18509531 -40.12526536 335.18048909 -38.1171875 337.17578125 C-36.74716361 338.54078258 -35.37736976 339.90601485 -34.0078125 341.27148438 C-30.67192207 344.59642336 -27.33291714 347.91820478 -23.9921875 351.23828125 C-18.57593194 346.68302935 -18.57593194 346.68302935 -13.52734375 341.73046875 C-12.66786133 340.83908203 -12.66786133 340.83908203 -11.79101562 339.9296875 C-10.63141246 338.71660433 -9.47776026 337.49780508 -8.33007812 336.2734375 C-2.31085699 330.07181572 4.06935072 326.56751204 12.7578125 325.98828125 C24.70939398 326.16599993 32.54663279 334.48962892 40.3828125 342.48828125 C41.21425781 343.32875 42.04570312 344.16921875 42.90234375 345.03515625 C44.94212797 347.09838627 46.97707536 349.16615248 49.0078125 351.23828125 C53.91255151 347.12498254 58.5297653 342.85285664 63.03515625 338.30859375 C63.98582726 337.35580032 63.98582726 337.35580032 64.95570374 336.38375854 C66.95462656 334.37929409 68.95002034 332.37135911 70.9453125 330.36328125 C72.31031383 328.99325736 73.6755461 327.62346351 75.04101562 326.25390625 C78.36595461 322.91801582 81.68773603 319.57901089 85.0078125 316.23828125 C80.83003485 311.2523525 76.47653973 306.57123841 71.859375 301.9921875 C70.87328751 301.00822243 70.87328751 301.00822243 69.86727905 300.00437927 C67.79148388 297.93412857 65.71221823 295.86740474 63.6328125 293.80078125 C62.2146117 292.38760263 60.79664074 290.97419331 59.37890625 289.56054688 C55.92452473 286.1171206 52.4670288 282.67685026 49.0078125 279.23828125 C45.86459596 280.46196641 43.15646582 281.69493953 40.3828125 283.61328125 C28.75589019 291.08457353 14.83352106 293.18648208 1.3828125 290.51953125 C-6.04959986 288.34590122 -12.97883484 285.36470515 -19.11328125 280.5859375 C-21.5422421 278.84375145 -22.29385052 278.6444462 -24.59939575 280.60498047 Z " transform="translate(242.9921875,-0.23828125)"/>
-</svg>
diff --git a/assets/icons/caelestia-media-generic-symbolic.svg b/assets/icons/caelestia-media-generic-symbolic.svg
deleted file mode 100644
index 8ff60ed..0000000
--- a/assets/icons/caelestia-media-generic-symbolic.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.772 4.28c.56-.144 1.097.246 1.206.814.1.517-.263 1.004-.771 1.14A7 7 0 1 0 19 12.9c.009-.5.4-.945.895-1 .603-.067 1.112.371 1.106.977L21 13c0 .107-.002.213-.006.32a.898.898 0 0 1 0 .164l-.008.122a9 9 0 0 1-9.172 8.392A9 9 0 0 1 9.772 4.28z" fill="#000000"/><path d="M15.93 13.753a4.001 4.001 0 1 1-6.758-3.581A4 4 0 0 1 12 9c.75 0 1.3.16 2 .53 0 0 .15.09.25.17-.1-.35-.228-1.296-.25-1.7a58.75 58.75 0 0 1-.025-2.035V2.96c0-.52.432-.94.965-.94.103 0 .206.016.305.048l4.572 1.689c.446.145.597.23.745.353.148.122.258.27.33.446.073.176.108.342.108.801v1.16c0 .518-.443.94-.975.94a.987.987 0 0 1-.305-.049l-1.379-.447-.151-.05c-.437-.14-.618-.2-.788-.26a5.697 5.697 0 0 1-.514-.207 3.53 3.53 0 0 1-.213-.107c-.098-.05-.237-.124-.521-.263L16 6l.011 7c0 .255-.028.507-.082.753h.001z" fill="#000000"/></svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-media-none-symbolic.svg b/assets/icons/caelestia-media-none-symbolic.svg
deleted file mode 100644
index 20ea19a..0000000
--- a/assets/icons/caelestia-media-none-symbolic.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="iso-8859-1"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
- width="800px" height="800px" viewBox="0 0 45.793 45.793"
- xml:space="preserve">
-<g>
- <g>
- <circle cx="22.899" cy="12.692" r="2.524"/>
- <path d="M22.899,26.661c-2.893,0-5.245,2.354-5.245,5.245c0,2.893,2.353,5.244,5.245,5.244s5.246-2.353,5.246-5.244
- C28.145,29.016,25.791,26.661,22.899,26.661z"/>
- <path d="M30.701,0H15.093c-4.647,0-8.415,3.768-8.415,8.414v28.965c0,4.646,3.768,8.414,8.415,8.414H30.7
- c4.647,0,8.415-3.768,8.415-8.414V8.414C39.116,3.768,35.348,0,30.701,0z M22.899,7.182c3.042,0,5.511,2.467,5.511,5.511
- c0,3.043-2.469,5.511-5.511,5.511c-3.044,0-5.511-2.468-5.511-5.511C17.388,9.648,19.855,7.182,22.899,7.182z M22.899,42.13
- c-5.646,0-10.223-4.577-10.223-10.224s4.576-10.223,10.223-10.223c5.646,0,10.223,4.577,10.223,10.223S28.544,42.13,22.899,42.13z
- "/>
- </g>
-</g>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-mozilla-firefox-symbolic.svg b/assets/icons/caelestia-mozilla-firefox-symbolic.svg
deleted file mode 100644
index 3ae6589..0000000
--- a/assets/icons/caelestia-mozilla-firefox-symbolic.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
- <path d="M11.807 9.776c0.011 0 0.005 0 0 0zM8.109 7.927c0.011 0 0.005 0 0 0zM30.229 10.781c-0.667-1.604-2.021-3.333-3.079-3.885 0.865 1.692 1.365 3.396 1.552 4.661l0.005 0.027c-1.739-4.329-4.681-6.073-7.088-9.871-0.12-0.192-0.24-0.385-0.36-0.588-0.063-0.104-0.115-0.208-0.172-0.319-0.099-0.192-0.171-0.395-0.224-0.609 0-0.020-0.015-0.036-0.036-0.041-0.011 0-0.021 0-0.031 0l-0.005 0.005c-0.005 0-0.011 0.005-0.011 0.005s0-0.005 0.005-0.011c-3.417 2-4.828 5.505-5.193 7.729-1.057 0.063-2.088 0.328-3.041 0.776-0.183 0.093-0.265 0.303-0.197 0.489 0.077 0.213 0.317 0.319 0.525 0.224 0.833-0.391 1.729-0.625 2.651-0.687l0.089-0.011c0.125-0.005 0.255-0.011 0.38-0.011 0.745-0.005 1.489 0.099 2.203 0.307l0.125 0.037c0.12 0.036 0.235 0.077 0.355 0.12 0.083 0.031 0.172 0.063 0.255 0.099 0.068 0.025 0.136 0.057 0.203 0.083 0.105 0.048 0.209 0.1 0.313 0.152l0.14 0.067c0.104 0.053 0.204 0.109 0.303 0.167 0.063 0.037 0.125 0.073 0.187 0.115 1.111 0.688 2.037 1.641 2.683 2.776-0.817-0.572-2.287-1.145-3.697-0.895 5.52 2.76 4.036 12.265-3.615 11.905-0.683-0.025-1.355-0.156-1.995-0.385-0.156-0.057-0.308-0.12-0.453-0.183-0.088-0.041-0.177-0.083-0.26-0.124-1.876-0.969-3.423-2.803-3.615-5.027 0 0 0.708-2.64 5.072-2.64 0.475 0 1.824-1.319 1.849-1.699-0.011-0.125-2.683-1.187-3.724-2.213-0.557-0.547-0.817-0.812-1.052-1.011-0.125-0.109-0.26-0.208-0.401-0.301-0.348-1.224-0.364-2.521-0.041-3.751-1.579 0.719-2.803 1.855-3.693 2.855h-0.009c-0.609-0.771-0.563-3.313-0.532-3.844-0.005-0.036-0.453 0.229-0.511 0.271-0.536 0.385-1.041 0.813-1.5 1.287-0.525 0.531-1.004 1.104-1.437 1.719-0.984 1.396-1.687 2.979-2.057 4.645-0.005 0.021-0.145 0.647-0.249 1.417-0.021 0.12-0.037 0.24-0.052 0.359-0.043 0.292-0.073 0.589-0.089 0.881l-0.005 0.047c-0.009 0.172-0.020 0.339-0.031 0.511v0.077c0 8.48 6.875 15.355 15.355 15.355 7.593 0 13.9-5.516 15.135-12.756 0.027-0.197 0.047-0.395 0.068-0.593 0.307-2.631-0.031-5.401-0.995-7.713z"/>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-no-repeat-symbolic.svg b/assets/icons/caelestia-no-repeat-symbolic.svg
deleted file mode 100644
index f878db7..0000000
--- a/assets/icons/caelestia-no-repeat-symbolic.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="256" height="256">
-<path d="M0 0 C1.79799878 2.16032056 3.18258972 4.11413044 4.64331055 6.4753418 C12.62749307 17.86435592 21.72708958 27.99152164 35.96392822 30.57183838 C53.77874667 32.82165628 71.98915986 31.86027544 89.87921798 31.08877361 C97.64789189 30.75976422 105.4174458 30.60398348 113.19139063 30.45938456 C129.13564992 30.1590977 145.06598824 29.65173437 161 29 C159.75283203 27.80503906 159.75283203 27.80503906 158.48046875 26.5859375 C157.38241192 25.51603597 156.28477797 24.4457003 155.1875 23.375 C154.36668945 22.59189453 154.36668945 22.59189453 153.52929688 21.79296875 C150.1544387 18.47947163 147.76487969 15.47722111 146 11 C146 6.62433957 146.52915222 4.61882851 149.1875 1.125 C152.40287167 -1.30439193 153.98099567 -2 158 -2 C164.24604009 0.05424477 168.00464154 3.41680539 172.578125 8.046875 C173.27137878 8.73630188 173.96463257 9.42572876 174.67889404 10.13604736 C176.13590114 11.59034329 177.58791247 13.04965967 179.03515625 14.51367188 C181.24314994 16.7430835 183.47166804 18.95035157 185.703125 21.15625 C187.1210759 22.57684431 188.53778538 23.99867905 189.953125 25.421875 C190.6156427 26.07629578 191.2781604 26.73071655 191.96075439 27.40496826 C195.83547757 31.35876484 198.34874543 34.67681385 200 40 C199.34661816 43.32135767 198.50686403 45.98627194 197 49 C196.34 49 195.68 49 195 49 C194.67 49.99 194.34 50.98 194 52 C191.5625 54.6875 191.5625 54.6875 189 57 C188.34 57 187.68 57 187 57 C187 57.66 187 58.32 187 59 C185.01739574 60.68732277 183.0167658 62.35366057 181 64 C180.67 64.66 180.34 65.32 180 66 C179.34 66 178.68 66 178 66 C177.75378906 66.58523437 177.50757813 67.17046875 177.25390625 67.7734375 C175.80626232 70.34402017 174.20757518 71.75455693 171.9375 73.625 C169.02124288 75.95485552 169.02124288 75.95485552 167 79 C166.34 79 165.68 79 165 79 C165 79.66 165 80.32 165 81 C160.8506805 83.20167973 159.03541912 84.15794529 154.3125 83.4375 C150.88386501 81.9496018 149.14099657 81.0468028 147 78 C145.76566076 73.91125126 145.7121895 71.74830731 147.25 67.75 C151.22493855 62.32962925 156.24707692 57.75292308 161 53 C125.36 53 89.72 53 53 53 C71.08254403 71.51975299 71.08254403 71.51975299 89.23632812 89.96875 C96.65828762 97.47962642 104.07188223 104.99759575 111.43481445 112.56640625 C116.79797462 118.07945519 122.18732166 123.56528295 127.60924524 129.02057266 C130.47739607 131.90821816 133.33104672 134.80740601 136.15367317 137.73962402 C138.81606268 140.5045212 141.51233903 143.23138266 144.23763466 145.93424988 C145.22669259 146.92740476 146.20315139 147.93331023 147.16487503 148.95295715 C151.88640629 153.94429208 155.7830369 157.43635807 162.88336033 157.8863945 C167.92416732 157.94361496 170.91745727 157.88384965 174.91279602 154.62182713 C177.463696 151.41753141 177.54906334 150.00868442 177.34204102 145.96655273 C177.27467491 144.17198891 177.27467491 144.17198891 177.20594788 142.34117126 C177.13927414 141.05167068 177.0726004 139.7621701 177.00390625 138.43359375 C176.96614355 137.07630985 176.93360061 135.71887176 176.9059906 134.36134338 C176.788308 128.62055941 176.64998524 122.88461542 176.33862305 117.15063477 C175.02215888 91.90585666 175.02215888 91.90585666 182.47998047 82.51220703 C187.15645822 77.48396814 192.21977735 72.74317025 198 69 C198.66 69 199.32 69 200 69 C200.04645583 79.40563275 200.08193635 89.81124059 200.10362434 100.21695614 C200.11403644 105.04937862 200.12814841 109.88172985 200.15087891 114.71411133 C200.17269936 119.38253538 200.18458797 124.05089113 200.18975449 128.71936226 C200.19343377 130.49543901 200.2006183 132.27151227 200.21146011 134.04755974 C200.22609755 136.54602854 200.22797287 139.04421944 200.22705078 141.54272461 C200.23424133 142.26777695 200.24143188 142.99282928 200.24884033 143.73985291 C200.20778578 154.10184201 197.45605788 163.16385718 190.26171875 170.859375 C186.81616149 173.94586088 183.25575701 176.13161887 179 178 C181.91064119 181.5759306 184.90701959 184.94456387 188.22265625 188.1484375 C188.80055908 188.70958252 189.37846191 189.27072754 189.97387695 189.84887695 C191.14224737 190.97613358 192.31900644 192.09477081 193.50463867 193.20385742 C196.94919526 196.53542811 199.05038634 199.13662145 200 204 C198.7760971 207.97768442 197.84299722 209.53166821 194.1875 211.5625 C189.96266929 212.14237872 188.65136384 211.22046845 185 209 C182.12294561 206.54865924 179.47515163 203.88853092 176.81304932 201.2074585 C176.00487408 200.40537437 175.19669885 199.60329024 174.36403346 198.77690053 C171.65770457 196.08722141 168.96221763 193.3869358 166.26660156 190.68652344 C164.3295221 188.75514641 162.39023618 186.82599004 160.45109558 184.89668274 C155.73226041 180.19888594 151.0216965 175.49287947 146.314262 170.78366128 C142.48806522 166.9561563 138.65932012 163.13121884 134.82875824 159.30808258 C133.72494387 158.20636742 132.62113031 157.10465146 131.51731753 156.00293469 C130.96790794 155.45456863 130.41849834 154.90620256 129.85243997 154.34121934 C119.46611618 143.9737908 109.08977828 133.59641174 98.71631131 123.21612135 C89.22630433 113.72053692 79.72315741 104.2382273 70.21382716 94.76199767 C60.44760953 85.02951132 50.69070239 75.28777909 40.94364715 65.53610063 C35.47206617 60.06227472 29.99680147 54.59225724 24.51166534 49.13201141 C19.84301821 44.48436985 15.1831671 39.82812539 10.5344747 35.16052118 C8.16272324 32.77960798 5.78592748 30.40439426 3.4006176 28.03701973 C0.81504117 25.46954999 -1.75469817 22.88720676 -4.32098389 20.30047607 C-5.45588151 19.18435523 -5.45588151 19.18435523 -6.61370635 18.04568648 C-10.71088849 13.87804861 -13.16459142 10.86412904 -14 5 C-12.824424 2.06105999 -11.67789455 0.55684195 -9.25 -1.4375 C-5.66718756 -2.33320311 -3.395935 -1.3397941 0 0 Z " fill="#000000" transform="translate(35,23)"/>
-<path d="M0 0 C5.66129431 4.69766975 10.85228904 9.74723372 16 15 C15.67 15.66 15.34 16.32 15 17 C14.98203966 18.89790632 15.03152819 20.79658907 15.11206055 22.69287109 C15.33673927 28.7197593 15.48052802 34.74048043 15.56296062 40.77055931 C15.61761292 44.41252348 15.70723181 48.04620147 15.88110352 51.68457031 C17.04781266 77.00423217 17.04781266 77.00423217 9.50244141 86.50097656 C4.83090096 91.52359516 -0.22540793 96.26162832 -6 100 C-6.66 100 -7.32 100 -8 100 C-8.04645583 89.59436725 -8.08193635 79.18875941 -8.10362434 68.78304386 C-8.11403644 63.95062138 -8.12814841 59.11827015 -8.15087891 54.28588867 C-8.17269936 49.61746462 -8.18458797 44.94910887 -8.18975449 40.28063774 C-8.19343377 38.50456099 -8.2006183 36.72848773 -8.21146011 34.95244026 C-8.22609755 32.45397146 -8.22797287 29.95578056 -8.22705078 27.45727539 C-8.23424133 26.73222305 -8.24143188 26.00717072 -8.24884033 25.26014709 C-8.21902583 17.73509805 -7.01549643 10.82826623 -2.875 4.4375 C-2.33617187 3.59058594 -1.79734375 2.74367188 -1.2421875 1.87109375 C-0.83226562 1.25363281 -0.42234375 0.63617188 0 0 Z " fill="#000000" transform="translate(29,64)"/>
-<path d="M0 0 C3.17921096 2.41896486 4.6866752 4.09919119 6 7.875 C6 12.61278576 4.4014321 16.26222419 1.4375 19.9375 C0 21 0 21 -2 21 C-2 21.66 -2 22.32 -2 23 C-2.66 23 -3.32 23 -4 23 C-4.495 24.485 -4.495 24.485 -5 26 C-6.4375 27.6875 -6.4375 27.6875 -8 29 C-8.66 29 -9.32 29 -10 29 C-10 29.66 -10 30.32 -10 31 C-8.92941345 30.95558777 -7.8588269 30.91117554 -6.75579834 30.86541748 C3.41998107 30.45690313 13.594065 30.15877142 23.77623367 29.96375656 C29.00977863 29.86010136 34.23662262 29.72004513 39.46630859 29.49121094 C77.91342952 27.85131975 77.91342952 27.85131975 89.27508545 38.14630127 C99 48.708632 99 48.708632 99 53 C45.54 53.495 45.54 53.495 -9 54 C-5.04 57.96 -1.08 61.92 3 66 C6 73 6 73 5.4375 76.6875 C3.9624108 80.08661858 2.99036407 81.84029262 0 84 C-4.29365825 85.43121942 -6.99765137 84.9515526 -11.08410645 83.10412598 C-14.76895551 80.98055415 -17.60744713 77.96055805 -20.578125 74.953125 C-21.27137878 74.26369812 -21.96463257 73.57427124 -22.67889404 72.86395264 C-24.13590114 71.40965671 -25.58791247 69.95034033 -27.03515625 68.48632812 C-29.24314994 66.2569165 -31.47166804 64.04964843 -33.703125 61.84375 C-35.1210759 60.42315569 -36.53778538 59.00132095 -37.953125 57.578125 C-38.94690155 56.59649384 -38.94690155 56.59649384 -39.96075439 55.59503174 C-43.83799989 51.63866136 -46.42900506 48.35923153 -48 43 C-47.87976258 38.20534717 -46.53359621 35.88778075 -43.265625 32.46875 C-42.45158203 31.60894531 -41.63753906 30.74914062 -40.79882812 29.86328125 C-39.8667813 28.90797927 -38.93379047 27.95359767 -38 27 C-36.96789703 25.93118454 -35.93600396 24.86216635 -34.90429688 23.79296875 C-32.1205698 20.92223064 -29.31005004 18.07941139 -26.48730469 15.24707031 C-24.7317091 13.48160628 -22.98706337 11.70573926 -21.2421875 9.9296875 C-20.11523979 8.79622194 -18.98765873 7.66338566 -17.859375 6.53125 C-17.34469666 6.00422485 -16.83001831 5.47719971 -16.29974365 4.9342041 C-11.09803289 -0.23571997 -7.22288037 -1.9347001 0 0 Z " fill="#000000" transform="translate(69,150)"/>
-</svg>
diff --git a/assets/icons/caelestia-pause-symbolic.svg b/assets/icons/caelestia-pause-symbolic.svg
deleted file mode 100644
index 8012654..0000000
--- a/assets/icons/caelestia-pause-symbolic.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
- <g fill="#000000">
- <path d="m 3 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
- <path d="m 10 1 h 3 c 0.550781 0 1 0.449219 1 1 v 12 c 0 0.550781 -0.449219 1 -1 1 h -3 c -0.550781 0 -1 -0.449219 -1 -1 v -12 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/>
- </g>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-play-symbolic.svg b/assets/icons/caelestia-play-symbolic.svg
deleted file mode 100644
index 8745847..0000000
--- a/assets/icons/caelestia-play-symbolic.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
- <path d="m 2 2.5 v 11 c 0 1.5 1.269531 1.492188 1.269531 1.492188 h 0.128907 c 0.246093 0.003906 0.488281 -0.050782 0.699218 -0.171876 l 9.796875 -5.597656 c 0.433594 -0.242187 0.65625 -0.734375 0.65625 -1.226562 c 0 -0.492188 -0.222656 -0.984375 -0.65625 -1.222656 l -9.796875 -5.597657 c -0.210937 -0.121093 -0.453125 -0.175781 -0.699218 -0.175781 h -0.128907 s -1.269531 0 -1.269531 1.5 z m 0 0" fill="#000000"/>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-repeat-one-symbolic.svg b/assets/icons/caelestia-repeat-one-symbolic.svg
deleted file mode 100644
index ee38bb6..0000000
--- a/assets/icons/caelestia-repeat-one-symbolic.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
- <path d="m 7 1 v 2 h -3 c -2.199219 0 -4 1.800781 -4 4 v 2 c 0 1.019531 0.386719 1.964844 1.019531 2.671875 c 0.367188 0.410156 1 0.445313 1.410157 0.078125 c 0.414062 -0.367188 0.449218 -1 0.078124 -1.414062 c -0.316406 -0.351563 -0.507812 -0.8125 -0.507812 -1.335938 v -2 c 0 -1.125 0.875 -2 2 -2 h 3 v 2 h 1 v -0.007812 c 0.265625 0.003906 0.519531 -0.101563 0.707031 -0.285157 l 2 -2 c 0.207031 -0.207031 0.3125 -0.496093 0.289063 -0.789062 c 0 -0.015625 -0.003906 -0.03125 -0.007813 -0.046875 c -0.007812 -0.070313 -0.023437 -0.136719 -0.046875 -0.203125 c -0.011718 -0.035157 -0.023437 -0.070313 -0.042968 -0.101563 c 0 -0.007812 -0.003907 -0.011718 -0.007813 -0.019531 c -0.011719 -0.023437 -0.027344 -0.046875 -0.039063 -0.070313 c -0.007812 -0.011718 -0.019531 -0.027343 -0.027343 -0.039062 c -0.015625 -0.019531 -0.027344 -0.042969 -0.042969 -0.0625 c -0.007812 -0.007812 -0.015625 -0.015625 -0.023438 -0.023438 c -0.015624 -0.019531 -0.03125 -0.039062 -0.050781 -0.058593 l -2 -2 c -0.1875 -0.183594 -0.441406 -0.289063 -0.707031 -0.285157 v -0.007812 z m 7 1 c -1.105469 0 -2 0.894531 -2 2 s 0.894531 2 2 2 s 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 1 5 c -0.550781 0 -1 0.449219 -1 1 v 1.007812 c 0 1.125 -0.875 2 -2 2 h -4 v -2.007812 h -1 v 0.007812 c -0.265625 -0.003906 -0.519531 0.101563 -0.707031 0.285157 l -2 2 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 2 2 c 0.1875 0.183594 0.441406 0.289063 0.707031 0.285157 v 0.007812 h 1 v -1.992188 h 4 c 2.199219 0 4 -1.804687 4 -4 v -1.007812 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0" fill="#000000"/>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-repeat-symbolic.svg b/assets/icons/caelestia-repeat-symbolic.svg
deleted file mode 100644
index fc514b7..0000000
--- a/assets/icons/caelestia-repeat-symbolic.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
- <path d="m 8 1 v 2 h -4 c -2.199219 0 -4 1.800781 -4 4 v 2 c 0 1.019531 0.386719 1.964844 1.019531 2.671875 c 0.367188 0.410156 1 0.445313 1.410157 0.078125 c 0.414062 -0.367188 0.449218 -1 0.078124 -1.414062 c -0.316406 -0.351563 -0.507812 -0.8125 -0.507812 -1.335938 v -2 c 0 -1.125 0.875 -2 2 -2 h 4 v 2 h 1 v -0.007812 c 0.265625 0.003906 0.519531 -0.101563 0.707031 -0.285157 l 2 -2 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 l -2 -2 c -0.1875 -0.183594 -0.441406 -0.289063 -0.707031 -0.285157 v -0.007812 z m 6.289062 3 c -0.265624 -0.011719 -0.523437 0.078125 -0.71875 0.257812 c -0.414062 0.367188 -0.449218 1 -0.078124 1.410157 c 0.316406 0.355469 0.507812 0.816406 0.507812 1.339843 v 2 c 0 1.125 -0.875 2 -2 2 h -4 v -2.007812 h -1 v 0.007812 c -0.265625 -0.003906 -0.519531 0.101563 -0.707031 0.285157 l -2 2 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 2 2 c 0.1875 0.183594 0.441406 0.289063 0.707031 0.285157 v 0.007812 h 1 v -1.992188 h 4 c 2.199219 0 4 -1.804687 4 -4 v -2 c 0 -1.023437 -0.386719 -1.96875 -1.019531 -2.675781 c -0.175781 -0.199219 -0.425781 -0.316406 -0.691407 -0.332031 z m 0 0" fill="#000000"/>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-shuffle-symbolic.svg b/assets/icons/caelestia-shuffle-symbolic.svg
deleted file mode 100644
index 232eaac..0000000
--- a/assets/icons/caelestia-shuffle-symbolic.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
- <path d="m 12 1 v 2 h -3 c -0.859375 0 -1.59375 0.480469 -2.011719 1.007812 c -0.417969 0.523438 -0.648437 1.074219 -0.882812 1.542969 l -0.105469 0.210938 l -0.105469 -0.210938 c -0.191406 -0.382812 -0.386719 -0.816406 -0.671875 -1.25 c -0.414062 -0.632812 -1.207031 -1.300781 -2.222656 -1.300781 h -3 v 2 h 3 c 0.296875 0 0.316406 0.039062 0.550781 0.398438 c 0.164063 0.246093 0.34375 0.625 0.554688 1.050781 l 0.777343 1.550781 l -0.777343 1.550781 c -0.261719 0.523438 -0.476563 0.96875 -0.65625 1.199219 c -0.183594 0.226562 -0.199219 0.25 -0.449219 0.25 h -3 v 2 h 3 c 0.859375 0 1.59375 -0.480469 2.011719 -1.007812 c 0.417969 -0.523438 0.648437 -1.074219 0.882812 -1.542969 l 0.105469 -0.210938 l 0.105469 0.210938 c 0.164062 0.328125 0.335937 0.703125 0.5625 1.078125 c 0.414062 0.6875 1.222656 1.472656 2.332031 1.472656 h 3 v 2 h 1 v -0.007812 c 0.265625 0.003906 0.519531 -0.101563 0.707031 -0.285157 l 2 -2 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 l -2 -2 c -0.1875 -0.183594 -0.441406 -0.289063 -0.707031 -0.285157 v -0.007812 h -1 v 2 h -3 c -0.324219 0 -0.351562 -0.058594 -0.617188 -0.503906 c -0.148437 -0.242188 -0.304687 -0.574219 -0.488281 -0.945313 l -0.777343 -1.550781 l 0.777343 -1.550781 c 0.261719 -0.523438 0.476563 -0.96875 0.65625 -1.199219 c 0.183594 -0.226562 0.199219 -0.25 0.449219 -0.25 h 3 v 2 h 1 v -0.007812 c 0.265625 0.003906 0.519531 -0.101563 0.707031 -0.285157 l 2 -2 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 l -2 -2 c -0.1875 -0.183594 -0.441406 -0.289063 -0.707031 -0.285157 v -0.007812 z m 0 0" fill="#000000"/>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-skip-next-symbolic.svg b/assets/icons/caelestia-skip-next-symbolic.svg
deleted file mode 100644
index 1d4721c..0000000
--- a/assets/icons/caelestia-skip-next-symbolic.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
- <g fill="#000000">
- <path d="m 1 3 v 10 c 0 1 1.085938 1 1.085938 1 h 0.113281 c 0.210937 0 0.417969 -0.046875 0.601562 -0.148438 l 8.398438 -4.800781 c 0.375 -0.207031 0.5625 -0.628906 0.5625 -1.050781 s -0.1875 -0.84375 -0.5625 -1.050781 l -8.398438 -4.800781 c -0.183593 -0.101563 -0.390625 -0.148438 -0.601562 -0.148438 h -0.113281 s -1.085938 0 -1.085938 1 z m 0 0"/>
- <path d="m 14.5 2 h -1 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 11 c 0 0.277344 0.222656 0.5 0.5 0.5 h 1 c 0.277344 0 0.5 -0.222656 0.5 -0.5 v -11 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 0 0"/>
- </g>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-skip-previous-symbolic.svg b/assets/icons/caelestia-skip-previous-symbolic.svg
deleted file mode 100644
index 10d0e59..0000000
--- a/assets/icons/caelestia-skip-previous-symbolic.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
- <g fill="#000000">
- <path d="m 15 3 v 10 c 0 1 -1.085938 1 -1.085938 1 h -0.113281 c -0.210937 0 -0.417969 -0.046875 -0.601562 -0.148438 l -8.398438 -4.800781 c -0.375 -0.207031 -0.5625 -0.628906 -0.5625 -1.050781 s 0.1875 -0.84375 0.5625 -1.050781 l 8.398438 -4.800781 c 0.183593 -0.101563 0.390625 -0.148438 0.601562 -0.148438 h 0.113281 s 1.085938 0 1.085938 1 z m 0 0"/>
- <path d="m 1.5 2 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 11 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -11 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0"/>
- </g>
-</svg> \ No newline at end of file
diff --git a/assets/icons/caelestia-spotify-symbolic.svg b/assets/icons/caelestia-spotify-symbolic.svg
deleted file mode 100644
index bf01823..0000000
--- a/assets/icons/caelestia-spotify-symbolic.svg
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="iso-8859-1"?>
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
-<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
- viewBox="0 0 305 305" xml:space="preserve">
-<g id="XMLID_85_">
- <path id="XMLID_86_" d="M152.441,0C68.385,0,0,68.39,0,152.453C0,236.568,68.385,305,152.441,305
- C236.562,305,305,236.568,305,152.453C305,68.39,236.562,0,152.441,0z M75.08,208.47c17.674-5.38,35.795-8.108,53.857-8.108
- c30.676,0,60.96,7.774,87.592,22.49c1.584,0.863,3.024,3.717,3.67,7.27c0.646,3.552,0.389,7.205-0.648,9.105
- c-1.309,2.438-3.965,4.014-6.768,4.014c-1.389,0-2.61-0.312-3.831-0.972c-24.448-13.438-52.116-20.542-80.015-20.542
- c-16.855,0-33.402,2.495-49.167,7.409c-0.768,0.233-1.558,0.352-2.348,0.352c-3.452,0.001-6.448-2.198-7.453-5.461
- C68.612,219.566,71.419,209.667,75.08,208.47z M68.43,152.303c19.699-5.355,40.057-8.071,60.508-8.071
- c36.765,0,73.273,8.896,105.601,25.739c2.266,1.15,3.936,3.1,4.701,5.49c0.776,2.421,0.542,5.024-0.669,7.347
- c-2.885,5.646-6.257,9.44-8.393,9.44c-1.514,0-2.975-0.363-4.43-1.09c-30.019-15.632-62.59-23.558-96.811-23.558
- c-19.035,0-37.71,2.503-55.489,7.435c-0.827,0.224-1.676,0.337-2.521,0.337c-4.277,0.001-8.046-2.888-9.162-7.013
- C60.336,162.994,63.601,153.616,68.43,152.303z M66.727,115.606c-0.903,0.223-1.826,0.335-2.744,0.335
- c-5.169,0.001-9.648-3.492-10.892-8.487c-1.559-6.323,2.397-13.668,8.126-15.111c22.281-5.473,45.065-8.248,67.72-8.248
- c43.856,0,85.857,9.86,124.851,29.312c2.708,1.336,4.727,3.642,5.687,6.493c0.96,2.854,0.748,5.926-0.592,8.64
- c-1.826,3.655-5.772,7.59-10.121,7.59c-1.677,0-3.399-0.393-4.924-1.109c-35.819-17.921-74.477-27.008-114.9-27.008
- C108.164,108.014,87.234,110.568,66.727,115.606z"/>
-</g>
-</svg> \ No newline at end of file
diff --git a/env.d.ts b/env.d.ts
deleted file mode 100644
index 435553f..0000000
--- a/env.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare const HOME: string;
-declare const CACHE: string;
-declare const STATE: string;
-declare const SRC: string;
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index d0a50f8..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,616 +0,0 @@
-{
- "name": "shell",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "dependencies": {
- "fuzzysort": "^3.1.0",
- "ical.js": "^2.1.0",
- "mathjs": "^14.0.1"
- },
- "devDependencies": {
- "esbuild": "^0.25.2",
- "typescript": "5.7.3"
- }
- },
- "node_modules/@babel/runtime": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
- "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
- "license": "MIT",
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
- "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
- "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
- "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
- "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
- "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
- "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
- "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
- "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
- "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
- "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
- "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
- "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
- "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
- "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
- "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
- "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
- "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
- "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
- "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
- "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
- "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
- "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
- "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
- "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
- "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/complex.js": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
- "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==",
- "license": "MIT",
- "engines": {
- "node": "*"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/rawify"
- }
- },
- "node_modules/decimal.js": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
- "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
- "license": "MIT"
- },
- "node_modules/esbuild": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
- "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.2",
- "@esbuild/android-arm": "0.25.2",
- "@esbuild/android-arm64": "0.25.2",
- "@esbuild/android-x64": "0.25.2",
- "@esbuild/darwin-arm64": "0.25.2",
- "@esbuild/darwin-x64": "0.25.2",
- "@esbuild/freebsd-arm64": "0.25.2",
- "@esbuild/freebsd-x64": "0.25.2",
- "@esbuild/linux-arm": "0.25.2",
- "@esbuild/linux-arm64": "0.25.2",
- "@esbuild/linux-ia32": "0.25.2",
- "@esbuild/linux-loong64": "0.25.2",
- "@esbuild/linux-mips64el": "0.25.2",
- "@esbuild/linux-ppc64": "0.25.2",
- "@esbuild/linux-riscv64": "0.25.2",
- "@esbuild/linux-s390x": "0.25.2",
- "@esbuild/linux-x64": "0.25.2",
- "@esbuild/netbsd-arm64": "0.25.2",
- "@esbuild/netbsd-x64": "0.25.2",
- "@esbuild/openbsd-arm64": "0.25.2",
- "@esbuild/openbsd-x64": "0.25.2",
- "@esbuild/sunos-x64": "0.25.2",
- "@esbuild/win32-arm64": "0.25.2",
- "@esbuild/win32-ia32": "0.25.2",
- "@esbuild/win32-x64": "0.25.2"
- }
- },
- "node_modules/escape-latex": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
- "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==",
- "license": "MIT"
- },
- "node_modules/fraction.js": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.2.tgz",
- "integrity": "sha512-uXBDv5knpYmv/2gLzWQ5mBHGBRk9wcKTeWu6GLTUEQfjCxO09uM/mHDrojlL+Q1mVGIIFo149Gba7od1XPgSzQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/rawify"
- }
- },
- "node_modules/fuzzysort": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz",
- "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==",
- "license": "MIT"
- },
- "node_modules/ical.js": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.1.0.tgz",
- "integrity": "sha512-BOVfrH55xQ6kpS3muGvIXIg2l7p+eoe12/oS7R5yrO3TL/j/bLsR0PR+tYQESFbyTbvGgPHn9zQ6tI4FWyuSaQ==",
- "license": "MPL-2.0"
- },
- "node_modules/javascript-natural-sort": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
- "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
- "license": "MIT"
- },
- "node_modules/mathjs": {
- "version": "14.4.0",
- "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.4.0.tgz",
- "integrity": "sha512-CpoYDhNENefjIG9wU9epr+0pBHzlaySfpWcblZdAf5qXik/j/U8eSmx/oNbmXO0F5PyfwPGVD/wK4VWsTho1SA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@babel/runtime": "^7.26.10",
- "complex.js": "^2.2.5",
- "decimal.js": "^10.4.3",
- "escape-latex": "^1.2.0",
- "fraction.js": "^5.2.1",
- "javascript-natural-sort": "^0.7.1",
- "seedrandom": "^3.0.5",
- "tiny-emitter": "^2.1.0",
- "typed-function": "^4.2.1"
- },
- "bin": {
- "mathjs": "bin/cli.js"
- },
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "license": "MIT"
- },
- "node_modules/seedrandom": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
- "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
- "license": "MIT"
- },
- "node_modules/tiny-emitter": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
- "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
- "license": "MIT"
- },
- "node_modules/typed-function": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",
- "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==",
- "license": "MIT",
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/typescript": {
- "version": "5.7.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
- "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- }
- }
-}
diff --git a/package.json b/package.json
deleted file mode 100644
index 1c6d73e..0000000
--- a/package.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "dependencies": {
- "fuzzysort": "^3.1.0",
- "ical.js": "^2.1.0",
- "mathjs": "^14.0.1"
- },
- "devDependencies": {
- "esbuild": "^0.25.2",
- "typescript": "5.7.3"
- }
-}
diff --git a/run.fish b/run.fish
deleted file mode 100755
index 6f2b072..0000000
--- a/run.fish
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/fish
-
-cd (dirname (status filename)) || exit 1
-
-set -q XDG_RUNTIME_DIR && set -l bundle_dir $XDG_RUNTIME_DIR || set -l bundle_dir /tmp
-set -q XDG_CACHE_HOME && set -l cache_dir $XDG_CACHE_HOME/caelestia || set -l cache_dir $HOME/.cache/caelestia
-set -q XDG_STATE_HOME && set -l state_dir $XDG_STATE_HOME/caelestia || set -l state_dir $HOME/.local/state/caelestia
-
-mkdir -p $cache_dir
-
-set -q DEBUG || set -l minify --minify-identifiers
-
-./node_modules/.bin/esbuild app.tsx --bundle --minify-whitespace $minify --outfile=$bundle_dir/caelestia.js \
- --external:console --external:system --external:cairo --external:gettext --external:'file://*' --external:'gi://*' --external:'resource://*' \
- --define:HOME=\"$HOME\" --define:CACHE=\"$cache_dir\" --define:STATE=\"$state_dir\" --define:SRC=\"(pwd)\" --format=esm --platform=neutral --main-fields=module,main
-
-gjs -m $bundle_dir/caelestia.js
diff --git a/scss/_font.scss b/scss/_font.scss
deleted file mode 100644
index 405a850..0000000
--- a/scss/_font.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-@mixin title {
- font-family: "Gabarito", "Poppins", "Readex Pro", "Lexend", sans-serif;
-}
-
-@mixin main {
- font-family: "Rubik", "Geist", "AR One Sans", "Reddit Sans", "Inter", "Roboto", "Ubuntu", "Noto Sans", sans-serif;
-}
-
-@mixin icon {
- font-family: "Material Symbols Rounded", "MaterialSymbolsRounded", "Material Symbols Outlined",
- "Material Symbols Sharp";
-}
-
-@mixin mono {
- font-family: "JetBrains Mono NF", "JetBrains Mono Nerd Font", "JetBrains Mono NL", "SpaceMono NF",
- "SpaceMono Nerd Font", monospace;
-}
-
-@mixin reading {
- font-family: "Readex Pro", "Lexend", "Noto Sans", sans-serif;
-}
diff --git a/scss/_lib.scss b/scss/_lib.scss
deleted file mode 100644
index ef8cc8d..0000000
--- a/scss/_lib.scss
+++ /dev/null
@@ -1,47 +0,0 @@
-@use "sass:color";
-@use "scheme";
-@use "font";
-
-$scale: 0.068rem;
-@function s($value: 1) {
- @return $value * $scale;
-}
-
-@mixin rounded($all, $tl: $all, $tr: $all, $br: $all, $bl: $all) {
- border-radius: s($tl) s($tr) s($br) s($bl);
- -gtk-outline-radius: s($tl) s($tr) s($br) s($bl);
-}
-
-@mixin border($colour, $alpha: 1, $width: 1, $style: solid, $force: false) {
- @if $force or scheme.$borders {
- border: s($width) $style color.change($colour, $alpha: $alpha);
- }
-}
-
-@mixin shadow($colour: black, $alpha: 0.64, $x: 0, $y: 0, $blur: 3, $spread: 0) {
- box-shadow: s($x) s($y) s($blur) s($spread) color.change($colour, $alpha: $alpha);
-}
-
-@mixin spacing($val: 5, $vertical: false) {
- $dir: if($vertical, bottom, right);
-
- & > *:not(:last-child) {
- margin-#{$dir}: s($val);
- }
-}
-
-@mixin element-decel($duration: 200ms) {
- transition: $duration cubic-bezier(0, 0.55, 0.45, 1);
-}
-
-@mixin fluent-decel($duration: 200ms) {
- transition: $duration cubic-bezier(0.1, 1, 0, 1);
-}
-
-@mixin overshot {
- transition-timing-function: cubic-bezier(0.05, 0.9, 0.1, 1.1);
-}
-
-@mixin ease-in-out {
- transition-timing-function: cubic-bezier(0.85, 0, 0.15, 1);
-}
diff --git a/scss/bar.scss b/scss/bar.scss
deleted file mode 100644
index b14ceeb..0000000
--- a/scss/bar.scss
+++ /dev/null
@@ -1,394 +0,0 @@
-@use "sass:color";
-@use "lib";
-@use "scheme";
-@use "font";
-
-@mixin bar-spacing($vertical: false) {
- @include lib.spacing(10, $vertical);
-
- & > * {
- @include lib.spacing(10, $vertical);
- }
-}
-
-.bar {
- @include font.mono;
-
- font-size: lib.s(14);
-
- label.icon {
- font-size: lib.s(18);
- }
-
- .screen-corner {
- background-color: transparent;
- }
-
- .os-icon {
- color: scheme.$yellow;
- }
-
- .active-window {
- color: scheme.$pink;
- }
-
- .media-playing {
- color: scheme.$lavender;
-
- icon {
- font-size: lib.s(16);
- }
- }
-
- .workspaces {
- & > button {
- @include lib.rounded(100);
- @include lib.element-decel;
-
- font-size: lib.s(13);
- font-weight: bold;
-
- &.focused {
- background-color: scheme.$mauve;
- }
- }
-
- &:not(.labels-shown) > button {
- min-width: lib.s(8);
- min-height: lib.s(8);
- background-color: scheme.$surface1;
-
- &.occupied {
- background-color: scheme.$overlay1;
- }
-
- &.focused {
- background-color: scheme.$mauve;
- }
- }
-
- &.labels-shown > button {
- color: color.change(scheme.$overlay1, $alpha: 1);
-
- .icon {
- font-size: lib.s(13);
- color: color.change(scheme.$subtext0, $alpha: 1);
- }
-
- &.occupied {
- color: color.mix(scheme.$text, scheme.$mauve, 50%);
- }
-
- &.focused {
- color: color.change(scheme.$base, $alpha: 1);
-
- .icon {
- color: color.change(scheme.$surface0, $alpha: 1);
- }
- }
- }
- }
-
- .tray {
- font-size: lib.s(15);
- color: scheme.$text;
- }
-
- .status-icons {
- color: scheme.$rosewater;
- }
-
- .pkg-updates {
- color: scheme.$blue;
- }
-
- .notif-count {
- color: scheme.$mauve;
- }
-
- .battery {
- color: scheme.$teal;
-
- &.charging {
- color: scheme.$success;
- }
-
- &.low {
- color: scheme.$error;
- }
- }
-
- .date-time {
- color: scheme.$peach;
- }
-
- .power {
- @include lib.element-decel;
- @include font.icon;
-
- color: scheme.$red;
- font-weight: bold;
- font-size: lib.s(16);
-
- &:hover,
- &:focus {
- color: color.change(scheme.$red, $alpha: 0.8);
- }
-
- &:active {
- color: color.change(scheme.$red, $alpha: 0.6);
- }
- }
-
- &.horizontal {
- margin: 10px 10px 0 10px;
-
- .module {
- padding: lib.s(5) lib.s(10);
-
- @include lib.spacing;
- }
-
- .os-icon {
- padding-right: lib.s(14);
- }
-
- .media-playing {
- @include lib.spacing(8);
- }
-
- .workspaces {
- @include lib.spacing(10);
-
- & > .focused {
- min-width: lib.s(30);
- }
-
- &.labels-shown > button {
- padding: lib.s(3) lib.s(8);
-
- &.focused {
- min-width: 0;
- padding-left: lib.s(20);
- padding-right: lib.s(20);
- }
-
- .icon {
- margin-left: lib.s(5);
-
- &:nth-child(2) {
- margin-left: lib.s(12);
- }
- }
- }
- }
-
- .tray {
- @include lib.spacing(10);
- }
-
- .status-icons .bluetooth {
- @include lib.spacing(10);
-
- // The spacing doesn't look right for some reason so this
- & > :first-child:not(:last-child) {
- margin-right: lib.s(5);
- }
- }
- }
-
- &.vertical {
- margin: 10px 0 10px 10px;
-
- .module {
- padding: lib.s(8);
-
- @include lib.spacing($vertical: true);
- }
-
- .os-icon > * {
- margin-left: lib.s(-5);
- }
-
- .media-playing {
- @include lib.spacing(8, true);
- }
-
- .workspaces {
- @include lib.spacing(10, true);
-
- & > .focused {
- min-height: lib.s(30);
- }
-
- &.labels-shown > button {
- padding: lib.s(3) lib.s(8);
-
- &.focused {
- min-height: 0;
- padding-top: lib.s(15);
- padding-bottom: lib.s(15);
- }
-
- .icon {
- margin-top: lib.s(2);
-
- &:nth-child(2) {
- margin-top: lib.s(3);
- }
- }
- }
- }
-
- .tray {
- @include lib.spacing(10, true);
- }
-
- .status-icons .bluetooth {
- @include lib.spacing(10, true);
- }
- }
-
- &.gaps {
- padding-right: lib.s(3);
-
- .module {
- @include lib.rounded(8);
-
- background-color: scheme.$base;
- }
-
- .screen-corner {
- background-color: transparent;
- }
-
- .os-icon {
- @include lib.border(scheme.$yellow);
-
- @if not scheme.$borders {
- @include lib.shadow;
-
- background-color: scheme.$yellow;
- color: scheme.$base;
- }
- }
-
- .power {
- @include lib.border(scheme.$red);
-
- @if not scheme.$borders {
- @include lib.shadow;
-
- background-color: scheme.$red;
- color: scheme.$base;
- }
- }
-
- &.horizontal {
- @include bar-spacing;
-
- .workspaces {
- padding: lib.s(6) lib.s(15);
- }
- }
-
- &.vertical {
- @include bar-spacing(true);
-
- .workspaces {
- padding: lib.s(15) lib.s(6);
- }
- }
- }
-
- &.panel {
- @include lib.rounded(20);
- @include lib.border(scheme.$primary, 0.5, 2);
-
- background-color: scheme.$base;
-
- .os-icon {
- font-size: lib.s(16);
- }
-
- &.horizontal {
- padding: lib.s(5) lib.s(10);
- }
-
- &.vertical {
- padding: lib.s(10) lib.s(5);
-
- .os-icon > * {
- margin-left: lib.s(-7);
- }
- }
- }
-
- &.embedded {
- $-rounding: 23;
-
- margin: 0;
-
- .module {
- background-color: scheme.$base;
- }
-
- .screen-corner {
- @include lib.rounded($-rounding);
-
- background-color: scheme.$base;
- }
-
- &.horizontal {
- .module {
- padding: lib.s(10) lib.s(10);
- padding-left: lib.s(15);
- }
-
- .before-spacer {
- border-bottom-right-radius: lib.s($-rounding);
- padding-right: lib.s(15);
- }
-
- .after-spacer {
- border-bottom-left-radius: lib.s($-rounding);
- }
-
- .workspaces.odd {
- margin-right: -1px;
- }
-
- .last {
- padding-right: lib.s(12);
- }
- }
-
- &.vertical {
- .module {
- padding: lib.s(8) lib.s(10);
- }
-
- .before-spacer {
- border-bottom-right-radius: lib.s($-rounding);
- padding-bottom: lib.s(15);
- }
-
- .after-spacer {
- border-top-right-radius: lib.s($-rounding);
- padding-top: lib.s(15);
- }
-
- .workspaces.odd {
- margin-bottom: -1px;
- }
-
- .first {
- padding-top: lib.s(12);
- }
-
- .last {
- padding-bottom: lib.s(12);
- }
- }
- }
-}
diff --git a/scss/common.scss b/scss/common.scss
deleted file mode 100644
index d0fb799..0000000
--- a/scss/common.scss
+++ /dev/null
@@ -1,68 +0,0 @@
-@use "scheme";
-@use "lib";
-@use "font";
-
-label.icon {
- @include font.icon;
-}
-
-.screen-corner {
- @include lib.rounded(15);
-
- background-color: scheme.$base;
-}
-
-.notification {
- .inner {
- @include font.main;
-
- color: scheme.$text;
- padding: lib.s(10) lib.s(12);
-
- @include lib.spacing($vertical: true);
- }
-
- .header,
- .content {
- padding: 0 lib.s(5);
- }
-
- .header {
- @include font.mono;
- @include lib.spacing(8);
- }
-
- .content {
- @include lib.spacing(10);
- }
-
- .app-icon {
- font-size: lib.s(18);
- }
-
- .image {
- @include lib.rounded(10);
-
- background-size: cover;
- background-position: center;
- margin-top: lib.s(3);
- min-width: lib.s(64);
- min-height: lib.s(64);
-
- &.small {
- min-width: lib.s(48);
- min-height: lib.s(48);
- }
- }
-
- .summary {
- @include font.title;
-
- font-size: lib.s(16);
- }
-
- .body {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-}
diff --git a/scss/launcher.scss b/scss/launcher.scss
deleted file mode 100644
index ff2c422..0000000
--- a/scss/launcher.scss
+++ /dev/null
@@ -1,332 +0,0 @@
-@use "sass:color";
-@use "scheme";
-@use "lib";
-@use "font";
-
-@mixin launcher($mode, $colour) {
- &.#{$mode} {
- @include lib.border($colour, 0.7, 2);
-
- label.icon {
- color: $colour;
- }
-
- .separator {
- background-color: color.change($colour, $alpha: 0.7);
- }
-
- .result:focus {
- color: $colour;
-
- .sublabel {
- color: color.mix(scheme.$subtext0, $colour, 60%);
- }
- }
-
- &.lines {
- .search-bar {
- .mode {
- @include lib.border($colour, $width: 2, $force: true);
- }
-
- .entry {
- border-bottom: lib.s(2) solid $colour;
- }
- }
-
- .mode-switcher .mode {
- &.selected {
- border-top: lib.s(2) solid $colour;
- }
-
- &:hover,
- &:focus {
- color: $colour;
- }
-
- &:active {
- color: color.mix($colour, scheme.$base, 80%);
- }
- }
-
- .result:focus {
- border-bottom: lib.s(2) solid $colour;
- }
- }
-
- &.round {
- .search-bar .mode {
- background-color: $colour;
- }
-
- .mode-switcher .mode.selected {
- color: $colour;
- }
- }
- }
-}
-
-.launcher {
- @include lib.rounded(10);
- @include lib.element-decel;
- @include lib.shadow;
- @include font.mono;
-
- background-color: scheme.$base;
- color: scheme.$text;
- padding: lib.s(14);
- font-size: lib.s(16);
- min-width: lib.s(700);
- min-height: lib.s(420);
-
- @include launcher(apps, scheme.$primary);
- @include launcher(files, scheme.$secondary);
- @include launcher(math, scheme.$tertiary);
-
- .search-bar {
- margin-bottom: lib.s(15);
-
- @include lib.spacing(10);
-
- .mode {
- @include lib.rounded(5);
- @include lib.element-decel;
-
- padding: lib.s(5) lib.s(10);
-
- @include lib.spacing(3);
-
- .icon {
- font-size: lib.s(20);
- }
- }
- }
-
- .mode-switcher .mode {
- @include lib.element-decel;
-
- padding-top: lib.s(10);
-
- .icon {
- font-size: lib.s(24);
- }
-
- & > box {
- @include lib.spacing(10);
- }
- }
-
- .result {
- @include lib.element-decel;
-
- padding-left: lib.s(10);
-
- .icon {
- font-size: lib.s(32);
- }
-
- .has-sublabel {
- padding: lib.s(3) 0;
- }
-
- .sublabel {
- @include lib.element-decel;
-
- color: scheme.$subtext0;
- font-size: lib.s(14);
- }
-
- & > box {
- @include lib.spacing(10);
- }
-
- &.italic {
- font-style: italic;
- }
-
- &:hover {
- background-color: scheme.$surface0;
- }
-
- &:active {
- background-color: color.mix(scheme.$surface0, scheme.$surface1, 70%);
- }
- }
-
- .math {
- .preview > * {
- margin-bottom: lib.s(10);
- }
-
- .result {
- @include lib.spacing(10);
- }
- }
-
- .swatches {
- margin-bottom: lib.s(5);
-
- @include lib.spacing(3);
- }
-
- .swatch {
- @include lib.rounded(100);
-
- min-width: lib.s(16);
- min-height: lib.s(16);
-
- &.big {
- min-height: lib.s(32);
-
- &.left {
- @include lib.border(scheme.$overlay0, 0.3, $force: true);
-
- border-right: none;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
-
- &.right {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- }
- }
- }
-
- .wallpaper {
- .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);
-
- & > * {
- background-size: cover;
- background-position: center;
-
- &:first-child {
- @include lib.rounded(10, $tr: 0, $br: 0);
- }
-
- &:last-child {
- @include lib.rounded(10, $tl: 0, $bl: 0);
- }
- }
- }
- }
-
- &.medium .thumbnail {
- min-height: lib.s(96);
- }
-
- &.large .thumbnail {
- min-height: lib.s(160);
- }
- }
-
- &.lines {
- .mode-switcher .mode {
- border-top: lib.s(2) solid transparent;
- }
-
- .result {
- border-bottom: lib.s(2) solid transparent;
- }
-
- .wallpaper {
- padding-top: lib.s(5);
-
- &.compact {
- padding-top: lib.s(3);
- padding-bottom: lib.s(3);
- }
- }
- }
-
- &.round {
- .search-bar {
- .mode {
- @include lib.rounded(10);
-
- color: color.change(scheme.$base, $alpha: 1);
-
- .icon {
- color: color.change(scheme.$base, $alpha: 1);
- }
- }
-
- .entry {
- @include lib.rounded(10);
-
- padding: lib.s(5) lib.s(10);
- background-color: color.mix(scheme.$base, scheme.$surface0, 30%);
- }
- }
-
- .mode-switcher {
- padding-top: lib.s(10);
-
- @include lib.spacing(10);
-
- .mode {
- @include lib.rounded(10);
-
- padding: lib.s(5);
- background-color: color.mix(scheme.$base, scheme.$surface0, 50%);
-
- &:not(.selected) .icon {
- color: scheme.$text;
- }
-
- &:hover,
- &:focus {
- background-color: scheme.$surface0;
- }
-
- &:active {
- background-color: color.mix(scheme.$surface0, scheme.$surface1, 70%);
- }
- }
- }
-
- .result {
- @include lib.rounded(10);
-
- padding-right: lib.s(10);
- margin-bottom: lib.s(5);
- margin-right: lib.s(5);
-
- &:focus {
- background-color: scheme.$surface0;
- }
- }
-
- .math .preview .result {
- @include lib.rounded(20);
-
- background-color: scheme.$surface0;
- padding: lib.s(5) lib.s(10);
- }
-
- .wallpaper-container {
- padding-right: 0;
-
- .wallpaper:not(.compact) {
- padding-top: lib.s(8);
- padding-bottom: lib.s(3);
- }
- }
- }
-}
diff --git a/scss/mediadisplay.scss b/scss/mediadisplay.scss
deleted file mode 100644
index a167133..0000000
--- a/scss/mediadisplay.scss
+++ /dev/null
@@ -1,139 +0,0 @@
-@use "sass:color";
-@use "scheme";
-@use "lib";
-@use "font";
-
-.mediadisplay {
- @include font.mono;
-
- background-color: scheme.$base;
- color: scheme.$text;
- padding: lib.s(20);
- min-height: lib.s(200);
-
- .visualiser {
- background-color: scheme.$primary; // Visualiser colour
- margin-right: lib.s(5); // Gaps between bars
- min-width: lib.s(10); // Bar width
- color: scheme.$error;
- font-size: lib.s(24);
- font-weight: bold;
- }
-
- .cover-art {
- @include lib.rounded(10);
- @include lib.element-decel;
-
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- min-width: lib.s(196);
- min-height: lib.s(196);
- font-size: lib.s(96);
- font-weight: bold;
- background-color: scheme.$surface0;
- color: scheme.$subtext0;
- }
-
- .details {
- @include font.title;
-
- font-size: lib.s(14);
- margin-top: lib.s(5);
- margin-left: lib.s(15);
-
- .title {
- font-size: lib.s(28);
- font-weight: 500;
- color: scheme.$text;
- }
-
- .artist {
- font-size: lib.s(18);
- color: scheme.$secondary;
- }
-
- .controls {
- @include lib.rounded(1000);
- @include font.icon;
-
- margin-top: lib.s(10);
- background-color: color.change(scheme.$overlay0, $alpha: 0.4);
- font-size: lib.s(28);
- padding: lib.s(3) lib.s(8);
-
- @include lib.spacing(10);
-
- & > button {
- @include lib.element-decel;
-
- &:hover,
- &:focus {
- color: color.mix(scheme.$subtext1, scheme.$subtext0, 50%);
- }
-
- &:active {
- color: scheme.$subtext0;
- }
-
- &:disabled {
- color: scheme.$subtext0;
- }
- }
- }
- }
-
- .center-module {
- @include lib.rounded(20);
- margin: 0 lib.s(40);
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- }
-
- .selector {
- @include lib.rounded(15);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$overlay0, $alpha: 0.4);
- padding: lib.s(8) lib.s(15);
-
- .identity {
- @include lib.spacing(8);
- }
-
- button {
- @include lib.element-decel;
-
- &:hover,
- &:focus {
- color: color.mix(scheme.$subtext1, scheme.$subtext0, 50%);
- }
-
- &:active {
- color: scheme.$subtext0;
- }
- }
-
- .list > button {
- margin-top: lib.s(5);
- color: scheme.$subtext1;
-
- &:hover,
- &:focus {
- color: color.mix(scheme.$subtext1, scheme.$subtext0, 50%);
- }
-
- &:active {
- color: scheme.$subtext0;
- }
- }
- }
-
- .time {
- @include lib.rounded(1000);
-
- font-size: lib.s(16);
- background-color: color.change(scheme.$overlay0, $alpha: 0.4);
- padding: lib.s(5) lib.s(10);
- margin-bottom: lib.s(10);
- }
-}
diff --git a/scss/navbar.scss b/scss/navbar.scss
deleted file mode 100644
index 72ace59..0000000
--- a/scss/navbar.scss
+++ /dev/null
@@ -1,65 +0,0 @@
-@use "sass:color";
-@use "scheme";
-@use "lib";
-@use "font";
-
-.navbar {
- @include font.mono;
-
- background-color: scheme.$base;
-
- button {
- color: scheme.$subtext1;
-
- &:hover,
- &:focus {
- color: scheme.$subtext0;
- }
-
- &:active {
- color: color.change(scheme.$overlay2, $alpha: 1);
- }
-
- &.current {
- .nav-button {
- background-color: scheme.$primary;
- color: color.change(scheme.$base, $alpha: 1);
- }
-
- &:hover .nav-button,
- &:focus .nav-button {
- background-color: color.mix(scheme.$primary, scheme.$base, 80%);
- }
-
- &:active .nav-button {
- background-color: color.mix(scheme.$primary, scheme.$base, 70%);
- }
- }
-
- &:first-child .nav-button {
- margin-top: lib.s(10);
- }
-
- &:last-child .nav-button {
- margin-bottom: lib.s(10);
- }
- }
-
- .nav-button {
- @include lib.rounded(20);
- @include lib.element-decel;
-
- padding: lib.s(10) lib.s(8);
- margin: lib.s(5) lib.s(8);
- min-width: lib.s(40);
-
- .icon {
- font-size: lib.s(28);
- }
-
- .label {
- font-size: lib.s(12);
- margin-bottom: lib.s(5);
- }
- }
-}
diff --git a/scss/notifpopups.scss b/scss/notifpopups.scss
deleted file mode 100644
index 92d2760..0000000
--- a/scss/notifpopups.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-@use "sass:color";
-@use "scheme";
-@use "lib";
-@use "font";
-
-@mixin popup($colour) {
- .separator {
- background-color: $colour;
- }
-
- .image {
- @include lib.border($colour, 0.05);
- }
-}
-
-.notifpopups {
- min-width: lib.s(425);
- padding-left: lib.s(10); // So notifications can overshoot for init animation
- padding-right: lib.s(5);
- padding-top: lib.s(5);
-
- .notification {
- .wrapper {
- padding: lib.s(5); // For shadow
- }
-
- .inner {
- @include lib.rounded(10);
- @include lib.shadow;
-
- background-color: scheme.$base;
-
- &.low {
- @include popup(scheme.$overlay0);
- }
-
- &.normal {
- @include popup(scheme.$primary);
- }
-
- &.critical {
- @include lib.border(scheme.$error, 0.5);
- @include popup(scheme.$error);
-
- @if not scheme.$borders {
- background-color: color.mix(scheme.$base, scheme.$error, 95%);
- }
- }
- }
- }
-}
diff --git a/scss/osds.scss b/scss/osds.scss
deleted file mode 100644
index d7afd24..0000000
--- a/scss/osds.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-@use "scheme";
-@use "lib";
-@use "font";
-
-.brightness,
-.volume {
- @include lib.rounded(8);
- @include lib.border(scheme.$overlay0, 0.1);
- @include lib.shadow;
- @include font.mono;
-
- background-color: scheme.$base;
- font-size: lib.s(16);
- padding: lib.s(3);
-
- .inner {
- @include lib.fluent-decel(1000ms);
-
- min-width: lib.s(300);
- min-height: lib.s(32);
- background-color: scheme.$teal;
- }
-}
-
-.volume .inner.mute {
- background-color: scheme.$overlay0;
-}
-
-.lock {
- @include lib.rounded(10);
- @include lib.border(scheme.$overlay0, 0.1);
- @include lib.shadow;
- @include lib.element-decel;
- @include font.mono;
-
- min-width: lib.s(80);
- min-height: lib.s(80);
- padding: lib.s(10);
- background-color: scheme.$base;
- color: scheme.$overlay0;
- font-size: lib.s(16);
- font-weight: bold;
-
- &.enabled {
- color: scheme.$text;
- }
-
- .icon {
- font-size: lib.s(48);
- }
-}
diff --git a/scss/scheme/_default.scss b/scss/scheme/_default.scss
deleted file mode 100644
index 71ea197..0000000
--- a/scss/scheme/_default.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-$rosewater: #edcbc5;
-$flamingo: #d3a4a4;
-$pink: #d792c6;
-$mauve: #c678dd;
-$red: #be5046;
-$maroon: #e06c75;
-$peach: #d19a66;
-$yellow: #e5c07b;
-$green: #98c379;
-$teal: #56b6c2;
-$sky: #90ccd7;
-$sapphire: #389dcc;
-$blue: #61afef;
-$lavender: #8e98d9;
-$text: #abb2bf;
-$subtext1: #95a0b5;
-$subtext0: #838b9c;
-$overlay2: #767f8f;
-$overlay1: #666e7c;
-$overlay0: #5c6370;
-$surface2: #4b5263;
-$surface1: #3c414f;
-$surface0: #30343e;
-$base: #282c34;
-$mantle: #21242b;
-$crust: #1e2126;
-$success: #98c379;
-$error: #be5046;
-$primary: #d19a66;
-$secondary: #61afef;
-$tertiary: #98c379;
diff --git a/scss/session.scss b/scss/session.scss
deleted file mode 100644
index 69c8b33..0000000
--- a/scss/session.scss
+++ /dev/null
@@ -1,58 +0,0 @@
-@use "sass:color";
-@use "scheme";
-@use "lib";
-@use "font";
-
-.session {
- background-color: rgba(0, 0, 0, 0.3);
-
- .inner {
- @include lib.rounded(10);
- @include lib.border(scheme.$flamingo, 0.5, 2);
- @include lib.shadow;
- @include font.mono;
-
- background-color: scheme.$mantle;
- color: scheme.$text;
- padding: lib.s(18) lib.s(20);
-
- @include lib.spacing(10, true);
-
- & > * {
- @include lib.spacing(20);
- }
-
- .item {
- font-size: lib.s(14);
-
- @include lib.spacing($vertical: true);
-
- button {
- @include lib.rounded(100);
- @include lib.element-decel(300ms);
-
- background-color: color.change(scheme.$surface0, $alpha: 0.4);
- min-width: lib.s(100);
- min-height: lib.s(100);
- font-size: lib.s(32);
-
- &:hover {
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- }
-
- &:focus {
- background-color: color.mix(scheme.$flamingo, scheme.$base, 70%);
- color: color.change(scheme.$base, $alpha: 1);
- }
-
- &:active {
- background-color: color.mix(scheme.$flamingo, scheme.$base, 50%);
- }
- }
-
- .label {
- font-weight: 500;
- }
- }
- }
-}
diff --git a/scss/sidebar.scss b/scss/sidebar.scss
deleted file mode 100644
index d82ad7f..0000000
--- a/scss/sidebar.scss
+++ /dev/null
@@ -1,1118 +0,0 @@
-@use "sass:color";
-@use "sass:list";
-@use "scheme";
-@use "lib";
-@use "font";
-
-@mixin notification($accent) {
- .separator {
- background-color: $accent;
- }
-
- .image {
- @include lib.border($accent, 0.05);
- }
-}
-
-@mixin button {
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface1, $alpha: 0.5);
-
- &:hover,
- &:focus {
- background-color: color.change(scheme.$surface2, $alpha: 0.5);
- }
-
- &:active {
- background-color: color.change(scheme.$overlay0, $alpha: 0.5);
- }
-
- &:disabled {
- color: scheme.$subtext0;
- }
-}
-
-@mixin button-active {
- @include lib.element-decel;
-
- background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.5);
-
- &:hover,
- &:focus {
- background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 30%), $alpha: 0.5);
- }
-
- &:active {
- background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 20%), $alpha: 0.5);
- }
-}
-
-@mixin media-button {
- @include lib.element-decel;
-
- &:disabled {
- color: color.change(scheme.$overlay2, $alpha: 1);
- }
-
- &:hover,
- &:focus {
- color: color.mix(scheme.$subtext1, scheme.$subtext0, 50%);
- }
-
- &:active {
- color: scheme.$subtext0;
- }
-}
-
-.sidebar {
- @include font.mono;
-
- background-color: scheme.$base;
- color: scheme.$text;
- padding: lib.s(18) lib.s(20);
- min-width: lib.s(380);
-
- .pane {
- @include lib.spacing(20, true);
- }
-
- .separator {
- background-color: if(scheme.$light, scheme.$surface1, scheme.$overlay0);
- margin: 0 lib.s(10);
- }
-
- .header-bar {
- margin-bottom: lib.s(10);
-
- @include lib.spacing;
-
- & > :not(button) {
- font-weight: bold;
- font-size: lib.s(16);
- }
-
- & > button {
- @include lib.element-decel;
- @include lib.rounded(10);
-
- padding: lib.s(3) lib.s(8);
-
- &:disabled {
- color: color.change(scheme.$overlay0, $alpha: 1);
- }
-
- &:hover,
- &:focus {
- color: scheme.$subtext0;
- }
-
- &:active {
- color: color.change(scheme.$overlay2, $alpha: 1);
- }
-
- &.enabled {
- $-base: color.change(scheme.$base, $alpha: 1);
-
- background-color: scheme.$primary;
- color: $-base;
-
- &:hover,
- &:focus {
- background-color: color.mix(scheme.$primary, $-base, 80%);
- }
-
- &:active {
- background-color: color.mix(scheme.$primary, $-base, 70%);
- }
- }
- }
- }
-
- .empty {
- color: scheme.$subtext0;
- font-size: lib.s(18);
-
- .icon {
- font-size: lib.s(48);
- }
- }
-
- .user {
- @include lib.spacing(15);
-
- .face {
- @include lib.rounded(10);
-
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- min-width: lib.s(96);
- min-height: lib.s(96);
- font-size: lib.s(48);
- font-weight: bold;
- background-color: scheme.$base;
- }
-
- .details {
- font-size: lib.s(14);
-
- @include lib.spacing(8, true);
-
- .name {
- font-size: lib.s(18);
- color: scheme.$text;
- margin-bottom: lib.s(10);
- }
-
- $-colours: scheme.$yellow, scheme.$blue;
- @for $i from 1 through list.length($-colours) {
- :nth-child(#{$i + 1}) {
- color: list.nth($-colours, $i);
- }
- }
- }
- }
-
- .media {
- @include lib.spacing(15);
-
- .cover-art {
- @include lib.rounded(10);
- @include lib.element-decel;
-
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- min-width: lib.s(128);
- min-height: lib.s(128);
- font-size: lib.s(64);
- font-weight: bold;
- background-color: scheme.$base;
- color: scheme.$subtext0;
- }
-
- .details {
- font-size: lib.s(14);
-
- .title {
- font-size: lib.s(16);
- color: scheme.$text;
- }
-
- .artist {
- color: scheme.$green;
- }
-
- .controls {
- margin-top: lib.s(20);
- margin-bottom: lib.s(5);
- font-size: lib.s(24);
-
- & > button {
- @include media-button;
- }
- }
-
- .slider {
- @include lib.rounded(5);
- @include lib.fluent-decel(1000ms);
-
- min-height: lib.s(8);
- background-color: scheme.$overlay0;
- color: scheme.$subtext1;
- }
-
- .time {
- margin-top: lib.s(5);
- font-size: lib.s(13);
- color: scheme.$subtext0;
- }
- }
- }
-
- .notification {
- .wrapper {
- padding-bottom: lib.s(10);
- }
-
- .inner {
- @include lib.rounded(20);
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
-
- &.low {
- @include notification(if(scheme.$light, scheme.$surface1, scheme.$overlay0));
-
- @if not scheme.$borders {
- background-color: color.change(scheme.$surface0, $alpha: 0.4);
- }
- }
-
- &.normal {
- @include lib.border(scheme.$primary, if(scheme.$light, 0.5, 0.3));
- @include notification(scheme.$primary);
- }
-
- &.critical {
- @include lib.border(scheme.$error, 0.8);
- @include notification(scheme.$error);
-
- @if not scheme.$borders {
- background-color: color.change(color.mix(scheme.$surface1, scheme.$error, 80%), $alpha: 0.4);
- }
- }
- }
-
- .actions {
- @include lib.spacing;
-
- & > button {
- @include button;
- @include lib.rounded(10);
-
- padding: lib.s(5) lib.s(10);
- }
- }
- }
-
- .upcoming {
- .list {
- min-height: lib.s(300);
- }
-
- .day {
- @include lib.spacing($vertical: true);
-
- &:not(:first-child) {
- margin-top: lib.s(20);
- }
-
- .date {
- margin-left: lib.s(10);
- }
-
- .sublabel {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-
- .events {
- @include lib.rounded(20);
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- padding: lib.s(10) lib.s(15);
-
- @include lib.spacing(10, true);
- }
-
- .event {
- @include lib.spacing(8);
- }
-
- .calendar-indicator {
- @include lib.rounded(5);
-
- min-width: lib.s(1);
-
- $-colours: scheme.$red, scheme.$sapphire, scheme.$flamingo, scheme.$maroon, scheme.$pink, scheme.$sky,
- scheme.$peach, scheme.$yellow, scheme.$green, scheme.$rosewater, scheme.$mauve, scheme.$teal,
- scheme.$blue;
- @for $i from 1 through list.length($-colours) {
- &.calendar-#{$i} {
- background-color: list.nth($-colours, $i);
- }
- }
- }
- }
- }
-
- .players {
- .player {
- @include lib.spacing(40, true);
-
- .cover-art {
- @include lib.rounded(10);
- @include lib.element-decel;
- @include lib.shadow(scheme.$mantle, $blur: 5, $spread: 2);
-
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- min-width: lib.s(256);
- min-height: lib.s(256);
- font-size: lib.s(96);
- font-weight: bold;
- background-color: scheme.$base;
- color: scheme.$subtext0;
- margin-top: lib.s(20);
- }
-
- .progress {
- margin: 0 lib.s(40);
-
- .slider {
- @include lib.rounded(8);
- @include lib.fluent-decel(1000ms);
-
- min-height: lib.s(15);
- background-color: scheme.$overlay0;
- color: scheme.$subtext1;
- }
-
- .time {
- margin-top: lib.s(5);
- font-size: lib.s(13);
- color: scheme.$subtext1;
- }
- }
-
- .details {
- font-size: lib.s(14);
- margin-top: lib.s(20);
-
- @include lib.spacing(3, true);
-
- .title {
- font-size: lib.s(18);
- color: scheme.$text;
- font-weight: bold;
- }
-
- .artist {
- color: scheme.$green;
- }
-
- .album {
- color: scheme.$subtext0;
- }
- }
-
- .controls {
- margin-top: lib.s(-20);
- margin-bottom: lib.s(5);
-
- button {
- @include media-button;
-
- // Cause some nerd font icons don't have the correct width
- &.needs-adjustment {
- padding-right: lib.s(5);
- }
- }
-
- .playback {
- font-size: lib.s(32);
-
- @include lib.spacing(40);
- }
-
- .options {
- margin: 0 lib.s(40);
- margin-top: lib.s(-10);
- font-size: lib.s(20);
-
- @include lib.spacing(20);
- }
- }
- }
-
- .indicators {
- @include lib.spacing(10);
-
- & > button {
- @include lib.rounded(1000);
- @include lib.element-decel;
-
- min-width: lib.s(10);
- min-height: lib.s(10);
-
- background-color: color.change(scheme.$overlay0, $alpha: 0.5);
-
- &:hover,
- &:focus {
- background-color: color.change(scheme.$overlay1, $alpha: 0.5);
- }
-
- &:active {
- background-color: color.change(scheme.$overlay2, $alpha: 0.5);
- }
-
- &.active {
- background-color: color.change(scheme.$primary, $alpha: 0.9);
-
- &:hover,
- &:focus {
- background-color: color.change(scheme.$primary, $alpha: 0.7);
- }
-
- &:active {
- background-color: color.change(scheme.$primary, $alpha: 0.6);
- }
- }
- }
- }
- }
-
- .no-wp-prompt {
- font-size: lib.s(16);
- color: scheme.$error;
- margin-top: lib.s(8);
- }
-
- .streams {
- .list {
- @include lib.spacing(10, true);
- }
-
- .stream {
- @include lib.rounded(20);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- padding: lib.s(10) lib.s(15);
-
- @include lib.spacing(5);
-
- &.playing {
- background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.4);
- }
-
- .icon {
- font-size: lib.s(28);
- margin-right: lib.s(12);
- }
-
- .sublabel {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-
- trough {
- @include lib.rounded(10);
-
- min-width: lib.s(100);
- min-height: lib.s(10);
- background-color: color.change(scheme.$error, $alpha: 0.3);
-
- fill {
- @include lib.rounded(10);
-
- background-color: color.change(scheme.$overlay0, $alpha: 1);
- }
-
- highlight {
- @include lib.rounded(10);
-
- background-color: scheme.$subtext1;
- }
- }
-
- & > button {
- @include media-button;
-
- font-size: lib.s(18);
- min-width: lib.s(20);
- min-height: lib.s(20);
- }
- }
- }
-
- .device-selector {
- @include lib.spacing(10, true);
-
- .selector {
- @include lib.rounded(20);
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- padding: lib.s(10) lib.s(15);
-
- .icon {
- font-size: lib.s(20);
- }
-
- .separator {
- margin-bottom: lib.s(8);
- margin-top: lib.s(5);
- background-color: if(scheme.$light, scheme.$overlay1, scheme.$overlay0);
- }
-
- .list {
- color: scheme.$subtext0;
-
- @include lib.spacing(3, true);
- }
-
- .device {
- @include lib.spacing;
- }
-
- .selected {
- color: scheme.$text;
-
- @include lib.spacing(10);
-
- .icon {
- font-size: lib.s(32);
- }
-
- .sublabel {
- color: scheme.$subtext0;
- }
- }
-
- button {
- @include lib.element-decel;
-
- &:hover,
- &:focus {
- color: scheme.$subtext1;
- }
-
- &:active {
- color: scheme.$text;
- }
- }
- }
-
- .stream {
- @include lib.rounded(20);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- padding: lib.s(10) lib.s(15);
-
- @include lib.spacing(5);
-
- &.playing {
- background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.4);
- }
-
- .icon {
- font-size: lib.s(28);
- margin-right: lib.s(12);
- }
-
- .sublabel {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-
- trough {
- @include lib.rounded(10);
-
- min-width: lib.s(100);
- min-height: lib.s(10);
- background-color: color.change(scheme.$error, $alpha: 0.3);
-
- fill {
- @include lib.rounded(10);
-
- background-color: color.change(scheme.$overlay0, $alpha: 1);
- }
-
- highlight {
- @include lib.rounded(10);
-
- background-color: scheme.$subtext1;
- }
- }
-
- & > button {
- @include media-button;
-
- font-size: lib.s(18);
- min-width: lib.s(20);
- min-height: lib.s(20);
- }
- }
- }
-
- .networks {
- .list {
- @include lib.spacing(10, true);
- }
-
- .network {
- @include lib.rounded(20);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- padding: lib.s(10) lib.s(15);
-
- @include lib.spacing(5);
-
- &.connected {
- background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.4);
-
- & > button {
- @include button-active;
- }
- }
-
- .icon {
- font-size: lib.s(28);
- margin-right: lib.s(12);
- }
-
- .sublabel {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-
- & > button {
- @include button;
- @include lib.rounded(1000);
- @include font.icon;
-
- font-size: lib.s(18);
- min-width: lib.s(30);
- min-height: lib.s(30);
- }
- }
- }
-
- .bluetooth {
- .list {
- @include lib.spacing(10, true);
- }
-
- .device {
- @include lib.rounded(20);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- padding: lib.s(10) lib.s(15);
-
- @include lib.spacing(5);
-
- &.connected {
- background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.4);
-
- & > button {
- @include button-active;
- }
- }
-
- .icon {
- font-size: lib.s(28);
- margin-right: lib.s(12);
- }
-
- .sublabel {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-
- & > button {
- @include button;
- @include lib.rounded(1000);
- @include font.icon;
-
- font-size: lib.s(18);
- min-width: lib.s(30);
- min-height: lib.s(30);
- }
- }
- }
-
- .updates {
- .list {
- @include lib.spacing(10, true);
- }
-
- .repo {
- @include lib.rounded(20);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- padding: lib.s(10) lib.s(15);
-
- @include lib.spacing(5);
-
- .icon {
- font-size: lib.s(28);
-
- &:not(:last-child) {
- margin-right: lib.s(12);
- }
- }
-
- .sublabel {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-
- .body {
- margin-top: lib.s(10);
- font-size: lib.s(14);
- }
- }
- }
-
- .news {
- min-height: lib.s(200);
-
- .empty {
- margin-top: lib.s(40);
- }
-
- .list {
- @include lib.spacing(10, true);
- }
-
- .article {
- @include lib.rounded(20);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- padding: lib.s(10) lib.s(15);
-
- @include lib.spacing(5);
-
- .icon {
- font-size: lib.s(28);
-
- &:not(:last-child) {
- margin-right: lib.s(12);
- }
- }
-
- .sublabel {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-
- .body {
- margin-top: lib.s(10);
- font-size: lib.s(14);
- }
- }
- }
-
- .headlines {
- min-height: lib.s(200);
-
- .empty {
- margin-top: lib.s(40);
- }
-
- .list {
- @include lib.spacing(10, true);
- }
-
- .category {
- @include lib.rounded(20);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface0, $alpha: 0.5);
- padding: lib.s(10) lib.s(15);
-
- @include lib.spacing(5);
-
- .icon {
- font-size: lib.s(28);
-
- &:not(:last-child) {
- margin-right: lib.s(12);
- }
- }
-
- .sublabel {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-
- .body {
- margin-top: lib.s(10);
- font-size: lib.s(14);
-
- @include lib.spacing(8, true);
- }
- }
-
- .article {
- @include lib.rounded(20);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface1, $alpha: 0.3);
- padding: lib.s(10) lib.s(15);
-
- .article-body {
- @include font.reading;
- @include lib.element-decel;
-
- font-size: lib.s(15);
- margin-top: lib.s(10);
- color: scheme.$subtext1;
-
- & > :last-child {
- margin-top: lib.s(8);
- }
-
- .title {
- @include font.title;
-
- font-size: lib.s(18);
- font-weight: 500;
- margin-bottom: lib.s(3);
- }
- }
-
- button:hover .article-body,
- button:focus .article-body {
- color: color.mix(scheme.$subtext0, scheme.$blue, 60%);
- }
- }
- }
-
- .time-date {
- padding: lib.s(12) lib.s(8);
- font-size: lib.s(48);
- font-weight: bold;
- color: scheme.$primary;
-
- & > * {
- @include lib.spacing(3);
- }
-
- .ampm {
- font-size: lib.s(24);
- font-weight: normal;
- margin-top: lib.s(18);
- color: scheme.$secondary;
- }
-
- .date {
- font-size: lib.s(20);
- color: scheme.$tertiary;
- }
- }
-
- .calendar {
- @include lib.rounded(20);
-
- background-color: color.change(scheme.$surface1, $alpha: 0.4);
- padding: lib.s(15);
-
- .calendar-view {
- @include lib.spacing(10, true);
-
- .header {
- @include lib.spacing(10);
-
- & > button {
- @include lib.rounded(1000);
- @include lib.element-decel;
-
- background-color: color.change(scheme.$surface2, $alpha: 0.4);
- min-width: lib.s(28);
- min-height: lib.s(28);
- font-size: lib.s(18);
-
- &:first-child {
- padding: 0 lib.s(10);
- }
-
- &:hover,
- &:focus {
- background-color: color.change(scheme.$surface2, $alpha: 0.6);
- }
-
- &:active {
- background-color: color.change(scheme.$surface2, $alpha: 0.8);
- }
- }
- }
-
- .weekdays {
- @include lib.spacing(10);
-
- & > label {
- min-width: lib.s(40);
- font-weight: bold;
- color: scheme.$subtext1;
- }
- }
-
- .month {
- @include lib.spacing(10, true);
- }
-
- .week {
- @include lib.spacing(10);
- }
-
- .day {
- @include lib.rounded(1000);
- @include lib.element-decel;
-
- min-width: lib.s(40);
- min-height: lib.s(40);
-
- &.dim {
- color: scheme.$subtext0;
- }
-
- &.today:not(.dim) {
- background-color: scheme.$primary;
- color: color.change(scheme.$base, $alpha: 1);
- }
-
- &:hover,
- &:focus {
- color: scheme.$subtext0;
- }
-
- &:active {
- color: color.change(scheme.$overlay2, $alpha: 1);
- }
-
- &.dim {
- color: scheme.$subtext0;
-
- &:hover,
- &:focus {
- color: color.change(scheme.$overlay2, $alpha: 1);
- }
-
- &:active {
- color: color.change(scheme.$overlay1, $alpha: 1);
- }
- }
-
- &.today:not(.dim) {
- background-color: scheme.$primary;
- color: color.change(scheme.$base, $alpha: 1);
-
- &:hover,
- &:focus {
- background-color: color.mix(scheme.$primary, scheme.$base, 80%);
- }
-
- &:active {
- background-color: color.mix(scheme.$primary, scheme.$base, 70%);
- }
- }
-
- label {
- margin-top: lib.s(8);
- }
-
- .indicator {
- @include lib.rounded(10);
- @include lib.element-decel;
-
- min-height: lib.s(3);
- margin: 0 lib.s(8);
- }
-
- $-max: 5;
- @for $i from 1 through $-max {
- &.events-#{$i} {
- $-colour: color.mix(scheme.$red, scheme.$green, calc(100% / $-max) * $i);
-
- .indicator {
- background-color: $-colour;
- }
-
- &:hover .indicator,
- &:focus .indicator {
- background-color: color.mix($-colour, scheme.$base, 80%);
- }
-
- &:active .indicator {
- background-color: color.mix($-colour, scheme.$base, 70%);
- }
-
- &.dim .indicator {
- background-color: color.mix($-colour, scheme.$base, 60%);
- }
-
- &.today:not(.dim) {
- $-colour: color.mix($-colour, color.complement(scheme.$primary), 50%);
-
- .indicator {
- background-color: $-colour;
- }
-
- &:hover .indicator,
- &:focus .indicator {
- background-color: color.mix($-colour, scheme.$base, 80%);
- }
-
- &:active .indicator {
- background-color: color.mix($-colour, scheme.$base, 70%);
- }
- }
- }
- }
- }
- }
-
- .events {
- @include lib.spacing(10, true);
-
- .header {
- font-weight: bold;
-
- @include lib.spacing(10);
-
- & > button {
- @include lib.rounded(1000);
- @include lib.element-decel;
-
- min-width: lib.s(24);
- min-height: lib.s(24);
-
- &:hover,
- &:focus {
- color: scheme.$subtext0;
- }
-
- &:active {
- color: color.change(scheme.$overlay2, $alpha: 1);
- }
- }
- }
-
- scrollable {
- min-height: lib.s(315);
- }
-
- .date {
- margin-left: lib.s(10);
- }
-
- .sublabel {
- font-size: lib.s(14);
- color: scheme.$subtext0;
- }
-
- .list {
- padding: lib.s(5);
-
- @include lib.spacing(10, true);
- }
-
- .event {
- @include lib.spacing(8);
- }
-
- .calendar-indicator {
- @include lib.rounded(5);
-
- min-width: lib.s(1);
-
- $-colours: scheme.$red, scheme.$sapphire, scheme.$flamingo, scheme.$maroon, scheme.$pink, scheme.$sky,
- scheme.$peach, scheme.$yellow, scheme.$green, scheme.$rosewater, scheme.$mauve, scheme.$teal,
- scheme.$blue;
- @for $i from 1 through list.length($-colours) {
- &.calendar-#{$i} {
- background-color: list.nth($-colours, $i);
- }
- }
- }
- }
- }
-}
diff --git a/scss/widgets.scss b/scss/widgets.scss
deleted file mode 100644
index 719e82c..0000000
--- a/scss/widgets.scss
+++ /dev/null
@@ -1,136 +0,0 @@
-@use "sass:color";
-@use "scheme";
-@use "lib";
-@use "font";
-
-separator,
-.separator {
- @include lib.rounded(2);
-
- min-width: lib.s(0.5);
- min-height: lib.s(0.5);
-}
-
-@keyframes appear {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
-}
-
-@mixin -appear($duration: 100ms) {
- animation-name: appear;
- animation-duration: $duration;
- animation-timing-function: ease-out;
- animation-iteration-count: 1;
-}
-
-menu {
- @include -appear;
- @include lib.rounded(10);
- @include lib.border(scheme.$blue, 0.4);
- @include font.mono;
-
- padding: lib.s(8);
- background-color: scheme.$surface0;
- color: scheme.$text;
- font-size: lib.s(14);
-
- & > menuitem {
- @include lib.element-decel;
- @include lib.rounded(8);
-
- padding: lib.s(5) lib.s(8);
-
- &:hover,
- &:focus {
- background-color: scheme.$surface1;
- }
-
- &:active {
- background-color: scheme.$surface2;
- }
-
- &:disabled {
- color: scheme.$subtext0;
- }
-
- & > arrow {
- @include lib.rounded(1000);
-
- min-width: lib.s(5);
- min-height: lib.s(5);
- background-color: scheme.$blue;
-
- &.right {
- margin-left: lib.s(12);
- }
-
- &.left {
- margin-right: lib.s(12);
- }
- }
- }
-
- & > separator {
- background-color: color.mix(scheme.$blue, scheme.$surface0, 70%);
- margin: lib.s(5) 0;
- }
-}
-
-tooltip,
-.tooltip {
- @include lib.rounded(10);
- @include lib.border(scheme.$primary, 0.7);
- @include font.mono;
-
- background-color: scheme.$surface0;
- color: scheme.$text;
- padding: lib.s(5) lib.s(10);
-}
-
-tooltip {
- @include -appear(200ms);
-}
-
-.tooltip {
- @include lib.shadow;
-
- margin: lib.s(3);
-}
-
-scrollbar {
- slider {
- @include lib.rounded(1000);
- @include lib.element-decel;
-
- min-width: lib.s(3);
- min-height: lib.s(30);
- background-color: color.change(scheme.$overlay0, $alpha: 0.6);
-
- &:hover,
- &:focus {
- min-width: lib.s(6);
- background-color: color.change(scheme.$overlay0, $alpha: 0.7);
- }
-
- &:active {
- background-color: color.change(scheme.$overlay1, $alpha: 0.8);
- }
- }
-}
-
-popover {
- @include -appear;
- @include lib.rounded(10);
- @include lib.border(scheme.$yellow, 0.4);
- @include font.mono;
-
- padding: lib.s(8);
- background-color: color.mix(scheme.$base, scheme.$yellow, 90%);
- color: scheme.$text;
- font-size: lib.s(14);
-}
diff --git a/src/config/defaults.ts b/src/config/defaults.ts
deleted file mode 100644
index a5ebbbc..0000000
--- a/src/config/defaults.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import { Astal } from "astal/gtk3";
-
-export default {
- style: {
- transparency: "normal", // One of "off", "low", "normal", "high"
- borders: true,
- vibrant: false, // Extra saturation
- },
- config: {
- notifyOnError: true,
- },
- // Modules
- bar: {
- vertical: true,
- style: "gaps", // One of "gaps", "panel", "embedded"
- layout: {
- type: "centerbox", // One of "centerbox", "flowbox"
- centerbox: {
- start: ["osIcon", "activeWindow", "mediaPlaying", "brightnessSpacer"],
- center: ["workspaces"],
- end: [
- "volumeSpacer",
- "tray",
- "statusIcons",
- "pkgUpdates",
- "notifCount",
- "battery",
- "dateTime",
- "power",
- ],
- },
- flowbox: [
- "osIcon",
- "workspaces",
- "brightnessSpacer",
- "activeWindow",
- "volumeSpacer",
- "dateTime",
- "tray",
- "battery",
- "statusIcons",
- "notifCount",
- "power",
- ],
- },
- modules: {
- workspaces: {
- shown: 5,
- showLabels: false,
- labels: ["󰮯", "󰮯", "󰮯", "󰮯", "󰮯"],
- xalign: -1,
- showWindows: false,
- },
- dateTime: {
- format: "%d/%m/%y %R",
- detailedFormat: "%c",
- },
- },
- },
- launcher: {
- style: "lines", // One of "lines", "round"
- actionPrefix: ">", // Prefix for launcher actions
- apps: {
- maxResults: 30, // Actual max results, -1 for infinite
- },
- files: {
- maxResults: 40, // Actual max results, -1 for infinite
- fdOpts: ["-a", "-t", "f"], // Options to pass to `fd`
- shortenThreshold: 30, // Threshold to shorten paths in characters
- },
- math: {
- maxResults: 40, // Actual max results, -1 for infinite
- },
- todo: {
- notify: true,
- },
- wallpaper: {
- maxResults: 20, // Actual max results, -1 for infinite
- showAllEmpty: true, // Show all wallpapers when search is empty
- style: "medium", // One of "compact", "medium", "large"
- },
- disabledActions: ["logout", "shutdown", "reboot", "hibernate"], // Actions to hide, see launcher/actions.tsx for available actions
- },
- notifpopups: {
- maxPopups: -1,
- expire: false,
- agoTime: true, // Whether to show time in ago format, e.g. 10 mins ago, or raw time, e.g. 10:42
- },
- osds: {
- volume: {
- position: Astal.WindowAnchor.RIGHT, // Top = 2, Right = 4, Left = 8, Bottom = 16
- margin: 20,
- hideDelay: 1500,
- showValue: true,
- },
- brightness: {
- position: Astal.WindowAnchor.LEFT, // Top = 2, Right = 4, Left = 8, Bottom = 16
- margin: 20,
- hideDelay: 1500,
- showValue: true,
- },
- lock: {
- spacing: 5,
- caps: {
- hideDelay: 1000,
- },
- num: {
- hideDelay: 1000,
- },
- },
- },
- sidebar: {
- showOnStartup: false,
- modules: {
- headlines: {
- enabled: true,
- },
- },
- },
- navbar: {
- persistent: false, // Whether to show all the time or only on hover
- appearWidth: 10, // The width in pixels of the hover area for the navbar to show up
- showLabels: false, // Whether to show labels for active buttons
- },
- // Services
- math: {
- maxHistory: 100,
- },
- updates: {
- interval: 900000,
- },
- weather: {
- interval: 600000,
- apiKey: "", // An API key from https://weatherapi.com for accessing weather data
- location: "", // Location as a string or empty to autodetect
- imperial: false,
- },
- cpu: {
- interval: 2000,
- },
- gpu: {
- interval: 2000,
- },
- memory: {
- interval: 5000,
- },
- storage: {
- interval: 5000,
- },
- wallpapers: {
- paths: [
- {
- recursive: true, // Whether to search recursively
- path: "~/Pictures/Wallpapers", // Path to search
- threshold: 0.8, // The threshold to filter wallpapers by size (e.g. 0.8 means wallpaper must be at least 80% of the screen size), 0 to disable
- },
- ],
- },
- calendar: {
- webcals: [] as string[], // An array of urls to ICS files which you can curl
- upcomingDays: 7, // Number of days which count as upcoming
- notify: true,
- },
- thumbnailer: {
- maxAttempts: 5,
- timeBetweenAttempts: 300,
- defaults: {
- width: 100,
- height: 100,
- exact: true,
- },
- },
- news: {
- apiKey: "", // An API key from https://newsdata.io for accessing news
- countries: ["current"], // A list of country codes or "current" for the current location
- categories: ["business", "top", "technology", "world"], // A list of news categories to filter by
- languages: ["en"], // A list of languages codes to filter by
- domains: [] as string[], // A list of news domains to pull from, see https://newsdata.io/news-sources for available domains
- excludeDomains: ["news.google.com"], // A list of news domains to exclude, e.g. bbc.co.uk
- timezone: "", // A timezone to filter by, e.g. "America/New_York", see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- pages: 3, // Number of pages to pull (each page is 10 articles)
- },
-};
diff --git a/src/config/funcs.ts b/src/config/funcs.ts
deleted file mode 100644
index 77ee8dd..0000000
--- a/src/config/funcs.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { notify } from "@/utils/system";
-import { Gio, GLib, monitorFile, readFileAsync, Variable, writeFileAsync } from "astal";
-import config from ".";
-import { loadStyleAsync } from "../../app";
-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]> };
-
-const CONFIG = `${GLib.get_user_config_dir()}/caelestia/shell.json`;
-
-const warn = (msg: string) => {
- console.warn(`[CONFIG] ${msg}`);
- if (config.config.notifyOnError.get())
- notify({
- summary: "Invalid config",
- body: msg,
- icon: "dialog-error-symbolic",
- urgency: "critical",
- });
-};
-
-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)) {
- 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);
- if (Array.isArray(type)) {
- v.splice(0, v.length, ...v.filter((item, i) => isCorrectType(item, type, `${path}[${i}]`)));
- } else {
- const valid = v.filter((item, i) =>
- Object.entries(type).every(([k, t]) => {
- if (!item.hasOwnProperty(k)) {
- 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) {
- 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
- warn(`Invalid type for ${path}: ${typeof v} != ${type}`);
- return false;
- }
- } else if (typeof v !== type) {
- // Value is not correct type
- 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 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, `${path}${k}.`);
- else if (!isCorrectType(bv, types[path + k], path + k)) merged[k] = v;
- } else merged[k] = v;
- }
- return merged as any;
-};
-
-export 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)) {
- if (to.hasOwnProperty(k)) {
- if (isObject(v)) updateSection(v, to[k], `${path}${k}.`);
- else if (!Array.isArray(v) || JSON.stringify(to[k].get()) !== JSON.stringify(v)) to[k].set(v);
- } else warn(`Unknown config key: ${path}${k}`);
- }
-};
-
-export const updateConfig = async () => {
- if (GLib.file_test(CONFIG, GLib.FileTest.EXISTS))
- updateSection(deepMerge(defaults, JSON.parse(await readFileAsync(CONFIG))), config);
- else updateSection(defaults, config);
- await loadStyleAsync();
- console.log("[LOG] Config updated");
-};
-
-export const initConfig = async () => {
- monitorFile(CONFIG, (_, e) => {
- if (e === Gio.FileMonitorEvent.CHANGES_DONE_HINT || e === Gio.FileMonitorEvent.DELETED)
- updateConfig().catch(warn);
- });
- await updateConfig().catch(warn);
-};
-
-export const setConfig = async (path: string, value: any) => {
- const conf = JSON.parse(await readFileAsync(CONFIG));
- let obj = conf;
- for (const p of path.split(".").slice(0, -1)) obj = obj[p];
- obj[path.split(".").at(-1)!] = value;
- await writeFileAsync(CONFIG, JSON.stringify(conf, null, 4));
-};
diff --git a/src/config/index.ts b/src/config/index.ts
deleted file mode 100644
index 80b4dc4..0000000
--- a/src/config/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import defaults from "./defaults";
-import { convertSettings } from "./funcs";
-
-const config = convertSettings(defaults);
-
-export const {
- style,
- bar,
- launcher,
- notifpopups,
- osds,
- sidebar,
- navbar,
- math,
- updates,
- weather,
- cpu,
- gpu,
- memory,
- storage,
- wallpapers,
- calendar,
- thumbnailer,
- news,
-} = config;
-export default config;
diff --git a/src/config/literals.ts b/src/config/literals.ts
deleted file mode 100644
index 1908c71..0000000
--- a/src/config/literals.ts
+++ /dev/null
@@ -1,336 +0,0 @@
-export const BAR_MODULES = [
- "osIcon",
- "activeWindow",
- "mediaPlaying",
- "brightnessSpacer",
- "workspaces",
- "volumeSpacer",
- "tray",
- "statusIcons",
- "pkgUpdates",
- "notifCount",
- "battery",
- "dateTime",
- "power",
-];
-
-export const NEWS_COUNTRIES = [
- "af", // Afghanistan
- "al", // Albania
- "dz", // Algeria
- "ad", // Andorra
- "ao", // Angola
- "ar", // Argentina
- "am", // Armenia
- "au", // Australia
- "at", // Austria
- "az", // Azerbaijan
- "bs", // Bahamas
- "bh", // Bahrain
- "bd", // Bangladesh
- "bb", // Barbados
- "by", // Belarus
- "be", // Belgium
- "bz", // Belize
- "bj", // Benin
- "bm", // Bermuda
- "bt", // Bhutan
- "bo", // Bolivia
- "ba", // Bosnia And Herzegovina
- "bw", // Botswana
- "br", // Brazil
- "bn", // Brunei
- "bg", // Bulgaria
- "bf", // Burkina fasco
- "bi", // Burundi
- "kh", // Cambodia
- "cm", // Cameroon
- "ca", // Canada
- "cv", // Cape Verde
- "ky", // Cayman Islands
- "cf", // Central African Republic
- "td", // Chad
- "cl", // Chile
- "cn", // China
- "co", // Colombia
- "km", // Comoros
- "cg", // Congo
- "ck", // Cook islands
- "cr", // Costa Rica
- "hr", // Croatia
- "cu", // Cuba
- "cw", // Curaçao
- "cy", // Cyprus
- "cz", // Czech republic
- "dk", // Denmark
- "dj", // Djibouti
- "dm", // Dominica
- "do", // Dominican republic
- "cd", // DR Congo
- "ec", // Ecuador
- "eg", // Egypt
- "sv", // El Salvador
- "gq", // Equatorial Guinea
- "er", // Eritrea
- "ee", // Estonia
- "sz", // Eswatini
- "et", // Ethiopia
- "fj", // Fiji
- "fi", // Finland
- "fr", // France
- "pf", // French polynesia
- "ga", // Gabon
- "gm", // Gambia
- "ge", // Georgia
- "de", // Germany
- "gh", // Ghana
- "gi", // Gibraltar
- "gr", // Greece
- "gd", // Grenada
- "gt", // Guatemala
- "gn", // Guinea
- "gy", // Guyana
- "ht", // Haiti
- "hn", // Honduras
- "hk", // Hong kong
- "hu", // Hungary
- "is", // Iceland
- "in", // India
- "id", // Indonesia
- "ir", // Iran
- "iq", // Iraq
- "ie", // Ireland
- "il", // Israel
- "it", // Italy
- "ci", // Ivory Coast
- "jm", // Jamaica
- "jp", // Japan
- "je", // Jersey
- "jo", // Jordan
- "kz", // Kazakhstan
- "ke", // Kenya
- "ki", // Kiribati
- "xk", // Kosovo
- "kw", // Kuwait
- "kg", // Kyrgyzstan
- "la", // Laos
- "lv", // Latvia
- "lb", // Lebanon
- "ls", // Lesotho
- "lr", // Liberia
- "ly", // Libya
- "li", // Liechtenstein
- "lt", // Lithuania
- "lu", // Luxembourg
- "mo", // Macau
- "mk", // Macedonia
- "mg", // Madagascar
- "mw", // Malawi
- "my", // Malaysia
- "mv", // Maldives
- "ml", // Mali
- "mt", // Malta
- "mh", // Marshall Islands
- "mr", // Mauritania
- "mu", // Mauritius
- "mx", // Mexico
- "fm", // Micronesia
- "md", // Moldova
- "mc", // Monaco
- "mn", // Mongolia
- "me", // Montenegro
- "ma", // Morocco
- "mz", // Mozambique
- "mm", // Myanmar
- "na", // Namibia
- "nr", // Nauru
- "np", // Nepal
- "nl", // Netherland
- "nc", // New caledonia
- "nz", // New zealand
- "ni", // Nicaragua
- "ne", // Niger
- "ng", // Nigeria
- "kp", // North korea
- "no", // Norway
- "om", // Oman
- "pk", // Pakistan
- "pw", // Palau
- "ps", // Palestine
- "pa", // Panama
- "pg", // Papua New Guinea
- "py", // Paraguay
- "pe", // Peru
- "ph", // Philippines
- "pl", // Poland
- "pt", // Portugal
- "pr", // Puerto rico
- "qa", // Qatar
- "ro", // Romania
- "ru", // Russia
- "rw", // Rwanda
- "lc", // Saint lucia
- "sx", // Saint martin(dutch)
- "ws", // Samoa
- "sm", // San Marino
- "st", // Sao tome and principe
- "sa", // Saudi arabia
- "sn", // Senegal
- "rs", // Serbia
- "sc", // Seychelles
- "sl", // Sierra Leone
- "sg", // Singapore
- "sk", // Slovakia
- "si", // Slovenia
- "sb", // Solomon Islands
- "so", // Somalia
- "za", // South africa
- "kr", // South korea
- "es", // Spain
- "lk", // Sri Lanka
- "sd", // Sudan
- "sr", // Suriname
- "se", // Sweden
- "ch", // Switzerland
- "sy", // Syria
- "tw", // Taiwan
- "tj", // Tajikistan
- "tz", // Tanzania
- "th", // Thailand
- "tl", // Timor-Leste
- "tg", // Togo
- "to", // Tonga
- "tt", // Trinidad and tobago
- "tn", // Tunisia
- "tr", // Turkey
- "tm", // Turkmenistan
- "tv", // Tuvalu
- "ug", // Uganda
- "ua", // Ukraine
- "ae", // United arab emirates
- "gb", // United kingdom
- "us", // United states of america
- "uy", // Uruguay
- "uz", // Uzbekistan
- "vu", // Vanuatu
- "va", // Vatican
- "ve", // Venezuela
- "vi", // Vietnam
- "vg", // Virgin Islands (British)
- "wo", // World
- "ye", // Yemen
- "zm", // Zambia
- "zw", // Zimbabwe
-];
-
-export const NEWS_CATEGORIES = [
- "business",
- "crime",
- "domestic",
- "education",
- "entertainment",
- "environment",
- "food",
- "health",
- "lifestyle",
- "other",
- "politics",
- "science",
- "sports",
- "technology",
- "top",
- "tourism",
- "world",
-];
-
-export const NEWS_LANGUAGES = [
- "af", // Afrikaans
- "sq", // Albanian
- "am", // Amharic
- "ar", // Arabic
- "hy", // Armenian
- "as", // Assamese
- "az", // Azerbaijani
- "bm", // Bambara
- "eu", // Basque
- "be", // Belarusian
- "bn", // Bengali
- "bs", // Bosnian
- "bg", // Bulgarian
- "my", // Burmese
- "ca", // Catalan
- "ckb", // Central Kurdish
- "zh", // Chinese
- "hr", // Croatian
- "cs", // Czech
- "da", // Danish
- "nl", // Dutch
- "en", // English
- "et", // Estonian
- "pi", // Filipino
- "fi", // Finnish
- "fr", // French
- "gl", // Galician
- "ka", // Georgian
- "de", // German
- "el", // Greek
- "gu", // Gujarati
- "ha", // Hausa
- "he", // Hebrew
- "hi", // Hindi
- "hu", // Hungarian
- "is", // Icelandic
- "id", // Indonesian
- "it", // Italian
- "jp", // Japanese
- "kn", // Kannada
- "kz", // Kazakh
- "kh", // Khmer
- "rw", // Kinyarwanda
- "ko", // Korean
- "ku", // Kurdish
- "lv", // Latvian
- "lt", // Lithuanian
- "lb", // Luxembourgish
- "mk", // Macedonian
- "ms", // Malay
- "ml", // Malayalam
- "mt", // Maltese
- "mi", // Maori
- "mr", // Marathi
- "mn", // Mongolian
- "ne", // Nepali
- "no", // Norwegian
- "or", // Oriya
- "ps", // Pashto
- "fa", // Persian
- "pl", // Polish
- "pt", // Portuguese
- "pa", // Punjabi
- "ro", // Romanian
- "ru", // Russian
- "sm", // Samoan
- "sr", // Serbian
- "sn", // Shona
- "sd", // Sindhi
- "si", // Sinhala
- "sk", // Slovak
- "sl", // Slovenian
- "so", // Somali
- "es", // Spanish
- "sw", // Swahili
- "sv", // Swedish
- "tg", // Tajik
- "ta", // Tamil
- "te", // Telugu
- "th", // Thai
- "zht", // Traditional chinese
- "tr", // Turkish
- "tk", // Turkmen
- "uk", // Ukrainian
- "ur", // Urdu
- "uz", // Uzbek
- "vi", // Vietnamese
- "cy", // Welsh
- "zu", // Zulu
-];
diff --git a/src/config/types.ts b/src/config/types.ts
deleted file mode 100644
index c8fb9b4..0000000
--- a/src/config/types.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { BAR_MODULES, NEWS_CATEGORIES, NEWS_COUNTRIES, NEWS_LANGUAGES } from "./literals";
-
-const BOOL = "boolean";
-const STR = "string";
-const NUM = "number";
-const ARR = (type: string | string[]) => `array of ${typeof type === "string" ? type : JSON.stringify(type)}`;
-const OBJ_ARR = (shape: object) => ARR(JSON.stringify(shape));
-
-export default {
- "style.transparency": ["off", "low", "normal", "high"],
- "style.borders": BOOL,
- "style.vibrant": BOOL,
- "config.notifyOnError": BOOL,
- // Bar
- "bar.vertical": BOOL,
- "bar.style": ["gaps", "panel", "embedded"],
- "bar.layout.type": ["centerbox", "flowbox"],
- "bar.layout.centerbox.start": ARR(BAR_MODULES),
- "bar.layout.centerbox.center": ARR(BAR_MODULES),
- "bar.layout.centerbox.end": ARR(BAR_MODULES),
- "bar.layout.flowbox": ARR(BAR_MODULES),
- "bar.modules.workspaces.shown": NUM,
- "bar.modules.workspaces.showLabels": BOOL,
- "bar.modules.workspaces.labels": ARR(STR),
- "bar.modules.workspaces.xalign": NUM,
- "bar.modules.workspaces.showWindows": BOOL,
- "bar.modules.dateTime.format": STR,
- "bar.modules.dateTime.detailedFormat": STR,
- // Launcher
- "launcher.style": ["lines", "round"],
- "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.maxResults": NUM,
- "launcher.wallpaper.showAllEmpty": 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,
- // Sidebar
- "sidebar.showOnStartup": BOOL,
- "sidebar.modules.headlines.enabled": BOOL,
- // Navbar
- "navbar.persistent": BOOL,
- "navbar.appearWidth": NUM,
- "navbar.showLabels": BOOL,
- // Services
- "math.maxHistory": NUM,
- "updates.interval": NUM,
- "weather.interval": NUM,
- "weather.apiKey": 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, threshold: NUM }),
- "calendar.webcals": ARR(STR),
- "calendar.upcomingDays": NUM,
- "calendar.notify": BOOL,
- "thumbnailer.maxAttempts": NUM,
- "thumbnailer.timeBetweenAttempts": NUM,
- "thumbnailer.defaults.width": NUM,
- "thumbnailer.defaults.height": NUM,
- "thumbnailer.defaults.exact": BOOL,
- "news.apiKey": STR,
- "news.countries": ARR(NEWS_COUNTRIES),
- "news.categories": ARR(NEWS_CATEGORIES),
- "news.languages": ARR(NEWS_LANGUAGES),
- "news.domains": ARR(STR),
- "news.excludeDomains": ARR(STR),
- "news.timezone": STR,
- "news.pages": NUM,
-} as { [k: string]: string | string[] | number[] };
diff --git a/src/modules/bar.tsx b/src/modules/bar.tsx
deleted file mode 100644
index c131029..0000000
--- a/src/modules/bar.tsx
+++ /dev/null
@@ -1,703 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Players from "@/services/players";
-import Updates from "@/services/updates";
-import { getAppCategoryIcon } from "@/utils/icons";
-import { bindCurrentTime, osIcon } from "@/utils/system";
-import type { AstalWidget } from "@/utils/types";
-import { setupCustomTooltip } from "@/utils/widgets";
-import ScreenCorner from "@/widgets/screencorner";
-import { execAsync, GLib, Variable } from "astal";
-import { bind, kebabify } from "astal/binding";
-import { App, Astal, Gtk, Widget } from "astal/gtk3";
-import { bar as config } from "config";
-import AstalBattery from "gi://AstalBattery";
-import AstalBluetooth from "gi://AstalBluetooth";
-import AstalHyprland from "gi://AstalHyprland";
-import AstalNetwork from "gi://AstalNetwork";
-import AstalNotifd from "gi://AstalNotifd";
-import AstalTray from "gi://AstalTray";
-import AstalWp from "gi://AstalWp";
-import { switchPane } from "./sidebar";
-
-interface ClassNameProps {
- beforeSpacer: boolean;
- afterSpacer: boolean;
- first: boolean;
- last: boolean;
-}
-
-interface ModuleProps extends ClassNameProps {
- monitor: Monitor;
-}
-
-const hyprland = AstalHyprland.get_default();
-
-const getBatteryIcon = (perc: number) => {
- if (perc < 0.1) return "󰁺";
- if (perc < 0.2) return "󰁻";
- if (perc < 0.3) return "󰁼";
- if (perc < 0.4) return "󰁽";
- if (perc < 0.5) return "󰁾";
- if (perc < 0.6) return "󰁿";
- if (perc < 0.7) return "󰂀";
- if (perc < 0.8) return "󰂁";
- if (perc < 0.9) return "󰂂";
- return "󰁹";
-};
-
-const formatSeconds = (sec: number) => {
- if (sec >= 3600) {
- const hours = Math.floor(sec / 3600);
- let str = `${hours} hour${hours === 1 ? "" : "s"}`;
- const mins = Math.floor((sec % 3600) / 60);
- if (mins > 0) str += ` ${mins} minute${mins === 1 ? "" : "s"}`;
- return str;
- } else if (sec >= 60) {
- const mins = Math.floor(sec / 60);
- return `${mins} minute${mins === 1 ? "" : "s"}`;
- } else return `${sec} second${sec === 1 ? "" : "s"}`;
-};
-
-const hookFocusedClientProp = (
- self: AstalWidget,
- prop: keyof AstalHyprland.Client,
- callback: (c: AstalHyprland.Client | null) => void
-) => {
- let id: number | null = null;
- let lastClient: AstalHyprland.Client | null = null;
- self.hook(hyprland, "notify::focused-client", () => {
- if (id) lastClient?.disconnect(id);
- lastClient = hyprland.focusedClient; // Can be null
- id = lastClient?.connect(`notify::${kebabify(prop)}`, () => callback(lastClient));
- callback(lastClient);
- });
- self.connect("destroy", () => id && lastClient?.disconnect(id));
- callback(lastClient);
-};
-
-const getClassName = ({ beforeSpacer, afterSpacer, first, last }: ClassNameProps) =>
- `${beforeSpacer ? "before-spacer" : ""} ${afterSpacer ? "after-spacer" : ""}` +
- ` ${first ? "first" : ""} ${last ? "last" : ""}`;
-
-const getModule = (module: string) => {
- module = module.toLowerCase();
- if (module === "osicon") return OSIcon;
- if (module === "activewindow") return ActiveWindow;
- if (module === "mediaplaying") return MediaPlaying;
- if (module === "workspaces") return Workspaces;
- if (module === "tray") return Tray;
- if (module === "statusicons") return StatusIcons;
- if (module === "pkgupdates") return PkgUpdates;
- if (module === "notifcount") return NotifCount;
- if (module === "battery") return Battery;
- if (module === "datetime") return DateTime;
- if (module === "power") return Power;
- if (module === "brightnessspacer") return BrightnessSpacer;
- if (module === "volumespacer") return VolumeSpacer;
- return () => null;
-};
-
-const isSpacer = (module?: string) => module?.toLowerCase().endsWith("spacer") ?? false;
-
-const OSIcon = ({ monitor, ...props }: ModuleProps) => (
- <button
- className={`module os-icon ${getClassName(props)}`}
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "dashboard")}
- >
- {osIcon}
- </button>
-);
-
-const ActiveWindow = ({ monitor, ...props }: ModuleProps) => (
- <box
- vertical={bind(config.vertical)}
- className={`module active-window ${getClassName(props)}`}
- setup={self => {
- const title = Variable("");
- const updateTooltip = (c: AstalHyprland.Client | null) =>
- title.set(c?.class && c?.title ? `${c.class}: ${c.title}` : "");
- hookFocusedClientProp(self, "class", updateTooltip);
- hookFocusedClientProp(self, "title", updateTooltip);
- updateTooltip(hyprland.focusedClient);
-
- const window = setupCustomTooltip(self, bind(title));
- if (window) {
- self.hook(title, (_, v) => !v && window.hide());
- self.hook(window, "map", () => !title.get() && window.hide());
- }
- }}
- >
- <label
- className="icon"
- setup={self =>
- hookFocusedClientProp(self, "class", c => {
- self.label = c?.class ? getAppCategoryIcon(c.class) : "desktop_windows";
- })
- }
- />
- <label
- truncate
- angle={bind(config.vertical).as(v => (v ? 270 : 0))}
- setup={self => {
- const update = () =>
- (self.label = hyprland.focusedClient?.title ? hyprland.focusedClient.title : "Desktop");
- hookFocusedClientProp(self, "title", update);
- self.hook(config.vertical, update);
- }}
- />
- </box>
-);
-
-const MediaPlaying = ({ monitor, ...props }: ModuleProps) => {
- const players = Players.get_default();
- const getLabel = (fallback = "") =>
- players.lastPlayer ? `${players.lastPlayer.title} - ${players.lastPlayer.artist}` : fallback;
- return (
- <button
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "audio");
- else if (event.button === Astal.MouseButton.SECONDARY) players.lastPlayer?.play_pause();
- else if (event.button === Astal.MouseButton.MIDDLE) players.lastPlayer?.raise();
- }}
- setup={self => {
- const label = Variable(getLabel());
- players.hookLastPlayer(self, ["notify::title", "notify::artist"], () => label.set(getLabel()));
- setupCustomTooltip(self, bind(label));
- }}
- >
- <box vertical={bind(config.vertical)} className={`module media-playing ${getClassName(props)}`}>
- <icon
- setup={self =>
- players.hookLastPlayer(self, "notify::identity", () => {
- const icon = `caelestia-${players.lastPlayer?.identity
- .toLowerCase()
- .replaceAll(" ", "-")}-symbolic`;
- self.icon = players.lastPlayer
- ? Astal.Icon.lookup_icon(icon)
- ? icon
- : "caelestia-media-generic-symbolic"
- : "caelestia-media-none-symbolic";
- })
- }
- />
- <label
- truncate
- angle={bind(config.vertical).as(v => (v ? 270 : 0))}
- setup={self => {
- const update = () => (self.label = getLabel("No media"));
- players.hookLastPlayer(self, ["notify::title", "notify::artist"], update);
- self.hook(config.vertical, update);
- }}
- />
- </box>
- </button>
- );
-};
-
-const Workspace = ({ idx }: { idx: number }) => {
- const wsId = Variable.derive([bind(hyprland, "focusedWorkspace"), config.modules.workspaces.shown], (f, s) =>
- f ? Math.floor((f.id - 1) / s) * s + idx : idx
- );
-
- const label = (
- <label
- css={bind(config.modules.workspaces.xalign).as(a => `margin-left: ${a}px; margin-right: ${-a}px;`)}
- label={bind(config.modules.workspaces.labels).as(l => l[idx - 1] ?? String(idx))}
- />
- );
-
- return (
- <button
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- onClicked={() => hyprland.dispatch("workspace", String(wsId.get()))}
- setup={self => {
- const updateOccupied = () => {
- const occupied = hyprland.clients.some(c => c.workspace?.id === wsId.get());
- self.toggleClassName("occupied", occupied);
- };
- const updateFocused = () => {
- self.toggleClassName("focused", hyprland.focusedWorkspace?.id === wsId.get());
- updateOccupied();
- };
-
- self.hook(hyprland, "client-added", updateOccupied);
- self.hook(hyprland, "client-moved", updateOccupied);
- self.hook(hyprland, "client-removed", updateOccupied);
- self.hook(hyprland, "notify::focused-workspace", updateFocused);
- updateFocused();
- }}
- onDestroy={() => wsId.drop()}
- >
- <box
- visible={bind(config.modules.workspaces.showLabels)}
- vertical={bind(config.vertical)}
- setup={self => {
- const update = () => {
- if (config.modules.workspaces.showWindows.get()) {
- const clients = hyprland.clients.filter(c => c.workspace?.id === wsId.get());
- self.children = [
- label,
- ...clients.map(c => (
- <label className="icon" label={bind(c, "class").as(getAppCategoryIcon)} />
- )),
- ];
- } else self.children = [label];
- };
- self.hook(wsId, update);
- self.hook(hyprland, "client-added", update);
- self.hook(hyprland, "client-moved", update);
- self.hook(hyprland, "client-removed", update);
- update();
- }}
- />
- </button>
- );
-};
-
-const Workspaces = ({ monitor, ...props }: ModuleProps) => {
- const className = Variable.derive(
- [config.modules.workspaces.shown, config.modules.workspaces.showLabels],
- (s, l) => `module workspaces ${s % 2 === 0 ? "even" : "odd"} ${l ? "labels-shown" : ""} ${getClassName(props)}`
- );
-
- return (
- <eventbox
- onScroll={(_, event) => {
- const activeWs = hyprland.focusedClient?.workspace.name;
- if (activeWs?.startsWith("special:")) hyprland.dispatch("togglespecialworkspace", activeWs.slice(8));
- else if (event.delta_y > 0 || hyprland.focusedWorkspace?.id > 1)
- hyprland.dispatch("workspace", (event.delta_y < 0 ? "-" : "+") + 1);
- }}
- >
- <box vertical={bind(config.vertical)} className={bind(className)} onDestroy={() => className.drop()}>
- {bind(config.modules.workspaces.shown).as(
- n => Array.from({ length: n }).map((_, idx) => <Workspace idx={idx + 1} />) // Start from 1
- )}
- </box>
- </eventbox>
- );
-};
-
-const TrayItem = (item: AstalTray.TrayItem) => (
- <menubutton
- onButtonPressEvent={(_, event) => event.get_button()[1] === Astal.MouseButton.SECONDARY && item.activate(0, 0)}
- usePopover={false}
- direction={bind(config.vertical).as(v => (v ? Gtk.ArrowType.RIGHT : Gtk.ArrowType.DOWN))}
- menuModel={bind(item, "menuModel")}
- actionGroup={bind(item, "actionGroup").as(a => ["dbusmenu", a])}
- setup={self => setupCustomTooltip(self, bind(item, "tooltipMarkup"))}
- >
- <icon halign={Gtk.Align.CENTER} gicon={bind(item, "gicon")} />
- </menubutton>
-);
-
-const Tray = ({ monitor, ...props }: ModuleProps) => (
- <box
- visible={bind(AstalTray.get_default(), "items").as(i => i.length > 0)}
- vertical={bind(config.vertical)}
- className={`module tray ${getClassName(props)}`}
- >
- {bind(AstalTray.get_default(), "items").as(i => i.map(TrayItem))}
- </box>
-);
-
-const Network = ({ monitor }: { monitor: Monitor }) => (
- <button
- onClick={(_, event) => {
- const network = AstalNetwork.get_default();
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity");
- else if (event.button === Astal.MouseButton.SECONDARY) network.wifi.enabled = !network.wifi.enabled;
- else if (event.button === Astal.MouseButton.MIDDLE) {
- if (GLib.find_program_in_path("gnome-control-center"))
- execAsync("app2unit -- gnome-control-center wifi").catch(console.error);
- else {
- network.wifi.scan();
- execAsync(
- "app2unit -- foot -T nmtui -- fish -c 'sleep .1; set -e COLORTERM; TERM=xterm-old nmtui connect'"
- ).catch(() => {}); // Ignore errors
- }
- }
- }}
- setup={self => {
- const network = AstalNetwork.get_default();
- const tooltipText = Variable("");
- const update = () => {
- if (network.primary === AstalNetwork.Primary.WIFI) {
- if (network.wifi.internet === AstalNetwork.Internet.CONNECTED)
- tooltipText.set(`${network.wifi.ssid} | Strength: ${network.wifi.strength}/100`);
- else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING)
- tooltipText.set(`Connecting to ${network.wifi.ssid}`);
- else tooltipText.set("Disconnected");
- } else if (network.primary === AstalNetwork.Primary.WIRED) {
- if (network.wired.internet === AstalNetwork.Internet.CONNECTED)
- tooltipText.set(`Speed: ${network.wired.speed}`);
- else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) tooltipText.set("Connecting");
- else tooltipText.set("Disconnected");
- } else {
- tooltipText.set("Unknown");
- }
- };
- self.hook(network, "notify::primary", update);
- self.hook(network.wifi, "notify::internet", update);
- self.hook(network.wifi, "notify::ssid", update);
- self.hook(network.wifi, "notify::strength", update);
- if (network.wired) {
- self.hook(network.wired, "notify::internet", update);
- self.hook(network.wired, "notify::speed", update);
- }
- update();
- setupCustomTooltip(self, bind(tooltipText));
- }}
- >
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- shown={bind(AstalNetwork.get_default(), "primary").as(p =>
- p === AstalNetwork.Primary.WIFI ? "wifi" : "wired"
- )}
- >
- <stack
- name="wifi"
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- setup={self => {
- const network = AstalNetwork.get_default();
- const update = () => {
- if (network.wifi.internet === AstalNetwork.Internet.CONNECTED)
- self.shown = String(Math.ceil(network.wifi.strength / 25));
- else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting";
- else self.shown = "disconnected";
- };
- self.hook(network.wifi, "notify::internet", update);
- self.hook(network.wifi, "notify::strength", update);
- update();
- }}
- >
- <label className="icon" label="wifi_off" name="disconnected" />
- <label className="icon" label="settings_ethernet" name="connecting" />
- <label className="icon" label="signal_wifi_0_bar" name="0" />
- <label className="icon" label="network_wifi_1_bar" name="1" />
- <label className="icon" label="network_wifi_2_bar" name="2" />
- <label className="icon" label="network_wifi_3_bar" name="3" />
- <label className="icon" label="signal_wifi_4_bar" name="4" />
- </stack>
- <stack
- name="wired"
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- setup={self => {
- const network = AstalNetwork.get_default();
- const update = () => {
- if (network.primary !== AstalNetwork.Primary.WIRED) return;
-
- if (network.wired.internet === AstalNetwork.Internet.CONNECTED) self.shown = "connected";
- else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting";
- else self.shown = "disconnected";
- };
- self.hook(network, "notify::primary", update);
- if (network.wired) self.hook(network.wired, "notify::internet", update);
- update();
- }}
- >
- <label className="icon" label="wifi_off" name="disconnected" />
- <label className="icon" label="settings_ethernet" name="connecting" />
- <label className="icon" label="lan" name="connected" />
- </stack>
- </stack>
- </button>
-);
-
-const BluetoothDevice = ({ monitor, device }: { monitor: Monitor; device: AstalBluetooth.Device }) => (
- <button
- visible={bind(device, "connected")}
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity");
- else if (event.button === Astal.MouseButton.SECONDARY)
- device.disconnect_device((_, res) => device.disconnect_device_finish(res));
- else if (event.button === Astal.MouseButton.MIDDLE)
- execAsync("app2unit -- blueman-manager").catch(console.error);
- }}
- setup={self => setupCustomTooltip(self, bind(device, "alias"))}
- >
- <icon
- icon={bind(device, "icon").as(i =>
- Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "caelestia-bluetooth-device-symbolic"
- )}
- />
- </button>
-);
-
-const Bluetooth = ({ monitor }: { monitor: Monitor }) => (
- <box vertical={bind(config.vertical)} className="bluetooth">
- <button
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity");
- else if (event.button === Astal.MouseButton.SECONDARY) AstalBluetooth.get_default().toggle();
- else if (event.button === Astal.MouseButton.MIDDLE)
- execAsync("app2unit -- blueman-manager").catch(console.error);
- }}
- setup={self => {
- const bluetooth = AstalBluetooth.get_default();
- const tooltipText = Variable("");
- const update = () => {
- const devices = bluetooth.get_devices().filter(d => d.connected);
- tooltipText.set(
- devices.length > 0
- ? `Connected devices: ${devices.map(d => d.alias).join(", ")}`
- : "No connected devices"
- );
- };
- const hookDevice = (device: AstalBluetooth.Device) => {
- self.hook(device, "notify::connected", update);
- self.hook(device, "notify::alias", update);
- };
- bluetooth.get_devices().forEach(hookDevice);
- self.hook(bluetooth, "device-added", (_, device) => {
- hookDevice(device);
- update();
- });
- update();
- setupCustomTooltip(self, bind(tooltipText));
- }}
- >
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- shown={bind(AstalBluetooth.get_default(), "isPowered").as(p => (p ? "enabled" : "disabled"))}
- >
- <label className="icon" label="bluetooth" name="enabled" />
- <label className="icon" label="bluetooth_disabled" name="disabled" />
- </stack>
- </button>
- {bind(AstalBluetooth.get_default(), "devices").as(d =>
- d.map(d => <BluetoothDevice monitor={monitor} device={d} />)
- )}
- </box>
-);
-
-const StatusIcons = ({ monitor, ...props }: ModuleProps) => (
- <box vertical={bind(config.vertical)} className={`module status-icons ${getClassName(props)}`}>
- <Network monitor={monitor} />
- <Bluetooth monitor={monitor} />
- </box>
-);
-
-const PkgUpdates = ({ monitor, ...props }: ModuleProps) => (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "packages")}
- setup={self =>
- setupCustomTooltip(
- self,
- bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"} available`)
- )
- }
- >
- <box vertical={bind(config.vertical)} className={`module pkg-updates ${getClassName(props)}`}>
- <label className="icon" label="download" />
- <label label={bind(Updates.get_default(), "numUpdates").as(String)} />
- </box>
- </button>
-);
-
-const NotifCount = ({ monitor, ...props }: ModuleProps) => (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "alerts")}
- setup={self =>
- setupCustomTooltip(
- self,
- bind(AstalNotifd.get_default(), "notifications").as(
- n => `${n.length} notification${n.length === 1 ? "" : "s"}`
- )
- )
- }
- >
- <box vertical={bind(config.vertical)} className={`module notif-count ${getClassName(props)}`}>
- <label
- className="icon"
- label={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "notifications_off" : "info"))}
- />
- <revealer
- transitionType={bind(config.vertical).as(v =>
- v ? Gtk.RevealerTransitionType.SLIDE_DOWN : Gtk.RevealerTransitionType.SLIDE_RIGHT
- )}
- transitionDuration={120}
- revealChild={bind(AstalNotifd.get_default(), "dontDisturb").as(d => !d)}
- >
- <label label={bind(AstalNotifd.get_default(), "notifications").as(n => String(n.length))} />
- </revealer>
- </box>
- </button>
-);
-
-const Battery = ({ monitor, ...props }: ModuleProps) => {
- const className = Variable.derive(
- [bind(AstalBattery.get_default(), "percentage"), bind(AstalBattery.get_default(), "charging")],
- (p, c) => `module battery ${c ? "charging" : p < 0.2 ? "low" : ""} ${getClassName(props)}`
- );
- const tooltip = Variable.derive(
- [bind(AstalBattery.get_default(), "timeToEmpty"), bind(AstalBattery.get_default(), "timeToFull")],
- (e, f) => (f > 0 ? `${formatSeconds(f)} until full` : `${formatSeconds(e)} remaining`)
- );
-
- return (
- <box
- visible={bind(AstalBattery.get_default(), "isBattery")}
- vertical={bind(config.vertical)}
- className={bind(className)}
- setup={self => setupCustomTooltip(self, bind(tooltip))}
- onDestroy={() => {
- className.drop();
- tooltip.drop();
- }}
- >
- <label className="icon" label={bind(AstalBattery.get_default(), "percentage").as(getBatteryIcon)} />
- <label label={bind(AstalBattery.get_default(), "percentage").as(p => `${Math.round(p * 100)}%`)} />
- </box>
- );
-};
-
-const DateTimeHoriz = (props: ClassNameProps) => (
- <box className={`module date-time ${getClassName(props)}`}>
- <label className="icon" label="calendar_month" />
- <label
- setup={self => {
- const time = bindCurrentTime(bind(config.modules.dateTime.format), undefined, self);
- self.label = time.get();
- self.hook(time, (_, t) => (self.label = t));
- }}
- />
- </box>
-);
-
-const DateTimeVertical = (props: ClassNameProps) => (
- <box vertical className={`module date-time ${getClassName(props)}`}>
- <label className="icon" label="calendar_month" />
- <label label={bindCurrentTime("%H")} />
- <label label={bindCurrentTime("%M")} />
- </box>
-);
-
-const DateTime = ({ monitor, ...props }: ModuleProps) => (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "time")}
- setup={self =>
- setupCustomTooltip(self, bindCurrentTime(bind(config.modules.dateTime.detailedFormat), undefined, self))
- }
- >
- {bind(config.vertical).as(v => (v ? <DateTimeVertical {...props} /> : <DateTimeHoriz {...props} />))}
- </button>
-);
-
-const Power = ({ monitor, ...props }: ModuleProps) => (
- <button
- className={`module power ${getClassName(props)}`}
- label="power_settings_new"
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && App.toggle_window("session")}
- />
-);
-
-const Spacer = ({ onScroll }: { onScroll: (self: Widget.EventBox, event: Astal.ScrollEvent) => void }) => (
- <eventbox onScroll={onScroll}>
- <box vertical={bind(config.vertical)}>
- <ScreenCorner place="topleft" />
- <box expand />
- <ScreenCorner place={bind(config.vertical).as(v => (v ? "bottomleft" : "topright"))} />
- </box>
- </eventbox>
-);
-
-const BrightnessSpacer = ({ monitor }: { monitor: Monitor }) => (
- <Spacer onScroll={(_, event) => (event.delta_y > 0 ? (monitor.brightness -= 0.1) : (monitor.brightness += 0.1))} />
-);
-
-const VolumeSpacer = () => (
- <Spacer
- onScroll={(_, event) => {
- const speaker = AstalWp.get_default()?.audio.defaultSpeaker;
- if (!speaker) return console.error("Unable to connect to WirePlumber.");
- speaker.mute = false;
- if (event.delta_y > 0) speaker.volume -= 0.1;
- else speaker.volume += 0.1;
- }}
- />
-);
-
-const Bar = ({ monitor, layout }: { monitor: Monitor; layout: string }) => {
- const className = Variable.derive(
- [bind(config.vertical), bind(config.style)],
- (v, s) => `bar ${v ? "vertical" : " horizontal"} ${s}`
- );
- const modules =
- layout === "centerbox"
- ? Variable.derive(Object.values(config.layout.centerbox))
- : bind(config.layout.flowbox).as(m => [m]);
-
- const Layout = layout === "centerbox" ? Widget.CenterBox : Widget.Box;
- return (
- <Layout
- vertical={bind(config.vertical)}
- className={bind(className)}
- onDestroy={() => {
- className.drop();
- if (modules instanceof Variable) modules.drop();
- }}
- >
- {bind(modules).as(modules =>
- modules.map((m, i) => (
- <box vertical={bind(config.vertical)}>
- {m.map((n, j) => {
- let beforeSpacer = false;
- if (j < m.length - 1) beforeSpacer = isSpacer(m[j + 1]);
- else if (i < modules.length - 1) beforeSpacer = isSpacer(modules[i + 1][0]);
- let afterSpacer = false;
- if (j > 0) afterSpacer = isSpacer(m[j - 1]);
- else if (i > 0) afterSpacer = isSpacer(modules[i - 1].at(-1));
- const M = getModule(n);
- return (
- <M
- monitor={monitor}
- beforeSpacer={beforeSpacer}
- afterSpacer={afterSpacer}
- first={i === 0 && j === 0}
- last={i === modules.length - 1 && j === m.length - 1}
- />
- );
- })}
- </box>
- ))
- )}
- </Layout>
- );
-};
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-bar"
- monitor={monitor.id}
- anchor={bind(config.vertical).as(
- v =>
- Astal.WindowAnchor.TOP |
- Astal.WindowAnchor.LEFT |
- (v ? Astal.WindowAnchor.BOTTOM : Astal.WindowAnchor.RIGHT)
- )}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- >
- <overlay
- passThrough
- overlays={[
- <ScreenCorner visible={bind(config.style).as(s => s !== "embedded")} place="topleft" />,
- <ScreenCorner
- visible={bind(config.style).as(s => s !== "embedded")}
- halign={bind(config.vertical).as(v => (v ? undefined : Gtk.Align.END))}
- valign={bind(config.vertical).as(v => (v ? Gtk.Align.END : undefined))}
- place={bind(config.vertical).as(v => (v ? "bottomleft" : "topright"))}
- />,
- ]}
- >
- {bind(config.layout.type).as(l => (
- <Bar monitor={monitor} layout={l} />
- ))}
- </overlay>
- </window>
-);
diff --git a/src/modules/launcher/actions.tsx b/src/modules/launcher/actions.tsx
deleted file mode 100644
index 40d37b5..0000000
--- a/src/modules/launcher/actions.tsx
+++ /dev/null
@@ -1,522 +0,0 @@
-import { Apps } from "@/services/apps";
-import Palette from "@/services/palette";
-import Schemes, { type Colours } from "@/services/schemes";
-import Wallpapers, { type ICategory, type IWallpaper } from "@/services/wallpapers";
-import { basename } from "@/utils/strings";
-import { notify } from "@/utils/system";
-import { setupCustomTooltip, type FlowBox } from "@/utils/widgets";
-import { bind, execAsync, GLib, readFile, register, type Variable } from "astal";
-import { Gtk, Widget } from "astal/gtk3";
-import { launcher as config } from "config";
-import { setConfig } from "config/funcs";
-import fuzzysort from "fuzzysort";
-import AstalHyprland from "gi://AstalHyprland";
-import { close, ContentBox, type LauncherContent, type Mode } from "./util";
-
-interface IAction {
- icon: string;
- name: string;
- description: string;
- action: (...args: string[]) => void;
- available?: () => boolean;
-}
-
-interface ActionMap {
- [k: string]: IAction;
-}
-
-const variantActions = {
- vibrant: {
- icon: "sentiment_very_dissatisfied",
- name: "Vibrant",
- description: "A high chroma palette. The primary palette's chroma is at maximum.",
- },
- tonalspot: {
- icon: "android",
- name: "Tonal Spot",
- description: "Default for Material theme colours. A pastel palette with a low chroma.",
- },
- expressive: {
- icon: "compare_arrows",
- name: "Expressive",
- description:
- "A medium chroma palette. The primary palette's hue is different from the seed colour, for variety.",
- },
- fidelity: {
- icon: "compare",
- name: "Fidelity",
- description: "Matches the seed colour, even if the seed colour is very bright (high chroma).",
- },
- content: {
- icon: "sentiment_calm",
- name: "Content",
- description: "Almost identical to fidelity.",
- },
- fruitsalad: {
- icon: "nutrition",
- name: "Fruit Salad",
- description: "A playful theme - the seed colour's hue does not appear in the theme.",
- },
- rainbow: {
- icon: "looks",
- name: "Rainbow",
- description: "A playful theme - the seed colour's hue does not appear in the theme.",
- },
- neutral: {
- icon: "contrast",
- name: "Neutral",
- description: "Close to grayscale, a hint of chroma.",
- },
- monochrome: {
- icon: "filter_b_and_w",
- name: "Monochrome",
- description: "All colours are grayscale, no chroma.",
- },
-};
-
-const transparencyActions = {
- off: {
- icon: "blur_off",
- name: "Off",
- description: "Completely opaque",
- },
- low: {
- icon: "blur_circular",
- name: "Low",
- description: "Less transparent",
- },
- normal: {
- icon: "blur_linear",
- name: "Normal",
- description: "Somewhat transparent",
- },
- high: {
- icon: "blur_on",
- name: "High",
- description: "Extremely transparent",
- },
-};
-
-const autocomplete = (entry: Widget.Entry, action: string) => {
- entry.set_text(`${config.actionPrefix.get()}${action} `);
- entry.set_position(-1);
-};
-
-const actions = (mode: Variable<Mode>, entry: Widget.Entry): ActionMap => ({
- apps: {
- icon: "apps",
- name: "Apps",
- description: "Search for apps",
- action: () => {
- mode.set("apps");
- entry.set_text("");
- },
- },
- files: {
- icon: "folder",
- name: "Files",
- description: "Search for files",
- action: () => {
- mode.set("files");
- entry.set_text("");
- },
- },
- math: {
- icon: "calculate",
- name: "Math",
- description: "Do math calculations",
- action: () => {
- mode.set("math");
- entry.set_text("");
- },
- },
- light: {
- icon: "light_mode",
- name: "Light",
- description: "Change scheme to light mode",
- action: () => {
- Palette.get_default().switchMode("light");
- close();
- },
- available: () => Palette.get_default().hasMode("light"),
- },
- dark: {
- icon: "dark_mode",
- name: "Dark",
- description: "Change scheme to dark mode",
- action: () => {
- Palette.get_default().switchMode("dark");
- close();
- },
- available: () => Palette.get_default().hasMode("dark"),
- },
- scheme: {
- icon: "palette",
- name: "Scheme",
- description: "Change the current colour scheme",
- action: () => autocomplete(entry, "scheme"),
- },
- variant: {
- icon: "colors",
- name: "Variant",
- description: "Change the current scheme variant",
- action: () => autocomplete(entry, "variant"),
- available: () => Palette.get_default().scheme === "dynamic",
- },
- wallpaper: {
- icon: "image",
- name: "Wallpaper",
- description: "Change the current wallpaper",
- action: () => autocomplete(entry, "wallpaper"),
- },
- transparency: {
- icon: "opacity",
- name: "Transparency",
- description: "Change shell transparency",
- action: () => autocomplete(entry, "transparency"),
- },
- todo: {
- icon: "checklist",
- name: "Todo",
- description: "Create a todo in Todoist",
- action: (...args) => {
- // If no args, autocomplete cmd
- if (args.length === 0) return autocomplete(entry, "todo");
-
- // If tod not configured, notify
- let token = null;
- try {
- token = JSON.parse(readFile(GLib.get_user_config_dir() + "/tod.cfg")).token;
- } catch {} // Ignore
- if (!token) {
- notify({
- summary: "Tod not configured",
- body: "You need to configure tod first. Run any tod command to do this.",
- icon: "dialog-warning-symbolic",
- urgency: "critical",
- });
- } else {
- // Create todo and notify if configured
- execAsync(`tod t q -c ${args.join(" ")}`).catch(console.error);
- if (config.todo.notify.get())
- notify({
- summary: "Todo created",
- body: `Created todo with content: ${args.join(" ")}`,
- icon: "view-list-bullet-symbolic",
- urgency: "low",
- transient: true,
- actions: {
- "Copy content": () => execAsync(`wl-copy -- ${args.join(" ")}`).catch(console.error),
- View: () => {
- const client = AstalHyprland.get_default().clients.find(c => c.class === "Todoist");
- if (client) client.focus();
- else execAsync("app2unit -- todoist").catch(console.error);
- },
- },
- });
- }
-
- close();
- },
- available: () => !!GLib.find_program_in_path("tod"),
- },
- reload: {
- icon: "refresh",
- name: "Reload",
- description: "Reload app list",
- action: () => {
- Apps.reload();
- entry.set_text("");
- },
- },
- lock: {
- icon: "lock",
- name: "Lock",
- description: "Lock the current session",
- action: () => {
- execAsync("loginctl lock-session").catch(console.error);
- close();
- },
- },
- logout: {
- icon: "logout",
- name: "Logout",
- description: "End the current session",
- action: () => {
- execAsync("uwsm stop").catch(console.error);
- close();
- },
- },
- sleep: {
- icon: "bedtime",
- name: "Sleep",
- description: "Suspend then hibernate",
- action: () => {
- execAsync("systemctl suspend-then-hibernate").catch(console.error);
- close();
- },
- },
- reboot: {
- icon: "cached",
- name: "Reboot",
- description: "Restart the machine",
- action: () => {
- execAsync("systemctl reboot").catch(console.error);
- close();
- },
- },
- hibernate: {
- icon: "downloading",
- name: "Hibernate",
- description: "Suspend to RAM",
- action: () => {
- execAsync("systemctl hibernate").catch(console.error);
- close();
- },
- },
- shutdown: {
- icon: "power_settings_new",
- name: "Shutdown",
- description: "Suspend to disk",
- action: () => {
- execAsync("systemctl poweroff").catch(console.error);
- close();
- },
- },
-});
-
-const Action = ({ args, icon, name, description, action }: IAction & { args: string[] }) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => action(...args)}
- setup={self => setupCustomTooltip(self, description)}
- >
- <box>
- <label className="icon" label={icon} />
- <box vertical className="has-sublabel">
- <label truncate xalign={0} label={name} />
- <label truncate xalign={0} label={description} className="sublabel" />
- </box>
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const Swatch = ({ colour }: { colour: string }) => <box className="swatch" css={"background-color: " + colour + ";"} />;
-
-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.primary + ";"} />
- </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>
- </button>
- </Gtk.FlowBoxChild>
- );
-};
-
-const Variant = ({ name }: { name: keyof typeof variantActions }) => (
- <Action
- {...variantActions[name]}
- args={[]}
- action={() => {
- execAsync(`caelestia variant ${name}`).catch(console.error);
- close();
- }}
- />
-);
-
-const Wallpaper = ({ path, thumbnails }: IWallpaper) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result wallpaper-container"
- 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={bind(config.wallpaper.style).as(
- s => "background-image: url('" + thumbnails[s as keyof typeof thumbnails] + "');"
- )}
- />
- <label truncate label={basename(path)} />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const CategoryThumbnail = ({ style, wallpapers }: { style: string; wallpapers: IWallpaper[] }) => (
- <box className="thumbnail">
- {wallpapers.slice(0, 3).map(w => (
- <box hexpand css={"background-image: url('" + w.thumbnails[style as keyof typeof w.thumbnails] + "');"} />
- ))}
- </box>
-);
-
-const Category = ({ path, wallpapers }: ICategory) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result wallpaper-container"
- cursor="pointer"
- onClicked={() => {
- execAsync(`caelestia wallpaper -d ${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()}`}
- >
- {bind(config.wallpaper.style).as(s =>
- s === "compact" ? (
- <box
- className="thumbnail"
- css={"background-image: url('" + wallpapers[0].thumbnails.compact + "');"}
- />
- ) : (
- <CategoryThumbnail style={s} wallpapers={wallpapers} />
- )
- )}
- <label truncate label={basename(path)} />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const Transparency = ({ amount }: { amount: keyof typeof transparencyActions }) => (
- <Action
- {...transparencyActions[amount]}
- args={[]}
- action={() => {
- setConfig("style.transparency", amount).catch(console.error);
- close();
- }}
- />
-);
-
-@register()
-export default class Actions extends Widget.Box implements LauncherContent {
- #map: ActionMap;
- #list: string[];
-
- #content: FlowBox;
-
- constructor(mode: Variable<Mode>, entry: Widget.Entry) {
- super({ name: "actions", className: "actions" });
-
- this.#map = actions(mode, entry);
- this.#list = Object.keys(this.#map);
-
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- this.#content.foreach(c => c.destroy());
- const args = search.split(" ");
- const action = args[0].slice(1).toLowerCase();
-
- if (action === "scheme") {
- const scheme = args[1] ?? "";
- 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)
- .sort();
- 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 === "variant") {
- const list = Object.keys(variantActions);
-
- for (const { target } of fuzzysort.go(args[1], list, { all: true }))
- this.#content.add(<Variant name={target as keyof typeof variantActions} />);
- } else if (action === "wallpaper") {
- if (args[1]?.toLowerCase() === "random") {
- const list = Wallpapers.get_default().categories;
- for (const { obj } of fuzzysort.go(args[2] ?? "", list, { all: true, key: "path" }))
- this.#content.add(<Category {...obj} />);
- } else {
- const list = Wallpapers.get_default().list;
- let limit = undefined;
- if ((args[1] || !config.wallpaper.showAllEmpty.get()) && config.wallpaper.maxResults.get() > 0)
- limit = config.wallpaper.maxResults.get();
-
- for (const { obj } of fuzzysort.go(args[1] ?? "", list, { all: true, key: "path", limit }))
- this.#content.add(<Wallpaper {...obj} />);
- }
- } else if (action === "transparency") {
- const list = Object.keys(transparencyActions);
-
- for (const { target } of fuzzysort.go(args[1], list, { all: true }))
- this.#content.add(<Transparency amount={target as keyof typeof transparencyActions} />);
- } else {
- const list = this.#list.filter(
- a => this.#map[a].available?.() ?? !config.disabledActions.get().includes(a)
- );
- for (const { target } of fuzzysort.go(action, list, { all: true }))
- this.#content.add(<Action {...this.#map[target]} args={args.slice(1)} />);
- }
- }
-
- handleActivate(search: string): void {
- const args = search.split(" ");
- const action = args[0].slice(1).toLowerCase();
-
- if (action === "scheme" && args[1]?.toLowerCase() === "random") {
- execAsync(`caelestia scheme`).catch(console.error);
- close();
- } else this.#content.get_child_at_index(0)?.get_child()?.activate();
- }
-}
diff --git a/src/modules/launcher/index.tsx b/src/modules/launcher/index.tsx
deleted file mode 100644
index b75ecce..0000000
--- a/src/modules/launcher/index.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import PopupWindow from "@/widgets/popupwindow";
-import { bind, register, Variable } from "astal";
-import { Astal, Gtk, Widget } from "astal/gtk3";
-import { launcher as config } from "config";
-import Actions from "./actions";
-import Modes from "./modes";
-import type { Mode } from "./util";
-
-const getModeIcon = (mode: Mode) => {
- if (mode === "apps") return "apps";
- if (mode === "files") return "folder";
- if (mode === "math") return "calculate";
- return "search";
-};
-
-const getPrettyMode = (mode: Mode) => {
- if (mode === "apps") return "Apps";
- if (mode === "files") return "Files";
- if (mode === "math") return "Math";
- return mode;
-};
-
-const isAction = (text: string, action: string = "") => text.startsWith(config.actionPrefix.get() + action);
-
-const SearchBar = ({ mode, entry }: { mode: Variable<Mode>; entry: Widget.Entry }) => (
- <box className="search-bar">
- <box className="mode">
- <label className="icon" label={bind(mode).as(getModeIcon)} />
- <label label={bind(mode).as(getPrettyMode)} />
- </box>
- {entry}
- </box>
-);
-
-const ModeSwitcher = ({ mode, modes }: { mode: Variable<Mode>; modes: Mode[] }) => (
- <box homogeneous hexpand className="mode-switcher">
- {modes.map(m => (
- <button
- className={bind(mode).as(c => `mode ${c === m ? "selected" : ""}`)}
- cursor="pointer"
- onClicked={() => mode.set(m)}
- >
- <box halign={Gtk.Align.CENTER}>
- <label className="icon" label={getModeIcon(m)} />
- <label label={getPrettyMode(m)} />
- </box>
- </button>
- ))}
- </box>
-);
-
-@register()
-export default class Launcher extends PopupWindow {
- readonly mode: Variable<Mode>;
-
- constructor() {
- const entry = (
- <entry
- hexpand
- className="entry"
- placeholderText={bind(config.actionPrefix).as(p => `Type "${p}" for subcommands`)}
- />
- ) as Widget.Entry;
- const mode = Variable<Mode>("apps");
- const content = Modes();
- const actions = new Actions(mode, entry);
- const className = Variable.derive([mode, config.style], (m, s) => `launcher ${m} ${s}`);
-
- super({
- name: "launcher",
- anchor:
- Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
- keymode: Astal.Keymode.EXCLUSIVE,
- exclusivity: Astal.Exclusivity.IGNORE,
- borderWidth: 0,
- onKeyPressEvent(_, event) {
- const keyval = event.get_keyval()[1];
- // Focus entry on typing
- if (!entry.isFocus && keyval >= 32 && keyval <= 126) {
- entry.text += String.fromCharCode(keyval);
- entry.grab_focus();
- entry.set_position(-1);
-
- // Consume event, if not consumed it will duplicate character in entry
- return true;
- }
- },
- child: (
- <box
- vertical
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- className={bind(className)}
- onDestroy={() => className.drop()}
- >
- <SearchBar mode={mode} entry={entry} />
- <stack
- expand
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={100}
- shown={bind(entry, "text").as(t => (isAction(t) ? "actions" : "content"))}
- >
- <stack
- name="content"
- transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
- transitionDuration={200}
- shown={bind(mode)}
- >
- {Object.values(content)}
- </stack>
- {actions}
- </stack>
- <ModeSwitcher mode={mode} modes={Object.keys(content) as Mode[]} />
- </box>
- ),
- });
-
- this.mode = mode;
-
- content[mode.get()].updateContent(entry.get_text());
- this.hook(mode, (_, v: Mode) => {
- entry.set_text("");
- content[v].updateContent(entry.get_text());
- });
- this.hook(entry, "changed", () =>
- (isAction(entry.get_text()) ? actions : content[mode.get()]).updateContent(entry.get_text())
- );
- this.hook(entry, "activate", () => {
- (isAction(entry.get_text()) ? actions : content[mode.get()]).handleActivate(entry.get_text());
- if (mode.get() === "math" && !isAction(entry.get_text())) entry.set_text(""); // Cause math mode doesn't auto clear
- });
-
- // Clear search on hide if not in math mode or creating a todo
- this.connect("hide", () => {
- if ((mode.get() !== "math" || isAction(entry.get_text())) && !isAction(entry.get_text(), "todo"))
- entry.set_text("");
- });
- }
-
- open(mode: Mode) {
- this.mode.set(mode);
- this.show();
- }
-}
diff --git a/src/modules/launcher/modes.tsx b/src/modules/launcher/modes.tsx
deleted file mode 100644
index e278779..0000000
--- a/src/modules/launcher/modes.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-import { Apps as AppsService } from "@/services/apps";
-import MathService, { type HistoryItem } from "@/services/math";
-import { getAppCategoryIcon } from "@/utils/icons";
-import { launch } from "@/utils/system";
-import { type FlowBox, setupCustomTooltip } from "@/utils/widgets";
-import { bind, execAsync, Gio, register, Variable } from "astal";
-import { Astal, Gtk, Widget } from "astal/gtk3";
-import { launcher as config } from "config";
-import type AstalApps from "gi://AstalApps";
-import { close, ContentBox, type LauncherContent, limitLength } from "./util";
-
-const AppResult = ({ app }: { app: AstalApps.Application }) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- launch(app);
- close();
- }}
- setup={self => setupCustomTooltip(self, app.description ? `${app.name}: ${app.description}` : app.name)}
- >
- <box>
- {app.iconName && Astal.Icon.lookup_icon(app.iconName) ? (
- <icon className="icon" icon={app.iconName} />
- ) : (
- <label className="icon" label={getAppCategoryIcon(app)} />
- )}
- <label truncate label={app.name} />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const FileResult = ({ path }: { path: string }) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- execAsync([
- "bash",
- "-c",
- `dbus-send --session --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:'file://${path}' string:'' || app2unit -O '${path}'`,
- ]).catch(console.error);
- close();
- }}
- >
- <box setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))}>
- <icon
- className="icon"
- gicon={
- Gio.File.new_for_path(path)
- .query_info(Gio.FILE_ATTRIBUTE_STANDARD_ICON, Gio.FileQueryInfoFlags.NONE, null)
- .get_icon()!
- }
- />
- <label
- truncate
- label={
- path.replace(HOME, "~").length > config.files.shortenThreshold.get()
- ? path
- .replace(HOME, "~")
- .split("/")
- .map((n, i, arr) => (i === 0 || i === arr.length - 1 ? n : n.slice(0, 1)))
- .join("/")
- : path.replace(HOME, "~")
- }
- />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const MathResult = ({ icon, equation, result }: HistoryItem) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- execAsync(["wl-copy", "--", result]).catch(console.error);
- close();
- }}
- setup={self => setupCustomTooltip(self, `${equation} -> ${result}`)}
- >
- <box>
- <label className="icon" label={icon} />
- <box vertical className="has-sublabel">
- <label truncate xalign={0} label={equation} />
- <label truncate xalign={0} label={result} className="sublabel" />
- </box>
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-@register()
-class Apps extends Widget.Box implements LauncherContent {
- #content: FlowBox;
-
- constructor() {
- super({ name: "apps", className: "apps" });
-
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- this.#content.foreach(c => c.destroy());
- for (const app of limitLength(AppsService.fuzzy_query(search), config.apps))
- this.#content.add(<AppResult app={app} />);
- }
-
- handleActivate(): void {
- this.#content.get_child_at_index(0)?.get_child()?.grab_focus();
- this.#content.get_child_at_index(0)?.get_child()?.activate();
- }
-}
-
-@register()
-class Files extends Widget.Box implements LauncherContent {
- #content: FlowBox;
-
- constructor() {
- super({ name: "files", className: "files" });
-
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- execAsync(["fd", ...config.files.fdOpts.get(), search, HOME])
- .then(out => {
- this.#content.foreach(c => c.destroy());
- const paths = out.split("\n").filter(path => path);
- for (const path of limitLength(paths, config.files)) this.#content.add(<FileResult path={path} />);
- })
- .catch(() => {}); // Ignore errors
- }
-
- handleActivate(): void {
- this.#content.get_child_at_index(0)?.get_child()?.grab_focus();
- this.#content.get_child_at_index(0)?.get_child()?.activate();
- }
-}
-
-@register()
-class Math extends Widget.Box implements LauncherContent {
- #showResult: Variable<boolean>;
- #result: Variable<HistoryItem>;
- #content: FlowBox;
-
- constructor() {
- super({ name: "math", className: "math", vertical: true });
-
- this.#showResult = Variable(false);
- this.#result = Variable({ equation: "", result: "", icon: "" });
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- revealChild={bind(this.#showResult)}
- >
- <box vertical className="preview">
- <box className="result">
- <label className="icon" label={bind(this.#result).as(r => r.icon)} />
- <box vertical>
- <label xalign={0} label="Result" />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={bind(this.#result).as(r => r.result)}
- />
- </box>
- </box>
- <box visible={bind(config.style).as(s => s === "lines")} className="separator" />
- </box>
- </revealer>
- );
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- this.#showResult.set(search.length > 0);
- this.#result.set(MathService.get_default().evaluate(search));
-
- this.#content.foreach(c => c.destroy());
- for (const item of limitLength(MathService.get_default().history, config.math))
- this.#content.add(<MathResult {...item} />);
- }
-
- handleActivate(search: string): void {
- if (!search) return;
- MathService.get_default().commit();
- const res = this.#result.get();
- // Copy and close if not assignment, help or error
- if (!["equal", "help", "error"].includes(res.icon)) {
- execAsync(["wl-copy", "--", res.result]).catch(console.error);
- close();
- }
- }
-}
-
-export default () => ({
- apps: new Apps(),
- files: new Files(),
- math: new Math(),
-});
diff --git a/src/modules/launcher/util.tsx b/src/modules/launcher/util.tsx
deleted file mode 100644
index 8288588..0000000
--- a/src/modules/launcher/util.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { FlowBox } from "@/utils/widgets";
-import type { Variable } from "astal";
-import { App, Gtk } from "astal/gtk3";
-
-export type Mode = "apps" | "files" | "math";
-
-export interface LauncherContent {
- updateContent(search: string): void;
- handleActivate(search: string): void;
-}
-
-export const close = () => App.get_window("launcher")?.hide();
-
-export const limitLength = <T,>(arr: T[], cfg: { maxResults: Variable<number> }) =>
- cfg.maxResults.get() > 0 && arr.length > cfg.maxResults.get() ? arr.slice(0, cfg.maxResults.get()) : arr;
-
-export const ContentBox = () => (
- <FlowBox homogeneous valign={Gtk.Align.START} minChildrenPerLine={2} maxChildrenPerLine={2} />
-);
diff --git a/src/modules/mediadisplay/index.tsx b/src/modules/mediadisplay/index.tsx
deleted file mode 100644
index 307087c..0000000
--- a/src/modules/mediadisplay/index.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Players from "@/services/players";
-import { lengthStr } from "@/utils/strings";
-import { bind, Variable } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
-import AstalMpris from "gi://AstalMpris";
-import Visualiser from "./visualiser";
-
-type Selected = Variable<AstalMpris.Player | null>;
-
-const bindIcon = (player: AstalMpris.Player) =>
- bind(player, "identity").as(i => {
- const icon = `caelestia-${i?.toLowerCase().replaceAll(" ", "-")}-symbolic`;
- return Astal.Icon.lookup_icon(icon) ? icon : "caelestia-media-generic-symbolic";
- });
-
-const PlayerButton = ({
- player,
- selected,
- showDropdown,
-}: {
- player: AstalMpris.Player;
- selected: Selected;
- showDropdown: Variable<boolean>;
-}) => (
- <button
- cursor="pointer"
- onClicked={() => {
- showDropdown.set(false);
- selected.set(player);
- }}
- >
- <box className="identity" halign={Gtk.Align.CENTER}>
- <label label={bind(player, "identity").as(i => i ?? "-")} />
- <label label="•" />
- <label label={bind(player, "title").as(t => t ?? "-")} />
- </box>
- </button>
-);
-
-const Selector = ({ player, selected }: { player?: AstalMpris.Player; selected: Selected }) => {
- const showDropdown = Variable(false);
-
- return (
- <box vertical valign={Gtk.Align.START} className="selector">
- <button
- sensitive={bind(Players.get_default(), "list").as(ps => ps.length > 1)}
- cursor="pointer"
- onClicked={() => showDropdown.set(!showDropdown.get())}
- >
- <box className="identity" halign={Gtk.Align.CENTER}>
- <icon icon={player ? bindIcon(player) : "caelestia-media-none-symbolic"} />
- <label label={player ? bind(player, "identity").as(i => i ?? "") : "No media"} />
- </box>
- </button>
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- revealChild={bind(showDropdown)}
- >
- <box vertical className="list">
- {bind(Players.get_default(), "list").as(ps =>
- ps
- .filter(p => p !== player)
- .map(p => <PlayerButton player={p} selected={selected} showDropdown={showDropdown} />)
- )}
- </box>
- </revealer>
- </box>
- );
-};
-
-const NoMedia = ({ selected }: { selected: Selected }) => (
- <box>
- <box homogeneous halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="cover-art">
- <label xalign={0.36} label="" />
- </box>
- <box>
- <box vertical className="details">
- <label truncate xalign={0} className="title" label="No media" />
- <label truncate xalign={0} className="artist" label="Try play something!" />
- <box halign={Gtk.Align.START} className="controls">
- <button sensitive={false} label="skip_previous" />
- <button sensitive={false} label="play_arrow" />
- <button sensitive={false} label="skip_next" />
- </box>
- </box>
- <box className="center-module">
- <overlay
- expand
- overlay={<label halign={Gtk.Align.CENTER} valign={Gtk.Align.END} className="time" label="-1:-1" />}
- >
- <Visualiser />
- </overlay>
- </box>
- <Selector selected={selected} />
- </box>
- </box>
-);
-
-const Player = ({ player, selected }: { player: AstalMpris.Player; selected: Selected }) => {
- const time = Variable.derive(
- [bind(player, "position"), bind(player, "length")],
- (p, l) => lengthStr(p) + " / " + lengthStr(l)
- );
-
- return (
- <box>
- <box
- homogeneous
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- className="cover-art"
- css={bind(player, "coverArt").as(a => `background-image: url("${a}");`)}
- >
- {bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.36} label="" />))}
- </box>
- <box>
- <box vertical className="details">
- <label truncate xalign={0} className="title" label={bind(player, "title").as(t => t ?? "-")} />
- <label truncate xalign={0} className="artist" label={bind(player, "artist").as(t => t ?? "-")} />
- <box halign={Gtk.Align.START} className="controls">
- <button
- sensitive={bind(player, "canGoPrevious")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="skip_previous"
- />
- <button
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.play_pause()}
- label={bind(player, "playbackStatus").as(s =>
- s === AstalMpris.PlaybackStatus.PLAYING ? "pause" : "play_arrow"
- )}
- />
- <button
- sensitive={bind(player, "canGoNext")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="skip_next"
- />
- </box>
- </box>
- <box className="center-module">
- <overlay
- expand
- overlay={
- <label
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.END}
- className="time"
- label={bind(time)}
- onDestroy={() => time.drop()}
- />
- }
- >
- <Visualiser />
- </overlay>
- </box>
- <Selector player={player} selected={selected} />
- </box>
- </box>
- );
-};
-
-export default ({ monitor }: { monitor: Monitor }) => {
- const selected = Variable(Players.get_default().lastPlayer);
- selected.observe(Players.get_default(), "notify::last-player", () => Players.get_default().lastPlayer);
-
- return (
- <window
- application={App}
- name={`mediadisplay${monitor.id}`}
- namespace="caelestia-mediadisplay"
- monitor={monitor.id}
- anchor={Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- visible={false}
- >
- <box className="mediadisplay" onDestroy={() => selected.drop()}>
- {bind(selected).as(p =>
- p ? <Player player={p} selected={selected} /> : <NoMedia selected={selected} />
- )}
- </box>
- </window>
- );
-};
diff --git a/src/modules/mediadisplay/visualiser.tsx b/src/modules/mediadisplay/visualiser.tsx
deleted file mode 100644
index d788e7b..0000000
--- a/src/modules/mediadisplay/visualiser.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Gtk } from "astal/gtk3";
-import cairo from "cairo";
-import AstalCava from "gi://AstalCava";
-import PangoCairo from "gi://PangoCairo";
-
-export default () => (
- <drawingarea
- className="visualiser"
- setup={self => {
- const cava = AstalCava.get_default();
-
- if (cava) {
- cava.set_stereo(true);
- cava.set_noise_reduction(0.77);
- cava.set_input(AstalCava.Input.PIPEWIRE);
-
- self.hook(cava, "notify::values", () => self.queue_draw());
- self.connect("size-allocate", () => {
- const width = self.get_allocated_width();
- const barWidth = self
- .get_style_context()
- .get_property("min-width", Gtk.StateFlags.NORMAL) as number;
- const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right;
- const bars = Math.floor((width - gaps) / (barWidth + gaps));
- if (bars > 0) cava.set_bars(bars % 2 ? bars : bars - 1);
- });
- }
-
- self.connect("draw", (_, cr: cairo.Context) => {
- const { width, height } = self.get_allocation();
-
- if (!cava) {
- // Show error text if cava unavailable
- const fg = self.get_style_context().get_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
- const layout = self.create_pango_layout("Visualiser module requires Cava");
- const [w, h] = layout.get_pixel_size();
- cr.moveTo((width - w) / 2, (height - h) / 2);
- cr.setAntialias(cairo.Antialias.BEST);
- PangoCairo.show_layout(cr, layout);
-
- return;
- }
-
- const bg = self.get_style_context().get_background_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
- const barWidth = self.get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL) as number;
- const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right;
-
- const values = cava.get_values();
- const len = values.length - 1;
- const radius = barWidth / 2;
- const xOff = (width - len * (barWidth + gaps) - gaps) / 2 - radius;
- const center = height / 2;
- const half = len / 2;
-
- const renderPill = (x: number, value: number) => {
- x = x * (barWidth + gaps) + xOff;
- value *= center;
- cr.arc(x, center + value, radius, 0, Math.PI);
- cr.arc(x, center - value, radius, Math.PI, Math.PI * 2);
- cr.fill();
- };
-
- // Render channels facing each other
- for (let i = half - 1; i >= 0; i--) renderPill(half - i, values[i]);
- for (let i = half; i < len; i++) renderPill(i + 1, values[i]);
- });
- }}
- />
-);
diff --git a/src/modules/navbar.tsx b/src/modules/navbar.tsx
deleted file mode 100644
index 35d3900..0000000
--- a/src/modules/navbar.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import { capitalize } from "@/utils/strings";
-import type { AstalWidget } from "@/utils/types";
-import { bind, execAsync, Variable } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
-import { navbar as config } from "config";
-import AstalHyprland from "gi://AstalHyprland";
-import Pango from "gi://Pango";
-import SideBar, { awaitSidebar, paneNames, switchPane, type PaneName } from "./sidebar";
-
-const layerNames = ["mediadisplay"] as const;
-type LayerName = `${(typeof layerNames)[number]}${number}`;
-
-const specialWsNames = ["sysmon", "communication", "music", "todo"] as const;
-type SpecialWsName = (typeof specialWsNames)[number];
-
-const getPaneIcon = (name: PaneName) => {
- if (name === "dashboard") return "dashboard";
- if (name === "audio") return "tune";
- if (name === "connectivity") return "settings_ethernet";
- if (name === "packages") return "package_2";
- if (name === "alerts") return "notifications";
- return "date_range";
-};
-
-const getLayerIcon = (name: LayerName) => {
- return "graphic_eq";
-};
-
-const getSpecialWsIcon = (name: SpecialWsName) => {
- if (name === "sysmon") return "speed";
- if (name === "communication") return "communication";
- if (name === "music") return "music_note";
- return "checklist";
-};
-
-const hookIsCurrent = (
- self: AstalWidget,
- sidebar: Variable<SideBar | null>,
- name: PaneName,
- callback: (isCurrent: boolean) => void
-) => {
- const unsub = sidebar.subscribe(s => {
- if (!s) return;
- self.hook(s.shown, (_, v) => callback(s.visible && v === name));
- self.hook(s, "notify::visible", () => callback(s.visible && s.shown.get() === name));
- callback(s.visible && s.shown.get() === name);
- unsub();
- });
-};
-
-const PaneButton = ({
- monitor,
- name,
- sidebar,
-}: {
- monitor: Monitor;
- name: PaneName;
- sidebar: Variable<SideBar | null>;
-}) => (
- <button
- cursor="pointer"
- onClicked={() => switchPane(monitor, name)}
- setup={self => hookIsCurrent(self, sidebar, name, c => self.toggleClassName("current", c))}
- >
- <box vertical className="nav-button">
- <label className="icon" label={getPaneIcon(name)} />
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- setup={self => {
- let isCurrent = false;
- hookIsCurrent(self, sidebar, name, c => {
- isCurrent = c;
- self.set_reveal_child(config.showLabels.get() && c);
- });
- self.hook(config.showLabels, (_, v) => self.set_reveal_child(v && isCurrent));
- }}
- >
- <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} />
- </revealer>
- </box>
- </button>
-);
-
-const LayerButton = ({ name }: { name: LayerName }) => (
- <button
- cursor="pointer"
- onClicked={() => App.toggle_window(name)}
- setup={self =>
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === name) self.toggleClassName("current", window.visible);
- })
- }
- >
- <box vertical className="nav-button">
- <label className="icon" label={getLayerIcon(name)} />
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- setup={self => {
- let visible = false;
- self.hook(config.showLabels, (_, v) => self.toggleClassName(v && visible));
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === name)
- self.toggleClassName("current", config.showLabels.get() && window.visible);
- });
- }}
- >
- <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} />
- </revealer>
- </box>
- </button>
-);
-
-const SpecialWsButton = ({ name }: { name: SpecialWsName }) => {
- const revealChild = Variable.derive(
- [config.showLabels, bind(AstalHyprland.get_default(), "focusedClient")],
- (l, c) => l && c?.get_workspace().get_name() === `special:${name}`
- );
-
- return (
- <button
- className={bind(AstalHyprland.get_default(), "focusedClient").as(c =>
- c?.get_workspace().get_name() === `special:${name}` ? "current" : ""
- )}
- cursor="pointer"
- onClicked={() => execAsync(`caelestia toggle ${name}`).catch(console.error)}
- >
- <box vertical className="nav-button">
- <label className="icon" label={getSpecialWsIcon(name)} />
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- revealChild={bind(revealChild)}
- onDestroy={() => revealChild.drop()}
- >
- <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} />
- </revealer>
- </box>
- </button>
- );
-};
-
-export default ({ monitor }: { monitor: Monitor }) => {
- const sidebar = Variable<SideBar | null>(null);
- awaitSidebar(monitor).then(s => sidebar.set(s));
-
- return (
- <window
- namespace="caelestia-navbar"
- monitor={monitor.id}
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- visible={config.persistent.get()}
- setup={self => {
- const hyprland = AstalHyprland.get_default();
- const visible = Variable(config.persistent.get());
-
- visible.poll(100, () => {
- const width = self.visible
- ? Math.max(config.appearWidth.get(), self.get_allocated_width())
- : config.appearWidth.get();
- return hyprland.get_cursor_position().x < width;
- });
- if (config.persistent.get()) visible.stopPoll();
-
- self.hook(config.persistent, (_, v) => {
- if (v) {
- visible.stopPoll();
- visible.set(true);
- } else visible.startPoll();
- });
-
- self.hook(visible, (_, v) => self.set_visible(v));
- self.connect("destroy", () => visible.drop());
- }}
- >
- <eventbox
- onScroll={(_, event) => {
- const shown = sidebar.get()?.shown;
- if (!shown) return;
- const idx = paneNames.indexOf(shown.get());
- if (event.delta_y > 0) shown.set(paneNames[Math.min(paneNames.length - 1, idx + 1)]);
- else shown.set(paneNames[Math.max(0, idx - 1)]);
- }}
- >
- <box vertical className="navbar">
- {paneNames.map(n => (
- <PaneButton monitor={monitor} name={n} sidebar={sidebar} />
- ))}
- {layerNames.map(n => (
- <LayerButton name={`${n}${monitor.id}`} />
- ))}
- <box vexpand />
- {specialWsNames.map(n => (
- <SpecialWsButton name={n} />
- ))}
- </box>
- </eventbox>
- </window>
- );
-};
diff --git a/src/modules/notifpopups.tsx b/src/modules/notifpopups.tsx
deleted file mode 100644
index cb5984d..0000000
--- a/src/modules/notifpopups.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import { setupChildClickthrough } from "@/utils/widgets";
-import Notification from "@/widgets/notification";
-import { Astal, Gtk } from "astal/gtk3";
-import { notifpopups as config } from "config";
-import AstalNotifd from "gi://AstalNotifd";
-import type SideBar from "./sidebar";
-import { awaitSidebar } from "./sidebar";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- monitor={monitor.id}
- namespace="caelestia-notifpopups"
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM}
- >
- <box
- vertical
- valign={Gtk.Align.START}
- className="notifpopups"
- setup={self => {
- const notifd = AstalNotifd.get_default();
- const map = new Map<number, Notification>();
-
- self.hook(notifd, "notified", (self, id) => {
- if (notifd.dontDisturb) return;
-
- const notification = notifd.get_notification(id);
-
- const popup = (<Notification popup notification={notification} />) as Notification;
- popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id));
- map.get(notification.id)?.destroyWithAnims();
- map.set(notification.id, popup);
-
- self.add(
- <eventbox
- onClick={(_, event) => {
- // Activate notif or go to notif center on primary click
- if (event.button === Astal.MouseButton.PRIMARY) {
- if (notification.actions.length === 1)
- notification.invoke(notification.actions[0].id);
- else {
- sidebar?.shown.set("alerts");
- sidebar?.show();
- popup.destroyWithAnims();
- }
- }
- // Dismiss on middle click
- else if (event.button === Astal.MouseButton.MIDDLE) notification.dismiss();
- }}
- // Close on hover lost
- onHoverLost={() => popup.destroyWithAnims()}
- setup={self => self.hook(popup, "destroy", () => self.destroy())}
- >
- {popup}
- </eventbox>
- );
-
- // Limit number of popups
- if (config.maxPopups.get() > 0 && self.children.length > config.maxPopups.get())
- map.values().next().value?.destroyWithAnims();
- });
- self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
-
- let sidebar: SideBar | null = null;
- awaitSidebar(monitor).then(s => (sidebar = s));
-
- // Change input region to child region so can click through empty space
- setupChildClickthrough(self);
- }}
- />
- </window>
-);
diff --git a/src/modules/osds.tsx b/src/modules/osds.tsx
deleted file mode 100644
index 0f38823..0000000
--- a/src/modules/osds.tsx
+++ /dev/null
@@ -1,327 +0,0 @@
-import Monitors, { type Monitor } from "@/services/monitors";
-import { capitalize } from "@/utils/strings";
-import PopupWindow from "@/widgets/popupwindow";
-import { bind, execAsync, register, timeout, Variable, type Time } from "astal";
-import { App, Astal, Gtk, Widget } from "astal/gtk3";
-import cairo from "cairo";
-import { osds as config } from "config";
-import AstalWp from "gi://AstalWp";
-import Cairo from "gi://cairo";
-import Pango from "gi://Pango";
-import PangoCairo from "gi://PangoCairo";
-
-const getStyle = (context: Gtk.StyleContext, prop: string) => context.get_property(prop, Gtk.StateFlags.NORMAL);
-const getNumStyle = (context: Gtk.StyleContext, prop: string) => getStyle(context, prop) as number;
-
-const mix = (a: number, b: number, r: number) => a * r + b * (1 - r);
-
-const pangoWeightToStr = (weight: Pango.Weight) => {
- switch (weight) {
- case Pango.Weight.ULTRALIGHT:
- return "UltraLight";
- case Pango.Weight.LIGHT:
- return "Light";
- case Pango.Weight.BOLD:
- return "Bold";
- case Pango.Weight.ULTRABOLD:
- return "UltraBold";
- case Pango.Weight.HEAVY:
- return "Heavy";
- default:
- return "Normal";
- }
-};
-
-const SliderOsd = ({
- fillIcons,
- monitor,
- type,
- windowSetup,
- className = "",
- initValue,
- drawAreaSetup,
-}: {
- fillIcons?: boolean;
- monitor?: Monitor;
- type: "volume" | "brightness";
- windowSetup: (self: Widget.Window, show: () => void) => void;
- className?: string;
- initValue: number;
- drawAreaSetup: (self: Widget.DrawingArea, icon: Variable<string>) => void;
-}) => (
- <PopupWindow
- name={type}
- monitor={monitor?.id}
- keymode={Astal.Keymode.NONE}
- anchor={bind(config[type].position)}
- margin={bind(config[type].margin)}
- setup={self => {
- let time: Time | null = null;
- const hideAfterTimeout = () => {
- time?.cancel();
- time = timeout(config[type].hideDelay.get(), () => self.hide());
- };
- self.connect("show", hideAfterTimeout);
- windowSetup(self, () => {
- self.show();
- hideAfterTimeout();
- });
- }}
- >
- <box className={type}>
- <drawingarea
- className={`inner ${className}`}
- css={"font-size: " + initValue + "px;"}
- setup={self => {
- const halfPi = Math.PI / 2;
- const vertical =
- config[type].position.get() === Astal.WindowAnchor.LEFT ||
- config[type].position.get() === Astal.WindowAnchor.RIGHT;
-
- const icon = Variable("");
- drawAreaSetup(self, icon);
- self.hook(icon, () => self.queue_draw());
-
- // Init size
- const styleContext = self.get_style_context();
- const width = getNumStyle(styleContext, "min-width");
- const height = getNumStyle(styleContext, "min-height");
- if (vertical) self.set_size_request(height, width);
- else self.set_size_request(width, height);
-
- let fontDesc: Pango.FontDescription | null = null;
-
- self.connect("draw", (_, cr: cairo.Context) => {
- const parent = self.get_parent();
- if (!parent) return;
-
- const styleContext = self.get_style_context();
- const pContext = parent.get_style_context();
-
- let width = getNumStyle(styleContext, "min-width");
- let height = getNumStyle(styleContext, "min-height");
-
- const progressValue = getNumStyle(styleContext, "font-size");
- let radius = getNumStyle(pContext, "border-radius");
- // Flatten when near 0, do before swap cause its simpler
- radius = Math.min(radius, Math.min(width * progressValue, height) / 2);
-
- if (vertical) [width, height] = [height, width]; // Swap if vertical
- self.set_size_request(width, height);
-
- const progressPosition = vertical
- ? height * (1 - progressValue) + radius // Top is 0, but we want it to start from the bottom
- : width * progressValue - radius;
-
- const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
-
- // Background
- if (vertical) {
- cr.arc(radius, progressPosition, radius, -Math.PI, -halfPi); // Top left
- cr.arc(width - radius, progressPosition, radius, -halfPi, 0); // Top right
- cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
- } else {
- cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
- cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
- cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
- }
- cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
- cr.fill();
-
- const fg = pContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setAntialias(cairo.Antialias.BEST);
-
- // Progress number, at top/right
- let nw = 0;
- let nh = 0;
- if (config[type].showValue.get()) {
- const numLayout = parent.create_pango_layout(String(Math.round(progressValue * 100)));
- [nw, nh] = numLayout.get_pixel_size();
- let diff;
- if (vertical) {
- diff = ((1 - progressValue) * height) / nh;
- cr.moveTo((width - nw) / 2, radius / 2);
- } else {
- diff = ((1 - progressValue) * width) / nw;
- cr.moveTo(width - nw - radius, (height - nh) / 2);
- }
- diff = Math.max(0, Math.min(1, diff));
-
- cr.setSourceRGBA(
- mix(bg.red, fg.red, diff),
- mix(bg.green, fg.green, diff),
- mix(bg.blue, fg.blue, diff),
- mix(bg.alpha, fg.alpha, diff)
- );
-
- PangoCairo.show_layout(cr, numLayout);
- }
-
- // Progress icon, follows progress
- if (fontDesc === null) {
- const weight = pangoWeightToStr(getStyle(pContext, "font-weight") as Pango.Weight);
- const size = getNumStyle(pContext, "font-size") * 1.5;
- fontDesc = Pango.font_description_from_string(
- `Material Symbols Rounded ${weight} ${size}px`
- );
- // Ugh GTK CSS doesn't support font-variations, so you need to manually create the layout and font desc instead of using Gtk.Widget#create_pango_layout
- if (fillIcons) fontDesc.set_variations("FILL=1");
- }
-
- const iconLayout = PangoCairo.create_layout(cr);
- iconLayout.set_font_description(fontDesc);
- iconLayout.set_text(icon.get(), -1);
-
- const [iw, ih] = iconLayout.get_pixel_size();
- let diff;
- if (vertical) {
- diff = (progressValue * height) / ih;
- cr.moveTo(
- (width - iw) / 2,
- Math.max(nh, Math.min(height - ih, progressPosition - ih / 2 + radius))
- );
- } else {
- diff = (progressValue * width) / iw;
- cr.moveTo(
- Math.min(
- width - nw * 1.1 - iw - radius,
- Math.max(0, progressPosition - iw / 2 - radius)
- ),
- (height - ih) / 2
- );
- }
- diff = Math.max(0, Math.min(1, diff));
-
- cr.setSourceRGBA(
- mix(fg.red, bg.red, diff),
- mix(fg.green, bg.green, diff),
- mix(fg.blue, bg.blue, diff),
- mix(fg.alpha, bg.alpha, diff)
- );
-
- PangoCairo.show_layout(cr, iconLayout);
- });
- }}
- />
- </box>
- </PopupWindow>
-);
-
-const Volume = ({ audio }: { audio: AstalWp.Audio }) => (
- <SliderOsd
- fillIcons
- type="volume"
- windowSetup={(self, show) => {
- self.hook(audio.defaultSpeaker, "notify::volume", show);
- self.hook(audio.defaultSpeaker, "notify::mute", show);
- }}
- className={audio.defaultSpeaker.mute ? "mute" : ""}
- initValue={audio.defaultSpeaker.volume}
- drawAreaSetup={(self, icon) => {
- const updateIcon = () => {
- if (/head(phone|set)/i.test(audio.defaultSpeaker.icon)) icon.set("headphones");
- else if (audio.defaultSpeaker.mute) icon.set("no_sound");
- else if (audio.defaultSpeaker.volume === 0) icon.set("volume_mute");
- else if (audio.defaultSpeaker.volume <= 0.5) icon.set("volume_down");
- else icon.set("volume_up");
- };
- updateIcon();
- self.hook(audio.defaultSpeaker, "notify::icon", updateIcon);
- self.hook(audio.defaultSpeaker, "notify::mute", () => {
- updateIcon();
- self.toggleClassName("mute", audio.defaultSpeaker.mute);
- });
- self.hook(audio.defaultSpeaker, "notify::volume", () => {
- updateIcon();
- self.css = `font-size: ${audio.defaultSpeaker.volume}px`;
- });
- }}
- />
-);
-
-const Brightness = ({ monitor }: { monitor: Monitor }) => (
- <SliderOsd
- monitor={monitor}
- type="brightness"
- windowSetup={(self, show) => self.hook(monitor, "notify::brightness", show)}
- initValue={monitor.brightness}
- drawAreaSetup={(self, icon) => {
- const update = () => {
- if (monitor.brightness > 0.66) icon.set("brightness_high");
- else if (monitor.brightness > 0.33) icon.set("brightness_medium");
- else if (monitor.brightness > 0) icon.set("brightness_low");
- else icon.set("brightness_empty");
- self.css = `font-size: ${monitor.brightness}px`;
- };
- self.hook(monitor, "notify::brightness", update);
- update();
- }}
- />
-);
-
-@register()
-class LockOsd extends Widget.Window {
- readonly lockType: "caps" | "num";
-
- #timeout: Time | null = null;
-
- constructor({ type, icon, right }: { type: "caps" | "num"; icon: string; right?: boolean }) {
- super({
- visible: false,
- name: `lock-${type}`,
- application: App,
- namespace: `caelestia-lock-${type}`,
- anchor:
- Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
- exclusivity: Astal.Exclusivity.IGNORE,
- });
-
- this.lockType = type;
- this.#update();
-
- this.add(
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className={`lock ${type}`}>
- <label vexpand className="icon" label={icon} />
- <label vexpand className="text" label={capitalize(type) + "lock"} />
- </box>
- );
-
- // Clickthrough
- this.connect("size-allocate", () => this.input_shape_combine_region(new Cairo.Region()));
-
- // Move over when other indicator opens/closes
- this.hook(App, "window-toggled", (_, window) => {
- if (window !== this && window instanceof LockOsd) {
- const child = this.get_child();
- if (!child) return;
- this[right ? "marginLeft" : "marginRight"] = window.visible
- ? child.get_preferred_width()[1] + config.lock.spacing.get()
- : 0;
- }
- });
- }
-
- #update() {
- execAsync(`fish -c 'cat /sys/class/leds/input*::${this.lockType}lock/brightness'`)
- .then(out => (this.get_child() as Widget.Box | null)?.toggleClassName("enabled", out.includes("1")))
- .catch(console.error);
- }
-
- show() {
- super.show();
- this.#update();
- this.#timeout?.cancel();
- this.#timeout = timeout(config.lock[this.lockType].hideDelay.get(), () => this.hide());
- }
-}
-
-export default () => {
- if (AstalWp.get_default()) <Volume audio={AstalWp.get_default()!.audio} />;
- Monitors.get_default().forEach(monitor => <Brightness monitor={monitor} />);
-
- <LockOsd type="caps" icon="keyboard_capslock" />;
- <LockOsd right type="num" icon="filter_1" />;
-
- return null;
-};
diff --git a/src/modules/screencorners.tsx b/src/modules/screencorners.tsx
deleted file mode 100644
index 4368b87..0000000
--- a/src/modules/screencorners.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import ScreenCorner from "@/widgets/screencorner";
-import { bind } from "astal/binding";
-import { Astal } from "astal/gtk3";
-import { bar } from "config";
-import Cairo from "gi://cairo";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-screencorners"
- monitor={monitor.id}
- anchor={bind(bar.vertical).as(
- v =>
- Astal.WindowAnchor.BOTTOM |
- Astal.WindowAnchor.RIGHT |
- (v ? Astal.WindowAnchor.TOP : Astal.WindowAnchor.LEFT)
- )}
- setup={self =>
- self.connect("size-allocate", () => self.get_window()?.input_shape_combine_region(new Cairo.Region(), 0, 0))
- }
- >
- <box vertical={bind(bar.vertical)}>
- <ScreenCorner place={bind(bar.vertical).as(v => (v ? "topright" : "bottomleft"))} />
- <box expand />
- <ScreenCorner place="bottomright" />
- </box>
- </window>
-);
-
-export const BarScreenCorners = ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-screencorners"
- monitor={monitor.id}
- anchor={bind(bar.vertical).as(
- v =>
- Astal.WindowAnchor.TOP |
- Astal.WindowAnchor.LEFT |
- (v ? Astal.WindowAnchor.BOTTOM : Astal.WindowAnchor.RIGHT)
- )}
- visible={bind(bar.style).as(s => s === "embedded")}
- setup={self =>
- self.connect("size-allocate", () => self.get_window()?.input_shape_combine_region(new Cairo.Region(), 0, 0))
- }
- >
- <box vertical={bind(bar.vertical)}>
- <ScreenCorner place="topleft" />
- <box expand />
- <ScreenCorner place={bind(bar.vertical).as(v => (v ? "bottomleft" : "topright"))} />
- </box>
- </window>
-);
diff --git a/src/modules/session.tsx b/src/modules/session.tsx
deleted file mode 100644
index 40d3b31..0000000
--- a/src/modules/session.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import PopupWindow from "@/widgets/popupwindow";
-import { execAsync } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
-
-const Item = ({ icon, label, cmd, isDefault }: { icon: string; label: string; cmd: string; isDefault?: boolean }) => (
- <box vertical className="item">
- <button
- cursor="pointer"
- onClicked={() => execAsync(cmd).catch(console.error)}
- setup={self =>
- isDefault &&
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === "session" && window.visible) self.grab_focus();
- })
- }
- >
- <label className="icon" label={icon} />
- </button>
- <label className="label" label={label} />
- </box>
-);
-
-export default () => (
- <PopupWindow
- className="session"
- name="session"
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT}
- exclusivity={Astal.Exclusivity.IGNORE}
- keymode={Astal.Keymode.EXCLUSIVE}
- layer={Astal.Layer.OVERLAY}
- borderWidth={0} // Don't need border width cause takes up entire screen
- >
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="inner">
- <box>
- <Item icon="logout" label="Logout" cmd="uwsm stop" isDefault />
- <Item icon="cached" label="Reboot" cmd="systemctl reboot" />
- </box>
- <box>
- <Item icon="downloading" label="Hibernate" cmd="systemctl hibernate" />
- <Item icon="power_settings_new" label="Shutdown" cmd="systemctl poweroff" />
- </box>
- </box>
- </PopupWindow>
-);
diff --git a/src/modules/sidebar/alerts.tsx b/src/modules/sidebar/alerts.tsx
deleted file mode 100644
index 9599aff..0000000
--- a/src/modules/sidebar/alerts.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Headlines from "./modules/headlines";
-import Notifications from "./modules/notifications";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="pane alerts" name="alerts">
- <Notifications />
- <box className="separator" />
- <Headlines monitor={monitor} />
- </box>
-);
diff --git a/src/modules/sidebar/audio.tsx b/src/modules/sidebar/audio.tsx
deleted file mode 100644
index 20a6551..0000000
--- a/src/modules/sidebar/audio.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import DeviceSelector from "./modules/deviceselector";
-import Media from "./modules/media";
-import Streams from "./modules/streams";
-
-export default () => (
- <box vertical className="pane audio" name="audio">
- <Media />
- <box className="separator" />
- <Streams />
- <box className="separator" />
- <DeviceSelector />
- </box>
-);
diff --git a/src/modules/sidebar/connectivity.tsx b/src/modules/sidebar/connectivity.tsx
deleted file mode 100644
index 2962b56..0000000
--- a/src/modules/sidebar/connectivity.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import Bluetooth from "./modules/bluetooth";
-import Networks from "./modules/networks";
-
-export default () => (
- <box vertical className="pane connectivity" name="connectivity">
- <Networks />
- <box className="separator" />
- <Bluetooth />
- </box>
-);
diff --git a/src/modules/sidebar/dashboard.tsx b/src/modules/sidebar/dashboard.tsx
deleted file mode 100644
index 1a8626f..0000000
--- a/src/modules/sidebar/dashboard.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import Players from "@/services/players";
-import { lengthStr } from "@/utils/strings";
-import { bindCurrentTime, osIcon } from "@/utils/system";
-import Slider from "@/widgets/slider";
-import { bind, GLib, monitorFile, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalMpris from "gi://AstalMpris";
-import Notifications from "./modules/notifications";
-import Upcoming from "./modules/upcoming";
-
-const noNull = (s: string | null) => s ?? "-";
-
-const FaceFallback = () => (
- <label
- setup={self => {
- const name = GLib.get_real_name();
- if (name !== "Unknown")
- self.label = name
- .split(" ")
- .map(s => s[0].toUpperCase())
- .join("");
- else {
- self.label = "";
- self.xalign = 0.44;
- }
- }}
- />
-);
-
-const User = () => {
- const uptime = Variable("").poll(5000, "uptime -p");
- const hasFace = Variable(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS));
-
- return (
- <box className="user">
- <box
- homogeneous
- className="face"
- setup={self => {
- self.css = `background-image: url("${HOME}/.face");`;
- const monitor = monitorFile(HOME + "/.face", () => {
- hasFace.set(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS));
- self.css = `background-image: url("${HOME}/.face");`;
- });
- self.connect("destroy", () => monitor.cancel());
- }}
- >
- {bind(hasFace).as(h => (h ? <box visible={false} /> : <FaceFallback />))}
- </box>
- <box vertical hexpand valign={Gtk.Align.CENTER} className="details">
- <label truncate xalign={0} className="name" label={`${osIcon} ${GLib.get_user_name()}`} />
- <label truncate xalign={0} label={bind(uptime)} onDestroy={() => uptime.drop()} />
- <label truncate xalign={0} label={bindCurrentTime("%A, %e %B")} />
- </box>
- </box>
- );
-};
-
-const Media = ({ player }: { player: AstalMpris.Player | null }) => {
- const position = player
- ? Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l)
- : Variable(0);
-
- return (
- <box className="media" onDestroy={() => position.drop()}>
- <box
- homogeneous
- className="cover-art"
- css={player ? bind(player, "coverArt").as(a => `background-image: url("${a}");`) : ""}
- >
- {player ? (
- bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.31} label="" />))
- ) : (
- <label xalign={0.31} label="" />
- )}
- </box>
- <box vertical className="details">
- <label truncate className="title" label={player ? bind(player, "title").as(noNull) : ""} />
- <label truncate className="artist" label={player ? bind(player, "artist").as(noNull) : "No media"} />
- <box hexpand className="controls">
- <button
- hexpand
- sensitive={player ? bind(player, "canGoPrevious") : false}
- cursor="pointer"
- onClicked={() => player?.next()}
- label="󰒮"
- />
- <button
- hexpand
- sensitive={player ? bind(player, "canControl") : false}
- cursor="pointer"
- onClicked={() => player?.play_pause()}
- label={
- player
- ? bind(player, "playbackStatus").as(s =>
- s === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"
- )
- : "󰐊"
- }
- />
- <button
- hexpand
- sensitive={player ? bind(player, "canGoNext") : false}
- cursor="pointer"
- onClicked={() => player?.next()}
- label="󰒭"
- />
- </box>
- <Slider value={bind(position)} onChange={(_, v) => player?.set_position(v * player.length)} />
- <box className="time">
- <label label={player ? bind(player, "position").as(lengthStr) : "-1:-1"} />
- <box hexpand />
- <label label={player ? bind(player, "length").as(lengthStr) : "-1:-1"} />
- </box>
- </box>
- </box>
- );
-};
-
-export default () => (
- <box vertical className="pane dashboard" name="dashboard">
- <User />
- <box className="separator" />
- {bind(Players.get_default(), "lastPlayer").as(p => (
- <Media player={p} />
- ))}
- <box className="separator" />
- <Notifications compact />
- <box className="separator" />
- <Upcoming />
- </box>
-);
diff --git a/src/modules/sidebar/index.tsx b/src/modules/sidebar/index.tsx
deleted file mode 100644
index 7570283..0000000
--- a/src/modules/sidebar/index.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import { bind, idle, register, Variable } from "astal";
-import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3";
-import { sidebar as config } from "config";
-import Alerts from "./alerts";
-import Audio from "./audio";
-import Connectivity from "./connectivity";
-import Dashboard from "./dashboard";
-import Packages from "./packages";
-import Time from "./time";
-
-export const paneNames = ["dashboard", "audio", "connectivity", "packages", "alerts", "time"] as const;
-export type PaneName = (typeof paneNames)[number];
-
-export const switchPane = (monitor: Monitor, name: PaneName) => {
- const sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
- if (sidebar) {
- if (sidebar.visible && sidebar.shown.get() === name) sidebar.hide();
- else sidebar.show();
- sidebar.shown.set(name);
- }
-};
-
-export const awaitSidebar = (monitor: Monitor) =>
- new Promise<SideBar>(resolve => {
- let sidebar: SideBar | null = null;
-
- const awaitSidebar = () => {
- sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
- if (sidebar) resolve(sidebar);
- else idle(awaitSidebar);
- };
- idle(awaitSidebar);
- });
-
-const getPane = (monitor: Monitor, name: PaneName) => {
- if (name === "dashboard") return <Dashboard />;
- if (name === "audio") return <Audio />;
- if (name === "connectivity") return <Connectivity />;
- if (name === "packages") return <Packages monitor={monitor} />;
- if (name === "alerts") return <Alerts monitor={monitor} />;
- return <Time />;
-};
-
-@register()
-export default class SideBar extends Widget.Window {
- readonly shown: Variable<PaneName>;
-
- constructor({ monitor }: { monitor: Monitor }) {
- super({
- application: App,
- name: `sidebar${monitor.id}`,
- namespace: "caelestia-sidebar",
- monitor: monitor.id,
- anchor: Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM,
- exclusivity: Astal.Exclusivity.EXCLUSIVE,
- visible: false,
- });
-
- this.shown = Variable(paneNames[0]);
-
- this.add(
- <eventbox
- onScroll={(_, event) => {
- if (event.modifier & Gdk.ModifierType.BUTTON1_MASK) {
- const index = paneNames.indexOf(this.shown.get()) + (event.delta_y < 0 ? -1 : 1);
- if (index < 0 || index >= paneNames.length) return;
- this.shown.set(paneNames[index]);
- }
- }}
- >
- <box vertical className="sidebar">
- <stack
- vexpand
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={200}
- shown={bind(this.shown)}
- >
- {paneNames.map(n => getPane(monitor, n))}
- </stack>
- </box>
- </eventbox>
- );
-
- if (config.showOnStartup.get()) idle(() => this.show());
- }
-}
diff --git a/src/modules/sidebar/modules/bluetooth.tsx b/src/modules/sidebar/modules/bluetooth.tsx
deleted file mode 100644
index 89d0cb7..0000000
--- a/src/modules/sidebar/modules/bluetooth.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import { bind, Variable } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-import AstalBluetooth from "gi://AstalBluetooth";
-
-const sortDevices = (a: AstalBluetooth.Device, b: AstalBluetooth.Device) => {
- if (a.connected || b.connected) return a.connected ? -1 : 1;
- if (a.paired || b.paired) return a.paired ? -1 : 1;
- return 0;
-};
-
-const BluetoothDevice = (device: AstalBluetooth.Device) => (
- <box className={bind(device, "connected").as(c => `device ${c ? "connected" : ""}`)}>
- <icon
- className="icon"
- icon={bind(device, "icon").as(i =>
- Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "bluetooth-symbolic"
- )}
- />
- <box vertical hexpand>
- <label truncate xalign={0} label={bind(device, "alias")} />
- <label
- truncate
- className="sublabel"
- xalign={0}
- setup={self => {
- const update = () => {
- self.label =
- (device.connected ? "Connected" : "Paired") +
- (device.batteryPercentage >= 0 ? ` (${device.batteryPercentage * 100}%)` : "");
- self.visible = device.connected || device.paired;
- };
- self.hook(device, "notify::connected", update);
- self.hook(device, "notify::paired", update);
- self.hook(device, "notify::battery-percentage", update);
- update();
- }}
- />
- </box>
- <button
- valign={Gtk.Align.CENTER}
- visible={bind(device, "paired")}
- cursor="pointer"
- onClicked={() => AstalBluetooth.get_default().adapter.remove_device(device)}
- label="delete"
- />
- <button
- valign={Gtk.Align.CENTER}
- cursor="pointer"
- onClicked={self => {
- if (device.connected)
- device.disconnect_device((_, res) => {
- self.sensitive = true;
- device.disconnect_device_finish(res);
- });
- else
- device.connect_device((_, res) => {
- self.sensitive = true;
- device.connect_device_finish(res);
- });
- self.sensitive = false;
- }}
- label={bind(device, "connected").as(c => (c ? "bluetooth_disabled" : "bluetooth_searching"))}
- />
- </box>
-);
-
-const List = ({ devNotify }: { devNotify: Variable<boolean> }) => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(devNotify).as(() => AstalBluetooth.get_default().devices.sort(sortDevices).map(BluetoothDevice))}
- </box>
-);
-
-const NoDevices = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="bluetooth_searching" />
- <label label="No Bluetooth devices" />
- </box>
- </box>
-);
-
-export default () => {
- const bluetooth = AstalBluetooth.get_default();
- const devNotify = Variable(false); // Aggregator for device state changes (connected/paired)
-
- const update = () => devNotify.set(!devNotify.get());
- const connectSignals = (device: AstalBluetooth.Device) => {
- device.connect("notify::connected", update);
- device.connect("notify::paired", update);
- };
- bluetooth.get_devices().forEach(connectSignals);
- bluetooth.connect("device-added", (_, device) => connectSignals(device));
- bluetooth.connect("notify::devices", update);
-
- return (
- <box vertical className="bluetooth">
- <box className="header-bar">
- <label
- label={bind(devNotify).as(() => {
- const nConnected = bluetooth.get_devices().filter(d => d.connected).length;
- return `${nConnected} connected device${nConnected === 1 ? "" : "s"}`;
- })}
- />
- <box hexpand />
- <button
- className={bind(bluetooth.adapter, "discovering").as(d => (d ? "enabled" : ""))}
- cursor="pointer"
- onClicked={() => {
- if (bluetooth.adapter.discovering) bluetooth.adapter.start_discovery();
- else bluetooth.adapter.stop_discovery();
- }}
- label="󰀂 Discovery"
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(bluetooth, "devices").as(d => (d.length > 0 ? "list" : "empty"))}
- >
- <NoDevices />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List devNotify={devNotify} />
- </scrollable>
- </stack>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/calendar.tsx b/src/modules/sidebar/modules/calendar.tsx
deleted file mode 100644
index bb36909..0000000
--- a/src/modules/sidebar/modules/calendar.tsx
+++ /dev/null
@@ -1,252 +0,0 @@
-import Calendar, { type IEvent } from "@/services/calendar";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, GLib, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import ical from "ical.js";
-
-const isLeapYear = (year: number) => year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0);
-
-const getMonthDays = (month: number, year: number) => {
- const leapYear = isLeapYear(year);
- if (month === 2 && leapYear) return leapYear ? 29 : 28;
- if ((month <= 7 && month % 2 === 1) || (month >= 8 && month % 2 === 0)) return 31;
- return 30;
-};
-
-const getNextMonthDays = (month: number, year: number) => {
- if (month === 12) return 31;
- return getMonthDays(month + 1, year);
-};
-
-const getPrevMonthDays = (month: number, year: number) => {
- if (month === 1) return 31;
- return getMonthDays(month - 1, year);
-};
-
-export function getCalendarLayout(date: ical.Time) {
- const weekdayOfMonthFirst = date.startOfMonth().dayOfWeek(ical.Time.MONDAY);
- const daysInMonth = getMonthDays(date.month, date.year);
- const daysInPrevMonth = getPrevMonthDays(date.month, date.year);
-
- const calendar: ical.Time[][] = [];
- let idx = -weekdayOfMonthFirst + 2;
-
- for (let i = 0; i < 6; i++) {
- calendar.push([]);
-
- for (let j = 0; j < 7; j++) {
- let cDay = idx++;
- let cMonth = date.month;
- let cYear = date.year;
-
- if (idx < 0) {
- cDay = daysInPrevMonth + cDay;
- cMonth--;
-
- if (cMonth < 0) {
- cMonth += 12;
- cYear--;
- }
- } else if (idx > daysInMonth) {
- cDay -= daysInMonth;
- cMonth++;
-
- if (cMonth > 12) {
- cMonth -= 12;
- cYear++;
- }
- }
-
- calendar[i].push(ical.Time.fromData({ day: cDay, month: cMonth, year: cYear }));
- }
- }
-
- return calendar;
-}
-
-const dateToMonthYear = (date: ical.Time) => {
- const months = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
- ];
- return `${months[date.month - 1]} ${date.year}`;
-};
-
-const addMonths = (date: ical.Time, num: number) => {
- date = date.clone();
- if (num > 0) for (let i = 0; i < num; i++) date.adjust(getNextMonthDays(date.month, date.year), 0, 0, 0);
- else for (let i = 0; i > num; i--) date.adjust(-getPrevMonthDays(date.month, date.year), 0, 0, 0);
- return date;
-};
-
-const getDayClassName = (day: ical.Time, current: Variable<ical.Time>) => {
- const isToday = day.toJSDate().toDateString() === new Date().toDateString() ? "today" : "";
- const numEvents = Math.min(5, Calendar.get_default().getEventsForDay(day).length);
- return `day ${isToday} ${day.month !== current.get().month ? "dim" : ""} events-${numEvents}`;
-};
-
-const getDayTooltip = (day: ical.Time) => {
- const events = Calendar.get_default().getEventsForDay(day);
- if (!events.length) return "";
- const eventsStr = events
- .map(e => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(e.endDate.toUnixTime());
- const sameAmPm = start.format("%P") === end.format("%P");
- const time = `${start.format(`%-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`;
- return `<b>${e.event.summary.replaceAll("&", "&amp;")}</b> • ${time}`;
- })
- .join("\n");
- return `${events.length} event${events.length === 1 ? "" : "s"}\n${eventsStr}`;
-};
-
-const getEventsHeader = (current: ical.Time) => {
- const events = Calendar.get_default().getEventsForDay(current);
- const isToday = current.toJSDate().toDateString() === new Date().toDateString();
- return (
- (isToday ? "Today • " : "") +
- GLib.DateTime.new_from_unix_local(current.toUnixTime()).format("%B %-d • %A") +
- ` • ${events.length} event${events.length === 1 ? "" : "s"}`
- );
-};
-
-const getEventHeader = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const time = `${start.format("%-I")}${start.get_minute() > 0 ? `:${start.get_minute()}` : ""}${start.format("%P")}`;
- return `${time} <b>${e.event.summary.replaceAll("&", "&amp;")}</b>`;
-};
-
-const getEventTooltip = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(e.event.endDate.toUnixTime());
- const sameAmPm = start.format("%P") === end.format("%P");
- const time = `${start.format(`%A, %-d %B • %-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`;
- const locIfExists = e.event.location ? ` ${e.event.location}\n` : "";
- const descIfExists = e.event.description ? `󰒿 ${e.event.description}\n` : "";
- return `<b>${e.event.summary}</b>\n${time}\n${locIfExists}${descIfExists}󰃭 ${e.calendar}`.replaceAll("&", "&amp;");
-};
-
-const Day = ({ day, shown, current }: { day: ical.Time; shown: Variable<string>; current: Variable<ical.Time> }) => (
- <button
- className={bind(Calendar.get_default(), "calendars").as(() => getDayClassName(day, current))}
- cursor="pointer"
- onClicked={() => {
- shown.set("events");
- current.set(day);
- }}
- setup={self =>
- setupCustomTooltip(
- self,
- bind(Calendar.get_default(), "calendars").as(() => getDayTooltip(day)),
- { useMarkup: true }
- )
- }
- >
- <box vertical>
- <label label={day.day.toString()} />
- <box className="indicator" />
- </box>
- </button>
-);
-
-const CalendarView = ({ shown, current }: { shown: Variable<string>; current: Variable<ical.Time> }) => (
- <box vertical className="calendar-view" name="calendar">
- <box className="header">
- <button
- cursor="pointer"
- onClicked={() => current.set(ical.Time.now())}
- label={bind(current).as(dateToMonthYear)}
- />
- <box hexpand />
- <button cursor="pointer" onClicked={() => current.set(addMonths(current.get(), -1))} label="" />
- <button cursor="pointer" onClicked={() => current.set(addMonths(current.get(), 1))} label="" />
- </box>
- <box halign={Gtk.Align.CENTER} className="weekdays">
- {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(d => (
- <label label={d} />
- ))}
- </box>
- <box vertical halign={Gtk.Align.CENTER} className="month">
- {bind(current).as(c =>
- getCalendarLayout(c).map(r => (
- <box className="week">
- {r.map(d => (
- <Day day={d} shown={shown} current={current} />
- ))}
- </box>
- ))
- )}
- </box>
- </box>
-);
-
-const Event = (event: IEvent) => (
- <box className="event" setup={self => setupCustomTooltip(self, getEventTooltip(event), { useMarkup: true })}>
- <box className={`calendar-indicator calendar-${Calendar.get_default().getCalendarIndex(event.calendar)}`} />
- <box vertical>
- <label truncate useMarkup xalign={0} label={getEventHeader(event)} />
- {event.event.location && <label truncate xalign={0} label={event.event.location} className="sublabel" />}
- {event.event.description && (
- <label truncate useMarkup xalign={0} label={event.event.description} className="sublabel" />
- )}
- </box>
- </box>
-);
-
-const List = ({ current }: { current: Variable<ical.Time> }) => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(current).as(c => Calendar.get_default().getEventsForDay(c).map(Event))}
- </box>
-);
-
-const NoEvents = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="calendar_month" />
- <label label="Day all clear!" />
- </box>
- </box>
-);
-
-const Events = ({ shown, current }: { shown: Variable<string>; current: Variable<ical.Time> }) => (
- <box vertical className="events" name="events">
- <box className="header">
- <button cursor="pointer" onClicked={() => shown.set("calendar")} label="" />
- <label hexpand truncate xalign={0} label={bind(current).as(getEventsHeader)} />
- </box>
- <stack shown={bind(current).as(c => (Calendar.get_default().getEventsForDay(c).length > 0 ? "list" : "empty"))}>
- <NoEvents />
- <scrollable hscroll={Gtk.PolicyType.NEVER} name="list">
- <List current={current} />
- </scrollable>
- </stack>
- </box>
-);
-
-export default () => {
- const shown = Variable<"calendar" | "events">("calendar");
- const current = Variable(ical.Time.now());
-
- return (
- <box vertical className="calendar">
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
- transitionDuration={150}
- shown={bind(shown)}
- >
- <CalendarView shown={shown} current={current} />
- <Events shown={shown} current={current} />
- </stack>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/deviceselector.tsx b/src/modules/sidebar/modules/deviceselector.tsx
deleted file mode 100644
index e74e6f5..0000000
--- a/src/modules/sidebar/modules/deviceselector.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import { bind, execAsync, Variable, type Binding } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-import AstalWp from "gi://AstalWp";
-
-const Device = ({
- input,
- defaultDevice,
- showDropdown,
- device,
-}: {
- input?: boolean;
- defaultDevice: Binding<AstalWp.Endpoint>;
- showDropdown: Variable<boolean>;
- device: AstalWp.Endpoint;
-}) => (
- <button
- visible={defaultDevice.get().id !== device.id}
- cursor="pointer"
- onClicked={() => {
- execAsync(`wpctl set-default ${device.id}`).catch(console.error);
- showDropdown.set(false);
- }}
- setup={self => {
- let last: { d: AstalWp.Endpoint; id: number } | null = {
- d: defaultDevice.get(),
- id: defaultDevice
- .get()
- .connect("notify::id", () => self.set_visible(defaultDevice.get().id !== device.id)),
- };
- self.hook(defaultDevice, (_, d) => {
- last?.d.disconnect(last.id);
- self.set_visible(d.id !== device.id);
- last = {
- d,
- id: d.connect("notify::id", () => self.set_visible(d.id !== device.id)),
- };
- });
- self.connect("destroy", () => last?.d.disconnect(last.id));
- }}
- >
- <box className="device">
- {bind(device, "icon").as(i =>
- Astal.Icon.lookup_icon(i) ? (
- <icon className="icon" icon={device.icon} />
- ) : (
- <label className="icon" label={input ? "mic" : "media_output"} />
- )
- )}
- <label truncate label={bind(device, "description")} />
- </box>
- </button>
-);
-
-const DefaultDevice = ({ input, device }: { input?: boolean; device: AstalWp.Endpoint }) => (
- <box className="selected">
- <label className="icon" label={input ? "mic" : "media_output"} />
- <box vertical>
- <label
- truncate
- xalign={0}
- label={bind(device, "description").as(d => (input ? "[In] " : "[Out] ") + (d ?? "Unknown"))}
- />
- <label
- xalign={0}
- className="sublabel"
- label={bind(device, "volume").as(v => `Volume ${Math.round(v * 100)}%`)}
- />
- </box>
- </box>
-);
-
-const Selector = ({ input, audio }: { input?: boolean; audio: AstalWp.Audio }) => {
- const showDropdown = Variable(false);
- const defaultDevice = bind(audio, input ? "defaultMicrophone" : "defaultSpeaker");
-
- return (
- <box vertical className="selector">
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_UP}
- transitionDuration={150}
- revealChild={bind(showDropdown)}
- >
- <box vertical className="list">
- {bind(audio, input ? "microphones" : "speakers").as(ds =>
- ds.map(d => (
- <Device
- input={input}
- defaultDevice={defaultDevice}
- showDropdown={showDropdown}
- device={d}
- />
- ))
- )}
- <box className="separator" />
- </box>
- </revealer>
- <button cursor="pointer" onClick={() => showDropdown.set(!showDropdown.get())}>
- {defaultDevice.as(d => (
- <DefaultDevice input={input} device={d} />
- ))}
- </button>
- </box>
- );
-};
-
-const NoWp = () => (
- <box homogeneous>
- <box vertical valign={Gtk.Align.CENTER}>
- <label label="Device selector unavailable" />
- <label className="no-wp-prompt" label="WirePlumber is required for this module" />
- </box>
- </box>
-);
-
-export default () => {
- const audio = AstalWp.get_default()?.get_audio();
-
- if (!audio) return <NoWp />;
-
- return (
- <box vertical className="device-selector">
- <Selector input audio={audio} />
- <Selector audio={audio} />
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/headlines.tsx b/src/modules/sidebar/modules/headlines.tsx
deleted file mode 100644
index 40d468b..0000000
--- a/src/modules/sidebar/modules/headlines.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import News, { type IArticle } from "@/services/news";
-import Palette, { type IPalette } from "@/services/palette";
-import { capitalize } from "@/utils/strings";
-import { bind, execAsync, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import { sidebar } from "config";
-import { setConfig } from "config/funcs";
-
-const fixGoogleNews = (colours: IPalette, title: string, desc: string) => {
- // Add separator, bold and split at domain (domain is at the end of each headline)
- const domain = title.split(" - ").at(-1);
- if (domain) desc = desc.replaceAll(domain, `— <span foreground="${colours.subtext0}">${domain}</span>\n\n`);
- // Split headlines
- desc = desc.replace(/(( |\.)[^A-Z][a-z]+)([A-Z])/g, "$1\n\n$3");
- desc = desc.replace(/( [A-Z]+)([A-Z](?![s])[a-z])/g, "$1\n\n$2");
- // Add separator and bold domains
- desc = desc.replace(/ ([a-zA-Z.]+)\n\n/g, ` — <span foreground="${colours.subtext0}">$1</span>\n\n`);
- desc = desc.replace(/ ([a-zA-Z.]+)$/, ` — <span foreground="${colours.subtext0}">$1</span>`); // Last domain
- return desc.trim();
-};
-
-const fixNews = (colours: IPalette, title: string, desc: string, source: string) => {
- // Add spaces between sentences
- desc = desc.replace(/\.([A-Z])/g, ". $1");
- // Google News needs some other fixes
- if (source === "Google News") desc = fixGoogleNews(colours, title, desc);
- return desc.replaceAll("&", "&amp;");
-};
-
-const getCategoryIcon = (category: string) => {
- if (category === "business") return "monitoring";
- if (category === "crime") return "speed_camera";
- if (category === "domestic") return "home";
- if (category === "education") return "school";
- if (category === "entertainment") return "tv";
- if (category === "environment") return "eco";
- if (category === "food") return "restaurant";
- if (category === "health") return "health_and_safety";
- if (category === "lifestyle") return "digital_wellbeing";
- if (category === "politics") return "account_balance";
- if (category === "science") return "science";
- if (category === "sports") return "sports_basketball";
- if (category === "technology") return "account_tree";
- if (category === "top") return "breaking_news";
- if (category === "tourism") return "travel";
- if (category === "world") return "public";
- return "newsmode";
-};
-
-const Article = ({ title, description, creator, pubDate, source_name, link }: IArticle) => {
- const expanded = Variable(false);
-
- return (
- <box vertical className="article">
- <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
- <box hexpand className="header">
- <box vertical>
- <label truncate xalign={0} label={title} />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={source_name + (creator ? ` (${creator.join(", ")})` : "")}
- />
- </box>
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <button onClicked={() => execAsync(`app2unit -O -- ${link}`)}>
- <box vertical className="article-body">
- <label wrap className="title" xalign={0} label={title} />
- <label wrap xalign={0} label={`Published on ${new Date(pubDate).toLocaleString()}`} />
- <label
- wrap
- xalign={0}
- className="sublabel"
- label={`By ${
- creator?.join(", ") ??
- (source_name === "Google News" ? title.split(" - ").at(-1) : source_name)
- }`}
- />
- {description && (
- <label
- wrap
- useMarkup
- xalign={0}
- label={bind(Palette.get_default(), "colours").as(c =>
- fixNews(c, title, description, source_name)
- )}
- />
- )}
- </box>
- </button>
- </revealer>
- </box>
- );
-};
-
-const Category = ({ title, articles }: { title: string; articles: IArticle[] }) => {
- const expanded = Variable(false);
-
- return (
- <box vertical className="category">
- <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
- <box className="header">
- <label className="icon" label={getCategoryIcon(title)} />
- <label label={`${capitalize(title)} (${articles.length})`} />
- <box hexpand />
- <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <box vertical className="body">
- {articles
- .sort((a, b) => a.source_priority - b.source_priority)
- .map(a => (
- <Article {...a} />
- ))}
- </box>
- </revealer>
- </box>
- );
-};
-
-const List = () => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(News.get_default(), "categories").as(c =>
- Object.entries(c).map(([k, v]) => <Category title={k} articles={v} />)
- )}
- </box>
-);
-
-const NoNews = ({ disabled }: { disabled?: boolean }) => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="full_coverage" />
- <label label={disabled ? "Headlines disabled" : "No news headlines!"} />
- </box>
- </box>
-);
-
-const HeadlinesDisabled = () => (
- <>
- <box vertical className="headlines">
- <box className="header-bar">
- <label label="Top news headlines" />
- <box hexpand />
- <button
- cursor="pointer"
- onClicked={() => setConfig("sidebar.modules.headlines.enabled", true)}
- label="󰞉 Enable"
- />
- </box>
- <NoNews disabled />
- </box>
- </>
-);
-
-const Headlines = ({ monitor }: { monitor: Monitor }) => (
- <>
- <box className="header-bar">
- <label label="Top news headlines" />
- <box hexpand />
- <button
- className={bind(News.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(News.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => News.get_default().getNews()}
- label={bind(News.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(News.get_default(), "articles").as(a => (a.length > 0 ? "list" : "empty"))}
- >
- <NoNews />
- <scrollable
- css={bind(News.get_default(), "articles").as(a =>
- a.length > 0 ? `min-height: ${Math.round(monitor.height * 0.4)}px;` : ""
- )}
- hscroll={Gtk.PolicyType.NEVER}
- name="list"
- >
- <List />
- </scrollable>
- </stack>
- </>
-);
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="headlines">
- {bind(sidebar.modules.headlines.enabled).as(e => (e ? <Headlines monitor={monitor} /> : <HeadlinesDisabled />))}
- </box>
-);
diff --git a/src/modules/sidebar/modules/hwresources.tsx b/src/modules/sidebar/modules/hwresources.tsx
deleted file mode 100644
index 768d8bd..0000000
--- a/src/modules/sidebar/modules/hwresources.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import Cpu from "@/services/cpu";
-import Gpu from "@/services/gpu";
-import Memory from "@/services/memory";
-import Storage from "@/services/storage";
-import Slider from "@/widgets/slider";
-import { bind, type Binding } from "astal";
-import { Gtk, type Widget } from "astal/gtk3";
-
-const fmt = (bytes: number, pow: number) => +(bytes / 1024 ** pow).toFixed(2);
-const format = ({ total, used }: { total: number; used: number }) => {
- if (total >= 1024 ** 4) return `${fmt(used, 4)}/${fmt(total, 4)} TiB`;
- if (total >= 1024 ** 3) return `${fmt(used, 3)}/${fmt(total, 3)} GiB`;
- if (total >= 1024 ** 2) return `${fmt(used, 2)}/${fmt(total, 2)} MiB`;
- if (total >= 1024) return `${fmt(used, 1)}/${fmt(total, 1)} KiB`;
- return `${used}/${total} B`;
-};
-
-const Resource = ({
- icon,
- name,
- value,
- labelSetup,
-}: {
- icon: string;
- name: string;
- value: Binding<number>;
- labelSetup?: (self: Widget.Label) => void;
-}) => (
- <box vertical className={`resource ${name}`}>
- <box className="inner">
- <label label={icon} />
- <Slider value={value.as(v => v / 100)} />
- </box>
- <label halign={Gtk.Align.END} label={labelSetup ? "" : value.as(v => `${+v.toFixed(2)}%`)} setup={labelSetup} />
- </box>
-);
-
-export default () => (
- <box vertical className="hw-resources">
- {Gpu.get_default().available && <Resource icon="󰢮" name="gpu" value={bind(Gpu.get_default(), "usage")} />}
- <Resource icon="" name="cpu" value={bind(Cpu.get_default(), "usage")} />
- <Resource
- icon=""
- name="memory"
- value={bind(Memory.get_default(), "usage")}
- labelSetup={self => {
- const mem = Memory.get_default();
- const update = () => (self.label = format(mem));
- self.hook(mem, "notify::used", update);
- self.hook(mem, "notify::total", update);
- update();
- }}
- />
- <Resource
- icon="󰋊"
- name="storage"
- value={bind(Storage.get_default(), "usage")}
- labelSetup={self => {
- const storage = Storage.get_default();
- const update = () => (self.label = format(storage));
- self.hook(storage, "notify::used", update);
- self.hook(storage, "notify::total", update);
- update();
- }}
- />
- </box>
-);
diff --git a/src/modules/sidebar/modules/media.tsx b/src/modules/sidebar/modules/media.tsx
deleted file mode 100644
index 169a98d..0000000
--- a/src/modules/sidebar/modules/media.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import Players from "@/services/players";
-import { lengthStr } from "@/utils/strings";
-import Slider from "@/widgets/slider";
-import { bind, timeout, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalMpris from "gi://AstalMpris";
-
-const noNull = (s: string | null) => s ?? "-";
-
-const NoMedia = () => (
- <box vertical className="player" name="none">
- <box homogeneous halign={Gtk.Align.CENTER} className="cover-art">
- <label xalign={0.4} label="" />
- </box>
- <box vertical className="progress">
- <Slider value={bind(Variable(0))} />
- <box className="time">
- <label label="-1:-1" />
- <box hexpand />
- <label label="-1:-1" />
- </box>
- </box>
- <box vertical className="details">
- <label truncate className="title" label="No media" />
- <label truncate className="artist" label="Try play some music!" />
- <label truncate className="album" label="" />
- </box>
- <box vertical className="controls">
- <box halign={Gtk.Align.CENTER} className="playback">
- <button sensitive={false} cursor="pointer" label="󰒮" />
- <button sensitive={false} cursor="pointer" label="󰐊" />
- <button sensitive={false} cursor="pointer" label="󰒭" />
- </box>
- <box className="options">
- <button sensitive={false} cursor="pointer" label="󰊓" />
- <button sensitive={false} cursor="pointer" label="󰒞" />
- <box hexpand />
- <button className="needs-adjustment" sensitive={false} cursor="pointer" label="󰑗" />
- <button className="needs-adjustment" sensitive={false} cursor="pointer" label="󰀽" />
- </box>
- </box>
- </box>
-);
-
-const Player = ({ player }: { player: AstalMpris.Player }) => {
- const position = Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l);
-
- return (
- <box vertical className="player" name={player.busName} onDestroy={() => position.drop()}>
- <box
- homogeneous
- halign={Gtk.Align.CENTER}
- className="cover-art"
- css={bind(player, "coverArt").as(a => `background-image: url("${a}");`)}
- >
- {bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.4} label="" />))}
- </box>
- <box vertical className="progress">
- <Slider value={bind(position)} onChange={(_, v) => player.set_position(v * player.length)} />
- <box className="time">
- <label label={bind(player, "position").as(lengthStr)} />
- <box hexpand />
- <label label={bind(player, "length").as(lengthStr)} />
- </box>
- </box>
- <box vertical className="details">
- <label truncate className="title" label={bind(player, "title").as(noNull)} />
- <label truncate className="artist" label={bind(player, "artist").as(noNull)} />
- <label truncate className="album" label={bind(player, "album").as(noNull)} />
- </box>
- <box vertical className="controls">
- <box halign={Gtk.Align.CENTER} className="playback">
- <button
- sensitive={bind(player, "canGoPrevious")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="󰒮"
- />
- <button
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.play_pause()}
- label={bind(player, "playbackStatus").as(s =>
- s === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"
- )}
- />
- <button
- sensitive={bind(player, "canGoNext")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="󰒭"
- />
- </box>
- <box className="options">
- <button
- sensitive={bind(player, "canSetFullscreen")}
- cursor="pointer"
- onClicked={() => player.toggle_fullscreen()}
- label={bind(player, "fullscreen").as(f => (f ? "󰊔" : "󰊓"))}
- />
- <button
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.shuffle()}
- label={bind(player, "shuffleStatus").as(s => (s === AstalMpris.Shuffle.ON ? "󰒝" : "󰒞"))}
- />
- <box hexpand />
- <button
- className="needs-adjustment"
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.loop()}
- label={bind(player, "loopStatus").as(l =>
- l === AstalMpris.Loop.TRACK ? "󰑘" : l === AstalMpris.Loop.PLAYLIST ? "󰑖" : "󰑗"
- )}
- />
- <button
- className="needs-adjustment"
- sensitive={bind(player, "canRaise")}
- cursor="pointer"
- onClicked={() => player.raise()}
- label="󰀽"
- />
- </box>
- </box>
- </box>
- );
-};
-
-const Indicator = ({ active, player }: { active: Variable<string>; player: AstalMpris.Player }) => (
- <button
- className={bind(active).as(a => (a === player.busName ? "active" : ""))}
- cursor="pointer"
- onClicked={() => active.set(player.busName)}
- />
-);
-
-export default () => {
- const players = Players.get_default();
- const active = Variable(players.lastPlayer?.busName ?? "none");
-
- active.observe(players, "notify::list", () => {
- timeout(10, () => active.set(players.lastPlayer?.busName ?? "none"));
- return "none";
- });
-
- return (
- <box vertical className="players" onDestroy={() => active.drop()}>
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
- transitionDuration={150}
- shown={bind(active)}
- >
- <NoMedia />
- {bind(players, "list").as(ps => ps.map(p => <Player player={p} />))}
- </stack>
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={120}
- revealChild={bind(players, "list").as(l => l.length > 1)}
- >
- <box halign={Gtk.Align.CENTER} className="indicators">
- {bind(players, "list").as(ps => ps.map(p => <Indicator active={active} player={p} />))}
- </box>
- </revealer>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/networks.tsx b/src/modules/sidebar/modules/networks.tsx
deleted file mode 100644
index f98a62c..0000000
--- a/src/modules/sidebar/modules/networks.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { bind, execAsync, Variable, type Binding } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalNetwork from "gi://AstalNetwork";
-
-const sortAPs = (saved: string[], a: AstalNetwork.AccessPoint, b: AstalNetwork.AccessPoint) => {
- const { wifi } = AstalNetwork.get_default();
- if (a === wifi.activeAccessPoint || b === wifi.activeAccessPoint) return a === wifi.activeAccessPoint ? -1 : 1;
- if (saved.includes(a.ssid) || saved.includes(b.ssid)) return saved.includes(a.ssid) ? -1 : 1;
- return b.strength - a.strength;
-};
-
-const Network = (accessPoint: AstalNetwork.AccessPoint) => (
- <box
- className={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(
- a => `network ${a === accessPoint ? "connected" : ""}`
- )}
- >
- <icon className="icon" icon={bind(accessPoint, "iconName")} />
- <box vertical hexpand>
- <label truncate xalign={0} label={bind(accessPoint, "ssid").as(s => s ?? "Unknown")} />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={bind(accessPoint, "strength").as(s => `${accessPoint.frequency > 5000 ? 5 : 2.4}GHz • ${s}/100`)}
- />
- </box>
- <box hexpand />
- <button
- valign={Gtk.Align.CENTER}
- visible={false}
- cursor="pointer"
- onClicked={() => execAsync(`nmcli c delete id '${accessPoint.ssid}'`).catch(console.error)}
- label="delete_forever"
- setup={self => {
- let destroyed = false;
- execAsync(`fish -c "nmcli -t -f name,type c show | sed -nE 's/(.*)\\:.*wireless/\\1/p'"`)
- .then(out => !destroyed && (self.visible = out.split("\n").includes(accessPoint.ssid)))
- .catch(console.error);
- self.connect("destroy", () => (destroyed = true));
- }}
- />
- <button
- valign={Gtk.Align.CENTER}
- cursor="pointer"
- onClicked={self => {
- let destroyed = false;
- const id = self.connect("destroy", () => (destroyed = true));
- const cmd =
- AstalNetwork.get_default().wifi.activeAccessPoint === accessPoint ? "c down id" : "d wifi connect";
- execAsync(`nmcli ${cmd} '${accessPoint.ssid}'`)
- .then(() => {
- if (!destroyed) {
- self.sensitive = true;
- self.disconnect(id);
- }
- })
- .catch(console.error);
- self.sensitive = false;
- }}
- label={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(a =>
- a === accessPoint ? "wifi_off" : "wifi"
- )}
- />
- </box>
-);
-
-const List = () => {
- const { wifi } = AstalNetwork.get_default();
- const children = Variable<JSX.Element[]>([]);
-
- const update = async () => {
- const out = await execAsync(`fish -c "nmcli -t -f name,type c show | sed -nE 's/(.*)\\:.*wireless/\\1/p'"`);
- const saved = out.split("\n");
- const aps = wifi.accessPoints
- .filter(a => a.ssid)
- .sort((a, b) => sortAPs(saved, a, b))
- .map(Network);
- children.set(aps);
- };
-
- wifi.connect("notify::active-access-point", () => update().catch(console.error));
- wifi.connect("notify::access-points", () => update().catch(console.error));
- update().catch(console.error);
-
- return (
- <box vertical valign={Gtk.Align.START} className="list" onDestroy={() => children.drop()}>
- {bind(children)}
- </box>
- );
-};
-
-const NoNetworks = ({ label }: { label: Binding<string> | string }) => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="wifi_off" />
- <label label={label} />
- </box>
- </box>
-);
-
-export default () => {
- const network = AstalNetwork.get_default();
- const label = Variable("");
-
- const update = () => {
- if (network.primary === AstalNetwork.Primary.WIFI) label.set(network.wifi.ssid ?? "Disconnected");
- else if (network.primary === AstalNetwork.Primary.WIRED) label.set(`Ethernet (${network.wired.speed})`);
- else label.set("No Wifi");
- };
- network.connect("notify::primary", update);
- network.get_wifi()?.connect("notify::ssid", update);
- network.get_wired()?.connect("notify::speed", update);
- update();
-
- return (
- <box vertical className="networks">
- <box className="header-bar">
- <label label={bind(label)} />
- <box hexpand />
- <button
- sensitive={network.get_wifi() ? bind(network.wifi, "scanning").as(e => !e) : false}
- className={network.get_wifi() ? bind(network.wifi, "scanning").as(s => (s ? "enabled" : "")) : ""}
- cursor="pointer"
- onClicked={() => network.get_wifi()?.scan()}
- label={
- network.get_wifi()
- ? bind(network.wifi, "scanning").as(s => (s ? "󰀂 Scanning" : "󰀂 Scan"))
- : "󰀂 Scan"
- }
- />
- </box>
- {network.get_wifi() ? (
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(network.wifi, "accessPoints").as(a => (a.length > 0 ? "list" : "empty"))}
- >
- <NoNetworks
- label={bind(network.wifi, "enabled").as(p => (p ? "No available networks" : "Wifi is off"))}
- />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List />
- </scrollable>
- </stack>
- ) : (
- <NoNetworks label="Wifi not available" />
- )}
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/news.tsx b/src/modules/sidebar/modules/news.tsx
deleted file mode 100644
index c799757..0000000
--- a/src/modules/sidebar/modules/news.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Palette from "@/services/palette";
-import Updates from "@/services/updates";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-
-const countNews = (news: string) => news.match(/^([0-9]{4}-[0-9]{2}-[0-9]{2} .+)$/gm)?.length ?? 0;
-
-const News = ({ header, body }: { header: string; body: string }) => {
- const expanded = Variable(false);
-
- body = body
- .slice(0, -5) // Remove last unopened \x1b[0m
- .replaceAll("\x1b[0m", "</span>"); // Replace reset code with end span
-
- return (
- <box vertical className="article">
- <button
- className="wrapper"
- cursor="pointer"
- onClicked={() => expanded.set(!expanded.get())}
- setup={self => setupCustomTooltip(self, header)}
- >
- <box hexpand className="header">
- <label className="icon" label="newspaper" />
- <box vertical>
- <label xalign={0} label={header.split(" ")[0]} />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={header.replace(/[0-9]{4}-[0-9]{2}-[0-9]{2} /, "")}
- />
- </box>
- <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <label
- wrap
- useMarkup
- xalign={0}
- className="body"
- label={bind(Palette.get_default(), "teal").as(
- c => body.replaceAll("\x1b[36m", `<span foreground="${c}">`) // Replace colour codes with html spans
- )}
- />
- </revealer>
- </box>
- );
-};
-
-const List = () => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(Updates.get_default(), "news").as(n => {
- const children = [];
- const news = n.split(/^([0-9]{4}-[0-9]{2}-[0-9]{2} .+)$/gm);
- for (let i = 1; i < news.length - 1; i += 2)
- children.push(<News header={news[i].trim()} body={news[i + 1].trim()} />);
- return children;
- })}
- </box>
-);
-
-const NoNews = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="breaking_news" />
- <label label="No Arch news!" />
- </box>
- </box>
-);
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="news">
- <box className="header-bar">
- <label
- label={bind(Updates.get_default(), "news")
- .as(countNews)
- .as(n => `${n} news article${n === 1 ? "" : "s"}`)}
- />
- <box hexpand />
- <button
- className={bind(Updates.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(Updates.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => Updates.get_default().getUpdates()}
- label={bind(Updates.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(Updates.get_default(), "news").as(n => (n ? "list" : "empty"))}
- >
- <NoNews />
- <scrollable
- css={bind(Updates.get_default(), "news").as(n =>
- n ? `min-height: ${Math.round(monitor.height * 0.4)}px;` : ""
- )}
- hscroll={Gtk.PolicyType.NEVER}
- name="list"
- >
- <List />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/modules/notifications.tsx b/src/modules/sidebar/modules/notifications.tsx
deleted file mode 100644
index e9347ec..0000000
--- a/src/modules/sidebar/modules/notifications.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import Notification from "@/widgets/notification";
-import { bind } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-import AstalNotifd from "gi://AstalNotifd";
-
-const List = ({ compact }: { compact?: boolean }) => (
- <box
- vertical
- valign={Gtk.Align.START}
- className="list"
- setup={self => {
- const notifd = AstalNotifd.get_default();
- const map = new Map<number, Notification>();
-
- const addNotification = (notification: AstalNotifd.Notification) => {
- const notif = (<Notification notification={notification} compact={compact} />) as Notification;
- notif.connect("destroy", () => map.get(notification.id) === notif && map.delete(notification.id));
- map.get(notification.id)?.destroyWithAnims();
- map.set(notification.id, notif);
-
- const widget = (
- <eventbox
- // Dismiss on middle click
- onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
- setup={self => self.hook(notif, "destroy", () => self.destroy())}
- >
- {notif}
- </eventbox>
- );
-
- self.pack_end(widget, false, false, 0);
- };
-
- notifd
- .get_notifications()
- .sort((a, b) => a.time - b.time)
- .forEach(addNotification);
-
- self.hook(notifd, "notified", (_, id) => addNotification(notifd.get_notification(id)));
- self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
- }}
- />
-);
-
-const NoNotifs = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="mark_email_unread" />
- <label label="All caught up!" />
- </box>
- </box>
-);
-
-export default ({ compact }: { compact?: boolean }) => (
- <box vertical className="notifications">
- <box className="header-bar">
- <label
- label={bind(AstalNotifd.get_default(), "notifications").as(
- n => `${n.length} notification${n.length === 1 ? "" : "s"}`
- )}
- />
- <box hexpand />
- <button
- className={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "enabled" : ""))}
- cursor="pointer"
- onClicked={() => (AstalNotifd.get_default().dontDisturb = !AstalNotifd.get_default().dontDisturb)}
- label="󰂛 Silence"
- />
- <button
- cursor="pointer"
- onClicked={() =>
- AstalNotifd.get_default()
- .get_notifications()
- .forEach(n => n.dismiss())
- }
- label="󰎟 Clear"
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(AstalNotifd.get_default(), "notifications").as(n => (n.length > 0 ? "list" : "empty"))}
- >
- <NoNotifs />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List compact={compact} />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/modules/streams.tsx b/src/modules/sidebar/modules/streams.tsx
deleted file mode 100644
index 18a9a58..0000000
--- a/src/modules/sidebar/modules/streams.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { bind, execAsync, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalWp from "gi://AstalWp";
-
-interface IStream {
- stream: AstalWp.Endpoint;
- playing: boolean;
-}
-
-const header = (audio: AstalWp.Audio, key: "streams" | "speakers" | "recorders") =>
- `${audio[key].length} ${audio[key].length === 1 ? key.slice(0, -1) : key}`;
-
-const sortStreams = (a: IStream, b: IStream) => {
- if (a.playing || b.playing) return a.playing ? -1 : 1;
- return 0;
-};
-
-const Stream = ({ stream, playing }: IStream) => (
- <box className={`stream ${playing ? "playing" : ""}`}>
- <icon className="icon" icon={bind(stream, "icon")} />
- <box vertical hexpand>
- <label truncate xalign={0} label={bind(stream, "name")} />
- <label truncate xalign={0} className="sublabel" label={bind(stream, "description")} />
- </box>
- <button valign={Gtk.Align.CENTER} cursor="pointer" onClicked={() => (stream.volume -= 0.05)} label="-" />
- <slider
- showFillLevel
- restrictToFillLevel={false}
- fillLevel={2 / 3}
- value={bind(stream, "volume").as(v => v * (2 / 3))}
- setup={self => self.connect("value-changed", () => stream.set_volume(self.value * 1.5))}
- />
- <button valign={Gtk.Align.CENTER} cursor="pointer" onClicked={() => (stream.volume += 0.05)} label="+" />
- </box>
-);
-
-const List = ({ audio }: { audio: AstalWp.Audio }) => {
- const streams = Variable<IStream[]>([]);
-
- const update = async () => {
- const paStreams = JSON.parse(await execAsync("pactl -f json list sink-inputs"));
- streams.set(
- audio.streams.map(s => ({
- stream: s,
- playing: paStreams.find((p: any) => p.properties["object.serial"] == s.serial)?.corked === false,
- }))
- );
- };
-
- streams.watch("pactl -f json subscribe", out => {
- if (JSON.parse(out).on === "sink-input") update().catch(console.error);
- return streams.get();
- });
- audio.connect("notify::streams", () => update().catch(console.error));
-
- return (
- <box vertical valign={Gtk.Align.START} className="list" onDestroy={() => streams.drop()}>
- {bind(streams).as(ps => ps.sort(sortStreams).map(s => <Stream stream={s.stream} playing={s.playing} />))}
- </box>
- );
-};
-
-const NoSources = ({ icon, label }: { icon: string; label: string }) => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label={icon} />
- <label label={label} />
- </box>
- </box>
-);
-
-const NoWp = () => (
- <box vexpand homogeneous>
- <box vertical valign={Gtk.Align.CENTER}>
- <NoSources icon="no_sound" label="Streams module unavailable" />
- <label className="no-wp-prompt" label="WirePlumber is required for this module" />
- </box>
- </box>
-);
-
-export default () => {
- const audio = AstalWp.get_default()?.get_audio();
-
- if (!audio) return <NoWp />;
-
- const label = Variable(`${header(audio, "streams")} • ${header(audio, "recorders")}`);
-
- label.observe(
- ["streams", "recorders"].map(k => [audio, `notify::${k}`]),
- () => `${header(audio, "streams")} • ${header(audio, "recorders")}`
- );
-
- return (
- <box vertical className="streams" onDestroy={() => label.drop()}>
- <box halign={Gtk.Align.CENTER} className="header-bar">
- <label label={bind(label)} />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(audio, "streams").as(s => (s.length > 0 ? "list" : "empty"))}
- >
- <NoSources icon="stream" label="No audio sources" />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List audio={audio} />
- </scrollable>
- </stack>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/upcoming.tsx b/src/modules/sidebar/modules/upcoming.tsx
deleted file mode 100644
index a64e051..0000000
--- a/src/modules/sidebar/modules/upcoming.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import Calendar, { type IEvent } from "@/services/calendar";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, GLib } from "astal";
-import { Gtk } from "astal/gtk3";
-
-const getDateHeader = (events: IEvent[]) => {
- const date = events[0].startDate;
- const isToday = date.toJSDate().toDateString() === new Date().toDateString();
- return (
- (isToday ? "Today • " : "") +
- GLib.DateTime.new_from_unix_local(date.toUnixTime()).format("%B %-d • %A") +
- ` • ${events.length} event${events.length === 1 ? "" : "s"}`
- );
-};
-
-const getEventHeader = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const time = `${start.format("%-I")}${start.get_minute() > 0 ? `:${start.get_minute()}` : ""}${start.format("%P")}`;
- return `${time} <b>${e.event.summary.replaceAll("&", "&amp;")}</b>`;
-};
-
-const getEventTooltip = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(e.event.endDate.toUnixTime());
- const sameAmPm = start.format("%P") === end.format("%P");
- const time = `${start.format(`%A, %-d %B • %-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`;
- const locIfExists = e.event.location ? ` ${e.event.location}\n` : "";
- const descIfExists = e.event.description ? `󰒿 ${e.event.description}\n` : "";
- return `<b>${e.event.summary}</b>\n${time}\n${locIfExists}${descIfExists}󰃭 ${e.calendar}`.replaceAll("&", "&amp;");
-};
-
-const Event = (event: IEvent) => (
- <box className="event" setup={self => setupCustomTooltip(self, getEventTooltip(event), { useMarkup: true })}>
- <box className={`calendar-indicator calendar-${Calendar.get_default().getCalendarIndex(event.calendar)}`} />
- <box vertical>
- <label truncate useMarkup xalign={0} label={getEventHeader(event)} />
- {event.event.location && <label truncate xalign={0} label={event.event.location} className="sublabel" />}
- {event.event.description && (
- <label truncate useMarkup xalign={0} label={event.event.description} className="sublabel" />
- )}
- </box>
- </box>
-);
-
-const Day = ({ events }: { events: IEvent[] }) => (
- <box vertical className="day">
- <label className="date" xalign={0} label={getDateHeader(events)} />
- <box vertical className="events">
- {events.map(Event)}
- </box>
- </box>
-);
-
-const List = () => (
- <box vertical valign={Gtk.Align.START}>
- {bind(Calendar.get_default(), "upcoming").as(u =>
- Object.values(u)
- .sort((a, b) => a[0].startDate.compare(b[0].startDate))
- .map(e => <Day events={e} />)
- )}
- </box>
-);
-
-const NoEvents = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="calendar_month" />
- <label label="No upcoming events" />
- </box>
- </box>
-);
-
-export default () => (
- <box vertical className="upcoming">
- <box className="header-bar">
- <label
- label={bind(Calendar.get_default(), "numUpcoming").as(n => `${n} upcoming event${n === 1 ? "" : "s"}`)}
- />
- <box hexpand />
- <button
- className={bind(Calendar.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(Calendar.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => Calendar.get_default().updateCalendars().catch(console.error)}
- label={bind(Calendar.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(Calendar.get_default(), "numUpcoming").as(n => (n > 0 ? "list" : "empty"))}
- >
- <NoEvents />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/modules/updates.tsx b/src/modules/sidebar/modules/updates.tsx
deleted file mode 100644
index e58d848..0000000
--- a/src/modules/sidebar/modules/updates.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import Palette from "@/services/palette";
-import Updates, { Repo as IRepo, Update as IUpdate } from "@/services/updates";
-import { MenuItem, setupCustomTooltip } from "@/utils/widgets";
-import { bind, execAsync, GLib, Variable } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-
-const constructItem = (label: string, exec: string, quiet = true) =>
- new MenuItem({ label, onActivate: () => execAsync(exec).catch(e => !quiet && console.error(e)) });
-
-const Update = (update: IUpdate) => {
- const menu = new Gtk.Menu();
- menu.append(constructItem("Open info in browser", `app2unit -O '${update.url}'`, false));
- menu.append(constructItem("Open info in terminal", `app2unit -- foot -H -- pacman -Qi ${update.name}`));
- menu.append(new Gtk.SeparatorMenuItem({ visible: true }));
- menu.append(constructItem("Reinstall", `app2unit -- foot -H -- yay -S ${update.name}`));
- menu.append(constructItem("Remove with dependencies", `app2unit -- foot -H -- yay -Rns ${update.name}`));
-
- return (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.SECONDARY && menu.popup_at_pointer(null)}
- onDestroy={() => menu.destroy()}
- >
- <label
- truncate
- useMarkup
- xalign={0}
- label={bind(Palette.get_default(), "colours").as(
- c =>
- `${update.name} <span foreground="${c.teal}">(${update.version.old} -> ${
- update.version.new
- })</span>\n <span foreground="${c.subtext0}">${GLib.markup_escape_text(
- update.description,
- update.description.length
- )}</span>`
- )}
- setup={self => setupCustomTooltip(self, `${update.name} • ${update.description}`)}
- />
- </button>
- );
-};
-
-const Repo = ({ repo }: { repo: IRepo }) => {
- const expanded = Variable(false);
-
- return (
- <box vertical className="repo">
- <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
- <box className="header">
- <label className="icon" label={repo.icon} />
- <label label={`${repo.name} (${repo.updates.length})`} />
- <box hexpand />
- <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <box vertical className="body">
- {repo.updates.map(Update)}
- </box>
- </revealer>
- </box>
- );
-};
-
-const List = () => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(Updates.get_default(), "updateData").as(d => d.repos.map(r => <Repo repo={r} />))}
- </box>
-);
-
-const NoUpdates = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="deployed_code_history" />
- <label label="All packages up to date!" />
- </box>
- </box>
-);
-
-export default () => (
- <box vertical className="updates">
- <box className="header-bar">
- <label
- label={bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"} available`)}
- />
- <box hexpand />
- <button
- className={bind(Updates.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(Updates.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => Updates.get_default().getUpdates()}
- label={bind(Updates.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(Updates.get_default(), "numUpdates").as(n => (n > 0 ? "list" : "empty"))}
- >
- <NoUpdates />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/packages.tsx b/src/modules/sidebar/packages.tsx
deleted file mode 100644
index 02b0702..0000000
--- a/src/modules/sidebar/packages.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import News from "./modules/news";
-import Updates from "./modules/updates";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="pane packages" name="packages">
- <Updates />
- <box className="separator" />
- <News monitor={monitor} />
- </box>
-);
diff --git a/src/modules/sidebar/time.tsx b/src/modules/sidebar/time.tsx
deleted file mode 100644
index 1f5ef99..0000000
--- a/src/modules/sidebar/time.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { bindCurrentTime } from "@/utils/system";
-import { Gtk } from "astal/gtk3";
-import Calendar from "./modules/calendar";
-import Upcoming from "./modules/upcoming";
-
-const TimeDate = () => (
- <box vertical className="time-date">
- <box halign={Gtk.Align.CENTER}>
- <label label={bindCurrentTime("%I:%M:%S")} />
- <label className="ampm" label={bindCurrentTime("%p", c => (c.get_hour() < 12 ? "AM" : "PM"))} />
- </box>
- <label className="date" label={bindCurrentTime("%A, %d %B")} />
- </box>
-);
-
-export default () => (
- <box vertical className="pane time" name="time">
- <TimeDate />
- <box className="separator" />
- <Upcoming />
- <box className="separator" />
- <Calendar />
- </box>
-);
diff --git a/src/services/apps.ts b/src/services/apps.ts
deleted file mode 100644
index 5396ac7..0000000
--- a/src/services/apps.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import AstalApps from "gi://AstalApps";
-
-export const Apps = new AstalApps.Apps();
diff --git a/src/services/calendar.ts b/src/services/calendar.ts
deleted file mode 100644
index d5e0329..0000000
--- a/src/services/calendar.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-import { pathToFileName } from "@/utils/strings";
-import { notify } from "@/utils/system";
-import {
- execAsync,
- GLib,
- GObject,
- property,
- readFileAsync,
- register,
- timeout,
- writeFileAsync,
- type AstalIO,
-} from "astal";
-import { calendar as config } from "config";
-import ical from "ical.js";
-
-export interface IEvent {
- calendar: string;
- event: ical.Event;
- startDate: ical.Time;
- endDate: ical.Time;
-}
-
-@register({ GTypeName: "Calendar" })
-export default class Calendar extends GObject.Object {
- static instance: Calendar;
- static get_default() {
- if (!this.instance) this.instance = new Calendar();
-
- return this.instance;
- }
-
- readonly #cacheDir = `${CACHE}/calendars`;
-
- #calCount: number = 1;
- #reminders: AstalIO.Time[] = [];
- #loading: boolean = false;
- #calendars: { [name: string]: ical.Component } = {};
- #upcoming: { [date: string]: IEvent[] } = {};
- #cachedEvents: { [date: string]: IEvent[] } = {};
- #cachedMonths: Set<string> = new Set();
-
- @property(Boolean)
- get loading() {
- return this.#loading;
- }
-
- @property(Object)
- get calendars() {
- return this.#calendars;
- }
-
- @property(Object)
- get upcoming() {
- return this.#upcoming;
- }
-
- @property(Number)
- get numUpcoming() {
- return Object.values(this.#upcoming).reduce((acc, e) => acc + e.length, 0);
- }
-
- getCalendarIndex(name: string) {
- return Object.keys(this.#calendars).indexOf(name) + 1;
- }
-
- getEventsForMonth(date: ical.Time) {
- const start = date.startOfMonth();
-
- if (this.#cachedMonths.has(start.toJSDate().toDateString())) return this.#cachedEvents;
-
- this.#cachedMonths.add(start.toJSDate().toDateString());
- const end = date.endOfMonth();
-
- const modDates = new Set<string>();
-
- for (const [name, cal] of Object.entries(this.#calendars)) {
- for (const e of cal.getAllSubcomponents()) {
- const event = new ical.Event(e);
-
- // Skip invalid events
- if (!event.startDate) continue;
-
- if (event.isRecurring()) {
- // Recurring events
- const iter = event.iterator();
- for (let next = iter.next(); next && next.compare(end) <= 0; next = iter.next())
- if (next.compare(start) >= 0) {
- const date = next.toJSDate().toDateString();
- if (!this.#cachedEvents.hasOwnProperty(date)) this.#cachedEvents[date] = [];
-
- const end = next.clone();
- end.addDuration(event.duration);
- this.#cachedEvents[date].push({ calendar: name, event, startDate: next, endDate: end });
- modDates.add(date);
- }
- } else if (event.startDate.compare(start) >= 0 && event.startDate.compare(end) <= 0) {
- const date = event.startDate.toJSDate().toDateString();
- if (!this.#cachedEvents.hasOwnProperty(date)) this.#cachedEvents[date] = [];
- this.#cachedEvents[date].push({
- calendar: name,
- event,
- startDate: event.startDate,
- endDate: event.endDate,
- });
- modDates.add(date);
- }
- }
- }
-
- for (const date of modDates) this.#cachedEvents[date].sort((a, b) => a.startDate.compare(b.startDate));
-
- return this.#cachedEvents;
- }
-
- getEventsForDay(date: ical.Time) {
- return this.getEventsForMonth(date)[date.toJSDate().toDateString()] ?? [];
- }
-
- async updateCalendars() {
- this.#loading = true;
- this.notify("loading");
-
- this.#calendars = {};
- this.#calCount = 1;
-
- const cals = await Promise.allSettled(config.webcals.get().map(c => execAsync(["curl", c])));
- for (let i = 0; i < cals.length; i++) {
- const cal = cals[i];
- const webcal = pathToFileName(config.webcals.get()[i]);
-
- let icalStr;
- if (cal.status === "fulfilled") {
- icalStr = cal.value;
- } else {
- console.error(`Failed to get calendar from ${config.webcals.get()[i]}:\n${cal.reason}`);
- if (GLib.file_test(`${this.#cacheDir}/${webcal}`, GLib.FileTest.EXISTS))
- icalStr = await readFileAsync(`${this.#cacheDir}/${webcal}`);
- }
-
- if (icalStr) {
- const comp = new ical.Component(ical.parse(icalStr));
- const name = (comp.getFirstPropertyValue("x-wr-calname") ?? `Calendar ${this.#calCount++}`) as string;
- this.#calendars[name] = comp;
- writeFileAsync(`${this.#cacheDir}/${webcal}`, icalStr).catch(console.error);
- }
- }
- this.#cachedEvents = {};
- this.#cachedMonths.clear();
-
- this.notify("calendars");
-
- this.updateUpcoming();
-
- this.#loading = false;
- this.notify("loading");
- }
-
- updateUpcoming() {
- this.#upcoming = {};
-
- for (let i = 0; i < config.upcomingDays.get(); i++) {
- const date = ical.Time.now().adjust(i, 0, 0, 0);
- const events = this.getEventsForDay(date);
- if (events.length > 0) this.#upcoming[date.toJSDate().toDateString()] = events;
- }
-
- this.notify("upcoming");
- this.notify("num-upcoming");
-
- this.setReminders();
- }
-
- #notifyEvent(event: IEvent) {
- const start = GLib.DateTime.new_from_unix_local(event.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(event.endDate.toUnixTime());
- const time = `${start.format(`%A, %-d %B`)} • Now — ${end.format("%-I:%M%P")}`;
- const locIfExists = event.event.location ? ` ${event.event.location}\n` : "";
- const descIfExists = event.event.description ? `󰒿 ${event.event.description}\n` : "";
-
- notify({
- summary: `󰨱 ${event.event.summary} 󰨱`,
- body: `${time}\n${locIfExists}${descIfExists}󰃭 ${event.calendar}`,
- }).catch(console.error);
- }
-
- #createReminder(event: IEvent) {
- const diff = event.startDate.toJSDate().getTime() - ical.Time.now().toJSDate().getTime();
- if (diff > 0) this.#reminders.push(timeout(diff, () => this.#notifyEvent(event)));
- }
-
- setReminders() {
- this.#reminders.forEach(r => r.cancel());
- this.#reminders = [];
-
- if (!config.notify.get()) return;
-
- for (const events of Object.values(this.#upcoming)) for (const event of events) this.#createReminder(event);
- }
-
- constructor() {
- super();
-
- GLib.mkdir_with_parents(this.#cacheDir, 0o755);
-
- const cals = config.webcals.get().map(async c => {
- const webcal = pathToFileName(c);
-
- if (GLib.file_test(`${this.#cacheDir}/${webcal}`, GLib.FileTest.EXISTS)) {
- const data = await readFileAsync(`${this.#cacheDir}/${webcal}`);
- const comp = new ical.Component(ical.parse(data));
- const name = (comp.getFirstPropertyValue("x-wr-calname") ?? `Calendar ${this.#calCount++}`) as string;
- this.#calendars[name] = comp;
- }
- });
- Promise.allSettled(cals).then(() => {
- this.#cachedEvents = {};
- this.#cachedMonths.clear();
- this.notify("calendars");
- this.updateUpcoming();
- });
-
- this.updateCalendars().catch(console.error);
- config.webcals.subscribe(() => this.updateCalendars().catch(console.error));
- config.upcomingDays.subscribe(() => this.updateUpcoming());
- config.notify.subscribe(() => this.setReminders());
- }
-}
diff --git a/src/services/cpu.ts b/src/services/cpu.ts
deleted file mode 100644
index 5f80d11..0000000
--- a/src/services/cpu.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { GObject, interval, property, register } from "astal";
-import { cpu as config } from "config";
-import GTop from "gi://GTop";
-
-@register({ GTypeName: "Cpu" })
-export default class Cpu extends GObject.Object {
- static instance: Cpu;
- static get_default() {
- if (!this.instance) this.instance = new Cpu();
-
- return this.instance;
- }
-
- #previous: GTop.glibtop_cpu = new GTop.glibtop_cpu();
- #usage: number = 0;
-
- @property(Number)
- get usage() {
- return this.#usage;
- }
-
- calculateUsage() {
- const current = new GTop.glibtop_cpu();
- GTop.glibtop_get_cpu(current);
-
- // Calculate the differences from the previous to current data
- const total = current.total - this.#previous.total;
- const idle = current.idle - this.#previous.idle;
-
- this.#previous = current;
-
- return total > 0 ? ((total - idle) / total) * 100 : 0;
- }
-
- update() {
- this.#usage = this.calculateUsage();
- this.notify("usage");
- }
-
- constructor() {
- super();
-
- let source = interval(config.interval.get(), () => this.update());
- config.interval.subscribe(i => {
- source.cancel();
- source = interval(i, () => this.update());
- });
- }
-}
diff --git a/src/services/gpu.ts b/src/services/gpu.ts
deleted file mode 100644
index 5ac2d8d..0000000
--- a/src/services/gpu.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { execAsync, Gio, GLib, GObject, interval, property, register } from "astal";
-import { gpu as config } from "config";
-
-@register({ GTypeName: "Gpu" })
-export default class Gpu extends GObject.Object {
- static instance: Gpu;
- static get_default() {
- if (!this.instance) this.instance = new Gpu();
-
- return this.instance;
- }
-
- readonly available: boolean = false;
- #usage: number = 0;
-
- @property(Number)
- get usage() {
- return this.#usage;
- }
-
- async calculateUsage() {
- const percs = (await execAsync("fish -c 'cat /sys/class/drm/card*/device/gpu_busy_percent'")).split("\n");
- return percs.reduce((a, b) => a + parseFloat(b), 0) / percs.length;
- }
-
- update() {
- this.calculateUsage()
- .then(usage => {
- this.#usage = usage;
- this.notify("usage");
- })
- .catch(console.error);
- }
-
- constructor() {
- super();
-
- let enumerator = null;
- try {
- enumerator = Gio.File.new_for_path("/sys/class/drm").enumerate_children(
- Gio.FILE_ATTRIBUTE_STANDARD_NAME,
- Gio.FileQueryInfoFlags.NONE,
- null
- );
- } catch {}
-
- let info: Gio.FileInfo | undefined | null;
- while ((info = enumerator?.next_file(null))) {
- if (GLib.file_test(`/sys/class/drm/${info.get_name()}/device/gpu_busy_percent`, GLib.FileTest.EXISTS)) {
- this.available = true;
- break;
- }
- }
-
- if (this.available) {
- let source = interval(config.interval.get(), () => this.update());
- config.interval.subscribe(i => {
- source.cancel();
- source = interval(i, () => this.update());
- });
- }
- }
-}
diff --git a/src/services/math.ts b/src/services/math.ts
deleted file mode 100644
index 0cddf1b..0000000
--- a/src/services/math.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import { GLib, GObject, property, readFile, register, writeFileAsync } from "astal";
-import { math as config } from "config";
-import { derivative, evaluate, rationalize, simplify } from "mathjs/number";
-
-export interface HistoryItem {
- equation: string;
- result: string;
- icon: string;
-}
-
-@register({ GTypeName: "Math" })
-export default class Math extends GObject.Object {
- static instance: Math;
- static get_default() {
- if (!this.instance) this.instance = new Math();
-
- return this.instance;
- }
-
- readonly #path = `${STATE}/math-history.json`;
- readonly #history: HistoryItem[] = [];
-
- #variables: Record<string, string> = {};
- #lastExpression: HistoryItem | null = null;
-
- @property(Object)
- get history() {
- return this.#history;
- }
-
- #save() {
- writeFileAsync(this.#path, JSON.stringify(this.#history)).catch(console.error);
- }
-
- /**
- * Commits the last evaluated expression to the history
- */
- commit() {
- if (!this.#lastExpression) return;
-
- // Try select first to prevent duplicates, if it fails, add it
- if (!this.select(this.#lastExpression)) {
- this.#history.unshift(this.#lastExpression);
- while (this.#history.length > config.maxHistory.get()) this.#history.pop();
- this.notify("history");
- this.#save();
- }
- this.#lastExpression = null;
- }
-
- /**
- * Moves an item in the history to the top
- * @param item The item to select
- * @returns If the item was successfully selected
- */
- select(item: HistoryItem) {
- const idx = this.#history.findIndex(i => i.equation === item.equation && i.result === item.result);
- if (idx >= 0) {
- this.#history.splice(idx, 1);
- this.#history.unshift(item);
- this.notify("history");
- this.#save();
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Clears the history and variables
- */
- clear() {
- if (this.#history.length > 0) {
- this.#history.length = 0;
- this.notify("history");
- this.#save();
- }
- this.#lastExpression = null;
- this.#variables = {};
- }
-
- /**
- * Evaluates an equation and adds it to the history
- * @param equation The equation to evaluate
- * @returns A {@link HistoryItem} representing the result of the equation
- */
- evaluate(equation: string): HistoryItem {
- if (equation.startsWith("clear"))
- return {
- equation: "Clear history",
- result: "Delete history and previously set variables",
- icon: "delete_forever",
- };
-
- let result: string, icon: string;
- try {
- if (equation.startsWith("help")) {
- equation = "Help";
- result =
- "This is a calculator powered by Math.js.\nAvailable functions:\n\thelp: show help\n\tclear: clear history\n\t<x> = <equation>: sets <x> to <equation>\n\tsimplify <equation>: simplifies <equation>\n\tderive <x> <equation>: derives <equation> with respect to <x>\n\tdd<x> <equation>: short form of derive\n\trationalize <equation>: rationalizes <equation>\n\t<equation>: evaluates <equation>\nSee the documentation for syntax and inbuilt functions.";
- icon = "help";
- } else if (equation.includes("=")) {
- const [left, right] = equation.split("=");
- try {
- this.#variables[left.trim()] = simplify(right, this.#variables).toString();
- } catch {
- this.#variables[left.trim()] = right.trim();
- }
- result = this.#variables[left.trim()];
- icon = "equal";
- } else if (equation.startsWith("simplify")) {
- result = simplify(equation.slice(8), this.#variables).toString();
- icon = "function";
- } else if (equation.startsWith("derive") || equation.startsWith("dd")) {
- const isShortForm = equation.startsWith("dd");
- const respectTo = isShortForm ? equation.split(" ")[0].slice(2) : equation.split(" ")[1];
- if (!respectTo) throw new Error(`Format: ${isShortForm ? "dd" : "derive "}<respect-to> <equation>`);
- result = derivative(equation.slice((isShortForm ? 2 : 7) + respectTo.length), respectTo).toString();
- icon = "function";
- } else if (equation.startsWith("rationalize")) {
- result = rationalize(equation.slice(11), this.#variables).toString();
- icon = "function";
- } else {
- result = evaluate(equation, this.#variables).toString();
- icon = "calculate";
- }
- } catch (e) {
- equation = "Invalid equation: " + equation;
- result = String(e);
- icon = "error";
- }
-
- return (this.#lastExpression = { equation, result, icon });
- }
-
- constructor() {
- super();
-
- // Load history
- if (GLib.file_test(this.#path, GLib.FileTest.EXISTS)) {
- try {
- this.#history = JSON.parse(readFile(this.#path));
- // Init eval to create variables and last expression
- for (const item of this.#history) this.evaluate(item.equation);
- } catch (e) {
- console.error("Math - Unable to load history", e);
- }
- }
-
- config.maxHistory.subscribe(n => {
- while (this.#history.length > n) this.#history.pop();
- });
- }
-}
diff --git a/src/services/memory.ts b/src/services/memory.ts
deleted file mode 100644
index b1231b9..0000000
--- a/src/services/memory.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { GObject, interval, property, readFileAsync, register } from "astal";
-import { memory as config } from "config";
-
-@register({ GTypeName: "Memory" })
-export default class Memory extends GObject.Object {
- static instance: Memory;
- static get_default() {
- if (!this.instance) this.instance = new Memory();
-
- return this.instance;
- }
-
- #total: number = 0;
- #free: number = 0;
- #used: number = 0;
- #usage: number = 0;
-
- @property(Number)
- get total() {
- return this.#total;
- }
-
- @property(Number)
- get free() {
- return this.#free;
- }
-
- @property(Number)
- get used() {
- return this.#used;
- }
-
- @property(Number)
- get usage() {
- return this.#usage;
- }
-
- async update() {
- const info = await readFileAsync("/proc/meminfo");
- this.#total = parseInt(info.match(/MemTotal:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
- this.#free = parseInt(info.match(/MemAvailable:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
-
- if (isNaN(this.#total)) this.#total = 0;
- if (isNaN(this.#free)) this.#free = 0;
-
- this.#used = this.#total - this.#free;
- this.#usage = this.#total > 0 ? (this.#used / this.#total) * 100 : 0;
-
- this.notify("total");
- this.notify("free");
- this.notify("used");
- this.notify("usage");
- }
-
- constructor() {
- super();
-
- let source = interval(config.interval.get(), () => this.update().catch(console.error));
- config.interval.subscribe(i => {
- source.cancel();
- source = interval(i, () => this.update().catch(console.error));
- });
- }
-}
diff --git a/src/services/monitors.ts b/src/services/monitors.ts
deleted file mode 100644
index 6ae7ecb..0000000
--- a/src/services/monitors.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { GObject, execAsync, property, register } from "astal";
-import AstalHyprland from "gi://AstalHyprland";
-
-@register({ GTypeName: "Monitor" })
-export class Monitor extends GObject.Object {
- readonly monitor: AstalHyprland.Monitor;
- readonly width: number;
- readonly height: number;
- readonly id: number;
- readonly serial: string;
- readonly name: string;
- readonly description: string;
-
- @property(AstalHyprland.Workspace)
- get activeWorkspace() {
- return this.monitor.activeWorkspace;
- }
-
- isDdc: boolean = false;
- busNum?: string;
-
- #brightness: number = 0;
-
- @property(Number)
- get brightness() {
- return this.#brightness;
- }
-
- set brightness(value) {
- value = Math.min(1, Math.max(0, value));
-
- this.#brightness = value;
- this.notify("brightness");
- execAsync(
- this.isDdc
- ? `ddcutil -b ${this.busNum} setvcp 10 ${Math.round(value * 100)}`
- : `brightnessctl set ${Math.floor(value * 100)}% -q`
- ).catch(console.error);
- }
-
- constructor(monitor: AstalHyprland.Monitor) {
- super();
-
- this.monitor = monitor;
- this.width = monitor.width;
- this.height = monitor.height;
- this.id = monitor.id;
- this.serial = monitor.serial;
- this.name = monitor.name;
- this.description = monitor.description;
-
- monitor.connect("notify::active-workspace", () => this.notify("active-workspace"));
-
- execAsync("ddcutil detect --brief")
- .then(out => {
- this.isDdc = out.split("\n\n").some(display => {
- if (!/^Display \d+/.test(display)) return false;
- const lines = display.split("\n").map(l => l.trimStart());
- if (lines.find(l => l.startsWith("Monitor:"))?.split(":")[3] !== monitor.serial) return false;
- this.busNum = lines.find(l => l.startsWith("I2C bus:"))?.split("/dev/i2c-")[1];
- return this.busNum !== undefined;
- });
- })
- .catch(() => (this.isDdc = false))
- .finally(async () => {
- if (this.isDdc) {
- const info = (await execAsync(`ddcutil -b ${this.busNum} getvcp 10 --brief`)).split(" ");
- this.#brightness = Number(info[3]) / Number(info[4]);
- } else
- this.#brightness =
- Number(await execAsync("brightnessctl get")) / Number(await execAsync("brightnessctl max"));
- });
- }
-}
-
-@register({ GTypeName: "Monitors" })
-export default class Monitors extends GObject.Object {
- static instance: Monitors;
- static get_default() {
- if (!this.instance) this.instance = new Monitors();
-
- return this.instance;
- }
-
- readonly #map: Map<number, Monitor> = new Map();
-
- @property(Object)
- get map() {
- return this.#map;
- }
-
- @property(Object)
- get list() {
- return Array.from(this.#map.values());
- }
-
- @property(Monitor)
- get active() {
- return this.#map.get(AstalHyprland.get_default().focusedMonitor.id)!;
- }
-
- #notify() {
- this.notify("map");
- this.notify("list");
- }
-
- forEach(fn: (monitor: Monitor) => void) {
- for (const monitor of this.#map.values()) fn(monitor);
- }
-
- constructor() {
- super();
-
- const hyprland = AstalHyprland.get_default();
-
- for (const monitor of hyprland.monitors) this.#map.set(monitor.id, new Monitor(monitor));
- if (this.#map.size > 0) this.#notify();
-
- hyprland.connect("monitor-added", (_, monitor) => {
- this.#map.set(monitor.id, new Monitor(monitor));
- this.#notify();
- });
- hyprland.connect("monitor-removed", (_, id) => this.#map.delete(id) && this.#notify());
-
- hyprland.connect("notify::focused-monitor", () => this.notify("active"));
- }
-}
diff --git a/src/services/news.ts b/src/services/news.ts
deleted file mode 100644
index 14c980c..0000000
--- a/src/services/news.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import { notify } from "@/utils/system";
-import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal";
-import { news as config } from "config";
-import { setConfig } from "config/funcs";
-
-export interface IArticle {
- title: string;
- link: string;
- keywords: string[] | null;
- creator: string[] | null;
- description: string | null;
- pubDate: string;
- source_name: string;
- source_priority: number;
- category: string[];
-}
-
-@register({ GTypeName: "News" })
-export default class News extends GObject.Object {
- static instance: News;
- static get_default() {
- if (!this.instance) this.instance = new News();
-
- return this.instance;
- }
-
- readonly #cachePath = `${CACHE}/news.json`;
- #notified = false;
-
- #loading: boolean = false;
- #articles: IArticle[] = [];
- #categories: { [category: string]: IArticle[] } = {};
-
- @property(Boolean)
- get loading() {
- return this.#loading;
- }
-
- @property(Object)
- get articles() {
- return this.#articles;
- }
-
- @property(Object)
- get categories() {
- return this.#categories;
- }
-
- async getNews() {
- if (!config.apiKey.get()) {
- if (!this.#notified) {
- notify({
- summary: "A newsdata.io API key is required",
- body: "You can get one by creating an account at https://newsdata.io",
- icon: "dialog-warning-symbolic",
- urgency: "critical",
- actions: {
- "Get API key": () => execAsync("app2unit -O -- https://newsdata.io").catch(console.error),
- Disable: () => setConfig("sidebar.modules.headlines.enabled", false),
- },
- });
- this.#notified = true;
- }
- return;
- }
-
- this.#loading = true;
- this.notify("loading");
-
- let countries = config.countries.get().join(",");
- const categories = config.categories.get().join(",");
- const languages = config.languages.get().join(",");
- const domains = config.domains.get().join(",");
- const excludeDomains = config.excludeDomains.get().join(",");
- const timezone = config.timezone.get();
-
- if (countries.includes("current")) {
- const out = JSON.parse(await execAsync("curl ipinfo.io")).country.toLowerCase();
- countries = countries.replace("current", out);
- }
-
- let args = "removeduplicate=1&prioritydomain=top";
- if (countries) args += `&country=${countries}`;
- if (categories) args += `&category=${categories}`;
- if (languages) args += `&language=${languages}`;
- if (domains) args += `&domain=${domains}`;
- if (excludeDomains) args += `&excludedomain=${excludeDomains}`;
- if (timezone) args += `&timezone=${timezone}`;
-
- const url = `https://newsdata.io/api/1/latest?apikey=${config.apiKey.get()}&${args}`;
- try {
- const res = JSON.parse(await execAsync(["curl", url]));
- if (res.status !== "success") throw new Error(`Failed to get news: ${res.results.message}`);
-
- this.#articles = [...res.results];
-
- let page = res.nextPage;
- for (let i = 1; i < config.pages.get(); i++) {
- const res = JSON.parse(await execAsync(["curl", `${url}&page=${page}`]));
- if (res.status !== "success") throw new Error(`Failed to get news: ${res.results.message}`);
- this.#articles.push(...res.results);
- page = res.nextPage;
- }
-
- writeFileAsync(this.#cachePath, JSON.stringify(this.#articles)).catch(console.error);
- } catch (e) {
- console.error(e);
-
- if (GLib.file_test(this.#cachePath, GLib.FileTest.EXISTS))
- this.#articles = JSON.parse(await readFileAsync(this.#cachePath));
- }
- this.notify("articles");
-
- this.updateCategories();
-
- this.#loading = false;
- this.notify("loading");
- }
-
- updateCategories() {
- this.#categories = {};
- for (const article of this.#articles) {
- for (const category of article.category) {
- if (!this.#categories.hasOwnProperty(category)) this.#categories[category] = [];
- this.#categories[category].push(article);
- }
- }
- this.notify("categories");
- }
-
- constructor() {
- super();
-
- if (GLib.file_test(this.#cachePath, GLib.FileTest.EXISTS))
- readFileAsync(this.#cachePath)
- .then(data => {
- this.#articles = JSON.parse(data);
- this.notify("articles");
- this.updateCategories();
- })
- .catch(console.error);
-
- this.getNews().catch(console.error);
- config.apiKey.subscribe(() => this.getNews().catch(console.error));
- config.countries.subscribe(() => this.getNews().catch(console.error));
- config.categories.subscribe(() => this.getNews().catch(console.error));
- config.languages.subscribe(() => this.getNews().catch(console.error));
- config.domains.subscribe(() => this.getNews().catch(console.error));
- config.excludeDomains.subscribe(() => this.getNews().catch(console.error));
- config.timezone.subscribe(() => this.getNews().catch(console.error));
- config.pages.subscribe(() => this.getNews().catch(console.error));
- }
-}
diff --git a/src/services/palette.ts b/src/services/palette.ts
deleted file mode 100644
index 952543f..0000000
--- a/src/services/palette.ts
+++ /dev/null
@@ -1,298 +0,0 @@
-import { execAsync, GLib, GObject, monitorFile, property, readFile, readFileAsync, register } from "astal";
-import Schemes from "./schemes";
-
-export type ColourMode = "light" | "dark";
-
-export type Hex = `#${string}`;
-
-export interface IPalette {
- rosewater: Hex;
- flamingo: Hex;
- pink: Hex;
- mauve: Hex;
- red: Hex;
- maroon: Hex;
- peach: Hex;
- yellow: Hex;
- green: Hex;
- teal: Hex;
- sky: Hex;
- sapphire: Hex;
- blue: Hex;
- lavender: Hex;
- text: Hex;
- subtext1: Hex;
- subtext0: Hex;
- overlay2: Hex;
- overlay1: Hex;
- overlay0: Hex;
- surface2: Hex;
- surface1: Hex;
- surface0: Hex;
- base: Hex;
- mantle: Hex;
- crust: Hex;
- primary: Hex;
- secondary: Hex;
- tertiary: Hex;
-}
-
-@register({ GTypeName: "Palette" })
-export default class Palette extends GObject.Object {
- static instance: Palette;
- static get_default() {
- if (!this.instance) this.instance = new Palette();
-
- return this.instance;
- }
-
- #mode: ColourMode;
- #scheme: string;
- #flavour?: string;
- #colours!: IPalette;
-
- @property(Boolean)
- get mode() {
- return this.#mode;
- }
-
- @property(String)
- get scheme() {
- return this.#scheme;
- }
-
- @property(String)
- get flavour() {
- return this.#flavour;
- }
-
- @property(Object)
- get colours() {
- return this.#colours;
- }
-
- @property(String)
- get rosewater() {
- return this.#colours.rosewater;
- }
-
- @property(String)
- get flamingo() {
- return this.#colours.flamingo;
- }
-
- @property(String)
- get pink() {
- return this.#colours.pink;
- }
-
- @property(String)
- get mauve() {
- return this.#colours.mauve;
- }
-
- @property(String)
- get red() {
- return this.#colours.red;
- }
-
- @property(String)
- get maroon() {
- return this.#colours.maroon;
- }
-
- @property(String)
- get peach() {
- return this.#colours.peach;
- }
-
- @property(String)
- get yellow() {
- return this.#colours.yellow;
- }
-
- @property(String)
- get green() {
- return this.#colours.green;
- }
-
- @property(String)
- get teal() {
- return this.#colours.teal;
- }
-
- @property(String)
- get sky() {
- return this.#colours.sky;
- }
-
- @property(String)
- get sapphire() {
- return this.#colours.sapphire;
- }
-
- @property(String)
- get blue() {
- return this.#colours.blue;
- }
-
- @property(String)
- get lavender() {
- return this.#colours.lavender;
- }
-
- @property(String)
- get text() {
- return this.#colours.text;
- }
-
- @property(String)
- get subtext1() {
- return this.#colours.subtext1;
- }
-
- @property(String)
- get subtext0() {
- return this.#colours.subtext0;
- }
-
- @property(String)
- get overlay2() {
- return this.#colours.overlay2;
- }
-
- @property(String)
- get overlay1() {
- return this.#colours.overlay1;
- }
-
- @property(String)
- get overlay0() {
- return this.#colours.overlay0;
- }
-
- @property(String)
- get surface2() {
- return this.#colours.surface2;
- }
-
- @property(String)
- get surface1() {
- return this.#colours.surface1;
- }
-
- @property(String)
- get surface0() {
- return this.#colours.surface0;
- }
-
- @property(String)
- get base() {
- return this.#colours.base;
- }
-
- @property(String)
- get mantle() {
- return this.#colours.mantle;
- }
-
- @property(String)
- get crust() {
- return this.#colours.crust;
- }
-
- @property(String)
- get primary() {
- return this.#colours.primary;
- }
-
- @property(String)
- get secondary() {
- return this.#colours.secondary;
- }
-
- @property(String)
- get tertiary() {
- return this.#colours.tertiary;
- }
-
- #notify() {
- this.notify("colours");
- this.notify("rosewater");
- this.notify("flamingo");
- this.notify("pink");
- this.notify("mauve");
- this.notify("red");
- this.notify("maroon");
- this.notify("peach");
- this.notify("yellow");
- this.notify("green");
- this.notify("teal");
- this.notify("sky");
- this.notify("sapphire");
- this.notify("blue");
- this.notify("lavender");
- this.notify("text");
- this.notify("subtext1");
- this.notify("subtext0");
- this.notify("overlay2");
- this.notify("overlay1");
- this.notify("overlay0");
- this.notify("surface2");
- this.notify("surface1");
- this.notify("surface0");
- this.notify("base");
- this.notify("mantle");
- this.notify("crust");
- this.notify("primary");
- this.notify("secondary");
- this.notify("tertiary");
- }
-
- update() {
- let schemeColours;
- if (GLib.file_test(`${STATE}/scheme/current.txt`, GLib.FileTest.EXISTS)) {
- const currentScheme = readFile(`${STATE}/scheme/current.txt`);
- schemeColours = currentScheme.split("\n").map(l => l.split(" "));
- } else
- schemeColours = readFile(`${SRC}/scss/scheme/_default.scss`)
- .split("\n")
- .map(l => {
- const [name, hex] = l.split(":");
- return [name.slice(1), hex.trim().slice(1, -1)];
- });
-
- this.#colours = schemeColours.reduce((acc, [name, hex]) => ({ ...acc, [name]: `#${hex}` }), {} as IPalette);
- this.#notify();
- }
-
- switchMode(mode: ColourMode) {
- execAsync(`caelestia scheme ${this.scheme} ${this.flavour ?? ""} ${mode}`).catch(console.error);
- }
-
- hasMode(mode: ColourMode) {
- const scheme = Schemes.get_default().map[this.scheme];
- if (scheme?.colours?.[mode]) return true;
- return scheme?.flavours?.[this.flavour ?? ""]?.colours?.[mode] !== undefined;
- }
-
- constructor() {
- super();
-
- this.#mode = readFile(`${STATE}/scheme/current-mode.txt`) === "light" ? "light" : "dark";
- monitorFile(`${STATE}/scheme/current-mode.txt`, async file => {
- this.#mode = (await readFileAsync(file)) === "light" ? "light" : "dark";
- this.notify("mode");
- });
-
- [this.#scheme, this.#flavour] = readFile(`${STATE}/scheme/current-name.txt`).split("-");
- monitorFile(`${STATE}/scheme/current-name.txt`, async file => {
- [this.#scheme, this.#flavour] = (await readFileAsync(file)).split("-");
- this.notify("scheme");
- this.notify("flavour");
- });
-
- this.update();
- monitorFile(`${STATE}/scheme/current.txt`, () => this.update());
- }
-}
diff --git a/src/services/players.ts b/src/services/players.ts
deleted file mode 100644
index aca7344..0000000
--- a/src/services/players.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { isRealPlayer } from "@/utils/mpris";
-import { GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal";
-import AstalMpris from "gi://AstalMpris";
-
-@register({ GTypeName: "Players" })
-export default class Players extends GObject.Object {
- static instance: Players;
- static get_default() {
- if (!this.instance) this.instance = new Players();
-
- return this.instance;
- }
-
- readonly #path = `${STATE}/players.txt`;
- readonly #players: AstalMpris.Player[] = [];
- readonly #subs = new Map<
- JSX.Element,
- { signals: string[]; callback: () => void; ids: number[]; player: AstalMpris.Player | null }
- >();
-
- @property(AstalMpris.Player)
- get lastPlayer(): AstalMpris.Player | null {
- return this.#players.length > 0 && this.#players[0].identity !== null ? this.#players[0] : null;
- }
-
- /**
- * List of real players.
- */
- @property(Object)
- get list() {
- return this.#players;
- }
-
- hookLastPlayer(widget: JSX.Element, signal: string, callback: () => void): this;
- hookLastPlayer(widget: JSX.Element, signals: string[], callback: () => void): this;
- hookLastPlayer(widget: JSX.Element, signals: string | string[], callback: () => void) {
- if (!Array.isArray(signals)) signals = [signals];
- // Add subscription
- if (this.lastPlayer)
- this.#subs.set(widget, {
- signals,
- callback,
- ids: signals.map(s => this.lastPlayer!.connect(s, callback)),
- player: this.lastPlayer,
- });
- else this.#subs.set(widget, { signals, callback, ids: [], player: null });
-
- // Remove subscription on widget destroyed
- widget.connect("destroy", () => {
- const sub = this.#subs.get(widget);
- if (sub?.player) sub.ids.forEach(id => sub.player!.disconnect(id));
- this.#subs.delete(widget);
- });
-
- // Initial run of callback
- callback();
-
- // For chaining
- return this;
- }
-
- makeCurrent(player: AstalMpris.Player) {
- const index = this.#players.findIndex(p => p.busName === player.busName);
- // Ignore if already current
- if (index === 0 || !isRealPlayer(player)) return;
- // Remove if present
- else if (index > 0) this.#players.splice(index, 1);
- // Connect signals if not already in list (i.e. new player)
- else this.#connectPlayerSignals(player);
-
- // Add to front
- this.#players.unshift(player);
- this.#updatePlayer();
- this.notify("list");
-
- // Save to file
- this.#save();
- }
-
- #updatePlayer() {
- this.notify("last-player");
-
- for (const sub of this.#subs.values()) {
- sub.callback();
- if (sub.player) sub.ids.forEach(id => sub.player!.disconnect(id));
- sub.ids = this.lastPlayer ? sub.signals.map(s => this.lastPlayer!.connect(s, sub.callback)) : [];
- sub.player = this.lastPlayer;
- }
- }
-
- #save() {
- writeFileAsync(this.#path, this.#players.map(p => p.busName).join("\n")).catch(console.error);
- }
-
- #connectPlayerSignals(player: AstalMpris.Player) {
- // Change order on attribute change
- for (const signal of [
- "notify::playback-status",
- "notify::shuffle-status",
- "notify::loop-status",
- "notify::volume",
- "notify::rate",
- ])
- player.connect(signal, () => this.makeCurrent(player));
- }
-
- constructor() {
- super();
-
- const mpris = AstalMpris.get_default();
-
- // Load players
- if (GLib.file_test(this.#path, GLib.FileTest.EXISTS)) {
- readFileAsync(this.#path).then(out => {
- for (const busName of out.split("\n").reverse()) {
- const player = mpris.get_players().find(p => p.busName === busName);
- if (player) this.makeCurrent(player);
- }
- // Add new players from in between sessions
- for (const player of mpris.get_players()) this.makeCurrent(player);
- });
- } else {
- const sortOrder = [
- AstalMpris.PlaybackStatus.PLAYING,
- AstalMpris.PlaybackStatus.PAUSED,
- AstalMpris.PlaybackStatus.STOPPED,
- ];
- const players = mpris
- .get_players()
- .sort((a, b) => sortOrder.indexOf(b.playbackStatus) - sortOrder.indexOf(a.playbackStatus));
- for (const player of players) this.makeCurrent(player);
- }
-
- // Add and connect signals when added
- mpris.connect("player-added", (_, player) => this.makeCurrent(player));
-
- // Remove when closed
- mpris.connect("player-closed", (_, player) => {
- const index = this.#players.indexOf(player);
- if (index >= 0) {
- this.#players.splice(index, 1);
- this.notify("list");
- if (index === 0) this.#updatePlayer();
- this.#save();
- }
- });
- }
-}
diff --git a/src/services/schemes.ts b/src/services/schemes.ts
deleted file mode 100644
index c85fa72..0000000
--- a/src/services/schemes.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { basename } from "@/utils/strings";
-import { monitorDirectory } from "@/utils/system";
-import { execAsync, Gio, 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" })
-export default class Schemes extends GObject.Object {
- static instance: Schemes;
- static get_default() {
- if (!this.instance) this.instance = new Schemes();
-
- return this.instance;
- }
-
- readonly #schemeDir: string = `${DATA}/scripts/data/schemes`;
-
- #map: { [k: string]: Scheme } = {};
-
- @property(Object)
- get map() {
- return this.#map;
- }
-
- async parseMode(path: string): Promise<IPalette | undefined> {
- const schemeColours = (await readFileAsync(path).catch(() => undefined))?.split("\n").map(l => l.split(" "));
- return schemeColours?.reduce((acc, [name, hex]) => ({ ...acc, [name]: `#${hex}` }), {} as IPalette);
- }
-
- async parseColours(path: string): Promise<Colours> {
- const light = await this.parseMode(`${path}/light.txt`);
- const dark = await this.parseMode(`${path}/dark.txt`);
- return { light, dark };
- }
-
- async parseFlavour(scheme: string, name: string): Promise<Flavour> {
- const path = `${this.#schemeDir}/${scheme}/${name}`;
- return { name, scheme, colours: await this.parseColours(path) };
- }
-
- 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 }),
- };
-
- return { name, colours: await this.parseColours(path) };
- }
-
- async update() {
- const schemes = await execAsync(`find ${this.#schemeDir}/ -mindepth 1 -maxdepth 1 -type d`);
- (await Promise.all(schemes.split("\n").map(s => this.parseScheme(basename(s))))).forEach(
- s => (this.#map[s.name] = s)
- );
- this.notify("map");
- }
-
- async updateFile(file: Gio.File) {
- if (file.get_basename() !== "light.txt" && file.get_basename() !== "dark.txt") {
- await this.update();
- return;
- }
-
- const mode = file.get_basename()!.slice(0, -4) as "light" | "dark";
- const parent = file.get_parent()!;
- const parentParent = parent.get_parent()!;
-
- if (parentParent.get_basename() === "schemes")
- this.#map[parent.get_basename()!].colours![mode] = await this.parseMode(file.get_path()!);
- else
- this.#map[parentParent.get_basename()!].flavours![parent.get_basename()!].colours![mode] =
- await this.parseMode(file.get_path()!);
-
- this.notify("map");
- }
-
- constructor() {
- super();
-
- this.update().catch(console.error);
- monitorDirectory(this.#schemeDir, (_m, file, _f, type) => {
- if (type !== Gio.FileMonitorEvent.DELETED) this.updateFile(file).catch(console.error);
- });
- }
-}
diff --git a/src/services/storage.ts b/src/services/storage.ts
deleted file mode 100644
index 3f8992d..0000000
--- a/src/services/storage.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { GObject, interval, property, register } from "astal";
-import { storage as config } from "config";
-import GTop from "gi://GTop";
-
-@register({ GTypeName: "Storage" })
-export default class Storage extends GObject.Object {
- static instance: Storage;
- static get_default() {
- if (!this.instance) this.instance = new Storage();
-
- return this.instance;
- }
-
- #total: number = 0;
- #free: number = 0;
- #used: number = 0;
- #usage: number = 0;
-
- @property(Number)
- get total() {
- return this.#total;
- }
-
- @property(Number)
- get free() {
- return this.#free;
- }
-
- @property(Number)
- get used() {
- return this.#used;
- }
-
- @property(Number)
- get usage() {
- return this.#usage;
- }
-
- update() {
- const root = new GTop.glibtop_fsusage();
- GTop.glibtop_get_fsusage(root, "/");
- const home = new GTop.glibtop_fsusage();
- GTop.glibtop_get_fsusage(home, "/home");
-
- this.#total = root.blocks * root.block_size + home.blocks * home.block_size;
- this.#free = root.bavail * root.block_size + home.bavail * home.block_size;
- this.#used = this.#total - this.#free;
- this.#usage = this.#total > 0 ? (this.#used / this.#total) * 100 : 0;
-
- this.notify("total");
- this.notify("free");
- this.notify("used");
- this.notify("usage");
- }
-
- constructor() {
- super();
-
- let source = interval(config.interval.get(), () => this.update());
- config.interval.subscribe(i => {
- source.cancel();
- source = interval(i, () => this.update());
- });
- }
-}
diff --git a/src/services/updates.ts b/src/services/updates.ts
deleted file mode 100644
index 62a8f65..0000000
--- a/src/services/updates.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import { capitalize } from "@/utils/strings";
-import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal";
-import { updates as config } from "config";
-
-export interface Update {
- full: string;
- name: string;
- description: string;
- url: string;
- version: {
- old: string;
- new: string;
- };
-}
-
-export interface Repo {
- repo?: string[];
- updates: Update[];
- icon: string;
- name: string;
-}
-
-export interface Data {
- cached?: boolean;
- repos: Repo[];
- errors: string[];
- news: string;
-}
-
-@register({ GTypeName: "Updates" })
-export default class Updates extends GObject.Object {
- static instance: Updates;
- static get_default() {
- if (!this.instance) this.instance = new Updates();
-
- return this.instance;
- }
-
- readonly #cachePath = `${CACHE}/updates.json`;
-
- #timeout?: GLib.Source;
- #loading = false;
- #data: Data = { cached: true, repos: [], errors: [], news: "" };
-
- @property(Boolean)
- get loading() {
- return this.#loading;
- }
-
- @property(Object)
- get updateData() {
- return this.#data;
- }
-
- @property(Object)
- get list() {
- return this.#data.repos.map(r => r.updates).flat();
- }
-
- @property(Number)
- get numUpdates() {
- return this.#data.repos.reduce((acc, repo) => acc + repo.updates.length, 0);
- }
-
- @property(String)
- get news() {
- return this.#data.news;
- }
-
- async #updateFromCache() {
- this.#data = JSON.parse(await readFileAsync(this.#cachePath));
- this.notify("update-data");
- this.notify("list");
- this.notify("num-updates");
- this.notify("news");
- }
-
- async getRepo(repo: string) {
- return (await execAsync(`bash -c "comm -12 <(pacman -Qq | sort) <(pacman -Slq '${repo}' | sort)"`)).split("\n");
- }
-
- async constructUpdate(update: string) {
- const info = await execAsync(`pacman -Qi ${update.split(" ")[0]}`);
- return info.split("\n").reduce(
- (acc, line) => {
- let [key, value] = line.split(" : ");
- key = key.trim().toLowerCase();
- if (key === "name" || key === "description" || key === "url") acc[key] = value.trim();
- else if (key === "version") acc.version.old = value.trim();
- return acc;
- },
- { version: { new: update.split("->")[1].trim() } } as Update
- );
- }
-
- getRepoIcon(repo: string) {
- switch (repo) {
- case "core":
- return "hub";
- case "extra":
- return "add_circle";
- case "multilib":
- return "account_tree";
- default:
- return "deployed_code_update";
- }
- }
-
- getUpdates() {
- // Return if already getting updates
- if (this.#loading) return;
-
- this.#loading = true;
- this.notify("loading");
-
- // Get new updates
- Promise.allSettled([execAsync("checkupdates"), execAsync("yay -Qua"), execAsync("yay -Pw")])
- .then(async ([pacman, yay, news]) => {
- const data: Data = { repos: [], errors: [], news: news.status === "fulfilled" ? news.value : "" };
-
- // Pacman updates (checkupdates)
- if (pacman.status === "fulfilled") {
- const repos: Repo[] = await Promise.all(
- (await execAsync("pacman-conf -l")).split("\n").map(async r => ({
- repo: await this.getRepo(r),
- updates: [],
- icon: this.getRepoIcon(r),
- name: capitalize(r) + " repository",
- }))
- );
-
- for (const update of pacman.value.split("\n")) {
- const pkg = update.split(" ")[0];
- for (const repo of repos)
- if (repo.repo?.includes(pkg)) repo.updates.push(await this.constructUpdate(update));
- }
-
- for (const repo of repos) if (repo.updates.length > 0) data.repos.push(repo);
- }
-
- // AUR and devel updates (yay -Qua)
- if (yay.status === "fulfilled") {
- const aur: Repo = { updates: [], icon: "deployed_code_account", name: "AUR" };
-
- for (const update of yay.value.split("\n")) {
- if (/^\s*->/.test(update)) data.errors.push(update); // Error
- else aur.updates.push(await this.constructUpdate(update));
- }
-
- if (aur.updates.length > 0) data.repos.push(aur);
- }
-
- if (data.errors.length > 0 && data.repos.length === 0) {
- this.#updateFromCache().catch(console.error);
- } else {
- // Sort updates by name
- for (const repo of data.repos) repo.updates.sort((a, b) => a.name.localeCompare(b.name));
-
- // Cache and set
- writeFileAsync(this.#cachePath, JSON.stringify({ cached: true, ...data })).catch(console.error);
- this.#data = data;
- this.notify("update-data");
- this.notify("list");
- this.notify("num-updates");
- this.notify("news");
- }
-
- this.#loading = false;
- this.notify("loading");
-
- this.#timeout?.destroy();
- this.#timeout = setTimeout(() => this.getUpdates(), config.interval.get());
- })
- .catch(console.error);
- }
-
- constructor() {
- super();
-
- // Initial update from cache, if fail then write valid data to cache so future reads don't fail
- this.#updateFromCache().catch(() =>
- writeFileAsync(this.#cachePath, JSON.stringify(this.#data)).catch(console.error)
- );
- this.getUpdates();
-
- config.interval.subscribe(i => {
- this.#timeout?.destroy();
- this.#timeout = setTimeout(() => this.getUpdates(), i);
- });
- }
-}
diff --git a/src/services/wallpapers.ts b/src/services/wallpapers.ts
deleted file mode 100644
index b5447c2..0000000
--- a/src/services/wallpapers.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { monitorDirectory } from "@/utils/system";
-import Thumbnailer from "@/utils/thumbnailer";
-import { execAsync, GObject, property, register } from "astal";
-import { wallpapers as config } from "config";
-import Monitors from "./monitors";
-
-export interface IWallpaper {
- path: string;
- thumbnails: {
- compact: string;
- medium: string;
- large: string;
- };
-}
-
-export interface ICategory {
- path: string;
- wallpapers: IWallpaper[];
-}
-
-@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;
- }
-
- #list: IWallpaper[] = [];
- #categories: ICategory[] = [];
-
- @property(Object)
- get list() {
- return this.#list;
- }
-
- @property(Object)
- get categories() {
- return this.#categories;
- }
-
- async #listDir(path: { path: string; recursive: boolean; threshold: number }, type: "f" | "d") {
- const absPath = path.path.replace("~", HOME);
- const maxDepth = path.recursive ? "" : "-maxdepth 1";
- const files = await execAsync(`find ${absPath} ${maxDepth} -path '*/.*' -prune -o -type ${type} -print`);
-
- if (type === "f" && path.threshold > 0) {
- const data = (
- await execAsync([
- "fish",
- "-c",
- `identify -ping -format '%i %w %h\n' ${files.replaceAll("\n", " ")} ; true`,
- ])
- ).split("\n");
-
- return data
- .filter(l => l && this.#filterSize(l, path.threshold))
- .map(l => l.split(" ").slice(0, -2).join(" "))
- .join("\n");
- }
-
- return files;
- }
-
- #filterSize(line: string, threshold: number) {
- const [width, height] = line.split(" ").slice(-2).map(Number);
- const mWidth = Math.max(...Monitors.get_default().list.map(m => m.width));
- const mHeight = Math.max(...Monitors.get_default().list.map(m => m.height));
-
- return width >= mWidth * threshold && height >= mHeight * threshold;
- }
-
- async update() {
- const results = await Promise.allSettled(
- config.paths.get().map(async p => ({ path: p, files: await this.#listDir(p, "f") }))
- );
- const successes = results.filter(r => r.status === "fulfilled").map(r => r.value);
-
- if (!successes.length) {
- this.#list = [];
- this.notify("list");
- this.#categories = [];
- this.notify("categories");
- return;
- }
-
- const files = successes.map(r => r.files.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,
- thumbnails: {
- compact: await Thumbnailer.thumbnail(p, { width: 60, height: 60, exact: true }),
- medium: await Thumbnailer.thumbnail(p, { width: 400, height: 150, exact: true }),
- large: await Thumbnailer.thumbnail(p, { width: 400, height: 200, exact: true }),
- },
- }))
- );
- this.#list.sort((a, b) => a.path.localeCompare(b.path));
- this.notify("list");
-
- const categories = await Promise.all(successes.map(r => this.#listDir(r.path, "d")));
- this.#categories = categories
- .flatMap(c => c.split("\n"))
- .map(c => ({ path: c, wallpapers: this.#list.filter(w => w.path.startsWith(c)) }))
- .filter(c => c.wallpapers.length > 0)
- .sort((a, b) => a.path.localeCompare(b.path));
- this.notify("categories");
- }
-
- constructor() {
- super();
-
- this.update().catch(console.error);
-
- let monitors = config.paths
- .get()
- .map(p => monitorDirectory(p.path, () => this.update().catch(console.error), p.recursive));
- config.paths.subscribe(v => {
- this.update().catch(console.error);
- for (const m of monitors) m.cancel();
- monitors = v.map(p => monitorDirectory(p.path, () => this.update().catch(console.error), p.recursive));
- });
- }
-}
diff --git a/src/services/weather.ts b/src/services/weather.ts
deleted file mode 100644
index d51e7fc..0000000
--- a/src/services/weather.ts
+++ /dev/null
@@ -1,388 +0,0 @@
-import { weatherIcons } from "@/utils/icons";
-import { notify } from "@/utils/system";
-import {
- execAsync,
- GLib,
- GObject,
- interval,
- property,
- readFileAsync,
- register,
- writeFileAsync,
- type Time,
-} from "astal";
-import { weather as config } from "config";
-
-export interface WeatherCondition {
- text: string;
- icon: string;
- code: number;
-}
-
-interface _WeatherState {
- temp_c: number;
- temp_f: number;
- is_day: number;
- condition: WeatherCondition;
- wind_mph: number;
- wind_kph: number;
- wind_degree: number;
- wind_dir: "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW";
- pressure_mb: number;
- pressure_in: number;
- precip_mm: number;
- precip_in: number;
- humidity: number;
- cloud: number;
- feelslike_c: number;
- feelslike_f: number;
- windchill_c: number;
- windchill_f: number;
- heatindex_c: number;
- heatindex_f: number;
- dewpoint_c: number;
- dewpoint_f: number;
- vis_km: number;
- vis_miles: number;
- uv: number;
- gust_mph: number;
- gust_kph: number;
-}
-
-export interface WeatherCurrent extends _WeatherState {
- last_updated_epoch: number;
- last_updated: string;
-}
-
-export interface WeatherHour extends _WeatherState {
- time_epoch: number;
- time: string;
-}
-
-export interface WeatherDay {
- date: string;
- date_epoch: number;
- day: {
- maxtemp_c: number;
- maxtemp_f: number;
- mintemp_c: number;
- mintemp_f: number;
- avgtemp_c: number;
- avgtemp_f: number;
- maxwind_mph: number;
- maxwind_kph: number;
- totalprecip_mm: number;
- totalprecip_in: number;
- totalsnow_cm: number;
- avgvis_km: number;
- avgvis_miles: number;
- avghumidity: number;
- daily_will_it_rain: number;
- daily_chance_of_rain: number;
- daily_will_it_snow: number;
- daily_chance_of_snow: number;
- condition: WeatherCondition;
- uv: number;
- };
- astro: {
- sunrise: string;
- sunset: string;
- moonrise: string;
- moonset: string;
- moon_phase: string;
- moon_illumination: string;
- is_moon_up: number;
- is_sun_up: number;
- };
- hour: WeatherHour[];
-}
-
-export interface WeatherLocation {
- name: string;
- region: string;
- country: string;
- lat: number;
- lon: number;
- tz_id: string;
- localtime_epoch: number;
- localtime: string;
-}
-
-export interface WeatherData {
- current: WeatherCurrent;
- forecast: { forecastday: WeatherDay[] };
- location: WeatherLocation;
-}
-
-const DEFAULT_STATE: _WeatherState = {
- temp_c: 0,
- temp_f: 0,
- is_day: 0,
- condition: { text: "", icon: "", code: 0 },
- wind_mph: 0,
- wind_kph: 0,
- wind_degree: 0,
- wind_dir: "N",
- pressure_mb: 0,
- pressure_in: 0,
- precip_mm: 0,
- precip_in: 0,
- humidity: 0,
- cloud: 0,
- feelslike_c: 0,
- feelslike_f: 0,
- windchill_c: 0,
- windchill_f: 0,
- heatindex_c: 0,
- heatindex_f: 0,
- dewpoint_c: 0,
- dewpoint_f: 0,
- vis_km: 0,
- vis_miles: 0,
- uv: 0,
- gust_mph: 0,
- gust_kph: 0,
-};
-
-const DEFAULT: WeatherData = {
- current: {
- last_updated_epoch: 0,
- last_updated: "",
- ...DEFAULT_STATE,
- },
- forecast: {
- forecastday: [
- {
- date: "",
- date_epoch: 0,
- day: {
- maxtemp_c: 0,
- maxtemp_f: 0,
- mintemp_c: 0,
- mintemp_f: 0,
- avgtemp_c: 0,
- avgtemp_f: 0,
- maxwind_mph: 0,
- maxwind_kph: 0,
- totalprecip_mm: 0,
- totalprecip_in: 0,
- totalsnow_cm: 0,
- avgvis_km: 0,
- avgvis_miles: 0,
- avghumidity: 0,
- daily_will_it_rain: 0,
- daily_chance_of_rain: 0,
- daily_will_it_snow: 0,
- daily_chance_of_snow: 0,
- condition: { text: "", icon: "", code: 0 },
- uv: 0,
- },
- astro: {
- sunrise: "",
- sunset: "",
- moonrise: "",
- moonset: "",
- moon_phase: "",
- moon_illumination: "",
- is_moon_up: 0,
- is_sun_up: 0,
- },
- hour: Array.from({ length: 24 }, () => ({
- time_epoch: 0,
- time: "",
- ...DEFAULT_STATE,
- })),
- },
- ],
- },
- location: {
- name: "",
- region: "",
- country: "",
- lat: 0,
- lon: 0,
- tz_id: "",
- localtime_epoch: 0,
- localtime: "",
- },
-};
-
-@register({ GTypeName: "Weather" })
-export default class Weather extends GObject.Object {
- static instance: Weather;
- static get_default() {
- if (!this.instance) this.instance = new Weather();
-
- return this.instance;
- }
-
- readonly #cache: string = `${CACHE}/weather.json`;
- #notified = false;
-
- #data: WeatherData = DEFAULT;
-
- #interval: Time | null = null;
-
- @property(Object)
- get raw() {
- return this.#data;
- }
-
- @property(Object)
- get current() {
- return this.#data.current;
- }
-
- @property(Object)
- get forecast() {
- return this.#data.forecast.forecastday[0].hour;
- }
-
- @property(Object)
- get location() {
- return this.#data.location;
- }
-
- @property(String)
- get condition() {
- return this.#data.current.condition.text;
- }
-
- @property(String)
- get temperature() {
- return this.getTemp(this.#data.current);
- }
-
- @property(String)
- get wind() {
- return `${Math.round(this.#data.current[`wind_${config.imperial.get() ? "m" : "k"}ph`])} ${
- config.imperial.get() ? "m" : "k"
- }ph`;
- }
-
- @property(String)
- get rainChance() {
- return this.#data.forecast.forecastday[0].day.daily_chance_of_rain + "%";
- }
-
- @property(String)
- get icon() {
- return this.getIcon(this.#data.current.condition.text);
- }
-
- @property(String)
- get tempIcon() {
- return this.getTempIcon(this.#data.current.temp_c);
- }
-
- @property(String)
- get tempColour() {
- return this.getTempDesc(this.#data.current.temp_c);
- }
-
- getIcon(status: string) {
- let query = status.trim().toLowerCase().replaceAll(" ", "_");
- if (!this.#data.current.is_day && query + "_night" in weatherIcons) query += "_night";
- return weatherIcons[query] ?? weatherIcons.warning;
- }
-
- getTemp(data: _WeatherState) {
- return `${Math.round(data[`temp_${config.imperial.get() ? "f" : "c"}`])}°${config.imperial.get() ? "F" : "C"}`;
- }
-
- getTempIcon(temp: number) {
- if (temp >= 40) return "";
- if (temp >= 30) return "";
- if (temp >= 20) return "";
- if (temp >= 10) return "";
- return "";
- }
-
- getTempDesc(temp: number) {
- if (temp >= 40) return "burning";
- if (temp >= 30) return "hot";
- if (temp >= 20) return "normal";
- if (temp >= 10) return "cold";
- return "freezing";
- }
-
- #notify() {
- this.notify("raw");
- this.notify("current");
- this.notify("forecast");
- this.notify("location");
- this.notify("condition");
- this.notify("temperature");
- this.notify("wind");
- this.notify("rain-chance");
- this.notify("icon");
- this.notify("temp-icon");
- this.notify("temp-colour");
- }
-
- async getWeather() {
- const location = config.location || JSON.parse(await execAsync("curl ipinfo.io")).city;
- const opts = `key=${config.apiKey.get()}&q=${location}&days=1&aqi=no&alerts=no`;
- const url = `https://api.weatherapi.com/v1/forecast.json?${opts}`;
- return JSON.parse(await execAsync(["curl", url]));
- }
-
- async updateWeather() {
- if (!config.apiKey.get()) {
- if (!this.#notified) {
- notify({
- summary: "Weather API key required",
- body: `A weather API key is required to get weather data. Get one from https://www.weatherapi.com.`,
- icon: "dialog-warning-symbolic",
- urgency: "critical",
- actions: {
- "Get API key": () => execAsync(`app2unit -O 'https://www.weatherapi.com'`).catch(print),
- },
- });
- this.#notified = true;
- }
- return;
- }
-
- if (GLib.file_test(this.#cache, GLib.FileTest.EXISTS)) {
- const cache = await readFileAsync(this.#cache);
- const cache_data: WeatherData = JSON.parse(cache);
- if (cache_data.location.localtime_epoch * 1000 + config.interval.get() > Date.now()) {
- if (JSON.stringify(this.#data) !== cache) {
- this.#data = cache_data;
- this.#notify();
- }
- return;
- }
- }
-
- try {
- const data = await this.getWeather();
- this.#data = data;
- writeFileAsync(this.#cache, JSON.stringify(data)).catch(console.error); // Catch here so it doesn't propagate
- } catch (e) {
- console.error("Error getting weather:", e);
- this.#data = DEFAULT;
- }
- this.#notify();
- }
-
- constructor() {
- super();
-
- this.updateWeather().catch(console.error);
- this.#interval = interval(config.interval.get(), () => this.updateWeather().catch(console.error));
-
- config.apiKey.subscribe(() => this.updateWeather());
-
- config.interval.subscribe(i => {
- this.#interval?.cancel();
- this.#interval = interval(i, () => this.updateWeather().catch(console.error));
- });
-
- config.imperial.subscribe(() => {
- this.notify("temperature");
- this.notify("wind");
- });
- }
-}
diff --git a/src/utils/icons.ts b/src/utils/icons.ts
deleted file mode 100644
index f164692..0000000
--- a/src/utils/icons.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import { Apps } from "@/services/apps";
-import { Gio } from "astal";
-import type AstalApps from "gi://AstalApps";
-
-export const osIcons: Record<string, string> = {
- almalinux: "",
- alpine: "",
- arch: "",
- archcraft: "",
- arcolinux: "",
- artix: "",
- centos: "",
- debian: "",
- devuan: "",
- elementary: "",
- endeavouros: "",
- fedora: "",
- freebsd: "",
- garuda: "",
- gentoo: "",
- hyperbola: "",
- kali: "",
- linuxmint: "󰣭",
- mageia: "",
- openmandriva: "",
- manjaro: "",
- neon: "",
- nixos: "",
- opensuse: "",
- suse: "",
- sles: "",
- sles_sap: "",
- "opensuse-tumbleweed": "",
- parrot: "",
- pop: "",
- raspbian: "",
- rhel: "",
- rocky: "",
- slackware: "",
- solus: "",
- steamos: "",
- tails: "",
- trisquel: "",
- ubuntu: "",
- vanilla: "",
- void: "",
- zorin: "",
-};
-
-export const weatherIcons: Record<string, string> = {
- warning: "󰼯",
- sunny: "󰖙",
- clear: "󰖔",
- partly_cloudy: "󰖕",
- partly_cloudy_night: "󰼱",
- cloudy: "󰖐",
- overcast: "󰖕",
- mist: "󰖑",
- patchy_rain_nearby: "󰼳",
- patchy_rain_possible: "󰼳",
- patchy_snow_possible: "󰼴",
- patchy_sleet_possible: "󰙿",
- patchy_freezing_drizzle_possible: "󰙿",
- thundery_outbreaks_possible: "󰙾",
- blowing_snow: "󰼶",
- blizzard: "󰼶",
- fog: "󰖑",
- freezing_fog: "󰖑",
- patchy_light_drizzle: "󰼳",
- light_drizzle: "󰼳",
- freezing_drizzle: "󰙿",
- heavy_freezing_drizzle: "󰙿",
- patchy_light_rain: "󰼳",
- light_rain: "󰼳",
- moderate_rain_at_times: "󰖗",
- moderate_rain: "󰼳",
- heavy_rain_at_times: "󰖖",
- heavy_rain: "󰖖",
- light_freezing_rain: "󰙿",
- moderate_or_heavy_freezing_rain: "󰙿",
- light_sleet: "󰙿",
- moderate_or_heavy_sleet: "󰙿",
- patchy_light_snow: "󰼴",
- light_snow: "󰼴",
- patchy_moderate_snow: "󰼴",
- moderate_snow: "󰼶",
- patchy_heavy_snow: "󰼶",
- heavy_snow: "󰼶",
- ice_pellets: "󰖒",
- light_rain_shower: "󰖖",
- moderate_or_heavy_rain_shower: "󰖖",
- torrential_rain_shower: "󰖖",
- light_sleet_showers: "󰼵",
- moderate_or_heavy_sleet_showers: "󰼵",
- light_snow_showers: "󰼵",
- moderate_or_heavy_snow_showers: "󰼵",
- light_showers_of_ice_pellets: "󰖒",
- moderate_or_heavy_showers_of_ice_pellets: "󰖒",
- patchy_light_rain_with_thunder: "󰙾",
- moderate_or_heavy_rain_with_thunder: "󰙾",
- moderate_or_heavy_rain_in_area_with_thunder: "󰙾",
- patchy_light_snow_with_thunder: "󰼶",
- moderate_or_heavy_snow_with_thunder: "󰼶",
-};
-
-export const desktopEntrySubs: Record<string, string> = {
- Firefox: "firefox",
-};
-
-const categoryIcons: Record<string, string> = {
- WebBrowser: "web",
- Printing: "print",
- Security: "security",
- Network: "chat",
- Archiving: "archive",
- Compression: "archive",
- Development: "code",
- IDE: "code",
- TextEditor: "edit_note",
- Audio: "music_note",
- Music: "music_note",
- Player: "music_note",
- Recorder: "mic",
- Game: "sports_esports",
- FileTools: "files",
- FileManager: "files",
- Filesystem: "files",
- FileTransfer: "files",
- Settings: "settings",
- DesktopSettings: "settings",
- HardwareSettings: "settings",
- TerminalEmulator: "terminal",
- ConsoleOnly: "terminal",
- Utility: "build",
- Monitor: "monitor_heart",
- Midi: "graphic_eq",
- Mixer: "graphic_eq",
- AudioVideoEditing: "video_settings",
- AudioVideo: "music_video",
- Video: "videocam",
- Building: "construction",
- Graphics: "photo_library",
- "2DGraphics": "photo_library",
- RasterGraphics: "photo_library",
- TV: "tv",
- System: "host",
-};
-
-export const getAppCategoryIcon = (nameOrApp: string | AstalApps.Application) => {
- const categories =
- typeof nameOrApp === "string"
- ? Gio.DesktopAppInfo.new(`${nameOrApp}.desktop`)?.get_categories()?.split(";") ??
- Apps.fuzzy_query(nameOrApp)[0]?.categories
- : nameOrApp.categories;
- if (categories)
- for (const [key, value] of Object.entries(categoryIcons)) if (categories.includes(key)) return value;
- return "terminal";
-};
diff --git a/src/utils/mpris.ts b/src/utils/mpris.ts
deleted file mode 100644
index e0cc111..0000000
--- a/src/utils/mpris.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { GLib } from "astal";
-import AstalMpris from "gi://AstalMpris";
-
-const hasPlasmaIntegration = GLib.find_program_in_path("plasma-browser-integration-host") !== null;
-
-export const isRealPlayer = (player?: AstalMpris.Player) =>
- player !== undefined &&
- // Player closed
- player.identity !== null &&
- // Remove unecessary native buses from browsers if there's plasma integration
- !(hasPlasmaIntegration && player.busName.startsWith("org.mpris.MediaPlayer2.firefox")) &&
- !(hasPlasmaIntegration && player.busName.startsWith("org.mpris.MediaPlayer2.chromium")) &&
- // playerctld just copies other buses and we don't need duplicates
- !player.busName.startsWith("org.mpris.MediaPlayer2.playerctld") &&
- // Non-instance mpd bus
- !(player.busName.endsWith(".mpd") && !player.busName.endsWith("MediaPlayer2.mpd"));
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
deleted file mode 100644
index 1edad67..0000000
--- a/src/utils/strings.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export const basename = (path: string, stripExt = true) => {
- const lastSlash = path.lastIndexOf("/");
- const lastDot = path.lastIndexOf(".");
- return path.slice(lastSlash + 1, stripExt && lastDot > lastSlash ? lastDot : undefined);
-};
-
-export const pathToFileName = (path: string, ext?: string) => {
- const start = /[a-z]+:\/\//.test(path) ? 0 : path.indexOf("/") + 1;
- const dir = path.slice(start, path.lastIndexOf("/")).replaceAll("/", "-");
- return `${dir}-${basename(path, ext !== undefined)}${ext ? `.${ext}` : ""}`;
-};
-
-export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
-
-export const lengthStr = (length: number) =>
- `${Math.floor(length / 60)}:${Math.floor(length % 60)
- .toString()
- .padStart(2, "0")}`;
diff --git a/src/utils/system.ts b/src/utils/system.ts
deleted file mode 100644
index 3a9caa6..0000000
--- a/src/utils/system.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { bind, execAsync, Gio, GLib, Variable, type Binding } from "astal";
-import type AstalApps from "gi://AstalApps";
-import { osIcons } from "./icons";
-
-/**
- * See https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html
- * @param exec The exec field in a desktop file
- */
-const execToCmd = (app: AstalApps.Application) => {
- let exec = app.executable.replace(/%[fFuUdDnNvm]/g, ""); // Remove useless field codes
- exec = exec.replace(/%i/g, app.iconName ? `--icon ${app.iconName}` : ""); // Replace %i app icon
- exec = exec.replace(/%c/g, app.name); // Replace %c with app name
- exec = exec.replace(/%k/g, (app.app as Gio.DesktopAppInfo).get_filename() ?? ""); // Replace %k with desktop file path
- return exec;
-};
-
-export const launch = (app: AstalApps.Application) => {
- let now = Date.now();
- execAsync(["app2unit", "--", app.entry]).catch(() => {
- // Try manual exec if launch fails (exits with error within 1 second)
- if (Date.now() - now < 1000) {
- now = Date.now();
- execAsync(["app2unit", "--", execToCmd(app)]).catch(() => {
- // Fallback to regular launch
- if (Date.now() - now < 1000) {
- app.frequency--; // Decrement frequency cause launch also increments it
- app.launch();
- }
- });
- }
- });
- app.frequency++;
-};
-
-export const notify = (props: {
- summary: string;
- body?: string;
- icon?: string;
- urgency?: "low" | "normal" | "critical";
- transient?: boolean;
- actions?: Record<string, () => void>;
-}) =>
- execAsync([
- "notify-send",
- "-a",
- "caelestia-shell",
- ...(props.icon ? ["-i", props.icon] : []),
- ...(props.urgency ? ["-u", props.urgency] : []),
- ...(props.transient ? ["-e"] : []),
- ...Object.keys(props.actions ?? {}).flatMap((k, i) => ["-A", `${i}=${k}`]),
- props.summary,
- ...(props.body ? [props.body] : []),
- ])
- .then(action => props.actions && Object.values(props.actions)[parseInt(action, 10)]?.())
- .catch(console.error);
-
-export const osId = GLib.get_os_info("ID") ?? "unknown";
-export const osIdLike = GLib.get_os_info("ID_LIKE");
-export const osIcon = (() => {
- if (osIcons.hasOwnProperty(osId)) return osIcons[osId];
- if (osIdLike) for (const id of osIdLike.split(" ")) if (osIcons.hasOwnProperty(id)) return osIcons[id];
- return "";
-})();
-
-export const currentTime = Variable(GLib.DateTime.new_now_local()).poll(1000, () => GLib.DateTime.new_now_local());
-export const bindCurrentTime = (
- format: Binding<string> | string,
- fallback?: (time: GLib.DateTime) => string,
- self?: JSX.Element
-) => {
- const fmt = (c: GLib.DateTime, format: string) => c.format(format) ?? fallback?.(c) ?? new Date().toLocaleString();
- if (typeof format === "string") return bind(currentTime).as(c => fmt(c, format));
- if (!self) throw new Error("bindCurrentTime: self is required when format is a Binding");
- const time = Variable.derive([currentTime, format], (c, f) => fmt(c, f));
- self?.connect("destroy", () => time.drop());
- return bind(time);
-};
-
-const monitors = new Set();
-export const monitorDirectory = (
- path: string,
- callback: (
- source: Gio.FileMonitor,
- file: Gio.File,
- other_file: Gio.File | null,
- type: Gio.FileMonitorEvent
- ) => void,
- recursive: boolean = true
-) => {
- const file = Gio.file_new_for_path(path.replace("~", HOME));
- const monitor = file.monitor_directory(null, null);
- monitor.connect("changed", (m, f1, f2, t) => {
- if (t === Gio.FileMonitorEvent.CHANGES_DONE_HINT || t === Gio.FileMonitorEvent.DELETED) callback(m, f1, f2, t);
- });
-
- 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) {
- const m = monitorDirectory(`${path}/${child.get_name()}`, callback, recursive);
- monitor.connect("notify::cancelled", () => m.cancel());
- }
- }
-
- // Keep ref to monitor so it doesn't get GC'd
- monitors.add(monitor);
- monitor.connect("notify::cancelled", () => monitor.cancelled && monitors.delete(monitor));
-
- return monitor;
-};
diff --git a/src/utils/thumbnailer.ts b/src/utils/thumbnailer.ts
deleted file mode 100644
index d23dab1..0000000
--- a/src/utils/thumbnailer.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { execAsync, GLib, type Variable } from "astal";
-import { thumbnailer as config } from "config";
-
-export interface ThumbOpts {
- width?: number;
- height?: number;
- exact?: boolean;
-}
-
-export default class Thumbnailer {
- static readonly thumbnailDir = `${CACHE}/thumbnails`;
-
- static readonly #running = new Set<string>();
-
- static getOpt<T extends keyof ThumbOpts>(opt: T, opts: ThumbOpts) {
- return opts[opt] ?? (config.defaults[opt] as Variable<NonNullable<ThumbOpts[T]>>).get();
- }
-
- static async getThumbPath(path: string, opts: ThumbOpts) {
- const hash = (await execAsync(`sha1sum ${path}`)).split(" ")[0];
- const size = `${this.getOpt("width", opts)}x${this.getOpt("height", opts)}`;
- const exact = this.getOpt("exact", opts) ? "-exact" : "";
- return `${this.thumbnailDir}/${hash}@${size}${exact}.png`;
- }
-
- static async shouldThumbnail(path: string, opts: ThumbOpts) {
- const [w, h] = (await execAsync(`identify -ping -format "%w %h" ${path}`)).split(" ").map(parseInt);
- return w > this.getOpt("width", opts) || h > this.getOpt("height", opts);
- }
-
- static async #thumbnail(path: string, opts: ThumbOpts, attempts: number): Promise<string> {
- const thumbPath = await this.getThumbPath(path, opts);
-
- try {
- const width = this.getOpt("width", opts);
- const height = this.getOpt("height", opts);
- const cropCmd = this.getOpt("exact", opts)
- ? `-background none -gravity center -extent ${width}x${height}`
- : "";
- await execAsync(`magick ${path} -thumbnail ${width}x${height}^ ${cropCmd} -unsharp 0x.5 ${thumbPath}`);
- } catch {
- if (attempts >= config.maxAttempts.get()) {
- console.error(`Failed to generate thumbnail for ${path}`);
- return path;
- }
-
- await new Promise(r => setTimeout(r, config.timeBetweenAttempts.get()));
- return this.#thumbnail(path, opts, attempts + 1);
- }
-
- return thumbPath;
- }
-
- static async thumbnail(path: string, opts: ThumbOpts = {}): Promise<string> {
- if (!(await this.shouldThumbnail(path, opts))) return path;
-
- let thumbPath = await this.getThumbPath(path, opts);
-
- // Wait for existing thumbnail for path to finish
- while (this.#running.has(path)) await new Promise(r => setTimeout(r, 100));
-
- // If no thumbnail, generate
- if (!GLib.file_test(thumbPath, GLib.FileTest.EXISTS)) {
- this.#running.add(path);
-
- thumbPath = await this.#thumbnail(path, opts, 0);
-
- this.#running.delete(path);
- }
-
- return thumbPath;
- }
-
- // Static class
- private constructor() {}
-
- static {
- GLib.mkdir_with_parents(this.thumbnailDir, 0o755);
- }
-}
diff --git a/src/utils/types.ts b/src/utils/types.ts
deleted file mode 100644
index d2c1943..0000000
--- a/src/utils/types.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { astalify } from "astal/gtk3";
-import type AstalHyprland from "gi://AstalHyprland";
-
-export type AstalWidget = InstanceType<ReturnType<typeof astalify>>;
-
-export type Address = `0x${string}`;
-
-export interface Client {
- address: Address;
- mapped: boolean;
- hidden: boolean;
- at: [number, number];
- size: [number, number];
- workspace: {
- id: number;
- name: string;
- };
- floating: boolean;
- pseudo: boolean;
- monitor: number;
- class: string;
- title: string;
- initialClass: string;
- initialTitle: string;
- pid: number;
- xwayland: boolean;
- pinned: boolean;
- fullscreen: AstalHyprland.Fullscreen;
- fullscreenClient: AstalHyprland.Fullscreen;
- grouped: Address[];
- tags: string[];
- swallowing: string;
- focusHistoryID: number;
- inhibitingIdle: boolean;
-}
diff --git a/src/utils/widgets.ts b/src/utils/widgets.ts
deleted file mode 100644
index bef79f2..0000000
--- a/src/utils/widgets.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { Binding, idle, register } from "astal";
-import { Astal, astalify, Gtk, Widget, type ConstructProps } from "astal/gtk3";
-import AstalHyprland from "gi://AstalHyprland";
-import type { AstalWidget } from "./types";
-
-export const setupCustomTooltip = (
- self: AstalWidget,
- text: string | Binding<string>,
- labelProps: Widget.LabelProps = {}
-) => {
- if (!text) return null;
-
- self.set_has_tooltip(true);
-
- const window = new Widget.Window({
- visible: false,
- namespace: "caelestia-tooltip",
- layer: Astal.Layer.OVERLAY,
- keymode: Astal.Keymode.NONE,
- exclusivity: Astal.Exclusivity.IGNORE,
- anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT,
- child: new Widget.Label({ ...labelProps, className: "tooltip", label: text }),
- });
- self.set_tooltip_window(window);
-
- if (text instanceof Binding) self.hook(text, (_, v) => !v && window.hide());
-
- const positionWindow = ({ x, y }: { x: number; y: number }) => {
- const { width: mWidth, height: mHeight } = AstalHyprland.get_default().get_focused_monitor();
- const { width: pWidth, height: pHeight } = window.get_preferred_size()[1]!;
- const cursorSize = Gtk.Settings.get_default()?.gtkCursorThemeSize ?? 0;
-
- let marginLeft = x - pWidth / 2;
- if (marginLeft < 0) marginLeft = 0;
- else if (marginLeft + pWidth > mWidth) marginLeft = mWidth - pWidth;
-
- let marginTop = y + cursorSize;
- if (marginTop < 0) marginTop = 0;
- else if (marginTop + pHeight > mHeight) marginTop = y - pHeight;
-
- window.marginLeft = marginLeft;
- window.marginTop = marginTop;
- };
-
- let lastPos = { x: 0, y: 0 };
-
- window.connect("size-allocate", () => positionWindow(lastPos));
- self.connect("query-tooltip", () => {
- if (text instanceof Binding && !text.get()) return false;
- if (window.visible) return true;
-
- const cPos = AstalHyprland.get_default().get_cursor_position();
- positionWindow(cPos);
- lastPos = cPos;
-
- return true;
- });
-
- self.connect("destroy", () => window.destroy());
-
- return window;
-};
-
-export const setupChildClickthrough = (self: AstalWidget) => {
- self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes());
- self.set_size_request(1, 1);
- idle(() => self.set_size_request(-1, -1));
-};
-
-@register()
-export class MenuItem extends astalify(Gtk.MenuItem) {
- constructor(props: ConstructProps<MenuItem, Gtk.MenuItem.ConstructorProps, { onActivate: [] }>) {
- super(props as any);
- }
-}
-
-@register()
-export class FlowBox extends astalify(Gtk.FlowBox) {
- constructor(props: ConstructProps<FlowBox, Gtk.FlowBox.ConstructorProps>) {
- super(props as any);
- }
-}
diff --git a/src/widgets/notification.tsx b/src/widgets/notification.tsx
deleted file mode 100644
index 0dfd368..0000000
--- a/src/widgets/notification.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import { desktopEntrySubs } from "@/utils/icons";
-import Thumbnailer from "@/utils/thumbnailer";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, GLib, register, timeout, Variable } from "astal";
-import { Astal, Gtk, Widget } from "astal/gtk3";
-import { notifpopups as config } from "config";
-import AstalNotifd from "gi://AstalNotifd";
-
-const urgencyToString = (urgency: AstalNotifd.Urgency) => {
- switch (urgency) {
- case AstalNotifd.Urgency.LOW:
- return "low";
- case AstalNotifd.Urgency.NORMAL:
- return "normal";
- case AstalNotifd.Urgency.CRITICAL:
- return "critical";
- }
-};
-
-const getTime = (time: number) => {
- const messageTime = GLib.DateTime.new_from_unix_local(time);
- const now = GLib.DateTime.new_now_local();
- const todayDay = now.get_day_of_year();
-
- if (config.agoTime.get()) {
- const diff = now.difference(messageTime) / 1e6;
- if (diff < 60) return "Now";
- if (diff < 3600) {
- const d = Math.floor(diff / 60);
- return `${d} min${d === 1 ? "" : "s"} ago`;
- }
- if (diff < 86400) {
- const d = Math.floor(diff / 3600);
- return `${d} hour${d === 1 ? "" : "s"} ago`;
- }
- } else if (messageTime.get_day_of_year() === todayDay) {
- const aMinuteAgo = GLib.DateTime.new_now_local().add_seconds(-60);
- return aMinuteAgo !== null && messageTime.compare(aMinuteAgo) > 0 ? "Now" : messageTime.format("%H:%M")!;
- }
-
- if (messageTime.get_day_of_year() === todayDay - 1) return "Yesterday";
- return messageTime.format("%d/%m")!;
-};
-
-const AppIcon = ({ appIcon, desktopEntry }: { appIcon: string; desktopEntry: string }) => {
- // Try app icon
- let icon = Astal.Icon.lookup_icon(appIcon) && appIcon;
- // Try desktop entry
- if (!icon) {
- if (desktopEntrySubs.hasOwnProperty(desktopEntry)) icon = desktopEntrySubs[desktopEntry];
- else if (Astal.Icon.lookup_icon(desktopEntry)) icon = desktopEntry;
- }
- return icon ? <icon className="app-icon" icon={icon} /> : null;
-};
-
-const Image = ({ compact, icon }: { compact?: boolean; icon: string }) => {
- if (GLib.file_test(icon, GLib.FileTest.EXISTS))
- return (
- <box
- valign={Gtk.Align.START}
- className={`image ${compact ? "small" : ""}`}
- setup={self =>
- Thumbnailer.thumbnail(icon)
- .then(p => (self.css = `background-image: url("${p}");`))
- .catch(console.error)
- }
- />
- );
- if (Astal.Icon.lookup_icon(icon))
- return <icon valign={Gtk.Align.START} className={`image ${compact ? "small" : ""}`} icon={icon} />;
- return null;
-};
-
-@register()
-export default class Notification extends Widget.Box {
- readonly #revealer;
- #destroyed = false;
-
- constructor({
- notification,
- popup,
- compact = popup,
- }: {
- notification: AstalNotifd.Notification;
- popup?: boolean;
- compact?: boolean;
- }) {
- super({ className: "notification" });
-
- const time = Variable(getTime(notification.time)).poll(60000, () => getTime(notification.time));
- this.hook(config.agoTime, () => time.set(getTime(notification.time)));
-
- this.#revealer = (
- <revealer
- revealChild={popup}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- >
- <box className="wrapper">
- <box vertical className={`inner ${urgencyToString(notification.urgency)}`}>
- <box className="header">
- <AppIcon appIcon={notification.appIcon} desktopEntry={notification.appName} />
- <label className="app-name" label={notification.appName ?? "Unknown"} />
- <box hexpand />
- <label className="time" label={bind(time)} onDestroy={() => time.drop()} />
- </box>
- <box hexpand className="separator" />
- <box className="content">
- {notification.image && <Image compact={compact} icon={notification.image} />}
- <box vertical>
- <label className="summary" xalign={0} label={notification.summary} truncate />
- {notification.body && (
- <label
- className="body"
- xalign={0}
- label={compact ? notification.body.split("\n")[0] : notification.body}
- wrap
- lines={compact ? 1 : -1}
- truncate={compact}
- setup={self => compact && !popup && setupCustomTooltip(self, notification.body)}
- />
- )}
- </box>
- </box>
- {!popup && (
- <box className="actions">
- <button
- hexpand
- cursor="pointer"
- onClicked={() => notification.dismiss()}
- label="Close"
- />
- {notification.actions.map(a => (
- <button hexpand cursor="pointer" onClicked={() => notification.invoke(a.id)}>
- {notification.actionIcons ? <icon icon={a.label} /> : a.label}
- </button>
- ))}
- </box>
- )}
- </box>
- </box>
- </revealer>
- ) as Widget.Revealer;
- this.add(this.#revealer);
-
- // Init animation
- const width = this.get_preferred_width()[1];
- if (popup) this.css = `margin-left: ${width}px; margin-right: -${width}px;`;
- timeout(1, () => {
- this.#revealer.revealChild = true;
- this.css = `transition: 300ms cubic-bezier(0.05, 0.9, 0.1, 1.1); margin-left: 0; margin-right: 0;`;
- });
-
- // Close popup after timeout if transient or expire enabled in config
- if (popup && (config.expire.get() || notification.transient))
- timeout(
- notification.expireTimeout > 0
- ? notification.expireTimeout
- : notification.urgency === AstalNotifd.Urgency.CRITICAL
- ? 10000
- : 5000,
- () => this.destroyWithAnims()
- );
- }
-
- destroyWithAnims() {
- if (this.#destroyed) return;
- this.#destroyed = true;
-
- const animTime = 120;
- const animMargin = this.get_allocated_width();
- this.css = `transition: ${animTime}ms cubic-bezier(0.85, 0, 0.15, 1);
- margin-left: ${animMargin}px; margin-right: -${animMargin}px;`;
- timeout(animTime, () => {
- this.#revealer.revealChild = false;
- timeout(this.#revealer.transitionDuration, () => this.destroy());
- });
- }
-}
diff --git a/src/widgets/popupwindow.ts b/src/widgets/popupwindow.ts
deleted file mode 100644
index 5ffa061..0000000
--- a/src/widgets/popupwindow.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Binding, register } from "astal";
-import { App, Astal, Gdk, Widget } from "astal/gtk3";
-import { bar } from "config";
-import AstalHyprland from "gi://AstalHyprland";
-
-const extendProp = <T>(
- prop: T | Binding<T | undefined> | undefined,
- override: (prop: T | undefined) => T | undefined
-) => prop && (prop instanceof Binding ? prop.as(override) : override(prop));
-
-@register()
-export default class PopupWindow extends Widget.Window {
- constructor(props: Widget.WindowProps) {
- super({
- keymode: Astal.Keymode.ON_DEMAND,
- borderWidth: 20, // To allow shadow, cause if not it gets cut off
- ...props,
- visible: false,
- application: App,
- name: props.monitor ? extendProp(props.name, n => (n ? n + props.monitor : undefined)) : props.name,
- namespace: extendProp(props.name, n => `caelestia-${n}`),
- onKeyPressEvent: (self, event) => {
- // Close window on escape
- if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide();
-
- return props.onKeyPressEvent?.(self, event);
- },
- });
- }
-
- popup_at_widget(widget: JSX.Element, event: Gdk.Event | Astal.ClickEvent) {
- const { width, height } = widget.get_allocation();
- const { width: mWidth, height: mHeight } = AstalHyprland.get_default().get_focused_monitor();
- const pWidth = this.get_preferred_width()[1];
- const pHeight = this.get_preferred_height()[1];
- const [, x, y] = event instanceof Gdk.Event ? event.get_coords() : [null, event.x, event.y];
- const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position();
-
- let marginLeft = 0;
- let marginTop = 0;
- if (bar.vertical.get()) {
- marginLeft = cx + (width - x);
- marginTop = cy + ((height - pHeight) / 2 - y);
- if (marginTop < 0) marginTop = 0;
- else if (marginTop + pHeight > mHeight) marginTop = mHeight - pHeight;
- } else {
- marginLeft = cx + ((width - pWidth) / 2 - x);
- if (marginLeft < 0) marginLeft = 0;
- else if (marginLeft + pWidth > mWidth) marginLeft = mWidth - pWidth;
- marginTop = cy + (height - y);
- }
-
- this.anchor = Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT;
- this.exclusivity = Astal.Exclusivity.IGNORE;
- this.marginLeft = marginLeft;
- this.marginTop = marginTop;
-
- this.show();
- }
-
- popup_at_corner(corner: `${"top" | "bottom"} ${"left" | "right"}`) {
- let anchor = 0;
- if (corner.includes("top")) anchor |= Astal.WindowAnchor.TOP;
- else anchor |= Astal.WindowAnchor.BOTTOM;
- if (corner.includes("left")) anchor |= Astal.WindowAnchor.LEFT;
- else anchor |= Astal.WindowAnchor.RIGHT;
-
- this.anchor = anchor;
- this.exclusivity = Astal.Exclusivity.NORMAL;
- this.marginLeft = 0;
- this.marginTop = 0;
-
- this.show();
- }
-}
diff --git a/src/widgets/screencorner.tsx b/src/widgets/screencorner.tsx
deleted file mode 100644
index a55d782..0000000
--- a/src/widgets/screencorner.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { Binding } from "astal";
-import { Gtk, type Widget } from "astal/gtk3";
-import type cairo from "cairo";
-
-type Place = "topleft" | "topright" | "bottomleft" | "bottomright";
-
-export default ({ place, ...rest }: Widget.DrawingAreaProps & { place: Place | Binding<Place> }) => (
- <drawingarea
- {...rest}
- className="screen-corner"
- setup={self => {
- self.connect("realize", () => self.get_window()?.set_pass_through(true));
-
- const r = self.get_style_context().get_property("border-radius", Gtk.StateFlags.NORMAL) as number;
- self.set_size_request(r, r);
- self.connect("draw", (_, cr: cairo.Context) => {
- const c = self.get_style_context().get_background_color(Gtk.StateFlags.NORMAL);
- const r = self.get_style_context().get_property("border-radius", Gtk.StateFlags.NORMAL) as number;
- self.set_size_request(r, r);
-
- switch (typeof place === "string" ? place : place.get()) {
- case "topleft":
- cr.arc(r, r, r, Math.PI, (3 * Math.PI) / 2);
- cr.lineTo(0, 0);
- break;
-
- case "topright":
- cr.arc(0, r, r, (3 * Math.PI) / 2, 2 * Math.PI);
- cr.lineTo(r, 0);
- break;
-
- case "bottomleft":
- cr.arc(r, 0, r, Math.PI / 2, Math.PI);
- cr.lineTo(0, r);
- break;
-
- case "bottomright":
- cr.arc(0, 0, r, 0, Math.PI / 2);
- cr.lineTo(r, r);
- break;
- }
-
- cr.closePath();
- cr.setSourceRGBA(c.red, c.green, c.blue, c.alpha);
- cr.fill();
- });
- }}
- />
-);
diff --git a/src/widgets/slider.tsx b/src/widgets/slider.tsx
deleted file mode 100644
index 0a66609..0000000
--- a/src/widgets/slider.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { bind, type Binding } from "astal";
-import { Gdk, Gtk, type Widget } from "astal/gtk3";
-import type cairo from "cairo";
-
-export default ({
- value,
- onChange,
-}: {
- value: Binding<number>;
- onChange?: (self: Widget.DrawingArea, value: number) => void;
-}) => (
- <drawingarea
- hexpand
- valign={Gtk.Align.CENTER}
- className="slider"
- css={bind(value).as(v => `font-size: ${Math.min(1, Math.max(0, v))}px;`)}
- setup={self => {
- const halfPi = Math.PI / 2;
-
- const styleContext = self.get_style_context();
- self.set_size_request(-1, styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number);
-
- self.connect("draw", (_, cr: cairo.Context) => {
- const styleContext = self.get_style_context();
-
- const width = self.get_allocated_width();
- const height = styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number;
- self.set_size_request(-1, height);
-
- const progressValue = styleContext.get_property("font-size", Gtk.StateFlags.NORMAL) as number;
- let radius = styleContext.get_property("border-radius", Gtk.StateFlags.NORMAL) as number;
-
- const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
-
- // Background
- cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
- cr.arc(width - radius, radius, radius, -halfPi, 0); // Top right
- cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
- cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
- cr.fill();
-
- // Flatten when near 0
- radius = Math.min(radius, Math.min(width * progressValue, height) / 2);
-
- const progressPosition = width * progressValue - radius;
- const fg = styleContext.get_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
-
- // Foreground
- cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
- cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
- cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
- cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
- cr.fill();
- });
-
- self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
- self.connect("button-press-event", (_, event: Gdk.Event) =>
- onChange?.(self, event.get_coords()[1] / self.get_allocated_width())
- );
- }}
- />
-);
diff --git a/style.scss b/style.scss
deleted file mode 100644
index c7eb766..0000000
--- a/style.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-@use "sass:color";
-@use "scss/scheme";
-
-// Common styles
-@use "scss/widgets";
-@use "scss/common";
-
-// Modules
-@use "scss/bar";
-@use "scss/notifpopups";
-@use "scss/launcher";
-@use "scss/osds";
-@use "scss/session";
-@use "scss/sidebar";
-@use "scss/navbar";
-@use "scss/mediadisplay";
-
-* {
- all: unset; // Remove GTK theme styles
- caret-color: scheme.$rosewater;
-
- selection {
- background-color: color.change(scheme.$overlay2, $alpha: 0.3);
- }
-}
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 944ac5a..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/tsconfig",
- "compilerOptions": {
- "experimentalDecorators": true,
- "strict": true,
- "target": "ES2022",
- "module": "ES2022",
- "moduleResolution": "Bundler",
- // "checkJs": true,
- // "allowJs": true,
- "jsx": "react-jsx",
- "jsxImportSource": "/usr/share/astal/gjs/gtk3",
- "paths": {
- "astal": ["/usr/share/astal/gjs"],
- "astal/*": ["/usr/share/astal/gjs/*"],
- "config": ["./src/config/index"],
- "config/*": ["./src/config/*"],
- "@/*": ["./src/*"]
- }
- }
-}