feat(agsV2): fix breaking changes and add notification impl
All checks were successful
Discord / discord commits (push) Has been skipped

This commit is contained in:
matt1432 2024-10-15 20:01:20 -04:00
parent 5ab80867e1
commit 5481ec544b
21 changed files with 540 additions and 28 deletions

View file

@ -32,7 +32,7 @@
packages,
pname,
}: let
inherit (pkgs.lib) concatMapStrings elemAt filter hasAttr length map optionalString splitString toLower;
inherit (pkgs.lib) concatMapStrings elemAt length map optionalString splitString toLower;
withGirNames =
map (package: {
@ -40,9 +40,13 @@
girName =
if package.pname == "astal-wireplumber"
then "AstalWp-0.1"
else if package.name == "astal-0.1.0"
then "AstalIO-0.1"
else if package.name == "astal-3.0.0"
then "Astal-3.0"
else (concatMapStrings capitalise (splitString "-" package.pname)) + "-0.1";
})
(filter (hasAttr "pname") packages);
packages;
in {
"${configPath}${optionalString (length packages == 1) "/${toLower (elemAt withGirNames 0).girName}"}".source =
pkgs.callPackage

View file

@ -1,10 +1,11 @@
import { App } from 'astal';
import { App } from 'astal/gtk3';
import style from './style.scss';
import Bar from './widgets/bar/wim';
import BgFade from './widgets/bg-fade/main';
import Corners from './widgets/corners/main';
import { NotifPopups } from './widgets/notifs/main';
App.start({
@ -14,5 +15,6 @@ App.start({
Bar();
BgFade();
Corners();
NotifPopups();
},
});

View file

@ -9,7 +9,7 @@ self: {
inherit (self.inputs) agsV2;
agsV2Packages = agsV2.packages.${pkgs.system};
astalLibs = attrValues (removeAttrs agsV2.inputs.astal.packages.${pkgs.system} ["docs"]);
astalLibs = attrValues (removeAttrs agsV2.inputs.astal.packages.${pkgs.system} ["docs" "gjs"]);
configDir = "/home/matt/.nix/nixosModules/ags/v2";
in {
home = {
@ -42,21 +42,19 @@ self: {
"${configDir}/tsconfig.json".source = pkgs.writers.writeJSON "tsconfig.json" {
"$schema" = "https://json.schemastore.org/tsconfig";
"compilerOptions" = {
"experimentalDecorators" = true;
"strict" = true;
"target" = "ES2023";
"moduleResolution" = "Bundler";
"jsx" = "react-jsx";
"jsxImportSource" = "${agsV2Packages.gjs}/share/astal/gjs/gtk3";
"paths" = {
"astal" = ["${agsV2Packages.gjs}/share/astal/gjs"];
"astal/*" = ["${agsV2Packages.gjs}/share/astal/gjs/*"];
};
"skipLibCheck" = true;
"module" = "ES2022";
"lib" = ["ES2023"];
"strict" = true;
"moduleResolution" = "Bundler";
"skipLibCheck" = true;
"checkJs" = true;
"allowJs" = true;
"experimentalDecorators" = true;
"jsx" = "react-jsx";
"jsxImportSource" = "${agsV2Packages.astal3}/share/astal/gjs/src/jsx";
"paths" = {
"astal" = ["${agsV2Packages.astal3}/share/astal/gjs"];
"astal/*" = ["${agsV2Packages.astal3}/share/astal/gjs/src/*"];
};
};
};
}

View file

@ -1,4 +1,4 @@
import { Gdk } from 'astal';
import { Gdk } from 'astal/gtk3';
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
const Hyprland = AstalHyprland.get_default();

View file

@ -45,3 +45,4 @@ window {
}
@import 'widgets/bar/style.scss';
@import 'widgets/notifs/style.scss';

View file

@ -1,4 +1,5 @@
import { Astal, bind, Gdk, Gtk, Variable, Widget } from 'astal';
import { Astal, Gdk, Gtk, Widget } from 'astal/gtk3';
import { bind, Variable } from 'astal';
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
const Hyprland = AstalHyprland.get_default();

View file

@ -1,4 +1,5 @@
import { App, bind, Gdk, Gtk, idle, Widget } from 'astal';
import { App, Gdk, Gtk, Widget } from 'astal/gtk3';
import { bind, idle } from 'astal';
import AstalTray from 'gi://AstalTray';
const Tray = AstalTray.get_default();

View file

@ -1,4 +1,5 @@
import { Gtk, timeout, Widget } from 'astal';
import { Gtk, Widget } from 'astal/gtk3';
import { timeout } from 'astal';
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
const Hyprland = AstalHyprland.get_default();

View file

@ -1,4 +1,4 @@
import { Astal, Gtk } from 'astal';
import { Astal, Gtk } from 'astal/gtk3';
import Battery from './items/battery';
import Clock from './items/clock';

View file

@ -1,4 +1,4 @@
import { Astal } from 'astal';
import { Astal } from 'astal/gtk3';
export default () => {

View file

@ -1,4 +1,4 @@
import { Astal } from 'astal';
import { Astal } from 'astal/gtk3';
import RoundedCorner from './screen-corners';

View file

@ -1,4 +1,4 @@
import { Gtk } from 'astal';
import { Gtk } from 'astal/gtk3';
import Cairo from 'cairo';

View file

@ -1,5 +1,6 @@
import { Astal, Binding, idle, Widget } from 'astal';
import { Astal, Widget } from 'astal/gtk3';
import { register, property } from 'astal/gobject';
import { Binding, idle } from 'astal';
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
const Hyprland = AstalHyprland.get_default();

View file

@ -1,4 +1,4 @@
import { Widget } from 'astal';
import { Widget } from 'astal/gtk3';
export default ({

View file

@ -0,0 +1,249 @@
import { Gdk, Gtk, Widget } from 'astal/gtk3';
import { register, property } from 'astal/gobject';
import { idle } from 'astal';
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
const Notifications = AstalNotifd.get_default();
import { HasNotifs } from './notification';
const display = Gdk.Display.get_default();
const MAX_OFFSET = 200;
const OFFSCREEN = 300;
const ANIM_DURATION = 500;
const SLIDE_MIN_THRESHOLD = 10;
const TRANSITION = 'transition: margin 0.5s ease, opacity 0.5s ease;';
const MAX_LEFT = `
margin-left: -${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: ${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const MAX_RIGHT = `
margin-left: ${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: -${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const slideLeft = `${TRANSITION} ${MAX_LEFT} opacity: 0;`;
const slideRight = `${TRANSITION} ${MAX_RIGHT} opacity: 0;`;
const defaultStyle = `${TRANSITION} margin: unset; opacity: 1;`;
type NotifGestureWrapperProps = Widget.BoxProps & {
id: number
slide_in_from?: 'Left' | 'Right'
};
@register()
class NotifGestureWrapper extends Widget.EventBox {
readonly id: number;
readonly slide_in_from: 'Left' | 'Right';
readonly slideAway: (side: 'Left' | 'Right') => void;
@property(Boolean)
declare hovered: boolean;
@property(Boolean)
declare dragging: boolean;
constructor({
id,
slide_in_from = 'Left',
...rest
}: NotifGestureWrapperProps) {
super();
this.id = id;
this.dragging = false;
this.hovered = false;
this.slide_in_from = slide_in_from;
this.slideAway = (side: 'Left' | 'Right', force = false) => {
((this.get_child() as Widget.Revealer).get_child() as Widget.Box)
.css = side === 'Left' ? slideLeft : slideRight;
// Make it uninteractable
this.sensitive = false;
setTimeout(() => {
(this.get_child() as Widget.Revealer).revealChild = false;
setTimeout(() => {
// Kill notif and update HasNotifs after anim is done
if (force) {
Notifications.get_notification(this.id)?.dismiss();
}
HasNotifs.set(Notifications.get_notifications().length > 0);
this.destroy();
}, ANIM_DURATION);
}, ANIM_DURATION - 100);
};
this.connect('notify::dragging', () => {
if (!this.hovered && this.dragging) {
this.hovered = true;
}
});
// OnClick
this.connect('button-press-event', () => {
if (!display) {
return;
}
this.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grabbing',
));
if (!this.hovered) {
this.hovered = true;
}
});
// OnRelease
this.connect('button-release-event', () => {
if (!display) {
return;
}
this.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grab',
));
if (!this.hovered) {
this.hovered = true;
}
});
// OnHover
this.connect('enter-notify-event', () => {
if (!display) {
return;
}
this.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grab',
));
if (!this.hovered) {
this.hovered = true;
}
});
// OnHoverLost
this.connect('leave-notify-event', () => {
if (!display) {
return;
}
this.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grab',
));
if (this.hovered) {
this.hovered = false;
}
});
const gesture = Gtk.GestureDrag.new(this);
this.add(
<revealer
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
transitionDuration={500}
revealChild={false}
>
<box
{...rest}
setup={(self) => {
self
// When dragging
.hook(gesture, 'drag-update', () => {
let offset = gesture.get_offset()[1];
if (!offset || offset === 0) {
return;
}
// Slide right
if (offset > 0) {
self.css = `
opacity: 1; transition: none;
margin-left: ${offset}px;
margin-right: -${offset}px;
`;
}
// Slide left
else {
offset = Math.abs(offset);
self.css = `
opacity: 1; transition: none;
margin-right: ${offset}px;
margin-left: -${offset}px;
`;
}
// Put a threshold on if a click is actually dragging
this.dragging = Math.abs(offset) > SLIDE_MIN_THRESHOLD;
if (!display) {
return;
}
this.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grabbing',
));
})
// On drag end
.hook(gesture, 'drag-end', () => {
const offset = gesture.get_offset()[1];
if (!offset) {
return;
}
// If crosses threshold after letting go, slide away
if (Math.abs(offset) > MAX_OFFSET) {
if (offset > 0) {
this.slideAway('Right');
}
else {
this.slideAway('Left');
}
}
else {
self.css = defaultStyle;
this.dragging = false;
if (!display) {
return;
}
this.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grab',
));
}
});
// Reverse of slideAway, so it started at squeeze, then we go to slide
self.css = this.slide_in_from === 'Left' ?
slideLeft :
slideRight;
idle(() => {
(self.get_parent() as Widget.Revealer).revealChild = true;
setTimeout(() => {
// Then we go to center
self.css = defaultStyle;
}, ANIM_DURATION);
});
}}
/>
</revealer>,
);
}
}
export default (props: NotifGestureWrapperProps) => new NotifGestureWrapper(props);

