commit 4269d6b0bc849a5939aac62152501c4b75170ede Author: Franek Date: Sun Apr 20 13:02:58 2025 +0200 Initial commit diff --git a/ags/.gitignore b/ags/.gitignore new file mode 100644 index 0000000..f7587cb --- /dev/null +++ b/ags/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +@girs/ +styles/colors/index.scss \ No newline at end of file diff --git a/ags/app.ts b/ags/app.ts new file mode 100644 index 0000000..40c1804 --- /dev/null +++ b/ags/app.ts @@ -0,0 +1,67 @@ +import style from "./styles/style.scss" + +import { GLib, monitorFile, exec } from "astal"; +import { App, Gtk } from "astal/gtk4"; +import Hyprland from "gi://AstalHyprland"; + +import QuickSettings from "widget/quick_settings/quick_settings"; +import BluetoothWindow from "widget/quick_settings/bluetooth"; +import Launcher from "widget/launcher/launcher"; +import BatteryInfo from "@/widget/bar/battery_info"; +import Bar from "widget/bar/bar"; + +const hypr = Hyprland.get_default(); +const windows = new Map(); +const components = [ + Bar, + Launcher, + QuickSettings, + BluetoothWindow, + BatteryInfo +]; + +const setupBars = async (monitor_id: number) => { + const windows = await Promise.all( + components.map(item => Promise.resolve(item(monitor_id)) as Promise) + ); + + return windows; +}; + +const STYLES = `${GLib.get_user_config_dir()}/ags/styles`; +const monitorCSS = () => monitorFile( + STYLES + '/colors.scss', + () => { + exec(`sass ${STYLES}/style.scss /tmp/ags-style.css`); + App.apply_css('/tmp/ags-style.css', true); + } +); + +App.start({ + css: style, + async main() { + App.add_icons(`${GLib.get_user_data_dir()}/icons/Astal`); + monitorCSS(); + + const monitors = App.get_monitors(); + for (const monitor of monitors) { + const index = monitors.indexOf(monitor); + windows.set(index, await setupBars(index)); + + hypr.connect("monitor-added", async (_, monitor: Hyprland.Monitor) => { + if (!windows.has(monitor.id)) windows.set(monitor.id, await setupBars(monitor.id)) + }); + + hypr.connect("monitor-removed", (_, monitor_id: number) => { + const monitorWindows = windows.get(monitor_id) + if (monitorWindows) { + for (const monitorWindow of monitorWindows) { + monitorWindow.destroy(); + }; + + windows.delete(monitor_id); + }; + }); + } + }, +}) diff --git a/ags/env.d.ts b/ags/env.d.ts new file mode 100644 index 0000000..467c0a4 --- /dev/null +++ b/ags/env.d.ts @@ -0,0 +1,21 @@ +declare const SRC: string + +declare module "inline:*" { + const content: string + export default content +} + +declare module "*.scss" { + const content: string + export default content +} + +declare module "*.blp" { + const content: string + export default content +} + +declare module "*.css" { + const content: string + export default content +} diff --git a/ags/lib/icons.ts b/ags/lib/icons.ts new file mode 100644 index 0000000..3e30ea8 --- /dev/null +++ b/ags/lib/icons.ts @@ -0,0 +1,17 @@ +import { Binding } from "astal"; + +type Substitution = { charging?: string, idle?: string } + +export const getBatteryIcon = (percentage: number, charging: Binding) => { + const levels = Array.from({ length: 10 }, (_, i) => (i + 1) * 10); + const level = levels.find((level) => percentage <= level)!; + + const substitutions: Record = { + 100: { charging: "battery-level-100-charged-symbolic" } + }; + + return charging.as(c => c + ? substitutions[level]?.charging || `battery-level-${level}-charging-symbolic` + : substitutions[level]?.idle || `battery-level-${level}-symbolic` + ); +}; \ No newline at end of file diff --git a/ags/lib/utils.ts b/ags/lib/utils.ts new file mode 100644 index 0000000..1c58da2 --- /dev/null +++ b/ags/lib/utils.ts @@ -0,0 +1,24 @@ +import { Gtk, Gdk } from "astal/gtk4"; + +export type If = Condition extends true ? Then : Else; +export type Belongs = { + [K in keyof T]: T[K] extends U ? K : never; +}[keyof T]; + +export const hideWindow = (self: Gtk.Window, keyval: number) => { + const keys = ["quick_settings"].includes(self.name) + ? [Gdk.KEY_Escape] + : [ + Gdk.KEY_Escape, + Gdk.KEY_Super_L, + Gdk.KEY_Super_R + ]; + + if (keys.some(key => key === keyval)) self.hide(); +} + +export const openOnButton = (event: Gdk.ButtonEvent, keyval: number) => (action: () => void) => { + if (event.get_button() !== keyval) return; + + action(); +} \ No newline at end of file diff --git a/ags/lib/widgets/datetime.tsx b/ags/lib/widgets/datetime.tsx new file mode 100644 index 0000000..8ae205e --- /dev/null +++ b/ags/lib/widgets/datetime.tsx @@ -0,0 +1,24 @@ +import { Variable, GLib } from "astal"; +import { Widget } from "astal/gtk4"; + +type Props = { + format: string, + interval?: number +} & Widget.LabelProps; + +export default function DateTime({ format, interval, ...props }: Props) { + const shouldPoll = typeof interval === "number" && interval >= 1; + + const currentTime = () => { + const dateTime = GLib.DateTime.new_now_local(); + return dateTime.format(format)!; + } + + if (shouldPoll) { + const pollTime = new Variable(currentTime()).poll(interval || 1000, currentTime); + return