ags(bar, notifications, control-center): add status icons to bar, notification history fixed, notification history below control-center

This commit is contained in:
retrozinndev
2025-03-23 10:17:22 -03:00
parent 30d1fded84
commit 3db477598f
29 changed files with 746 additions and 363 deletions
+53 -34
View File
@@ -2,6 +2,8 @@ import { Astal, Gtk, Widget } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
import { Separator } from "./Separator";
import Pango from "gi://Pango";
import { HistoryNotification } from "../scripts/notifications";
import { GLib } from "astal";
export function getUrgencyString(notif: AstalNotifd.Notification) {
switch(notif.urgency) {
@@ -14,24 +16,29 @@ export function getUrgencyString(notif: AstalNotifd.Notification) {
return "normal";
}
export function NotificationWidget(notification: AstalNotifd.Notification|number,
onClose?: (notif: AstalNotifd.Notification) => void): Gtk.Widget {
export function NotificationWidget(notification: AstalNotifd.Notification|number|HistoryNotification,
onClose?: (notif: AstalNotifd.Notification|HistoryNotification) => void,
showTime?: boolean /* It's showTime :speaking_head: :boom: :bangbang: */): Gtk.Widget {
notification = (notification instanceof AstalNotifd.Notification) ?
notification
: AstalNotifd.get_default().get_notification(notification);
notification = (typeof notification === "number") ?
AstalNotifd.get_default().get_notification(notification)
: notification;
return new Widget.EventBox({
onClick: () => {
if(notification.actions.length >= 1 && notification.actions[0].label.toLowerCase() === "view") {
notification.invoke(notification.actions[0]!.id);
onClose && onClose(notification);
if(notification instanceof AstalNotifd.Notification) {
const viewAction = notification.actions.filter(action => action.label.toLowerCase() === "view")?.[0];
if(viewAction) notification.invoke(viewAction.id);
}
onClose && onClose(notification);
},
hexpand: true,
vexpand: false,
child: new Widget.Box({
className: `notification ${getUrgencyString(notification)}`,
className: `notification ${ (notification instanceof AstalNotifd.Notification) ? getUrgencyString(notification) : "" }`,
homogeneous: false,
expand: false,
expand: true,
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Box({
@@ -42,7 +49,7 @@ export function NotificationWidget(notification: AstalNotifd.Notification|number
children: [
new Widget.Icon({
className: "icon app-icon",
icon: Astal.Icon.lookup_icon(notification.appIcon) ?
icon: (notification instanceof AstalNotifd.Notification) && Astal.Icon.lookup_icon(notification.appIcon) ?
notification.appIcon
: (Astal.Icon.lookup_icon(notification.appName.toLowerCase()) ?
notification.appName.toLowerCase()
@@ -59,15 +66,25 @@ export function NotificationWidget(notification: AstalNotifd.Notification|number
hexpand: true,
label: notification.appName || "Unknown Application"
} as Widget.LabelProps),
new Widget.Button({
className: "close nf",
new Widget.Box({
halign: Gtk.Align.END,
onClick: () => onClose && onClose(notification),
image: new Widget.Icon({
className: "close icon",
icon: "window-close-symbolic"
} as Widget.IconProps)
} as Widget.ButtonProps)
children: [
new Widget.Label({
xalign: 1,
visible: !showTime ? false : true,
className: "time",
label: GLib.DateTime.new_from_unix_utc(notification.time).format("%H:%M"),
} as Widget.LabelProps),
new Widget.Button({
className: "close nf",
onClick: () => onClose && onClose(notification),
image: new Widget.Icon({
className: "close icon",
icon: "window-close-symbolic"
} as Widget.IconProps)
} as Widget.ButtonProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps),
Separator({
@@ -112,21 +129,23 @@ export function NotificationWidget(notification: AstalNotifd.Notification|number
new Widget.Box({
className: "actions button-row",
hexpand: true,
visible: (notification.actions.length === 1 &&
notification.actions[0].label.toLowerCase() === "view")
|| notification.actions.length === 0 ? false : true,
children: notification.actions.map((action: AstalNotifd.Action, i: number) =>
new Widget.Button({
className: "action",
visible: i === 0 ? (action.label.toLowerCase() !== "view") : true,
label: action.label,
hexpand: true,
onClicked: () => {
notification.invoke(action.id);
onClose && onClose(notification);
}
} as Widget.ButtonProps)
)
visible: (notification instanceof AstalNotifd.Notification) ?
(notification.actions.filter(action => action.label.toLowerCase() !== "view").length > 0)
: false,
children: (notification instanceof AstalNotifd.Notification) ?
notification.actions.filter(action => action.label.toLowerCase() !== "view")
.map((action: AstalNotifd.Action) =>
new Widget.Button({
className: "action",
label: action.label,
hexpand: true,
onClicked: () => {
notification.invoke(action.id);
onClose && onClose(notification);
}
} as Widget.ButtonProps)
)
: []
} as Widget.BoxProps)
]
} as Widget.BoxProps),
+8 -8
View File
@@ -18,6 +18,7 @@ export type PopupWindowProps = Pick<Widget.WindowProps,
| "heightRequest"
| "child"
| "monitor"
| "setup"
| "exclusivity"> & {
marginTop?: number;
marginLeft?: number;
@@ -42,8 +43,8 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window {
layer: props?.layer || Astal.Layer.OVERLAY,
focusOnMap: true,
visible: props?.visible,
acceptFocus: true,
monitor: props?.monitor || 0,
setup: props.setup,
onButtonPressEvent: (_, event: Gdk.Event) => {
const [, posX, posY] = event.get_coords();
const childAllocation = _.get_child()!.get_allocation();
@@ -70,20 +71,19 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window {
: `popup ${props?.className || ""}`,
halign: props?.halign || Gtk.Align.CENTER,
valign: props?.valign || Gtk.Align.CENTER,
expand: props?.expand || false,
widthRequest: props?.widthRequest,
heightRequest: props?.heightRequest,
hexpand: props?.hexpand || false,
vexpand: props?.vexpand || false,
visible: true,
css: `.popup {
margin-top: ${props.marginTop || 0}px;
margin-bottom: ${props.marginBottom || 0}px;
margin-left: ${props.marginLeft || 0}px;
margin-right: ${props.marginRight || 0}px;
}`,
expand: props.expand,
vexpand: props.vexpand,
hexpand: props.hexpand,
widthRequest: props.widthRequest,
heightRequest: props.heightRequest,
onButtonPressEvent: () => true,
child: props.child
} as Widget.BoxProps)
} as Widget.WindowProps);;
} as Widget.WindowProps);
}
@@ -1,15 +1,15 @@
import { Gtk, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
import { trGet } from "../../i18n/intl";
import { tr } from "../../i18n/intl";
import { Windows } from "../../windows";
export function Logo(): Gtk.Widget {
export function Apps(): Gtk.Widget {
return new Widget.EventBox({
onClickRelease: () => AstalHyprland.get_default().dispatch("exec", "anyrun"),
className: "logo",
onClickRelease: () => Windows.getWindow("apps-window")?.show(),
className: "apps",
child: new Widget.Box({
child: new Widget.Label({
className: "nf",
tooltipText: trGet()["bar"]["apps"]["tooltip"],
tooltipText: tr("bar.apps.tooltip"),
label: ""
} as Widget.LabelProps)
} as Widget.BoxProps)
-62
View File
@@ -1,62 +0,0 @@
import { bind, Process } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { Wireplumber } from "../../scripts/volume";
import { ControlCenter } from "../../window/ControlCenter";
export function Audio(): Gtk.Widget {
return new Widget.EventBox({
className: bind(ControlCenter, "visible").as((visible: boolean) =>
visible ? "audio open" : "audio"),
onClick: () => Process.exec_async("astal toggle control-center", () => {}),
child: new Widget.Box({
children: [
new Widget.EventBox({
className: "sink",
onScroll: (_, event) =>
event.delta_y > 0 ?
Wireplumber.getDefault().decreaseSinkVolume(5)
:
Wireplumber.getDefault().increaseSinkVolume(5),
child: new Widget.Box({
children: [
new Widget.Label({
className: "nf",
label: "󰕾"
} as Widget.LabelProps),
new Widget.Label({
className: "volume",
label: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) =>
Math.floor(volume * 100) + "%")
} as Widget.LabelProps)
]
})
} as Widget.EventBoxProps),
new Widget.EventBox({
className: "source",
onScroll: (_, event) =>
event.delta_y > 0 ?
Wireplumber.getDefault().decreaseSourceVolume(5)
:
Wireplumber.getDefault().increaseSourceVolume(5),
child: new Widget.Box({
children: [
new Widget.Label({
className: "nf",
label: "󰍬"
} as Widget.LabelProps),
new Widget.Label({
className: "volume",
label: bind(Wireplumber.getDefault().getDefaultSource(), "volume").as((volume: number) =>
Math.floor(volume * 100) + "%")
} as Widget.LabelProps)
]
})
} as Widget.EventBoxProps),
new Widget.Label({
className: "bell nf",
label: "󰂚"
} as Widget.LabelProps)
]
} as Widget.BoxProps)
} as Widget.EventBoxProps);
}
+1 -5
View File
@@ -15,11 +15,7 @@ export function FocusedClient(): Gtk.Widget {
vexpand: true,
css: ".icon { font-size: 18px; }",
icon: bind(hyprland, "focusedClient").as((client: AstalHyprland.Client) =>
client ?
(getAppIcon(client.initialClass) || client.initialClass)
:
"image-missing"
)
client ? getAppIcon(client.initialClass) : "image-missing")
}),
new Widget.Box({
className: "text-content",
+1 -1
View File
@@ -105,7 +105,7 @@ export function Media(): Gtk.Widget {
orientation: Gtk.Orientation.HORIZONTAL,
size: 2,
cssColor: `rgb(180, 180, 180)`,
alpha: 1
alpha: 0.3
} as SeparatorProps),
new Widget.Label({
className: "artist",
+128
View File
@@ -0,0 +1,128 @@
import AstalBluetooth from "gi://AstalBluetooth";
import AstalNetwork from "gi://AstalNetwork";
import AstalWp from "gi://AstalWp";
import { bind, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { Wireplumber } from "../../scripts/volume";
import { ControlCenter } from "../../window/ControlCenter";
import { Notifications } from "../../scripts/notifications";
import { Windows } from "../../windows";
export function Status(): Gtk.Widget {
return new Widget.EventBox({
className: bind(ControlCenter, "visible").as((visible: boolean) =>
visible ? "status open" : "status"),
onClick: () => Windows.toggle(ControlCenter!),
child: new Widget.Box({
children: [
volumeStatusSlider({
className: "sink",
endpoint: Wireplumber.getDefault().getDefaultSink(),
icon: "󰕾"
}),
volumeStatusSlider({
className: "source",
endpoint: Wireplumber.getDefault().getDefaultSource(),
icon: "󰍬"
}),
StatusIcons()
]
} as Widget.BoxProps)
} as Widget.EventBoxProps);
}
function volumeStatusSlider(props: { className?: string, endpoint: AstalWp.Endpoint, icon: string }): Gtk.Widget {
return new Widget.EventBox({
className: props.className,
onScroll: (_, event) =>
event.delta_y > 0 ?
Wireplumber.getDefault().decreaseEndpointVolume(props.endpoint, 5)
:
Wireplumber.getDefault().increaseEndpointVolume(props.endpoint, 5),
setup: (eventbox) => {
const connections: Array<number> = [];
connections.push(eventbox.connect("destroy-event", () =>
connections.map(id => eventbox.disconnect(id))));
eventbox.add(new Widget.Box({
children: [
new Widget.Label({
className: "nf",
label: props.icon,
} as Widget.LabelProps),
new Widget.Revealer({
revealChild: false,
transitionType: Gtk.RevealerTransitionType.SLIDE_RIGHT,
transitionDuration: 350,
setup: (revealer) => {
connections.push(
eventbox.connect("hover", () => revealer.revealChild = true),
eventbox.connect("hover-lost", () => revealer.revealChild = false));
revealer.add(new Widget.Slider({
className: "slider",
onDragged: (slider) => props.endpoint.set_volume(slider.value / 100),
value: bind(props.endpoint, "volume").as((volume) =>
Math.floor(volume * 100)),
max: 100
} as Widget.SliderProps));
}
} as Widget.RevealerProps),
new Widget.Label({
className: "volume",
label: bind(props.endpoint, "volume").as((volume: number) =>
Math.floor(volume * 100) + "%")
} as Widget.LabelProps),
]
} as Widget.BoxProps))
}
} as Widget.EventBoxProps)
}
function StatusIcons(): Gtk.Widget {
return new Widget.Box({
className: "status-icons",
children: [
new Widget.Label({
className: "bluetooth nf state",
label: Variable.derive([
bind(AstalBluetooth.get_default(), "isPowered"),
bind(AstalBluetooth.get_default(), "isConnected")
], (powered, connected) => {
return powered ? (
connected ? "󰂱"
: "󰂯"
) : "󰂲"
})()
} as Widget.LabelProps),
new Widget.Label({
className: "network nf state",
label: Variable.derive([
bind(AstalNetwork.get_default(), "primary"),
bind(AstalNetwork.get_default(), "wired"),
bind(AstalNetwork.get_default(), "wifi")
],
(primary, wired, wifi) => {
switch(primary) {
case AstalNetwork.Primary.WIRED: return wired ?
"󰛳"
: "󰛵";
case AstalNetwork.Primary.WIFI: return wifi ?
"󰤨"
: "󰤭";
}
return "󰲊";
})()
} as Widget.LabelProps),
new Widget.Label({
className: "bell nf state",
label: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as((dnd: boolean) =>
dnd ? "󰂠" : "󰂚")
} as Widget.LabelProps),
]
} as Widget.BoxProps);
}
+51 -15
View File
@@ -1,19 +1,55 @@
import { bind } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
import { Notifications } from "../../scripts/notifications";
import { HistoryNotification, Notifications } from "../../scripts/notifications";
import { NotificationWidget } from "../Notification";
export const NotifHistory: Gtk.Widget = new Widget.Scrollable({
hscroll: Gtk.PolicyType.NEVER,
vscroll: Gtk.PolicyType.AUTOMATIC,
vexpand: true,
hexpand: true,
child: new Widget.Box({
className: "notifications",
children: bind(Notifications.getDefault(), "history").as((history: Array<AstalNotifd.Notification>) =>
history.map((notification: AstalNotifd.Notification) => NotificationWidget(notification,
() => Notifications.getDefault().removeHistory(notification.id))
))
} as Widget.BoxProps)
} as Widget.ScrollableProps)
export const NotifHistory: Gtk.Widget = new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
className: "history",
expand: true,
visible: bind(Notifications.getDefault(), "history").as(history => history.length > 0),
children: [
new Widget.Scrollable({
className: "history",
hscroll: Gtk.PolicyType.NEVER,
vscroll: Gtk.PolicyType.AUTOMATIC,
expand: true,
visible: bind(Notifications.getDefault(), "history").as(history => history.length > 0),
child: new Widget.Box({
className: "notifications",
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
homogeneous: false,
children: bind(Notifications.getDefault(), "history").as((history: Array<HistoryNotification>) =>
history.map((notification: HistoryNotification) => NotificationWidget(notification,
() => Notifications.getDefault().removeHistory(notification.id))
))
} as Widget.BoxProps)
} as Widget.ScrollableProps),
new Widget.Box({
vexpand: false,
hexpand: true,
halign: Gtk.Align.END,
className: "button-row",
children: [
new Widget.Button({
className: "clear-all",
child: new Widget.Box({
children: [
new Widget.Label({
className: "nf",
css: "margin-right: 6px",
label: "󰎟"
} as Widget.LabelProps),
new Widget.Label({
label: "Clear"
} as Widget.LabelProps)
]
} as Widget.BoxProps),
onClick: () => Notifications.getDefault().clearHistory(),
} as Widget.ButtonProps)
]
})
]
} as Widget.BoxProps);
+2 -2
View File
@@ -1,12 +1,12 @@
import { Gtk, Widget } from "astal/gtk3";
import { TileNetwork } from "./tiles/Network";
import { TileBluetooth } from "./tiles/Bluetooth";
import { TileRecording } from "./tiles/Recording";
import { TileDND } from "./tiles/DoNotDisturb";
export const tileList: Array<any> = [
TileNetwork,
TileBluetooth,
TileRecording
TileDND
];
export function TilesWidget(): Gtk.Widget {
+14 -7
View File
@@ -1,10 +1,11 @@
import { bind, timeout } from "astal";
import { AstalIO, bind, timeout } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalBluetooth from "gi://AstalBluetooth";
import { Page } from "./Page";
import { Separator, SeparatorProps } from "../../Separator";
let watchingDevices: boolean = false;
let watchTimeout: (AstalIO.Time|undefined);
export const BluetoothPage: Page = new Page({
title: "Bluetooth Devices",
@@ -48,7 +49,7 @@ export const BluetoothPage: Page = new Page({
} as Widget.BoxProps)
]
} as Widget.BoxProps)
})
});
function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget {
return new Widget.Button({
@@ -85,11 +86,13 @@ function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget {
}
function watchNewDevices(): void {
if(watchingDevices) {
timeout(8000, () => {
reloadBluetoothDevicesList();
if(!watchTimeout) {
watchTimeout = timeout(5000, () => {
reloadBluetoothDevicesList(2500);
watchNewDevices();
watchTimeout = undefined;
});
return;
}
@@ -98,11 +101,15 @@ function watchNewDevices(): void {
export function stopBluetoothDevicesWatch(): void {
watchingDevices = false;
watchTimeout?.cancel();
watchTimeout = undefined;
AstalBluetooth.get_default().adapter.discovering &&
AstalBluetooth.get_default().adapter.stop_discovery();
}
export function reloadBluetoothDevicesList(): void {
export function reloadBluetoothDevicesList(discoveryTimeout?: number): void {
AstalBluetooth.get_default().adapter.start_discovery();
timeout(4000, () => AstalBluetooth.get_default().adapter.stop_discovery());
timeout(discoveryTimeout || 2500, () =>
AstalBluetooth.get_default().adapter.stop_discovery());
}
+4 -2
View File
@@ -6,8 +6,10 @@ import { BluetoothPage } from "../pages/Bluetooth";
export const TileBluetooth = Tile({
title: "Bluetooth",
description: bind(AstalBluetooth.get_default(), "devices").as((devices: Array<AstalBluetooth.Device>) =>
devices.filter((dev: AstalBluetooth.Device) => dev.connected)[0]?.get_alias()),
description: bind(AstalBluetooth.get_default(), "isConnected").as((connected) => {
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
return connected && connectedDev ? connectedDev.get_alias() : ""
}),
onToggledOn: () => AstalBluetooth.get_default().adapter.set_powered(true),
onToggledOff: () => AstalBluetooth.get_default().adapter.set_powered(false),
onClickMore: () => togglePage(BluetoothPage),
@@ -0,0 +1,15 @@
import { bind } from "astal";
import { Notifications } from "../../../scripts/notifications";
import { Tile } from "./Tile";
import { tr } from "../../../i18n/intl";
export const TileDND = Tile({
title: tr("control_center.tiles.dnd.title"),
description: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as(
(dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled")),
onToggledOff: () => Notifications.getDefault().getNotifd().dontDisturb = false,
onToggledOn: () => Notifications.getDefault().getNotifd().dontDisturb = true,
icon: "󰍶",
iconSize: 16,
toggleState: Notifications.getDefault().getNotifd().dontDisturb
});
@@ -0,0 +1,10 @@
import { Tile, TileProps } from "./Tile";
export const TileNightLight = Tile({
title: "Luz Noturna",
icon: "󰖔",
iconSize: 16,
onToggledOff: () => false,
onToggledOn: () => true,
toggleState: false
} as TileProps);
+7 -15
View File
@@ -56,7 +56,7 @@ export function Tile(props: TileProps): Widget.EventBox {
new Widget.Label({
className: "icon nf",
label: props.icon || "icon",
css: `label { font-size: ${props.iconSize || "12"}px; }`
css: `label { font-size: ${props.iconSize || 12}px; }`
} as Widget.LabelProps),
new Widget.Box({
className: "text",
@@ -74,23 +74,15 @@ export function Tile(props: TileProps): Widget.EventBox {
} as Widget.LabelProps),
new Widget.Label({
className: "description",
visible: Boolean(props.description),
setup: (label: Widget.Label) => {
if(props.description instanceof Binding) {
const sub = props.description.subscribe((value) => {
label.set_visible(Boolean(value));
});
const destroyId = label.connect("destroy-event", () => {
label.disconnect(destroyId);
sub();
});
}
},
visible: (props.description instanceof Binding) ?
props.description.as(Boolean)
: Boolean(props.description),
halign: Gtk.Align.START,
truncate: true,
xalign: 0,
label: props.description
label: (props.description instanceof Binding) ?
props.description.as((desc) => desc ? desc : "")
: (props.description || "")
} as Widget.LabelProps)
]
} as Widget.BoxProps)
+2 -2
View File
@@ -1,6 +1,6 @@
import { register } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { closeRunner } from "../../runner/Runner";
import { Runner } from "../../runner/Runner";
export { ResultWidget, ResultWidgetProps };
@@ -32,7 +32,7 @@ class ResultWidget extends Widget.Box {
this.onClick = () => {
props.onClick && props.onClick();
this.closeOnClick && closeRunner();
this.closeOnClick && Runner.close();
};
this.set_class_name("result");