View file

@ -0,0 +1,14 @@
import { Astal } from 'astal/gtk3';
import Popups from './popups';
export const NotifPopups = () => (
<window
name="notifications"
layer={Astal.Layer.OVERLAY}
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT}
>
<Popups />
</window>
);

View file

@ -0,0 +1,160 @@
import { App, Gtk } from 'astal/gtk3';
import { Variable } from 'astal';
import GLib from 'gi://GLib?version=2.0';
import AstalApps from 'gi://AstalApps?version=0.1';
const Applications = AstalApps.Apps.new();
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
const Notifications = AstalNotifd.get_default();
import NotifGestureWrapper from './gesture';
// Make a variable to connect to for Widgets
// to know when there are notifs or not
export const HasNotifs = Variable(false);
const setTime = (time: number): string => {
return GLib.DateTime
.new_from_unix_local(time)
.format('%H:%M') ?? '';
};
const NotifIcon = ({ notifObj }: { notifObj: AstalNotifd.Notification }) => {
let icon: string;
if (notifObj.get_image() !== '') {
icon = notifObj.get_image();
App.add_icons(icon);
}
else if (notifObj.get_app_icon() !== '') {
icon = notifObj.get_app_icon();
}
else {
icon = Applications.query(
notifObj.get_app_name(),
false,
)[0].get_icon_name();
}
return (
<box
valign={Gtk.Align.CENTER}
className="icon"
css={`
min-width: 78px;
min-height: 78px;
`}
>
<icon
icon={icon}
css="font-size: 58px;"
halign={Gtk.Align.CENTER}
hexpand
valign={Gtk.Align.CENTER}
vexpand
/>
</box>
);
};
const BlockedApps = [
'Spotify',
];
export const Notification = ({
id = 0,
}): ReturnType<typeof NotifGestureWrapper> | undefined => {
const notifObj = Notifications.get_notification(id);
if (!notifObj) {
return;
}
if (BlockedApps.find((app) => app === notifObj.app_name)) {
notifObj.dismiss();
return;
}
HasNotifs.set(Notifications.get_notifications().length > 0);
return (
<NotifGestureWrapper id={id}>
<box vertical className={`notification ${notifObj.urgency} widget`}>
{/* Content */}
<box>
<NotifIcon notifObj={notifObj} />
{/* Top of Content */}
<box vertical css="min-width: 400px">
<box>
{/* Title */}
<label
className="title"
halign={Gtk.Align.START}
valign={Gtk.Align.END}
xalign={0}
hexpand
max_width_chars={24}
truncate
wrap
label={notifObj.summary}
use_markup={notifObj.summary.startsWith('<')}
/>
{/* Time */}
<label
className="time"
valign={Gtk.Align.CENTER}
halign={Gtk.Align.END}
label={setTime(notifObj.time)}
/>
{/* Close button */}
<button
className="close-button"
valign={Gtk.Align.START}
halign={Gtk.Align.END}
onButtonReleaseEvent={() => {
notifObj.dismiss();
}}
>
<icon icon="window-close-symbolic" />
</button>
</box>
{/* Description */}
<label
className="description"
hexpand
use_markup
xalign={0}
label={notifObj.body}
wrap
/>
</box>
</box>
{/* Actions */}
<box className="actions">
{notifObj.get_actions().map((action) => (
<button
className="action-button"
hexpand
onButtonReleaseEvent={() => notifObj.invoke(action.id)}
>
<label label={action.label} />
</button>
))}
</box>
</box>
</NotifGestureWrapper>
) as ReturnType<typeof NotifGestureWrapper>;
};

