From 3db477598fc51e698b4ac79e1a2a004c46f474ac Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Sun, 23 Mar 2025 10:17:22 -0300 Subject: [PATCH] :sparkles: ags(bar, notifications, control-center): add status icons to bar, notification history fixed, notification history below control-center --- ags/i18n/lang/en_US.ts | 10 +- ags/i18n/lang/pt_BR.ts | 5 + ags/runner/Runner.ts | 87 +++++----- ags/scripts/apps.ts | 23 ++- ags/scripts/notifications.ts | 99 ++++++++--- ags/scripts/volume.ts | 42 ++--- ags/style.scss | 13 +- ags/style/_apps-window.scss | 42 ++++- ags/style/_bar.scss | 13 +- ags/style/_control-center.scss | 54 +++++- ags/style/_wal.scss | 40 ++--- ags/widget/Notification.ts | 87 ++++++---- ags/widget/PopupWindow.ts | 16 +- ags/widget/bar/{Logo.ts => Apps.ts} | 12 +- ags/widget/bar/Audio.ts | 62 ------- ags/widget/bar/FocusedClient.ts | 6 +- ags/widget/bar/Media.ts | 2 +- ags/widget/bar/Status.ts | 128 ++++++++++++++ ags/widget/control-center/NotifHistory.ts | 66 ++++++-- ags/widget/control-center/Tiles.ts | 4 +- ags/widget/control-center/pages/Bluetooth.ts | 21 ++- ags/widget/control-center/tiles/Bluetooth.ts | 6 +- .../control-center/tiles/DoNotDisturb.ts | 15 ++ ags/widget/control-center/tiles/NightLight.ts | 10 ++ ags/widget/control-center/tiles/Tile.ts | 22 +-- ags/widget/runner/ResultWidget.ts | 4 +- ags/window/AppsWindow.ts | 158 +++++++++++------- ags/window/Bar.ts | 9 +- ags/window/ControlCenter.ts | 53 ++++-- 29 files changed, 746 insertions(+), 363 deletions(-) rename ags/widget/bar/{Logo.ts => Apps.ts} (50%) delete mode 100644 ags/widget/bar/Audio.ts create mode 100644 ags/widget/bar/Status.ts create mode 100644 ags/widget/control-center/tiles/DoNotDisturb.ts create mode 100644 ags/widget/control-center/tiles/NightLight.ts diff --git a/ags/i18n/lang/en_US.ts b/ags/i18n/lang/en_US.ts index 6385654..848e81c 100644 --- a/ags/i18n/lang/en_US.ts +++ b/ags/i18n/lang/en_US.ts @@ -9,7 +9,10 @@ export default { }, control_center: { tiles: { + enabled: "Enabled", + disabled: "Disabled", more: "More", + network: { network: "Network", connected: "Connected", @@ -21,8 +24,11 @@ export default { }, recording: { title: "Screen Recording", - disabled_description: "Start recording", - enabled_description: "Stop recording", + disabled_desc: "Start recording", + enabled_desc: "Stop recording", + }, + dnd: { + title: "Do Not Disturb" } } }, diff --git a/ags/i18n/lang/pt_BR.ts b/ags/i18n/lang/pt_BR.ts index b1c61b2..ed0841a 100644 --- a/ags/i18n/lang/pt_BR.ts +++ b/ags/i18n/lang/pt_BR.ts @@ -9,6 +9,8 @@ export default { }, control_center: { tiles: { + enabled: "Ligado", + disabled: "Desligado", more: "Mais", network: { @@ -24,6 +26,9 @@ export default { title: "Gravação de Tela", disabled_description: "Iniciar gravação", enabled_description: "Parar gravação", + }, + dnd: { + title: "Não Perturbe" } } }, diff --git a/ags/runner/Runner.ts b/ags/runner/Runner.ts index 8ace524..d605819 100644 --- a/ags/runner/Runner.ts +++ b/ags/runner/Runner.ts @@ -7,34 +7,27 @@ import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget"; export let runnerInstance: (Widget.Window|null) = null; let onClickTimeout: (AstalIO.Time|undefined); -export function closeRunner(gtkWindow?: Widget.Window) { - const window = gtkWindow ? gtkWindow : runnerInstance; - - window?.destroy(); - runnerInstance = null; -} - export function startRunnerDefault() { - return Runner.RunnerWindow({ - entryPlaceHolder: "Start typing...", - resultsPlaceholder: () => [ - new ResultWidget({ - icon: "utilities-terminal-symbolic", - title: "Run shell commands", - description: "Start typing with '!' prefix to run shell commands" - } as ResultWidgetProps), - new ResultWidget({ - icon: "application-x-executable-symbolic", - title: "Run your applications", - description: "Type the name of the application to search" - } as ResultWidgetProps), - new ResultWidget({ - icon: "applications-internet-symbolic", - title: "Search the Web", - description: "Start typing with '?' prefix to search the web" - } as ResultWidgetProps) - ] - } as Runner.RunnerProps); + return Runner.openRunner({ + entryPlaceHolder: "Start typing..." + } as Runner.RunnerProps, + () => [ + new ResultWidget({ + icon: "utilities-terminal-symbolic", + title: "Run shell commands", + description: "Start typing with '!' prefix to run shell commands" + } as ResultWidgetProps), + new ResultWidget({ + icon: "application-x-executable-symbolic", + title: "Run your applications", + description: "Type the name of the application to search" + } as ResultWidgetProps), + new ResultWidget({ + icon: "applications-internet-symbolic", + title: "Search the Web", + description: "Start typing with '?' prefix to search the web" + } as ResultWidgetProps) + ]); } export namespace Runner { @@ -44,9 +37,18 @@ export namespace Runner { width?: number; height?: number; entryPlaceHolder?: string; - resultsPlaceholder?: () => Array; }; + export function close(gtkWindow?: Widget.Window) { + const window = gtkWindow ? gtkWindow : runnerInstance; + + [...plugins.values()].map(plugin => + plugin && plugin.onClose && plugin.onClose()); + + window?.close(); + runnerInstance = null; + } + const plugins = new Set([]); export interface Plugin { @@ -54,14 +56,19 @@ export namespace Runner { readonly prefix?: string; /** name of the plugin. e.g.: websearch, shell */ readonly name?: string; - /** handle the user input to return results (does not contain prefix) */ + /** ran on plugin load */ + readonly init?: () => void; + /** handle the user input to return results (does not include plugin's prefix) */ readonly handle: (inputText: string) => (ResultWidget|Array|null|undefined); + /** ran on runner close */ + readonly onClose?: () => void; } export function addPlugin(plugin: Runner.Plugin, force?: boolean) { if(!force && plugin.prefix && plugins.has(plugin)) throw new Error(`Runner plugin with prefix ${plugin.prefix} already exists`); + plugins.delete(plugin); plugins.add(plugin); } @@ -76,7 +83,7 @@ export namespace Runner { return plugins.delete(plugin); } - export function RunnerWindow(props?: RunnerProps): (Widget.Window|null) { + export function openRunner(props?: RunnerProps, placeholder?: () => Array): (Widget.Window|null) { let subs: Array<() => void> = []; const entryText: Variable = new Variable(""); @@ -87,8 +94,8 @@ export namespace Runner { onActivate: (entry) => { const resultWidget = resultsList.get_selected_row()?.get_child(); if(resultWidget instanceof ResultWidget) { - resultWidget.onClick(); entry.isFocus = false; + resultWidget.onClick(); } }, primary_icon_name: "system-search" @@ -114,8 +121,8 @@ export namespace Runner { }); // Insert placeholder if somehow no results are found - if((!entryText || !widgets || widgets.length === 0) && props?.resultsPlaceholder) - widgets.push(...props.resultsPlaceholder()); + if(placeholder && (!entryText || !widgets || widgets.length === 0)) + widgets.push(...placeholder()); // Insert results inside GtkListBox widgets.map((resultWidget: ResultWidget) => { @@ -142,25 +149,25 @@ export namespace Runner { if(!runnerInstance) runnerInstance = PopupWindow({ namespace: "runner", - halign: props?.halign || Gtk.Align.CENTER, - valign: props?.valign || Gtk.Align.CENTER, widthRequest: props?.width || 750, heightRequest: props?.height || 450, onKeyPressEvent: (_, event: Gdk.Event) => { const keyVal = event.get_keyval()[1]; if(!searchEntry.has_focus && keyVal !== Gdk.KEY_F5 && keyVal !== Gdk.KEY_Down && keyVal !== Gdk.KEY_Up - && keyVal !== Gdk.KEY_KP_Enter && keyVal !== Gdk.KEY_ISO_Enter) { + && keyVal !== Gdk.KEY_KP_Enter && keyVal !== Gdk.KEY_ISO_Enter + && keyVal !== Gdk.KEY_Escape) { searchEntry.grab_focus_without_selecting(); } - event.get_keyval()[1] === Gdk.KEY_F5 && updateApps(); - + + }, + closeAction: (_) => { + close(_); + subs.map(sub => sub()); }, - closeAction: (_) => closeRunner(_), - onClose: () => subs.map(sub => sub()), child: new Widget.Box({ className: "runner main", orientation: Gtk.Orientation.VERTICAL, diff --git a/ags/scripts/apps.ts b/ags/scripts/apps.ts index 9eadab9..6b70a36 100644 --- a/ags/scripts/apps.ts +++ b/ags/scripts/apps.ts @@ -1,4 +1,6 @@ +import { Astal } from "astal/gtk3"; import AstalApps from "gi://AstalApps"; +import AstalHyprland from "gi://AstalHyprland"; const astalApps: AstalApps.Apps = new AstalApps.Apps(); let appsList: Array = astalApps.get_list(); @@ -16,6 +18,10 @@ export function getAstalApps(): AstalApps.Apps { return astalApps; } +export function cleanExec(app: AstalApps.Application): void { + AstalHyprland.get_default().dispatch("exec", app.executable.replace(/(%f|%F|%u|%U|%i|%c|%k)/g, "")); +} + export function getAppsByName(appName: string): (Array|undefined) { let found: Array = []; @@ -29,6 +35,19 @@ export function getAppsByName(appName: string): (Array|un } export function getAppIcon(appName: string): (string|undefined) { - const found: (Array|undefined) = getAppsByName(appName); - return found ? found[0]?.iconName : undefined; + if(Astal.Icon.lookup_icon(appName)) + return appName; + + if(Astal.Icon.lookup_icon(appName.toLowerCase())) + return appName.toLowerCase(); + + const nameReverseDNS = appName.split('.'); + if(Astal.Icon.lookup_icon(nameReverseDNS[nameReverseDNS.length - 1])) + return nameReverseDNS[nameReverseDNS.length - 1]; + + const found: (AstalApps.Application|undefined) = getAppsByName(appName)?.[0]; + if(Boolean(found)) + return found?.iconName; + + return "application-x-executable-symbolic"; } diff --git a/ags/scripts/notifications.ts b/ags/scripts/notifications.ts index 4acdf41..21d8032 100644 --- a/ags/scripts/notifications.ts +++ b/ags/scripts/notifications.ts @@ -6,13 +6,24 @@ export const NOTIFICATION_TIMEOUT_NORMAL: number = 4000, NOTIFICATION_TIMEOUT_LOW: number = 2000; +export interface HistoryNotification { + id: number; + appName: string; + body: string; + summary: string; + urgency: AstalNotifd.Urgency; + time: number; + image?: string; +} + @register({ GTypeName: "Notifications" }) class Notifications extends GObject.Object { private static instance: (Notifications|null) = null; #notifications: Array = []; - #history: Array = []; + #history: Array = []; #connections: Array; + #historyLimit: number = 10; @property() @@ -21,6 +32,14 @@ class Notifications extends GObject.Object { @property() public get history() { return this.#history }; + @property() + public get historyLimit() { return this.#historyLimit }; + + public set historyLimit(newValue: number) { + this.#historyLimit = newValue; + this.notify("historyLimit"); + } + @signal(AstalNotifd.Notification) declare notificationAdded: (notification: AstalNotifd.Notification) => void; @@ -28,7 +47,7 @@ class Notifications extends GObject.Object { @signal(Number) declare notificationRemoved: (id: number) => void; - @signal(AstalNotifd.Notification) + @signal(Object) declare historyAdded: (notification: AstalNotifd.Notification) => void; @signal(Number) @@ -42,7 +61,7 @@ class Notifications extends GObject.Object { super(); this.#connections = [ - AstalNotifd.get_default().connect("notified", (notifd, id, _replaced) => { + AstalNotifd.get_default().connect("notified", (notifd, id) => { const notification = notifd.get_notification(id); const notifTimeout = notification.urgency === AstalNotifd.Urgency.LOW ? NOTIFICATION_TIMEOUT_LOW @@ -50,29 +69,39 @@ class Notifications extends GObject.Object { NOTIFICATION_TIMEOUT_URGENT : NOTIFICATION_TIMEOUT_NORMAL); + if(this.getNotifd().dontDisturb) { + this.addHistory(notification, () => notification.dismiss()); + return; + } + this.addNotification(notification, () => { if(notification.urgency !== AstalNotifd.Urgency.CRITICAL || (notification.urgency === AstalNotifd.Urgency.CRITICAL && NOTIFICATION_TIMEOUT_URGENT > 0)) { - let notifTimer: AstalIO.Time; + let notifTimer: (AstalIO.Time|undefined) = undefined; let replacedConnectionId: number; + const removeFun = () => { // Funny name haha lmao remove fun :skull: - this.removeNotification(id); - replacedConnectionId && this.disconnect(replacedConnectionId); + notifTimer = undefined; + this.addHistory(notification, () => { + replacedConnectionId && this.disconnect(replacedConnectionId); + this.removeNotification(id); + }); } + notifTimer = timeout(notifTimeout, removeFun); + replacedConnectionId = this.connect("notification-replaced", (_, id: number) => { if(notification.id === id) { - notifTimer.cancel(); + notifTimer?.cancel(); notifTimer = timeout(notifTimeout, removeFun); } }); - - notifTimer = timeout(notifTimeout, removeFun); } }); }), + AstalNotifd.get_default().connect("resolved", (notifd, id, _reason) => { this.removeNotification(id); this.addHistory(notifd.get_notification(id)); @@ -94,17 +123,41 @@ class Notifications extends GObject.Object { } private addHistory(notif: AstalNotifd.Notification, onAdded?: (notif: AstalNotifd.Notification) => void): void { - const newArray = this.#history.reverse().filter((item) => item.id !== notif.id); - newArray.push(notif); + if(!notif) return; + + this.#history.length === this.#historyLimit && + this.removeHistory(this.#history[this.#history.length - 1]); + + const newArray = this.#history.length > 0 ? this.#history.reverse().filter((item) => item.id !== notif.id) : []; + newArray.push({ + id: notif.id, + appName: notif.appName, + body: notif.body, + summary: notif.summary, + urgency: notif.urgency, + time: notif.time, + image: notif.image ? notif.image : undefined + } as HistoryNotification); this.#history = newArray.reverse(); this.notify("history"); - this.emit("history-added", notif); + this.emit("history-added", this.#history[0]); onAdded && onAdded(notif); } - public removeHistory(notif: (AstalNotifd.Notification|number)): void { - const notifId = (notif instanceof AstalNotifd.Notification) ? notif.id : notif; - this.#history = this.#history.filter((item: AstalNotifd.Notification) => + public clearHistory(): void { + for(let i = 0; i < this.history.length; i++) { + const notif = this.history[this.history.length-1]; + + if(this.#history.pop()) { + this.emit("history-removed", notif.id); + this.notify("history"); + } + } + } + + public removeHistory(notif: (HistoryNotification|number)): void { + const notifId = (typeof notif === "number") ? notif : notif.id; + this.#history = this.#history.filter((item: HistoryNotification) => item.id !== notifId); this.notify("history"); @@ -125,21 +178,27 @@ class Notifications extends GObject.Object { } public removeNotification(notif: (AstalNotifd.Notification|number)): void { - const notification = (notif instanceof AstalNotifd.Notification) ? notif : AstalNotifd.get_default().get_notification(notif); + const notificationId = (notif instanceof AstalNotifd.Notification) ? notif.id : notif; this.#notifications = this.#notifications.filter((item: AstalNotifd.Notification) => - item.id !== notification.id); + item.id !== notificationId); - notification.dismiss(); + AstalNotifd.get_default().get_notification(notificationId)?.dismiss(); this.notify("notifications"); - this.emit("notification-removed", notification.id); + this.emit("notification-removed", notificationId); } public toggleDoNotDisturb(): boolean { if(AstalNotifd.get_default().dontDisturb) { - + AstalNotifd.get_default().dontDisturb = false; + return false; } + + AstalNotifd.get_default().dontDisturb = true; + return true; } + public getNotifd(): AstalNotifd.Notifd { return AstalNotifd.get_default(); } + connect(signal: string, callback: (...args: any[]) => void): number { return super.connect(signal, callback); } diff --git a/ags/scripts/volume.ts b/ags/scripts/volume.ts index 6c908a2..7d01894 100644 --- a/ags/scripts/volume.ts +++ b/ags/scripts/volume.ts @@ -71,42 +71,42 @@ class WireplumberClass extends GObject.Object { ); } - public increaseSinkVolume(volumeIncrease: number): void { - if((this.getSinkVolume() + volumeIncrease) > this.maxSinkVolume) { - this.setSinkVolume(this.maxSinkVolume); + public increaseEndpointVolume(endpoint: AstalWp.Endpoint, volumeIncrease: number): void { + volumeIncrease = Math.abs(volumeIncrease) / 100; + + if((endpoint.get_volume() + volumeIncrease) > this.maxSinkVolume) { + endpoint.set_volume(1.0); return; } - this.setSinkVolume(this.getSinkVolume() + volumeIncrease); + endpoint.set_volume(endpoint.get_volume() + volumeIncrease); + } + + public increaseSinkVolume(volumeIncrease: number): void { + this.increaseEndpointVolume(this.getDefaultSink(), volumeIncrease); } public increaseSourceVolume(volumeIncrease: number): void { - if((this.getSourceVolume() + volumeIncrease) > this.maxSourceVolume) { - this.setSourceVolume(this.maxSourceVolume); + this.increaseEndpointVolume(this.getDefaultSource(), volumeIncrease); + } + + public decreaseEndpointVolume(endpoint: AstalWp.Endpoint, volumeDecrease: number): void { + volumeDecrease = Math.abs(volumeDecrease) / 100; + + if((endpoint.get_volume() - volumeDecrease) < 0) { + endpoint.set_volume(0); return; } - this.setSourceVolume(this.getSourceVolume() + volumeIncrease); + endpoint.set_volume(endpoint.get_volume() - volumeDecrease); } public decreaseSinkVolume(volumeDecrease: number): void { - const absDecrease = Math.abs(volumeDecrease); - - if((this.getSinkVolume() - absDecrease) < 0) { - this.setSinkVolume(0); - return; - } - - this.setSinkVolume(this.getSinkVolume() - absDecrease); + this.decreaseEndpointVolume(this.getDefaultSink(), volumeDecrease); } public decreaseSourceVolume(volumeDecrease: number): void { - const absDecrease = Math.abs(volumeDecrease); - - if((this.getSourceVolume() - absDecrease) < 0) - return this.setSourceVolume(0); - - this.setSourceVolume(this.getSourceVolume() - absDecrease); + this.decreaseEndpointVolume(this.getDefaultSource(), volumeDecrease); } public muteSink(): void { diff --git a/ags/style.scss b/ags/style.scss index e68ad10..22da83c 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -72,6 +72,14 @@ window.ask-popup { font-size: 12px; } + + & label.time { + font-size: 11px; + font-weight: 500; + color: colors.$fg-disabled; + margin-right: 6px; + } + & button.close { padding: 2px; border-radius: 8px; @@ -177,13 +185,14 @@ menu { scrollbar trough { @include mixins.reset-props; + background: colors.$bg-translucent; - border-top-left-radius: 8px; - border-bottom-left-radius: 8px; + border-radius: 8px; padding: 2px; & slider { @include mixins.reset-props; + min-width: .85em; background: colors.$bg-tertiary; border-radius: 12px; diff --git a/ags/style/_apps-window.scss b/ags/style/_apps-window.scss index a2e289a..2087498 100644 --- a/ags/style/_apps-window.scss +++ b/ags/style/_apps-window.scss @@ -1,5 +1,43 @@ -.apps-window.container { - & > entry { +@use "mixins"; +@use "colors"; +.apps-window-container { + padding: 24px; + + & > entry { + background: rgba(colors.$bg-primary, .4); + padding: 10px 9px; + margin-bottom: 32px; + border-radius: 12px; + min-width: 350px; + + &:focus { + box-shadow: inset 0 0 0 2px colors.$bg-secondary; + } + + & image.left { + margin-right: 6px; + } + } + + & flowbox { + padding: 16px 24px; + + & > flowboxchild > button { + padding: 8px; + border-radius: 24px; + + &:hover { + background: colors.$bg-translucent; + } + + & icon { + font-size: 64px; + } + + & label { + margin-top: 6px; + } + } } } diff --git a/ags/style/_bar.scss b/ags/style/_bar.scss index 6802eec..4f6f08a 100644 --- a/ags/style/_bar.scss +++ b/ags/style/_bar.scss @@ -171,7 +171,7 @@ } } - .audio { + .status { @include mixins.reset-props; &:hover > box, @@ -211,13 +211,17 @@ font-size: 12px; } - & .bell { - margin: 0 4px; + & .status-icons { + padding: 0 4px; + + & > * { + margin: 0 4px; + } } } } - .logo { + .apps { & > box { border-radius: 12px; transition: 100ms linear; @@ -225,7 +229,6 @@ & > label { font-size: 14px; - margin-right: 1px; } } &:hover { diff --git a/ags/style/_control-center.scss b/ags/style/_control-center.scss index 9c9edc0..7e7a3e4 100644 --- a/ags/style/_control-center.scss +++ b/ags/style/_control-center.scss @@ -66,6 +66,43 @@ } } +box.history { + margin-top: 10px; + background: colors.$bg-translucent; + border-radius: 24px; + padding: 20px 14px; + + & scrollable viewport .notifications > eventbox { + & > box { + margin: 4px 0; + } + + &:first-child { + & > box { + margin-top: 0; + } + } + + &:last-child { + & > box { + margin-bottom: 0; + } + } + } + + & > .button-row { + & button { + & label.nf { + font-size: 16px; + } + & label:not(.nf) { + font-size: 12px; + font-weight: 600; + } + } + } +} + .tiles-container { @include mixins.reset-props; @@ -150,15 +187,16 @@ font-weight: 500; } - &.bluetooth { - .connections button { - @include mixins.hover-shadow; - padding: 6px; - border-radius: 12px; + & button { + @include mixins.hover-shadow; - &.connected { - background: colors.$bg-tertiary; - } + padding: 6px; + border-radius: 12px; + } + + &.bluetooth { + button.connected { + background: colors.$bg-tertiary; } } } diff --git a/ags/style/_wal.scss b/ags/style/_wal.scss index 6a903c4..1d22737 100644 --- a/ags/style/_wal.scss +++ b/ags/style/_wal.scss @@ -1,26 +1,26 @@ // SCSS Variables // Generated by 'wal' -$wallpaper: "/home/joaov/wallpapers/Miku Guitar.jpg"; +$wallpaper: "/home/joaov/wallpapers/Garden Kita.png"; // Special -$background: #171418; -$foreground: #c5c4c5; -$cursor: #c5c4c5; +$background: #101212; +$foreground: #c3c3c3; +$cursor: #c3c3c3; // Colors -$color0: #171418; -$color1: #607985; -$color2: #208FB6; -$color3: #4C9CB4; -$color4: #63ADC9; -$color5: #C3B49C; -$color6: #89BBCF; -$color7: #96909b; -$color8: #715c71; -$color9: #6aa4bf; -$color10: #62a5bc; -$color11: #78b3c5; -$color12: #90becf; -$color13: #dac7ab; -$color14: #a7cbd9; -$color15: #c5c4c5; +$color0: #101212; +$color1: #778839; +$color2: #6C965F; +$color3: #B4B350; +$color4: #96A460; +$color5: #658388; +$color6: #B0B493; +$color7: #8e9898; +$color8: #596d6d; +$color9: #a3b757; +$color10: #85c373; +$color11: #c6c67a; +$color12: #b7c67a; +$color13: #6eb7c1; +$color14: #ced59e; +$color15: #c3c3c3; diff --git a/ags/widget/Notification.ts b/ags/widget/Notification.ts index 80dbe5a..d79fec6 100644 --- a/ags/widget/Notification.ts +++ b/ags/widget/Notification.ts @@ -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), diff --git a/ags/widget/PopupWindow.ts b/ags/widget/PopupWindow.ts index de05059..07967ab 100644 --- a/ags/widget/PopupWindow.ts +++ b/ags/widget/PopupWindow.ts @@ -18,6 +18,7 @@ export type PopupWindowProps = Pick & { 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); } diff --git a/ags/widget/bar/Logo.ts b/ags/widget/bar/Apps.ts similarity index 50% rename from ags/widget/bar/Logo.ts rename to ags/widget/bar/Apps.ts index 7b40269..fe423cc 100644 --- a/ags/widget/bar/Logo.ts +++ b/ags/widget/bar/Apps.ts @@ -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) diff --git a/ags/widget/bar/Audio.ts b/ags/widget/bar/Audio.ts deleted file mode 100644 index 8b4341a..0000000 --- a/ags/widget/bar/Audio.ts +++ /dev/null @@ -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); -} diff --git a/ags/widget/bar/FocusedClient.ts b/ags/widget/bar/FocusedClient.ts index 24f4306..a2ee4ea 100644 --- a/ags/widget/bar/FocusedClient.ts +++ b/ags/widget/bar/FocusedClient.ts @@ -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", diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts index 154b13e..269d586 100644 --- a/ags/widget/bar/Media.ts +++ b/ags/widget/bar/Media.ts @@ -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", diff --git a/ags/widget/bar/Status.ts b/ags/widget/bar/Status.ts new file mode 100644 index 0000000..94aa017 --- /dev/null +++ b/ags/widget/bar/Status.ts @@ -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 = []; + 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); +} diff --git a/ags/widget/control-center/NotifHistory.ts b/ags/widget/control-center/NotifHistory.ts index a4cd1cb..75101db 100644 --- a/ags/widget/control-center/NotifHistory.ts +++ b/ags/widget/control-center/NotifHistory.ts @@ -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) => - 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) => + 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); diff --git a/ags/widget/control-center/Tiles.ts b/ags/widget/control-center/Tiles.ts index cb93585..f6d23d6 100644 --- a/ags/widget/control-center/Tiles.ts +++ b/ags/widget/control-center/Tiles.ts @@ -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 = [ TileNetwork, TileBluetooth, - TileRecording + TileDND ]; export function TilesWidget(): Gtk.Widget { diff --git a/ags/widget/control-center/pages/Bluetooth.ts b/ags/widget/control-center/pages/Bluetooth.ts index dfe5d4d..4e828e6 100644 --- a/ags/widget/control-center/pages/Bluetooth.ts +++ b/ags/widget/control-center/pages/Bluetooth.ts @@ -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()); } diff --git a/ags/widget/control-center/tiles/Bluetooth.ts b/ags/widget/control-center/tiles/Bluetooth.ts index cd5e414..e466eca 100644 --- a/ags/widget/control-center/tiles/Bluetooth.ts +++ b/ags/widget/control-center/tiles/Bluetooth.ts @@ -6,8 +6,10 @@ import { BluetoothPage } from "../pages/Bluetooth"; export const TileBluetooth = Tile({ title: "Bluetooth", - description: bind(AstalBluetooth.get_default(), "devices").as((devices: Array) => - 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), diff --git a/ags/widget/control-center/tiles/DoNotDisturb.ts b/ags/widget/control-center/tiles/DoNotDisturb.ts new file mode 100644 index 0000000..f1226b7 --- /dev/null +++ b/ags/widget/control-center/tiles/DoNotDisturb.ts @@ -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 +}); diff --git a/ags/widget/control-center/tiles/NightLight.ts b/ags/widget/control-center/tiles/NightLight.ts new file mode 100644 index 0000000..4554b3b --- /dev/null +++ b/ags/widget/control-center/tiles/NightLight.ts @@ -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); diff --git a/ags/widget/control-center/tiles/Tile.ts b/ags/widget/control-center/tiles/Tile.ts index 2e65673..a146b70 100644 --- a/ags/widget/control-center/tiles/Tile.ts +++ b/ags/widget/control-center/tiles/Tile.ts @@ -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) diff --git a/ags/widget/runner/ResultWidget.ts b/ags/widget/runner/ResultWidget.ts index 054a9c7..c7877e2 100644 --- a/ags/widget/runner/ResultWidget.ts +++ b/ags/widget/runner/ResultWidget.ts @@ -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"); diff --git a/ags/window/AppsWindow.ts b/ags/window/AppsWindow.ts index 25dc929..6a5a7cc 100644 --- a/ags/window/AppsWindow.ts +++ b/ags/window/AppsWindow.ts @@ -1,12 +1,10 @@ import { Variable } from "astal"; import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; -import { getAstalApps } from "../scripts/apps"; +import { cleanExec, getAstalApps } from "../scripts/apps"; import AstalApps from "gi://AstalApps"; -import AstalHyprland from "gi://AstalHyprland"; const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; const searchString = new Variable(""); -const appsArray = new Variable>([]); let searchSubscription: () => void; export const AppsWindow = new Widget.Window({ @@ -16,64 +14,98 @@ export const AppsWindow = new Widget.Window({ anchor: TOP | LEFT | RIGHT | BOTTOM, visible: false, keymode: Astal.Keymode.EXCLUSIVE, - onKeyPressEvent: (_, event: Gdk.Event) => { - event.get_keyval()[1] === Gdk.KEY_Escape && - hideAppsWindow(_); - }, - setup: () => { - searchSubscription = searchString.subscribe((str: string) => { - appsArray.set(getAstalApps().fuzzy_query(str)); - }); - }, - child: new Widget.Box({ - className: "apps-window container", - expand: true, - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Entry({ - className: "entry", - hexpand: true, - vexpand: false, - onDraw: (_) => _.grab_focus(), - onChanged: (entry) => { - searchString.set(entry.text); - } - } as Widget.EntryProps), - new Widget.Box({ - className: "apps", - hexpand: true, - vexpand: true, - orientation: Gtk.Orientation.VERTICAL, - children: appsArray((apps: Array) => - apps.map((app: AstalApps.Application) => - new Widget.Button({ - className: "app", - onClickRelease: (_) => { - _.get_window()?.hide(); - AstalHyprland.get_default().dispatch("exec", app.get_executable()); - }, - child: new Widget.Box({ - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Icon({ - className: "icon", - iconName: app.get_icon_name() - } as Widget.IconProps), - new Widget.Label({ - className: "name", - label: app.get_name() - } as Widget.LabelProps) - ] - } as Widget.BoxProps) - } as Widget.ButtonProps) - ) - ) - } as Widget.BoxProps) - ] - } as Widget.BoxProps) -} as Widget.WindowProps); + onDestroy: () => searchSubscription(), + setup: (window) => { + const flowbox = new Gtk.FlowBox({ + rowSpacing: 6, + homogeneous: true, + columnSpacing: 6, + expand: false, + orientation: Gtk.Orientation.HORIZONTAL, + visible: true + } as Gtk.FlowBox.ConstructorProps); -function hideAppsWindow(window: Widget.Window) { - searchString.set(""); - window.hide(); -} + const entry = new Widget.Entry({ + className: "entry", + halign: Gtk.Align.CENTER, + primary_icon_name: "system-search", + onChanged: (entry) => { + searchString.set(entry.text); + } + } as Widget.EntryProps); + + searchSubscription = searchString.subscribe((str: string) => { + const results: Array = getAstalApps().fuzzy_query(str); + + // Destroy is handled by GnomeJS + flowbox.get_children().map(flowboxChild => flowbox.remove(flowboxChild)); + + results.map(app => { + flowbox.insert(new Widget.Button({ + onClick: (_button, event: Astal.ClickEvent) => { + if(event.button === Astal.MouseButton.PRIMARY) { + searchString.set(""); + entry.text = ""; + window.hide(); + cleanExec(app); + + return; + } + + // select app launch options TODO + }, + child: new Widget.Box({ + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Icon({ + className: "icon", + expand: true, + icon: app.get_icon_name() + } as Widget.IconProps), + new Widget.Label({ + className: "name", + truncate: true, + label: app.get_name() + } as Widget.LabelProps) + ] + } as Widget.BoxProps) + } as Widget.ButtonProps), -1); + + const flowboxchild = flowbox.get_children()[flowbox.get_children().length-1]; + flowboxchild.set_valign(Gtk.Align.START); + }); + + const firstChild = flowbox.get_child_at_index(0); + firstChild && flowbox.select_child(firstChild); + }); + + window.add(new Widget.EventBox({ + onClick: () => { + searchString.set(""); + entry.text = ""; + window.hide(); + }, + onKeyPressEvent: (_, event: Gdk.Event) => { + if(event.get_keyval()[1] === Gdk.KEY_Escape) { + searchString.set(""); + entry.text = ""; + window.hide(); + } + }, + child: new Widget.Box({ + className: "apps-window-container", + expand: true, + orientation: Gtk.Orientation.VERTICAL, + children: [ + entry, + new Widget.Scrollable({ + vscroll: Gtk.PolicyType.AUTOMATIC, + hscroll: Gtk.PolicyType.NEVER, + expand: true, + child: flowbox + } as Widget.ScrollableProps) + ] + } as Widget.BoxProps) + } as Widget.EventBoxProps)); + } +} as Widget.WindowProps); diff --git a/ags/window/Bar.ts b/ags/window/Bar.ts index 416246c..3c74377 100644 --- a/ags/window/Bar.ts +++ b/ags/window/Bar.ts @@ -1,12 +1,12 @@ import { Astal, Gtk, Widget } from "astal/gtk3"; import { Clock } from "../widget/bar/Clock"; -import { Logo } from "../widget/bar/Logo"; import { Tray } from "../widget/bar/Tray"; import { Workspaces } from "../widget/bar/Workspaces"; -import { Audio } from "../widget/bar/Audio"; import { FocusedClient } from "../widget/bar/FocusedClient"; import { Media } from "../widget/bar/Media"; +import { Status } from "../widget/bar/Status"; +import { Apps } from "../widget/bar/Apps"; export const Bar: Widget.Window = new Widget.Window({ monitor: 0, @@ -14,6 +14,7 @@ export const Bar: Widget.Window = new Widget.Window({ anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT, layer: Astal.Layer.TOP, exclusivity: Astal.Exclusivity.EXCLUSIVE, + heightRequest: 46, canFocus: false, visible: true, child: new Widget.Box({ @@ -27,7 +28,7 @@ export const Bar: Widget.Window = new Widget.Window({ homogeneous: false, halign: Gtk.Align.START, children: [ - Logo(), + Apps(), Workspaces(), FocusedClient() ] @@ -47,7 +48,7 @@ export const Bar: Widget.Window = new Widget.Window({ halign: Gtk.Align.END, children: [ Tray(), - Audio() + Status() ] } as Widget.BoxProps) } as Widget.CenterBoxProps) diff --git a/ags/window/ControlCenter.ts b/ags/window/ControlCenter.ts index 8a0f0e4..31f2e28 100644 --- a/ags/window/ControlCenter.ts +++ b/ags/window/ControlCenter.ts @@ -1,35 +1,58 @@ -import { Gtk, Widget } from "astal/gtk3"; +import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; import { QuickActions } from "../widget/control-center/QuickActions"; import { Tiles } from "../widget/control-center/Tiles"; import { Sliders } from "../widget/control-center/Sliders"; -import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; import { hidePages, PagesWidget } from "../widget/control-center/Pages"; import { NotifHistory } from "../widget/control-center/NotifHistory"; const connections: Array = []; +const { TOP, LEFT, BOTTOM, RIGHT } = Astal.WindowAnchor; -export const ControlCenter: Widget.Window = PopupWindow({ - className: "control-center", +export const ControlCenter = new Widget.Window({ namespace: "control-center", - marginTop: 10, - marginRight: 10, - monitor: 0, - onClose: () => hidePages(), - halign: Gtk.Align.END, - valign: Gtk.Align.START, + className: "control-center", + anchor: TOP | BOTTOM | LEFT | RIGHT, + exclusivity: Astal.Exclusivity.NORMAL, + keymode: Astal.Keymode.EXCLUSIVE, + layer: Astal.Layer.OVERLAY, + focusOnMap: true, visible: false, - vexpand: true, + monitor: 0, + onDestroy: (_) => connections.map(id => _.disconnect(id)), + onButtonPressEvent: (_, event: Gdk.Event) => { + const [, posX, posY] = event.get_coords(); + const childAllocation = _.get_child()!.get_allocation(); + + if((posX < childAllocation.x || posX > (childAllocation.x + childAllocation.width)) || + (posY < childAllocation.y || posY > (childAllocation.y + childAllocation.height))) { + _.hide(); + hidePages(); + } + }, + onKeyPressEvent: (_, event: Gdk.Event) => { + if(event.get_keyval()[1] === Gdk.KEY_Escape) { + _.hide(); + hidePages(); + } + }, child: new Widget.Box({ - orientation: Gtk.Orientation.VERTICAL, - expand: true, + className: "popup", + halign: Gtk.Align.END, + css: `.popup { + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + }`, vexpand: true, + widthRequest: 400, + onButtonPressEvent: () => true, + orientation: Gtk.Orientation.VERTICAL, children: [ new Widget.Box({ className: "control-center-container", orientation: Gtk.Orientation.VERTICAL, widthRequest: 400, vexpand: false, - hexpand: true, children: [ QuickActions, Sliders, @@ -40,7 +63,7 @@ export const ControlCenter: Widget.Window = PopupWindow({ NotifHistory ] } as Widget.BoxProps) -} as PopupWindowProps); +} as Widget.WindowProps); connections.push(ControlCenter.connect("hide", (_) => { hidePages();