Upgrade to Agsv2 + Astal (#533)

* migrate to astal

* Reorganize project structure.

* progress

* Migrate Dashboard and Window Title modules.

* Migrate clock and notification bar modules.

* Remove unused code

* Media menu

* Rework network and volume modules

* Finish custom modules.

* Migrate battery bar module.

* Update battery module and organize helpers.

* Migrate workspace module.

* Wrap up bar modules.

* Checkpoint before I inevitbly blow something up.

* Updates

* Fix event propagation logic.

* Type fixes

* More type fixes

* Fix padding for event boxes.

* Migrate volume menu and refactor scroll event handlers.

* network module WIP

* Migrate network service.

* Migrate bluetooth menu

* Updates

* Migrate notifications

* Update scrolling behavior for custom modules.

* Improve popup notifications and add timer functionality.

* Migration notifications menu header/controls.

* Migrate notifications menu and consolidate notifications menu code.

* Migrate power menu.

* Dashboard progress

* Migrate dashboard

* Migrate media menu.

* Reduce media menu nesting.

* Finish updating media menu bindings to navigate active player.

* Migrate battery menu

* Consolidate code

* Migrate calendar menu

* Fix workspace logic to update on client add/change/remove and consolidate code.

* Migrate osd

* Consolidate hyprland service connections.

* Implement startup dropdown menu position allocation.

* Migrate settings menu (WIP)

* Settings dialo menu fixes

* Finish Dashboard menu

* Type updates

* update submoldule for types

* update github ci

* ci

* Submodule update

* Ci updates

* Remove type checking for now.

* ci fix

* Fix a bunch of stuff, losing track... need rest. Brb coffee

* Validate dropdown menu before render.

* Consolidate code and add auto-hide functionality.

* Improve auto-hide behavior.

* Consolidate audio menu code

* Organize bluetooth code

* Improve active player logic

* Properly dismiss a notification on action button resolution.

* Implement CLI command engine and migrate CLI commands.

* Handle variable disposal

* Bar component fixes and add hyprland startup rules.

* Handle potentially null bindings network and bluetooth bindings.

* Handle potentially null wired adapter.

* Fix GPU stats

* Handle poller for GPU

* Fix gpu bar logic.

* Clean up logic for stat bars.

* Handle wifi and wired bar icon bindings.

* Fix battery percentages

* Fix switch behavior

* Wifi staging fixes

* Reduce redundant hyprland service calls.

* Code cleanup

* Document the option code and reduce redundant calls to optimize performance.

* Remove outdated comment.

* Add JSDocs

* Add meson to build hyprpanel

* Consistency updates

* Organize commands

* Fix images not showing up on notifications.

* Remove todo

* Move hyprpanel configuration to the ~/.config/hyprpanel directory and add utility commands.

* Handle SRC directory for the bundled/built hyprpanel.

* Add namespaces to all windows

* Migrate systray

* systray updates

* Update meson to include ts, tsx and scss files.

* Remove log from meson

* Fix file choose path and make it float.

* Added a command to check the dependency status

* Update dep names.

* Get scale directly from env

* Add todo
This commit is contained in:
Jas Singh
2024-12-20 18:10:10 -08:00
committed by GitHub
parent 955eed6c60
commit 2ffd602910
605 changed files with 19543 additions and 15999 deletions

View File

@@ -0,0 +1,61 @@
import { execAsync } from 'astal';
import { Gtk } from 'astal/gtk3';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import { isPrimaryClick } from 'src/lib/utils';
const ActionButton = ({ notification, action }: ActionButtonProps): JSX.Element => {
return (
<button
className={'notification-action-buttons'}
hexpand
onClick={(_, event) => {
if (!isPrimaryClick(event)) {
return;
}
if (action.id.includes('scriptAction:-')) {
execAsync(`${action.id.replace('scriptAction:-', '')}`).catch((err) => console.error(err));
notification.dismiss();
} else {
notification.invoke(action.id);
notification.dismiss();
}
}}
>
<box halign={Gtk.Align.CENTER} hexpand>
<label
className={'notification-action-buttons-label'}
label={action.label}
hexpand
max_width_chars={15}
truncate
wrap
/>
</box>
</button>
);
};
export const Actions = ({ notification, showActions }: ActionProps): JSX.Element => {
return (
<revealer transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} revealChild={showActions ? false : true}>
<eventbox>
<box className={'notification-card-actions'} hexpand valign={Gtk.Align.END}>
{notification.get_actions().map((action) => {
return <ActionButton notification={notification} action={action} />;
})}
</box>
</eventbox>
</revealer>
);
};
interface ActionProps {
notification: AstalNotifd.Notification;
showActions: boolean;
}
interface ActionButtonProps {
notification: AstalNotifd.Notification;
action: AstalNotifd.Action;
}