View file

@ -0,0 +1,48 @@
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
const Notifications = AstalNotifd.get_default();
import { Notification } from './notification';
export default () => (
<box
// Needed so it occupies space at the start
css="padding: 1px;"
vertical
setup={(self) => {
const NotifsMap = new Map<number, ReturnType<typeof Notification>>();
const addPopup = (id: number) => {
if (!id) {
return;
}
const NewNotif = Notification({ id });
if (NewNotif) {
// Use this instead of add to put it at the top
self.pack_end(NewNotif, false, false, 0);
self.show_all();
NotifsMap.set(id, NewNotif);
}
};
const handleResolved = (id: number) => {
const notif = NotifsMap.get(id);
if (!notif) {
return;
}
notif.slideAway('Left');
NotifsMap.delete(id);
};
self
.hook(Notifications, 'notified', (_, id) => addPopup(id))
.hook(Notifications, 'resolved', (_, id) => handleResolved(id));
}}
/>
);

View file

@ -0,0 +1,32 @@
.notification.widget {
// urgencies
// &.urgency ...
.icon {
margin-right: 10px;
}
.time {
margin: 3px;
}
.close-button {
margin: 3px;
}
.title {
font-weight: 800;
font-size: 20px;
margin-bottom: 5px;
}
.description {
margin-bottom: 5px;
}
.actions {
margin: 3px;
.action-button {}
}
}

View file

@ -14,10 +14,10 @@ Show Inline Previews=true
Show Preview=false
Show Speedbar=true
Show hidden files=true
Sort by=Name
Sort by=Date
Sort directories first=true
Sort hidden files last=false
Sort reversed=false
Sort reversed=true
Speedbar Width=189
View Style=DetailTree