diff options
Diffstat (limited to 'src/utils')
| -rw-r--r-- | src/utils/icons.ts | 158 | ||||
| -rw-r--r-- | src/utils/mpris.ts | 16 | ||||
| -rw-r--r-- | src/utils/strings.ts | 18 | ||||
| -rw-r--r-- | src/utils/system.ts | 111 | ||||
| -rw-r--r-- | src/utils/thumbnailer.ts | 80 | ||||
| -rw-r--r-- | src/utils/types.ts | 35 | ||||
| -rw-r--r-- | src/utils/widgets.ts | 82 |
7 files changed, 0 insertions, 500 deletions
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); - } -} |