View File

@@ -0,0 +1,27 @@
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import { Gtk } from 'astal/gtk3';
import { notifHasImg } from './helpers';
export const Body = ({ notification }: BodyProps): JSX.Element => {
return (
<box className={'notification-card-body'} valign={Gtk.Align.START} hexpand>
<label
className={'notification-card-body-label'}
halign={Gtk.Align.START}
label={notification.body}
maxWidthChars={!notifHasImg(notification) ? 35 : 28}
lines={2}
truncate
wrap
justify={Gtk.Justification.LEFT}
hexpand
useMarkup
onRealize={(self) => self.set_markup(notification.body)}
/>
</box>
);
};
interface BodyProps {
notification: AstalNotifd.Notification;
}

View File

@@ -0,0 +1,19 @@
import { Gtk } from 'astal/gtk3';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
export const CloseButton = ({ notification }: CloseButtonProps): JSX.Element => {
return (
<button
className={'close-notification-button'}
onClick={() => {
notification.dismiss();
}}
>
<label className={'txt-icon notification-close'} label={'󰅜'} halign={Gtk.Align.CENTER}></label>
</button>
);
};
interface CloseButtonProps {
notification: AstalNotifd.Notification;
}

View File

@@ -0,0 +1,64 @@
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import options from 'src/options.js';
import { GLib } from 'astal/gobject.js';
import { Gtk } from 'astal/gtk3';
import { getNotificationIcon } from 'src/globals/notification.js';
import { notifHasImg } from './helpers';
const { military } = options.menus.clock.time;
export const NotificationIcon = ({ notification }: HeaderProps): JSX.Element => {
const { appName, appIcon, desktopEntry } = notification;
return (
<box className={'notification-card-header'} halign={Gtk.Align.START}>
<box css={'min-width: 2rem; min-height: 2rem; '}>
<icon className={'notification-icon'} icon={getNotificationIcon(appName, appIcon, desktopEntry)} />
</box>
</box>
);
};
export const SummaryLabel = ({ notification }: HeaderProps): JSX.Element => {
return (
<box className={'notification-card-header'} halign={Gtk.Align.START} valign={Gtk.Align.START} hexpand>
<label
className={'notification-card-header-label'}
halign={Gtk.Align.START}
onRealize={(self) => self.set_markup(notification.summary)}
label={notification.summary}
maxWidthChars={!notifHasImg(notification) ? 30 : 19}
hexpand
vexpand
truncate
wrap
/>
</box>
);
};
export const TimeLabel = ({ notification }: HeaderProps): JSX.Element => {
const time = (time: number, format = '%I:%M %p'): string => {
return GLib.DateTime.new_from_unix_local(time).format(military.get() ? '%H:%M' : format) || '--';
};
return (
<box className={'notification-card-header menu'} halign={Gtk.Align.END} valign={Gtk.Align.START} hexpand>
<label className={'notification-time'} label={time(notification.time)} vexpand />
</box>
);
};
export const Header = ({ notification }: HeaderProps): JSX.Element => {
return (
<box vertical={false} hexpand>
<NotificationIcon notification={notification} />
<SummaryLabel notification={notification} />
<TimeLabel notification={notification} />
</box>
);
};
interface HeaderProps {
notification: AstalNotifd.Notification;
}

