clean
Remove everything
3
.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
|||
@girs/
|
||||
node_modules/
|
||||
scss/scheme/_index.scss
|
||||
3
.vscode/settings.json
vendored
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
132
app.tsx
|
|
@ -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");
|
||||
},
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 746 B |
|
Before Width: | Height: | Size: 13 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 1 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 608 B |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 640 B |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 819 B |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 820 B |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
4
env.d.ts
vendored
|
|
@ -1,4 +0,0 @@
|
|||
declare const HOME: string;
|
||||
declare const CACHE: string;
|
||||
declare const STATE: string;
|
||||
declare const SRC: string;
|
||||
616
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
17
run.fish
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
394
scss/bar.scss
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1118
scss/sidebar.scss
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
};
|
||||
|
|
@ -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));
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
];
|
||||
|
|
@ -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[] };
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
});
|
||||
|
|
@ -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} />
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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]);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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("&", "&")}</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("&", "&")}</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("&", "&");
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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("&", "&");
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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("&", "&")}</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("&", "&");
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import AstalApps from "gi://AstalApps";
|
||||
|
||||
export const Apps = new AstalApps.Apps();
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
|
|
@ -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"));
|
||||
|
|
@ -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")}`;
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
@ -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())
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
25
style.scss
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||