[AGS] GTK4 Migration: Partially complete

This commit is contained in:
Janis Hutz 2025-04-21 21:48:29 +02:00
parent 7380c75818
commit 8b70f80e60
8 changed files with 321 additions and 267 deletions

View File

@ -1,16 +1,16 @@
import { App } from "astal/gtk3" import { App } from "astal/gtk4"
import style from "./style.scss" import style from "./style.scss"
import notifications from "./components/notifications/handler"; import notifications from "./components/notifications/handler";
import Bar from "./components/bar/ui/Bar"; // import Bar from "./components/bar/ui/Bar";
App.start({ App.start({
instanceName: "runner", instanceName: "runner",
css: style, css: style,
main() { main() {
notifications.startNotificationHandler( 0, App.get_monitors()[0] ); notifications.startNotificationHandler( App.get_monitors()[0] );
// TODO: Monitor handling // TODO: Monitor handling
Bar.Bar( App.get_monitors()[0] ); // Bar.Bar( App.get_monitors()[0] );
}, },
requestHandler(request, res) { requestHandler(request, res) {
const args = request.trimStart().split( ' ' ); const args = request.trimStart().split( ' ' );
@ -19,7 +19,7 @@ App.start({
if ( args[ 0 ] === 'notifier' ) { if ( args[ 0 ] === 'notifier' ) {
res( notifications.cliHandler( args ) ); res( notifications.cliHandler( args ) );
} else if ( args[ 0 ] === 'bar' ) { } else if ( args[ 0 ] === 'bar' ) {
res( Bar.cliHandler( args ) ); // res( Bar.cliHandler( args ) );
} }
}, },
}) })

View File

@ -1,6 +1,7 @@
import AstalTray from "gi://AstalTray"; import AstalTray from "gi://AstalTray";
import { bind, Variable } from "astal"; import { bind, Variable } from "astal";
import AstalHyprland from "gi://AstalHyprland"; import AstalHyprland from "gi://AstalHyprland";
import { Gtk } from "astal/gtk4";
const SysTray = () => { const SysTray = () => {
const tray = AstalTray.get_default(); const tray = AstalTray.get_default();
@ -48,12 +49,12 @@ const ActiveWindow = () => {
visible.set( !visible.get() ); visible.set( !visible.get() );
} }
return <box className={"HyprlandFocusedClients"} visible={focused.as(Boolean)}> return <box cssName={"HyprlandFocusedClients"} visible={focused.as(Boolean)}>
<button onClicked={toggleOverlay}> <Gtk.Button onClicked={toggleOverlay}>
{focused.as( client => ( {focused.as( client => (
client && <label label={bind( client, "title" ).as( String )} /> client && <label label={bind( client, "title" ).as( String )} />
))} ))}
</button> </Gtk.Button>
<eventbox visible={bind(visible).as( v => v )} name="popover-container"> <eventbox visible={bind(visible).as( v => v )} name="popover-container">
<label label="This is a test"></label> <label label="This is a test"></label>
</eventbox> </eventbox>

View File

@ -7,209 +7,267 @@
* *
*/ */
import { Astal, Gtk, Gdk } from "astal/gtk3" import { Astal, Gdk } from "astal/gtk4"
import Notifd from "gi://AstalNotifd"; import Notifd from "gi://AstalNotifd";
import Notification from "./notifications"; import Notification from "./notifications";
import { type Subscribable } from "astal/binding"; import { timeout, Variable } from "astal"
import { Variable, bind, timeout } from "astal"
// ───────────────────────────────────────────────────────────────────
// Config // Config
// ───────────────────────────────────────────────────────────────────
const TIMEOUT_DELAY = 5000; const TIMEOUT_DELAY = 5000;
let isRunning = false;
let notificationMenuOpen = false;
class Notifier implements Subscribable { interface NotificationDetails {
private display: Map<number, Gtk.Widget> = new Map(); notification: Notifd.Notification;
private notifications: Map<number, Notifd.Notification> = new Map(); backendID: number;
notifdID: number;
}
// ───────────────────────────────────────────────────────────────────
// ╭───────────────────────────────────────────────╮
// │ Handler │
// ╰───────────────────────────────────────────────╯
// ───────────────────────────────────────────────────────────────────
let ShownNotifications: number[] = [];
const ShownNotificationsCount: Variable<number> = Variable( 0 );
let Notifications: NotificationDetails[] = [];
const notifd = Notifd.get_default();
notifd.ignoreTimeout = true;
private notifd: Notifd.Notifd;
private subscriberData: Variable<Gtk.Widget[]> = Variable( [] );
private instanceID: number;
private menuOpen: boolean;
/** /**
* Sets up the notifier * Delete a notification by its internal ID
* @param index The notifd ID of the notification
*/ */
constructor( id: number ) { const deleteNotification = ( index: number ): void => {
this.instanceID = id; hideNotification( index );
this.menuOpen = false; Notifications.splice( index, 1 );
this.notifd = Notifd.get_default(); }
this.notifd.ignoreTimeout = true;
this.notifd.connect( 'notified', ( _, id ) => { // ───────────────────────────────────────────────────────────────────
this.add( id );
/**
* Delete a notification by notifd id
* @param id The notifd ID of the notification
*/
const deleteNotificationByNotificationID = ( id: number ): void => {
const index = findNotificationByNotificationID( id );
if ( index > -1 ) {
deleteNotification( index );
}
}
// ───────────────────────────────────────────────────────────────────
/**
* Find the internal ID from the notifd id for a notification (helper function)
* @param id The notifd ID of the notification
* @returns The internal ID or -1 if not found
*/
const findNotificationByNotificationID = ( id: number ): number => {
// Find index in Notifications array
for (let index = 0; index < Notifications.length; index++) {
if ( Notifications[ index ].notifdID === id ) {
return index;
}
}
return -1;
}
// ───────────────────────────────────────────────────────────────────
/**
* Add a notification to the notification handler
* @param id The notifd ID of the notification
*/
const addNotification = ( id: number ): void => {
print( '[ Notifications ] Notification with id ' + id + ' added.' );
const currIndex = Notifications.length;
Notifications.push( {
notifdID: id,
backendID: currIndex,
notification: notifd.get_notification( id )
} ); } );
this.notifd.connect( 'resolved', ( _, id ) => { showNotification( currIndex );
this.delete( id ); }
// ───────────────────────────────────────────────────────────────────
/**
* Start the notifd runner and handle notifications.
*/
const hookToNotificationDaemon = (): void => {
if ( isRunning ) {
printerr( '[ Notifications ] Error: Already running' );
return;
}
isRunning = true;
notifd.connect( 'notified', ( _, id ) => {
addNotification( id );
} );
notifd.connect( 'resolved', ( _, id ) => {
deleteNotificationByNotificationID( 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 * Show a notification. It will stay on screen (regardless of removeAgain passed in), if
* @param id - The id of the element to be shown * critical urgency
* @param id The internal id (index in Notifications array)
* @param removeAgain = true If to remove the notification from the screen again automatically
*/ */
private show ( id: number ) { const showNotification = ( id: number, removeAgain: boolean = true ) => {
this.set( id, Notification( { // Add notification to UI for display
notification: this.notifications.get( id )!, ShownNotifications.reverse().push( id );
onHoverLost: () => { ShownNotifications.reverse();
if ( !this.menuOpen ) { ShownNotificationsCount.set( ShownNotifications.length );
this.hide( id );
}
},
setup: () => timeout( TIMEOUT_DELAY, () => {
if ( !this.menuOpen ) {
this.hide( id );
}
} ),
id: id,
delete: deleteHelper,
instanceID: this.instanceID
} ) )
}
/** // Set delay to remove the notification again
* Set a selected widget to be shown if ( removeAgain && Notifications[ id ].notification.get_urgency() !== Notifd.Urgency.CRITICAL ) {
* @param id - The id of the element to be referenced for later timeout( TIMEOUT_DELAY, () => {
* @param widget - A GTK widget instance hideNotification( id );
*/
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();
/**
* Stop displaying notification
* @param id The internal id (index in the Notifications array)
*/
const hideNotification = ( id: number ) => {
if ( !notificationMenuOpen ) {
ShownNotifications.splice( ShownNotifications.indexOf( id ), 1 );
}
}
// ───────────────────────────────────────────────────────────────────
/**
* Open the notification menu. Called by toggleNotificationMenu too
*/
const openNotificationMenu = () => {
// Simply show all notifications
notificationMenuOpen = true;
const ShownNotifications = [];
for (let index = 0; index < Notifications.length; index++) {
ShownNotifications.push( index );
}
}
// ───────────────────────────────────────────────────────────────────
/**
* Close the notification menu. Called by toggleNotificationMenu too
*/
const closeNotificationMenu = () => {
// Hide all notifications
notificationMenuOpen = true;
ShownNotifications = [];
ShownNotificationsCount.set( 0 );
}
// ───────────────────────────────────────────────────────────────────
/**
* Toggle the notification menu (i.e. show all notifications)
*/
const toggleNotificationMenu = () => {
if ( notificationMenuOpen ) {
closeNotificationMenu();
} else { } else {
this.openNotificationMenu(); openNotificationMenu();
} }
} }
clearAllNotifications () { // ───────────────────────────────────────────────────────────────────
this.menuOpen = false;
this.notifications.forEach( ( _, id ) => {
this.delete( id ); /**
} ) * Delete all notifications
*/
const clearAllNotifications = () => {
Notifications = [];
ShownNotifications = [];
ShownNotificationsCount.set( 0 );
} }
clearNewestNotification () { // ───────────────────────────────────────────────────────────────────
this.delete( [ ...this.notifications.keys() ][0] );
/**
* Delete the newest notifications
*/
const clearNewestNotifications = () => {
ShownNotifications.splice( 0, 1 );
Notifications.splice( Notifications.length - 1, 1 );
} }
subscribe(callback: (value: unknown) => void): () => void {
return this.subscriberData.subscribe( callback );
}
get() {
return this.subscriberData.get();
}
}
const notifiers: Map<number, Notifier> = new Map(); // ───────────────────────────────────────────────────────────────────
const deleteHelper = ( id: number, instanceID: number ) => { // ╭───────────────────────────────────────────────╮
notifiers.get( instanceID )?.delete( id ); // │ User Interface │
} // ╰───────────────────────────────────────────────╯
// ───────────────────────────────────────────────────────────────────
const openNotificationMenu = ( id: number ) => { const startNotificationHandler = (gdkmonitor: Gdk.Monitor) => {
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 { TOP, RIGHT } = Astal.WindowAnchor
const notifier: Notifier = new Notifier( id );
notifiers.set( id, notifier ); hookToNotificationDaemon();
const range = (n: number) => [...Array(n).keys()];
return <window return <window
className="NotificationHandler" cssClasses={["NotificationHandler"]}
gdkmonitor={gdkmonitor} gdkmonitor={gdkmonitor}
exclusivity={Astal.Exclusivity.EXCLUSIVE} exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | RIGHT}> anchor={TOP | RIGHT}>
<box vertical noImplicitDestroy> <box vertical noImplicitDestroy>
{bind(notifier)} {ShownNotificationsCount( n => range( n ).map( i => {
print( 'Rendering' );
// i is index in ShownNotifications array
return <Notification id={i} delete={deleteNotification} notification={Notifications[ i ].notification}></Notification>
} ) ) }
</box> </box>
</window> </window>
} }
const cliHandler = ( args: string[] ): string => { const cliHandler = ( args: string[] ): string => {
if ( args[ 1 ] == 'show' ) { if ( args[ 1 ] == 'show' ) {
openNotificationMenu( 0 ); openNotificationMenu();
return 'Showing all open notifications'; return 'Showing all open notifications';
} else if ( args[ 1 ] == 'hide' ) { } else if ( args[ 1 ] == 'hide' ) {
closeNotificationMenu( 0 ); closeNotificationMenu();
return 'Hid all notifications'; return 'Hid all notifications';
} else if ( args[ 1 ] == 'clear' ) { } else if ( args[ 1 ] == 'clear' ) {
clearAllNotifications( 0 ); clearAllNotifications();
return 'Cleared all notifications'; return 'Cleared all notifications';
} else if ( args[ 1 ] == 'clear-newest' ) { } else if ( args[ 1 ] == 'clear-newest' ) {
clearNewestNotifications( 0 ); clearNewestNotifications();
return 'Cleared newest notification'; return 'Cleared newest notification';
} else if ( args[ 1 ] == 'toggle' ) { } else if ( args[ 1 ] == 'toggle' ) {
toggleNotificationMenu( 0 ); toggleNotificationMenu();
return 'Toggled notifications'; return 'Toggled notifications';
} else { } else {
return 'Unknown command!'; return 'Unknown command!';

View File

@ -9,11 +9,17 @@ $fg-color: #{"@theme_fg_color"};
$bg-color: #{"@theme_bg_color"}; $bg-color: #{"@theme_bg_color"};
$error: red; $error: red;
window.NotificationPopups { window.NotificationHandler {
all: unset; all: unset;
} }
eventbox.Notification { box.Notification {
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);
&:first-child>box { &:first-child>box {
margin-top: 1rem; margin-top: 1rem;
@ -23,16 +29,6 @@ eventbox.Notification {
margin-bottom: 1rem; 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 { &.critical>box {
border: 1pt solid gtkalpha($error, .4); border: 1pt solid gtkalpha($error, .4);

View File

@ -1,12 +1,9 @@
// From astal examples // From astal examples
import { GLib } from "astal" import { GLib } from "astal"
import { Gtk, Astal } from "astal/gtk3" import { Gtk } from "astal/gtk4"
import { type EventBox } from "astal/gtk3/widget"
import Notifd from "gi://AstalNotifd" import Notifd from "gi://AstalNotifd"
// import Pango from "gi://Pango?version=1.0"
const isIcon = (icon: string) =>
!!Astal.Icon.lookup_icon(icon)
const fileExists = (path: string) => const fileExists = (path: string) =>
GLib.file_test(path, GLib.FileTest.EXISTS) GLib.file_test(path, GLib.FileTest.EXISTS)
@ -27,78 +24,73 @@ const urgency = (n: Notifd.Notification) => {
} }
type Props = { type Props = {
delete( id: number, instanceID: number ): void delete( id: number ): void
setup(self: EventBox): void
onHoverLost(self: EventBox): void
notification: Notifd.Notification notification: Notifd.Notification
id: number id: number
instanceID: number
} }
export default function Notification(props: Props) { export default function Notification(props: Props) {
const { notification: n, onHoverLost, setup, id: id, delete: del, instanceID: instance } = props const { notification: n, id: id, delete: del } = props
const { START, CENTER, END } = Gtk.Align const { START, CENTER, END } = Gtk.Align
return <eventbox return <box vertical
className={`Notification ${urgency(n)}`} cssClasses={['Notification', urgency(n)]}>
setup={setup} <box cssName="header">
onHoverLost={onHoverLost}> {(n.appIcon || n.desktopEntry) ? <Gtk.Image
<box vertical> cssName="app-icon"
<box className="header">
{(n.appIcon || n.desktopEntry) && <icon
className="app-icon"
visible={Boolean(n.appIcon || n.desktopEntry)} visible={Boolean(n.appIcon || n.desktopEntry)}
icon={n.appIcon || n.desktopEntry} iconName={n.appIcon || n.desktopEntry}
/>} /> : <Gtk.Image iconName={'window-close-symbolic'}></Gtk.Image>}
<label <label
className="app-name" cssName="app-name"
halign={START} halign={START}
truncate // ellipsize={Pango.EllipsizeMode.END}
label={n.appName || "Unknown"} label={n.appName || "Unknown"}
/> />
<label <label
className="time" cssName="time"
hexpand hexpand
halign={END} halign={END}
label={time(n.time)} label={time(n.time)}
/> />
<button onClicked={() => del( id, instance )}> <button onClicked={() => del( id )}>
<icon icon="window-close-symbolic" /> <Gtk.Image iconName="window-close-symbolic" />
</button> </button>
</box> </box>
<Gtk.Separator visible /> <Gtk.Separator visible />
<box className="content"> <box cssName="content">
{n.image && fileExists(n.image) && <box {n.image && fileExists(n.image) ? <box
valign={START} valign={START}
className="image" cssName="image">
css={`background-image: url('${n.image}')`} <Gtk.Image file={n.image}></Gtk.Image>
/>} </box>
{n.image && isIcon(n.image) && <box : <box></box>}
{n.image ? <box
expand={false} expand={false}
valign={START} valign={START}
className="icon-image"> className="icon-image">
<icon icon={n.image} expand halign={CENTER} valign={CENTER} /> <Gtk.Image iconName={n.image} expand halign={CENTER} valign={CENTER} />
</box>} </box>
: <box></box>}
<box vertical> <box vertical>
<label <Gtk.Label
className="summary" cssName="summary"
halign={START} halign={START}
xalign={0} xalign={0}
label={n.summary} label={n.summary}
truncate // ellipsize={Pango.EllipsizeMode.END}
/> />
{n.body && <label {n.body ? <label
className="body" cssName="body"
wrap wrap
useMarkup useMarkup
halign={START} halign={START}
xalign={0} xalign={0}
justifyFill
label={n.body} label={n.body}
/>} /> : <label></label>}
</box> </box>
</box> </box>
{n.get_actions().length > 0 && <box className="actions"> {n.get_actions().length > 0 ? <box cssName="actions">
{n.get_actions().map(({ label, id }) => ( {n.get_actions().map(({ label, id }) => (
<button <button
hexpand hexpand
@ -106,7 +98,6 @@ export default function Notification(props: Props) {
<label label={label} halign={CENTER} hexpand /> <label label={label} halign={CENTER} hexpand />
</button> </button>
))} ))}
</box>} </box> : <box></box>}
</box> </box>
</eventbox>
} }

View File

@ -1,20 +1,22 @@
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
$fg-color: #{"@theme_fg_color"}; /* $fg-color: #{"@theme_fg_color"}; */
$bg-color: #{"@theme_bg_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; */
/* } */
/* } */
window.Bar { @use './components/notifications/notifications.scss'
background: transparent;
color: $fg-color;
font-weight: bold;
>centerbox {
background: $bg-color;
border-radius: 10px;
margin: 8px;
}
button {
border-radius: 8px;
margin: 2px;
}
}

View File

@ -9,6 +9,6 @@
// "checkJs": true, // "checkJs": true,
// "allowJs": true, // "allowJs": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "astal/gtk3", "jsxImportSource": "astal/gtk4",
} }
} }

View File

@ -34,6 +34,8 @@
- [ ] Rofi - [ ] Rofi
- [ ] Spotlight-Search (or replace with anyrun) - [ ] Spotlight-Search (or replace with anyrun)
- [ ] Wallpaper selector (that automatically triggers the theming script) - [ ] Wallpaper selector (that automatically triggers the theming script)
- [ ] Kitty
- [ ] Configure colours
- [ ] Hyprland - [ ] Hyprland
- [ ] Keybinds: Resize window, move window, open calculator, plus more programs - [ ] Keybinds: Resize window, move window, open calculator, plus more programs
- [ ] Read docs - [ ] Read docs
@ -42,9 +44,12 @@
- [ ] New image viewer (eog) - [ ] New image viewer (eog)
- [ ] Other pdf reader (maybe -> zathura) - [ ] Other pdf reader (maybe -> zathura)
- [ ] Maybe TUI archive manager (felix-rs) - [ ] Maybe TUI archive manager (felix-rs)
- [ ] Lazygit: Configure - [ ] Lazygit
- [x] Nvim (other repo) - [ ] Configure
- [ ] Nvim (other repo)
- [x] Replace notification handler (noice) - [x] Replace notification handler (noice)
- [ ] Configure formatters (of Java, Cpp, TS/JS/Vue, Python)
- [ ] Maybe: Add extra configs to commentbox
- [ ] Yazi - [ ] Yazi
- [ ] More keybinds - [ ] More keybinds
- [ ] Configure - [ ] Configure
@ -57,7 +62,8 @@
- [ ] Theming script - [ ] Theming script
- [ ] Installer for configs - [ ] Installer for configs
- [ ] Vivado cleanup (run after vivado and hope vivado is blocking (or simply execute vivado in /tmp)) - [ ] Vivado cleanup (run after vivado and hope vivado is blocking (or simply execute vivado in /tmp))
- [ ] migrate to zoxide from autojump - [x] migrate to zoxide from autojump
- [ ] properly swap escape and caps (at lowest level possible)
Using astal (https://aylur.github.io/astal, which is gjs based), write a component that takes as argument a UIComponent array (pre-defined interface) and depending on what subclass of that interface it is renders a different component for each element. Using astal (https://aylur.github.io/astal, which is gjs based), write a component that takes as argument a UIComponent array (pre-defined interface) and depending on what subclass of that interface it is renders a different component for each element.