View File

@@ -0,0 +1,48 @@
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import { Gtk } from 'astal/gtk3';
import { notifHasImg } from './helpers';
import { isAnImage } from 'src/lib/utils';
const ImageItem = ({ notification }: ImageProps): JSX.Element => {
if (notification.appIcon && !isAnImage(notification.appIcon)) {
return (
<icon
className={'notification-card-image icon'}
halign={Gtk.Align.CENTER}
vexpand={false}
icon={notification.appIcon}
/>
);
}
return (
<box
className={'notification-card-image'}
halign={Gtk.Align.CENTER}
vexpand={false}
css={`
background-image: url('${notification.image || notification.appIcon}');
`}
/>
);
};
export const Image = ({ notification }: ImageProps): JSX.Element => {
if (!notifHasImg(notification)) {
return <box />;
}
return (
<box
className={'notification-card-image-container'}
halign={Gtk.Align.CENTER}
valign={Gtk.Align.CENTER}
vexpand={false}
>
<ImageItem notification={notification} />
</box>
);
};
interface ImageProps {
notification: AstalNotifd.Notification;
}

View File

@@ -0,0 +1,65 @@
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import { Actions } from './Actions';
import { Body } from './Body';
import { CloseButton } from './CloseButton';
import { Header } from './Header';
import { Image } from './Image';
import { Gtk, Widget } from 'astal/gtk3';
import { isSecondaryClick } from 'src/lib/utils';
import { notifHasImg } from './helpers';
const NotificationContent = ({ actionBox, notification }: NotificationContentProps): JSX.Element => {
return (
<box className={`notification-card-content ${!notifHasImg(notification) ? 'noimg' : ''}`} hexpand vertical>
<Header notification={notification} />
<Body notification={notification} />
{actionBox}
</box>
);
};
export const NotificationCard = ({ notification, showActions, ...props }: NotificationCardProps): JSX.Element => {
const actionBox: IActionBox | null = notification.get_actions().length ? (
<Actions notification={notification} showActions={showActions} />
) : null;
return (
<eventbox
onClick={(_, event) => {
if (isSecondaryClick(event)) {
notification.dismiss();
}
}}
onHover={() => {
if (actionBox !== null && showActions === true) {
actionBox.revealChild = true;
}
}}
onHoverLost={() => {
if (actionBox !== null && showActions === true) {
actionBox.revealChild = false;
}
}}
>
<box className={'notification-card'} {...props} hexpand valign={Gtk.Align.START}>
<Image notification={notification} />
<NotificationContent notification={notification} actionBox={actionBox} />
<CloseButton notification={notification} />
</box>
</eventbox>
);
};
interface NotificationCardProps extends Widget.BoxProps {
notification: AstalNotifd.Notification;
showActions: boolean;
}
interface IActionBox extends Gtk.Widget {
revealChild?: boolean;
}
interface NotificationContentProps {
actionBox: IActionBox | null;
notification: AstalNotifd.Notification;
}

View File

