From 7b758bd298c2ada28d2c411194479e1e9da78591 Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Sun, 6 Jul 2025 19:55:27 -0300 Subject: [PATCH] :sparkles: chore: migrate bar widgets to ags v3 and gtk4 --- ags/widget/bar/Apps.ts | 20 ---- ags/widget/bar/Apps.tsx | 16 +++ ags/widget/bar/Clock.ts | 17 --- ags/widget/bar/Clock.tsx | 20 ++++ ags/widget/bar/FocusedClient.ts | 53 --------- ags/widget/bar/FocusedClient.tsx | 39 +++++++ ags/widget/bar/Media.ts | 141 ------------------------ ags/widget/bar/Media.tsx | 160 +++++++++++++++++++++++++++ ags/widget/bar/Status.ts | 170 ----------------------------- ags/widget/bar/Status.tsx | 130 ++++++++++++++++++++++ ags/widget/bar/Tray.ts | 49 --------- ags/widget/bar/Tray.tsx | 60 ++++++++++ ags/widget/bar/Workspaces.ts | 181 ------------------------------- ags/widget/bar/Workspaces.tsx | 144 ++++++++++++++++++++++++ 14 files changed, 569 insertions(+), 631 deletions(-) delete mode 100644 ags/widget/bar/Apps.ts create mode 100644 ags/widget/bar/Apps.tsx delete mode 100644 ags/widget/bar/Clock.ts create mode 100644 ags/widget/bar/Clock.tsx delete mode 100644 ags/widget/bar/FocusedClient.ts create mode 100644 ags/widget/bar/FocusedClient.tsx delete mode 100644 ags/widget/bar/Media.ts create mode 100644 ags/widget/bar/Media.tsx delete mode 100644 ags/widget/bar/Status.ts create mode 100644 ags/widget/bar/Status.tsx delete mode 100644 ags/widget/bar/Tray.ts create mode 100644 ags/widget/bar/Tray.tsx delete mode 100644 ags/widget/bar/Workspaces.ts create mode 100644 ags/widget/bar/Workspaces.tsx diff --git a/ags/widget/bar/Apps.ts b/ags/widget/bar/Apps.ts deleted file mode 100644 index e80a83b..0000000 --- a/ags/widget/bar/Apps.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Gtk, Widget } from "astal/gtk3"; -import { tr } from "../../i18n/intl"; -import { Windows } from "../../windows"; -import { bind } from "astal"; - -export function Apps(): Gtk.Widget { - return new Widget.EventBox({ - onClickRelease: () => Windows.open("apps-window"), - className: bind(Windows, "openWindows").as((openWindows) => - Object.hasOwn(openWindows, "apps-window") ? "apps open" : "apps"), - child: new Widget.Box({ - child: new Widget.Icon({ - tooltipText: tr("apps"), - icon: "applications-other-symbolic", - halign: Gtk.Align.CENTER, - hexpand: true - } as Widget.IconProps) - } as Widget.BoxProps) - } as Widget.EventBoxProps); -} diff --git a/ags/widget/bar/Apps.tsx b/ags/widget/bar/Apps.tsx new file mode 100644 index 0000000..56553fa --- /dev/null +++ b/ags/widget/bar/Apps.tsx @@ -0,0 +1,16 @@ +import { Gtk } from "ags/gtk4"; +import { Windows } from "../../windows"; +import { createBinding } from "ags"; +import { tr } from "../../i18n/intl"; + +export const Apps = () => + + `apps ${Object.hasOwn(openWindows, "apps-window") ? "open" : ""}` + )} $={(self) => { + const conns: Array = [ + self.connect("clicked", (_) => Windows.getDefault().open("apps-window")), + self.connect("destroy", (_) => conns.forEach(id => self.disconnect(id))) + ]; + }} iconName={"applications-other-symbolic"} halign={Gtk.Align.CENTER} + hexpand={true} tooltipText={tr("apps")} + />; diff --git a/ags/widget/bar/Clock.ts b/ags/widget/bar/Clock.ts deleted file mode 100644 index e137dd7..0000000 --- a/ags/widget/bar/Clock.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Gtk, Widget } from "astal/gtk3"; -import { getDateTime } from "../../scripts/time"; -import { bind, GLib } from "astal"; -import { Windows } from "../../windows"; -import { Config } from "../../scripts/config"; - -export function Clock(): Gtk.Widget { - return new Widget.Box({ - className: bind(Windows, "openWindows").as((openWins) => - Object.hasOwn(openWins, "center-window") ? "open clock" : "clock"), - child: new Widget.Button({ - onClick: () => Windows.toggle("center-window"), - label: getDateTime().as((dateTime: GLib.DateTime) => - dateTime.format(Config.getDefault().getProperty("clock.date_format", "string") as string)) - } as Widget.ButtonProps) - } as Widget.BoxProps); -} diff --git a/ags/widget/bar/Clock.tsx b/ags/widget/bar/Clock.tsx new file mode 100644 index 0000000..2ad44ac --- /dev/null +++ b/ags/widget/bar/Clock.tsx @@ -0,0 +1,20 @@ +import { Gtk } from "ags/gtk4"; +import { Windows } from "../../windows"; +import { createBinding } from "ags"; +import { time } from "../../scripts/utils"; +import { Config } from "../../scripts/config"; + +export const Clock = () => + + `clock ${Object.hasOwn(wins, "center-window") ? "open" : ""}`)} + $={(self) => { + const conns: Array = [ + self.connect("clicked", (_) => Windows.getDefault().toggle("center-window")), + self.connect("destroy", (_) => conns.forEach(id => self.disconnect(id))) + ]; + }} + label={time((dt) => dt.format( + Config.getDefault().getProperty("clock.date_format", "string")) + ?? "An error occurred" + )} + />; diff --git a/ags/widget/bar/FocusedClient.ts b/ags/widget/bar/FocusedClient.ts deleted file mode 100644 index c099fe6..0000000 --- a/ags/widget/bar/FocusedClient.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { bind } from "astal"; -import { Gtk, Widget } from "astal/gtk3"; -import AstalHyprland from "gi://AstalHyprland"; -import { getAppIcon } from "../../scripts/apps"; - -const hyprland = AstalHyprland.get_default(); - -export function FocusedClient(): Gtk.Widget { - return new Widget.Box({ - className: "focused-client", - visible: bind(hyprland, "focusedClient").as(fClient => - !fClient ? false : (fClient?.initialClass == null ? false : true)), - children: bind(hyprland, "focusedClient").as(focusedClient => focusedClient ? [ - new Widget.Icon({ - className: "icon", - vexpand: true, - css: ".icon { font-size: 18px; }", - icon: bind(focusedClient, "class").as(clss => - getAppIcon(clss) ?? "application-x-executable-symbolic") - }), - new Widget.Box({ - className: "text-content", - orientation: Gtk.Orientation.VERTICAL, - homogeneous: false, - valign: Gtk.Align.CENTER, - children: [ - new Widget.Label({ - className: "class", - xalign: 0, - visible: bind(focusedClient, "class").as(Boolean), - maxWidthChars: 55, - truncate: true, - tooltipText: bind(focusedClient, "class").as(clientClass => - clientClass ?? ""), - label: bind(focusedClient, "class").as(clientClass => - clientClass ?? "no_class") - } as Widget.LabelProps), - new Widget.Label({ - className: "title", - xalign: 0, - maxWidthChars: 50, - visible: bind(focusedClient, "title").as(Boolean), - truncate: true, - tooltipText: bind(focusedClient, "title").as((clientTitle: string) => - clientTitle ?? ""), - label: bind(focusedClient, "title").as(title => - title ?? "") - } as Widget.LabelProps) - ] - }) - ]: []) - } as Widget.BoxProps); -} diff --git a/ags/widget/bar/FocusedClient.tsx b/ags/widget/bar/FocusedClient.tsx new file mode 100644 index 0000000..8650c1c --- /dev/null +++ b/ags/widget/bar/FocusedClient.tsx @@ -0,0 +1,39 @@ +import { Gtk } from "ags/gtk4"; +import AstalHyprland from "gi://AstalHyprland"; +import { createBinding, With } from "ags"; +import { variableToBoolean } from "../../scripts/utils"; +import { getAppIcon, getSymbolicIcon } from "../../scripts/apps"; +import Pango from "gi://Pango?version=1.0"; + +const hyprland = AstalHyprland.get_default(); + +export const FocusedClient = () => { + const focusedClient = createBinding(hyprland, "focusedClient"); + + return + + {(focusedClient) => focusedClient && + + getSymbolicIcon(clss) ?? getAppIcon(clss) ?? + getAppIcon(focusedClient.initialClass) ?? + "application-x-executable-symbolic") + } css={"font-size: 18px;"} vexpand={true} /> + + + + + + + } + + ; +} diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts deleted file mode 100644 index a2487cb..0000000 --- a/ags/widget/bar/Media.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { bind, exec } from "astal"; -import { Gtk, Widget } from "astal/gtk3"; -import AstalMpris from "gi://AstalMpris"; -import { getSymbolicIcon } from "../../scripts/apps"; -import { Separator, SeparatorProps } from "../Separator"; -import { Windows } from "../../windows"; -import { Clipboard } from "../../scripts/clipboard"; - -export function Media(): Gtk.Widget { - const connections: Array = []; - - const mediaControlsRevealer: Widget.Revealer = new Widget.Revealer({ - transitionType: Gtk.RevealerTransitionType.SLIDE_RIGHT, - transitionDuration: 260, - revealChild: false, - child: new Widget.Box({ - className: "media-controls button-row", - expand: false, - homogeneous: false, - children: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] ? [ - new Widget.Button({ - className: "link", - image: new Widget.Icon({ - icon: "edit-paste-symbolic" - } as Widget.IconProps), - tooltipText: "Copy link to Clipboard", - // AstalMpris.Player.metadata works only sometimes, so I'm not using it - visible: bind(players[0], "metadata").as(Boolean), - onClick: async () => { - const link = exec(`playerctl --player=${ - players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") - } metadata xesam:url`); - - link && Clipboard.getDefault().copyAsync(link); - } - } as Widget.ButtonProps), - new Widget.Button({ - className: "previous", - image: new Widget.Icon({ - icon: "media-skip-backward-symbolic" - } as Widget.IconProps), - tooltipText: "Previous", - onClick: () => players[0].canGoPrevious && players[0].previous() - } as Widget.ButtonProps), - new Widget.Button({ - className: "play-pause", - tooltipText: bind(players[0], "playback_status").as((status) => - status === AstalMpris.PlaybackStatus.PLAYING ? - "Pause" - : "Play"), - image: new Widget.Icon({ - icon: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) => - status === AstalMpris.PlaybackStatus.PLAYING ? - "media-playback-pause-symbolic" - : "media-playback-start-symbolic") - } as Widget.IconProps), - onClick: () => players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? - players[0].play() - : players[0].pause() - } as Widget.ButtonProps), - new Widget.Button({ - className: "next", - image: new Widget.Icon({ - icon: "media-skip-forward-symbolic" - } as Widget.IconProps), - tooltipText: "Next", - onClick: () => players[0].canGoNext && players[0].next() - } as Widget.ButtonProps) - ] : new Widget.Label({ - label: "Don't Stop The Music!" - } as Widget.LabelProps) - ) - } as Widget.BoxProps) - } as Widget.RevealerProps); - - const mediaWidget = new Widget.EventBox({ - className: "media-eventbox", - visible: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] && players[0].get_available()), - onDestroy: (_) => connections.map(id => _.disconnect(id)), - onClick: () => Windows.toggle("center-window"), - child: new Widget.Box({ - className: "media", - children: [ - new Widget.Box({ - spacing: 4, - children: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] ? [ - new Widget.Icon({ - icon: bind(players[0], "busName").as((busName: string) => { - const splitName = busName.split('.').filter(str => str !== "" && !str.toLowerCase().includes('instance')); - if (getSymbolicIcon(splitName[splitName.length - 1])) { - return getSymbolicIcon(splitName[splitName.length - 1]); - } else { - return "folder-music-symbolic" - }; - }) - } as Widget.IconProps), - new Widget.Label({ - className: "title", - label: bind(players[0], "title").as((title: string) => title || "No Title"), - maxWidthChars: 20, - truncate: true - } as Widget.LabelProps), - Separator({ - orientation: Gtk.Orientation.HORIZONTAL, - size: 1, - margin: 5, - //cssColor: `rgb(180, 180, 180)`, - alpha: .3 - } as SeparatorProps), - new Widget.Label({ - className: "artist", - label: bind(players[0], "artist").as((artist: string) => artist || "No Artist"), - maxWidthChars: 18, - truncate: true - } as Widget.LabelProps) - ] : new Widget.Label({ - label: "Crazy to think this widget haven't disappeared yet!" - } as Widget.LabelProps) - ) - } as Widget.BoxProps), - mediaControlsRevealer - ] - } as Widget.BoxProps) - } as Widget.EventBoxProps); - - connections.push( - mediaWidget.connect("hover", () => { - mediaControlsRevealer.set_reveal_child(true); - mediaWidget.className = mediaWidget.className + " reveal"; - }), - mediaWidget.connect("hover-lost", (_) => { - mediaControlsRevealer.set_reveal_child(false); - _.className = mediaWidget.className.replaceAll(" reveal", ""); - }) - ); - - return mediaWidget; -} diff --git a/ags/widget/bar/Media.tsx b/ags/widget/bar/Media.tsx new file mode 100644 index 0000000..f6fc265 --- /dev/null +++ b/ags/widget/bar/Media.tsx @@ -0,0 +1,160 @@ +import { createBinding, createState, With } from "ags"; +import { execAsync } from "ags/process"; +import { Gtk } from "ags/gtk4"; +import { getSymbolicIcon } from "../../scripts/apps"; +import { Separator } from "../Separator"; +import { Windows } from "../../windows"; +import { Clipboard } from "../../scripts/clipboard"; + +import GObject from "ags/gobject"; +import AstalMpris from "gi://AstalMpris"; +import Pango from "gi://Pango?version=1.0"; + + +export const dummyPlayer = AstalMpris.Player.new("colorshellDummy"); + +export let [player, setPlayer] = createState(dummyPlayer); + +export const Media = () => { + const connections: Map|number> = new Map(); + + if(AstalMpris.get_default().players[0] && player.get() !== dummyPlayer) + setPlayer(AstalMpris.get_default().players[0]); + + connections.set(AstalMpris.get_default(), [ + AstalMpris.get_default().connect("player-added", (_, player) => + player.available && setPlayer(player)), + + AstalMpris.get_default().connect("player-closed", (_, closedPlayer) => { + if(player.get()?.busName !== closedPlayer.busName) + return; + + const players = AstalMpris.get_default().players.filter(pl => pl?.available); + + if(players.length > 0) { + setPlayer(players[0]); + return; + } else setPlayer(dummyPlayer); + }) + ]); + + return pl.available)} + $={(self) => { + const gestureClick = Gtk.GestureClick.new(), + controllerMotion = Gtk.EventControllerMotion.new(), + controllerScroll = Gtk.EventControllerScroll.new( + Gtk.EventControllerScrollFlags.VERTICAL); + + self.add_controller(gestureClick); + self.add_controller(controllerMotion); + self.add_controller(controllerScroll); + + connections.set(gestureClick, gestureClick.connect("released", () => + Windows.getDefault().toggle("center-window"))); + + connections.set(controllerScroll, + controllerScroll.connect("scroll", (_, _dx, dy) => { + if(AstalMpris.get_default().players.length === 1 && + player.get()?.busName === AstalMpris.get_default().players[0].busName) + return true; + + const players = AstalMpris.get_default().players; + + for(let i = 0; i < players.length; i++) { + const pl = players[i]; + + if(pl.busName !== player.get().busName) + continue; + + if(dy > 0 && players[i-1]) { + setPlayer(players[i-1]); + break; + } + + if(dy < 0 && players[i+1]) { + setPlayer(players[i+1]); + break; + } + } + + return true; + }) + ); + + connections.set(controllerMotion, [ + controllerMotion.connect("enter", () => { + const revealer = self.get_last_child() as Gtk.Revealer; + revealer.set_reveal_child(true); + }), + controllerMotion.connect("leave", () => { + const revealer = self.get_last_child() as Gtk.Revealer; + revealer.set_reveal_child(false); + }) + ]); + + connections.set(self, self.connect("destroy", () => + connections.forEach((ids, obj) => Array.isArray(ids) ? + ids.forEach(id => obj.disconnect(id)) + : obj.disconnect(ids)) + )); + }}> + + pl.available)}> + pl.available)}> + {(available: boolean) => available && + { + const splitName = busName.split('.').filter(str => str !== "" && !str.toLowerCase().includes('instance')); + return getSymbolicIcon(splitName[splitName.length - 1]) ? + getSymbolicIcon(splitName[splitName.length - 1])! + : "folder-music-symbolic"; + })} + /> + + title ?? "No Title")} maxWidthChars={20} ellipsize={Pango.EllipsizeMode.END} + /> + + + artist ?? "No Artist")} maxWidthChars={18} ellipsize={Pango.EllipsizeMode.END} + /> + } + + + + + pl.available)}> + {(available: boolean) => available && + { + execAsync(`playerctl --player=${ + player.get().busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") + } metadata xesam:url`).then(link => { + Clipboard.getDefault().copyAsync(link); + }).catch((e: Error) => { + console.error(`Media: couldn't copy media link. Stderr: \n${e.message}\n${e.stack}`); + }); + }} + /> + + player.get().canGoPrevious && player.get().previous()} + /> + + status === AstalMpris.PlaybackStatus.PAUSED ? + "media-playback-start-symbolic" + : "media-playback-pause-symbolic")} + tooltipText={ + createBinding(player.get(), "playbackStatus").as(status => + status === AstalMpris.PlaybackStatus.PAUSED ? "Play" : "Pause") + } onClicked={player.get().play_pause} + /> + player.get().canGoNext && + player.get().next()} + /> + } + + + +} diff --git a/ags/widget/bar/Status.ts b/ags/widget/bar/Status.ts deleted file mode 100644 index c075981..0000000 --- a/ags/widget/bar/Status.ts +++ /dev/null @@ -1,170 +0,0 @@ -import AstalBluetooth from "gi://AstalBluetooth"; -import AstalNetwork from "gi://AstalNetwork"; -import AstalWp from "gi://AstalWp"; - -import { bind, Binding, Variable } from "astal"; -import { Gtk, Widget } from "astal/gtk3"; -import { Wireplumber } from "../../scripts/volume"; -import { Notifications } from "../../scripts/notifications"; -import { Windows } from "../../windows"; -import { Recording } from "../../scripts/recording"; -import { getDateTime } from "../../scripts/time"; -import { tr } from "../../i18n/intl"; - - -export function Status(): Gtk.Widget { - const recordingTimer: Variable = Variable.derive([ - bind(Recording.getDefault(), "recording"), - getDateTime() - ], (recording, dateTime) => { - if(!recording || !Recording.getDefault().startedAt) - return "..."; - - const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!.to_unix(); - if(startedAtSeconds <= 0) return "00:00"; - - const minutes = Math.floor(startedAtSeconds / 60); - const seconds = Math.floor(startedAtSeconds % 60); - - return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`; - }); - - return new Widget.EventBox({ - className: bind(Windows, "openWindows").as((openWins) => - Object.hasOwn(openWins, "control-center") ? "open status" : "status"), - onClick: () => Windows.toggle("control-center"), - child: new Widget.Box({ - children: [ - new Widget.Box({ - className: "volume-indicators", - spacing: 5, - children: [ - volumeStatus({ - className: "sink", - endpoint: Wireplumber.getDefault().getDefaultSink(), - icon: bind(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon => - !Wireplumber.getDefault().isMutedSink() && Wireplumber.getDefault().getSinkVolume() > 0 ? - icon : "audio-volume-muted-symbolic"), - }), - volumeStatus({ - className: "source", - endpoint: Wireplumber.getDefault().getDefaultSource(), - icon: bind(Wireplumber.getDefault().getDefaultSource(), "volumeIcon").as(icon => - !Wireplumber.getDefault().isMutedSource() && Wireplumber.getDefault().getSourceVolume() > 0 ? - icon : "microphone-sensitivity-muted-symbolic"), - }) - ] - } as Widget.BoxProps), - new Widget.Revealer({ - revealChild: bind(Recording.getDefault(), "recording"), - transitionDuration: 500, - transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT, - onDestroy: () => recordingTimer.drop(), - child: new Widget.EventBox({ - onClick: () => Recording.getDefault().recording && - Recording.getDefault().stopRecording(), - tooltipText: tr("control_center.tiles.recording.enabled_desc"), - child: new Widget.Box({ - children: [ - new Widget.Icon({ - className: "recording state", - icon: "media-record-symbolic", - css: "margin-right: 4px;" - } as Widget.IconProps), - new Widget.Label({ - className: "rec-time", - label: recordingTimer() - } as Widget.LabelProps) - ] - } as Widget.BoxProps) - } as Widget.EventBoxProps) - } as Widget.RevealerProps), - StatusIcons() - ] - } as Widget.BoxProps) - } as Widget.EventBoxProps); -} - -function volumeStatus(props: { className?: string, endpoint: AstalWp.Endpoint, icon?: (string|Binding) }): 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), - child: new Widget.Box({ - spacing: 2, - children: [ - new Widget.Icon({ - visible: props.icon, - icon: props.icon, - } as Widget.IconProps), - 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 { - const bluetoothIcon: Variable = Variable.derive([ - bind(AstalBluetooth.get_default(), "isPowered"), - bind(AstalBluetooth.get_default(), "isConnected") - ], (powered, connected) => { - return powered ? ( - connected ? - "bluetooth-active-symbolic" - : "bluetooth-symbolic" - ) : "bluetooth-disabled-symbolic" - }); - - const networkIcon: Variable = Variable.derive([ - bind(AstalNetwork.get_default(), "primary"), - ], - (primary) => { - switch(primary) { - case AstalNetwork.Primary.WIRED: return AstalNetwork.get_default().wired.get_icon_name(); - - case AstalNetwork.Primary.WIFI: return AstalNetwork.get_default().wifi.get_icon_name(); - } - - return "network-no-route-symbolic"; - }); - - return new Widget.Box({ - className: "status-icons", - spacing: 8, - children: [ - new Widget.Icon({ - className: "bluetooth state", - visible: bind(AstalBluetooth.get_default(), "adapter").as(Boolean), - icon: bluetoothIcon(), - onDestroy: () => bluetoothIcon.drop() - } as Widget.IconProps), - new Widget.Icon({ - className: "network state", - icon: networkIcon(), - onDestroy: () => networkIcon.drop() - } as Widget.IconProps), - new Widget.Box({ - children: [ - new Widget.Icon({ - className: "bell state", - icon: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as((dnd) => - dnd ? "minus-circle-filled-symbolic" - : "preferences-system-notifications-symbolic") - } as Widget.IconProps), - new Widget.Icon({ - className: "notification-count", - visible: bind(Notifications.getDefault(), "history").as(history => - history.length > 0), - icon: "circle-filled-symbolic" - } as Widget.IconProps) - ] - } as Widget.BoxProps) - ] - } as Widget.BoxProps); -} diff --git a/ags/widget/bar/Status.tsx b/ags/widget/bar/Status.tsx new file mode 100644 index 0000000..2135ef5 --- /dev/null +++ b/ags/widget/bar/Status.tsx @@ -0,0 +1,130 @@ +import { Gtk } from "ags/gtk4"; +import { Wireplumber } from "../../scripts/volume"; +import { Notifications } from "../../scripts/notifications"; +import { Windows } from "../../windows"; +import { Recording } from "../../scripts/recording"; +import { Accessor, createBinding, createComputed } from "ags"; +import { time, variableToBoolean } from "../../scripts/utils"; + +import AstalBluetooth from "gi://AstalBluetooth"; +import AstalNetwork from "gi://AstalNetwork"; +import AstalWp from "gi://AstalWp"; +import GObject from "gi://GObject?version=2.0"; + + +export const Status = () => + + Object.hasOwn(openWins, "control-center") ? "open status" : "status")} + onClicked={() => Windows.getDefault().toggle("control-center")}> + + + + + !Wireplumber.getDefault().isMutedSink() && + Wireplumber.getDefault().getSinkVolume() > 0 ? + icon + : "audio-volume-muted-symbolic") + } /> + + + !Wireplumber.getDefault().isMutedSource() && + Wireplumber.getDefault().getSourceVolume() > 0 ? + icon + : "audio-volume-muted-symbolic") + } /> + + + + + + + { + if(!recording || !Recording.getDefault().startedAt) + return "..."; + + const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!; + if(startedAtSeconds <= 0) return "00:00"; + + const minutes = Math.floor(startedAtSeconds / 60); + const seconds = Math.floor(startedAtSeconds % 60); + + return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`; + })} + /> + + + + + as Gtk.Button; + +function VolumeStatus(props: { class?: string, endpoint: AstalWp.Endpoint, icon?: (string|Accessor) }) { + return { + const conns: Map = new Map(); + const controllerScroll = Gtk.EventControllerScroll.new( + Gtk.EventControllerScrollFlags.VERTICAL); + + conns.set(controllerScroll, controllerScroll.connect("scroll", (_, _dx, dy) => { + (dy > 0) ? + Wireplumber.getDefault().decreaseEndpointVolume(props.endpoint, 5) + : Wireplumber.getDefault().increaseEndpointVolume(props.endpoint, 5); + })); + + conns.set(self, self.connect("destroy", () => conns.forEach((id, obj) => + obj.disconnect(id)))); + }}> + + {props.icon && } + + `${Math.floor(vol * 100)}%`)} /> + as Gtk.Box; +} + +function StatusIcons() { + return + { + return powered ? ( + connected ? + "bluetooth-active-symbolic" + : "bluetooth-symbolic" + ) : "bluetooth-disabled-symbolic" + })} class={"bluetooth state"} visible={ + createBinding(AstalBluetooth.get_default(), "adapter").as(Boolean) + } + /> + + { + switch(primary) { + case AstalNetwork.Primary.WIRED: return AstalNetwork.get_default().wired.get_icon_name(); + + case AstalNetwork.Primary.WIFI: return AstalNetwork.get_default().wifi.get_icon_name(); + } + + return "network-no-route-symbolic"; + })} class={"network state"} + visible={createBinding(AstalNetwork.get_default(), "primary").as(primary => + primary !== AstalNetwork.Primary.UNKNOWN)} + /> + + + dnd ? + "minus-circle-filled-symbolic" + : "preferences-system-notifications-symbolic") + } + /> + + + +} diff --git a/ags/widget/bar/Tray.ts b/ags/widget/bar/Tray.ts deleted file mode 100644 index 512b442..0000000 --- a/ags/widget/bar/Tray.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { bind, Gio, Variable } from "astal"; -import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; -import AstalTray from "gi://AstalTray" - -const astalTray = AstalTray.get_default(); - -function menuFromModel(model: Gio.MenuModel, actionGroup: Gio.ActionGroup | null): Gtk.Menu { - const menu = Gtk.Menu.new_from_model(model); - menu.insert_action_group("dbusmenu", actionGroup) - - return menu; -} - -export function Tray(): Gtk.Widget { - return new Widget.Box({ - className: "tray", - visible: bind(astalTray, "items").as((items: Array) => items.length > 0), - children: bind(astalTray, "items").as((items: Array) => items - .filter(item => item?.gicon) - .map((item: AstalTray.TrayItem) => - new Widget.Box({ - className: "item", - child: Variable.derive( - [ bind(item, "menuModel"), bind(item, "actionGroup") ], - (menuModel: Gio.MenuModel, actionGroup: Gio.ActionGroup) => { - const menu = menuFromModel(menuModel, actionGroup); - - return new Widget.Button({ - className: "item-button", - tooltipMarkup: bind(item, "tooltipMarkup"), - onClick: (_, event: Astal.ClickEvent) => { - if(event.button === Astal.MouseButton.SECONDARY) { - item.about_to_show(); - menu.popup_at_widget(_, Gdk.Gravity.NORTH, Gdk.Gravity.SOUTH_WEST, null); - } else if(event.button === Astal.MouseButton.PRIMARY) - item.activate(event.x, event.y); - }, - halign: Gtk.Align.CENTER, - child: new Widget.Icon({ - gIcon: bind(item, "gicon") - }) - } as Widget.ButtonProps) - } - )() - } as Widget.BoxProps) - ) - ) - } as Widget.BoxProps); -} diff --git a/ags/widget/bar/Tray.tsx b/ags/widget/bar/Tray.tsx new file mode 100644 index 0000000..c0efdb6 --- /dev/null +++ b/ags/widget/bar/Tray.tsx @@ -0,0 +1,60 @@ +import { createBinding, createComputed, For, With } from "ags"; +import { Gdk, Gtk } from "ags/gtk4"; + +import AstalTray from "gi://AstalTray" +import Gio from "gi://Gio?version=2.0"; +import { variableToBoolean } from "../../scripts/utils"; +import GObject from "gi://GObject?version=2.0"; + + +const astalTray = AstalTray.get_default(); + +function popoverFromModel(model: Gio.MenuModel, actionGroup: Gio.ActionGroup | null): Gtk.PopoverMenu { + const menu = Gtk.PopoverMenu.new_from_model(model); + menu.insert_action_group("dbusmenu", actionGroup) + + return menu; +} + +export const Tray = () => { + const items = createBinding(astalTray, "items").as(items => items.filter(item => item?.gicon)); + + return + + {(item: AstalTray.TrayItem) => + + + {([actionGroup, menuModel]: [Gio.ActionGroup, Gio.MenuModel]) => { + const popover = popoverFromModel(menuModel, actionGroup); + + return { + const conns: Map = new Map(); + const gestureClick = Gtk.GestureClick.new(); + + self.add_controller(gestureClick); + + conns.set(gestureClick, gestureClick.connect("released", (gesture, _, x, y) => { + if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) { + item.activate(x, y); + return; + } else if(gesture.get_current_button() === Gdk.BUTTON_SECONDARY) { + item.about_to_show(); + self.popup(); + } + })) + }}> + + + + }} + + } + + +} diff --git a/ags/widget/bar/Workspaces.ts b/ags/widget/bar/Workspaces.ts deleted file mode 100644 index 217ed60..0000000 --- a/ags/widget/bar/Workspaces.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { bind, Variable } from "astal"; -import { Gtk, Widget } from "astal/gtk3"; -import AstalHyprland from "gi://AstalHyprland"; -import { getAppIcon, getSymbolicIcon } from "../../scripts/apps"; -import { Windows } from "../../windows"; -import { Config } from "../../scripts/config"; -import { Separator, SeparatorProps } from "../Separator"; - -let showWsNum: (Variable|undefined); -export const showWorkspaceNumber = (show: boolean) => - showWsNum?.set(show); - - -export function Workspaces(): Gtk.Widget { - showWsNum ??= new Variable(false); - - return new Widget.Box({ - className: "workspaces-row", - orientation: Gtk.Orientation.HORIZONTAL, - children: [ - new Widget.EventBox({ - className: "special", - visible: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) => - workspaces.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id).length > 0), - child: new Widget.Box({ - className: "special-workspaces", - spacing: 4, - children: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) => - workspaces.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id).map((workspace) => - new Widget.EventBox({ - className: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusWs => - `${focusWs.id === workspace.id ? "focus" : ""}`), - tooltipText: bind(workspace, "name").as((name) => { - name = name.replace(/^special\:/, ""); - return name.charAt(0).toUpperCase().concat(name.substring(1, name.length)); - }), - child: new Widget.Box({ - hexpand: true, - child: bind(workspace, "lastClient").as(lastClient => - new Widget.Icon({ - className: "last-app-icon", - halign: Gtk.Align.CENTER, - visible: Variable.derive([ - bind(workspace, "lastClient"), - bind(AstalHyprland.get_default(), "focusedWorkspace") - ], (lastClient, focusedWorkspace) => focusedWorkspace?.id === workspace.id ? - false : Boolean(lastClient))(), - icon: bind(lastClient, "initialClass").as((initialClass) => - getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ?? - "application-x-executable-symbolic") - } as Widget.IconProps) - ) - } as Widget.BoxProps), - onClickRelease: () => AstalHyprland.get_default().dispatch( - "togglespecialworkspace", workspace.name.replace(/^special\:/, "") - ) - } as Widget.EventBoxProps) - ) - ) - } as Widget.BoxProps) - } as Widget.EventBoxProps), - Separator({ - alpha: .2, - orientation: Gtk.Orientation.HORIZONTAL, - margin: 12, - spacing: 8, - visible: bind(AstalHyprland.get_default(), "workspaces").as(wss => - wss.filter(ws => ws.id < 0).length > 0) - } as SeparatorProps), - new Widget.EventBox({ - onScroll: (_, event) => - event.delta_y > 0 ? - AstalHyprland.get_default().dispatch("workspace", "e-1") - : AstalHyprland.get_default().dispatch("workspace", "e+1"), - onHover: () => showWorkspaceNumber(true), - onHoverLost: () => showWorkspaceNumber(false), - onDestroy: () => { - // check if the current widgets is from the only bar - if((Windows.openWindows["bar"] as (Array|undefined))?.length === 1) { - showWsNum?.drop(); - showWsNum = undefined; - } - }, - child: new Widget.Box({ - className: "workspaces", - spacing: 4, - children: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) => - workspaces.filter((ws) => ws.id > 0).sort((a, b) => a.id - b.id).map((workspace, wsIndex, workspaces) => { - - const showIds: Variable = Variable.derive([ - Config.getDefault().bindProperty("workspaces.always_show_id", "boolean").as(Boolean), - Config.getDefault().bindProperty("workspaces.enable_helper", "boolean").as(Boolean), - showWsNum!() - ], (alwaysShowIds, enableHelper, showIds) => { - if(enableHelper && !alwaysShowIds) { - const previousWorkspace = workspaces[wsIndex-1]; - const nextWorkspace = workspaces[wsIndex+1]; - - if((workspaces.filter((_, i) => i < wsIndex).length > 0 && - previousWorkspace?.id < (workspace.id-1)) || - (workspaces.filter((_, i) => i > wsIndex).length > 0 && - nextWorkspace?.id > (workspace.id+1))) { - - return true; - } - } - - return alwaysShowIds || showIds; - }); - - const className = Variable.derive([ - bind(AstalHyprland.get_default(), "focusedWorkspace"), - showIds!() - ], (focusedWs, showWsNumbers) => - `${focusedWs.id === workspace.id ? "focus" : ""} ${ - showWsNumbers ? "show" : ""}` - ); - - const tooltipText = Variable.derive([ - bind(workspace, "lastClient"), - bind(AstalHyprland.get_default(), "focusedWorkspace") - ], (lastClient, focusWs) => focusWs.id === workspace.id ? "" : - `Workspace ${workspace.id}${ lastClient ? ` - ${ - !lastClient.title.toLowerCase().includes(lastClient.class) ? - `${lastClient.get_class()}: ` - : "" - } ${lastClient.title}` : "" }` - ); - - return new Widget.EventBox({ - className: className(), - onClickRelease: () => workspace.focus(), - tooltipText: tooltipText(), - onDestroy: () => { - showIds.drop(); - className.drop(); - tooltipText.drop(); - }, - child: new Widget.Box({ - hexpand: true, - children: bind(workspace, "lastClient").as((lastClient) => { - const widgets: Array = [ - new Widget.Revealer({ - transitionDuration: 200, - transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT, - revealChild: showIds!(), - hexpand: true, - child: new Widget.Label({ - label: bind(workspace, "id").as(String), - className: "id", - } as Widget.LabelProps) - } as Widget.RevealerProps), - ]; - - if(lastClient) { - widgets.push(new Widget.Icon({ - className: "last-app-icon", - halign: Gtk.Align.CENTER, - expand: true, - visible: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusedWorkspace => - workspace.id === focusedWorkspace.id ? - false - : Boolean(lastClient)), - icon: lastClient ? - bind(lastClient, "initialClass").as((clss) => - getSymbolicIcon(clss) ?? getAppIcon(clss) ?? "application-x-executable-symbolic") - : undefined - } as Widget.IconProps)); - } - - return widgets; - }) - } as Widget.BoxProps) - } as Widget.EventBoxProps); - }) - ) - } as Widget.BoxProps) - } as Widget.EventBoxProps) - ] - } as Widget.BoxProps); -} diff --git a/ags/widget/bar/Workspaces.tsx b/ags/widget/bar/Workspaces.tsx new file mode 100644 index 0000000..52d2cae --- /dev/null +++ b/ags/widget/bar/Workspaces.tsx @@ -0,0 +1,144 @@ +import { Gtk } from "ags/gtk4"; +import AstalHyprland from "gi://AstalHyprland"; +import { getAppIcon, getSymbolicIcon } from "../../scripts/apps"; +import { Config } from "../../scripts/config"; +import { Separator } from "../Separator"; +import { createBinding, createComputed, createState, For, With } from "ags"; +import GObject from "gi://GObject?version=2.0"; +import { variableToBoolean } from "../../scripts/utils"; + +const [showNumbers, setShowNumbers] = createState(false); +export const showWorkspaceNumber = (show: boolean) => + setShowNumbers(show); + + +export const Workspaces = () => { + const workspaces = createBinding(AstalHyprland.get_default(), "workspaces"), + defaultWorkspaces = workspaces.as(wss => + wss.filter(ws => ws.id > 0).sort((a, b) => a.id - b.id)), + specialWorkspaces = workspaces.as(wss => + wss.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id)); + + + return + + + {(ws: AstalHyprland.Workspace) => + { + name = name.replace(/^special\:/, ""); + return name.charAt(0).toUpperCase().concat(name.substring(1, name.length)); + })} onClicked={() => AstalHyprland.get_default().dispatch( + "togglespecialworkspace", ws.name.replace(/^special[:]/, "") + )}> + + + {(lastClient: AstalHyprland.Client|null) => lastClient && + + getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ?? + "application-x-executable-symbolic")} + /> + } + + + } + + + + { + const conns: Map|number> = new Map(); + const controllerScroll = Gtk.EventControllerScroll.new( + Gtk.EventControllerScrollFlags.VERTICAL + ), controllerMotion = Gtk.EventControllerMotion.new(); + + self.add_controller(controllerScroll); + self.add_controller(controllerMotion); + + conns.set(controllerScroll, controllerScroll.connect("scroll", (_, _dx, dy) => { + dy > 0 ? + AstalHyprland.get_default().dispatch("workspace", "e-1") + : AstalHyprland.get_default().dispatch("workspace", "e+1"); + + return true; + })); + + conns.set(controllerMotion, [ + controllerMotion.connect("enter", () => setShowNumbers(true)), + controllerMotion.connect("leave", () => setShowNumbers(false)) + ]); + + conns.set(self, self.connect("destroy", () => conns.forEach((ids, obj) => + Array.isArray(ids) ? + ids.forEach(id => obj.disconnect(id)) + : obj.disconnect(ids) + ))); + }}> + + {(ws: AstalHyprland.Workspace, i) => { + const showId = createComputed([ + Config.getDefault().bindProperty("workspaces.always_show_id", "boolean").as(Boolean), + Config.getDefault().bindProperty("workspaces.enable_helper", "boolean").as(Boolean), + showNumbers + ], (alwaysShowIds, enableHelper, showIds) => { + if(enableHelper && !alwaysShowIds) { + const previousWorkspace = defaultWorkspaces.get()[i.get()-1]; + const nextWorkspace = defaultWorkspaces.get()[i.get()+1]; + + if((defaultWorkspaces.get().filter((_, ii) => ii < i.get()).length > 0 && + previousWorkspace?.id < (ws.id-1)) || + (defaultWorkspaces.get().filter((_, ii) => ii > i.get()).length > 0 && + nextWorkspace?.id > (ws.id+1))) { + + return true; + } + } + + return alwaysShowIds || showIds; + }); + + return + `workspace ${focusedWs.id === ws.id ? "focus" : ""} ${ + showWsNumbers ? "show" : ""}` + )} tooltipText={createComputed([ + createBinding(ws, "lastClient"), + createBinding(AstalHyprland.get_default(), "focusedWorkspace") + ], (lastClient, focusWs) => focusWs.id === ws.id ? "" : + `workspace ${ws.id}${ lastClient ? ` - ${ + !lastClient.title.toLowerCase().includes(lastClient.class) ? + `${lastClient.get_class()}: ` + : "" + } ${lastClient.title}` : "" }` + )} onClicked={ws.focus}> + + + + {(lastClient: AstalHyprland.Client) => + + + + + + {lastClient && + getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ?? + "application-x-executable-symbolic")} + hexpand={true} vexpand={true} + />} + + } + + + }} + + + +}