From 78e472beb8633d25f5fc25fd47b2c1c0cdfd6668 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Sat, 19 Apr 2025 15:20:50 +0200 Subject: [PATCH] [AGS] Redo basic setup --- config/astal/.gitignore | 2 + config/astal/app.ts | 39 ++++ config/astal/components/bar/ui/Bar.tsx | 23 ++ .../astal/components/bar/ui/QuickActions.tsx | 76 +++++++ .../components/bar/ui/modules/Calendar.tsx | 0 .../components/bar/ui/modules/Hyprland.tsx | 57 +++++ .../components/bar/ui/modules/QuickView.tsx | 0 .../components/bar/ui/modules/SystemInfo.tsx | 0 .../astal/components/bar/ui/quickactions.scss | 37 ++++ config/astal/components/bar/util/hyprland.ts | 68 ++++++ config/astal/components/bar/util/sysinfo.ts | 0 .../components/notifications/handler.tsx | 206 ++++++++++++++++++ .../notifications/notifications.scss | 125 +++++++++++ .../notifications/notifications.tsx | 112 ++++++++++ config/astal/env.d.ts | 21 ++ config/astal/package.json | 6 + config/astal/style.scss | 20 ++ config/astal/tsconfig.json | 14 ++ 18 files changed, 806 insertions(+) create mode 100644 config/astal/.gitignore create mode 100644 config/astal/app.ts create mode 100644 config/astal/components/bar/ui/Bar.tsx create mode 100644 config/astal/components/bar/ui/QuickActions.tsx create mode 100644 config/astal/components/bar/ui/modules/Calendar.tsx create mode 100644 config/astal/components/bar/ui/modules/Hyprland.tsx create mode 100644 config/astal/components/bar/ui/modules/QuickView.tsx create mode 100644 config/astal/components/bar/ui/modules/SystemInfo.tsx create mode 100644 config/astal/components/bar/ui/quickactions.scss create mode 100644 config/astal/components/bar/util/hyprland.ts create mode 100644 config/astal/components/bar/util/sysinfo.ts create mode 100644 config/astal/components/notifications/handler.tsx create mode 100644 config/astal/components/notifications/notifications.scss create mode 100644 config/astal/components/notifications/notifications.tsx create mode 100644 config/astal/env.d.ts create mode 100644 config/astal/package.json create mode 100644 config/astal/style.scss create mode 100644 config/astal/tsconfig.json diff --git a/config/astal/.gitignore b/config/astal/.gitignore new file mode 100644 index 0000000..298eb4d --- /dev/null +++ b/config/astal/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +@girs/ diff --git a/config/astal/app.ts b/config/astal/app.ts new file mode 100644 index 0000000..df44317 --- /dev/null +++ b/config/astal/app.ts @@ -0,0 +1,39 @@ +import { App } from "astal/gtk3" +import style from "./style.scss" + +import notifications from "./components/notifications/handler"; + +App.start({ + instanceName: "runner", + css: style, + main() { + notifications.startNotificationHandler( 0, App.get_monitors()[0] ) + }, + requestHandler(request, res) { + const args = request.trimStart().split( ' ' ); + + // Notifications + if ( args[ 0 ] === 'notifier' ) { + if ( args[ 1 ] == 'show' ) { + notifications.openNotificationMenu( 0 ); + res( 'Showing all open notifications' ); + } else if ( args[ 1 ] == 'hide' ) { + notifications.closeNotificationMenu( 0 ); + res( 'Hid all notifications' ); + } else if ( args[ 1 ] == 'clear' ) { + notifications.clearAllNotifications( 0 ); + res( 'Cleared all notifications' ); + } else if ( args[ 1 ] == 'clear-newest' ) { + notifications.clearNewestNotifications( 0 ); + res( 'Cleared newest notification' ); + } else if ( args[ 1 ] == 'toggle' ) { + notifications.toggleNotificationMenu( 0 ); + res( 'Toggled notifications' ); + } else { + res( 'Unknown command!' ); + } + } else if ( args[ 0 ] === 'bar' ) { + + } + }, +}) diff --git a/config/astal/components/bar/ui/Bar.tsx b/config/astal/components/bar/ui/Bar.tsx new file mode 100644 index 0000000..97be98e --- /dev/null +++ b/config/astal/components/bar/ui/Bar.tsx @@ -0,0 +1,23 @@ +import { createQuickActionsMenu } from "./QuickActions"; +import { GLib } from "astal"; +import { Astal, Gdk, Gtk } from "astal/gtk3"; + +const Bar = (gdkmonitor: Gdk.Monitor) => { + + return ( + + + + + + + + + ) ) ) } + +} + + +const HyprlandWorkspace = () => { + const hypr = AstalHyprland.get_default() + + return + {bind(hypr, "workspaces").as(wss => wss + .filter(ws => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces + .sort((a, b) => a.id - b.id) + .map(ws => ( + + )) + )} + +} + + +const HyprlandActiveWindow = () => { + const hypr = AstalHyprland.get_default(); + const focused = bind( hypr, "focusedClient" ); + + return + {focused.as( client => ( + client && +} + +export default { + HyprlandWorkspace, + HyprlandActiveWindow, + SysTray +} diff --git a/config/astal/components/bar/ui/modules/QuickView.tsx b/config/astal/components/bar/ui/modules/QuickView.tsx new file mode 100644 index 0000000..e69de29 diff --git a/config/astal/components/bar/ui/modules/SystemInfo.tsx b/config/astal/components/bar/ui/modules/SystemInfo.tsx new file mode 100644 index 0000000..e69de29 diff --git a/config/astal/components/bar/ui/quickactions.scss b/config/astal/components/bar/ui/quickactions.scss new file mode 100644 index 0000000..dc4a40e --- /dev/null +++ b/config/astal/components/bar/ui/quickactions.scss @@ -0,0 +1,37 @@ +@import "colors"; + +.toggle-row { + background-color: $bg; + border-radius: 12px; + margin: 6px 0; + overflow: hidden; + border: 1px solid $border; + + button { + padding: 10px 16px; + font-size: 14px; + transition: background 0.2s ease; + border: none; + background: transparent; + + &:hover { + background-color: $hover; + } + } + + .toggle { + flex: 1; + background-color: transparent; + text-align: left; + &.active { + background-color: $accent; + color: white; + } + } + + .arrow { + width: 40px; + background-color: transparent; + text-align: center; + } +} diff --git a/config/astal/components/bar/util/hyprland.ts b/config/astal/components/bar/util/hyprland.ts new file mode 100644 index 0000000..47a1481 --- /dev/null +++ b/config/astal/components/bar/util/hyprland.ts @@ -0,0 +1,68 @@ +import AstalHyprland from "gi://AstalHyprland"; + +const getAvailableWorkspaces = (): number[] => { + const workspaces: number[] = []; + AstalHyprland.get_default().get_workspaces().forEach( val => { + workspaces.push( val.get_id() ); + } ) + return workspaces; +} + +const getCurrentWorkspaceID = (): number => { + return AstalHyprland.get_default().get_focused_workspace().get_id(); +} + +const getCurrentWindowTitle = (): string => { + return AstalHyprland.get_default().get_focused_client().get_title(); +} + +/** + * Get the workspace ID of a window by its address + * @param address - The address of the window + * @returns The workspace ID + */ +const getWorkspaceIDOfWindowByAddress = ( address: string ): number => { + AstalHyprland.get_default().get_clients().forEach( client => { + if ( client.get_address() === address ) { + return client.get_workspace().get_id(); + } + } ); + + return -1; +} + + +interface HyprlandSubscriptions { + [key: string]: ( data: string ) => void; +} +const hooks: HyprlandSubscriptions = {}; + +/** + * Add an event listener for Hyprland events. + * @param event - A hyprland IPC event. See https://wiki.hyprland.org/IPC/. Useful events include: urgent, windowtitlev2, workspace, createworkspacev2, destroyworkspacev2, activewindowv2 + * @param cb - The callback function + */ +const subscribeToUpdates = ( event: string, cb: ( data: string ) => void ): void => { + hooks[ event ] = cb; +} + +/** + * Listen to events. Must be called at some point if events are to be listened for + */ +const listen = () => { + AstalHyprland.get_default().connect( "event", ( name: string, data: string ) => { + if ( hooks[ name ] ) { + hooks[ name ]( data ); + } + } ); +} + + +export default { + getAvailableWorkspaces, + getCurrentWorkspaceID, + getCurrentWindowTitle, + getWorkspaceIDOfWindowByAddress, + subscribeToUpdates, + listen +} diff --git a/config/astal/components/bar/util/sysinfo.ts b/config/astal/components/bar/util/sysinfo.ts new file mode 100644 index 0000000..e69de29 diff --git a/config/astal/components/notifications/handler.tsx b/config/astal/components/notifications/handler.tsx new file mode 100644 index 0000000..79386ca --- /dev/null +++ b/config/astal/components/notifications/handler.tsx @@ -0,0 +1,206 @@ +/* +* dotfiles - handler.ts +* +* Created by Janis Hutz 03/21/2025, Licensed under the GPL V3 License +* https://janishutz.com, development@janishutz.com +* +* +*/ + +import { Astal, Gtk, Gdk } from "astal/gtk3" +import Notifd from "gi://AstalNotifd"; +import Notification from "./notifications"; +import { type Subscribable } from "astal/binding"; +import { Variable, bind, timeout } from "astal" + +// Config +const TIMEOUT_DELAY = 5000; + +class Notifier implements Subscribable { + private display: Map = new Map(); + private notifications: Map = new Map(); + + private notifd: Notifd.Notifd; + private subscriberData: Variable = Variable( [] ); + private instanceID: number; + private menuOpen: boolean; + + /** + * Sets up the notifier + */ + constructor( id: number ) { + this.instanceID = id; + this.menuOpen = false; + this.notifd = Notifd.get_default(); + this.notifd.ignoreTimeout = true; + + this.notifd.connect( 'notified', ( _, id ) => { + this.add( id ); + } ); + + this.notifd.connect( 'resolved', ( _, id ) => { + this.delete( id ); + } ); + } + + private notify () { + this.subscriberData.set( [ ...this.display.values() ].reverse() ); + } + + private add ( id: number ) { + this.notifications.set( id, this.notifd.get_notification( id )! ); + this.show( id ); + } + + /** + * Show an element on screen + * @param id - The id of the element to be shown + */ + private show ( id: number ) { + this.set( id, Notification( { + notification: this.notifications.get( id )!, + onHoverLost: () => { + if ( !this.menuOpen ) { + this.hide( id ); + } + }, + setup: () => timeout( TIMEOUT_DELAY, () => { + if ( !this.menuOpen ) { + this.hide( id ); + } + } ), + id: id, + delete: deleteHelper, + instanceID: this.instanceID + } ) ) + } + + /** + * Set a selected widget to be shown + * @param id - The id of the element to be referenced for later + * @param widget - A GTK widget instance + */ + private set ( id: number, widget: Gtk.Widget ) { + this.display.get( id )?.destroy(); + this.display.set( id, widget ); + this.notify(); + } + + /** + * Hide, not delete notification (= send to notification centre) + * @param id - The id of the notification to hide + */ + private hide ( id: number ) { + this.display.get( id )?.destroy(); + this.display.delete( id ); + this.notify(); + } + + /** + * Delete a notification (from notification centre too) + * @param id - The id of the notification to hide + */ + delete ( id: number ) { + this.hide( id ); + this.notifications.get( id )?.dismiss(); + this.notifications.delete( id ); + if ( this.notifications.size == 0 ) { + this.menuOpen = false; + } + } + + openNotificationMenu () { + // Show all notifications that have not been cleared + if ( this.notifications.size > 0 ) { + this.menuOpen = true; + this.notifications.forEach( ( _, id ) => { + this.show( id ); + } ) + } + } + + hideNotifications () { + this.menuOpen = false; + this.notifications.forEach( ( _, id ) => { + this.hide( id ); + } ); + } + + toggleNotificationMenu () { + if ( this.menuOpen ) { + this.hideNotifications(); + } else { + this.openNotificationMenu(); + } + } + + clearAllNotifications () { + this.menuOpen = false; + this.notifications.forEach( ( _, id ) => { + this.delete( id ); + } ) + } + + clearNewestNotification () { + this.delete( [ ...this.notifications.keys() ][0] ); + } + + subscribe(callback: (value: unknown) => void): () => void { + return this.subscriberData.subscribe( callback ); + } + + get() { + return this.subscriberData.get(); + } +} + +const notifiers: Map = new Map(); +const deleteHelper = ( id: number, instanceID: number ) => { + notifiers.get( instanceID )?.delete( id ); +} + +const openNotificationMenu = ( id: number ) => { + notifiers.get( id )?.openNotificationMenu(); +} + +const closeNotificationMenu = ( id: number ) => { + notifiers.get( id )?.hideNotifications(); +} + +const toggleNotificationMenu = ( id: number ) => { + notifiers.get( id )?.toggleNotificationMenu(); +} + +const clearAllNotifications = ( id: number ) => { + notifiers.get( id )?.clearAllNotifications(); +} + +const clearNewestNotifications = ( id: number ) => { + notifiers.get( id )?.clearNewestNotification(); +} + +const startNotificationHandler = (id: number, gdkmonitor: Gdk.Monitor) => { + const { TOP, RIGHT } = Astal.WindowAnchor + const notifier: Notifier = new Notifier( id ); + notifiers.set( id, notifier ); + + return + + {bind(notifier)} + + +} + + +export default { + startNotificationHandler, + openNotificationMenu, + closeNotificationMenu, + clearAllNotifications, + clearNewestNotifications, + toggleNotificationMenu +} diff --git a/config/astal/components/notifications/notifications.scss b/config/astal/components/notifications/notifications.scss new file mode 100644 index 0000000..a32f08b --- /dev/null +++ b/config/astal/components/notifications/notifications.scss @@ -0,0 +1,125 @@ +@use "sass:string"; + +@function gtkalpha($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"}; +$bg-color: #{"@theme_bg_color"}; +$error: red; + +window.NotificationPopups { + all: unset; +} + +eventbox.Notification { + + &: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: $bg-color; + 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); + } + } + } + + .header { + padding: .5rem; + color: gtkalpha($fg-color, 0.5); + + .app-icon { + margin: 0 .4rem; + } + + .app-name { + margin-right: .3rem; + font-weight: bold; + + &:first-child { + margin-left: .4rem; + } + } + + .time { + margin: 0 .4rem; + } + + button { + padding: .2rem; + min-width: 0; + min-height: 0; + } + } + + separator { + margin: 0 .4rem; + background-color: gtkalpha($fg-color, .1); + } + + .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; + } + } + + .actions { + margin: 1rem; + margin-top: 0; + + button { + margin: 0 .3rem; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/config/astal/components/notifications/notifications.tsx b/config/astal/components/notifications/notifications.tsx new file mode 100644 index 0000000..de4e7b2 --- /dev/null +++ b/config/astal/components/notifications/notifications.tsx @@ -0,0 +1,112 @@ +// From astal examples + +import { GLib } from "astal" +import { Gtk, Astal } from "astal/gtk3" +import { type EventBox } from "astal/gtk3/widget" +import Notifd from "gi://AstalNotifd" + +const isIcon = (icon: string) => + !!Astal.Icon.lookup_icon(icon) + +const fileExists = (path: string) => + GLib.file_test(path, GLib.FileTest.EXISTS) + +const time = (time: number, format = "%H:%M") => GLib.DateTime + .new_from_unix_local(time) + .format(format)! + +const urgency = (n: Notifd.Notification) => { + const { LOW, NORMAL, CRITICAL } = Notifd.Urgency + // match operator when? + switch (n.urgency) { + case LOW: return "low" + case CRITICAL: return "critical" + case NORMAL: + default: return "normal" + } +} + +type Props = { + delete( id: number, instanceID: number ): void + setup(self: EventBox): void + onHoverLost(self: EventBox): void + notification: Notifd.Notification + id: number + instanceID: number +} + +export default function Notification(props: Props) { + const { notification: n, onHoverLost, setup, id: id, delete: del, instanceID: instance } = props + const { START, CENTER, END } = Gtk.Align + + return + + + {(n.appIcon || n.desktopEntry) && } + + + + {n.image && fileExists(n.image) && } + {n.image && isIcon(n.image) && + + } + + + + {n.get_actions().length > 0 && + {n.get_actions().map(({ label, id }) => ( + + ))} + } + + +} diff --git a/config/astal/env.d.ts b/config/astal/env.d.ts new file mode 100644 index 0000000..467c0a4 --- /dev/null +++ b/config/astal/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/config/astal/package.json b/config/astal/package.json new file mode 100644 index 0000000..44226f2 --- /dev/null +++ b/config/astal/package.json @@ -0,0 +1,6 @@ +{ + "name": "astal-shell", + "dependencies": { + "astal": "/usr/share/astal/gjs" + } +} diff --git a/config/astal/style.scss b/config/astal/style.scss new file mode 100644 index 0000000..1d0d3a9 --- /dev/null +++ b/config/astal/style.scss @@ -0,0 +1,20 @@ +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; + +window.Bar { + background: transparent; + color: $fg-color; + font-weight: bold; + + >centerbox { + background: $bg-color; + border-radius: 10px; + margin: 8px; + } + + button { + border-radius: 8px; + margin: 2px; + } +} diff --git a/config/astal/tsconfig.json b/config/astal/tsconfig.json new file mode 100644 index 0000000..9471e35 --- /dev/null +++ b/config/astal/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "experimentalDecorators": true, + "strict": true, + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + // "checkJs": true, + // "allowJs": true, + "jsx": "react-jsx", + "jsxImportSource": "astal/gtk3", + } +}