From ffed9ee873fdc877f0a696ce5a4a4bb2c8a7f432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Av=C3=A9?= Date: Wed, 9 Jul 2025 16:09:53 +0200 Subject: [PATCH] Ported to new ags version --- home/ags/files/Bar.tsx | 309 ++++++++++-------- home/ags/files/app.ts | 21 +- home/ags/files/env.d.ts | 16 +- .../ags/files/notifications/Notification.scss | 183 +++++------ home/ags/files/notifications/Notification.tsx | 199 +++++------ .../notifications/NotificationPopups.tsx | 150 ++++----- home/ags/files/package.json | 5 + home/ags/files/style.scss | 11 +- home/ags/files/tsconfig.json | 16 +- 9 files changed, 451 insertions(+), 459 deletions(-) create mode 100644 home/ags/files/package.json diff --git a/home/ags/files/Bar.tsx b/home/ags/files/Bar.tsx index bbdd9b0d..98c37932 100644 --- a/home/ags/files/Bar.tsx +++ b/home/ags/files/Bar.tsx @@ -1,11 +1,15 @@ -import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3"; -import { GLib, Variable, bind } from "astal"; +import { Astal, Gdk } from "ags/gtk4"; +import app from "ags/gtk4/app" +import Gtk from "gi://Gtk?version=4.0" +import { createBinding, createState, For, With, Accessor } from "ags" +import { createPoll } from "ags/time" import Tray from "gi://AstalTray"; -import { execAsync } from "astal/process" +import { execAsync } from "ags/process" import Hyprland from "gi://AstalHyprland"; import { getIconName } from "./utils"; import Wp from "gi://AstalWp" import Battery from "gi://AstalBattery" +import GLib from "gi://GLib"; const battery = Battery.get_default() const sensorsAvailable = await execAsync(['sensors']).then(() => true).catch(() => false); @@ -13,28 +17,35 @@ const wirePlumber = Wp.get_default(); function SysTray(): JSX.Element { const tray = Tray.get_default(); + let items = createBinding(tray, "items"); + const init = (btn: Gtk.MenuButton, item: Tray.TrayItem) => { + btn.menuModel = item.menuModel + btn.insert_action_group("dbusmenu", item.actionGroup) + item.connect("notify::action-group", () => { + btn.insert_action_group("dbusmenu", item.actionGroup) + }) + } return ( - {bind(tray, "items").as((items) => - items.map((item) => { - if (item.iconThemePath) App.add_icons(item.iconThemePath); + + {(item: Tray.TrayItem) => { + if (item.iconThemePath) app.add_icons(item.iconThemePath); return ( ["dbusmenu", ag])} - menuModel={bind(item, "menuModel")}> - + $={(self) => init(self, item)} + class="systray" + tooltipMarkup={createBinding(item, "tooltipMarkup")} + menuModel={createBinding(item, "menuModel")}> + ); - }), - )} + }} + ); } -function Left() : JSX.Element { +function Left(): JSX.Element { return ( @@ -42,7 +53,7 @@ function Left() : JSX.Element { ); } -function Center() : JSX.Element { +function Center(): JSX.Element { return ( @@ -51,85 +62,77 @@ function Center() : JSX.Element { } function Date({ format = "%Y-%m-%d" }): JSX.Element { - const time = Variable("").poll(60000, () => - GLib.DateTime.new_now_local().format(format)!) + const time = createPoll("", 60000, () => GLib.DateTime.new_now_local().format(format)!) return } function Icons() { return ( - + @@ -173,60 +177,74 @@ function Volume(): JSX.Element { if (!wirePlumber) return ; const audio = wirePlumber.audio; - const icon = bind(audio.default_speaker, "volume").as((volume) => { - const vol = volume * 100 - const icon = [ - [101, 'overamplified'], - [67, 'high'], - [34, 'medium'], - [1, 'low'], - [0, 'muted'], - ].find(([threshold]) => Number(threshold) <= vol)?.[1] - return `audio-volume-${icon}-symbolic` + const icon = createBinding(audio.default_speaker, "volume").as((volume) => { + const vol = volume * 100 + const icon = [ + [101, 'overamplified'], + [67, 'high'], + [34, 'medium'], + [1, 'low'], + [0, 'muted'], + ].find(([threshold]) => Number(threshold) <= vol)?.[1] + return `audio-volume-${icon}-symbolic` }); - const css = bind(audio.default_speaker, "mute").as((mute) => { - return mute ? "margin-left:0;": "margin-left: 0.7em;" + const css = createBinding(audio.default_speaker, "mute").as((mute) => { + return mute ? "margin-left:0;" : "margin-left: 0.7em;" }); + let volume = createBinding(audio.default_speaker, "volume"); + let mute = createBinding(audio.default_speaker, "mute"); return ( - ); } -function Workspaces() : JSX.Element { +function Workspaces(): JSX.Element { const hyprland = Hyprland.get_default(); + let workspaces = createBinding(hyprland, "workspaces"); return ( - - {bind(hyprland, "workspaces").as((wss) => - - {bind(hyprland, "focusedMonitor").as((fm) => - wss.sort((a, b) => a.id - b.id) - .filter(ws => ws && ws.get_monitor() && ws.get_monitor().get_id() === fm.get_id()) - .map((ws) => ( - - )))} - - )} + + + {(wss: Array) => ( + + + {(fm: Hyprland.Monitor) => { + let filtered_wss = new Accessor(() => wss.sort((a, b) => a.id - b.id) + .filter(ws => ws && ws.get_monitor() && ws.get_monitor().get_id() === fm.get_id())) + return ( + + + {(ws: Hyprland.Workspace, _index) => ( + + )} + + + ) + }} + + + )} + ); } @@ -235,59 +253,66 @@ function shorten(title: string) { return title.length > 40 ? title.slice(0, 20) + "..." : title } -function Clients() : JSX.Element { +function Clients(): JSX.Element { const hyprland = Hyprland.get_default(); + let clients = createBinding(hyprland, "clients") return ( - { - bind(hyprland, "focusedWorkspace").as(fw => ( - - { - bind(hyprland, "clients").as(cls => - cls - .sort((a, b) => a.pid - b.pid) - .filter(cl => !cl.title.includes("rofi")) - .filter(cl => fw && cl.get_workspace().get_id() === fw.get_id()) - .map(cl => ( - a && a.address === cl.address ? "focused" : "unfocused")} - > - - + } + + ); } -export default function Bar(gdkmonitor: Gdk.Monitor, scaleFactor: number = 1): Widget.Window { - return new Widget.Window({ - gdkmonitor, - css: "font-size: " + scaleFactor + "em;", - exclusivity: Astal.Exclusivity.EXCLUSIVE, - anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT, - application: App, - className: "Bar", - name: "top-bar", - setup: self => self.connect("destroy", () => { - print("Detroying bar"); - App.remove_window(self); - }), - child: - -
- +export default function Bar(gdkmonitor: Gdk.Monitor, scaleFactor: number = 1) { + console.log("Creating Bar on monitor:", gdkmonitor); + return ( + + + +
+ - }) + + ) } diff --git a/home/ags/files/app.ts b/home/ags/files/app.ts index 1d896d2a..2e88a286 100644 --- a/home/ags/files/app.ts +++ b/home/ags/files/app.ts @@ -1,8 +1,10 @@ -import { App, Gdk, Widget } from "astal/gtk3" +import app from "ags/gtk4/app" import style from "./style.scss" import Bar from "./Bar" import Hyprland from "gi://AstalHyprland"; import NotificationPopups from "./notifications/NotificationPopups" +import Gtk from "gi://Gtk?version=4.0"; +import { Gdk } from "ags/gtk4"; const hyprland = Hyprland.get_default(); @@ -22,21 +24,26 @@ function find_main_monitor(): Hyprland.Monitor { } function register_windows(monitor: Hyprland.Monitor) { - let gtkMonitor = App.get_monitors()[0].get_display().get_monitor_at_point(monitor.get_x(), monitor.get_y()) - let scale = (monitor.get_width() >= 3000)? 1.2: 1 + let gtkMonitors = app.get_monitors()[0].get_display().get_monitors() + let gtkMonitor = gtkMonitors.get_item(0) + if (!gtkMonitor) { + console.error("No GTK monitor found for the Hyprland monitor:", monitor.get_name()); + return; + } + let scale = (monitor.get_width() >= 3000) ? 1.2 : 1 Bar(gtkMonitor, scale) - NotificationPopups(gtkMonitor) + NotificationPopups() } function switch_to_best_monitor() { let mainMonitor = find_main_monitor() - for (var wd of App.get_windows()) { + for (var wd of app.get_windows()) { wd.destroy(); } register_windows(mainMonitor); } -hyprland.connect("monitor-added", (_, monitor) => { +hyprland.connect("monitor-added", (_, _monitor: Hyprland.Monitor) => { switch_to_best_monitor() }) @@ -44,7 +51,7 @@ hyprland.connect("monitor-removed", () => { switch_to_best_monitor() }) -App.start({ +app.start({ css: style, iconTheme: "Papirus", main() { diff --git a/home/ags/files/env.d.ts b/home/ags/files/env.d.ts index 467c0a41..792ebfda 100644 --- a/home/ags/files/env.d.ts +++ b/home/ags/files/env.d.ts @@ -1,21 +1,21 @@ declare const SRC: string declare module "inline:*" { - const content: string - export default content + const content: string + export default content } declare module "*.scss" { - const content: string - export default content + const content: string + export default content } declare module "*.blp" { - const content: string - export default content + const content: string + export default content } declare module "*.css" { - const content: string - export default content + const content: string + export default content } diff --git a/home/ags/files/notifications/Notification.scss b/home/ags/files/notifications/Notification.scss index 0b59a912..b4041c94 100644 --- a/home/ags/files/notifications/Notification.scss +++ b/home/ags/files/notifications/Notification.scss @@ -1,125 +1,110 @@ @use "sass:string"; @function gtkalpha($c, $a) { - @return string.unquote("alpha(#{$c},#{$a})"); + @return string.unquote("alpha(#{$c},#{$a})"); } // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss -$fg-color: #{"@theme_fg_color"}; +$fg-color: #{"@theme_bg_color"}; $bg-color: #1f2430; $error: red; window.NotificationPopups { - all: unset; + all: unset; } -eventbox.Notification { +.Notification { + border-radius: 13px; + background-color: $bg-color; + margin: 0.5rem 1rem 0.5rem 1rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, 0.4); + border: 1pt solid gtkalpha($fg-color, 0.03); - &:first-child>box { - margin-top: 1rem; - } - - &:last-child>box { - margin-bottom: 1rem; - } - - // eventboxes can not take margins so we style its inner box instead - >box { - min-width: 400px; - border-radius: 13px; - background-color: #1f2430; - margin: .5rem 1rem .5rem 1rem; - box-shadow: 2px 3px 8px 0 gtkalpha(black, .4); - border: 1pt solid gtkalpha($fg-color, .03); - } - - &.critical>box { - border: 1pt solid gtkalpha($error, .4); - - .header { - - .app-name { - color: gtkalpha($error, .8); - - } - - .app-icon { - color: gtkalpha($error, .6); - } - } - } + &.critical { + border: 1pt solid gtkalpha($error, 0.4); .header { - padding: .5rem; - color: gtkalpha($fg-color, 0.5); + .app-name { + color: gtkalpha($error, 0.8); + } - .app-icon { - margin: 0 .4rem; - } + .app-icon { + color: gtkalpha($error, 0.6); + } + } + } - .app-name { - margin-right: .3rem; - font-weight: bold; + .header { + padding: 0.5rem; + color: gtkalpha($fg-color, 0.5); - &:first-child { - margin-left: .4rem; - } - } - - .time { - margin: 0 .4rem; - } - - button { - padding: .2rem; - min-width: 0; - min-height: 0; - } + .app-icon { + margin: 0 0.4rem; } - separator { - margin: 0 .4rem; - background-color: gtkalpha($fg-color, .1); + .app-name { + margin-right: 0.3rem; + font-weight: bold; + + &:first-child { + margin-left: 0.4rem; + } } - .content { - margin: 1rem; - margin-top: .5rem; - - .summary { - font-size: 1.2em; - color: $fg-color; - } - - .body { - color: gtkalpha($fg-color, 0.8); - } - - .image { - border: 1px solid gtkalpha($fg-color, .02); - margin-right: .5rem; - border-radius: 9px; - min-width: 100px; - min-height: 100px; - background-size: cover; - background-position: center; - } + .time { + margin: 0 0.4rem; } - .actions { - margin: 1rem; - margin-top: 0; - - button { - margin: 0 .3rem; - - &:first-child { - margin-left: 0; - } - - &:last-child { - margin-right: 0; - } - } + button { + padding: 0.2rem; + min-width: 0; + min-height: 0; } + } + + separator { + margin: 0 0.4rem; + background-color: gtkalpha($fg-color, 0.1); + } + + .content { + margin: 1rem; + margin-top: 0.5rem; + + .summary { + font-size: 1.2em; + color: $fg-color; + } + + .body { + color: gtkalpha($fg-color, 0.8); + } + + .image { + border: 1px solid gtkalpha($fg-color, 0.02); + margin-right: 0.5rem; + border-radius: 9px; + min-width: 100px; + min-height: 100px; + background-size: cover; + background-position: center; + } + } + + .actions { + margin: 1rem; + margin-top: 0; + + button { + margin: 0 0.3rem; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } } diff --git a/home/ags/files/notifications/Notification.tsx b/home/ags/files/notifications/Notification.tsx index 5149d5b7..c216ff0c 100644 --- a/home/ags/files/notifications/Notification.tsx +++ b/home/ags/files/notifications/Notification.tsx @@ -1,107 +1,120 @@ -import { GLib } from "astal" -import { Gtk, Astal } from "astal/gtk3" -import { type EventBox } from "astal/gtk3/widget" -import Notifd from "gi://AstalNotifd" +import Gtk from "gi://Gtk?version=4.0" +import Gdk from "gi://Gdk?version=4.0" +import Adw from "gi://Adw" +import GLib from "gi://GLib" +import AstalNotifd from "gi://AstalNotifd" +import Pango from "gi://Pango" -const isIcon = (icon: string) => - !!Astal.Icon.lookup_icon(icon) +function isIcon(icon?: string | null) { + const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!) + return icon && iconTheme.has_icon(icon) +} -const fileExists = (path: string) => - GLib.file_test(path, GLib.FileTest.EXISTS) +function fileExists(path: string) { + return GLib.file_test(path, GLib.FileTest.EXISTS) +} -const time = (time: number, format = "%H:%M") => GLib.DateTime - .new_from_unix_local(time) - .format(format)! +function time(time: number, format = "%H:%M") { + return GLib.DateTime.new_from_unix_local(time).format(format)! +} -const urgency = (n: Notifd.Notification) => { - const { LOW, NORMAL, CRITICAL } = Notifd.Urgency - // match operator when? +function urgency(n: AstalNotifd.Notification) { + const { LOW, NORMAL, CRITICAL } = AstalNotifd.Urgency switch (n.urgency) { - case LOW: return "low" - case CRITICAL: return "critical" + case LOW: + return "low" + case CRITICAL: + return "critical" case NORMAL: - default: return "normal" + default: + return "normal" } } -type Props = { - setup(self: EventBox): void - onHoverLost(self: EventBox): void - notification: Notifd.Notification -} - -export default function Notification(props: Props) { - const { notification: n, onHoverLost, setup } = props - const { START, CENTER, END } = Gtk.Align - - return - - - {(n.appIcon || n.desktopEntry) && } - - - - {n.image && fileExists(n.image) && } - {n.image && isIcon(n.image) && - - } - +export default function Notification({ + notification: n, + onHoverLost, +}: { + notification: AstalNotifd.Notification + onHoverLost: () => void +}) { + return ( + + + + + {(n.appIcon || isIcon(n.desktopEntry)) && ( + + )} - - {n.get_actions().length > 0 && - {n.get_actions().map(({ label, id }) => ( - - ))} - } - - + + + + {n.image && fileExists(n.image) && ( + + )} + {n.image && isIcon(n.image) && ( + + + + )} + + + + {n.actions.length > 0 && ( + + {n.actions.map(({ label, id }) => ( + + ))} + + )} + + + ) } diff --git a/home/ags/files/notifications/NotificationPopups.tsx b/home/ags/files/notifications/NotificationPopups.tsx index 616b1ea4..2a8aab99 100644 --- a/home/ags/files/notifications/NotificationPopups.tsx +++ b/home/ags/files/notifications/NotificationPopups.tsx @@ -1,105 +1,65 @@ -import { Astal, Gtk, Gdk } from "astal/gtk3" -import Notifd from "gi://AstalNotifd" +import app from "ags/gtk4/app" +import { Astal, Gtk } from "ags/gtk4" +import AstalNotifd from "gi://AstalNotifd" import Notification from "./Notification" -import { type Subscribable } from "astal/binding" -import { GLib, Variable, bind, timeout } from "astal" +import { createBinding, For, createState, onCleanup } from "ags" -// see comment below in constructor -const TIMEOUT_DELAY = 5000 +export default function NotificationPopups() { + const monitors = createBinding(app, "monitors") -// The purpose if this class is to replace Variable> -// with a Map type in order to track notification widgets -// by their id, while making it conviniently bindable as an array -class NotifiationMap implements Subscribable { - // the underlying map to keep track of id widget pairs - private map: Map = new Map() + const notifd = AstalNotifd.get_default() - // it makes sense to use a Variable under the hood and use its - // reactivity implementation instead of keeping track of subscribers ourselves - private var: Variable> = Variable([]) + const [notifications, setNotifications] = createState( + new Array(), + ) - // notify subscribers to rerender when state changes - private notifiy() { - this.var.set([...this.map.values()].reverse()) - } + const notifiedHandler = notifd.connect("notified", (_, id, replaced) => { + const notification = notifd.get_notification(id) - private constructor() { - const notifd = Notifd.get_default() + if (replaced && notifications.get().some(n => n.id === id)) { + setNotifications((ns) => ns.map((n) => (n.id === id ? notification : n))) + } else { + setNotifications((ns) => [notification, ...ns]) + } + }) - /** - * uncomment this if you want to - * ignore timeout by senders and enforce our own timeout - * note that if the notification has any actions - * they might not work, since the sender already treats them as resolved - */ - // notifd.ignoreTimeout = true + const resolvedHandler = notifd.connect("resolved", (_, id) => { + setNotifications((ns) => ns.filter((n) => n.id !== id)) + }) - notifd.connect("notified", (_, id) => { - this.set(id, Notification({ - notification: notifd.get_notification(id)!, + // technically, we don't need to cleanup because in this example this is a root component + // and this cleanup function is only called when the program exits, but exiting will cleanup either way + // but it's here to remind you that you should not forget to cleanup signal connections + onCleanup(() => { + notifd.disconnect(notifiedHandler) + notifd.disconnect(resolvedHandler) + }) - // once hovering over the notification is done - // destroy the widget without calling notification.dismiss() - // so that it acts as a "popup" and we can still display it - // in a notification center like widget - // but clicking on the close button will close it - onHoverLost: () => this.delete(id), - - // notifd by default does not close notifications - // until user input or the timeout specified by sender - // which we set to ignore above - setup: () => timeout(TIMEOUT_DELAY, () => { - /** - * uncomment this if you want to "hide" the notifications - * after TIMEOUT_DELAY - */ - this.delete(id) - }) - })) - }) - - // notifications can be closed by the outside before - // any user input, which have to be handled too - notifd.connect("resolved", (_, id) => { - this.delete(id) - }) - } - - private set(key: number, value: Gtk.Widget) { - // in case of replacecment destroy previous widget - this.map.get(key)?.destroy() - this.map.set(key, value) - this.notifiy() - } - - private delete(key: number) { - this.map.get(key)?.destroy() - this.map.delete(key) - this.notifiy() - } - - // needed by the Subscribable interface - get() { - return this.var.get() - } - - // needed by the Subscribable interface - subscribe(callback: (list: Array) => void) { - return this.var.subscribe(callback) - } -} - -export default function NotificationPopups(gdkmonitor: Gdk.Monitor) { - const { TOP, RIGHT } = Astal.WindowAnchor - const notifs = new NotifiationMap() - - return - - {bind(notifs)} - - + return ( + (win as Gtk.Window).destroy()}> + {(monitor) => ( + ns.length > 0)} + anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT} + > + + + {(notification) => ( + + setNotifications((ns) => + ns.filter((n) => n.id !== notification.id), + ) + } + /> + )} + + + + )} + + ) } diff --git a/home/ags/files/package.json b/home/ags/files/package.json new file mode 100644 index 00000000..9805e006 --- /dev/null +++ b/home/ags/files/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "ags": "*" + } +} diff --git a/home/ags/files/style.scss b/home/ags/files/style.scss index 28e132da..6d3035d2 100644 --- a/home/ags/files/style.scss +++ b/home/ags/files/style.scss @@ -17,7 +17,7 @@ window.Bar { margin-top: 0.2em; } - .clients box { + .clients box box { margin-right: 0.3em; } @@ -27,11 +27,13 @@ window.Bar { border-radius: 0.3em; background: #1f2430; } + .battery-item:hover { background: #023269; } - .item, .clients box { + .item, + .clients box box { background: #1f2430; padding-left: 0.7em; padding-right: 0.7em; @@ -49,12 +51,13 @@ window.Bar { button { background: #1f2430; - border:none; + border: none; padding: 0.2em; border-radius: 0.3em; } - .focused, .clients box.focused { + .focused, + .clients box.focused { background: #023269; } diff --git a/home/ags/files/tsconfig.json b/home/ags/files/tsconfig.json index fc324635..b81c2ffa 100644 --- a/home/ags/files/tsconfig.json +++ b/home/ags/files/tsconfig.json @@ -1,22 +1,16 @@ { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { - "experimentalDecorators": true, "strict": true, - "target": "ES2022", "module": "ES2022", + "target": "ES2020", + "lib": [ + "ES2023" + ], "moduleResolution": "Bundler", // "checkJs": true, // "allowJs": true, "jsx": "react-jsx", - "jsxImportSource": "/nix/store/1pd4fdq90f4vla3zghmfci9axsbvkd3w-astal-gjs/share/astal/gjs/gtk3", - "paths": { - "astal": [ - "/nix/store/1pd4fdq90f4vla3zghmfci9axsbvkd3w-astal-gjs/share/astal/gjs" - ], - "astal/*": [ - "/nix/store/1pd4fdq90f4vla3zghmfci9axsbvkd3w-astal-gjs/share/astal/gjs/*" - ] - }, + "jsxImportSource": "ags/gtk4" } }