@@ -0,0 +1,87 @@
import { bind, timeout, Variable } from 'astal';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
import options from 'src/options';
import { hyprlandService, notifdService } from 'src/lib/constants/services';
import { isNotificationIgnored } from 'src/lib/shared/notifications';
const { ignore, timeout: popupTimeout } = options.notifications;
/**
* Checks if a notification has an image.
*
* This function determines whether the provided notification contains an image by checking the `image` property.
*
* @param notification The notification object to check.
*
* @returns True if the notification has an image, false otherwise.
*/
export const notifHasImg = (notification: AstalNotifd.Notification): boolean => {
return (notification.image && notification.image.length) || notification.appIcon ? true : false;
};
/**
* Tracks the active monitor and updates the provided variable.
*
* This function sets up a derived variable that updates the `curMonitor` variable with the ID of the focused monitor.
*
* @param curMonitor The variable to update with the active monitor ID.
*/
export const trackActiveMonitor = (curMonitor: Variable<number>): void => {
Variable.derive([bind(hyprlandService, 'focusedMonitor')], (monitor) => {
curMonitor.set(monitor.id);
});
};
/**
* Tracks popup notifications and updates the provided variable.
*
* This function connects to the `notified` and `resolved` signals of the `notifdService` to manage popup notifications.
* It updates the `popupNotifications` variable with the current list of notifications and handles dismissing notifications based on the timeout.
*
* @param popupNotifications The variable to update with the list of popup notifications.
*/
export const trackPopupNotifications = (popupNotifications: Variable<AstalNotifd.Notification[]>): void => {
notifdService.connect('notified', (_, id) => {
const notification = notifdService.get_notification(id);
const doNotDisturb = notifdService.dontDisturb;
if (isNotificationIgnored(notification, ignore.get())) {
notification.dismiss();
return;
}
if (doNotDisturb) {
return;
}
popupNotifications.set([...popupNotifications.get(), notification]);
timeout(popupTimeout.get(), () => {
dropNotificationPopup(notification, popupNotifications);
});
});
notifdService.connect('resolved', (_, id) => {
const filteredPopups = popupNotifications.get().filter((popupNotif) => popupNotif.id !== id);
popupNotifications.set(filteredPopups);
});
};
/**
* Dismisses a notification popup and updates the provided variable.
*
* This function removes the specified notification from the list of popup notifications and updates the `popupNotifications` variable.
*
* @param notificationToDismiss The notification to dismiss.
* @param popupNotifications The variable to update with the list of popup notifications.
*/
const dropNotificationPopup = (
notificationToDismiss: AstalNotifd.Notification,
popupNotifications: Variable<AstalNotifd.Notification[]>,
): void => {
const currentPopups = popupNotifications.get();
const undismissedNotifications = currentPopups.filter((popupNotif) => popupNotif.id !== notificationToDismiss.id);
popupNotifications.set(undismissedNotifications);
};

View File

@@ -0,0 +1,63 @@
import { hyprlandService } from 'src/lib/constants/services.js';
import options from 'src/options.js';
import { getPosition } from 'src/lib/utils.js';
import Variable from 'astal/variable.js';
import { bind } from 'astal/binding.js';
import { trackActiveMonitor, trackPopupNotifications } from './helpers.js';
import { Astal } from 'astal/gtk3';
import { NotificationCard } from './Notification.js';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
const { position, monitor, active_monitor, showActionsOnHover, displayedTotal } = options.notifications;
const { tear } = options;
const curMonitor = Variable(monitor.get());
const popupNotifications: Variable<AstalNotifd.Notification[]> = Variable([]);
trackActiveMonitor(curMonitor);
trackPopupNotifications(popupNotifications);
export default (): JSX.Element => {
const windowLayer = bind(tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY));
const windowAnchor = bind(position).as(getPosition);
const windowMonitor = Variable.derive(
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
(focusedMonitor, monitor, activeMonitor) => {
if (activeMonitor === true) {
return focusedMonitor.id;
}
return monitor;
},
);
const notificationsBinding = Variable.derive(
[bind(popupNotifications), bind(showActionsOnHover)],
(notifications, showActions) => {
const maxDisplayed = notifications.slice(0, displayedTotal.get());
return maxDisplayed.map((notification) => {
return <NotificationCard notification={notification} showActions={showActions} />;
});
},
);
return (
<window
name={'notifications-window'}
namespace={'notifications-window'}
className={'notifications-window'}
layer={windowLayer}
anchor={windowAnchor}
exclusivity={Astal.Exclusivity.NORMAL}
monitor={windowMonitor()}
onDestroy={() => {
windowMonitor.drop();
notificationsBinding.drop();
}}
>
<box vertical hexpand className={'notification-card-container'}>
{notificationsBinding()}
</box>
</window>
);
};