summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-04-26 22:36:23 +1000
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-04-26 22:36:23 +1000
commit3c579d0e275cdaf6f2c9589abade94bde7905c82 (patch)
tree4b825dc642cb6eb9a060e54bf8d69288fbee4904 /src
parentschemes: fix (diff)
downloadcaelestia-shell-3c579d0e275cdaf6f2c9589abade94bde7905c82.tar.gz
caelestia-shell-3c579d0e275cdaf6f2c9589abade94bde7905c82.tar.bz2
caelestia-shell-3c579d0e275cdaf6f2c9589abade94bde7905c82.zip
clean
Remove everything
Diffstat (limited to 'src')
-rw-r--r--src/config/defaults.ts183
-rw-r--r--src/config/funcs.ts124
-rw-r--r--src/config/index.ts26
-rw-r--r--src/config/literals.ts336
-rw-r--r--src/config/types.ts93
-rw-r--r--src/modules/bar.tsx703
-rw-r--r--src/modules/launcher/actions.tsx522
-rw-r--r--src/modules/launcher/index.tsx144
-rw-r--r--src/modules/launcher/modes.tsx225
-rw-r--r--src/modules/launcher/util.tsx19
-rw-r--r--src/modules/mediadisplay/index.tsx188
-rw-r--r--src/modules/mediadisplay/visualiser.tsx71
-rw-r--r--src/modules/navbar.tsx203
-rw-r--r--src/modules/notifpopups.tsx72
-rw-r--r--src/modules/osds.tsx327
-rw-r--r--src/modules/screencorners.tsx51
-rw-r--r--src/modules/session.tsx44
-rw-r--r--src/modules/sidebar/alerts.tsx11
-rw-r--r--src/modules/sidebar/audio.tsx13
-rw-r--r--src/modules/sidebar/connectivity.tsx10
-rw-r--r--src/modules/sidebar/dashboard.tsx132
-rw-r--r--src/modules/sidebar/index.tsx87
-rw-r--r--src/modules/sidebar/modules/bluetooth.tsx127
-rw-r--r--src/modules/sidebar/modules/calendar.tsx252
-rw-r--r--src/modules/sidebar/modules/deviceselector.tsx126
-rw-r--r--src/modules/sidebar/modules/headlines.tsx204
-rw-r--r--src/modules/sidebar/modules/hwresources.tsx67
-rw-r--r--src/modules/sidebar/modules/media.tsx168
-rw-r--r--src/modules/sidebar/modules/networks.tsx151
-rw-r--r--src/modules/sidebar/modules/news.tsx113
-rw-r--r--src/modules/sidebar/modules/notifications.tsx90
-rw-r--r--src/modules/sidebar/modules/streams.tsx110
-rw-r--r--src/modules/sidebar/modules/upcoming.tsx99
-rw-r--r--src/modules/sidebar/modules/updates.tsx109
-rw-r--r--src/modules/sidebar/packages.tsx11
-rw-r--r--src/modules/sidebar/time.tsx24
-rw-r--r--src/services/apps.ts3
-rw-r--r--src/services/calendar.ts228
-rw-r--r--src/services/cpu.ts49
-rw-r--r--src/services/gpu.ts63
-rw-r--r--src/services/math.ts155
-rw-r--r--src/services/memory.ts64
-rw-r--r--src/services/monitors.ts127
-rw-r--r--src/services/news.ts153
-rw-r--r--src/services/palette.ts298
-rw-r--r--src/services/players.ts148
-rw-r--r--src/services/schemes.ts109
-rw-r--r--src/services/storage.ts65
-rw-r--r--src/services/updates.ts191
-rw-r--r--src/services/wallpapers.ts127
-rw-r--r--src/services/weather.ts388
-rw-r--r--src/utils/icons.ts158
-rw-r--r--src/utils/mpris.ts16
-rw-r--r--src/utils/strings.ts18
-rw-r--r--src/utils/system.ts111
-rw-r--r--src/utils/thumbnailer.ts80
-rw-r--r--src/utils/types.ts35
-rw-r--r--src/utils/widgets.ts82
-rw-r--r--src/widgets/notification.tsx179
-rw-r--r--src/widgets/popupwindow.ts75
-rw-r--r--src/widgets/screencorner.tsx49
-rw-r--r--src/widgets/slider.tsx64
62 files changed, 0 insertions, 8270 deletions
diff --git a/src/config/defaults.ts b/src/config/defaults.ts
deleted file mode 100644
index a5ebbbc..0000000
--- a/src/config/defaults.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import { Astal } from "astal/gtk3";
-
-export default {
- style: {
- transparency: "normal", // One of "off", "low", "normal", "high"
- borders: true,
- vibrant: false, // Extra saturation
- },
- config: {
- notifyOnError: true,
- },
- // Modules
- bar: {
- vertical: true,
- style: "gaps", // One of "gaps", "panel", "embedded"
- layout: {
- type: "centerbox", // One of "centerbox", "flowbox"
- centerbox: {
- start: ["osIcon", "activeWindow", "mediaPlaying", "brightnessSpacer"],
- center: ["workspaces"],
- end: [
- "volumeSpacer",
- "tray",
- "statusIcons",
- "pkgUpdates",
- "notifCount",
- "battery",
- "dateTime",
- "power",
- ],
- },
- flowbox: [
- "osIcon",
- "workspaces",
- "brightnessSpacer",
- "activeWindow",
- "volumeSpacer",
- "dateTime",
- "tray",
- "battery",
- "statusIcons",
- "notifCount",
- "power",
- ],
- },
- modules: {
- workspaces: {
- shown: 5,
- showLabels: false,
- labels: ["󰮯", "󰮯", "󰮯", "󰮯", "󰮯"],
- xalign: -1,
- showWindows: false,
- },
- dateTime: {
- format: "%d/%m/%y %R",
- detailedFormat: "%c",
- },
- },
- },
- launcher: {
- style: "lines", // One of "lines", "round"
- actionPrefix: ">", // Prefix for launcher actions
- apps: {
- maxResults: 30, // Actual max results, -1 for infinite
- },
- files: {
- maxResults: 40, // Actual max results, -1 for infinite
- fdOpts: ["-a", "-t", "f"], // Options to pass to `fd`
- shortenThreshold: 30, // Threshold to shorten paths in characters
- },
- math: {
- maxResults: 40, // Actual max results, -1 for infinite
- },
- todo: {
- notify: true,
- },
- wallpaper: {
- maxResults: 20, // Actual max results, -1 for infinite
- showAllEmpty: true, // Show all wallpapers when search is empty
- style: "medium", // One of "compact", "medium", "large"
- },
- disabledActions: ["logout", "shutdown", "reboot", "hibernate"], // Actions to hide, see launcher/actions.tsx for available actions
- },
- notifpopups: {
- maxPopups: -1,
- expire: false,
- agoTime: true, // Whether to show time in ago format, e.g. 10 mins ago, or raw time, e.g. 10:42
- },
- osds: {
- volume: {
- position: Astal.WindowAnchor.RIGHT, // Top = 2, Right = 4, Left = 8, Bottom = 16
- margin: 20,
- hideDelay: 1500,
- showValue: true,
- },
- brightness: {
- position: Astal.WindowAnchor.LEFT, // Top = 2, Right = 4, Left = 8, Bottom = 16
- margin: 20,
- hideDelay: 1500,
- showValue: true,
- },
- lock: {
- spacing: 5,
- caps: {
- hideDelay: 1000,
- },
- num: {
- hideDelay: 1000,
- },
- },
- },
- sidebar: {
- showOnStartup: false,
- modules: {
- headlines: {
- enabled: true,
- },
- },
- },
- navbar: {
- persistent: false, // Whether to show all the time or only on hover
- appearWidth: 10, // The width in pixels of the hover area for the navbar to show up
- showLabels: false, // Whether to show labels for active buttons
- },
- // Services
- math: {
- maxHistory: 100,
- },
- updates: {
- interval: 900000,
- },
- weather: {
- interval: 600000,
- apiKey: "", // An API key from https://weatherapi.com for accessing weather data
- location: "", // Location as a string or empty to autodetect
- imperial: false,
- },
- cpu: {
- interval: 2000,
- },
- gpu: {
- interval: 2000,
- },
- memory: {
- interval: 5000,
- },
- storage: {
- interval: 5000,
- },
- wallpapers: {
- paths: [
- {
- recursive: true, // Whether to search recursively
- path: "~/Pictures/Wallpapers", // Path to search
- threshold: 0.8, // The threshold to filter wallpapers by size (e.g. 0.8 means wallpaper must be at least 80% of the screen size), 0 to disable
- },
- ],
- },
- calendar: {
- webcals: [] as string[], // An array of urls to ICS files which you can curl
- upcomingDays: 7, // Number of days which count as upcoming
- notify: true,
- },
- thumbnailer: {
- maxAttempts: 5,
- timeBetweenAttempts: 300,
- defaults: {
- width: 100,
- height: 100,
- exact: true,
- },
- },
- news: {
- apiKey: "", // An API key from https://newsdata.io for accessing news
- countries: ["current"], // A list of country codes or "current" for the current location
- categories: ["business", "top", "technology", "world"], // A list of news categories to filter by
- languages: ["en"], // A list of languages codes to filter by
- domains: [] as string[], // A list of news domains to pull from, see https://newsdata.io/news-sources for available domains
- excludeDomains: ["news.google.com"], // A list of news domains to exclude, e.g. bbc.co.uk
- timezone: "", // A timezone to filter by, e.g. "America/New_York", see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- pages: 3, // Number of pages to pull (each page is 10 articles)
- },
-};
diff --git a/src/config/funcs.ts b/src/config/funcs.ts
deleted file mode 100644
index 77ee8dd..0000000
--- a/src/config/funcs.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { notify } from "@/utils/system";
-import { Gio, GLib, monitorFile, readFileAsync, Variable, writeFileAsync } from "astal";
-import config from ".";
-import { loadStyleAsync } from "../../app";
-import defaults from "./defaults";
-import types from "./types";
-
-type Settings<T> = { [P in keyof T]: T[P] extends object & { length?: never } ? Settings<T[P]> : Variable<T[P]> };
-
-const CONFIG = `${GLib.get_user_config_dir()}/caelestia/shell.json`;
-
-const warn = (msg: string) => {
- console.warn(`[CONFIG] ${msg}`);
- if (config.config.notifyOnError.get())
- notify({
- summary: "Invalid config",
- body: msg,
- icon: "dialog-error-symbolic",
- urgency: "critical",
- });
-};
-
-const isObject = (o: any): o is object => typeof o === "object" && o !== null && !Array.isArray(o);
-
-const isCorrectType = (v: any, type: string | string[] | number[], path: string) => {
- if (Array.isArray(type)) {
- // type is array of valid values
- if (!type.includes(v as never)) {
- warn(`Invalid value for ${path}: ${v} != ${type.map(v => `"${v}"`).join(" | ")}`);
- return false;
- }
- } else if (type.startsWith("array of ")) {
- // Array of ...
- if (Array.isArray(v)) {
- // Remove invalid items but always return true
- const arrType = type.slice(9);
- try {
- // Recursively check type
- const type = JSON.parse(arrType);
- if (Array.isArray(type)) {
- v.splice(0, v.length, ...v.filter((item, i) => isCorrectType(item, type, `${path}[${i}]`)));
- } else {
- const valid = v.filter((item, i) =>
- Object.entries(type).every(([k, t]) => {
- if (!item.hasOwnProperty(k)) {
- warn(`Invalid shape for ${path}[${i}]: ${JSON.stringify(item)} != ${arrType}`);
- return false;
- }
- return isCorrectType(item[k], t as any, `${path}[${i}].${k}`);
- })
- );
- v.splice(0, v.length, ...valid); // In-place filter
- }
- } catch {
- const valid = v.filter((item, i) => {
- if (typeof item !== arrType) {
- warn(`Invalid type for ${path}[${i}]: ${typeof item} != ${arrType}`);
- return false;
- }
- return true;
- });
- v.splice(0, v.length, ...valid); // In-place filter
- }
- } else {
- // Type is array but value is not
- warn(`Invalid type for ${path}: ${typeof v} != ${type}`);
- return false;
- }
- } else if (typeof v !== type) {
- // Value is not correct type
- warn(`Invalid type for ${path}: ${typeof v} != ${type}`);
- return false;
- }
-
- return true;
-};
-
-const deepMerge = <T extends object, U extends object>(a: T, b: U, path = ""): T & U => {
- const merged: { [k: string]: any } = { ...b };
- for (const [k, v] of Object.entries(a)) {
- if (b.hasOwnProperty(k)) {
- const bv = b[k as keyof U];
- if (isObject(v) && isObject(bv)) merged[k] = deepMerge(v, bv, `${path}${k}.`);
- else if (!isCorrectType(bv, types[path + k], path + k)) merged[k] = v;
- } else merged[k] = v;
- }
- return merged as any;
-};
-
-export const convertSettings = <T extends object>(obj: T): Settings<T> =>
- Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, isObject(v) ? convertSettings(v) : Variable(v)])) as any;
-
-const updateSection = (from: { [k: string]: any }, to: { [k: string]: any }, path = "") => {
- for (const [k, v] of Object.entries(from)) {
- if (to.hasOwnProperty(k)) {
- if (isObject(v)) updateSection(v, to[k], `${path}${k}.`);
- else if (!Array.isArray(v) || JSON.stringify(to[k].get()) !== JSON.stringify(v)) to[k].set(v);
- } else warn(`Unknown config key: ${path}${k}`);
- }
-};
-
-export const updateConfig = async () => {
- if (GLib.file_test(CONFIG, GLib.FileTest.EXISTS))
- updateSection(deepMerge(defaults, JSON.parse(await readFileAsync(CONFIG))), config);
- else updateSection(defaults, config);
- await loadStyleAsync();
- console.log("[LOG] Config updated");
-};
-
-export const initConfig = async () => {
- monitorFile(CONFIG, (_, e) => {
- if (e === Gio.FileMonitorEvent.CHANGES_DONE_HINT || e === Gio.FileMonitorEvent.DELETED)
- updateConfig().catch(warn);
- });
- await updateConfig().catch(warn);
-};
-
-export const setConfig = async (path: string, value: any) => {
- const conf = JSON.parse(await readFileAsync(CONFIG));
- let obj = conf;
- for (const p of path.split(".").slice(0, -1)) obj = obj[p];
- obj[path.split(".").at(-1)!] = value;
- await writeFileAsync(CONFIG, JSON.stringify(conf, null, 4));
-};
diff --git a/src/config/index.ts b/src/config/index.ts
deleted file mode 100644
index 80b4dc4..0000000
--- a/src/config/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import defaults from "./defaults";
-import { convertSettings } from "./funcs";
-
-const config = convertSettings(defaults);
-
-export const {
- style,
- bar,
- launcher,
- notifpopups,
- osds,
- sidebar,
- navbar,
- math,
- updates,
- weather,
- cpu,
- gpu,
- memory,
- storage,
- wallpapers,
- calendar,
- thumbnailer,
- news,
-} = config;
-export default config;
diff --git a/src/config/literals.ts b/src/config/literals.ts
deleted file mode 100644
index 1908c71..0000000
--- a/src/config/literals.ts
+++ /dev/null
@@ -1,336 +0,0 @@
-export const BAR_MODULES = [
- "osIcon",
- "activeWindow",
- "mediaPlaying",
- "brightnessSpacer",
- "workspaces",
- "volumeSpacer",
- "tray",
- "statusIcons",
- "pkgUpdates",
- "notifCount",
- "battery",
- "dateTime",
- "power",
-];
-
-export const NEWS_COUNTRIES = [
- "af", // Afghanistan
- "al", // Albania
- "dz", // Algeria
- "ad", // Andorra
- "ao", // Angola
- "ar", // Argentina
- "am", // Armenia
- "au", // Australia
- "at", // Austria
- "az", // Azerbaijan
- "bs", // Bahamas
- "bh", // Bahrain
- "bd", // Bangladesh
- "bb", // Barbados
- "by", // Belarus
- "be", // Belgium
- "bz", // Belize
- "bj", // Benin
- "bm", // Bermuda
- "bt", // Bhutan
- "bo", // Bolivia
- "ba", // Bosnia And Herzegovina
- "bw", // Botswana
- "br", // Brazil
- "bn", // Brunei
- "bg", // Bulgaria
- "bf", // Burkina fasco
- "bi", // Burundi
- "kh", // Cambodia
- "cm", // Cameroon
- "ca", // Canada
- "cv", // Cape Verde
- "ky", // Cayman Islands
- "cf", // Central African Republic
- "td", // Chad
- "cl", // Chile
- "cn", // China
- "co", // Colombia
- "km", // Comoros
- "cg", // Congo
- "ck", // Cook islands
- "cr", // Costa Rica
- "hr", // Croatia
- "cu", // Cuba
- "cw", // Curaçao
- "cy", // Cyprus
- "cz", // Czech republic
- "dk", // Denmark
- "dj", // Djibouti
- "dm", // Dominica
- "do", // Dominican republic
- "cd", // DR Congo
- "ec", // Ecuador
- "eg", // Egypt
- "sv", // El Salvador
- "gq", // Equatorial Guinea
- "er", // Eritrea
- "ee", // Estonia
- "sz", // Eswatini
- "et", // Ethiopia
- "fj", // Fiji
- "fi", // Finland
- "fr", // France
- "pf", // French polynesia
- "ga", // Gabon
- "gm", // Gambia
- "ge", // Georgia
- "de", // Germany
- "gh", // Ghana
- "gi", // Gibraltar
- "gr", // Greece
- "gd", // Grenada
- "gt", // Guatemala
- "gn", // Guinea
- "gy", // Guyana
- "ht", // Haiti
- "hn", // Honduras
- "hk", // Hong kong
- "hu", // Hungary
- "is", // Iceland
- "in", // India
- "id", // Indonesia
- "ir", // Iran
- "iq", // Iraq
- "ie", // Ireland
- "il", // Israel
- "it", // Italy
- "ci", // Ivory Coast
- "jm", // Jamaica
- "jp", // Japan
- "je", // Jersey
- "jo", // Jordan
- "kz", // Kazakhstan
- "ke", // Kenya
- "ki", // Kiribati
- "xk", // Kosovo
- "kw", // Kuwait
- "kg", // Kyrgyzstan
- "la", // Laos
- "lv", // Latvia
- "lb", // Lebanon
- "ls", // Lesotho
- "lr", // Liberia
- "ly", // Libya
- "li", // Liechtenstein
- "lt", // Lithuania
- "lu", // Luxembourg
- "mo", // Macau
- "mk", // Macedonia
- "mg", // Madagascar
- "mw", // Malawi
- "my", // Malaysia
- "mv", // Maldives
- "ml", // Mali
- "mt", // Malta
- "mh", // Marshall Islands
- "mr", // Mauritania
- "mu", // Mauritius
- "mx", // Mexico
- "fm", // Micronesia
- "md", // Moldova
- "mc", // Monaco
- "mn", // Mongolia
- "me", // Montenegro
- "ma", // Morocco
- "mz", // Mozambique
- "mm", // Myanmar
- "na", // Namibia
- "nr", // Nauru
- "np", // Nepal
- "nl", // Netherland
- "nc", // New caledonia
- "nz", // New zealand
- "ni", // Nicaragua
- "ne", // Niger
- "ng", // Nigeria
- "kp", // North korea
- "no", // Norway
- "om", // Oman
- "pk", // Pakistan
- "pw", // Palau
- "ps", // Palestine
- "pa", // Panama
- "pg", // Papua New Guinea
- "py", // Paraguay
- "pe", // Peru
- "ph", // Philippines
- "pl", // Poland
- "pt", // Portugal
- "pr", // Puerto rico
- "qa", // Qatar
- "ro", // Romania
- "ru", // Russia
- "rw", // Rwanda
- "lc", // Saint lucia
- "sx", // Saint martin(dutch)
- "ws", // Samoa
- "sm", // San Marino
- "st", // Sao tome and principe
- "sa", // Saudi arabia
- "sn", // Senegal
- "rs", // Serbia
- "sc", // Seychelles
- "sl", // Sierra Leone
- "sg", // Singapore
- "sk", // Slovakia
- "si", // Slovenia
- "sb", // Solomon Islands
- "so", // Somalia
- "za", // South africa
- "kr", // South korea
- "es", // Spain
- "lk", // Sri Lanka
- "sd", // Sudan
- "sr", // Suriname
- "se", // Sweden
- "ch", // Switzerland
- "sy", // Syria
- "tw", // Taiwan
- "tj", // Tajikistan
- "tz", // Tanzania
- "th", // Thailand
- "tl", // Timor-Leste
- "tg", // Togo
- "to", // Tonga
- "tt", // Trinidad and tobago
- "tn", // Tunisia
- "tr", // Turkey
- "tm", // Turkmenistan
- "tv", // Tuvalu
- "ug", // Uganda
- "ua", // Ukraine
- "ae", // United arab emirates
- "gb", // United kingdom
- "us", // United states of america
- "uy", // Uruguay
- "uz", // Uzbekistan
- "vu", // Vanuatu
- "va", // Vatican
- "ve", // Venezuela
- "vi", // Vietnam
- "vg", // Virgin Islands (British)
- "wo", // World
- "ye", // Yemen
- "zm", // Zambia
- "zw", // Zimbabwe
-];
-
-export const NEWS_CATEGORIES = [
- "business",
- "crime",
- "domestic",
- "education",
- "entertainment",
- "environment",
- "food",
- "health",
- "lifestyle",
- "other",
- "politics",
- "science",
- "sports",
- "technology",
- "top",
- "tourism",
- "world",
-];
-
-export const NEWS_LANGUAGES = [
- "af", // Afrikaans
- "sq", // Albanian
- "am", // Amharic
- "ar", // Arabic
- "hy", // Armenian
- "as", // Assamese
- "az", // Azerbaijani
- "bm", // Bambara
- "eu", // Basque
- "be", // Belarusian
- "bn", // Bengali
- "bs", // Bosnian
- "bg", // Bulgarian
- "my", // Burmese
- "ca", // Catalan
- "ckb", // Central Kurdish
- "zh", // Chinese
- "hr", // Croatian
- "cs", // Czech
- "da", // Danish
- "nl", // Dutch
- "en", // English
- "et", // Estonian
- "pi", // Filipino
- "fi", // Finnish
- "fr", // French
- "gl", // Galician
- "ka", // Georgian
- "de", // German
- "el", // Greek
- "gu", // Gujarati
- "ha", // Hausa
- "he", // Hebrew
- "hi", // Hindi
- "hu", // Hungarian
- "is", // Icelandic
- "id", // Indonesian
- "it", // Italian
- "jp", // Japanese
- "kn", // Kannada
- "kz", // Kazakh
- "kh", // Khmer
- "rw", // Kinyarwanda
- "ko", // Korean
- "ku", // Kurdish
- "lv", // Latvian
- "lt", // Lithuanian
- "lb", // Luxembourgish
- "mk", // Macedonian
- "ms", // Malay
- "ml", // Malayalam
- "mt", // Maltese
- "mi", // Maori
- "mr", // Marathi
- "mn", // Mongolian
- "ne", // Nepali
- "no", // Norwegian
- "or", // Oriya
- "ps", // Pashto
- "fa", // Persian
- "pl", // Polish
- "pt", // Portuguese
- "pa", // Punjabi
- "ro", // Romanian
- "ru", // Russian
- "sm", // Samoan
- "sr", // Serbian
- "sn", // Shona
- "sd", // Sindhi
- "si", // Sinhala
- "sk", // Slovak
- "sl", // Slovenian
- "so", // Somali
- "es", // Spanish
- "sw", // Swahili
- "sv", // Swedish
- "tg", // Tajik
- "ta", // Tamil
- "te", // Telugu
- "th", // Thai
- "zht", // Traditional chinese
- "tr", // Turkish
- "tk", // Turkmen
- "uk", // Ukrainian
- "ur", // Urdu
- "uz", // Uzbek
- "vi", // Vietnamese
- "cy", // Welsh
- "zu", // Zulu
-];
diff --git a/src/config/types.ts b/src/config/types.ts
deleted file mode 100644
index c8fb9b4..0000000
--- a/src/config/types.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { BAR_MODULES, NEWS_CATEGORIES, NEWS_COUNTRIES, NEWS_LANGUAGES } from "./literals";
-
-const BOOL = "boolean";
-const STR = "string";
-const NUM = "number";
-const ARR = (type: string | string[]) => `array of ${typeof type === "string" ? type : JSON.stringify(type)}`;
-const OBJ_ARR = (shape: object) => ARR(JSON.stringify(shape));
-
-export default {
- "style.transparency": ["off", "low", "normal", "high"],
- "style.borders": BOOL,
- "style.vibrant": BOOL,
- "config.notifyOnError": BOOL,
- // Bar
- "bar.vertical": BOOL,
- "bar.style": ["gaps", "panel", "embedded"],
- "bar.layout.type": ["centerbox", "flowbox"],
- "bar.layout.centerbox.start": ARR(BAR_MODULES),
- "bar.layout.centerbox.center": ARR(BAR_MODULES),
- "bar.layout.centerbox.end": ARR(BAR_MODULES),
- "bar.layout.flowbox": ARR(BAR_MODULES),
- "bar.modules.workspaces.shown": NUM,
- "bar.modules.workspaces.showLabels": BOOL,
- "bar.modules.workspaces.labels": ARR(STR),
- "bar.modules.workspaces.xalign": NUM,
- "bar.modules.workspaces.showWindows": BOOL,
- "bar.modules.dateTime.format": STR,
- "bar.modules.dateTime.detailedFormat": STR,
- // Launcher
- "launcher.style": ["lines", "round"],
- "launcher.actionPrefix": STR,
- "launcher.apps.maxResults": NUM,
- "launcher.files.maxResults": NUM,
- "launcher.files.fdOpts": ARR(STR),
- "launcher.files.shortenThreshold": NUM,
- "launcher.math.maxResults": NUM,
- "launcher.todo.notify": BOOL,
- "launcher.wallpaper.maxResults": NUM,
- "launcher.wallpaper.showAllEmpty": BOOL,
- "launcher.wallpaper.style": ["compact", "medium", "large"],
- "launcher.disabledActions": ARR(STR),
- // Notif popups
- "notifpopups.maxPopups": NUM,
- "notifpopups.expire": BOOL,
- "notifpopups.agoTime": BOOL,
- // OSDs
- "osds.volume.position": [2, 4, 8, 16],
- "osds.volume.margin": NUM,
- "osds.volume.hideDelay": NUM,
- "osds.volume.showValue": BOOL,
- "osds.brightness.position": [2, 4, 8, 16],
- "osds.brightness.margin": NUM,
- "osds.brightness.hideDelay": NUM,
- "osds.brightness.showValue": BOOL,
- "osds.lock.spacing": NUM,
- "osds.lock.caps.hideDelay": NUM,
- "osds.lock.num.hideDelay": NUM,
- // Sidebar
- "sidebar.showOnStartup": BOOL,
- "sidebar.modules.headlines.enabled": BOOL,
- // Navbar
- "navbar.persistent": BOOL,
- "navbar.appearWidth": NUM,
- "navbar.showLabels": BOOL,
- // Services
- "math.maxHistory": NUM,
- "updates.interval": NUM,
- "weather.interval": NUM,
- "weather.apiKey": STR,
- "weather.location": STR,
- "weather.imperial": BOOL,
- "cpu.interval": NUM,
- "gpu.interval": NUM,
- "memory.interval": NUM,
- "storage.interval": NUM,
- "wallpapers.paths": OBJ_ARR({ recursive: BOOL, path: STR, threshold: NUM }),
- "calendar.webcals": ARR(STR),
- "calendar.upcomingDays": NUM,
- "calendar.notify": BOOL,
- "thumbnailer.maxAttempts": NUM,
- "thumbnailer.timeBetweenAttempts": NUM,
- "thumbnailer.defaults.width": NUM,
- "thumbnailer.defaults.height": NUM,
- "thumbnailer.defaults.exact": BOOL,
- "news.apiKey": STR,
- "news.countries": ARR(NEWS_COUNTRIES),
- "news.categories": ARR(NEWS_CATEGORIES),
- "news.languages": ARR(NEWS_LANGUAGES),
- "news.domains": ARR(STR),
- "news.excludeDomains": ARR(STR),
- "news.timezone": STR,
- "news.pages": NUM,
-} as { [k: string]: string | string[] | number[] };
diff --git a/src/modules/bar.tsx b/src/modules/bar.tsx
deleted file mode 100644
index c131029..0000000
--- a/src/modules/bar.tsx
+++ /dev/null
@@ -1,703 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Players from "@/services/players";
-import Updates from "@/services/updates";
-import { getAppCategoryIcon } from "@/utils/icons";
-import { bindCurrentTime, osIcon } from "@/utils/system";
-import type { AstalWidget } from "@/utils/types";
-import { setupCustomTooltip } from "@/utils/widgets";
-import ScreenCorner from "@/widgets/screencorner";
-import { execAsync, GLib, Variable } from "astal";
-import { bind, kebabify } from "astal/binding";
-import { App, Astal, Gtk, Widget } from "astal/gtk3";
-import { bar as config } from "config";
-import AstalBattery from "gi://AstalBattery";
-import AstalBluetooth from "gi://AstalBluetooth";
-import AstalHyprland from "gi://AstalHyprland";
-import AstalNetwork from "gi://AstalNetwork";
-import AstalNotifd from "gi://AstalNotifd";
-import AstalTray from "gi://AstalTray";
-import AstalWp from "gi://AstalWp";
-import { switchPane } from "./sidebar";
-
-interface ClassNameProps {
- beforeSpacer: boolean;
- afterSpacer: boolean;
- first: boolean;
- last: boolean;
-}
-
-interface ModuleProps extends ClassNameProps {
- monitor: Monitor;
-}
-
-const hyprland = AstalHyprland.get_default();
-
-const getBatteryIcon = (perc: number) => {
- if (perc < 0.1) return "󰁺";
- if (perc < 0.2) return "󰁻";
- if (perc < 0.3) return "󰁼";
- if (perc < 0.4) return "󰁽";
- if (perc < 0.5) return "󰁾";
- if (perc < 0.6) return "󰁿";
- if (perc < 0.7) return "󰂀";
- if (perc < 0.8) return "󰂁";
- if (perc < 0.9) return "󰂂";
- return "󰁹";
-};
-
-const formatSeconds = (sec: number) => {
- if (sec >= 3600) {
- const hours = Math.floor(sec / 3600);
- let str = `${hours} hour${hours === 1 ? "" : "s"}`;
- const mins = Math.floor((sec % 3600) / 60);
- if (mins > 0) str += ` ${mins} minute${mins === 1 ? "" : "s"}`;
- return str;
- } else if (sec >= 60) {
- const mins = Math.floor(sec / 60);
- return `${mins} minute${mins === 1 ? "" : "s"}`;
- } else return `${sec} second${sec === 1 ? "" : "s"}`;
-};
-
-const hookFocusedClientProp = (
- self: AstalWidget,
- prop: keyof AstalHyprland.Client,
- callback: (c: AstalHyprland.Client | null) => void
-) => {
- let id: number | null = null;
- let lastClient: AstalHyprland.Client | null = null;
- self.hook(hyprland, "notify::focused-client", () => {
- if (id) lastClient?.disconnect(id);
- lastClient = hyprland.focusedClient; // Can be null
- id = lastClient?.connect(`notify::${kebabify(prop)}`, () => callback(lastClient));
- callback(lastClient);
- });
- self.connect("destroy", () => id && lastClient?.disconnect(id));
- callback(lastClient);
-};
-
-const getClassName = ({ beforeSpacer, afterSpacer, first, last }: ClassNameProps) =>
- `${beforeSpacer ? "before-spacer" : ""} ${afterSpacer ? "after-spacer" : ""}` +
- ` ${first ? "first" : ""} ${last ? "last" : ""}`;
-
-const getModule = (module: string) => {
- module = module.toLowerCase();
- if (module === "osicon") return OSIcon;
- if (module === "activewindow") return ActiveWindow;
- if (module === "mediaplaying") return MediaPlaying;
- if (module === "workspaces") return Workspaces;
- if (module === "tray") return Tray;
- if (module === "statusicons") return StatusIcons;
- if (module === "pkgupdates") return PkgUpdates;
- if (module === "notifcount") return NotifCount;
- if (module === "battery") return Battery;
- if (module === "datetime") return DateTime;
- if (module === "power") return Power;
- if (module === "brightnessspacer") return BrightnessSpacer;
- if (module === "volumespacer") return VolumeSpacer;
- return () => null;
-};
-
-const isSpacer = (module?: string) => module?.toLowerCase().endsWith("spacer") ?? false;
-
-const OSIcon = ({ monitor, ...props }: ModuleProps) => (
- <button
- className={`module os-icon ${getClassName(props)}`}
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "dashboard")}
- >
- {osIcon}
- </button>
-);
-
-const ActiveWindow = ({ monitor, ...props }: ModuleProps) => (
- <box
- vertical={bind(config.vertical)}
- className={`module active-window ${getClassName(props)}`}
- setup={self => {
- const title = Variable("");
- const updateTooltip = (c: AstalHyprland.Client | null) =>
- title.set(c?.class && c?.title ? `${c.class}: ${c.title}` : "");
- hookFocusedClientProp(self, "class", updateTooltip);
- hookFocusedClientProp(self, "title", updateTooltip);
- updateTooltip(hyprland.focusedClient);
-
- const window = setupCustomTooltip(self, bind(title));
- if (window) {
- self.hook(title, (_, v) => !v && window.hide());
- self.hook(window, "map", () => !title.get() && window.hide());
- }
- }}
- >
- <label
- className="icon"
- setup={self =>
- hookFocusedClientProp(self, "class", c => {
- self.label = c?.class ? getAppCategoryIcon(c.class) : "desktop_windows";
- })
- }
- />
- <label
- truncate
- angle={bind(config.vertical).as(v => (v ? 270 : 0))}
- setup={self => {
- const update = () =>
- (self.label = hyprland.focusedClient?.title ? hyprland.focusedClient.title : "Desktop");
- hookFocusedClientProp(self, "title", update);
- self.hook(config.vertical, update);
- }}
- />
- </box>
-);
-
-const MediaPlaying = ({ monitor, ...props }: ModuleProps) => {
- const players = Players.get_default();
- const getLabel = (fallback = "") =>
- players.lastPlayer ? `${players.lastPlayer.title} - ${players.lastPlayer.artist}` : fallback;
- return (
- <button
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "audio");
- else if (event.button === Astal.MouseButton.SECONDARY) players.lastPlayer?.play_pause();
- else if (event.button === Astal.MouseButton.MIDDLE) players.lastPlayer?.raise();
- }}
- setup={self => {
- const label = Variable(getLabel());
- players.hookLastPlayer(self, ["notify::title", "notify::artist"], () => label.set(getLabel()));
- setupCustomTooltip(self, bind(label));
- }}
- >
- <box vertical={bind(config.vertical)} className={`module media-playing ${getClassName(props)}`}>
- <icon
- setup={self =>
- players.hookLastPlayer(self, "notify::identity", () => {
- const icon = `caelestia-${players.lastPlayer?.identity
- .toLowerCase()
- .replaceAll(" ", "-")}-symbolic`;
- self.icon = players.lastPlayer
- ? Astal.Icon.lookup_icon(icon)
- ? icon
- : "caelestia-media-generic-symbolic"
- : "caelestia-media-none-symbolic";
- })
- }
- />
- <label
- truncate
- angle={bind(config.vertical).as(v => (v ? 270 : 0))}
- setup={self => {
- const update = () => (self.label = getLabel("No media"));
- players.hookLastPlayer(self, ["notify::title", "notify::artist"], update);
- self.hook(config.vertical, update);
- }}
- />
- </box>
- </button>
- );
-};
-
-const Workspace = ({ idx }: { idx: number }) => {
- const wsId = Variable.derive([bind(hyprland, "focusedWorkspace"), config.modules.workspaces.shown], (f, s) =>
- f ? Math.floor((f.id - 1) / s) * s + idx : idx
- );
-
- const label = (
- <label
- css={bind(config.modules.workspaces.xalign).as(a => `margin-left: ${a}px; margin-right: ${-a}px;`)}
- label={bind(config.modules.workspaces.labels).as(l => l[idx - 1] ?? String(idx))}
- />
- );
-
- return (
- <button
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- onClicked={() => hyprland.dispatch("workspace", String(wsId.get()))}
- setup={self => {
- const updateOccupied = () => {
- const occupied = hyprland.clients.some(c => c.workspace?.id === wsId.get());
- self.toggleClassName("occupied", occupied);
- };
- const updateFocused = () => {
- self.toggleClassName("focused", hyprland.focusedWorkspace?.id === wsId.get());
- updateOccupied();
- };
-
- self.hook(hyprland, "client-added", updateOccupied);
- self.hook(hyprland, "client-moved", updateOccupied);
- self.hook(hyprland, "client-removed", updateOccupied);
- self.hook(hyprland, "notify::focused-workspace", updateFocused);
- updateFocused();
- }}
- onDestroy={() => wsId.drop()}
- >
- <box
- visible={bind(config.modules.workspaces.showLabels)}
- vertical={bind(config.vertical)}
- setup={self => {
- const update = () => {
- if (config.modules.workspaces.showWindows.get()) {
- const clients = hyprland.clients.filter(c => c.workspace?.id === wsId.get());
- self.children = [
- label,
- ...clients.map(c => (
- <label className="icon" label={bind(c, "class").as(getAppCategoryIcon)} />
- )),
- ];
- } else self.children = [label];
- };
- self.hook(wsId, update);
- self.hook(hyprland, "client-added", update);
- self.hook(hyprland, "client-moved", update);
- self.hook(hyprland, "client-removed", update);
- update();
- }}
- />
- </button>
- );
-};
-
-const Workspaces = ({ monitor, ...props }: ModuleProps) => {
- const className = Variable.derive(
- [config.modules.workspaces.shown, config.modules.workspaces.showLabels],
- (s, l) => `module workspaces ${s % 2 === 0 ? "even" : "odd"} ${l ? "labels-shown" : ""} ${getClassName(props)}`
- );
-
- return (
- <eventbox
- onScroll={(_, event) => {
- const activeWs = hyprland.focusedClient?.workspace.name;
- if (activeWs?.startsWith("special:")) hyprland.dispatch("togglespecialworkspace", activeWs.slice(8));
- else if (event.delta_y > 0 || hyprland.focusedWorkspace?.id > 1)
- hyprland.dispatch("workspace", (event.delta_y < 0 ? "-" : "+") + 1);
- }}
- >
- <box vertical={bind(config.vertical)} className={bind(className)} onDestroy={() => className.drop()}>
- {bind(config.modules.workspaces.shown).as(
- n => Array.from({ length: n }).map((_, idx) => <Workspace idx={idx + 1} />) // Start from 1
- )}
- </box>
- </eventbox>
- );
-};
-
-const TrayItem = (item: AstalTray.TrayItem) => (
- <menubutton
- onButtonPressEvent={(_, event) => event.get_button()[1] === Astal.MouseButton.SECONDARY && item.activate(0, 0)}
- usePopover={false}
- direction={bind(config.vertical).as(v => (v ? Gtk.ArrowType.RIGHT : Gtk.ArrowType.DOWN))}
- menuModel={bind(item, "menuModel")}
- actionGroup={bind(item, "actionGroup").as(a => ["dbusmenu", a])}
- setup={self => setupCustomTooltip(self, bind(item, "tooltipMarkup"))}
- >
- <icon halign={Gtk.Align.CENTER} gicon={bind(item, "gicon")} />
- </menubutton>
-);
-
-const Tray = ({ monitor, ...props }: ModuleProps) => (
- <box
- visible={bind(AstalTray.get_default(), "items").as(i => i.length > 0)}
- vertical={bind(config.vertical)}
- className={`module tray ${getClassName(props)}`}
- >
- {bind(AstalTray.get_default(), "items").as(i => i.map(TrayItem))}
- </box>
-);
-
-const Network = ({ monitor }: { monitor: Monitor }) => (
- <button
- onClick={(_, event) => {
- const network = AstalNetwork.get_default();
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity");
- else if (event.button === Astal.MouseButton.SECONDARY) network.wifi.enabled = !network.wifi.enabled;
- else if (event.button === Astal.MouseButton.MIDDLE) {
- if (GLib.find_program_in_path("gnome-control-center"))
- execAsync("app2unit -- gnome-control-center wifi").catch(console.error);
- else {
- network.wifi.scan();
- execAsync(
- "app2unit -- foot -T nmtui -- fish -c 'sleep .1; set -e COLORTERM; TERM=xterm-old nmtui connect'"
- ).catch(() => {}); // Ignore errors
- }
- }
- }}
- setup={self => {
- const network = AstalNetwork.get_default();
- const tooltipText = Variable("");
- const update = () => {
- if (network.primary === AstalNetwork.Primary.WIFI) {
- if (network.wifi.internet === AstalNetwork.Internet.CONNECTED)
- tooltipText.set(`${network.wifi.ssid} | Strength: ${network.wifi.strength}/100`);
- else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING)
- tooltipText.set(`Connecting to ${network.wifi.ssid}`);
- else tooltipText.set("Disconnected");
- } else if (network.primary === AstalNetwork.Primary.WIRED) {
- if (network.wired.internet === AstalNetwork.Internet.CONNECTED)
- tooltipText.set(`Speed: ${network.wired.speed}`);
- else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) tooltipText.set("Connecting");
- else tooltipText.set("Disconnected");
- } else {
- tooltipText.set("Unknown");
- }
- };
- self.hook(network, "notify::primary", update);
- self.hook(network.wifi, "notify::internet", update);
- self.hook(network.wifi, "notify::ssid", update);
- self.hook(network.wifi, "notify::strength", update);
- if (network.wired) {
- self.hook(network.wired, "notify::internet", update);
- self.hook(network.wired, "notify::speed", update);
- }
- update();
- setupCustomTooltip(self, bind(tooltipText));
- }}
- >
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- shown={bind(AstalNetwork.get_default(), "primary").as(p =>
- p === AstalNetwork.Primary.WIFI ? "wifi" : "wired"
- )}
- >
- <stack
- name="wifi"
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- setup={self => {
- const network = AstalNetwork.get_default();
- const update = () => {
- if (network.wifi.internet === AstalNetwork.Internet.CONNECTED)
- self.shown = String(Math.ceil(network.wifi.strength / 25));
- else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting";
- else self.shown = "disconnected";
- };
- self.hook(network.wifi, "notify::internet", update);
- self.hook(network.wifi, "notify::strength", update);
- update();
- }}
- >
- <label className="icon" label="wifi_off" name="disconnected" />
- <label className="icon" label="settings_ethernet" name="connecting" />
- <label className="icon" label="signal_wifi_0_bar" name="0" />
- <label className="icon" label="network_wifi_1_bar" name="1" />
- <label className="icon" label="network_wifi_2_bar" name="2" />
- <label className="icon" label="network_wifi_3_bar" name="3" />
- <label className="icon" label="signal_wifi_4_bar" name="4" />
- </stack>
- <stack
- name="wired"
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- setup={self => {
- const network = AstalNetwork.get_default();
- const update = () => {
- if (network.primary !== AstalNetwork.Primary.WIRED) return;
-
- if (network.wired.internet === AstalNetwork.Internet.CONNECTED) self.shown = "connected";
- else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting";
- else self.shown = "disconnected";
- };
- self.hook(network, "notify::primary", update);
- if (network.wired) self.hook(network.wired, "notify::internet", update);
- update();
- }}
- >
- <label className="icon" label="wifi_off" name="disconnected" />
- <label className="icon" label="settings_ethernet" name="connecting" />
- <label className="icon" label="lan" name="connected" />
- </stack>
- </stack>
- </button>
-);
-
-const BluetoothDevice = ({ monitor, device }: { monitor: Monitor; device: AstalBluetooth.Device }) => (
- <button
- visible={bind(device, "connected")}
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity");
- else if (event.button === Astal.MouseButton.SECONDARY)
- device.disconnect_device((_, res) => device.disconnect_device_finish(res));
- else if (event.button === Astal.MouseButton.MIDDLE)
- execAsync("app2unit -- blueman-manager").catch(console.error);
- }}
- setup={self => setupCustomTooltip(self, bind(device, "alias"))}
- >
- <icon
- icon={bind(device, "icon").as(i =>
- Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "caelestia-bluetooth-device-symbolic"
- )}
- />
- </button>
-);
-
-const Bluetooth = ({ monitor }: { monitor: Monitor }) => (
- <box vertical={bind(config.vertical)} className="bluetooth">
- <button
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity");
- else if (event.button === Astal.MouseButton.SECONDARY) AstalBluetooth.get_default().toggle();
- else if (event.button === Astal.MouseButton.MIDDLE)
- execAsync("app2unit -- blueman-manager").catch(console.error);
- }}
- setup={self => {
- const bluetooth = AstalBluetooth.get_default();
- const tooltipText = Variable("");
- const update = () => {
- const devices = bluetooth.get_devices().filter(d => d.connected);
- tooltipText.set(
- devices.length > 0
- ? `Connected devices: ${devices.map(d => d.alias).join(", ")}`
- : "No connected devices"
- );
- };
- const hookDevice = (device: AstalBluetooth.Device) => {
- self.hook(device, "notify::connected", update);
- self.hook(device, "notify::alias", update);
- };
- bluetooth.get_devices().forEach(hookDevice);
- self.hook(bluetooth, "device-added", (_, device) => {
- hookDevice(device);
- update();
- });
- update();
- setupCustomTooltip(self, bind(tooltipText));
- }}
- >
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- shown={bind(AstalBluetooth.get_default(), "isPowered").as(p => (p ? "enabled" : "disabled"))}
- >
- <label className="icon" label="bluetooth" name="enabled" />
- <label className="icon" label="bluetooth_disabled" name="disabled" />
- </stack>
- </button>
- {bind(AstalBluetooth.get_default(), "devices").as(d =>
- d.map(d => <BluetoothDevice monitor={monitor} device={d} />)
- )}
- </box>
-);
-
-const StatusIcons = ({ monitor, ...props }: ModuleProps) => (
- <box vertical={bind(config.vertical)} className={`module status-icons ${getClassName(props)}`}>
- <Network monitor={monitor} />
- <Bluetooth monitor={monitor} />
- </box>
-);
-
-const PkgUpdates = ({ monitor, ...props }: ModuleProps) => (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "packages")}
- setup={self =>
- setupCustomTooltip(
- self,
- bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"} available`)
- )
- }
- >
- <box vertical={bind(config.vertical)} className={`module pkg-updates ${getClassName(props)}`}>
- <label className="icon" label="download" />
- <label label={bind(Updates.get_default(), "numUpdates").as(String)} />
- </box>
- </button>
-);
-
-const NotifCount = ({ monitor, ...props }: ModuleProps) => (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "alerts")}
- setup={self =>
- setupCustomTooltip(
- self,
- bind(AstalNotifd.get_default(), "notifications").as(
- n => `${n.length} notification${n.length === 1 ? "" : "s"}`
- )
- )
- }
- >
- <box vertical={bind(config.vertical)} className={`module notif-count ${getClassName(props)}`}>
- <label
- className="icon"
- label={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "notifications_off" : "info"))}
- />
- <revealer
- transitionType={bind(config.vertical).as(v =>
- v ? Gtk.RevealerTransitionType.SLIDE_DOWN : Gtk.RevealerTransitionType.SLIDE_RIGHT
- )}
- transitionDuration={120}
- revealChild={bind(AstalNotifd.get_default(), "dontDisturb").as(d => !d)}
- >
- <label label={bind(AstalNotifd.get_default(), "notifications").as(n => String(n.length))} />
- </revealer>
- </box>
- </button>
-);
-
-const Battery = ({ monitor, ...props }: ModuleProps) => {
- const className = Variable.derive(
- [bind(AstalBattery.get_default(), "percentage"), bind(AstalBattery.get_default(), "charging")],
- (p, c) => `module battery ${c ? "charging" : p < 0.2 ? "low" : ""} ${getClassName(props)}`
- );
- const tooltip = Variable.derive(
- [bind(AstalBattery.get_default(), "timeToEmpty"), bind(AstalBattery.get_default(), "timeToFull")],
- (e, f) => (f > 0 ? `${formatSeconds(f)} until full` : `${formatSeconds(e)} remaining`)
- );
-
- return (
- <box
- visible={bind(AstalBattery.get_default(), "isBattery")}
- vertical={bind(config.vertical)}
- className={bind(className)}
- setup={self => setupCustomTooltip(self, bind(tooltip))}
- onDestroy={() => {
- className.drop();
- tooltip.drop();
- }}
- >
- <label className="icon" label={bind(AstalBattery.get_default(), "percentage").as(getBatteryIcon)} />
- <label label={bind(AstalBattery.get_default(), "percentage").as(p => `${Math.round(p * 100)}%`)} />
- </box>
- );
-};
-
-const DateTimeHoriz = (props: ClassNameProps) => (
- <box className={`module date-time ${getClassName(props)}`}>
- <label className="icon" label="calendar_month" />
- <label
- setup={self => {
- const time = bindCurrentTime(bind(config.modules.dateTime.format), undefined, self);
- self.label = time.get();
- self.hook(time, (_, t) => (self.label = t));
- }}
- />
- </box>
-);
-
-const DateTimeVertical = (props: ClassNameProps) => (
- <box vertical className={`module date-time ${getClassName(props)}`}>
- <label className="icon" label="calendar_month" />
- <label label={bindCurrentTime("%H")} />
- <label label={bindCurrentTime("%M")} />
- </box>
-);
-
-const DateTime = ({ monitor, ...props }: ModuleProps) => (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "time")}
- setup={self =>
- setupCustomTooltip(self, bindCurrentTime(bind(config.modules.dateTime.detailedFormat), undefined, self))
- }
- >
- {bind(config.vertical).as(v => (v ? <DateTimeVertical {...props} /> : <DateTimeHoriz {...props} />))}
- </button>
-);
-
-const Power = ({ monitor, ...props }: ModuleProps) => (
- <button
- className={`module power ${getClassName(props)}`}
- label="power_settings_new"
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && App.toggle_window("session")}
- />
-);
-
-const Spacer = ({ onScroll }: { onScroll: (self: Widget.EventBox, event: Astal.ScrollEvent) => void }) => (
- <eventbox onScroll={onScroll}>
- <box vertical={bind(config.vertical)}>
- <ScreenCorner place="topleft" />
- <box expand />
- <ScreenCorner place={bind(config.vertical).as(v => (v ? "bottomleft" : "topright"))} />
- </box>
- </eventbox>
-);
-
-const BrightnessSpacer = ({ monitor }: { monitor: Monitor }) => (
- <Spacer onScroll={(_, event) => (event.delta_y > 0 ? (monitor.brightness -= 0.1) : (monitor.brightness += 0.1))} />
-);
-
-const VolumeSpacer = () => (
- <Spacer
- onScroll={(_, event) => {
- const speaker = AstalWp.get_default()?.audio.defaultSpeaker;
- if (!speaker) return console.error("Unable to connect to WirePlumber.");
- speaker.mute = false;
- if (event.delta_y > 0) speaker.volume -= 0.1;
- else speaker.volume += 0.1;
- }}
- />
-);
-
-const Bar = ({ monitor, layout }: { monitor: Monitor; layout: string }) => {
- const className = Variable.derive(
- [bind(config.vertical), bind(config.style)],
- (v, s) => `bar ${v ? "vertical" : " horizontal"} ${s}`
- );
- const modules =
- layout === "centerbox"
- ? Variable.derive(Object.values(config.layout.centerbox))
- : bind(config.layout.flowbox).as(m => [m]);
-
- const Layout = layout === "centerbox" ? Widget.CenterBox : Widget.Box;
- return (
- <Layout
- vertical={bind(config.vertical)}
- className={bind(className)}
- onDestroy={() => {
- className.drop();
- if (modules instanceof Variable) modules.drop();
- }}
- >
- {bind(modules).as(modules =>
- modules.map((m, i) => (
- <box vertical={bind(config.vertical)}>
- {m.map((n, j) => {
- let beforeSpacer = false;
- if (j < m.length - 1) beforeSpacer = isSpacer(m[j + 1]);
- else if (i < modules.length - 1) beforeSpacer = isSpacer(modules[i + 1][0]);
- let afterSpacer = false;
- if (j > 0) afterSpacer = isSpacer(m[j - 1]);
- else if (i > 0) afterSpacer = isSpacer(modules[i - 1].at(-1));
- const M = getModule(n);
- return (
- <M
- monitor={monitor}
- beforeSpacer={beforeSpacer}
- afterSpacer={afterSpacer}
- first={i === 0 && j === 0}
- last={i === modules.length - 1 && j === m.length - 1}
- />
- );
- })}
- </box>
- ))
- )}
- </Layout>
- );
-};
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-bar"
- monitor={monitor.id}
- anchor={bind(config.vertical).as(
- v =>
- Astal.WindowAnchor.TOP |
- Astal.WindowAnchor.LEFT |
- (v ? Astal.WindowAnchor.BOTTOM : Astal.WindowAnchor.RIGHT)
- )}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- >
- <overlay
- passThrough
- overlays={[
- <ScreenCorner visible={bind(config.style).as(s => s !== "embedded")} place="topleft" />,
- <ScreenCorner
- visible={bind(config.style).as(s => s !== "embedded")}
- halign={bind(config.vertical).as(v => (v ? undefined : Gtk.Align.END))}
- valign={bind(config.vertical).as(v => (v ? Gtk.Align.END : undefined))}
- place={bind(config.vertical).as(v => (v ? "bottomleft" : "topright"))}
- />,
- ]}
- >
- {bind(config.layout.type).as(l => (
- <Bar monitor={monitor} layout={l} />
- ))}
- </overlay>
- </window>
-);
diff --git a/src/modules/launcher/actions.tsx b/src/modules/launcher/actions.tsx
deleted file mode 100644
index 40d37b5..0000000
--- a/src/modules/launcher/actions.tsx
+++ /dev/null
@@ -1,522 +0,0 @@
-import { Apps } from "@/services/apps";
-import Palette from "@/services/palette";
-import Schemes, { type Colours } from "@/services/schemes";
-import Wallpapers, { type ICategory, type IWallpaper } from "@/services/wallpapers";
-import { basename } from "@/utils/strings";
-import { notify } from "@/utils/system";
-import { setupCustomTooltip, type FlowBox } from "@/utils/widgets";
-import { bind, execAsync, GLib, readFile, register, type Variable } from "astal";
-import { Gtk, Widget } from "astal/gtk3";
-import { launcher as config } from "config";
-import { setConfig } from "config/funcs";
-import fuzzysort from "fuzzysort";
-import AstalHyprland from "gi://AstalHyprland";
-import { close, ContentBox, type LauncherContent, type Mode } from "./util";
-
-interface IAction {
- icon: string;
- name: string;
- description: string;
- action: (...args: string[]) => void;
- available?: () => boolean;
-}
-
-interface ActionMap {
- [k: string]: IAction;
-}
-
-const variantActions = {
- vibrant: {
- icon: "sentiment_very_dissatisfied",
- name: "Vibrant",
- description: "A high chroma palette. The primary palette's chroma is at maximum.",
- },
- tonalspot: {
- icon: "android",
- name: "Tonal Spot",
- description: "Default for Material theme colours. A pastel palette with a low chroma.",
- },
- expressive: {
- icon: "compare_arrows",
- name: "Expressive",
- description:
- "A medium chroma palette. The primary palette's hue is different from the seed colour, for variety.",
- },
- fidelity: {
- icon: "compare",
- name: "Fidelity",
- description: "Matches the seed colour, even if the seed colour is very bright (high chroma).",
- },
- content: {
- icon: "sentiment_calm",
- name: "Content",
- description: "Almost identical to fidelity.",
- },
- fruitsalad: {
- icon: "nutrition",
- name: "Fruit Salad",
- description: "A playful theme - the seed colour's hue does not appear in the theme.",
- },
- rainbow: {
- icon: "looks",
- name: "Rainbow",
- description: "A playful theme - the seed colour's hue does not appear in the theme.",
- },
- neutral: {
- icon: "contrast",
- name: "Neutral",
- description: "Close to grayscale, a hint of chroma.",
- },
- monochrome: {
- icon: "filter_b_and_w",
- name: "Monochrome",
- description: "All colours are grayscale, no chroma.",
- },
-};
-
-const transparencyActions = {
- off: {
- icon: "blur_off",
- name: "Off",
- description: "Completely opaque",
- },
- low: {
- icon: "blur_circular",
- name: "Low",
- description: "Less transparent",
- },
- normal: {
- icon: "blur_linear",
- name: "Normal",
- description: "Somewhat transparent",
- },
- high: {
- icon: "blur_on",
- name: "High",
- description: "Extremely transparent",
- },
-};
-
-const autocomplete = (entry: Widget.Entry, action: string) => {
- entry.set_text(`${config.actionPrefix.get()}${action} `);
- entry.set_position(-1);
-};
-
-const actions = (mode: Variable<Mode>, entry: Widget.Entry): ActionMap => ({
- apps: {
- icon: "apps",
- name: "Apps",
- description: "Search for apps",
- action: () => {
- mode.set("apps");
- entry.set_text("");
- },
- },
- files: {
- icon: "folder",
- name: "Files",
- description: "Search for files",
- action: () => {
- mode.set("files");
- entry.set_text("");
- },
- },
- math: {
- icon: "calculate",
- name: "Math",
- description: "Do math calculations",
- action: () => {
- mode.set("math");
- entry.set_text("");
- },
- },
- light: {
- icon: "light_mode",
- name: "Light",
- description: "Change scheme to light mode",
- action: () => {
- Palette.get_default().switchMode("light");
- close();
- },
- available: () => Palette.get_default().hasMode("light"),
- },
- dark: {
- icon: "dark_mode",
- name: "Dark",
- description: "Change scheme to dark mode",
- action: () => {
- Palette.get_default().switchMode("dark");
- close();
- },
- available: () => Palette.get_default().hasMode("dark"),
- },
- scheme: {
- icon: "palette",
- name: "Scheme",
- description: "Change the current colour scheme",
- action: () => autocomplete(entry, "scheme"),
- },
- variant: {
- icon: "colors",
- name: "Variant",
- description: "Change the current scheme variant",
- action: () => autocomplete(entry, "variant"),
- available: () => Palette.get_default().scheme === "dynamic",
- },
- wallpaper: {
- icon: "image",
- name: "Wallpaper",
- description: "Change the current wallpaper",
- action: () => autocomplete(entry, "wallpaper"),
- },
- transparency: {
- icon: "opacity",
- name: "Transparency",
- description: "Change shell transparency",
- action: () => autocomplete(entry, "transparency"),
- },
- todo: {
- icon: "checklist",
- name: "Todo",
- description: "Create a todo in Todoist",
- action: (...args) => {
- // If no args, autocomplete cmd
- if (args.length === 0) return autocomplete(entry, "todo");
-
- // If tod not configured, notify
- let token = null;
- try {
- token = JSON.parse(readFile(GLib.get_user_config_dir() + "/tod.cfg")).token;
- } catch {} // Ignore
- if (!token) {
- notify({
- summary: "Tod not configured",
- body: "You need to configure tod first. Run any tod command to do this.",
- icon: "dialog-warning-symbolic",
- urgency: "critical",
- });
- } else {
- // Create todo and notify if configured
- execAsync(`tod t q -c ${args.join(" ")}`).catch(console.error);
- if (config.todo.notify.get())
- notify({
- summary: "Todo created",
- body: `Created todo with content: ${args.join(" ")}`,
- icon: "view-list-bullet-symbolic",
- urgency: "low",
- transient: true,
- actions: {
- "Copy content": () => execAsync(`wl-copy -- ${args.join(" ")}`).catch(console.error),
- View: () => {
- const client = AstalHyprland.get_default().clients.find(c => c.class === "Todoist");
- if (client) client.focus();
- else execAsync("app2unit -- todoist").catch(console.error);
- },
- },
- });
- }
-
- close();
- },
- available: () => !!GLib.find_program_in_path("tod"),
- },
- reload: {
- icon: "refresh",
- name: "Reload",
- description: "Reload app list",
- action: () => {
- Apps.reload();
- entry.set_text("");
- },
- },
- lock: {
- icon: "lock",
- name: "Lock",
- description: "Lock the current session",
- action: () => {
- execAsync("loginctl lock-session").catch(console.error);
- close();
- },
- },
- logout: {
- icon: "logout",
- name: "Logout",
- description: "End the current session",
- action: () => {
- execAsync("uwsm stop").catch(console.error);
- close();
- },
- },
- sleep: {
- icon: "bedtime",
- name: "Sleep",
- description: "Suspend then hibernate",
- action: () => {
- execAsync("systemctl suspend-then-hibernate").catch(console.error);
- close();
- },
- },
- reboot: {
- icon: "cached",
- name: "Reboot",
- description: "Restart the machine",
- action: () => {
- execAsync("systemctl reboot").catch(console.error);
- close();
- },
- },
- hibernate: {
- icon: "downloading",
- name: "Hibernate",
- description: "Suspend to RAM",
- action: () => {
- execAsync("systemctl hibernate").catch(console.error);
- close();
- },
- },
- shutdown: {
- icon: "power_settings_new",
- name: "Shutdown",
- description: "Suspend to disk",
- action: () => {
- execAsync("systemctl poweroff").catch(console.error);
- close();
- },
- },
-});
-
-const Action = ({ args, icon, name, description, action }: IAction & { args: string[] }) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => action(...args)}
- setup={self => setupCustomTooltip(self, description)}
- >
- <box>
- <label className="icon" label={icon} />
- <box vertical className="has-sublabel">
- <label truncate xalign={0} label={name} />
- <label truncate xalign={0} label={description} className="sublabel" />
- </box>
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const Swatch = ({ colour }: { colour: string }) => <box className="swatch" css={"background-color: " + colour + ";"} />;
-
-const Scheme = ({ scheme, name, colours }: { scheme?: string; name: string; colours?: Colours }) => {
- const palette = colours![Palette.get_default().mode] ?? colours!.light ?? colours!.dark!;
- return (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- execAsync(`caelestia scheme ${scheme ?? ""} ${name}`).catch(console.error);
- close();
- }}
- >
- <box>
- <box valign={Gtk.Align.CENTER}>
- <box className="swatch big left" css={"background-color: " + palette.base + ";"} />
- <box className="swatch big right" css={"background-color: " + palette.primary + ";"} />
- </box>
- <box vertical className="has-sublabel">
- <label truncate xalign={0} label={scheme ? `${scheme} (${name})` : name} />
- <box className="swatches">
- <Swatch colour={palette.rosewater} />
- <Swatch colour={palette.flamingo} />
- <Swatch colour={palette.pink} />
- <Swatch colour={palette.mauve} />
- <Swatch colour={palette.red} />
- <Swatch colour={palette.maroon} />
- <Swatch colour={palette.peach} />
- <Swatch colour={palette.yellow} />
- <Swatch colour={palette.green} />
- <Swatch colour={palette.teal} />
- <Swatch colour={palette.sky} />
- <Swatch colour={palette.sapphire} />
- <Swatch colour={palette.blue} />
- <Swatch colour={palette.lavender} />
- </box>
- </box>
- </box>
- </button>
- </Gtk.FlowBoxChild>
- );
-};
-
-const Variant = ({ name }: { name: keyof typeof variantActions }) => (
- <Action
- {...variantActions[name]}
- args={[]}
- action={() => {
- execAsync(`caelestia variant ${name}`).catch(console.error);
- close();
- }}
- />
-);
-
-const Wallpaper = ({ path, thumbnails }: IWallpaper) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result wallpaper-container"
- cursor="pointer"
- onClicked={() => {
- execAsync(`caelestia wallpaper -f ${path}`).catch(console.error);
- close();
- }}
- setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))}
- >
- <box
- vertical={config.wallpaper.style.get() !== "compact"}
- className={`wallpaper ${config.wallpaper.style.get()}`}
- >
- <box
- className="thumbnail"
- css={bind(config.wallpaper.style).as(
- s => "background-image: url('" + thumbnails[s as keyof typeof thumbnails] + "');"
- )}
- />
- <label truncate label={basename(path)} />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const CategoryThumbnail = ({ style, wallpapers }: { style: string; wallpapers: IWallpaper[] }) => (
- <box className="thumbnail">
- {wallpapers.slice(0, 3).map(w => (
- <box hexpand css={"background-image: url('" + w.thumbnails[style as keyof typeof w.thumbnails] + "');"} />
- ))}
- </box>
-);
-
-const Category = ({ path, wallpapers }: ICategory) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result wallpaper-container"
- cursor="pointer"
- onClicked={() => {
- execAsync(`caelestia wallpaper -d ${path}`).catch(console.error);
- close();
- }}
- setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))}
- >
- <box
- vertical={config.wallpaper.style.get() !== "compact"}
- className={`wallpaper ${config.wallpaper.style.get()}`}
- >
- {bind(config.wallpaper.style).as(s =>
- s === "compact" ? (
- <box
- className="thumbnail"
- css={"background-image: url('" + wallpapers[0].thumbnails.compact + "');"}
- />
- ) : (
- <CategoryThumbnail style={s} wallpapers={wallpapers} />
- )
- )}
- <label truncate label={basename(path)} />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const Transparency = ({ amount }: { amount: keyof typeof transparencyActions }) => (
- <Action
- {...transparencyActions[amount]}
- args={[]}
- action={() => {
- setConfig("style.transparency", amount).catch(console.error);
- close();
- }}
- />
-);
-
-@register()
-export default class Actions extends Widget.Box implements LauncherContent {
- #map: ActionMap;
- #list: string[];
-
- #content: FlowBox;
-
- constructor(mode: Variable<Mode>, entry: Widget.Entry) {
- super({ name: "actions", className: "actions" });
-
- this.#map = actions(mode, entry);
- this.#list = Object.keys(this.#map);
-
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- this.#content.foreach(c => c.destroy());
- const args = search.split(" ");
- const action = args[0].slice(1).toLowerCase();
-
- if (action === "scheme") {
- const scheme = args[1] ?? "";
- const schemes = Object.values(Schemes.get_default().map)
- .flatMap(s => (s.colours ? s.name : Object.values(s.flavours!).map(f => `${f.scheme}-${f.name}`)))
- .filter(s => s !== undefined)
- .sort();
- for (const { target } of fuzzysort.go(scheme, schemes, { all: true })) {
- if (Schemes.get_default().map.hasOwnProperty(target))
- this.#content.add(<Scheme {...Schemes.get_default().map[target]} />);
- else {
- const [scheme, flavour] = target.split("-");
- this.#content.add(<Scheme {...Schemes.get_default().map[scheme].flavours![flavour]} />);
- }
- }
- } else if (action === "variant") {
- const list = Object.keys(variantActions);
-
- for (const { target } of fuzzysort.go(args[1], list, { all: true }))
- this.#content.add(<Variant name={target as keyof typeof variantActions} />);
- } else if (action === "wallpaper") {
- if (args[1]?.toLowerCase() === "random") {
- const list = Wallpapers.get_default().categories;
- for (const { obj } of fuzzysort.go(args[2] ?? "", list, { all: true, key: "path" }))
- this.#content.add(<Category {...obj} />);
- } else {
- const list = Wallpapers.get_default().list;
- let limit = undefined;
- if ((args[1] || !config.wallpaper.showAllEmpty.get()) && config.wallpaper.maxResults.get() > 0)
- limit = config.wallpaper.maxResults.get();
-
- for (const { obj } of fuzzysort.go(args[1] ?? "", list, { all: true, key: "path", limit }))
- this.#content.add(<Wallpaper {...obj} />);
- }
- } else if (action === "transparency") {
- const list = Object.keys(transparencyActions);
-
- for (const { target } of fuzzysort.go(args[1], list, { all: true }))
- this.#content.add(<Transparency amount={target as keyof typeof transparencyActions} />);
- } else {
- const list = this.#list.filter(
- a => this.#map[a].available?.() ?? !config.disabledActions.get().includes(a)
- );
- for (const { target } of fuzzysort.go(action, list, { all: true }))
- this.#content.add(<Action {...this.#map[target]} args={args.slice(1)} />);
- }
- }
-
- handleActivate(search: string): void {
- const args = search.split(" ");
- const action = args[0].slice(1).toLowerCase();
-
- if (action === "scheme" && args[1]?.toLowerCase() === "random") {
- execAsync(`caelestia scheme`).catch(console.error);
- close();
- } else this.#content.get_child_at_index(0)?.get_child()?.activate();
- }
-}
diff --git a/src/modules/launcher/index.tsx b/src/modules/launcher/index.tsx
deleted file mode 100644
index b75ecce..0000000
--- a/src/modules/launcher/index.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import PopupWindow from "@/widgets/popupwindow";
-import { bind, register, Variable } from "astal";
-import { Astal, Gtk, Widget } from "astal/gtk3";
-import { launcher as config } from "config";
-import Actions from "./actions";
-import Modes from "./modes";
-import type { Mode } from "./util";
-
-const getModeIcon = (mode: Mode) => {
- if (mode === "apps") return "apps";
- if (mode === "files") return "folder";
- if (mode === "math") return "calculate";
- return "search";
-};
-
-const getPrettyMode = (mode: Mode) => {
- if (mode === "apps") return "Apps";
- if (mode === "files") return "Files";
- if (mode === "math") return "Math";
- return mode;
-};
-
-const isAction = (text: string, action: string = "") => text.startsWith(config.actionPrefix.get() + action);
-
-const SearchBar = ({ mode, entry }: { mode: Variable<Mode>; entry: Widget.Entry }) => (
- <box className="search-bar">
- <box className="mode">
- <label className="icon" label={bind(mode).as(getModeIcon)} />
- <label label={bind(mode).as(getPrettyMode)} />
- </box>
- {entry}
- </box>
-);
-
-const ModeSwitcher = ({ mode, modes }: { mode: Variable<Mode>; modes: Mode[] }) => (
- <box homogeneous hexpand className="mode-switcher">
- {modes.map(m => (
- <button
- className={bind(mode).as(c => `mode ${c === m ? "selected" : ""}`)}
- cursor="pointer"
- onClicked={() => mode.set(m)}
- >
- <box halign={Gtk.Align.CENTER}>
- <label className="icon" label={getModeIcon(m)} />
- <label label={getPrettyMode(m)} />
- </box>
- </button>
- ))}
- </box>
-);
-
-@register()
-export default class Launcher extends PopupWindow {
- readonly mode: Variable<Mode>;
-
- constructor() {
- const entry = (
- <entry
- hexpand
- className="entry"
- placeholderText={bind(config.actionPrefix).as(p => `Type "${p}" for subcommands`)}
- />
- ) as Widget.Entry;
- const mode = Variable<Mode>("apps");
- const content = Modes();
- const actions = new Actions(mode, entry);
- const className = Variable.derive([mode, config.style], (m, s) => `launcher ${m} ${s}`);
-
- super({
- name: "launcher",
- anchor:
- Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
- keymode: Astal.Keymode.EXCLUSIVE,
- exclusivity: Astal.Exclusivity.IGNORE,
- borderWidth: 0,
- onKeyPressEvent(_, event) {
- const keyval = event.get_keyval()[1];
- // Focus entry on typing
- if (!entry.isFocus && keyval >= 32 && keyval <= 126) {
- entry.text += String.fromCharCode(keyval);
- entry.grab_focus();
- entry.set_position(-1);
-
- // Consume event, if not consumed it will duplicate character in entry
- return true;
- }
- },
- child: (
- <box
- vertical
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- className={bind(className)}
- onDestroy={() => className.drop()}
- >
- <SearchBar mode={mode} entry={entry} />
- <stack
- expand
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={100}
- shown={bind(entry, "text").as(t => (isAction(t) ? "actions" : "content"))}
- >
- <stack
- name="content"
- transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
- transitionDuration={200}
- shown={bind(mode)}
- >
- {Object.values(content)}
- </stack>
- {actions}
- </stack>
- <ModeSwitcher mode={mode} modes={Object.keys(content) as Mode[]} />
- </box>
- ),
- });
-
- this.mode = mode;
-
- content[mode.get()].updateContent(entry.get_text());
- this.hook(mode, (_, v: Mode) => {
- entry.set_text("");
- content[v].updateContent(entry.get_text());
- });
- this.hook(entry, "changed", () =>
- (isAction(entry.get_text()) ? actions : content[mode.get()]).updateContent(entry.get_text())
- );
- this.hook(entry, "activate", () => {
- (isAction(entry.get_text()) ? actions : content[mode.get()]).handleActivate(entry.get_text());
- if (mode.get() === "math" && !isAction(entry.get_text())) entry.set_text(""); // Cause math mode doesn't auto clear
- });
-
- // Clear search on hide if not in math mode or creating a todo
- this.connect("hide", () => {
- if ((mode.get() !== "math" || isAction(entry.get_text())) && !isAction(entry.get_text(), "todo"))
- entry.set_text("");
- });
- }
-
- open(mode: Mode) {
- this.mode.set(mode);
- this.show();
- }
-}
diff --git a/src/modules/launcher/modes.tsx b/src/modules/launcher/modes.tsx
deleted file mode 100644
index e278779..0000000
--- a/src/modules/launcher/modes.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-import { Apps as AppsService } from "@/services/apps";
-import MathService, { type HistoryItem } from "@/services/math";
-import { getAppCategoryIcon } from "@/utils/icons";
-import { launch } from "@/utils/system";
-import { type FlowBox, setupCustomTooltip } from "@/utils/widgets";
-import { bind, execAsync, Gio, register, Variable } from "astal";
-import { Astal, Gtk, Widget } from "astal/gtk3";
-import { launcher as config } from "config";
-import type AstalApps from "gi://AstalApps";
-import { close, ContentBox, type LauncherContent, limitLength } from "./util";
-
-const AppResult = ({ app }: { app: AstalApps.Application }) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- launch(app);
- close();
- }}
- setup={self => setupCustomTooltip(self, app.description ? `${app.name}: ${app.description}` : app.name)}
- >
- <box>
- {app.iconName && Astal.Icon.lookup_icon(app.iconName) ? (
- <icon className="icon" icon={app.iconName} />
- ) : (
- <label className="icon" label={getAppCategoryIcon(app)} />
- )}
- <label truncate label={app.name} />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const FileResult = ({ path }: { path: string }) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- execAsync([
- "bash",
- "-c",
- `dbus-send --session --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:'file://${path}' string:'' || app2unit -O '${path}'`,
- ]).catch(console.error);
- close();
- }}
- >
- <box setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))}>
- <icon
- className="icon"
- gicon={
- Gio.File.new_for_path(path)
- .query_info(Gio.FILE_ATTRIBUTE_STANDARD_ICON, Gio.FileQueryInfoFlags.NONE, null)
- .get_icon()!
- }
- />
- <label
- truncate
- label={
- path.replace(HOME, "~").length > config.files.shortenThreshold.get()
- ? path
- .replace(HOME, "~")
- .split("/")
- .map((n, i, arr) => (i === 0 || i === arr.length - 1 ? n : n.slice(0, 1)))
- .join("/")
- : path.replace(HOME, "~")
- }
- />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const MathResult = ({ icon, equation, result }: HistoryItem) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- execAsync(["wl-copy", "--", result]).catch(console.error);
- close();
- }}
- setup={self => setupCustomTooltip(self, `${equation} -> ${result}`)}
- >
- <box>
- <label className="icon" label={icon} />
- <box vertical className="has-sublabel">
- <label truncate xalign={0} label={equation} />
- <label truncate xalign={0} label={result} className="sublabel" />
- </box>
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-@register()
-class Apps extends Widget.Box implements LauncherContent {
- #content: FlowBox;
-
- constructor() {
- super({ name: "apps", className: "apps" });
-
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- this.#content.foreach(c => c.destroy());
- for (const app of limitLength(AppsService.fuzzy_query(search), config.apps))
- this.#content.add(<AppResult app={app} />);
- }
-
- handleActivate(): void {
- this.#content.get_child_at_index(0)?.get_child()?.grab_focus();
- this.#content.get_child_at_index(0)?.get_child()?.activate();
- }
-}
-
-@register()
-class Files extends Widget.Box implements LauncherContent {
- #content: FlowBox;
-
- constructor() {
- super({ name: "files", className: "files" });
-
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- execAsync(["fd", ...config.files.fdOpts.get(), search, HOME])
- .then(out => {
- this.#content.foreach(c => c.destroy());
- const paths = out.split("\n").filter(path => path);
- for (const path of limitLength(paths, config.files)) this.#content.add(<FileResult path={path} />);
- })
- .catch(() => {}); // Ignore errors
- }
-
- handleActivate(): void {
- this.#content.get_child_at_index(0)?.get_child()?.grab_focus();
- this.#content.get_child_at_index(0)?.get_child()?.activate();
- }
-}
-
-@register()
-class Math extends Widget.Box implements LauncherContent {
- #showResult: Variable<boolean>;
- #result: Variable<HistoryItem>;
- #content: FlowBox;
-
- constructor() {
- super({ name: "math", className: "math", vertical: true });
-
- this.#showResult = Variable(false);
- this.#result = Variable({ equation: "", result: "", icon: "" });
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- revealChild={bind(this.#showResult)}
- >
- <box vertical className="preview">
- <box className="result">
- <label className="icon" label={bind(this.#result).as(r => r.icon)} />
- <box vertical>
- <label xalign={0} label="Result" />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={bind(this.#result).as(r => r.result)}
- />
- </box>
- </box>
- <box visible={bind(config.style).as(s => s === "lines")} className="separator" />
- </box>
- </revealer>
- );
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- this.#showResult.set(search.length > 0);
- this.#result.set(MathService.get_default().evaluate(search));
-
- this.#content.foreach(c => c.destroy());
- for (const item of limitLength(MathService.get_default().history, config.math))
- this.#content.add(<MathResult {...item} />);
- }
-
- handleActivate(search: string): void {
- if (!search) return;
- MathService.get_default().commit();
- const res = this.#result.get();
- // Copy and close if not assignment, help or error
- if (!["equal", "help", "error"].includes(res.icon)) {
- execAsync(["wl-copy", "--", res.result]).catch(console.error);
- close();
- }
- }
-}
-
-export default () => ({
- apps: new Apps(),
- files: new Files(),
- math: new Math(),
-});
diff --git a/src/modules/launcher/util.tsx b/src/modules/launcher/util.tsx
deleted file mode 100644
index 8288588..0000000
--- a/src/modules/launcher/util.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { FlowBox } from "@/utils/widgets";
-import type { Variable } from "astal";
-import { App, Gtk } from "astal/gtk3";
-
-export type Mode = "apps" | "files" | "math";
-
-export interface LauncherContent {
- updateContent(search: string): void;
- handleActivate(search: string): void;
-}
-
-export const close = () => App.get_window("launcher")?.hide();
-
-export const limitLength = <T,>(arr: T[], cfg: { maxResults: Variable<number> }) =>
- cfg.maxResults.get() > 0 && arr.length > cfg.maxResults.get() ? arr.slice(0, cfg.maxResults.get()) : arr;
-
-export const ContentBox = () => (
- <FlowBox homogeneous valign={Gtk.Align.START} minChildrenPerLine={2} maxChildrenPerLine={2} />
-);
diff --git a/src/modules/mediadisplay/index.tsx b/src/modules/mediadisplay/index.tsx
deleted file mode 100644
index 307087c..0000000
--- a/src/modules/mediadisplay/index.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Players from "@/services/players";
-import { lengthStr } from "@/utils/strings";
-import { bind, Variable } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
-import AstalMpris from "gi://AstalMpris";
-import Visualiser from "./visualiser";
-
-type Selected = Variable<AstalMpris.Player | null>;
-
-const bindIcon = (player: AstalMpris.Player) =>
- bind(player, "identity").as(i => {
- const icon = `caelestia-${i?.toLowerCase().replaceAll(" ", "-")}-symbolic`;
- return Astal.Icon.lookup_icon(icon) ? icon : "caelestia-media-generic-symbolic";
- });
-
-const PlayerButton = ({
- player,
- selected,
- showDropdown,
-}: {
- player: AstalMpris.Player;
- selected: Selected;
- showDropdown: Variable<boolean>;
-}) => (
- <button
- cursor="pointer"
- onClicked={() => {
- showDropdown.set(false);
- selected.set(player);
- }}
- >
- <box className="identity" halign={Gtk.Align.CENTER}>
- <label label={bind(player, "identity").as(i => i ?? "-")} />
- <label label="•" />
- <label label={bind(player, "title").as(t => t ?? "-")} />
- </box>
- </button>
-);
-
-const Selector = ({ player, selected }: { player?: AstalMpris.Player; selected: Selected }) => {
- const showDropdown = Variable(false);
-
- return (
- <box vertical valign={Gtk.Align.START} className="selector">
- <button
- sensitive={bind(Players.get_default(), "list").as(ps => ps.length > 1)}
- cursor="pointer"
- onClicked={() => showDropdown.set(!showDropdown.get())}
- >
- <box className="identity" halign={Gtk.Align.CENTER}>
- <icon icon={player ? bindIcon(player) : "caelestia-media-none-symbolic"} />
- <label label={player ? bind(player, "identity").as(i => i ?? "") : "No media"} />
- </box>
- </button>
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- revealChild={bind(showDropdown)}
- >
- <box vertical className="list">
- {bind(Players.get_default(), "list").as(ps =>
- ps
- .filter(p => p !== player)
- .map(p => <PlayerButton player={p} selected={selected} showDropdown={showDropdown} />)
- )}
- </box>
- </revealer>
- </box>
- );
-};
-
-const NoMedia = ({ selected }: { selected: Selected }) => (
- <box>
- <box homogeneous halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="cover-art">
- <label xalign={0.36} label="" />
- </box>
- <box>
- <box vertical className="details">
- <label truncate xalign={0} className="title" label="No media" />
- <label truncate xalign={0} className="artist" label="Try play something!" />
- <box halign={Gtk.Align.START} className="controls">
- <button sensitive={false} label="skip_previous" />
- <button sensitive={false} label="play_arrow" />
- <button sensitive={false} label="skip_next" />
- </box>
- </box>
- <box className="center-module">
- <overlay
- expand
- overlay={<label halign={Gtk.Align.CENTER} valign={Gtk.Align.END} className="time" label="-1:-1" />}
- >
- <Visualiser />
- </overlay>
- </box>
- <Selector selected={selected} />
- </box>
- </box>
-);
-
-const Player = ({ player, selected }: { player: AstalMpris.Player; selected: Selected }) => {
- const time = Variable.derive(
- [bind(player, "position"), bind(player, "length")],
- (p, l) => lengthStr(p) + " / " + lengthStr(l)
- );
-
- return (
- <box>
- <box
- homogeneous
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- className="cover-art"
- css={bind(player, "coverArt").as(a => `background-image: url("${a}");`)}
- >
- {bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.36} label="" />))}
- </box>
- <box>
- <box vertical className="details">
- <label truncate xalign={0} className="title" label={bind(player, "title").as(t => t ?? "-")} />
- <label truncate xalign={0} className="artist" label={bind(player, "artist").as(t => t ?? "-")} />
- <box halign={Gtk.Align.START} className="controls">
- <button
- sensitive={bind(player, "canGoPrevious")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="skip_previous"
- />
- <button
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.play_pause()}
- label={bind(player, "playbackStatus").as(s =>
- s === AstalMpris.PlaybackStatus.PLAYING ? "pause" : "play_arrow"
- )}
- />
- <button
- sensitive={bind(player, "canGoNext")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="skip_next"
- />
- </box>
- </box>
- <box className="center-module">
- <overlay
- expand
- overlay={
- <label
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.END}
- className="time"
- label={bind(time)}
- onDestroy={() => time.drop()}
- />
- }
- >
- <Visualiser />
- </overlay>
- </box>
- <Selector player={player} selected={selected} />
- </box>
- </box>
- );
-};
-
-export default ({ monitor }: { monitor: Monitor }) => {
- const selected = Variable(Players.get_default().lastPlayer);
- selected.observe(Players.get_default(), "notify::last-player", () => Players.get_default().lastPlayer);
-
- return (
- <window
- application={App}
- name={`mediadisplay${monitor.id}`}
- namespace="caelestia-mediadisplay"
- monitor={monitor.id}
- anchor={Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- visible={false}
- >
- <box className="mediadisplay" onDestroy={() => selected.drop()}>
- {bind(selected).as(p =>
- p ? <Player player={p} selected={selected} /> : <NoMedia selected={selected} />
- )}
- </box>
- </window>
- );
-};
diff --git a/src/modules/mediadisplay/visualiser.tsx b/src/modules/mediadisplay/visualiser.tsx
deleted file mode 100644
index d788e7b..0000000
--- a/src/modules/mediadisplay/visualiser.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Gtk } from "astal/gtk3";
-import cairo from "cairo";
-import AstalCava from "gi://AstalCava";
-import PangoCairo from "gi://PangoCairo";
-
-export default () => (
- <drawingarea
- className="visualiser"
- setup={self => {
- const cava = AstalCava.get_default();
-
- if (cava) {
- cava.set_stereo(true);
- cava.set_noise_reduction(0.77);
- cava.set_input(AstalCava.Input.PIPEWIRE);
-
- self.hook(cava, "notify::values", () => self.queue_draw());
- self.connect("size-allocate", () => {
- const width = self.get_allocated_width();
- const barWidth = self
- .get_style_context()
- .get_property("min-width", Gtk.StateFlags.NORMAL) as number;
- const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right;
- const bars = Math.floor((width - gaps) / (barWidth + gaps));
- if (bars > 0) cava.set_bars(bars % 2 ? bars : bars - 1);
- });
- }
-
- self.connect("draw", (_, cr: cairo.Context) => {
- const { width, height } = self.get_allocation();
-
- if (!cava) {
- // Show error text if cava unavailable
- const fg = self.get_style_context().get_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
- const layout = self.create_pango_layout("Visualiser module requires Cava");
- const [w, h] = layout.get_pixel_size();
- cr.moveTo((width - w) / 2, (height - h) / 2);
- cr.setAntialias(cairo.Antialias.BEST);
- PangoCairo.show_layout(cr, layout);
-
- return;
- }
-
- const bg = self.get_style_context().get_background_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
- const barWidth = self.get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL) as number;
- const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right;
-
- const values = cava.get_values();
- const len = values.length - 1;
- const radius = barWidth / 2;
- const xOff = (width - len * (barWidth + gaps) - gaps) / 2 - radius;
- const center = height / 2;
- const half = len / 2;
-
- const renderPill = (x: number, value: number) => {
- x = x * (barWidth + gaps) + xOff;
- value *= center;
- cr.arc(x, center + value, radius, 0, Math.PI);
- cr.arc(x, center - value, radius, Math.PI, Math.PI * 2);
- cr.fill();
- };
-
- // Render channels facing each other
- for (let i = half - 1; i >= 0; i--) renderPill(half - i, values[i]);
- for (let i = half; i < len; i++) renderPill(i + 1, values[i]);
- });
- }}
- />
-);
diff --git a/src/modules/navbar.tsx b/src/modules/navbar.tsx
deleted file mode 100644
index 35d3900..0000000
--- a/src/modules/navbar.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import { capitalize } from "@/utils/strings";
-import type { AstalWidget } from "@/utils/types";
-import { bind, execAsync, Variable } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
-import { navbar as config } from "config";
-import AstalHyprland from "gi://AstalHyprland";
-import Pango from "gi://Pango";
-import SideBar, { awaitSidebar, paneNames, switchPane, type PaneName } from "./sidebar";
-
-const layerNames = ["mediadisplay"] as const;
-type LayerName = `${(typeof layerNames)[number]}${number}`;
-
-const specialWsNames = ["sysmon", "communication", "music", "todo"] as const;
-type SpecialWsName = (typeof specialWsNames)[number];
-
-const getPaneIcon = (name: PaneName) => {
- if (name === "dashboard") return "dashboard";
- if (name === "audio") return "tune";
- if (name === "connectivity") return "settings_ethernet";
- if (name === "packages") return "package_2";
- if (name === "alerts") return "notifications";
- return "date_range";
-};
-
-const getLayerIcon = (name: LayerName) => {
- return "graphic_eq";
-};
-
-const getSpecialWsIcon = (name: SpecialWsName) => {
- if (name === "sysmon") return "speed";
- if (name === "communication") return "communication";
- if (name === "music") return "music_note";
- return "checklist";
-};
-
-const hookIsCurrent = (
- self: AstalWidget,
- sidebar: Variable<SideBar | null>,
- name: PaneName,
- callback: (isCurrent: boolean) => void
-) => {
- const unsub = sidebar.subscribe(s => {
- if (!s) return;
- self.hook(s.shown, (_, v) => callback(s.visible && v === name));
- self.hook(s, "notify::visible", () => callback(s.visible && s.shown.get() === name));
- callback(s.visible && s.shown.get() === name);
- unsub();
- });
-};
-
-const PaneButton = ({
- monitor,
- name,
- sidebar,
-}: {
- monitor: Monitor;
- name: PaneName;
- sidebar: Variable<SideBar | null>;
-}) => (
- <button
- cursor="pointer"
- onClicked={() => switchPane(monitor, name)}
- setup={self => hookIsCurrent(self, sidebar, name, c => self.toggleClassName("current", c))}
- >
- <box vertical className="nav-button">
- <label className="icon" label={getPaneIcon(name)} />
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- setup={self => {
- let isCurrent = false;
- hookIsCurrent(self, sidebar, name, c => {
- isCurrent = c;
- self.set_reveal_child(config.showLabels.get() && c);
- });
- self.hook(config.showLabels, (_, v) => self.set_reveal_child(v && isCurrent));
- }}
- >
- <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} />
- </revealer>
- </box>
- </button>
-);
-
-const LayerButton = ({ name }: { name: LayerName }) => (
- <button
- cursor="pointer"
- onClicked={() => App.toggle_window(name)}
- setup={self =>
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === name) self.toggleClassName("current", window.visible);
- })
- }
- >
- <box vertical className="nav-button">
- <label className="icon" label={getLayerIcon(name)} />
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- setup={self => {
- let visible = false;
- self.hook(config.showLabels, (_, v) => self.toggleClassName(v && visible));
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === name)
- self.toggleClassName("current", config.showLabels.get() && window.visible);
- });
- }}
- >
- <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} />
- </revealer>
- </box>
- </button>
-);
-
-const SpecialWsButton = ({ name }: { name: SpecialWsName }) => {
- const revealChild = Variable.derive(
- [config.showLabels, bind(AstalHyprland.get_default(), "focusedClient")],
- (l, c) => l && c?.get_workspace().get_name() === `special:${name}`
- );
-
- return (
- <button
- className={bind(AstalHyprland.get_default(), "focusedClient").as(c =>
- c?.get_workspace().get_name() === `special:${name}` ? "current" : ""
- )}
- cursor="pointer"
- onClicked={() => execAsync(`caelestia toggle ${name}`).catch(console.error)}
- >
- <box vertical className="nav-button">
- <label className="icon" label={getSpecialWsIcon(name)} />
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- revealChild={bind(revealChild)}
- onDestroy={() => revealChild.drop()}
- >
- <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} />
- </revealer>
- </box>
- </button>
- );
-};
-
-export default ({ monitor }: { monitor: Monitor }) => {
- const sidebar = Variable<SideBar | null>(null);
- awaitSidebar(monitor).then(s => sidebar.set(s));
-
- return (
- <window
- namespace="caelestia-navbar"
- monitor={monitor.id}
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- visible={config.persistent.get()}
- setup={self => {
- const hyprland = AstalHyprland.get_default();
- const visible = Variable(config.persistent.get());
-
- visible.poll(100, () => {
- const width = self.visible
- ? Math.max(config.appearWidth.get(), self.get_allocated_width())
- : config.appearWidth.get();
- return hyprland.get_cursor_position().x < width;
- });
- if (config.persistent.get()) visible.stopPoll();
-
- self.hook(config.persistent, (_, v) => {
- if (v) {
- visible.stopPoll();
- visible.set(true);
- } else visible.startPoll();
- });
-
- self.hook(visible, (_, v) => self.set_visible(v));
- self.connect("destroy", () => visible.drop());
- }}
- >
- <eventbox
- onScroll={(_, event) => {
- const shown = sidebar.get()?.shown;
- if (!shown) return;
- const idx = paneNames.indexOf(shown.get());
- if (event.delta_y > 0) shown.set(paneNames[Math.min(paneNames.length - 1, idx + 1)]);
- else shown.set(paneNames[Math.max(0, idx - 1)]);
- }}
- >
- <box vertical className="navbar">
- {paneNames.map(n => (
- <PaneButton monitor={monitor} name={n} sidebar={sidebar} />
- ))}
- {layerNames.map(n => (
- <LayerButton name={`${n}${monitor.id}`} />
- ))}
- <box vexpand />
- {specialWsNames.map(n => (
- <SpecialWsButton name={n} />
- ))}
- </box>
- </eventbox>
- </window>
- );
-};
diff --git a/src/modules/notifpopups.tsx b/src/modules/notifpopups.tsx
deleted file mode 100644
index cb5984d..0000000
--- a/src/modules/notifpopups.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import { setupChildClickthrough } from "@/utils/widgets";
-import Notification from "@/widgets/notification";
-import { Astal, Gtk } from "astal/gtk3";
-import { notifpopups as config } from "config";
-import AstalNotifd from "gi://AstalNotifd";
-import type SideBar from "./sidebar";
-import { awaitSidebar } from "./sidebar";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- monitor={monitor.id}
- namespace="caelestia-notifpopups"
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM}
- >
- <box
- vertical
- valign={Gtk.Align.START}
- className="notifpopups"
- setup={self => {
- const notifd = AstalNotifd.get_default();
- const map = new Map<number, Notification>();
-
- self.hook(notifd, "notified", (self, id) => {
- if (notifd.dontDisturb) return;
-
- const notification = notifd.get_notification(id);
-
- const popup = (<Notification popup notification={notification} />) as Notification;
- popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id));
- map.get(notification.id)?.destroyWithAnims();
- map.set(notification.id, popup);
-
- self.add(
- <eventbox
- onClick={(_, event) => {
- // Activate notif or go to notif center on primary click
- if (event.button === Astal.MouseButton.PRIMARY) {
- if (notification.actions.length === 1)
- notification.invoke(notification.actions[0].id);
- else {
- sidebar?.shown.set("alerts");
- sidebar?.show();
- popup.destroyWithAnims();
- }
- }
- // Dismiss on middle click
- else if (event.button === Astal.MouseButton.MIDDLE) notification.dismiss();
- }}
- // Close on hover lost
- onHoverLost={() => popup.destroyWithAnims()}
- setup={self => self.hook(popup, "destroy", () => self.destroy())}
- >
- {popup}
- </eventbox>
- );
-
- // Limit number of popups
- if (config.maxPopups.get() > 0 && self.children.length > config.maxPopups.get())
- map.values().next().value?.destroyWithAnims();
- });
- self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
-
- let sidebar: SideBar | null = null;
- awaitSidebar(monitor).then(s => (sidebar = s));
-
- // Change input region to child region so can click through empty space
- setupChildClickthrough(self);
- }}
- />
- </window>
-);
diff --git a/src/modules/osds.tsx b/src/modules/osds.tsx
deleted file mode 100644
index 0f38823..0000000
--- a/src/modules/osds.tsx
+++ /dev/null
@@ -1,327 +0,0 @@
-import Monitors, { type Monitor } from "@/services/monitors";
-import { capitalize } from "@/utils/strings";
-import PopupWindow from "@/widgets/popupwindow";
-import { bind, execAsync, register, timeout, Variable, type Time } from "astal";
-import { App, Astal, Gtk, Widget } from "astal/gtk3";
-import cairo from "cairo";
-import { osds as config } from "config";
-import AstalWp from "gi://AstalWp";
-import Cairo from "gi://cairo";
-import Pango from "gi://Pango";
-import PangoCairo from "gi://PangoCairo";
-
-const getStyle = (context: Gtk.StyleContext, prop: string) => context.get_property(prop, Gtk.StateFlags.NORMAL);
-const getNumStyle = (context: Gtk.StyleContext, prop: string) => getStyle(context, prop) as number;
-
-const mix = (a: number, b: number, r: number) => a * r + b * (1 - r);
-
-const pangoWeightToStr = (weight: Pango.Weight) => {
- switch (weight) {
- case Pango.Weight.ULTRALIGHT:
- return "UltraLight";
- case Pango.Weight.LIGHT:
- return "Light";
- case Pango.Weight.BOLD:
- return "Bold";
- case Pango.Weight.ULTRABOLD:
- return "UltraBold";
- case Pango.Weight.HEAVY:
- return "Heavy";
- default:
- return "Normal";
- }
-};
-
-const SliderOsd = ({
- fillIcons,
- monitor,
- type,
- windowSetup,
- className = "",
- initValue,
- drawAreaSetup,
-}: {
- fillIcons?: boolean;
- monitor?: Monitor;
- type: "volume" | "brightness";
- windowSetup: (self: Widget.Window, show: () => void) => void;
- className?: string;
- initValue: number;
- drawAreaSetup: (self: Widget.DrawingArea, icon: Variable<string>) => void;
-}) => (
- <PopupWindow
- name={type}
- monitor={monitor?.id}
- keymode={Astal.Keymode.NONE}
- anchor={bind(config[type].position)}
- margin={bind(config[type].margin)}
- setup={self => {
- let time: Time | null = null;
- const hideAfterTimeout = () => {
- time?.cancel();
- time = timeout(config[type].hideDelay.get(), () => self.hide());
- };
- self.connect("show", hideAfterTimeout);
- windowSetup(self, () => {
- self.show();
- hideAfterTimeout();
- });
- }}
- >
- <box className={type}>
- <drawingarea
- className={`inner ${className}`}
- css={"font-size: " + initValue + "px;"}
- setup={self => {
- const halfPi = Math.PI / 2;
- const vertical =
- config[type].position.get() === Astal.WindowAnchor.LEFT ||
- config[type].position.get() === Astal.WindowAnchor.RIGHT;
-
- const icon = Variable("");
- drawAreaSetup(self, icon);
- self.hook(icon, () => self.queue_draw());
-
- // Init size
- const styleContext = self.get_style_context();
- const width = getNumStyle(styleContext, "min-width");
- const height = getNumStyle(styleContext, "min-height");
- if (vertical) self.set_size_request(height, width);
- else self.set_size_request(width, height);
-
- let fontDesc: Pango.FontDescription | null = null;
-
- self.connect("draw", (_, cr: cairo.Context) => {
- const parent = self.get_parent();
- if (!parent) return;
-
- const styleContext = self.get_style_context();
- const pContext = parent.get_style_context();
-
- let width = getNumStyle(styleContext, "min-width");
- let height = getNumStyle(styleContext, "min-height");
-
- const progressValue = getNumStyle(styleContext, "font-size");
- let radius = getNumStyle(pContext, "border-radius");
- // Flatten when near 0, do before swap cause its simpler
- radius = Math.min(radius, Math.min(width * progressValue, height) / 2);
-
- if (vertical) [width, height] = [height, width]; // Swap if vertical
- self.set_size_request(width, height);
-
- const progressPosition = vertical
- ? height * (1 - progressValue) + radius // Top is 0, but we want it to start from the bottom
- : width * progressValue - radius;
-
- const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
-
- // Background
- if (vertical) {
- cr.arc(radius, progressPosition, radius, -Math.PI, -halfPi); // Top left
- cr.arc(width - radius, progressPosition, radius, -halfPi, 0); // Top right
- cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
- } else {
- cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
- cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
- cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
- }
- cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
- cr.fill();
-
- const fg = pContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setAntialias(cairo.Antialias.BEST);
-
- // Progress number, at top/right
- let nw = 0;
- let nh = 0;
- if (config[type].showValue.get()) {
- const numLayout = parent.create_pango_layout(String(Math.round(progressValue * 100)));
- [nw, nh] = numLayout.get_pixel_size();
- let diff;
- if (vertical) {
- diff = ((1 - progressValue) * height) / nh;
- cr.moveTo((width - nw) / 2, radius / 2);
- } else {
- diff = ((1 - progressValue) * width) / nw;
- cr.moveTo(width - nw - radius, (height - nh) / 2);
- }
- diff = Math.max(0, Math.min(1, diff));
-
- cr.setSourceRGBA(
- mix(bg.red, fg.red, diff),
- mix(bg.green, fg.green, diff),
- mix(bg.blue, fg.blue, diff),
- mix(bg.alpha, fg.alpha, diff)
- );
-
- PangoCairo.show_layout(cr, numLayout);
- }
-
- // Progress icon, follows progress
- if (fontDesc === null) {
- const weight = pangoWeightToStr(getStyle(pContext, "font-weight") as Pango.Weight);
- const size = getNumStyle(pContext, "font-size") * 1.5;
- fontDesc = Pango.font_description_from_string(
- `Material Symbols Rounded ${weight} ${size}px`
- );
- // Ugh GTK CSS doesn't support font-variations, so you need to manually create the layout and font desc instead of using Gtk.Widget#create_pango_layout
- if (fillIcons) fontDesc.set_variations("FILL=1");
- }
-
- const iconLayout = PangoCairo.create_layout(cr);
- iconLayout.set_font_description(fontDesc);
- iconLayout.set_text(icon.get(), -1);
-
- const [iw, ih] = iconLayout.get_pixel_size();
- let diff;
- if (vertical) {
- diff = (progressValue * height) / ih;
- cr.moveTo(
- (width - iw) / 2,
- Math.max(nh, Math.min(height - ih, progressPosition - ih / 2 + radius))
- );
- } else {
- diff = (progressValue * width) / iw;
- cr.moveTo(
- Math.min(
- width - nw * 1.1 - iw - radius,
- Math.max(0, progressPosition - iw / 2 - radius)
- ),
- (height - ih) / 2
- );
- }
- diff = Math.max(0, Math.min(1, diff));
-
- cr.setSourceRGBA(
- mix(fg.red, bg.red, diff),
- mix(fg.green, bg.green, diff),
- mix(fg.blue, bg.blue, diff),
- mix(fg.alpha, bg.alpha, diff)
- );
-
- PangoCairo.show_layout(cr, iconLayout);
- });
- }}
- />
- </box>
- </PopupWindow>
-);
-
-const Volume = ({ audio }: { audio: AstalWp.Audio }) => (
- <SliderOsd
- fillIcons
- type="volume"
- windowSetup={(self, show) => {
- self.hook(audio.defaultSpeaker, "notify::volume", show);
- self.hook(audio.defaultSpeaker, "notify::mute", show);
- }}
- className={audio.defaultSpeaker.mute ? "mute" : ""}
- initValue={audio.defaultSpeaker.volume}
- drawAreaSetup={(self, icon) => {
- const updateIcon = () => {
- if (/head(phone|set)/i.test(audio.defaultSpeaker.icon)) icon.set("headphones");
- else if (audio.defaultSpeaker.mute) icon.set("no_sound");
- else if (audio.defaultSpeaker.volume === 0) icon.set("volume_mute");
- else if (audio.defaultSpeaker.volume <= 0.5) icon.set("volume_down");
- else icon.set("volume_up");
- };
- updateIcon();
- self.hook(audio.defaultSpeaker, "notify::icon", updateIcon);
- self.hook(audio.defaultSpeaker, "notify::mute", () => {
- updateIcon();
- self.toggleClassName("mute", audio.defaultSpeaker.mute);
- });
- self.hook(audio.defaultSpeaker, "notify::volume", () => {
- updateIcon();
- self.css = `font-size: ${audio.defaultSpeaker.volume}px`;
- });
- }}
- />
-);
-
-const Brightness = ({ monitor }: { monitor: Monitor }) => (
- <SliderOsd
- monitor={monitor}
- type="brightness"
- windowSetup={(self, show) => self.hook(monitor, "notify::brightness", show)}
- initValue={monitor.brightness}
- drawAreaSetup={(self, icon) => {
- const update = () => {
- if (monitor.brightness > 0.66) icon.set("brightness_high");
- else if (monitor.brightness > 0.33) icon.set("brightness_medium");
- else if (monitor.brightness > 0) icon.set("brightness_low");
- else icon.set("brightness_empty");
- self.css = `font-size: ${monitor.brightness}px`;
- };
- self.hook(monitor, "notify::brightness", update);
- update();
- }}
- />
-);
-
-@register()
-class LockOsd extends Widget.Window {
- readonly lockType: "caps" | "num";
-
- #timeout: Time | null = null;
-
- constructor({ type, icon, right }: { type: "caps" | "num"; icon: string; right?: boolean }) {
- super({
- visible: false,
- name: `lock-${type}`,
- application: App,
- namespace: `caelestia-lock-${type}`,
- anchor:
- Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
- exclusivity: Astal.Exclusivity.IGNORE,
- });
-
- this.lockType = type;
- this.#update();
-
- this.add(
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className={`lock ${type}`}>
- <label vexpand className="icon" label={icon} />
- <label vexpand className="text" label={capitalize(type) + "lock"} />
- </box>
- );
-
- // Clickthrough
- this.connect("size-allocate", () => this.input_shape_combine_region(new Cairo.Region()));
-
- // Move over when other indicator opens/closes
- this.hook(App, "window-toggled", (_, window) => {
- if (window !== this && window instanceof LockOsd) {
- const child = this.get_child();
- if (!child) return;
- this[right ? "marginLeft" : "marginRight"] = window.visible
- ? child.get_preferred_width()[1] + config.lock.spacing.get()
- : 0;
- }
- });
- }
-
- #update() {
- execAsync(`fish -c 'cat /sys/class/leds/input*::${this.lockType}lock/brightness'`)
- .then(out => (this.get_child() as Widget.Box | null)?.toggleClassName("enabled", out.includes("1")))
- .catch(console.error);
- }
-
- show() {
- super.show();
- this.#update();
- this.#timeout?.cancel();
- this.#timeout = timeout(config.lock[this.lockType].hideDelay.get(), () => this.hide());
- }
-}
-
-export default () => {
- if (AstalWp.get_default()) <Volume audio={AstalWp.get_default()!.audio} />;
- Monitors.get_default().forEach(monitor => <Brightness monitor={monitor} />);
-
- <LockOsd type="caps" icon="keyboard_capslock" />;
- <LockOsd right type="num" icon="filter_1" />;
-
- return null;
-};
diff --git a/src/modules/screencorners.tsx b/src/modules/screencorners.tsx
deleted file mode 100644
index 4368b87..0000000
--- a/src/modules/screencorners.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import ScreenCorner from "@/widgets/screencorner";
-import { bind } from "astal/binding";
-import { Astal } from "astal/gtk3";
-import { bar } from "config";
-import Cairo from "gi://cairo";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-screencorners"
- monitor={monitor.id}
- anchor={bind(bar.vertical).as(
- v =>
- Astal.WindowAnchor.BOTTOM |
- Astal.WindowAnchor.RIGHT |
- (v ? Astal.WindowAnchor.TOP : Astal.WindowAnchor.LEFT)
- )}
- setup={self =>
- self.connect("size-allocate", () => self.get_window()?.input_shape_combine_region(new Cairo.Region(), 0, 0))
- }
- >
- <box vertical={bind(bar.vertical)}>
- <ScreenCorner place={bind(bar.vertical).as(v => (v ? "topright" : "bottomleft"))} />
- <box expand />
- <ScreenCorner place="bottomright" />
- </box>
- </window>
-);
-
-export const BarScreenCorners = ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-screencorners"
- monitor={monitor.id}
- anchor={bind(bar.vertical).as(
- v =>
- Astal.WindowAnchor.TOP |
- Astal.WindowAnchor.LEFT |
- (v ? Astal.WindowAnchor.BOTTOM : Astal.WindowAnchor.RIGHT)
- )}
- visible={bind(bar.style).as(s => s === "embedded")}
- setup={self =>
- self.connect("size-allocate", () => self.get_window()?.input_shape_combine_region(new Cairo.Region(), 0, 0))
- }
- >
- <box vertical={bind(bar.vertical)}>
- <ScreenCorner place="topleft" />
- <box expand />
- <ScreenCorner place={bind(bar.vertical).as(v => (v ? "bottomleft" : "topright"))} />
- </box>
- </window>
-);
diff --git a/src/modules/session.tsx b/src/modules/session.tsx
deleted file mode 100644
index 40d3b31..0000000
--- a/src/modules/session.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import PopupWindow from "@/widgets/popupwindow";
-import { execAsync } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
-
-const Item = ({ icon, label, cmd, isDefault }: { icon: string; label: string; cmd: string; isDefault?: boolean }) => (
- <box vertical className="item">
- <button
- cursor="pointer"
- onClicked={() => execAsync(cmd).catch(console.error)}
- setup={self =>
- isDefault &&
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === "session" && window.visible) self.grab_focus();
- })
- }
- >
- <label className="icon" label={icon} />
- </button>
- <label className="label" label={label} />
- </box>
-);
-
-export default () => (
- <PopupWindow
- className="session"
- name="session"
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT}
- exclusivity={Astal.Exclusivity.IGNORE}
- keymode={Astal.Keymode.EXCLUSIVE}
- layer={Astal.Layer.OVERLAY}
- borderWidth={0} // Don't need border width cause takes up entire screen
- >
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="inner">
- <box>
- <Item icon="logout" label="Logout" cmd="uwsm stop" isDefault />
- <Item icon="cached" label="Reboot" cmd="systemctl reboot" />
- </box>
- <box>
- <Item icon="downloading" label="Hibernate" cmd="systemctl hibernate" />
- <Item icon="power_settings_new" label="Shutdown" cmd="systemctl poweroff" />
- </box>
- </box>
- </PopupWindow>
-);
diff --git a/src/modules/sidebar/alerts.tsx b/src/modules/sidebar/alerts.tsx
deleted file mode 100644
index 9599aff..0000000
--- a/src/modules/sidebar/alerts.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Headlines from "./modules/headlines";
-import Notifications from "./modules/notifications";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="pane alerts" name="alerts">
- <Notifications />
- <box className="separator" />
- <Headlines monitor={monitor} />
- </box>
-);
diff --git a/src/modules/sidebar/audio.tsx b/src/modules/sidebar/audio.tsx
deleted file mode 100644
index 20a6551..0000000
--- a/src/modules/sidebar/audio.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import DeviceSelector from "./modules/deviceselector";
-import Media from "./modules/media";
-import Streams from "./modules/streams";
-
-export default () => (
- <box vertical className="pane audio" name="audio">
- <Media />
- <box className="separator" />
- <Streams />
- <box className="separator" />
- <DeviceSelector />
- </box>
-);
diff --git a/src/modules/sidebar/connectivity.tsx b/src/modules/sidebar/connectivity.tsx
deleted file mode 100644
index 2962b56..0000000
--- a/src/modules/sidebar/connectivity.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import Bluetooth from "./modules/bluetooth";
-import Networks from "./modules/networks";
-
-export default () => (
- <box vertical className="pane connectivity" name="connectivity">
- <Networks />
- <box className="separator" />
- <Bluetooth />
- </box>
-);
diff --git a/src/modules/sidebar/dashboard.tsx b/src/modules/sidebar/dashboard.tsx
deleted file mode 100644
index 1a8626f..0000000
--- a/src/modules/sidebar/dashboard.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import Players from "@/services/players";
-import { lengthStr } from "@/utils/strings";
-import { bindCurrentTime, osIcon } from "@/utils/system";
-import Slider from "@/widgets/slider";
-import { bind, GLib, monitorFile, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalMpris from "gi://AstalMpris";
-import Notifications from "./modules/notifications";
-import Upcoming from "./modules/upcoming";
-
-const noNull = (s: string | null) => s ?? "-";
-
-const FaceFallback = () => (
- <label
- setup={self => {
- const name = GLib.get_real_name();
- if (name !== "Unknown")
- self.label = name
- .split(" ")
- .map(s => s[0].toUpperCase())
- .join("");
- else {
- self.label = "";
- self.xalign = 0.44;
- }
- }}
- />
-);
-
-const User = () => {
- const uptime = Variable("").poll(5000, "uptime -p");
- const hasFace = Variable(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS));
-
- return (
- <box className="user">
- <box
- homogeneous
- className="face"
- setup={self => {
- self.css = `background-image: url("${HOME}/.face");`;
- const monitor = monitorFile(HOME + "/.face", () => {
- hasFace.set(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS));
- self.css = `background-image: url("${HOME}/.face");`;
- });
- self.connect("destroy", () => monitor.cancel());
- }}
- >
- {bind(hasFace).as(h => (h ? <box visible={false} /> : <FaceFallback />))}
- </box>
- <box vertical hexpand valign={Gtk.Align.CENTER} className="details">
- <label truncate xalign={0} className="name" label={`${osIcon} ${GLib.get_user_name()}`} />
- <label truncate xalign={0} label={bind(uptime)} onDestroy={() => uptime.drop()} />
- <label truncate xalign={0} label={bindCurrentTime("%A, %e %B")} />
- </box>
- </box>
- );
-};
-
-const Media = ({ player }: { player: AstalMpris.Player | null }) => {
- const position = player
- ? Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l)
- : Variable(0);
-
- return (
- <box className="media" onDestroy={() => position.drop()}>
- <box
- homogeneous
- className="cover-art"
- css={player ? bind(player, "coverArt").as(a => `background-image: url("${a}");`) : ""}
- >
- {player ? (
- bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.31} label="" />))
- ) : (
- <label xalign={0.31} label="" />
- )}
- </box>
- <box vertical className="details">
- <label truncate className="title" label={player ? bind(player, "title").as(noNull) : ""} />
- <label truncate className="artist" label={player ? bind(player, "artist").as(noNull) : "No media"} />
- <box hexpand className="controls">
- <button
- hexpand
- sensitive={player ? bind(player, "canGoPrevious") : false}
- cursor="pointer"
- onClicked={() => player?.next()}
- label="󰒮"
- />
- <button
- hexpand
- sensitive={player ? bind(player, "canControl") : false}
- cursor="pointer"
- onClicked={() => player?.play_pause()}
- label={
- player
- ? bind(player, "playbackStatus").as(s =>
- s === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"
- )
- : "󰐊"
- }
- />
- <button
- hexpand
- sensitive={player ? bind(player, "canGoNext") : false}
- cursor="pointer"
- onClicked={() => player?.next()}
- label="󰒭"
- />
- </box>
- <Slider value={bind(position)} onChange={(_, v) => player?.set_position(v * player.length)} />
- <box className="time">
- <label label={player ? bind(player, "position").as(lengthStr) : "-1:-1"} />
- <box hexpand />
- <label label={player ? bind(player, "length").as(lengthStr) : "-1:-1"} />
- </box>
- </box>
- </box>
- );
-};
-
-export default () => (
- <box vertical className="pane dashboard" name="dashboard">
- <User />
- <box className="separator" />
- {bind(Players.get_default(), "lastPlayer").as(p => (
- <Media player={p} />
- ))}
- <box className="separator" />
- <Notifications compact />
- <box className="separator" />
- <Upcoming />
- </box>
-);
diff --git a/src/modules/sidebar/index.tsx b/src/modules/sidebar/index.tsx
deleted file mode 100644
index 7570283..0000000
--- a/src/modules/sidebar/index.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import { bind, idle, register, Variable } from "astal";
-import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3";
-import { sidebar as config } from "config";
-import Alerts from "./alerts";
-import Audio from "./audio";
-import Connectivity from "./connectivity";
-import Dashboard from "./dashboard";
-import Packages from "./packages";
-import Time from "./time";
-
-export const paneNames = ["dashboard", "audio", "connectivity", "packages", "alerts", "time"] as const;
-export type PaneName = (typeof paneNames)[number];
-
-export const switchPane = (monitor: Monitor, name: PaneName) => {
- const sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
- if (sidebar) {
- if (sidebar.visible && sidebar.shown.get() === name) sidebar.hide();
- else sidebar.show();
- sidebar.shown.set(name);
- }
-};
-
-export const awaitSidebar = (monitor: Monitor) =>
- new Promise<SideBar>(resolve => {
- let sidebar: SideBar | null = null;
-
- const awaitSidebar = () => {
- sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
- if (sidebar) resolve(sidebar);
- else idle(awaitSidebar);
- };
- idle(awaitSidebar);
- });
-
-const getPane = (monitor: Monitor, name: PaneName) => {
- if (name === "dashboard") return <Dashboard />;
- if (name === "audio") return <Audio />;
- if (name === "connectivity") return <Connectivity />;
- if (name === "packages") return <Packages monitor={monitor} />;
- if (name === "alerts") return <Alerts monitor={monitor} />;
- return <Time />;
-};
-
-@register()
-export default class SideBar extends Widget.Window {
- readonly shown: Variable<PaneName>;
-
- constructor({ monitor }: { monitor: Monitor }) {
- super({
- application: App,
- name: `sidebar${monitor.id}`,
- namespace: "caelestia-sidebar",
- monitor: monitor.id,
- anchor: Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM,
- exclusivity: Astal.Exclusivity.EXCLUSIVE,
- visible: false,
- });
-
- this.shown = Variable(paneNames[0]);
-
- this.add(
- <eventbox
- onScroll={(_, event) => {
- if (event.modifier & Gdk.ModifierType.BUTTON1_MASK) {
- const index = paneNames.indexOf(this.shown.get()) + (event.delta_y < 0 ? -1 : 1);
- if (index < 0 || index >= paneNames.length) return;
- this.shown.set(paneNames[index]);
- }
- }}
- >
- <box vertical className="sidebar">
- <stack
- vexpand
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={200}
- shown={bind(this.shown)}
- >
- {paneNames.map(n => getPane(monitor, n))}
- </stack>
- </box>
- </eventbox>
- );
-
- if (config.showOnStartup.get()) idle(() => this.show());
- }
-}
diff --git a/src/modules/sidebar/modules/bluetooth.tsx b/src/modules/sidebar/modules/bluetooth.tsx
deleted file mode 100644
index 89d0cb7..0000000
--- a/src/modules/sidebar/modules/bluetooth.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import { bind, Variable } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-import AstalBluetooth from "gi://AstalBluetooth";
-
-const sortDevices = (a: AstalBluetooth.Device, b: AstalBluetooth.Device) => {
- if (a.connected || b.connected) return a.connected ? -1 : 1;
- if (a.paired || b.paired) return a.paired ? -1 : 1;
- return 0;
-};
-
-const BluetoothDevice = (device: AstalBluetooth.Device) => (
- <box className={bind(device, "connected").as(c => `device ${c ? "connected" : ""}`)}>
- <icon
- className="icon"
- icon={bind(device, "icon").as(i =>
- Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "bluetooth-symbolic"
- )}
- />
- <box vertical hexpand>
- <label truncate xalign={0} label={bind(device, "alias")} />
- <label
- truncate
- className="sublabel"
- xalign={0}
- setup={self => {
- const update = () => {
- self.label =
- (device.connected ? "Connected" : "Paired") +
- (device.batteryPercentage >= 0 ? ` (${device.batteryPercentage * 100}%)` : "");
- self.visible = device.connected || device.paired;
- };
- self.hook(device, "notify::connected", update);
- self.hook(device, "notify::paired", update);
- self.hook(device, "notify::battery-percentage", update);
- update();
- }}
- />
- </box>
- <button
- valign={Gtk.Align.CENTER}
- visible={bind(device, "paired")}
- cursor="pointer"
- onClicked={() => AstalBluetooth.get_default().adapter.remove_device(device)}
- label="delete"
- />
- <button
- valign={Gtk.Align.CENTER}
- cursor="pointer"
- onClicked={self => {
- if (device.connected)
- device.disconnect_device((_, res) => {
- self.sensitive = true;
- device.disconnect_device_finish(res);
- });
- else
- device.connect_device((_, res) => {
- self.sensitive = true;
- device.connect_device_finish(res);
- });
- self.sensitive = false;
- }}
- label={bind(device, "connected").as(c => (c ? "bluetooth_disabled" : "bluetooth_searching"))}
- />
- </box>
-);
-
-const List = ({ devNotify }: { devNotify: Variable<boolean> }) => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(devNotify).as(() => AstalBluetooth.get_default().devices.sort(sortDevices).map(BluetoothDevice))}
- </box>
-);
-
-const NoDevices = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="bluetooth_searching" />
- <label label="No Bluetooth devices" />
- </box>
- </box>
-);
-
-export default () => {
- const bluetooth = AstalBluetooth.get_default();
- const devNotify = Variable(false); // Aggregator for device state changes (connected/paired)
-
- const update = () => devNotify.set(!devNotify.get());
- const connectSignals = (device: AstalBluetooth.Device) => {
- device.connect("notify::connected", update);
- device.connect("notify::paired", update);
- };
- bluetooth.get_devices().forEach(connectSignals);
- bluetooth.connect("device-added", (_, device) => connectSignals(device));
- bluetooth.connect("notify::devices", update);
-
- return (
- <box vertical className="bluetooth">
- <box className="header-bar">
- <label
- label={bind(devNotify).as(() => {
- const nConnected = bluetooth.get_devices().filter(d => d.connected).length;
- return `${nConnected} connected device${nConnected === 1 ? "" : "s"}`;
- })}
- />
- <box hexpand />
- <button
- className={bind(bluetooth.adapter, "discovering").as(d => (d ? "enabled" : ""))}
- cursor="pointer"
- onClicked={() => {
- if (bluetooth.adapter.discovering) bluetooth.adapter.start_discovery();
- else bluetooth.adapter.stop_discovery();
- }}
- label="󰀂 Discovery"
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(bluetooth, "devices").as(d => (d.length > 0 ? "list" : "empty"))}
- >
- <NoDevices />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List devNotify={devNotify} />
- </scrollable>
- </stack>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/calendar.tsx b/src/modules/sidebar/modules/calendar.tsx
deleted file mode 100644
index bb36909..0000000
--- a/src/modules/sidebar/modules/calendar.tsx
+++ /dev/null
@@ -1,252 +0,0 @@
-import Calendar, { type IEvent } from "@/services/calendar";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, GLib, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import ical from "ical.js";
-
-const isLeapYear = (year: number) => year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0);
-
-const getMonthDays = (month: number, year: number) => {
- const leapYear = isLeapYear(year);
- if (month === 2 && leapYear) return leapYear ? 29 : 28;
- if ((month <= 7 && month % 2 === 1) || (month >= 8 && month % 2 === 0)) return 31;
- return 30;
-};
-
-const getNextMonthDays = (month: number, year: number) => {
- if (month === 12) return 31;
- return getMonthDays(month + 1, year);
-};
-
-const getPrevMonthDays = (month: number, year: number) => {
- if (month === 1) return 31;
- return getMonthDays(month - 1, year);
-};
-
-export function getCalendarLayout(date: ical.Time) {
- const weekdayOfMonthFirst = date.startOfMonth().dayOfWeek(ical.Time.MONDAY);
- const daysInMonth = getMonthDays(date.month, date.year);
- const daysInPrevMonth = getPrevMonthDays(date.month, date.year);
-
- const calendar: ical.Time[][] = [];
- let idx = -weekdayOfMonthFirst + 2;
-
- for (let i = 0; i < 6; i++) {
- calendar.push([]);
-
- for (let j = 0; j < 7; j++) {
- let cDay = idx++;
- let cMonth = date.month;
- let cYear = date.year;
-
- if (idx < 0) {
- cDay = daysInPrevMonth + cDay;
- cMonth--;
-
- if (cMonth < 0) {
- cMonth += 12;
- cYear--;
- }
- } else if (idx > daysInMonth) {
- cDay -= daysInMonth;
- cMonth++;
-
- if (cMonth > 12) {
- cMonth -= 12;
- cYear++;
- }
- }
-
- calendar[i].push(ical.Time.fromData({ day: cDay, month: cMonth, year: cYear }));
- }
- }
-
- return calendar;
-}
-
-const dateToMonthYear = (date: ical.Time) => {
- const months = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
- ];
- return `${months[date.month - 1]} ${date.year}`;
-};
-
-const addMonths = (date: ical.Time, num: number) => {
- date = date.clone();
- if (num > 0) for (let i = 0; i < num; i++) date.adjust(getNextMonthDays(date.month, date.year), 0, 0, 0);
- else for (let i = 0; i > num; i--) date.adjust(-getPrevMonthDays(date.month, date.year), 0, 0, 0);
- return date;
-};
-
-const getDayClassName = (day: ical.Time, current: Variable<ical.Time>) => {
- const isToday = day.toJSDate().toDateString() === new Date().toDateString() ? "today" : "";
- const numEvents = Math.min(5, Calendar.get_default().getEventsForDay(day).length);
- return `day ${isToday} ${day.month !== current.get().month ? "dim" : ""} events-${numEvents}`;
-};
-
-const getDayTooltip = (day: ical.Time) => {
- const events = Calendar.get_default().getEventsForDay(day);
- if (!events.length) return "";
- const eventsStr = events
- .map(e => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(e.endDate.toUnixTime());
- const sameAmPm = start.format("%P") === end.format("%P");
- const time = `${start.format(`%-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`;
- return `<b>${e.event.summary.replaceAll("&", "&amp;")}</b> • ${time}`;
- })
- .join("\n");
- return `${events.length} event${events.length === 1 ? "" : "s"}\n${eventsStr}`;
-};
-
-const getEventsHeader = (current: ical.Time) => {
- const events = Calendar.get_default().getEventsForDay(current);
- const isToday = current.toJSDate().toDateString() === new Date().toDateString();
- return (
- (isToday ? "Today • " : "") +
- GLib.DateTime.new_from_unix_local(current.toUnixTime()).format("%B %-d • %A") +
- ` • ${events.length} event${events.length === 1 ? "" : "s"}`
- );
-};
-
-const getEventHeader = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const time = `${start.format("%-I")}${start.get_minute() > 0 ? `:${start.get_minute()}` : ""}${start.format("%P")}`;
- return `${time} <b>${e.event.summary.replaceAll("&", "&amp;")}</b>`;
-};
-
-const getEventTooltip = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(e.event.endDate.toUnixTime());
- const sameAmPm = start.format("%P") === end.format("%P");
- const time = `${start.format(`%A, %-d %B • %-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`;
- const locIfExists = e.event.location ? ` ${e.event.location}\n` : "";
- const descIfExists = e.event.description ? `󰒿 ${e.event.description}\n` : "";
- return `<b>${e.event.summary}</b>\n${time}\n${locIfExists}${descIfExists}󰃭 ${e.calendar}`.replaceAll("&", "&amp;");
-};
-
-const Day = ({ day, shown, current }: { day: ical.Time; shown: Variable<string>; current: Variable<ical.Time> }) => (
- <button
- className={bind(Calendar.get_default(), "calendars").as(() => getDayClassName(day, current))}
- cursor="pointer"
- onClicked={() => {
- shown.set("events");
- current.set(day);
- }}
- setup={self =>
- setupCustomTooltip(
- self,
- bind(Calendar.get_default(), "calendars").as(() => getDayTooltip(day)),
- { useMarkup: true }
- )
- }
- >
- <box vertical>
- <label label={day.day.toString()} />
- <box className="indicator" />
- </box>
- </button>
-);
-
-const CalendarView = ({ shown, current }: { shown: Variable<string>; current: Variable<ical.Time> }) => (
- <box vertical className="calendar-view" name="calendar">
- <box className="header">
- <button
- cursor="pointer"
- onClicked={() => current.set(ical.Time.now())}
- label={bind(current).as(dateToMonthYear)}
- />
- <box hexpand />
- <button cursor="pointer" onClicked={() => current.set(addMonths(current.get(), -1))} label="" />
- <button cursor="pointer" onClicked={() => current.set(addMonths(current.get(), 1))} label="" />
- </box>
- <box halign={Gtk.Align.CENTER} className="weekdays">
- {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(d => (
- <label label={d} />
- ))}
- </box>
- <box vertical halign={Gtk.Align.CENTER} className="month">
- {bind(current).as(c =>
- getCalendarLayout(c).map(r => (
- <box className="week">
- {r.map(d => (
- <Day day={d} shown={shown} current={current} />
- ))}
- </box>
- ))
- )}
- </box>
- </box>
-);
-
-const Event = (event: IEvent) => (
- <box className="event" setup={self => setupCustomTooltip(self, getEventTooltip(event), { useMarkup: true })}>
- <box className={`calendar-indicator calendar-${Calendar.get_default().getCalendarIndex(event.calendar)}`} />
- <box vertical>
- <label truncate useMarkup xalign={0} label={getEventHeader(event)} />
- {event.event.location && <label truncate xalign={0} label={event.event.location} className="sublabel" />}
- {event.event.description && (
- <label truncate useMarkup xalign={0} label={event.event.description} className="sublabel" />
- )}
- </box>
- </box>
-);
-
-const List = ({ current }: { current: Variable<ical.Time> }) => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(current).as(c => Calendar.get_default().getEventsForDay(c).map(Event))}
- </box>
-);
-
-const NoEvents = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="calendar_month" />
- <label label="Day all clear!" />
- </box>
- </box>
-);
-
-const Events = ({ shown, current }: { shown: Variable<string>; current: Variable<ical.Time> }) => (
- <box vertical className="events" name="events">
- <box className="header">
- <button cursor="pointer" onClicked={() => shown.set("calendar")} label="" />
- <label hexpand truncate xalign={0} label={bind(current).as(getEventsHeader)} />
- </box>
- <stack shown={bind(current).as(c => (Calendar.get_default().getEventsForDay(c).length > 0 ? "list" : "empty"))}>
- <NoEvents />
- <scrollable hscroll={Gtk.PolicyType.NEVER} name="list">
- <List current={current} />
- </scrollable>
- </stack>
- </box>
-);
-
-export default () => {
- const shown = Variable<"calendar" | "events">("calendar");
- const current = Variable(ical.Time.now());
-
- return (
- <box vertical className="calendar">
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
- transitionDuration={150}
- shown={bind(shown)}
- >
- <CalendarView shown={shown} current={current} />
- <Events shown={shown} current={current} />
- </stack>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/deviceselector.tsx b/src/modules/sidebar/modules/deviceselector.tsx
deleted file mode 100644
index e74e6f5..0000000
--- a/src/modules/sidebar/modules/deviceselector.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import { bind, execAsync, Variable, type Binding } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-import AstalWp from "gi://AstalWp";
-
-const Device = ({
- input,
- defaultDevice,
- showDropdown,
- device,
-}: {
- input?: boolean;
- defaultDevice: Binding<AstalWp.Endpoint>;
- showDropdown: Variable<boolean>;
- device: AstalWp.Endpoint;
-}) => (
- <button
- visible={defaultDevice.get().id !== device.id}
- cursor="pointer"
- onClicked={() => {
- execAsync(`wpctl set-default ${device.id}`).catch(console.error);
- showDropdown.set(false);
- }}
- setup={self => {
- let last: { d: AstalWp.Endpoint; id: number } | null = {
- d: defaultDevice.get(),
- id: defaultDevice
- .get()
- .connect("notify::id", () => self.set_visible(defaultDevice.get().id !== device.id)),
- };
- self.hook(defaultDevice, (_, d) => {
- last?.d.disconnect(last.id);
- self.set_visible(d.id !== device.id);
- last = {
- d,
- id: d.connect("notify::id", () => self.set_visible(d.id !== device.id)),
- };
- });
- self.connect("destroy", () => last?.d.disconnect(last.id));
- }}
- >
- <box className="device">
- {bind(device, "icon").as(i =>
- Astal.Icon.lookup_icon(i) ? (
- <icon className="icon" icon={device.icon} />
- ) : (
- <label className="icon" label={input ? "mic" : "media_output"} />
- )
- )}
- <label truncate label={bind(device, "description")} />
- </box>
- </button>
-);
-
-const DefaultDevice = ({ input, device }: { input?: boolean; device: AstalWp.Endpoint }) => (
- <box className="selected">
- <label className="icon" label={input ? "mic" : "media_output"} />
- <box vertical>
- <label
- truncate
- xalign={0}
- label={bind(device, "description").as(d => (input ? "[In] " : "[Out] ") + (d ?? "Unknown"))}
- />
- <label
- xalign={0}
- className="sublabel"
- label={bind(device, "volume").as(v => `Volume ${Math.round(v * 100)}%`)}
- />
- </box>
- </box>
-);
-
-const Selector = ({ input, audio }: { input?: boolean; audio: AstalWp.Audio }) => {
- const showDropdown = Variable(false);
- const defaultDevice = bind(audio, input ? "defaultMicrophone" : "defaultSpeaker");
-
- return (
- <box vertical className="selector">
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_UP}
- transitionDuration={150}
- revealChild={bind(showDropdown)}
- >
- <box vertical className="list">
- {bind(audio, input ? "microphones" : "speakers").as(ds =>
- ds.map(d => (
- <Device
- input={input}
- defaultDevice={defaultDevice}
- showDropdown={showDropdown}
- device={d}
- />
- ))
- )}
- <box className="separator" />
- </box>
- </revealer>
- <button cursor="pointer" onClick={() => showDropdown.set(!showDropdown.get())}>
- {defaultDevice.as(d => (
- <DefaultDevice input={input} device={d} />
- ))}
- </button>
- </box>
- );
-};
-
-const NoWp = () => (
- <box homogeneous>
- <box vertical valign={Gtk.Align.CENTER}>
- <label label="Device selector unavailable" />
- <label className="no-wp-prompt" label="WirePlumber is required for this module" />
- </box>
- </box>
-);
-
-export default () => {
- const audio = AstalWp.get_default()?.get_audio();
-
- if (!audio) return <NoWp />;
-
- return (
- <box vertical className="device-selector">
- <Selector input audio={audio} />
- <Selector audio={audio} />
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/headlines.tsx b/src/modules/sidebar/modules/headlines.tsx
deleted file mode 100644
index 40d468b..0000000
--- a/src/modules/sidebar/modules/headlines.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import News, { type IArticle } from "@/services/news";
-import Palette, { type IPalette } from "@/services/palette";
-import { capitalize } from "@/utils/strings";
-import { bind, execAsync, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import { sidebar } from "config";
-import { setConfig } from "config/funcs";
-
-const fixGoogleNews = (colours: IPalette, title: string, desc: string) => {
- // Add separator, bold and split at domain (domain is at the end of each headline)
- const domain = title.split(" - ").at(-1);
- if (domain) desc = desc.replaceAll(domain, `— <span foreground="${colours.subtext0}">${domain}</span>\n\n`);
- // Split headlines
- desc = desc.replace(/(( |\.)[^A-Z][a-z]+)([A-Z])/g, "$1\n\n$3");
- desc = desc.replace(/( [A-Z]+)([A-Z](?![s])[a-z])/g, "$1\n\n$2");
- // Add separator and bold domains
- desc = desc.replace(/ ([a-zA-Z.]+)\n\n/g, ` — <span foreground="${colours.subtext0}">$1</span>\n\n`);
- desc = desc.replace(/ ([a-zA-Z.]+)$/, ` — <span foreground="${colours.subtext0}">$1</span>`); // Last domain
- return desc.trim();
-};
-
-const fixNews = (colours: IPalette, title: string, desc: string, source: string) => {
- // Add spaces between sentences
- desc = desc.replace(/\.([A-Z])/g, ". $1");
- // Google News needs some other fixes
- if (source === "Google News") desc = fixGoogleNews(colours, title, desc);
- return desc.replaceAll("&", "&amp;");
-};
-
-const getCategoryIcon = (category: string) => {
- if (category === "business") return "monitoring";
- if (category === "crime") return "speed_camera";
- if (category === "domestic") return "home";
- if (category === "education") return "school";
- if (category === "entertainment") return "tv";
- if (category === "environment") return "eco";
- if (category === "food") return "restaurant";
- if (category === "health") return "health_and_safety";
- if (category === "lifestyle") return "digital_wellbeing";
- if (category === "politics") return "account_balance";
- if (category === "science") return "science";
- if (category === "sports") return "sports_basketball";
- if (category === "technology") return "account_tree";
- if (category === "top") return "breaking_news";
- if (category === "tourism") return "travel";
- if (category === "world") return "public";
- return "newsmode";
-};
-
-const Article = ({ title, description, creator, pubDate, source_name, link }: IArticle) => {
- const expanded = Variable(false);
-
- return (
- <box vertical className="article">
- <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
- <box hexpand className="header">
- <box vertical>
- <label truncate xalign={0} label={title} />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={source_name + (creator ? ` (${creator.join(", ")})` : "")}
- />
- </box>
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <button onClicked={() => execAsync(`app2unit -O -- ${link}`)}>
- <box vertical className="article-body">
- <label wrap className="title" xalign={0} label={title} />
- <label wrap xalign={0} label={`Published on ${new Date(pubDate).toLocaleString()}`} />
- <label
- wrap
- xalign={0}
- className="sublabel"
- label={`By ${
- creator?.join(", ") ??
- (source_name === "Google News" ? title.split(" - ").at(-1) : source_name)
- }`}
- />
- {description && (
- <label
- wrap
- useMarkup
- xalign={0}
- label={bind(Palette.get_default(), "colours").as(c =>
- fixNews(c, title, description, source_name)
- )}
- />
- )}
- </box>
- </button>
- </revealer>
- </box>
- );
-};
-
-const Category = ({ title, articles }: { title: string; articles: IArticle[] }) => {
- const expanded = Variable(false);
-
- return (
- <box vertical className="category">
- <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
- <box className="header">
- <label className="icon" label={getCategoryIcon(title)} />
- <label label={`${capitalize(title)} (${articles.length})`} />
- <box hexpand />
- <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <box vertical className="body">
- {articles
- .sort((a, b) => a.source_priority - b.source_priority)
- .map(a => (
- <Article {...a} />
- ))}
- </box>
- </revealer>
- </box>
- );
-};
-
-const List = () => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(News.get_default(), "categories").as(c =>
- Object.entries(c).map(([k, v]) => <Category title={k} articles={v} />)
- )}
- </box>
-);
-
-const NoNews = ({ disabled }: { disabled?: boolean }) => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="full_coverage" />
- <label label={disabled ? "Headlines disabled" : "No news headlines!"} />
- </box>
- </box>
-);
-
-const HeadlinesDisabled = () => (
- <>
- <box vertical className="headlines">
- <box className="header-bar">
- <label label="Top news headlines" />
- <box hexpand />
- <button
- cursor="pointer"
- onClicked={() => setConfig("sidebar.modules.headlines.enabled", true)}
- label="󰞉 Enable"
- />
- </box>
- <NoNews disabled />
- </box>
- </>
-);
-
-const Headlines = ({ monitor }: { monitor: Monitor }) => (
- <>
- <box className="header-bar">
- <label label="Top news headlines" />
- <box hexpand />
- <button
- className={bind(News.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(News.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => News.get_default().getNews()}
- label={bind(News.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(News.get_default(), "articles").as(a => (a.length > 0 ? "list" : "empty"))}
- >
- <NoNews />
- <scrollable
- css={bind(News.get_default(), "articles").as(a =>
- a.length > 0 ? `min-height: ${Math.round(monitor.height * 0.4)}px;` : ""
- )}
- hscroll={Gtk.PolicyType.NEVER}
- name="list"
- >
- <List />
- </scrollable>
- </stack>
- </>
-);
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="headlines">
- {bind(sidebar.modules.headlines.enabled).as(e => (e ? <Headlines monitor={monitor} /> : <HeadlinesDisabled />))}
- </box>
-);
diff --git a/src/modules/sidebar/modules/hwresources.tsx b/src/modules/sidebar/modules/hwresources.tsx
deleted file mode 100644
index 768d8bd..0000000
--- a/src/modules/sidebar/modules/hwresources.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import Cpu from "@/services/cpu";
-import Gpu from "@/services/gpu";
-import Memory from "@/services/memory";
-import Storage from "@/services/storage";
-import Slider from "@/widgets/slider";
-import { bind, type Binding } from "astal";
-import { Gtk, type Widget } from "astal/gtk3";
-
-const fmt = (bytes: number, pow: number) => +(bytes / 1024 ** pow).toFixed(2);
-const format = ({ total, used }: { total: number; used: number }) => {
- if (total >= 1024 ** 4) return `${fmt(used, 4)}/${fmt(total, 4)} TiB`;
- if (total >= 1024 ** 3) return `${fmt(used, 3)}/${fmt(total, 3)} GiB`;
- if (total >= 1024 ** 2) return `${fmt(used, 2)}/${fmt(total, 2)} MiB`;
- if (total >= 1024) return `${fmt(used, 1)}/${fmt(total, 1)} KiB`;
- return `${used}/${total} B`;
-};
-
-const Resource = ({
- icon,
- name,
- value,
- labelSetup,
-}: {
- icon: string;
- name: string;
- value: Binding<number>;
- labelSetup?: (self: Widget.Label) => void;
-}) => (
- <box vertical className={`resource ${name}`}>
- <box className="inner">
- <label label={icon} />
- <Slider value={value.as(v => v / 100)} />
- </box>
- <label halign={Gtk.Align.END} label={labelSetup ? "" : value.as(v => `${+v.toFixed(2)}%`)} setup={labelSetup} />
- </box>
-);
-
-export default () => (
- <box vertical className="hw-resources">
- {Gpu.get_default().available && <Resource icon="󰢮" name="gpu" value={bind(Gpu.get_default(), "usage")} />}
- <Resource icon="" name="cpu" value={bind(Cpu.get_default(), "usage")} />
- <Resource
- icon=""
- name="memory"
- value={bind(Memory.get_default(), "usage")}
- labelSetup={self => {
- const mem = Memory.get_default();
- const update = () => (self.label = format(mem));
- self.hook(mem, "notify::used", update);
- self.hook(mem, "notify::total", update);
- update();
- }}
- />
- <Resource
- icon="󰋊"
- name="storage"
- value={bind(Storage.get_default(), "usage")}
- labelSetup={self => {
- const storage = Storage.get_default();
- const update = () => (self.label = format(storage));
- self.hook(storage, "notify::used", update);
- self.hook(storage, "notify::total", update);
- update();
- }}
- />
- </box>
-);
diff --git a/src/modules/sidebar/modules/media.tsx b/src/modules/sidebar/modules/media.tsx
deleted file mode 100644
index 169a98d..0000000
--- a/src/modules/sidebar/modules/media.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import Players from "@/services/players";
-import { lengthStr } from "@/utils/strings";
-import Slider from "@/widgets/slider";
-import { bind, timeout, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalMpris from "gi://AstalMpris";
-
-const noNull = (s: string | null) => s ?? "-";
-
-const NoMedia = () => (
- <box vertical className="player" name="none">
- <box homogeneous halign={Gtk.Align.CENTER} className="cover-art">
- <label xalign={0.4} label="" />
- </box>
- <box vertical className="progress">
- <Slider value={bind(Variable(0))} />
- <box className="time">
- <label label="-1:-1" />
- <box hexpand />
- <label label="-1:-1" />
- </box>
- </box>
- <box vertical className="details">
- <label truncate className="title" label="No media" />
- <label truncate className="artist" label="Try play some music!" />
- <label truncate className="album" label="" />
- </box>
- <box vertical className="controls">
- <box halign={Gtk.Align.CENTER} className="playback">
- <button sensitive={false} cursor="pointer" label="󰒮" />
- <button sensitive={false} cursor="pointer" label="󰐊" />
- <button sensitive={false} cursor="pointer" label="󰒭" />
- </box>
- <box className="options">
- <button sensitive={false} cursor="pointer" label="󰊓" />
- <button sensitive={false} cursor="pointer" label="󰒞" />
- <box hexpand />
- <button className="needs-adjustment" sensitive={false} cursor="pointer" label="󰑗" />
- <button className="needs-adjustment" sensitive={false} cursor="pointer" label="󰀽" />
- </box>
- </box>
- </box>
-);
-
-const Player = ({ player }: { player: AstalMpris.Player }) => {
- const position = Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l);
-
- return (
- <box vertical className="player" name={player.busName} onDestroy={() => position.drop()}>
- <box
- homogeneous
- halign={Gtk.Align.CENTER}
- className="cover-art"
- css={bind(player, "coverArt").as(a => `background-image: url("${a}");`)}
- >
- {bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.4} label="" />))}
- </box>
- <box vertical className="progress">
- <Slider value={bind(position)} onChange={(_, v) => player.set_position(v * player.length)} />
- <box className="time">
- <label label={bind(player, "position").as(lengthStr)} />
- <box hexpand />
- <label label={bind(player, "length").as(lengthStr)} />
- </box>
- </box>
- <box vertical className="details">
- <label truncate className="title" label={bind(player, "title").as(noNull)} />
- <label truncate className="artist" label={bind(player, "artist").as(noNull)} />
- <label truncate className="album" label={bind(player, "album").as(noNull)} />
- </box>
- <box vertical className="controls">
- <box halign={Gtk.Align.CENTER} className="playback">
- <button
- sensitive={bind(player, "canGoPrevious")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="󰒮"
- />
- <button
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.play_pause()}
- label={bind(player, "playbackStatus").as(s =>
- s === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"
- )}
- />
- <button
- sensitive={bind(player, "canGoNext")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="󰒭"
- />
- </box>
- <box className="options">
- <button
- sensitive={bind(player, "canSetFullscreen")}
- cursor="pointer"
- onClicked={() => player.toggle_fullscreen()}
- label={bind(player, "fullscreen").as(f => (f ? "󰊔" : "󰊓"))}
- />
- <button
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.shuffle()}
- label={bind(player, "shuffleStatus").as(s => (s === AstalMpris.Shuffle.ON ? "󰒝" : "󰒞"))}
- />
- <box hexpand />
- <button
- className="needs-adjustment"
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.loop()}
- label={bind(player, "loopStatus").as(l =>
- l === AstalMpris.Loop.TRACK ? "󰑘" : l === AstalMpris.Loop.PLAYLIST ? "󰑖" : "󰑗"
- )}
- />
- <button
- className="needs-adjustment"
- sensitive={bind(player, "canRaise")}
- cursor="pointer"
- onClicked={() => player.raise()}
- label="󰀽"
- />
- </box>
- </box>
- </box>
- );
-};
-
-const Indicator = ({ active, player }: { active: Variable<string>; player: AstalMpris.Player }) => (
- <button
- className={bind(active).as(a => (a === player.busName ? "active" : ""))}
- cursor="pointer"
- onClicked={() => active.set(player.busName)}
- />
-);
-
-export default () => {
- const players = Players.get_default();
- const active = Variable(players.lastPlayer?.busName ?? "none");
-
- active.observe(players, "notify::list", () => {
- timeout(10, () => active.set(players.lastPlayer?.busName ?? "none"));
- return "none";
- });
-
- return (
- <box vertical className="players" onDestroy={() => active.drop()}>
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
- transitionDuration={150}
- shown={bind(active)}
- >
- <NoMedia />
- {bind(players, "list").as(ps => ps.map(p => <Player player={p} />))}
- </stack>
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={120}
- revealChild={bind(players, "list").as(l => l.length > 1)}
- >
- <box halign={Gtk.Align.CENTER} className="indicators">
- {bind(players, "list").as(ps => ps.map(p => <Indicator active={active} player={p} />))}
- </box>
- </revealer>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/networks.tsx b/src/modules/sidebar/modules/networks.tsx
deleted file mode 100644
index f98a62c..0000000
--- a/src/modules/sidebar/modules/networks.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { bind, execAsync, Variable, type Binding } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalNetwork from "gi://AstalNetwork";
-
-const sortAPs = (saved: string[], a: AstalNetwork.AccessPoint, b: AstalNetwork.AccessPoint) => {
- const { wifi } = AstalNetwork.get_default();
- if (a === wifi.activeAccessPoint || b === wifi.activeAccessPoint) return a === wifi.activeAccessPoint ? -1 : 1;
- if (saved.includes(a.ssid) || saved.includes(b.ssid)) return saved.includes(a.ssid) ? -1 : 1;
- return b.strength - a.strength;
-};
-
-const Network = (accessPoint: AstalNetwork.AccessPoint) => (
- <box
- className={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(
- a => `network ${a === accessPoint ? "connected" : ""}`
- )}
- >
- <icon className="icon" icon={bind(accessPoint, "iconName")} />
- <box vertical hexpand>
- <label truncate xalign={0} label={bind(accessPoint, "ssid").as(s => s ?? "Unknown")} />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={bind(accessPoint, "strength").as(s => `${accessPoint.frequency > 5000 ? 5 : 2.4}GHz • ${s}/100`)}
- />
- </box>
- <box hexpand />
- <button
- valign={Gtk.Align.CENTER}
- visible={false}
- cursor="pointer"
- onClicked={() => execAsync(`nmcli c delete id '${accessPoint.ssid}'`).catch(console.error)}
- label="delete_forever"
- setup={self => {
- let destroyed = false;
- execAsync(`fish -c "nmcli -t -f name,type c show | sed -nE 's/(.*)\\:.*wireless/\\1/p'"`)
- .then(out => !destroyed && (self.visible = out.split("\n").includes(accessPoint.ssid)))
- .catch(console.error);
- self.connect("destroy", () => (destroyed = true));
- }}
- />
- <button
- valign={Gtk.Align.CENTER}
- cursor="pointer"
- onClicked={self => {
- let destroyed = false;
- const id = self.connect("destroy", () => (destroyed = true));
- const cmd =
- AstalNetwork.get_default().wifi.activeAccessPoint === accessPoint ? "c down id" : "d wifi connect";
- execAsync(`nmcli ${cmd} '${accessPoint.ssid}'`)
- .then(() => {
- if (!destroyed) {
- self.sensitive = true;
- self.disconnect(id);
- }
- })
- .catch(console.error);
- self.sensitive = false;
- }}
- label={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(a =>
- a === accessPoint ? "wifi_off" : "wifi"
- )}
- />
- </box>
-);
-
-const List = () => {
- const { wifi } = AstalNetwork.get_default();
- const children = Variable<JSX.Element[]>([]);
-
- const update = async () => {
- const out = await execAsync(`fish -c "nmcli -t -f name,type c show | sed -nE 's/(.*)\\:.*wireless/\\1/p'"`);
- const saved = out.split("\n");
- const aps = wifi.accessPoints
- .filter(a => a.ssid)
- .sort((a, b) => sortAPs(saved, a, b))
- .map(Network);
- children.set(aps);
- };
-
- wifi.connect("notify::active-access-point", () => update().catch(console.error));
- wifi.connect("notify::access-points", () => update().catch(console.error));
- update().catch(console.error);
-
- return (
- <box vertical valign={Gtk.Align.START} className="list" onDestroy={() => children.drop()}>
- {bind(children)}
- </box>
- );
-};
-
-const NoNetworks = ({ label }: { label: Binding<string> | string }) => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="wifi_off" />
- <label label={label} />
- </box>
- </box>
-);
-
-export default () => {
- const network = AstalNetwork.get_default();
- const label = Variable("");
-
- const update = () => {
- if (network.primary === AstalNetwork.Primary.WIFI) label.set(network.wifi.ssid ?? "Disconnected");
- else if (network.primary === AstalNetwork.Primary.WIRED) label.set(`Ethernet (${network.wired.speed})`);
- else label.set("No Wifi");
- };
- network.connect("notify::primary", update);
- network.get_wifi()?.connect("notify::ssid", update);
- network.get_wired()?.connect("notify::speed", update);
- update();
-
- return (
- <box vertical className="networks">
- <box className="header-bar">
- <label label={bind(label)} />
- <box hexpand />
- <button
- sensitive={network.get_wifi() ? bind(network.wifi, "scanning").as(e => !e) : false}
- className={network.get_wifi() ? bind(network.wifi, "scanning").as(s => (s ? "enabled" : "")) : ""}
- cursor="pointer"
- onClicked={() => network.get_wifi()?.scan()}
- label={
- network.get_wifi()
- ? bind(network.wifi, "scanning").as(s => (s ? "󰀂 Scanning" : "󰀂 Scan"))
- : "󰀂 Scan"
- }
- />
- </box>
- {network.get_wifi() ? (
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(network.wifi, "accessPoints").as(a => (a.length > 0 ? "list" : "empty"))}
- >
- <NoNetworks
- label={bind(network.wifi, "enabled").as(p => (p ? "No available networks" : "Wifi is off"))}
- />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List />
- </scrollable>
- </stack>
- ) : (
- <NoNetworks label="Wifi not available" />
- )}
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/news.tsx b/src/modules/sidebar/modules/news.tsx
deleted file mode 100644
index c799757..0000000
--- a/src/modules/sidebar/modules/news.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Palette from "@/services/palette";
-import Updates from "@/services/updates";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-
-const countNews = (news: string) => news.match(/^([0-9]{4}-[0-9]{2}-[0-9]{2} .+)$/gm)?.length ?? 0;
-
-const News = ({ header, body }: { header: string; body: string }) => {
- const expanded = Variable(false);
-
- body = body
- .slice(0, -5) // Remove last unopened \x1b[0m
- .replaceAll("\x1b[0m", "</span>"); // Replace reset code with end span
-
- return (
- <box vertical className="article">
- <button
- className="wrapper"
- cursor="pointer"
- onClicked={() => expanded.set(!expanded.get())}
- setup={self => setupCustomTooltip(self, header)}
- >
- <box hexpand className="header">
- <label className="icon" label="newspaper" />
- <box vertical>
- <label xalign={0} label={header.split(" ")[0]} />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={header.replace(/[0-9]{4}-[0-9]{2}-[0-9]{2} /, "")}
- />
- </box>
- <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <label
- wrap
- useMarkup
- xalign={0}
- className="body"
- label={bind(Palette.get_default(), "teal").as(
- c => body.replaceAll("\x1b[36m", `<span foreground="${c}">`) // Replace colour codes with html spans
- )}
- />
- </revealer>
- </box>
- );
-};
-
-const List = () => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(Updates.get_default(), "news").as(n => {
- const children = [];
- const news = n.split(/^([0-9]{4}-[0-9]{2}-[0-9]{2} .+)$/gm);
- for (let i = 1; i < news.length - 1; i += 2)
- children.push(<News header={news[i].trim()} body={news[i + 1].trim()} />);
- return children;
- })}
- </box>
-);
-
-const NoNews = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="breaking_news" />
- <label label="No Arch news!" />
- </box>
- </box>
-);
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="news">
- <box className="header-bar">
- <label
- label={bind(Updates.get_default(), "news")
- .as(countNews)
- .as(n => `${n} news article${n === 1 ? "" : "s"}`)}
- />
- <box hexpand />
- <button
- className={bind(Updates.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(Updates.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => Updates.get_default().getUpdates()}
- label={bind(Updates.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(Updates.get_default(), "news").as(n => (n ? "list" : "empty"))}
- >
- <NoNews />
- <scrollable
- css={bind(Updates.get_default(), "news").as(n =>
- n ? `min-height: ${Math.round(monitor.height * 0.4)}px;` : ""
- )}
- hscroll={Gtk.PolicyType.NEVER}
- name="list"
- >
- <List />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/modules/notifications.tsx b/src/modules/sidebar/modules/notifications.tsx
deleted file mode 100644
index e9347ec..0000000
--- a/src/modules/sidebar/modules/notifications.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import Notification from "@/widgets/notification";
-import { bind } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-import AstalNotifd from "gi://AstalNotifd";
-
-const List = ({ compact }: { compact?: boolean }) => (
- <box
- vertical
- valign={Gtk.Align.START}
- className="list"
- setup={self => {
- const notifd = AstalNotifd.get_default();
- const map = new Map<number, Notification>();
-
- const addNotification = (notification: AstalNotifd.Notification) => {
- const notif = (<Notification notification={notification} compact={compact} />) as Notification;
- notif.connect("destroy", () => map.get(notification.id) === notif && map.delete(notification.id));
- map.get(notification.id)?.destroyWithAnims();
- map.set(notification.id, notif);
-
- const widget = (
- <eventbox
- // Dismiss on middle click
- onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
- setup={self => self.hook(notif, "destroy", () => self.destroy())}
- >
- {notif}
- </eventbox>
- );
-
- self.pack_end(widget, false, false, 0);
- };
-
- notifd
- .get_notifications()
- .sort((a, b) => a.time - b.time)
- .forEach(addNotification);
-
- self.hook(notifd, "notified", (_, id) => addNotification(notifd.get_notification(id)));
- self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
- }}
- />
-);
-
-const NoNotifs = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="mark_email_unread" />
- <label label="All caught up!" />
- </box>
- </box>
-);
-
-export default ({ compact }: { compact?: boolean }) => (
- <box vertical className="notifications">
- <box className="header-bar">
- <label
- label={bind(AstalNotifd.get_default(), "notifications").as(
- n => `${n.length} notification${n.length === 1 ? "" : "s"}`
- )}
- />
- <box hexpand />
- <button
- className={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "enabled" : ""))}
- cursor="pointer"
- onClicked={() => (AstalNotifd.get_default().dontDisturb = !AstalNotifd.get_default().dontDisturb)}
- label="󰂛 Silence"
- />
- <button
- cursor="pointer"
- onClicked={() =>
- AstalNotifd.get_default()
- .get_notifications()
- .forEach(n => n.dismiss())
- }
- label="󰎟 Clear"
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(AstalNotifd.get_default(), "notifications").as(n => (n.length > 0 ? "list" : "empty"))}
- >
- <NoNotifs />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List compact={compact} />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/modules/streams.tsx b/src/modules/sidebar/modules/streams.tsx
deleted file mode 100644
index 18a9a58..0000000
--- a/src/modules/sidebar/modules/streams.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { bind, execAsync, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalWp from "gi://AstalWp";
-
-interface IStream {
- stream: AstalWp.Endpoint;
- playing: boolean;
-}
-
-const header = (audio: AstalWp.Audio, key: "streams" | "speakers" | "recorders") =>
- `${audio[key].length} ${audio[key].length === 1 ? key.slice(0, -1) : key}`;
-
-const sortStreams = (a: IStream, b: IStream) => {
- if (a.playing || b.playing) return a.playing ? -1 : 1;
- return 0;
-};
-
-const Stream = ({ stream, playing }: IStream) => (
- <box className={`stream ${playing ? "playing" : ""}`}>
- <icon className="icon" icon={bind(stream, "icon")} />
- <box vertical hexpand>
- <label truncate xalign={0} label={bind(stream, "name")} />
- <label truncate xalign={0} className="sublabel" label={bind(stream, "description")} />
- </box>
- <button valign={Gtk.Align.CENTER} cursor="pointer" onClicked={() => (stream.volume -= 0.05)} label="-" />
- <slider
- showFillLevel
- restrictToFillLevel={false}
- fillLevel={2 / 3}
- value={bind(stream, "volume").as(v => v * (2 / 3))}
- setup={self => self.connect("value-changed", () => stream.set_volume(self.value * 1.5))}
- />
- <button valign={Gtk.Align.CENTER} cursor="pointer" onClicked={() => (stream.volume += 0.05)} label="+" />
- </box>
-);
-
-const List = ({ audio }: { audio: AstalWp.Audio }) => {
- const streams = Variable<IStream[]>([]);
-
- const update = async () => {
- const paStreams = JSON.parse(await execAsync("pactl -f json list sink-inputs"));
- streams.set(
- audio.streams.map(s => ({
- stream: s,
- playing: paStreams.find((p: any) => p.properties["object.serial"] == s.serial)?.corked === false,
- }))
- );
- };
-
- streams.watch("pactl -f json subscribe", out => {
- if (JSON.parse(out).on === "sink-input") update().catch(console.error);
- return streams.get();
- });
- audio.connect("notify::streams", () => update().catch(console.error));
-
- return (
- <box vertical valign={Gtk.Align.START} className="list" onDestroy={() => streams.drop()}>
- {bind(streams).as(ps => ps.sort(sortStreams).map(s => <Stream stream={s.stream} playing={s.playing} />))}
- </box>
- );
-};
-
-const NoSources = ({ icon, label }: { icon: string; label: string }) => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label={icon} />
- <label label={label} />
- </box>
- </box>
-);
-
-const NoWp = () => (
- <box vexpand homogeneous>
- <box vertical valign={Gtk.Align.CENTER}>
- <NoSources icon="no_sound" label="Streams module unavailable" />
- <label className="no-wp-prompt" label="WirePlumber is required for this module" />
- </box>
- </box>
-);
-
-export default () => {
- const audio = AstalWp.get_default()?.get_audio();
-
- if (!audio) return <NoWp />;
-
- const label = Variable(`${header(audio, "streams")} • ${header(audio, "recorders")}`);
-
- label.observe(
- ["streams", "recorders"].map(k => [audio, `notify::${k}`]),
- () => `${header(audio, "streams")} • ${header(audio, "recorders")}`
- );
-
- return (
- <box vertical className="streams" onDestroy={() => label.drop()}>
- <box halign={Gtk.Align.CENTER} className="header-bar">
- <label label={bind(label)} />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(audio, "streams").as(s => (s.length > 0 ? "list" : "empty"))}
- >
- <NoSources icon="stream" label="No audio sources" />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List audio={audio} />
- </scrollable>
- </stack>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/upcoming.tsx b/src/modules/sidebar/modules/upcoming.tsx
deleted file mode 100644
index a64e051..0000000
--- a/src/modules/sidebar/modules/upcoming.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import Calendar, { type IEvent } from "@/services/calendar";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, GLib } from "astal";
-import { Gtk } from "astal/gtk3";
-
-const getDateHeader = (events: IEvent[]) => {
- const date = events[0].startDate;
- const isToday = date.toJSDate().toDateString() === new Date().toDateString();
- return (
- (isToday ? "Today • " : "") +
- GLib.DateTime.new_from_unix_local(date.toUnixTime()).format("%B %-d • %A") +
- ` • ${events.length} event${events.length === 1 ? "" : "s"}`
- );
-};
-
-const getEventHeader = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const time = `${start.format("%-I")}${start.get_minute() > 0 ? `:${start.get_minute()}` : ""}${start.format("%P")}`;
- return `${time} <b>${e.event.summary.replaceAll("&", "&amp;")}</b>`;
-};
-
-const getEventTooltip = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(e.event.endDate.toUnixTime());
- const sameAmPm = start.format("%P") === end.format("%P");
- const time = `${start.format(`%A, %-d %B • %-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`;
- const locIfExists = e.event.location ? ` ${e.event.location}\n` : "";
- const descIfExists = e.event.description ? `󰒿 ${e.event.description}\n` : "";
- return `<b>${e.event.summary}</b>\n${time}\n${locIfExists}${descIfExists}󰃭 ${e.calendar}`.replaceAll("&", "&amp;");
-};
-
-const Event = (event: IEvent) => (
- <box className="event" setup={self => setupCustomTooltip(self, getEventTooltip(event), { useMarkup: true })}>
- <box className={`calendar-indicator calendar-${Calendar.get_default().getCalendarIndex(event.calendar)}`} />
- <box vertical>
- <label truncate useMarkup xalign={0} label={getEventHeader(event)} />
- {event.event.location && <label truncate xalign={0} label={event.event.location} className="sublabel" />}
- {event.event.description && (
- <label truncate useMarkup xalign={0} label={event.event.description} className="sublabel" />
- )}
- </box>
- </box>
-);
-
-const Day = ({ events }: { events: IEvent[] }) => (
- <box vertical className="day">
- <label className="date" xalign={0} label={getDateHeader(events)} />
- <box vertical className="events">
- {events.map(Event)}
- </box>
- </box>
-);
-
-const List = () => (
- <box vertical valign={Gtk.Align.START}>
- {bind(Calendar.get_default(), "upcoming").as(u =>
- Object.values(u)
- .sort((a, b) => a[0].startDate.compare(b[0].startDate))
- .map(e => <Day events={e} />)
- )}
- </box>
-);
-
-const NoEvents = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="calendar_month" />
- <label label="No upcoming events" />
- </box>
- </box>
-);
-
-export default () => (
- <box vertical className="upcoming">
- <box className="header-bar">
- <label
- label={bind(Calendar.get_default(), "numUpcoming").as(n => `${n} upcoming event${n === 1 ? "" : "s"}`)}
- />
- <box hexpand />
- <button
- className={bind(Calendar.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(Calendar.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => Calendar.get_default().updateCalendars().catch(console.error)}
- label={bind(Calendar.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(Calendar.get_default(), "numUpcoming").as(n => (n > 0 ? "list" : "empty"))}
- >
- <NoEvents />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/modules/updates.tsx b/src/modules/sidebar/modules/updates.tsx
deleted file mode 100644
index e58d848..0000000
--- a/src/modules/sidebar/modules/updates.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import Palette from "@/services/palette";
-import Updates, { Repo as IRepo, Update as IUpdate } from "@/services/updates";
-import { MenuItem, setupCustomTooltip } from "@/utils/widgets";
-import { bind, execAsync, GLib, Variable } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-
-const constructItem = (label: string, exec: string, quiet = true) =>
- new MenuItem({ label, onActivate: () => execAsync(exec).catch(e => !quiet && console.error(e)) });
-
-const Update = (update: IUpdate) => {
- const menu = new Gtk.Menu();
- menu.append(constructItem("Open info in browser", `app2unit -O '${update.url}'`, false));
- menu.append(constructItem("Open info in terminal", `app2unit -- foot -H -- pacman -Qi ${update.name}`));
- menu.append(new Gtk.SeparatorMenuItem({ visible: true }));
- menu.append(constructItem("Reinstall", `app2unit -- foot -H -- yay -S ${update.name}`));
- menu.append(constructItem("Remove with dependencies", `app2unit -- foot -H -- yay -Rns ${update.name}`));
-
- return (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.SECONDARY && menu.popup_at_pointer(null)}
- onDestroy={() => menu.destroy()}
- >
- <label
- truncate
- useMarkup
- xalign={0}
- label={bind(Palette.get_default(), "colours").as(
- c =>
- `${update.name} <span foreground="${c.teal}">(${update.version.old} -> ${
- update.version.new
- })</span>\n <span foreground="${c.subtext0}">${GLib.markup_escape_text(
- update.description,
- update.description.length
- )}</span>`
- )}
- setup={self => setupCustomTooltip(self, `${update.name} • ${update.description}`)}
- />
- </button>
- );
-};
-
-const Repo = ({ repo }: { repo: IRepo }) => {
- const expanded = Variable(false);
-
- return (
- <box vertical className="repo">
- <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
- <box className="header">
- <label className="icon" label={repo.icon} />
- <label label={`${repo.name} (${repo.updates.length})`} />
- <box hexpand />
- <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <box vertical className="body">
- {repo.updates.map(Update)}
- </box>
- </revealer>
- </box>
- );
-};
-
-const List = () => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(Updates.get_default(), "updateData").as(d => d.repos.map(r => <Repo repo={r} />))}
- </box>
-);
-
-const NoUpdates = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="deployed_code_history" />
- <label label="All packages up to date!" />
- </box>
- </box>
-);
-
-export default () => (
- <box vertical className="updates">
- <box className="header-bar">
- <label
- label={bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"} available`)}
- />
- <box hexpand />
- <button
- className={bind(Updates.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(Updates.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => Updates.get_default().getUpdates()}
- label={bind(Updates.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(Updates.get_default(), "numUpdates").as(n => (n > 0 ? "list" : "empty"))}
- >
- <NoUpdates />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/packages.tsx b/src/modules/sidebar/packages.tsx
deleted file mode 100644
index 02b0702..0000000
--- a/src/modules/sidebar/packages.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import News from "./modules/news";
-import Updates from "./modules/updates";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="pane packages" name="packages">
- <Updates />
- <box className="separator" />
- <News monitor={monitor} />
- </box>
-);
diff --git a/src/modules/sidebar/time.tsx b/src/modules/sidebar/time.tsx
deleted file mode 100644
index 1f5ef99..0000000
--- a/src/modules/sidebar/time.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { bindCurrentTime } from "@/utils/system";
-import { Gtk } from "astal/gtk3";
-import Calendar from "./modules/calendar";
-import Upcoming from "./modules/upcoming";
-
-const TimeDate = () => (
- <box vertical className="time-date">
- <box halign={Gtk.Align.CENTER}>
- <label label={bindCurrentTime("%I:%M:%S")} />
- <label className="ampm" label={bindCurrentTime("%p", c => (c.get_hour() < 12 ? "AM" : "PM"))} />
- </box>
- <label className="date" label={bindCurrentTime("%A, %d %B")} />
- </box>
-);
-
-export default () => (
- <box vertical className="pane time" name="time">
- <TimeDate />
- <box className="separator" />
- <Upcoming />
- <box className="separator" />
- <Calendar />
- </box>
-);
diff --git a/src/services/apps.ts b/src/services/apps.ts
deleted file mode 100644
index 5396ac7..0000000
--- a/src/services/apps.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import AstalApps from "gi://AstalApps";
-
-export const Apps = new AstalApps.Apps();
diff --git a/src/services/calendar.ts b/src/services/calendar.ts
deleted file mode 100644
index d5e0329..0000000
--- a/src/services/calendar.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-import { pathToFileName } from "@/utils/strings";
-import { notify } from "@/utils/system";
-import {
- execAsync,
- GLib,
- GObject,
- property,
- readFileAsync,
- register,
- timeout,
- writeFileAsync,
- type AstalIO,
-} from "astal";
-import { calendar as config } from "config";
-import ical from "ical.js";
-
-export interface IEvent {
- calendar: string;
- event: ical.Event;
- startDate: ical.Time;
- endDate: ical.Time;
-}
-
-@register({ GTypeName: "Calendar" })
-export default class Calendar extends GObject.Object {
- static instance: Calendar;
- static get_default() {
- if (!this.instance) this.instance = new Calendar();
-
- return this.instance;
- }
-
- readonly #cacheDir = `${CACHE}/calendars`;
-
- #calCount: number = 1;
- #reminders: AstalIO.Time[] = [];
- #loading: boolean = false;
- #calendars: { [name: string]: ical.Component } = {};
- #upcoming: { [date: string]: IEvent[] } = {};
- #cachedEvents: { [date: string]: IEvent[] } = {};
- #cachedMonths: Set<string> = new Set();
-
- @property(Boolean)
- get loading() {
- return this.#loading;
- }
-
- @property(Object)
- get calendars() {
- return this.#calendars;
- }
-
- @property(Object)
- get upcoming() {
- return this.#upcoming;
- }
-
- @property(Number)
- get numUpcoming() {
- return Object.values(this.#upcoming).reduce((acc, e) => acc + e.length, 0);
- }
-
- getCalendarIndex(name: string) {
- return Object.keys(this.#calendars).indexOf(name) + 1;
- }
-
- getEventsForMonth(date: ical.Time) {
- const start = date.startOfMonth();
-
- if (this.#cachedMonths.has(start.toJSDate().toDateString())) return this.#cachedEvents;
-
- this.#cachedMonths.add(start.toJSDate().toDateString());
- const end = date.endOfMonth();
-
- const modDates = new Set<string>();
-
- for (const [name, cal] of Object.entries(this.#calendars)) {
- for (const e of cal.getAllSubcomponents()) {
- const event = new ical.Event(e);
-
- // Skip invalid events
- if (!event.startDate) continue;
-
- if (event.isRecurring()) {
- // Recurring events
- const iter = event.iterator();
- for (let next = iter.next(); next && next.compare(end) <= 0; next = iter.next())
- if (next.compare(start) >= 0) {
- const date = next.toJSDate().toDateString();
- if (!this.#cachedEvents.hasOwnProperty(date)) this.#cachedEvents[date] = [];
-
- const end = next.clone();
- end.addDuration(event.duration);
- this.#cachedEvents[date].push({ calendar: name, event, startDate: next, endDate: end });
- modDates.add(date);
- }
- } else if (event.startDate.compare(start) >= 0 && event.startDate.compare(end) <= 0) {
- const date = event.startDate.toJSDate().toDateString();
- if (!this.#cachedEvents.hasOwnProperty(date)) this.#cachedEvents[date] = [];
- this.#cachedEvents[date].push({
- calendar: name,
- event,
- startDate: event.startDate,
- endDate: event.endDate,
- });
- modDates.add(date);
- }
- }
- }
-
- for (const date of modDates) this.#cachedEvents[date].sort((a, b) => a.startDate.compare(b.startDate));
-
- return this.#cachedEvents;
- }
-
- getEventsForDay(date: ical.Time) {
- return this.getEventsForMonth(date)[date.toJSDate().toDateString()] ?? [];
- }
-
- async updateCalendars() {
- this.#loading = true;
- this.notify("loading");
-
- this.#calendars = {};
- this.#calCount = 1;
-
- const cals = await Promise.allSettled(config.webcals.get().map(c => execAsync(["curl", c])));
- for (let i = 0; i < cals.length; i++) {
- const cal = cals[i];
- const webcal = pathToFileName(config.webcals.get()[i]);
-
- let icalStr;
- if (cal.status === "fulfilled") {
- icalStr = cal.value;
- } else {
- console.error(`Failed to get calendar from ${config.webcals.get()[i]}:\n${cal.reason}`);
- if (GLib.file_test(`${this.#cacheDir}/${webcal}`, GLib.FileTest.EXISTS))
- icalStr = await readFileAsync(`${this.#cacheDir}/${webcal}`);
- }
-
- if (icalStr) {
- const comp = new ical.Component(ical.parse(icalStr));
- const name = (comp.getFirstPropertyValue("x-wr-calname") ?? `Calendar ${this.#calCount++}`) as string;
- this.#calendars[name] = comp;
- writeFileAsync(`${this.#cacheDir}/${webcal}`, icalStr).catch(console.error);
- }
- }
- this.#cachedEvents = {};
- this.#cachedMonths.clear();
-
- this.notify("calendars");
-
- this.updateUpcoming();
-
- this.#loading = false;
- this.notify("loading");
- }
-
- updateUpcoming() {
- this.#upcoming = {};
-
- for (let i = 0; i < config.upcomingDays.get(); i++) {
- const date = ical.Time.now().adjust(i, 0, 0, 0);
- const events = this.getEventsForDay(date);
- if (events.length > 0) this.#upcoming[date.toJSDate().toDateString()] = events;
- }
-
- this.notify("upcoming");
- this.notify("num-upcoming");
-
- this.setReminders();
- }
-
- #notifyEvent(event: IEvent) {
- const start = GLib.DateTime.new_from_unix_local(event.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(event.endDate.toUnixTime());
- const time = `${start.format(`%A, %-d %B`)} • Now — ${end.format("%-I:%M%P")}`;
- const locIfExists = event.event.location ? ` ${event.event.location}\n` : "";
- const descIfExists = event.event.description ? `󰒿 ${event.event.description}\n` : "";
-
- notify({
- summary: `󰨱 ${event.event.summary} 󰨱`,
- body: `${time}\n${locIfExists}${descIfExists}󰃭 ${event.calendar}`,
- }).catch(console.error);
- }
-
- #createReminder(event: IEvent) {
- const diff = event.startDate.toJSDate().getTime() - ical.Time.now().toJSDate().getTime();
- if (diff > 0) this.#reminders.push(timeout(diff, () => this.#notifyEvent(event)));
- }
-
- setReminders() {
- this.#reminders.forEach(r => r.cancel());
- this.#reminders = [];
-
- if (!config.notify.get()) return;
-
- for (const events of Object.values(this.#upcoming)) for (const event of events) this.#createReminder(event);
- }
-
- constructor() {
- super();
-
- GLib.mkdir_with_parents(this.#cacheDir, 0o755);
-
- const cals = config.webcals.get().map(async c => {
- const webcal = pathToFileName(c);
-
- if (GLib.file_test(`${this.#cacheDir}/${webcal}`, GLib.FileTest.EXISTS)) {
- const data = await readFileAsync(`${this.#cacheDir}/${webcal}`);
- const comp = new ical.Component(ical.parse(data));
- const name = (comp.getFirstPropertyValue("x-wr-calname") ?? `Calendar ${this.#calCount++}`) as string;
- this.#calendars[name] = comp;
- }
- });
- Promise.allSettled(cals).then(() => {
- this.#cachedEvents = {};
- this.#cachedMonths.clear();
- this.notify("calendars");
- this.updateUpcoming();
- });
-
- this.updateCalendars().catch(console.error);
- config.webcals.subscribe(() => this.updateCalendars().catch(console.error));
- config.upcomingDays.subscribe(() => this.updateUpcoming());
- config.notify.subscribe(() => this.setReminders());
- }
-}
diff --git a/src/services/cpu.ts b/src/services/cpu.ts
deleted file mode 100644
index 5f80d11..0000000
--- a/src/services/cpu.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { GObject, interval, property, register } from "astal";
-import { cpu as config } from "config";
-import GTop from "gi://GTop";
-
-@register({ GTypeName: "Cpu" })
-export default class Cpu extends GObject.Object {
- static instance: Cpu;
- static get_default() {
- if (!this.instance) this.instance = new Cpu();
-
- return this.instance;
- }
-
- #previous: GTop.glibtop_cpu = new GTop.glibtop_cpu();
- #usage: number = 0;
-
- @property(Number)
- get usage() {
- return this.#usage;
- }
-
- calculateUsage() {
- const current = new GTop.glibtop_cpu();
- GTop.glibtop_get_cpu(current);
-
- // Calculate the differences from the previous to current data
- const total = current.total - this.#previous.total;
- const idle = current.idle - this.#previous.idle;
-
- this.#previous = current;
-
- return total > 0 ? ((total - idle) / total) * 100 : 0;
- }
-
- update() {
- this.#usage = this.calculateUsage();
- this.notify("usage");
- }
-
- constructor() {
- super();
-
- let source = interval(config.interval.get(), () => this.update());
- config.interval.subscribe(i => {
- source.cancel();
- source = interval(i, () => this.update());
- });
- }
-}
diff --git a/src/services/gpu.ts b/src/services/gpu.ts
deleted file mode 100644
index 5ac2d8d..0000000
--- a/src/services/gpu.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { execAsync, Gio, GLib, GObject, interval, property, register } from "astal";
-import { gpu as config } from "config";
-
-@register({ GTypeName: "Gpu" })
-export default class Gpu extends GObject.Object {
- static instance: Gpu;
- static get_default() {
- if (!this.instance) this.instance = new Gpu();
-
- return this.instance;
- }
-
- readonly available: boolean = false;
- #usage: number = 0;
-
- @property(Number)
- get usage() {
- return this.#usage;
- }
-
- async calculateUsage() {
- const percs = (await execAsync("fish -c 'cat /sys/class/drm/card*/device/gpu_busy_percent'")).split("\n");
- return percs.reduce((a, b) => a + parseFloat(b), 0) / percs.length;
- }
-
- update() {
- this.calculateUsage()
- .then(usage => {
- this.#usage = usage;
- this.notify("usage");
- })
- .catch(console.error);
- }
-
- constructor() {
- super();
-
- let enumerator = null;
- try {
- enumerator = Gio.File.new_for_path("/sys/class/drm").enumerate_children(
- Gio.FILE_ATTRIBUTE_STANDARD_NAME,
- Gio.FileQueryInfoFlags.NONE,
- null
- );
- } catch {}
-
- let info: Gio.FileInfo | undefined | null;
- while ((info = enumerator?.next_file(null))) {
- if (GLib.file_test(`/sys/class/drm/${info.get_name()}/device/gpu_busy_percent`, GLib.FileTest.EXISTS)) {
- this.available = true;
- break;
- }
- }
-
- if (this.available) {
- let source = interval(config.interval.get(), () => this.update());
- config.interval.subscribe(i => {
- source.cancel();
- source = interval(i, () => this.update());
- });
- }
- }
-}
diff --git a/src/services/math.ts b/src/services/math.ts
deleted file mode 100644
index 0cddf1b..0000000
--- a/src/services/math.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import { GLib, GObject, property, readFile, register, writeFileAsync } from "astal";
-import { math as config } from "config";
-import { derivative, evaluate, rationalize, simplify } from "mathjs/number";
-
-export interface HistoryItem {
- equation: string;
- result: string;
- icon: string;
-}
-
-@register({ GTypeName: "Math" })
-export default class Math extends GObject.Object {
- static instance: Math;
- static get_default() {
- if (!this.instance) this.instance = new Math();
-
- return this.instance;
- }
-
- readonly #path = `${STATE}/math-history.json`;
- readonly #history: HistoryItem[] = [];
-
- #variables: Record<string, string> = {};
- #lastExpression: HistoryItem | null = null;
-
- @property(Object)
- get history() {
- return this.#history;
- }
-
- #save() {
- writeFileAsync(this.#path, JSON.stringify(this.#history)).catch(console.error);
- }
-
- /**
- * Commits the last evaluated expression to the history
- */
- commit() {
- if (!this.#lastExpression) return;
-
- // Try select first to prevent duplicates, if it fails, add it
- if (!this.select(this.#lastExpression)) {
- this.#history.unshift(this.#lastExpression);
- while (this.#history.length > config.maxHistory.get()) this.#history.pop();
- this.notify("history");
- this.#save();
- }
- this.#lastExpression = null;
- }
-
- /**
- * Moves an item in the history to the top
- * @param item The item to select
- * @returns If the item was successfully selected
- */
- select(item: HistoryItem) {
- const idx = this.#history.findIndex(i => i.equation === item.equation && i.result === item.result);
- if (idx >= 0) {
- this.#history.splice(idx, 1);
- this.#history.unshift(item);
- this.notify("history");
- this.#save();
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Clears the history and variables
- */
- clear() {
- if (this.#history.length > 0) {
- this.#history.length = 0;
- this.notify("history");
- this.#save();
- }
- this.#lastExpression = null;
- this.#variables = {};
- }
-
- /**
- * Evaluates an equation and adds it to the history
- * @param equation The equation to evaluate
- * @returns A {@link HistoryItem} representing the result of the equation
- */
- evaluate(equation: string): HistoryItem {
- if (equation.startsWith("clear"))
- return {
- equation: "Clear history",
- result: "Delete history and previously set variables",
- icon: "delete_forever",
- };
-
- let result: string, icon: string;
- try {
- if (equation.startsWith("help")) {
- equation = "Help";
- result =
- "This is a calculator powered by Math.js.\nAvailable functions:\n\thelp: show help\n\tclear: clear history\n\t<x> = <equation>: sets <x> to <equation>\n\tsimplify <equation>: simplifies <equation>\n\tderive <x> <equation>: derives <equation> with respect to <x>\n\tdd<x> <equation>: short form of derive\n\trationalize <equation>: rationalizes <equation>\n\t<equation>: evaluates <equation>\nSee the documentation for syntax and inbuilt functions.";
- icon = "help";
- } else if (equation.includes("=")) {
- const [left, right] = equation.split("=");
- try {
- this.#variables[left.trim()] = simplify(right, this.#variables).toString();
- } catch {
- this.#variables[left.trim()] = right.trim();
- }
- result = this.#variables[left.trim()];
- icon = "equal";
- } else if (equation.startsWith("simplify")) {
- result = simplify(equation.slice(8), this.#variables).toString();
- icon = "function";
- } else if (equation.startsWith("derive") || equation.startsWith("dd")) {
- const isShortForm = equation.startsWith("dd");
- const respectTo = isShortForm ? equation.split(" ")[0].slice(2) : equation.split(" ")[1];
- if (!respectTo) throw new Error(`Format: ${isShortForm ? "dd" : "derive "}<respect-to> <equation>`);
- result = derivative(equation.slice((isShortForm ? 2 : 7) + respectTo.length), respectTo).toString();
- icon = "function";
- } else if (equation.startsWith("rationalize")) {
- result = rationalize(equation.slice(11), this.#variables).toString();
- icon = "function";
- } else {
- result = evaluate(equation, this.#variables).toString();
- icon = "calculate";
- }
- } catch (e) {
- equation = "Invalid equation: " + equation;
- result = String(e);
- icon = "error";
- }
-
- return (this.#lastExpression = { equation, result, icon });
- }
-
- constructor() {
- super();
-
- // Load history
- if (GLib.file_test(this.#path, GLib.FileTest.EXISTS)) {
- try {
- this.#history = JSON.parse(readFile(this.#path));
- // Init eval to create variables and last expression
- for (const item of this.#history) this.evaluate(item.equation);
- } catch (e) {
- console.error("Math - Unable to load history", e);
- }
- }
-
- config.maxHistory.subscribe(n => {
- while (this.#history.length > n) this.#history.pop();
- });
- }
-}
diff --git a/src/services/memory.ts b/src/services/memory.ts
deleted file mode 100644
index b1231b9..0000000
--- a/src/services/memory.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { GObject, interval, property, readFileAsync, register } from "astal";
-import { memory as config } from "config";
-
-@register({ GTypeName: "Memory" })
-export default class Memory extends GObject.Object {
- static instance: Memory;
- static get_default() {
- if (!this.instance) this.instance = new Memory();
-
- return this.instance;
- }
-
- #total: number = 0;
- #free: number = 0;
- #used: number = 0;
- #usage: number = 0;
-
- @property(Number)
- get total() {
- return this.#total;
- }
-
- @property(Number)
- get free() {
- return this.#free;
- }
-
- @property(Number)
- get used() {
- return this.#used;
- }
-
- @property(Number)
- get usage() {
- return this.#usage;
- }
-
- async update() {
- const info = await readFileAsync("/proc/meminfo");
- this.#total = parseInt(info.match(/MemTotal:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
- this.#free = parseInt(info.match(/MemAvailable:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
-
- if (isNaN(this.#total)) this.#total = 0;
- if (isNaN(this.#free)) this.#free = 0;
-
- this.#used = this.#total - this.#free;
- this.#usage = this.#total > 0 ? (this.#used / this.#total) * 100 : 0;
-
- this.notify("total");
- this.notify("free");
- this.notify("used");
- this.notify("usage");
- }
-
- constructor() {
- super();
-
- let source = interval(config.interval.get(), () => this.update().catch(console.error));
- config.interval.subscribe(i => {
- source.cancel();
- source = interval(i, () => this.update().catch(console.error));
- });
- }
-}
diff --git a/src/services/monitors.ts b/src/services/monitors.ts
deleted file mode 100644
index 6ae7ecb..0000000
--- a/src/services/monitors.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { GObject, execAsync, property, register } from "astal";
-import AstalHyprland from "gi://AstalHyprland";
-
-@register({ GTypeName: "Monitor" })
-export class Monitor extends GObject.Object {
- readonly monitor: AstalHyprland.Monitor;
- readonly width: number;
- readonly height: number;
- readonly id: number;
- readonly serial: string;
- readonly name: string;
- readonly description: string;
-
- @property(AstalHyprland.Workspace)
- get activeWorkspace() {
- return this.monitor.activeWorkspace;
- }
-
- isDdc: boolean = false;
- busNum?: string;
-
- #brightness: number = 0;
-
- @property(Number)
- get brightness() {
- return this.#brightness;
- }
-
- set brightness(value) {
- value = Math.min(1, Math.max(0, value));
-
- this.#brightness = value;
- this.notify("brightness");
- execAsync(
- this.isDdc
- ? `ddcutil -b ${this.busNum} setvcp 10 ${Math.round(value * 100)}`
- : `brightnessctl set ${Math.floor(value * 100)}% -q`
- ).catch(console.error);
- }
-
- constructor(monitor: AstalHyprland.Monitor) {
- super();
-
- this.monitor = monitor;
- this.width = monitor.width;
- this.height = monitor.height;
- this.id = monitor.id;
- this.serial = monitor.serial;
- this.name = monitor.name;
- this.description = monitor.description;
-
- monitor.connect("notify::active-workspace", () => this.notify("active-workspace"));
-
- execAsync("ddcutil detect --brief")
- .then(out => {
- this.isDdc = out.split("\n\n").some(display => {
- if (!/^Display \d+/.test(display)) return false;
- const lines = display.split("\n").map(l => l.trimStart());
- if (lines.find(l => l.startsWith("Monitor:"))?.split(":")[3] !== monitor.serial) return false;
- this.busNum = lines.find(l => l.startsWith("I2C bus:"))?.split("/dev/i2c-")[1];
- return this.busNum !== undefined;
- });
- })
- .catch(() => (this.isDdc = false))
- .finally(async () => {
- if (this.isDdc) {
- const info = (await execAsync(`ddcutil -b ${this.busNum} getvcp 10 --brief`)).split(" ");
- this.#brightness = Number(info[3]) / Number(info[4]);
- } else
- this.#brightness =
- Number(await execAsync("brightnessctl get")) / Number(await execAsync("brightnessctl max"));
- });
- }
-}
-
-@register({ GTypeName: "Monitors" })
-export default class Monitors extends GObject.Object {
- static instance: Monitors;
- static get_default() {
- if (!this.instance) this.instance = new Monitors();
-
- return this.instance;
- }
-
- readonly #map: Map<number, Monitor> = new Map();
-
- @property(Object)
- get map() {
- return this.#map;
- }
-
- @property(Object)
- get list() {
- return Array.from(this.#map.values());
- }
-
- @property(Monitor)
- get active() {
- return this.#map.get(AstalHyprland.get_default().focusedMonitor.id)!;
- }
-
- #notify() {
- this.notify("map");
- this.notify("list");
- }
-
- forEach(fn: (monitor: Monitor) => void) {
- for (const monitor of this.#map.values()) fn(monitor);
- }
-
- constructor() {
- super();
-
- const hyprland = AstalHyprland.get_default();
-
- for (const monitor of hyprland.monitors) this.#map.set(monitor.id, new Monitor(monitor));
- if (this.#map.size > 0) this.#notify();
-
- hyprland.connect("monitor-added", (_, monitor) => {
- this.#map.set(monitor.id, new Monitor(monitor));
- this.#notify();
- });
- hyprland.connect("monitor-removed", (_, id) => this.#map.delete(id) && this.#notify());
-
- hyprland.connect("notify::focused-monitor", () => this.notify("active"));
- }
-}
diff --git a/src/services/news.ts b/src/services/news.ts
deleted file mode 100644
index 14c980c..0000000
--- a/src/services/news.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import { notify } from "@/utils/system";
-import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal";
-import { news as config } from "config";
-import { setConfig } from "config/funcs";
-
-export interface IArticle {
- title: string;
- link: string;
- keywords: string[] | null;
- creator: string[] | null;
- description: string | null;
- pubDate: string;
- source_name: string;
- source_priority: number;
- category: string[];
-}
-
-@register({ GTypeName: "News" })
-export default class News extends GObject.Object {
- static instance: News;
- static get_default() {
- if (!this.instance) this.instance = new News();
-
- return this.instance;
- }
-
- readonly #cachePath = `${CACHE}/news.json`;
- #notified = false;
-
- #loading: boolean = false;
- #articles: IArticle[] = [];
- #categories: { [category: string]: IArticle[] } = {};
-
- @property(Boolean)
- get loading() {
- return this.#loading;
- }
-
- @property(Object)
- get articles() {
- return this.#articles;
- }
-
- @property(Object)
- get categories() {
- return this.#categories;
- }
-
- async getNews() {
- if (!config.apiKey.get()) {
- if (!this.#notified) {
- notify({
- summary: "A newsdata.io API key is required",
- body: "You can get one by creating an account at https://newsdata.io",
- icon: "dialog-warning-symbolic",
- urgency: "critical",
- actions: {
- "Get API key": () => execAsync("app2unit -O -- https://newsdata.io").catch(console.error),
- Disable: () => setConfig("sidebar.modules.headlines.enabled", false),
- },
- });
- this.#notified = true;
- }
- return;
- }
-
- this.#loading = true;
- this.notify("loading");
-
- let countries = config.countries.get().join(",");
- const categories = config.categories.get().join(",");
- const languages = config.languages.get().join(",");
- const domains = config.domains.get().join(",");
- const excludeDomains = config.excludeDomains.get().join(",");
- const timezone = config.timezone.get();
-
- if (countries.includes("current")) {
- const out = JSON.parse(await execAsync("curl ipinfo.io")).country.toLowerCase();
- countries = countries.replace("current", out);
- }
-
- let args = "removeduplicate=1&prioritydomain=top";
- if (countries) args += `&country=${countries}`;
- if (categories) args += `&category=${categories}`;
- if (languages) args += `&language=${languages}`;
- if (domains) args += `&domain=${domains}`;
- if (excludeDomains) args += `&excludedomain=${excludeDomains}`;
- if (timezone) args += `&timezone=${timezone}`;
-
- const url = `https://newsdata.io/api/1/latest?apikey=${config.apiKey.get()}&${args}`;
- try {
- const res = JSON.parse(await execAsync(["curl", url]));
- if (res.status !== "success") throw new Error(`Failed to get news: ${res.results.message}`);
-
- this.#articles = [...res.results];
-
- let page = res.nextPage;
- for (let i = 1; i < config.pages.get(); i++) {
- const res = JSON.parse(await execAsync(["curl", `${url}&page=${page}`]));
- if (res.status !== "success") throw new Error(`Failed to get news: ${res.results.message}`);
- this.#articles.push(...res.results);
- page = res.nextPage;
- }
-
- writeFileAsync(this.#cachePath, JSON.stringify(this.#articles)).catch(console.error);
- } catch (e) {
- console.error(e);
-
- if (GLib.file_test(this.#cachePath, GLib.FileTest.EXISTS))
- this.#articles = JSON.parse(await readFileAsync(this.#cachePath));
- }
- this.notify("articles");
-
- this.updateCategories();
-
- this.#loading = false;
- this.notify("loading");
- }
-
- updateCategories() {
- this.#categories = {};
- for (const article of this.#articles) {
- for (const category of article.category) {
- if (!this.#categories.hasOwnProperty(category)) this.#categories[category] = [];
- this.#categories[category].push(article);
- }
- }
- this.notify("categories");
- }
-
- constructor() {
- super();
-
- if (GLib.file_test(this.#cachePath, GLib.FileTest.EXISTS))
- readFileAsync(this.#cachePath)
- .then(data => {
- this.#articles = JSON.parse(data);
- this.notify("articles");
- this.updateCategories();
- })
- .catch(console.error);
-
- this.getNews().catch(console.error);
- config.apiKey.subscribe(() => this.getNews().catch(console.error));
- config.countries.subscribe(() => this.getNews().catch(console.error));
- config.categories.subscribe(() => this.getNews().catch(console.error));
- config.languages.subscribe(() => this.getNews().catch(console.error));
- config.domains.subscribe(() => this.getNews().catch(console.error));
- config.excludeDomains.subscribe(() => this.getNews().catch(console.error));
- config.timezone.subscribe(() => this.getNews().catch(console.error));
- config.pages.subscribe(() => this.getNews().catch(console.error));
- }
-}
diff --git a/src/services/palette.ts b/src/services/palette.ts
deleted file mode 100644
index 952543f..0000000
--- a/src/services/palette.ts
+++ /dev/null
@@ -1,298 +0,0 @@
-import { execAsync, GLib, GObject, monitorFile, property, readFile, readFileAsync, register } from "astal";
-import Schemes from "./schemes";
-
-export type ColourMode = "light" | "dark";
-
-export type Hex = `#${string}`;
-
-export interface IPalette {
- rosewater: Hex;
- flamingo: Hex;
- pink: Hex;
- mauve: Hex;
- red: Hex;
- maroon: Hex;
- peach: Hex;
- yellow: Hex;
- green: Hex;
- teal: Hex;
- sky: Hex;
- sapphire: Hex;
- blue: Hex;
- lavender: Hex;
- text: Hex;
- subtext1: Hex;
- subtext0: Hex;
- overlay2: Hex;
- overlay1: Hex;
- overlay0: Hex;
- surface2: Hex;
- surface1: Hex;
- surface0: Hex;
- base: Hex;
- mantle: Hex;
- crust: Hex;
- primary: Hex;
- secondary: Hex;
- tertiary: Hex;
-}
-
-@register({ GTypeName: "Palette" })
-export default class Palette extends GObject.Object {
- static instance: Palette;
- static get_default() {
- if (!this.instance) this.instance = new Palette();
-
- return this.instance;
- }
-
- #mode: ColourMode;
- #scheme: string;
- #flavour?: string;
- #colours!: IPalette;
-
- @property(Boolean)
- get mode() {
- return this.#mode;
- }
-
- @property(String)
- get scheme() {
- return this.#scheme;
- }
-
- @property(String)
- get flavour() {
- return this.#flavour;
- }
-
- @property(Object)
- get colours() {
- return this.#colours;
- }
-
- @property(String)
- get rosewater() {
- return this.#colours.rosewater;
- }
-
- @property(String)
- get flamingo() {
- return this.#colours.flamingo;
- }
-
- @property(String)
- get pink() {
- return this.#colours.pink;
- }
-
- @property(String)
- get mauve() {
- return this.#colours.mauve;
- }
-
- @property(String)
- get red() {
- return this.#colours.red;
- }
-
- @property(String)
- get maroon() {
- return this.#colours.maroon;
- }
-
- @property(String)
- get peach() {
- return this.#colours.peach;
- }
-
- @property(String)
- get yellow() {
- return this.#colours.yellow;
- }
-
- @property(String)
- get green() {
- return this.#colours.green;
- }
-
- @property(String)
- get teal() {
- return this.#colours.teal;
- }
-
- @property(String)
- get sky() {
- return this.#colours.sky;
- }
-
- @property(String)
- get sapphire() {
- return this.#colours.sapphire;
- }
-
- @property(String)
- get blue() {
- return this.#colours.blue;
- }
-
- @property(String)
- get lavender() {
- return this.#colours.lavender;
- }
-
- @property(String)
- get text() {
- return this.#colours.text;
- }
-
- @property(String)
- get subtext1() {
- return this.#colours.subtext1;
- }
-
- @property(String)
- get subtext0() {
- return this.#colours.subtext0;
- }
-
- @property(String)
- get overlay2() {
- return this.#colours.overlay2;
- }
-
- @property(String)
- get overlay1() {
- return this.#colours.overlay1;
- }
-
- @property(String)
- get overlay0() {
- return this.#colours.overlay0;
- }
-
- @property(String)
- get surface2() {
- return this.#colours.surface2;
- }
-
- @property(String)
- get surface1() {
- return this.#colours.surface1;
- }
-
- @property(String)
- get surface0() {
- return this.#colours.surface0;
- }
-
- @property(String)
- get base() {
- return this.#colours.base;
- }
-
- @property(String)
- get mantle() {
- return this.#colours.mantle;
- }
-
- @property(String)
- get crust() {
- return this.#colours.crust;
- }
-
- @property(String)
- get primary() {
- return this.#colours.primary;
- }
-
- @property(String)
- get secondary() {
- return this.#colours.secondary;
- }
-
- @property(String)
- get tertiary() {
- return this.#colours.tertiary;
- }
-
- #notify() {
- this.notify("colours");
- this.notify("rosewater");
- this.notify("flamingo");
- this.notify("pink");
- this.notify("mauve");
- this.notify("red");
- this.notify("maroon");
- this.notify("peach");
- this.notify("yellow");
- this.notify("green");
- this.notify("teal");
- this.notify("sky");
- this.notify("sapphire");
- this.notify("blue");
- this.notify("lavender");
- this.notify("text");
- this.notify("subtext1");
- this.notify("subtext0");
- this.notify("overlay2");
- this.notify("overlay1");
- this.notify("overlay0");
- this.notify("surface2");
- this.notify("surface1");
- this.notify("surface0");
- this.notify("base");
- this.notify("mantle");
- this.notify("crust");
- this.notify("primary");
- this.notify("secondary");
- this.notify("tertiary");
- }
-
- update() {
- let schemeColours;
- if (GLib.file_test(`${STATE}/scheme/current.txt`, GLib.FileTest.EXISTS)) {
- const currentScheme = readFile(`${STATE}/scheme/current.txt`);
- schemeColours = currentScheme.split("\n").map(l => l.split(" "));
- } else
- schemeColours = readFile(`${SRC}/scss/scheme/_default.scss`)
- .split("\n")
- .map(l => {
- const [name, hex] = l.split(":");
- return [name.slice(1), hex.trim().slice(1, -1)];
- });
-
- this.#colours = schemeColours.reduce((acc, [name, hex]) => ({ ...acc, [name]: `#${hex}` }), {} as IPalette);
- this.#notify();
- }
-
- switchMode(mode: ColourMode) {
- execAsync(`caelestia scheme ${this.scheme} ${this.flavour ?? ""} ${mode}`).catch(console.error);
- }
-
- hasMode(mode: ColourMode) {
- const scheme = Schemes.get_default().map[this.scheme];
- if (scheme?.colours?.[mode]) return true;
- return scheme?.flavours?.[this.flavour ?? ""]?.colours?.[mode] !== undefined;
- }
-
- constructor() {
- super();
-
- this.#mode = readFile(`${STATE}/scheme/current-mode.txt`) === "light" ? "light" : "dark";
- monitorFile(`${STATE}/scheme/current-mode.txt`, async file => {
- this.#mode = (await readFileAsync(file)) === "light" ? "light" : "dark";
- this.notify("mode");
- });
-
- [this.#scheme, this.#flavour] = readFile(`${STATE}/scheme/current-name.txt`).split("-");
- monitorFile(`${STATE}/scheme/current-name.txt`, async file => {
- [this.#scheme, this.#flavour] = (await readFileAsync(file)).split("-");
- this.notify("scheme");
- this.notify("flavour");
- });
-
- this.update();
- monitorFile(`${STATE}/scheme/current.txt`, () => this.update());
- }
-}
diff --git a/src/services/players.ts b/src/services/players.ts
deleted file mode 100644
index aca7344..0000000
--- a/src/services/players.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { isRealPlayer } from "@/utils/mpris";
-import { GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal";
-import AstalMpris from "gi://AstalMpris";
-
-@register({ GTypeName: "Players" })
-export default class Players extends GObject.Object {
- static instance: Players;
- static get_default() {
- if (!this.instance) this.instance = new Players();
-
- return this.instance;
- }
-
- readonly #path = `${STATE}/players.txt`;
- readonly #players: AstalMpris.Player[] = [];
- readonly #subs = new Map<
- JSX.Element,
- { signals: string[]; callback: () => void; ids: number[]; player: AstalMpris.Player | null }
- >();
-
- @property(AstalMpris.Player)
- get lastPlayer(): AstalMpris.Player | null {
- return this.#players.length > 0 && this.#players[0].identity !== null ? this.#players[0] : null;
- }
-
- /**
- * List of real players.
- */
- @property(Object)
- get list() {
- return this.#players;
- }
-
- hookLastPlayer(widget: JSX.Element, signal: string, callback: () => void): this;
- hookLastPlayer(widget: JSX.Element, signals: string[], callback: () => void): this;
- hookLastPlayer(widget: JSX.Element, signals: string | string[], callback: () => void) {
- if (!Array.isArray(signals)) signals = [signals];
- // Add subscription
- if (this.lastPlayer)
- this.#subs.set(widget, {
- signals,
- callback,
- ids: signals.map(s => this.lastPlayer!.connect(s, callback)),
- player: this.lastPlayer,
- });
- else this.#subs.set(widget, { signals, callback, ids: [], player: null });
-
- // Remove subscription on widget destroyed
- widget.connect("destroy", () => {
- const sub = this.#subs.get(widget);
- if (sub?.player) sub.ids.forEach(id => sub.player!.disconnect(id));
- this.#subs.delete(widget);
- });
-
- // Initial run of callback
- callback();
-
- // For chaining
- return this;
- }
-
- makeCurrent(player: AstalMpris.Player) {
- const index = this.#players.findIndex(p => p.busName === player.busName);
- // Ignore if already current
- if (index === 0 || !isRealPlayer(player)) return;
- // Remove if present
- else if (index > 0) this.#players.splice(index, 1);
- // Connect signals if not already in list (i.e. new player)
- else this.#connectPlayerSignals(player);
-
- // Add to front
- this.#players.unshift(player);
- this.#updatePlayer();
- this.notify("list");
-
- // Save to file
- this.#save();
- }
-
- #updatePlayer() {
- this.notify("last-player");
-
- for (const sub of this.#subs.values()) {
- sub.callback();
- if (sub.player) sub.ids.forEach(id => sub.player!.disconnect(id));
- sub.ids = this.lastPlayer ? sub.signals.map(s => this.lastPlayer!.connect(s, sub.callback)) : [];
- sub.player = this.lastPlayer;
- }
- }
-
- #save() {
- writeFileAsync(this.#path, this.#players.map(p => p.busName).join("\n")).catch(console.error);
- }
-
- #connectPlayerSignals(player: AstalMpris.Player) {
- // Change order on attribute change
- for (const signal of [
- "notify::playback-status",
- "notify::shuffle-status",
- "notify::loop-status",
- "notify::volume",
- "notify::rate",
- ])
- player.connect(signal, () => this.makeCurrent(player));
- }
-
- constructor() {
- super();
-
- const mpris = AstalMpris.get_default();
-
- // Load players
- if (GLib.file_test(this.#path, GLib.FileTest.EXISTS)) {
- readFileAsync(this.#path).then(out => {
- for (const busName of out.split("\n").reverse()) {
- const player = mpris.get_players().find(p => p.busName === busName);
- if (player) this.makeCurrent(player);
- }
- // Add new players from in between sessions
- for (const player of mpris.get_players()) this.makeCurrent(player);
- });
- } else {
- const sortOrder = [
- AstalMpris.PlaybackStatus.PLAYING,
- AstalMpris.PlaybackStatus.PAUSED,
- AstalMpris.PlaybackStatus.STOPPED,
- ];
- const players = mpris
- .get_players()
- .sort((a, b) => sortOrder.indexOf(b.playbackStatus) - sortOrder.indexOf(a.playbackStatus));
- for (const player of players) this.makeCurrent(player);
- }
-
- // Add and connect signals when added
- mpris.connect("player-added", (_, player) => this.makeCurrent(player));
-
- // Remove when closed
- mpris.connect("player-closed", (_, player) => {
- const index = this.#players.indexOf(player);
- if (index >= 0) {
- this.#players.splice(index, 1);
- this.notify("list");
- if (index === 0) this.#updatePlayer();
- this.#save();
- }
- });
- }
-}
diff --git a/src/services/schemes.ts b/src/services/schemes.ts
deleted file mode 100644
index c85fa72..0000000
--- a/src/services/schemes.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { basename } from "@/utils/strings";
-import { monitorDirectory } from "@/utils/system";
-import { execAsync, Gio, GLib, GObject, property, readFileAsync, register } from "astal";
-import type { IPalette } from "./palette";
-
-export interface Colours {
- light?: IPalette;
- dark?: IPalette;
-}
-
-export interface Flavour {
- name: string;
- scheme: string;
- colours: Colours;
-}
-
-export interface Scheme {
- name: string;
- flavours?: { [k: string]: Flavour };
- colours?: Colours;
-}
-
-const DATA = `${GLib.get_user_data_dir()}/caelestia`;
-
-@register({ GTypeName: "Schemes" })
-export default class Schemes extends GObject.Object {
- static instance: Schemes;
- static get_default() {
- if (!this.instance) this.instance = new Schemes();
-
- return this.instance;
- }
-
- readonly #schemeDir: string = `${DATA}/scripts/data/schemes`;
-
- #map: { [k: string]: Scheme } = {};
-
- @property(Object)
- get map() {
- return this.#map;
- }
-
- async parseMode(path: string): Promise<IPalette | undefined> {
- const schemeColours = (await readFileAsync(path).catch(() => undefined))?.split("\n").map(l => l.split(" "));
- return schemeColours?.reduce((acc, [name, hex]) => ({ ...acc, [name]: `#${hex}` }), {} as IPalette);
- }
-
- async parseColours(path: string): Promise<Colours> {
- const light = await this.parseMode(`${path}/light.txt`);
- const dark = await this.parseMode(`${path}/dark.txt`);
- return { light, dark };
- }
-
- async parseFlavour(scheme: string, name: string): Promise<Flavour> {
- const path = `${this.#schemeDir}/${scheme}/${name}`;
- return { name, scheme, colours: await this.parseColours(path) };
- }
-
- async parseScheme(name: string): Promise<Scheme> {
- const path = `${this.#schemeDir}/${name}`;
-
- const flavours = await execAsync(`find ${path}/ -mindepth 1 -maxdepth 1 -type d`);
- if (flavours.trim())
- return {
- name,
- flavours: (
- await Promise.all(flavours.split("\n").map(f => this.parseFlavour(name, basename(f))))
- ).reduce((acc, f) => ({ ...acc, [f.name]: f }), {} as { [k: string]: Flavour }),
- };
-
- return { name, colours: await this.parseColours(path) };
- }
-
- async update() {
- const schemes = await execAsync(`find ${this.#schemeDir}/ -mindepth 1 -maxdepth 1 -type d`);
- (await Promise.all(schemes.split("\n").map(s => this.parseScheme(basename(s))))).forEach(
- s => (this.#map[s.name] = s)
- );
- this.notify("map");
- }
-
- async updateFile(file: Gio.File) {
- if (file.get_basename() !== "light.txt" && file.get_basename() !== "dark.txt") {
- await this.update();
- return;
- }
-
- const mode = file.get_basename()!.slice(0, -4) as "light" | "dark";
- const parent = file.get_parent()!;
- const parentParent = parent.get_parent()!;
-
- if (parentParent.get_basename() === "schemes")
- this.#map[parent.get_basename()!].colours![mode] = await this.parseMode(file.get_path()!);
- else
- this.#map[parentParent.get_basename()!].flavours![parent.get_basename()!].colours![mode] =
- await this.parseMode(file.get_path()!);
-
- this.notify("map");
- }
-
- constructor() {
- super();
-
- this.update().catch(console.error);
- monitorDirectory(this.#schemeDir, (_m, file, _f, type) => {
- if (type !== Gio.FileMonitorEvent.DELETED) this.updateFile(file).catch(console.error);
- });
- }
-}
diff --git a/src/services/storage.ts b/src/services/storage.ts
deleted file mode 100644
index 3f8992d..0000000
--- a/src/services/storage.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { GObject, interval, property, register } from "astal";
-import { storage as config } from "config";
-import GTop from "gi://GTop";
-
-@register({ GTypeName: "Storage" })
-export default class Storage extends GObject.Object {
- static instance: Storage;
- static get_default() {
- if (!this.instance) this.instance = new Storage();
-
- return this.instance;
- }
-
- #total: number = 0;
- #free: number = 0;
- #used: number = 0;
- #usage: number = 0;
-
- @property(Number)
- get total() {
- return this.#total;
- }
-
- @property(Number)
- get free() {
- return this.#free;
- }
-
- @property(Number)
- get used() {
- return this.#used;
- }
-
- @property(Number)
- get usage() {
- return this.#usage;
- }
-
- update() {
- const root = new GTop.glibtop_fsusage();
- GTop.glibtop_get_fsusage(root, "/");
- const home = new GTop.glibtop_fsusage();
- GTop.glibtop_get_fsusage(home, "/home");
-
- this.#total = root.blocks * root.block_size + home.blocks * home.block_size;
- this.#free = root.bavail * root.block_size + home.bavail * home.block_size;
- this.#used = this.#total - this.#free;
- this.#usage = this.#total > 0 ? (this.#used / this.#total) * 100 : 0;
-
- this.notify("total");
- this.notify("free");
- this.notify("used");
- this.notify("usage");
- }
-
- constructor() {
- super();
-
- let source = interval(config.interval.get(), () => this.update());
- config.interval.subscribe(i => {
- source.cancel();
- source = interval(i, () => this.update());
- });
- }
-}
diff --git a/src/services/updates.ts b/src/services/updates.ts
deleted file mode 100644
index 62a8f65..0000000
--- a/src/services/updates.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import { capitalize } from "@/utils/strings";
-import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal";
-import { updates as config } from "config";
-
-export interface Update {
- full: string;
- name: string;
- description: string;
- url: string;
- version: {
- old: string;
- new: string;
- };
-}
-
-export interface Repo {
- repo?: string[];
- updates: Update[];
- icon: string;
- name: string;
-}
-
-export interface Data {
- cached?: boolean;
- repos: Repo[];
- errors: string[];
- news: string;
-}
-
-@register({ GTypeName: "Updates" })
-export default class Updates extends GObject.Object {
- static instance: Updates;
- static get_default() {
- if (!this.instance) this.instance = new Updates();
-
- return this.instance;
- }
-
- readonly #cachePath = `${CACHE}/updates.json`;
-
- #timeout?: GLib.Source;
- #loading = false;
- #data: Data = { cached: true, repos: [], errors: [], news: "" };
-
- @property(Boolean)
- get loading() {
- return this.#loading;
- }
-
- @property(Object)
- get updateData() {
- return this.#data;
- }
-
- @property(Object)
- get list() {
- return this.#data.repos.map(r => r.updates).flat();
- }
-
- @property(Number)
- get numUpdates() {
- return this.#data.repos.reduce((acc, repo) => acc + repo.updates.length, 0);
- }
-
- @property(String)
- get news() {
- return this.#data.news;
- }
-
- async #updateFromCache() {
- this.#data = JSON.parse(await readFileAsync(this.#cachePath));
- this.notify("update-data");
- this.notify("list");
- this.notify("num-updates");
- this.notify("news");
- }
-
- async getRepo(repo: string) {
- return (await execAsync(`bash -c "comm -12 <(pacman -Qq | sort) <(pacman -Slq '${repo}' | sort)"`)).split("\n");
- }
-
- async constructUpdate(update: string) {
- const info = await execAsync(`pacman -Qi ${update.split(" ")[0]}`);
- return info.split("\n").reduce(
- (acc, line) => {
- let [key, value] = line.split(" : ");
- key = key.trim().toLowerCase();
- if (key === "name" || key === "description" || key === "url") acc[key] = value.trim();
- else if (key === "version") acc.version.old = value.trim();
- return acc;
- },
- { version: { new: update.split("->")[1].trim() } } as Update
- );
- }
-
- getRepoIcon(repo: string) {
- switch (repo) {
- case "core":
- return "hub";
- case "extra":
- return "add_circle";
- case "multilib":
- return "account_tree";
- default:
- return "deployed_code_update";
- }
- }
-
- getUpdates() {
- // Return if already getting updates
- if (this.#loading) return;
-
- this.#loading = true;
- this.notify("loading");
-
- // Get new updates
- Promise.allSettled([execAsync("checkupdates"), execAsync("yay -Qua"), execAsync("yay -Pw")])
- .then(async ([pacman, yay, news]) => {
- const data: Data = { repos: [], errors: [], news: news.status === "fulfilled" ? news.value : "" };
-
- // Pacman updates (checkupdates)
- if (pacman.status === "fulfilled") {
- const repos: Repo[] = await Promise.all(
- (await execAsync("pacman-conf -l")).split("\n").map(async r => ({
- repo: await this.getRepo(r),
- updates: [],
- icon: this.getRepoIcon(r),
- name: capitalize(r) + " repository",
- }))
- );
-
- for (const update of pacman.value.split("\n")) {
- const pkg = update.split(" ")[0];
- for (const repo of repos)
- if (repo.repo?.includes(pkg)) repo.updates.push(await this.constructUpdate(update));
- }
-
- for (const repo of repos) if (repo.updates.length > 0) data.repos.push(repo);
- }
-
- // AUR and devel updates (yay -Qua)
- if (yay.status === "fulfilled") {
- const aur: Repo = { updates: [], icon: "deployed_code_account", name: "AUR" };
-
- for (const update of yay.value.split("\n")) {
- if (/^\s*->/.test(update)) data.errors.push(update); // Error
- else aur.updates.push(await this.constructUpdate(update));
- }
-
- if (aur.updates.length > 0) data.repos.push(aur);
- }
-
- if (data.errors.length > 0 && data.repos.length === 0) {
- this.#updateFromCache().catch(console.error);
- } else {
- // Sort updates by name
- for (const repo of data.repos) repo.updates.sort((a, b) => a.name.localeCompare(b.name));
-
- // Cache and set
- writeFileAsync(this.#cachePath, JSON.stringify({ cached: true, ...data })).catch(console.error);
- this.#data = data;
- this.notify("update-data");
- this.notify("list");
- this.notify("num-updates");
- this.notify("news");
- }
-
- this.#loading = false;
- this.notify("loading");
-
- this.#timeout?.destroy();
- this.#timeout = setTimeout(() => this.getUpdates(), config.interval.get());
- })
- .catch(console.error);
- }
-
- constructor() {
- super();
-
- // Initial update from cache, if fail then write valid data to cache so future reads don't fail
- this.#updateFromCache().catch(() =>
- writeFileAsync(this.#cachePath, JSON.stringify(this.#data)).catch(console.error)
- );
- this.getUpdates();
-
- config.interval.subscribe(i => {
- this.#timeout?.destroy();
- this.#timeout = setTimeout(() => this.getUpdates(), i);
- });
- }
-}
diff --git a/src/services/wallpapers.ts b/src/services/wallpapers.ts
deleted file mode 100644
index b5447c2..0000000
--- a/src/services/wallpapers.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { monitorDirectory } from "@/utils/system";
-import Thumbnailer from "@/utils/thumbnailer";
-import { execAsync, GObject, property, register } from "astal";
-import { wallpapers as config } from "config";
-import Monitors from "./monitors";
-
-export interface IWallpaper {
- path: string;
- thumbnails: {
- compact: string;
- medium: string;
- large: string;
- };
-}
-
-export interface ICategory {
- path: string;
- wallpapers: IWallpaper[];
-}
-
-@register({ GTypeName: "Wallpapers" })
-export default class Wallpapers extends GObject.Object {
- static instance: Wallpapers;
- static get_default() {
- if (!this.instance) this.instance = new Wallpapers();
-
- return this.instance;
- }
-
- #list: IWallpaper[] = [];
- #categories: ICategory[] = [];
-
- @property(Object)
- get list() {
- return this.#list;
- }
-
- @property(Object)
- get categories() {
- return this.#categories;
- }
-
- async #listDir(path: { path: string; recursive: boolean; threshold: number }, type: "f" | "d") {
- const absPath = path.path.replace("~", HOME);
- const maxDepth = path.recursive ? "" : "-maxdepth 1";
- const files = await execAsync(`find ${absPath} ${maxDepth} -path '*/.*' -prune -o -type ${type} -print`);
-
- if (type === "f" && path.threshold > 0) {
- const data = (
- await execAsync([
- "fish",
- "-c",
- `identify -ping -format '%i %w %h\n' ${files.replaceAll("\n", " ")} ; true`,
- ])
- ).split("\n");
-
- return data
- .filter(l => l && this.#filterSize(l, path.threshold))
- .map(l => l.split(" ").slice(0, -2).join(" "))
- .join("\n");
- }
-
- return files;
- }
-
- #filterSize(line: string, threshold: number) {
- const [width, height] = line.split(" ").slice(-2).map(Number);
- const mWidth = Math.max(...Monitors.get_default().list.map(m => m.width));
- const mHeight = Math.max(...Monitors.get_default().list.map(m => m.height));
-
- return width >= mWidth * threshold && height >= mHeight * threshold;
- }
-
- async update() {
- const results = await Promise.allSettled(
- config.paths.get().map(async p => ({ path: p, files: await this.#listDir(p, "f") }))
- );
- const successes = results.filter(r => r.status === "fulfilled").map(r => r.value);
-
- if (!successes.length) {
- this.#list = [];
- this.notify("list");
- this.#categories = [];
- this.notify("categories");
- return;
- }
-
- const files = successes.map(r => r.files.replaceAll("\n", " ")).join(" ");
- const list = (await execAsync(["fish", "-c", `identify -ping -format '%i\n' ${files} ; true`])).split("\n");
-
- this.#list = await Promise.all(
- list.map(async p => ({
- path: p,
- thumbnails: {
- compact: await Thumbnailer.thumbnail(p, { width: 60, height: 60, exact: true }),
- medium: await Thumbnailer.thumbnail(p, { width: 400, height: 150, exact: true }),
- large: await Thumbnailer.thumbnail(p, { width: 400, height: 200, exact: true }),
- },
- }))
- );
- this.#list.sort((a, b) => a.path.localeCompare(b.path));
- this.notify("list");
-
- const categories = await Promise.all(successes.map(r => this.#listDir(r.path, "d")));
- this.#categories = categories
- .flatMap(c => c.split("\n"))
- .map(c => ({ path: c, wallpapers: this.#list.filter(w => w.path.startsWith(c)) }))
- .filter(c => c.wallpapers.length > 0)
- .sort((a, b) => a.path.localeCompare(b.path));
- this.notify("categories");
- }
-
- constructor() {
- super();
-
- this.update().catch(console.error);
-
- let monitors = config.paths
- .get()
- .map(p => monitorDirectory(p.path, () => this.update().catch(console.error), p.recursive));
- config.paths.subscribe(v => {
- this.update().catch(console.error);
- for (const m of monitors) m.cancel();
- monitors = v.map(p => monitorDirectory(p.path, () => this.update().catch(console.error), p.recursive));
- });
- }
-}
diff --git a/src/services/weather.ts b/src/services/weather.ts
deleted file mode 100644
index d51e7fc..0000000
--- a/src/services/weather.ts
+++ /dev/null
@@ -1,388 +0,0 @@
-import { weatherIcons } from "@/utils/icons";
-import { notify } from "@/utils/system";
-import {
- execAsync,
- GLib,
- GObject,
- interval,
- property,
- readFileAsync,
- register,
- writeFileAsync,
- type Time,
-} from "astal";
-import { weather as config } from "config";
-
-export interface WeatherCondition {
- text: string;
- icon: string;
- code: number;
-}
-
-interface _WeatherState {
- temp_c: number;
- temp_f: number;
- is_day: number;
- condition: WeatherCondition;
- wind_mph: number;
- wind_kph: number;
- wind_degree: number;
- wind_dir: "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW";
- pressure_mb: number;
- pressure_in: number;
- precip_mm: number;
- precip_in: number;
- humidity: number;
- cloud: number;
- feelslike_c: number;
- feelslike_f: number;
- windchill_c: number;
- windchill_f: number;
- heatindex_c: number;
- heatindex_f: number;
- dewpoint_c: number;
- dewpoint_f: number;
- vis_km: number;
- vis_miles: number;
- uv: number;
- gust_mph: number;
- gust_kph: number;
-}
-
-export interface WeatherCurrent extends _WeatherState {
- last_updated_epoch: number;
- last_updated: string;
-}
-
-export interface WeatherHour extends _WeatherState {
- time_epoch: number;
- time: string;
-}
-
-export interface WeatherDay {
- date: string;
- date_epoch: number;
- day: {
- maxtemp_c: number;
- maxtemp_f: number;
- mintemp_c: number;
- mintemp_f: number;
- avgtemp_c: number;
- avgtemp_f: number;
- maxwind_mph: number;
- maxwind_kph: number;
- totalprecip_mm: number;
- totalprecip_in: number;
- totalsnow_cm: number;
- avgvis_km: number;
- avgvis_miles: number;
- avghumidity: number;
- daily_will_it_rain: number;
- daily_chance_of_rain: number;
- daily_will_it_snow: number;
- daily_chance_of_snow: number;
- condition: WeatherCondition;
- uv: number;
- };
- astro: {
- sunrise: string;
- sunset: string;
- moonrise: string;
- moonset: string;
- moon_phase: string;
- moon_illumination: string;
- is_moon_up: number;
- is_sun_up: number;
- };
- hour: WeatherHour[];
-}
-
-export interface WeatherLocation {
- name: string;
- region: string;
- country: string;
- lat: number;
- lon: number;
- tz_id: string;
- localtime_epoch: number;
- localtime: string;
-}
-
-export interface WeatherData {
- current: WeatherCurrent;
- forecast: { forecastday: WeatherDay[] };
- location: WeatherLocation;
-}
-
-const DEFAULT_STATE: _WeatherState = {
- temp_c: 0,
- temp_f: 0,
- is_day: 0,
- condition: { text: "", icon: "", code: 0 },
- wind_mph: 0,
- wind_kph: 0,
- wind_degree: 0,
- wind_dir: "N",
- pressure_mb: 0,
- pressure_in: 0,
- precip_mm: 0,
- precip_in: 0,
- humidity: 0,
- cloud: 0,
- feelslike_c: 0,
- feelslike_f: 0,
- windchill_c: 0,
- windchill_f: 0,
- heatindex_c: 0,
- heatindex_f: 0,
- dewpoint_c: 0,
- dewpoint_f: 0,
- vis_km: 0,
- vis_miles: 0,
- uv: 0,
- gust_mph: 0,
- gust_kph: 0,
-};
-
-const DEFAULT: WeatherData = {
- current: {
- last_updated_epoch: 0,
- last_updated: "",
- ...DEFAULT_STATE,
- },
- forecast: {
- forecastday: [
- {
- date: "",
- date_epoch: 0,
- day: {
- maxtemp_c: 0,
- maxtemp_f: 0,
- mintemp_c: 0,
- mintemp_f: 0,
- avgtemp_c: 0,
- avgtemp_f: 0,
- maxwind_mph: 0,
- maxwind_kph: 0,
- totalprecip_mm: 0,
- totalprecip_in: 0,
- totalsnow_cm: 0,
- avgvis_km: 0,
- avgvis_miles: 0,
- avghumidity: 0,
- daily_will_it_rain: 0,
- daily_chance_of_rain: 0,
- daily_will_it_snow: 0,
- daily_chance_of_snow: 0,
- condition: { text: "", icon: "", code: 0 },
- uv: 0,
- },
- astro: {
- sunrise: "",
- sunset: "",
- moonrise: "",
- moonset: "",
- moon_phase: "",
- moon_illumination: "",
- is_moon_up: 0,
- is_sun_up: 0,
- },
- hour: Array.from({ length: 24 }, () => ({
- time_epoch: 0,
- time: "",
- ...DEFAULT_STATE,
- })),
- },
- ],
- },
- location: {
- name: "",
- region: "",
- country: "",
- lat: 0,
- lon: 0,
- tz_id: "",
- localtime_epoch: 0,
- localtime: "",
- },
-};
-
-@register({ GTypeName: "Weather" })
-export default class Weather extends GObject.Object {
- static instance: Weather;
- static get_default() {
- if (!this.instance) this.instance = new Weather();
-
- return this.instance;
- }
-
- readonly #cache: string = `${CACHE}/weather.json`;
- #notified = false;
-
- #data: WeatherData = DEFAULT;
-
- #interval: Time | null = null;
-
- @property(Object)
- get raw() {
- return this.#data;
- }
-
- @property(Object)
- get current() {
- return this.#data.current;
- }
-
- @property(Object)
- get forecast() {
- return this.#data.forecast.forecastday[0].hour;
- }
-
- @property(Object)
- get location() {
- return this.#data.location;
- }
-
- @property(String)
- get condition() {
- return this.#data.current.condition.text;
- }
-
- @property(String)
- get temperature() {
- return this.getTemp(this.#data.current);
- }
-
- @property(String)
- get wind() {
- return `${Math.round(this.#data.current[`wind_${config.imperial.get() ? "m" : "k"}ph`])} ${
- config.imperial.get() ? "m" : "k"
- }ph`;
- }
-
- @property(String)
- get rainChance() {
- return this.#data.forecast.forecastday[0].day.daily_chance_of_rain + "%";
- }
-
- @property(String)
- get icon() {
- return this.getIcon(this.#data.current.condition.text);
- }
-
- @property(String)
- get tempIcon() {
- return this.getTempIcon(this.#data.current.temp_c);
- }
-
- @property(String)
- get tempColour() {
- return this.getTempDesc(this.#data.current.temp_c);
- }
-
- getIcon(status: string) {
- let query = status.trim().toLowerCase().replaceAll(" ", "_");
- if (!this.#data.current.is_day && query + "_night" in weatherIcons) query += "_night";
- return weatherIcons[query] ?? weatherIcons.warning;
- }
-
- getTemp(data: _WeatherState) {
- return `${Math.round(data[`temp_${config.imperial.get() ? "f" : "c"}`])}°${config.imperial.get() ? "F" : "C"}`;
- }
-
- getTempIcon(temp: number) {
- if (temp >= 40) return "";
- if (temp >= 30) return "";
- if (temp >= 20) return "";
- if (temp >= 10) return "";
- return "";
- }
-
- getTempDesc(temp: number) {
- if (temp >= 40) return "burning";
- if (temp >= 30) return "hot";
- if (temp >= 20) return "normal";
- if (temp >= 10) return "cold";
- return "freezing";
- }
-
- #notify() {
- this.notify("raw");
- this.notify("current");
- this.notify("forecast");
- this.notify("location");
- this.notify("condition");
- this.notify("temperature");
- this.notify("wind");
- this.notify("rain-chance");
- this.notify("icon");
- this.notify("temp-icon");
- this.notify("temp-colour");
- }
-
- async getWeather() {
- const location = config.location || JSON.parse(await execAsync("curl ipinfo.io")).city;
- const opts = `key=${config.apiKey.get()}&q=${location}&days=1&aqi=no&alerts=no`;
- const url = `https://api.weatherapi.com/v1/forecast.json?${opts}`;
- return JSON.parse(await execAsync(["curl", url]));
- }
-
- async updateWeather() {
- if (!config.apiKey.get()) {
- if (!this.#notified) {
- notify({
- summary: "Weather API key required",
- body: `A weather API key is required to get weather data. Get one from https://www.weatherapi.com.`,
- icon: "dialog-warning-symbolic",
- urgency: "critical",
- actions: {
- "Get API key": () => execAsync(`app2unit -O 'https://www.weatherapi.com'`).catch(print),
- },
- });
- this.#notified = true;
- }
- return;
- }
-
- if (GLib.file_test(this.#cache, GLib.FileTest.EXISTS)) {
- const cache = await readFileAsync(this.#cache);
- const cache_data: WeatherData = JSON.parse(cache);
- if (cache_data.location.localtime_epoch * 1000 + config.interval.get() > Date.now()) {
- if (JSON.stringify(this.#data) !== cache) {
- this.#data = cache_data;
- this.#notify();
- }
- return;
- }
- }
-
- try {
- const data = await this.getWeather();
- this.#data = data;
- writeFileAsync(this.#cache, JSON.stringify(data)).catch(console.error); // Catch here so it doesn't propagate
- } catch (e) {
- console.error("Error getting weather:", e);
- this.#data = DEFAULT;
- }
- this.#notify();
- }
-
- constructor() {
- super();
-
- this.updateWeather().catch(console.error);
- this.#interval = interval(config.interval.get(), () => this.updateWeather().catch(console.error));
-
- config.apiKey.subscribe(() => this.updateWeather());
-
- config.interval.subscribe(i => {
- this.#interval?.cancel();
- this.#interval = interval(i, () => this.updateWeather().catch(console.error));
- });
-
- config.imperial.subscribe(() => {
- this.notify("temperature");
- this.notify("wind");
- });
- }
-}
diff --git a/src/utils/icons.ts b/src/utils/icons.ts
deleted file mode 100644
index f164692..0000000
--- a/src/utils/icons.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import { Apps } from "@/services/apps";
-import { Gio } from "astal";
-import type AstalApps from "gi://AstalApps";
-
-export const osIcons: Record<string, string> = {
- almalinux: "",
- alpine: "",
- arch: "",
- archcraft: "",
- arcolinux: "",
- artix: "",
- centos: "",
- debian: "",
- devuan: "",
- elementary: "",
- endeavouros: "",
- fedora: "",
- freebsd: "",
- garuda: "",
- gentoo: "",
- hyperbola: "",
- kali: "",
- linuxmint: "󰣭",
- mageia: "",
- openmandriva: "",
- manjaro: "",
- neon: "",
- nixos: "",
- opensuse: "",
- suse: "",
- sles: "",
- sles_sap: "",
- "opensuse-tumbleweed": "",
- parrot: "",
- pop: "",
- raspbian: "",
- rhel: "",
- rocky: "",
- slackware: "",
- solus: "",
- steamos: "",
- tails: "",
- trisquel: "",
- ubuntu: "",
- vanilla: "",
- void: "",
- zorin: "",
-};
-
-export const weatherIcons: Record<string, string> = {
- warning: "󰼯",
- sunny: "󰖙",
- clear: "󰖔",
- partly_cloudy: "󰖕",
- partly_cloudy_night: "󰼱",
- cloudy: "󰖐",
- overcast: "󰖕",
- mist: "󰖑",
- patchy_rain_nearby: "󰼳",
- patchy_rain_possible: "󰼳",
- patchy_snow_possible: "󰼴",
- patchy_sleet_possible: "󰙿",
- patchy_freezing_drizzle_possible: "󰙿",
- thundery_outbreaks_possible: "󰙾",
- blowing_snow: "󰼶",
- blizzard: "󰼶",
- fog: "󰖑",
- freezing_fog: "󰖑",
- patchy_light_drizzle: "󰼳",
- light_drizzle: "󰼳",
- freezing_drizzle: "󰙿",
- heavy_freezing_drizzle: "󰙿",
- patchy_light_rain: "󰼳",
- light_rain: "󰼳",
- moderate_rain_at_times: "󰖗",
- moderate_rain: "󰼳",
- heavy_rain_at_times: "󰖖",
- heavy_rain: "󰖖",
- light_freezing_rain: "󰙿",
- moderate_or_heavy_freezing_rain: "󰙿",
- light_sleet: "󰙿",
- moderate_or_heavy_sleet: "󰙿",
- patchy_light_snow: "󰼴",
- light_snow: "󰼴",
- patchy_moderate_snow: "󰼴",
- moderate_snow: "󰼶",
- patchy_heavy_snow: "󰼶",
- heavy_snow: "󰼶",
- ice_pellets: "󰖒",
- light_rain_shower: "󰖖",
- moderate_or_heavy_rain_shower: "󰖖",
- torrential_rain_shower: "󰖖",
- light_sleet_showers: "󰼵",
- moderate_or_heavy_sleet_showers: "󰼵",
- light_snow_showers: "󰼵",
- moderate_or_heavy_snow_showers: "󰼵",
- light_showers_of_ice_pellets: "󰖒",
- moderate_or_heavy_showers_of_ice_pellets: "󰖒",
- patchy_light_rain_with_thunder: "󰙾",
- moderate_or_heavy_rain_with_thunder: "󰙾",
- moderate_or_heavy_rain_in_area_with_thunder: "󰙾",
- patchy_light_snow_with_thunder: "󰼶",
- moderate_or_heavy_snow_with_thunder: "󰼶",
-};
-
-export const desktopEntrySubs: Record<string, string> = {
- Firefox: "firefox",
-};
-
-const categoryIcons: Record<string, string> = {
- WebBrowser: "web",
- Printing: "print",
- Security: "security",
- Network: "chat",
- Archiving: "archive",
- Compression: "archive",
- Development: "code",
- IDE: "code",
- TextEditor: "edit_note",
- Audio: "music_note",
- Music: "music_note",
- Player: "music_note",
- Recorder: "mic",
- Game: "sports_esports",
- FileTools: "files",
- FileManager: "files",
- Filesystem: "files",
- FileTransfer: "files",
- Settings: "settings",
- DesktopSettings: "settings",
- HardwareSettings: "settings",
- TerminalEmulator: "terminal",
- ConsoleOnly: "terminal",
- Utility: "build",
- Monitor: "monitor_heart",
- Midi: "graphic_eq",
- Mixer: "graphic_eq",
- AudioVideoEditing: "video_settings",
- AudioVideo: "music_video",
- Video: "videocam",
- Building: "construction",
- Graphics: "photo_library",
- "2DGraphics": "photo_library",
- RasterGraphics: "photo_library",
- TV: "tv",
- System: "host",
-};
-
-export const getAppCategoryIcon = (nameOrApp: string | AstalApps.Application) => {
- const categories =
- typeof nameOrApp === "string"
- ? Gio.DesktopAppInfo.new(`${nameOrApp}.desktop`)?.get_categories()?.split(";") ??
- Apps.fuzzy_query(nameOrApp)[0]?.categories
- : nameOrApp.categories;
- if (categories)
- for (const [key, value] of Object.entries(categoryIcons)) if (categories.includes(key)) return value;
- return "terminal";
-};
diff --git a/src/utils/mpris.ts b/src/utils/mpris.ts
deleted file mode 100644
index e0cc111..0000000
--- a/src/utils/mpris.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { GLib } from "astal";
-import AstalMpris from "gi://AstalMpris";
-
-const hasPlasmaIntegration = GLib.find_program_in_path("plasma-browser-integration-host") !== null;
-
-export const isRealPlayer = (player?: AstalMpris.Player) =>
- player !== undefined &&
- // Player closed
- player.identity !== null &&
- // Remove unecessary native buses from browsers if there's plasma integration
- !(hasPlasmaIntegration && player.busName.startsWith("org.mpris.MediaPlayer2.firefox")) &&
- !(hasPlasmaIntegration && player.busName.startsWith("org.mpris.MediaPlayer2.chromium")) &&
- // playerctld just copies other buses and we don't need duplicates
- !player.busName.startsWith("org.mpris.MediaPlayer2.playerctld") &&
- // Non-instance mpd bus
- !(player.busName.endsWith(".mpd") && !player.busName.endsWith("MediaPlayer2.mpd"));
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
deleted file mode 100644
index 1edad67..0000000
--- a/src/utils/strings.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export const basename = (path: string, stripExt = true) => {
- const lastSlash = path.lastIndexOf("/");
- const lastDot = path.lastIndexOf(".");
- return path.slice(lastSlash + 1, stripExt && lastDot > lastSlash ? lastDot : undefined);
-};
-
-export const pathToFileName = (path: string, ext?: string) => {
- const start = /[a-z]+:\/\//.test(path) ? 0 : path.indexOf("/") + 1;
- const dir = path.slice(start, path.lastIndexOf("/")).replaceAll("/", "-");
- return `${dir}-${basename(path, ext !== undefined)}${ext ? `.${ext}` : ""}`;
-};
-
-export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
-
-export const lengthStr = (length: number) =>
- `${Math.floor(length / 60)}:${Math.floor(length % 60)
- .toString()
- .padStart(2, "0")}`;
diff --git a/src/utils/system.ts b/src/utils/system.ts
deleted file mode 100644
index 3a9caa6..0000000
--- a/src/utils/system.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { bind, execAsync, Gio, GLib, Variable, type Binding } from "astal";
-import type AstalApps from "gi://AstalApps";
-import { osIcons } from "./icons";
-
-/**
- * See https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html
- * @param exec The exec field in a desktop file
- */
-const execToCmd = (app: AstalApps.Application) => {
- let exec = app.executable.replace(/%[fFuUdDnNvm]/g, ""); // Remove useless field codes
- exec = exec.replace(/%i/g, app.iconName ? `--icon ${app.iconName}` : ""); // Replace %i app icon
- exec = exec.replace(/%c/g, app.name); // Replace %c with app name
- exec = exec.replace(/%k/g, (app.app as Gio.DesktopAppInfo).get_filename() ?? ""); // Replace %k with desktop file path
- return exec;
-};
-
-export const launch = (app: AstalApps.Application) => {
- let now = Date.now();
- execAsync(["app2unit", "--", app.entry]).catch(() => {
- // Try manual exec if launch fails (exits with error within 1 second)
- if (Date.now() - now < 1000) {
- now = Date.now();
- execAsync(["app2unit", "--", execToCmd(app)]).catch(() => {
- // Fallback to regular launch
- if (Date.now() - now < 1000) {
- app.frequency--; // Decrement frequency cause launch also increments it
- app.launch();
- }
- });
- }
- });
- app.frequency++;
-};
-
-export const notify = (props: {
- summary: string;
- body?: string;
- icon?: string;
- urgency?: "low" | "normal" | "critical";
- transient?: boolean;
- actions?: Record<string, () => void>;
-}) =>
- execAsync([
- "notify-send",
- "-a",
- "caelestia-shell",
- ...(props.icon ? ["-i", props.icon] : []),
- ...(props.urgency ? ["-u", props.urgency] : []),
- ...(props.transient ? ["-e"] : []),
- ...Object.keys(props.actions ?? {}).flatMap((k, i) => ["-A", `${i}=${k}`]),
- props.summary,
- ...(props.body ? [props.body] : []),
- ])
- .then(action => props.actions && Object.values(props.actions)[parseInt(action, 10)]?.())
- .catch(console.error);
-
-export const osId = GLib.get_os_info("ID") ?? "unknown";
-export const osIdLike = GLib.get_os_info("ID_LIKE");
-export const osIcon = (() => {
- if (osIcons.hasOwnProperty(osId)) return osIcons[osId];
- if (osIdLike) for (const id of osIdLike.split(" ")) if (osIcons.hasOwnProperty(id)) return osIcons[id];
- return "";
-})();
-
-export const currentTime = Variable(GLib.DateTime.new_now_local()).poll(1000, () => GLib.DateTime.new_now_local());
-export const bindCurrentTime = (
- format: Binding<string> | string,
- fallback?: (time: GLib.DateTime) => string,
- self?: JSX.Element
-) => {
- const fmt = (c: GLib.DateTime, format: string) => c.format(format) ?? fallback?.(c) ?? new Date().toLocaleString();
- if (typeof format === "string") return bind(currentTime).as(c => fmt(c, format));
- if (!self) throw new Error("bindCurrentTime: self is required when format is a Binding");
- const time = Variable.derive([currentTime, format], (c, f) => fmt(c, f));
- self?.connect("destroy", () => time.drop());
- return bind(time);
-};
-
-const monitors = new Set();
-export const monitorDirectory = (
- path: string,
- callback: (
- source: Gio.FileMonitor,
- file: Gio.File,
- other_file: Gio.File | null,
- type: Gio.FileMonitorEvent
- ) => void,
- recursive: boolean = true
-) => {
- const file = Gio.file_new_for_path(path.replace("~", HOME));
- const monitor = file.monitor_directory(null, null);
- monitor.connect("changed", (m, f1, f2, t) => {
- if (t === Gio.FileMonitorEvent.CHANGES_DONE_HINT || t === Gio.FileMonitorEvent.DELETED) callback(m, f1, f2, t);
- });
-
- if (recursive) {
- const enumerator = file.enumerate_children("standard::*", null, null);
- let child;
- while ((child = enumerator.next_file(null)))
- if (child.get_file_type() === Gio.FileType.DIRECTORY) {
- const m = monitorDirectory(`${path}/${child.get_name()}`, callback, recursive);
- monitor.connect("notify::cancelled", () => m.cancel());
- }
- }
-
- // Keep ref to monitor so it doesn't get GC'd
- monitors.add(monitor);
- monitor.connect("notify::cancelled", () => monitor.cancelled && monitors.delete(monitor));
-
- return monitor;
-};
diff --git a/src/utils/thumbnailer.ts b/src/utils/thumbnailer.ts
deleted file mode 100644
index d23dab1..0000000
--- a/src/utils/thumbnailer.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { execAsync, GLib, type Variable } from "astal";
-import { thumbnailer as config } from "config";
-
-export interface ThumbOpts {
- width?: number;
- height?: number;
- exact?: boolean;
-}
-
-export default class Thumbnailer {
- static readonly thumbnailDir = `${CACHE}/thumbnails`;
-
- static readonly #running = new Set<string>();
-
- static getOpt<T extends keyof ThumbOpts>(opt: T, opts: ThumbOpts) {
- return opts[opt] ?? (config.defaults[opt] as Variable<NonNullable<ThumbOpts[T]>>).get();
- }
-
- static async getThumbPath(path: string, opts: ThumbOpts) {
- const hash = (await execAsync(`sha1sum ${path}`)).split(" ")[0];
- const size = `${this.getOpt("width", opts)}x${this.getOpt("height", opts)}`;
- const exact = this.getOpt("exact", opts) ? "-exact" : "";
- return `${this.thumbnailDir}/${hash}@${size}${exact}.png`;
- }
-
- static async shouldThumbnail(path: string, opts: ThumbOpts) {
- const [w, h] = (await execAsync(`identify -ping -format "%w %h" ${path}`)).split(" ").map(parseInt);
- return w > this.getOpt("width", opts) || h > this.getOpt("height", opts);
- }
-
- static async #thumbnail(path: string, opts: ThumbOpts, attempts: number): Promise<string> {
- const thumbPath = await this.getThumbPath(path, opts);
-
- try {
- const width = this.getOpt("width", opts);
- const height = this.getOpt("height", opts);
- const cropCmd = this.getOpt("exact", opts)
- ? `-background none -gravity center -extent ${width}x${height}`
- : "";
- await execAsync(`magick ${path} -thumbnail ${width}x${height}^ ${cropCmd} -unsharp 0x.5 ${thumbPath}`);
- } catch {
- if (attempts >= config.maxAttempts.get()) {
- console.error(`Failed to generate thumbnail for ${path}`);
- return path;
- }
-
- await new Promise(r => setTimeout(r, config.timeBetweenAttempts.get()));
- return this.#thumbnail(path, opts, attempts + 1);
- }
-
- return thumbPath;
- }
-
- static async thumbnail(path: string, opts: ThumbOpts = {}): Promise<string> {
- if (!(await this.shouldThumbnail(path, opts))) return path;
-
- let thumbPath = await this.getThumbPath(path, opts);
-
- // Wait for existing thumbnail for path to finish
- while (this.#running.has(path)) await new Promise(r => setTimeout(r, 100));
-
- // If no thumbnail, generate
- if (!GLib.file_test(thumbPath, GLib.FileTest.EXISTS)) {
- this.#running.add(path);
-
- thumbPath = await this.#thumbnail(path, opts, 0);
-
- this.#running.delete(path);
- }
-
- return thumbPath;
- }
-
- // Static class
- private constructor() {}
-
- static {
- GLib.mkdir_with_parents(this.thumbnailDir, 0o755);
- }
-}
diff --git a/src/utils/types.ts b/src/utils/types.ts
deleted file mode 100644
index d2c1943..0000000
--- a/src/utils/types.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { astalify } from "astal/gtk3";
-import type AstalHyprland from "gi://AstalHyprland";
-
-export type AstalWidget = InstanceType<ReturnType<typeof astalify>>;
-
-export type Address = `0x${string}`;
-
-export interface Client {
- address: Address;
- mapped: boolean;
- hidden: boolean;
- at: [number, number];
- size: [number, number];
- workspace: {
- id: number;
- name: string;
- };
- floating: boolean;
- pseudo: boolean;
- monitor: number;
- class: string;
- title: string;
- initialClass: string;
- initialTitle: string;
- pid: number;
- xwayland: boolean;
- pinned: boolean;
- fullscreen: AstalHyprland.Fullscreen;
- fullscreenClient: AstalHyprland.Fullscreen;
- grouped: Address[];
- tags: string[];
- swallowing: string;
- focusHistoryID: number;
- inhibitingIdle: boolean;
-}
diff --git a/src/utils/widgets.ts b/src/utils/widgets.ts
deleted file mode 100644
index bef79f2..0000000
--- a/src/utils/widgets.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { Binding, idle, register } from "astal";
-import { Astal, astalify, Gtk, Widget, type ConstructProps } from "astal/gtk3";
-import AstalHyprland from "gi://AstalHyprland";
-import type { AstalWidget } from "./types";
-
-export const setupCustomTooltip = (
- self: AstalWidget,
- text: string | Binding<string>,
- labelProps: Widget.LabelProps = {}
-) => {
- if (!text) return null;
-
- self.set_has_tooltip(true);
-
- const window = new Widget.Window({
- visible: false,
- namespace: "caelestia-tooltip",
- layer: Astal.Layer.OVERLAY,
- keymode: Astal.Keymode.NONE,
- exclusivity: Astal.Exclusivity.IGNORE,
- anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT,
- child: new Widget.Label({ ...labelProps, className: "tooltip", label: text }),
- });
- self.set_tooltip_window(window);
-
- if (text instanceof Binding) self.hook(text, (_, v) => !v && window.hide());
-
- const positionWindow = ({ x, y }: { x: number; y: number }) => {
- const { width: mWidth, height: mHeight } = AstalHyprland.get_default().get_focused_monitor();
- const { width: pWidth, height: pHeight } = window.get_preferred_size()[1]!;
- const cursorSize = Gtk.Settings.get_default()?.gtkCursorThemeSize ?? 0;
-
- let marginLeft = x - pWidth / 2;
- if (marginLeft < 0) marginLeft = 0;
- else if (marginLeft + pWidth > mWidth) marginLeft = mWidth - pWidth;
-
- let marginTop = y + cursorSize;
- if (marginTop < 0) marginTop = 0;
- else if (marginTop + pHeight > mHeight) marginTop = y - pHeight;
-
- window.marginLeft = marginLeft;
- window.marginTop = marginTop;
- };
-
- let lastPos = { x: 0, y: 0 };
-
- window.connect("size-allocate", () => positionWindow(lastPos));
- self.connect("query-tooltip", () => {
- if (text instanceof Binding && !text.get()) return false;
- if (window.visible) return true;
-
- const cPos = AstalHyprland.get_default().get_cursor_position();
- positionWindow(cPos);
- lastPos = cPos;
-
- return true;
- });
-
- self.connect("destroy", () => window.destroy());
-
- return window;
-};
-
-export const setupChildClickthrough = (self: AstalWidget) => {
- self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes());
- self.set_size_request(1, 1);
- idle(() => self.set_size_request(-1, -1));
-};
-
-@register()
-export class MenuItem extends astalify(Gtk.MenuItem) {
- constructor(props: ConstructProps<MenuItem, Gtk.MenuItem.ConstructorProps, { onActivate: [] }>) {
- super(props as any);
- }
-}
-
-@register()
-export class FlowBox extends astalify(Gtk.FlowBox) {
- constructor(props: ConstructProps<FlowBox, Gtk.FlowBox.ConstructorProps>) {
- super(props as any);
- }
-}
diff --git a/src/widgets/notification.tsx b/src/widgets/notification.tsx
deleted file mode 100644
index 0dfd368..0000000
--- a/src/widgets/notification.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import { desktopEntrySubs } from "@/utils/icons";
-import Thumbnailer from "@/utils/thumbnailer";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, GLib, register, timeout, Variable } from "astal";
-import { Astal, Gtk, Widget } from "astal/gtk3";
-import { notifpopups as config } from "config";
-import AstalNotifd from "gi://AstalNotifd";
-
-const urgencyToString = (urgency: AstalNotifd.Urgency) => {
- switch (urgency) {
- case AstalNotifd.Urgency.LOW:
- return "low";
- case AstalNotifd.Urgency.NORMAL:
- return "normal";
- case AstalNotifd.Urgency.CRITICAL:
- return "critical";
- }
-};
-
-const getTime = (time: number) => {
- const messageTime = GLib.DateTime.new_from_unix_local(time);
- const now = GLib.DateTime.new_now_local();
- const todayDay = now.get_day_of_year();
-
- if (config.agoTime.get()) {
- const diff = now.difference(messageTime) / 1e6;
- if (diff < 60) return "Now";
- if (diff < 3600) {
- const d = Math.floor(diff / 60);
- return `${d} min${d === 1 ? "" : "s"} ago`;
- }
- if (diff < 86400) {
- const d = Math.floor(diff / 3600);
- return `${d} hour${d === 1 ? "" : "s"} ago`;
- }
- } else if (messageTime.get_day_of_year() === todayDay) {
- const aMinuteAgo = GLib.DateTime.new_now_local().add_seconds(-60);
- return aMinuteAgo !== null && messageTime.compare(aMinuteAgo) > 0 ? "Now" : messageTime.format("%H:%M")!;
- }
-
- if (messageTime.get_day_of_year() === todayDay - 1) return "Yesterday";
- return messageTime.format("%d/%m")!;
-};
-
-const AppIcon = ({ appIcon, desktopEntry }: { appIcon: string; desktopEntry: string }) => {
- // Try app icon
- let icon = Astal.Icon.lookup_icon(appIcon) && appIcon;
- // Try desktop entry
- if (!icon) {
- if (desktopEntrySubs.hasOwnProperty(desktopEntry)) icon = desktopEntrySubs[desktopEntry];
- else if (Astal.Icon.lookup_icon(desktopEntry)) icon = desktopEntry;
- }
- return icon ? <icon className="app-icon" icon={icon} /> : null;
-};
-
-const Image = ({ compact, icon }: { compact?: boolean; icon: string }) => {
- if (GLib.file_test(icon, GLib.FileTest.EXISTS))
- return (
- <box
- valign={Gtk.Align.START}
- className={`image ${compact ? "small" : ""}`}
- setup={self =>
- Thumbnailer.thumbnail(icon)
- .then(p => (self.css = `background-image: url("${p}");`))
- .catch(console.error)
- }
- />
- );
- if (Astal.Icon.lookup_icon(icon))
- return <icon valign={Gtk.Align.START} className={`image ${compact ? "small" : ""}`} icon={icon} />;
- return null;
-};
-
-@register()
-export default class Notification extends Widget.Box {
- readonly #revealer;
- #destroyed = false;
-
- constructor({
- notification,
- popup,
- compact = popup,
- }: {
- notification: AstalNotifd.Notification;
- popup?: boolean;
- compact?: boolean;
- }) {
- super({ className: "notification" });
-
- const time = Variable(getTime(notification.time)).poll(60000, () => getTime(notification.time));
- this.hook(config.agoTime, () => time.set(getTime(notification.time)));
-
- this.#revealer = (
- <revealer
- revealChild={popup}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- >
- <box className="wrapper">
- <box vertical className={`inner ${urgencyToString(notification.urgency)}`}>
- <box className="header">
- <AppIcon appIcon={notification.appIcon} desktopEntry={notification.appName} />
- <label className="app-name" label={notification.appName ?? "Unknown"} />
- <box hexpand />
- <label className="time" label={bind(time)} onDestroy={() => time.drop()} />
- </box>
- <box hexpand className="separator" />
- <box className="content">
- {notification.image && <Image compact={compact} icon={notification.image} />}
- <box vertical>
- <label className="summary" xalign={0} label={notification.summary} truncate />
- {notification.body && (
- <label
- className="body"
- xalign={0}
- label={compact ? notification.body.split("\n")[0] : notification.body}
- wrap
- lines={compact ? 1 : -1}
- truncate={compact}
- setup={self => compact && !popup && setupCustomTooltip(self, notification.body)}
- />
- )}
- </box>
- </box>
- {!popup && (
- <box className="actions">
- <button
- hexpand
- cursor="pointer"
- onClicked={() => notification.dismiss()}
- label="Close"
- />
- {notification.actions.map(a => (
- <button hexpand cursor="pointer" onClicked={() => notification.invoke(a.id)}>
- {notification.actionIcons ? <icon icon={a.label} /> : a.label}
- </button>
- ))}
- </box>
- )}
- </box>
- </box>
- </revealer>
- ) as Widget.Revealer;
- this.add(this.#revealer);
-
- // Init animation
- const width = this.get_preferred_width()[1];
- if (popup) this.css = `margin-left: ${width}px; margin-right: -${width}px;`;
- timeout(1, () => {
- this.#revealer.revealChild = true;
- this.css = `transition: 300ms cubic-bezier(0.05, 0.9, 0.1, 1.1); margin-left: 0; margin-right: 0;`;
- });
-
- // Close popup after timeout if transient or expire enabled in config
- if (popup && (config.expire.get() || notification.transient))
- timeout(
- notification.expireTimeout > 0
- ? notification.expireTimeout
- : notification.urgency === AstalNotifd.Urgency.CRITICAL
- ? 10000
- : 5000,
- () => this.destroyWithAnims()
- );
- }
-
- destroyWithAnims() {
- if (this.#destroyed) return;
- this.#destroyed = true;
-
- const animTime = 120;
- const animMargin = this.get_allocated_width();
- this.css = `transition: ${animTime}ms cubic-bezier(0.85, 0, 0.15, 1);
- margin-left: ${animMargin}px; margin-right: -${animMargin}px;`;
- timeout(animTime, () => {
- this.#revealer.revealChild = false;
- timeout(this.#revealer.transitionDuration, () => this.destroy());
- });
- }
-}
diff --git a/src/widgets/popupwindow.ts b/src/widgets/popupwindow.ts
deleted file mode 100644
index 5ffa061..0000000
--- a/src/widgets/popupwindow.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Binding, register } from "astal";
-import { App, Astal, Gdk, Widget } from "astal/gtk3";
-import { bar } from "config";
-import AstalHyprland from "gi://AstalHyprland";
-
-const extendProp = <T>(
- prop: T | Binding<T | undefined> | undefined,
- override: (prop: T | undefined) => T | undefined
-) => prop && (prop instanceof Binding ? prop.as(override) : override(prop));
-
-@register()
-export default class PopupWindow extends Widget.Window {
- constructor(props: Widget.WindowProps) {
- super({
- keymode: Astal.Keymode.ON_DEMAND,
- borderWidth: 20, // To allow shadow, cause if not it gets cut off
- ...props,
- visible: false,
- application: App,
- name: props.monitor ? extendProp(props.name, n => (n ? n + props.monitor : undefined)) : props.name,
- namespace: extendProp(props.name, n => `caelestia-${n}`),
- onKeyPressEvent: (self, event) => {
- // Close window on escape
- if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide();
-
- return props.onKeyPressEvent?.(self, event);
- },
- });
- }
-
- popup_at_widget(widget: JSX.Element, event: Gdk.Event | Astal.ClickEvent) {
- const { width, height } = widget.get_allocation();
- const { width: mWidth, height: mHeight } = AstalHyprland.get_default().get_focused_monitor();
- const pWidth = this.get_preferred_width()[1];
- const pHeight = this.get_preferred_height()[1];
- const [, x, y] = event instanceof Gdk.Event ? event.get_coords() : [null, event.x, event.y];
- const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position();
-
- let marginLeft = 0;
- let marginTop = 0;
- if (bar.vertical.get()) {
- marginLeft = cx + (width - x);
- marginTop = cy + ((height - pHeight) / 2 - y);
- if (marginTop < 0) marginTop = 0;
- else if (marginTop + pHeight > mHeight) marginTop = mHeight - pHeight;
- } else {
- marginLeft = cx + ((width - pWidth) / 2 - x);
- if (marginLeft < 0) marginLeft = 0;
- else if (marginLeft + pWidth > mWidth) marginLeft = mWidth - pWidth;
- marginTop = cy + (height - y);
- }
-
- this.anchor = Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT;
- this.exclusivity = Astal.Exclusivity.IGNORE;
- this.marginLeft = marginLeft;
- this.marginTop = marginTop;
-
- this.show();
- }
-
- popup_at_corner(corner: `${"top" | "bottom"} ${"left" | "right"}`) {
- let anchor = 0;
- if (corner.includes("top")) anchor |= Astal.WindowAnchor.TOP;
- else anchor |= Astal.WindowAnchor.BOTTOM;
- if (corner.includes("left")) anchor |= Astal.WindowAnchor.LEFT;
- else anchor |= Astal.WindowAnchor.RIGHT;
-
- this.anchor = anchor;
- this.exclusivity = Astal.Exclusivity.NORMAL;
- this.marginLeft = 0;
- this.marginTop = 0;
-
- this.show();
- }
-}
diff --git a/src/widgets/screencorner.tsx b/src/widgets/screencorner.tsx
deleted file mode 100644
index a55d782..0000000
--- a/src/widgets/screencorner.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { Binding } from "astal";
-import { Gtk, type Widget } from "astal/gtk3";
-import type cairo from "cairo";
-
-type Place = "topleft" | "topright" | "bottomleft" | "bottomright";
-
-export default ({ place, ...rest }: Widget.DrawingAreaProps & { place: Place | Binding<Place> }) => (
- <drawingarea
- {...rest}
- className="screen-corner"
- setup={self => {
- self.connect("realize", () => self.get_window()?.set_pass_through(true));
-
- const r = self.get_style_context().get_property("border-radius", Gtk.StateFlags.NORMAL) as number;
- self.set_size_request(r, r);
- self.connect("draw", (_, cr: cairo.Context) => {
- const c = self.get_style_context().get_background_color(Gtk.StateFlags.NORMAL);
- const r = self.get_style_context().get_property("border-radius", Gtk.StateFlags.NORMAL) as number;
- self.set_size_request(r, r);
-
- switch (typeof place === "string" ? place : place.get()) {
- case "topleft":
- cr.arc(r, r, r, Math.PI, (3 * Math.PI) / 2);
- cr.lineTo(0, 0);
- break;
-
- case "topright":
- cr.arc(0, r, r, (3 * Math.PI) / 2, 2 * Math.PI);
- cr.lineTo(r, 0);
- break;
-
- case "bottomleft":
- cr.arc(r, 0, r, Math.PI / 2, Math.PI);
- cr.lineTo(0, r);
- break;
-
- case "bottomright":
- cr.arc(0, 0, r, 0, Math.PI / 2);
- cr.lineTo(r, r);
- break;
- }
-
- cr.closePath();
- cr.setSourceRGBA(c.red, c.green, c.blue, c.alpha);
- cr.fill();
- });
- }}
- />
-);
diff --git a/src/widgets/slider.tsx b/src/widgets/slider.tsx
deleted file mode 100644
index 0a66609..0000000
--- a/src/widgets/slider.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { bind, type Binding } from "astal";
-import { Gdk, Gtk, type Widget } from "astal/gtk3";
-import type cairo from "cairo";
-
-export default ({
- value,
- onChange,
-}: {
- value: Binding<number>;
- onChange?: (self: Widget.DrawingArea, value: number) => void;
-}) => (
- <drawingarea
- hexpand
- valign={Gtk.Align.CENTER}
- className="slider"
- css={bind(value).as(v => `font-size: ${Math.min(1, Math.max(0, v))}px;`)}
- setup={self => {
- const halfPi = Math.PI / 2;
-
- const styleContext = self.get_style_context();
- self.set_size_request(-1, styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number);
-
- self.connect("draw", (_, cr: cairo.Context) => {
- const styleContext = self.get_style_context();
-
- const width = self.get_allocated_width();
- const height = styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number;
- self.set_size_request(-1, height);
-
- const progressValue = styleContext.get_property("font-size", Gtk.StateFlags.NORMAL) as number;
- let radius = styleContext.get_property("border-radius", Gtk.StateFlags.NORMAL) as number;
-
- const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
-
- // Background
- cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
- cr.arc(width - radius, radius, radius, -halfPi, 0); // Top right
- cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
- cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
- cr.fill();
-
- // Flatten when near 0
- radius = Math.min(radius, Math.min(width * progressValue, height) / 2);
-
- const progressPosition = width * progressValue - radius;
- const fg = styleContext.get_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
-
- // Foreground
- cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
- cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
- cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
- cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
- cr.fill();
- });
-
- self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
- self.connect("button-press-event", (_, event: Gdk.Event) =>
- onChange?.(self, event.get_coords()[1] / self.get_allocated_width())
- );
- }}
- />
-);