From 8fd009557da1d3a00e079395a5c64b2899d3d52f Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Sun, 6 Jul 2025 19:53:48 -0300 Subject: [PATCH] :sparkles: chore: migrate all windows to gtk4 and ags v3 --- ags/window/AppsWindow.ts | 166 --------------------------- ags/window/AppsWindow.tsx | 72 ++++++++++++ ags/window/Bar.ts | 62 ---------- ags/window/Bar.tsx | 45 ++++++++ ags/window/CenterWindow.ts | 68 ----------- ags/window/CenterWindow.tsx | 34 ++++++ ags/window/ControlCenter.ts | 37 ------ ags/window/ControlCenter.tsx | 28 +++++ ags/window/FloatingNotifications.ts | 30 ----- ags/window/FloatingNotifications.tsx | 34 ++++++ ags/window/LogoutMenu.ts | 155 ------------------------- ags/window/LogoutMenu.tsx | 131 +++++++++++++++++++++ ags/window/OSD.ts | 72 ------------ ags/window/OSD.tsx | 40 +++++++ 14 files changed, 384 insertions(+), 590 deletions(-) delete mode 100644 ags/window/AppsWindow.ts create mode 100644 ags/window/AppsWindow.tsx delete mode 100644 ags/window/Bar.ts create mode 100644 ags/window/Bar.tsx delete mode 100644 ags/window/CenterWindow.ts create mode 100644 ags/window/CenterWindow.tsx delete mode 100644 ags/window/ControlCenter.ts create mode 100644 ags/window/ControlCenter.tsx delete mode 100644 ags/window/FloatingNotifications.ts create mode 100644 ags/window/FloatingNotifications.tsx delete mode 100644 ags/window/LogoutMenu.ts create mode 100644 ags/window/LogoutMenu.tsx delete mode 100644 ags/window/OSD.ts create mode 100644 ags/window/OSD.tsx diff --git a/ags/window/AppsWindow.ts b/ags/window/AppsWindow.ts deleted file mode 100644 index 1e9a903..0000000 --- a/ags/window/AppsWindow.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { GObject, Variable } from "astal"; -import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; -import { execApp, getAppIcon, getApps, getAstalApps } from "../scripts/apps"; -import AstalApps from "gi://AstalApps"; -import { PopupWindow } from "../widget/PopupWindow"; - -export const AppsWindow = (mon: number): (Widget.Window) => { - const searchString = new Variable(""); - const searchSubscription = searchString.subscribe((str: string) => { - updateResults(str); - }); - - let results: Array = []; - - const flowboxConnections: Array = []; - const flowbox = new Gtk.FlowBox({ - rowSpacing: 60, - columnSpacing: 60, - homogeneous: true, - visible: true, - minChildrenPerLine: 1, - activateOnSingleClick: true - } as Gtk.FlowBox.ConstructorProps); - - const entry = new Widget.Entry({ - className: "entry", - halign: Gtk.Align.CENTER, - placeholderText: "Start typing...", - primary_icon_name: "system-search", - onChanged: (entry) => searchString.set(entry.text), - onActivate: () => flowbox.get_selected_children()?.[0]?.get_child()?.activate() - } as Widget.EntryProps); - - async function updateResults(str?: string) { - if(!str) results = getApps().sort((a, b) => - a.name > b.name ? 1 : -1); - else results = getAstalApps().fuzzy_query(str); - - flowbox.get_children().map(flowboxChild => - flowbox.remove(flowboxChild)); - - results.map(app => { - flowbox.insert(AppWidget(app), -1); - - const child = flowbox.get_child_at_index(flowbox.get_children().length - 1); - child?.set_valign(Gtk.Align.START); - }); - - const firstChild = flowbox.get_child_at_index(0); - firstChild && flowbox.select_child(firstChild); - } - - function AppWidget(app: AstalApps.Application) { - const connections: Array = []; - // Astal3.Button doesn't work the way I need, so I'll use normal GtkButton - const button = new Gtk.Button({ - visible: true, - // widthRequest: 180, - heightRequest: 140, - expand: true, - tooltipMarkup: `${app.name}${app.description ? - `\n${app.description}` - : ""}`.replace(/\&/g, "&"), - child: new Widget.Box({ - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Icon({ - className: "icon", - expand: true, - icon: getAppIcon(app) || "application-x-executable" - } as Widget.IconProps), - new Widget.Label({ - className: "name", - truncate: true, - maxWidthChars: 10, - valign: Gtk.Align.START, - label: app.name || "Unnamed App" - } as Widget.LabelProps) - ] - } as Widget.BoxProps) as Gtk.Widget, - } as Gtk.Button.ConstructorProps); - - button.set_can_focus(false); - - const openFun = () => { - execApp(app); - window.close(); - }; - - connections.push( - button.connect("activate", openFun), - button.connect("clicked", openFun) - ); - - button.vfunc_destroy = () => { - connections.map(id => - GObject.signal_handler_is_connected(button, id) && - button.disconnect(id) - ); - }; - - return button; - } - - const window = PopupWindow({ - namespace: "apps-window", - layer: Astal.Layer.OVERLAY, - exclusivity: Astal.Exclusivity.IGNORE, - monitor: mon, - marginTop: 64, - cssBackgroundWindow: "background: rgba(0, 0, 0, .2)", - onDestroy: () => { - searchSubscription?.(); - searchString.drop(); - flowboxConnections.map(id => flowbox.disconnect(id)); - }, - onKeyPressEvent: (_, event: Gdk.Event) => { - if(event.get_keyval()[1] === Gdk.KEY_Escape) { - _.close(); - return; - } - - if(event.get_keyval()[1] !== Gdk.KEY_Right && - event.get_keyval()[1] !== Gdk.KEY_Down && - event.get_keyval()[1] !== Gdk.KEY_Up && - event.get_keyval()[1] !== Gdk.KEY_Left && - event.get_keyval()[1] !== Gdk.KEY_Return && - event.get_keyval()[1] !== Gdk.KEY_space && - event.get_keyval()[1] !== Gdk.KEY_Escape) { - !entry.isFocus && entry.grab_focus_without_selecting(); - } - }, - child: new Widget.Box({ - className: "apps-window-container", - expand: true, - orientation: Gtk.Orientation.VERTICAL, - children: [ - entry, - new Widget.Box({ - className: "apps-area", - child: new Widget.Scrollable({ - vscroll: Gtk.PolicyType.AUTOMATIC, - hscroll: Gtk.PolicyType.NEVER, - overlayScrolling: true, - expand: true, - child: flowbox - } as Widget.ScrollableProps) - } as Widget.BoxProps) - ] - } as Widget.BoxProps) - }); - - const connId = window.connect("focus-in-event", (_) => { - updateResults(); - window.disconnect(connId); - }); - - flowboxConnections.push( - flowbox.connect("child-activated", (_, item) => { - if(!item || !item.get_child()) return; - item.get_child()!.activate(); - }) - ); - - return window; -} diff --git a/ags/window/AppsWindow.tsx b/ags/window/AppsWindow.tsx new file mode 100644 index 0000000..2359dbe --- /dev/null +++ b/ags/window/AppsWindow.tsx @@ -0,0 +1,72 @@ +import { Astal, Gdk, Gtk } from "ags/gtk4"; +import { execApp, getAppIcon, getApps, getAstalApps } from "../scripts/apps"; +import { PopupWindow } from "../widget/PopupWindow"; + +import AstalApps from "gi://AstalApps"; +import Pango from "gi://Pango?version=1.0"; +import { createState, For } from "ags"; + +const ignoredKeys = [ + Gdk.KEY_Right, + Gdk.KEY_Down, + Gdk.KEY_Up, + Gdk.KEY_Left, + Gdk.KEY_Return, + Gdk.KEY_space +]; + +export const AppsWindow = (mon: number) => { + const [results, setResults] = createState(getApps() as Array); + + const entry: Gtk.SearchEntry = { + setResults(getAstalApps().fuzzy_query(self.text.trim())); + }}/> as Gtk.SearchEntry; + + return { + for(const ignoredKey of ignoredKeys) + if(key === ignoredKey) return + + !entry.is_focus && entry.grab_focus(); + }}> + + + + + + + + + {(app) => + ${ + app.description}` : ""}`.replace(/\&/g, "&")} + + onActivate={() => { + execApp(app); + window.close(); + }} onClicked={() => { + execApp(app); + window.close(); + }}> + + + + + + + } + + + + + + +} diff --git a/ags/window/Bar.ts b/ags/window/Bar.ts deleted file mode 100644 index d10d5b9..0000000 --- a/ags/window/Bar.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Astal, Gtk, Widget } from "astal/gtk3"; - -import { Tray } from "../widget/bar/Tray"; -import { Workspaces } from "../widget/bar/Workspaces"; -import { FocusedClient } from "../widget/bar/FocusedClient"; -import { Media } from "../widget/bar/Media"; -import { Apps } from "../widget/bar/Apps"; -import { Clock } from "../widget/bar/Clock"; -import { Status } from "../widget/bar/Status"; - -export const Bar = (mon: number) => { - const widgetSpacing = 4; - return new Widget.Window({ - namespace: "top-bar", - anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT, - layer: Astal.Layer.TOP, - exclusivity: Astal.Exclusivity.EXCLUSIVE, - heightRequest: 46, - monitor: mon, - visible: true, - canFocus: false, - child: new Widget.Box({ - className: "bar-container", - child: new Widget.CenterBox({ - className: "bar-centerbox", - expand: true, - homogeneous: false, - startWidget: new Widget.Box({ - className: "widgets-left", - homogeneous: false, - halign: Gtk.Align.START, - spacing: widgetSpacing, - children: [ - Apps(), - Workspaces(), - FocusedClient() - ] - } as Widget.BoxProps), - centerWidget: new Widget.Box({ - className: "widgets-center", - homogeneous: false, - spacing: widgetSpacing, - halign: Gtk.Align.CENTER, - children: [ - Clock(), - Media() - ] - } as Widget.BoxProps), - endWidget: new Widget.Box({ - className: "widgets-right", - homogeneous: false, - spacing: widgetSpacing, - halign: Gtk.Align.END, - children: [ - Tray(), - Status() - ] - } as Widget.BoxProps) - } as Widget.CenterBoxProps) - } as Widget.BoxProps) - } as Widget.WindowProps); -} diff --git a/ags/window/Bar.tsx b/ags/window/Bar.tsx new file mode 100644 index 0000000..cd99bfe --- /dev/null +++ b/ags/window/Bar.tsx @@ -0,0 +1,45 @@ +import { Astal, Gtk } from "ags/gtk4"; + +import { Tray } from "../widget/bar/Tray"; +import { Workspaces } from "../widget/bar/Workspaces"; +import { FocusedClient } from "../widget/bar/FocusedClient"; +import { Media } from "../widget/bar/Media"; +import { Apps } from "../widget/bar/Apps"; +import { Clock } from "../widget/bar/Clock"; +import { Status } from "../widget/bar/Status"; + +export const Bar = (mon: number) => { + const widgetSpacing = 4; + return + + + + + + + + + + + + + + + + + + + + +} diff --git a/ags/window/CenterWindow.ts b/ags/window/CenterWindow.ts deleted file mode 100644 index ccccc96..0000000 --- a/ags/window/CenterWindow.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Gtk, Widget } from "astal/gtk3"; -import { bind, GLib } from "astal"; - -import { getDateTime } from "../scripts/time"; -import { Separator, SeparatorProps } from "../widget/Separator"; -import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; -import { BigMedia } from "../widget/center-window/BigMedia"; -import AstalMpris from "gi://AstalMpris"; - -export const CenterWindow = (mon: number) => PopupWindow({ - namespace: "center-window", - marginTop: 10, - halign: Gtk.Align.CENTER, - valign: Gtk.Align.START, - monitor: mon, - child: new Widget.Box({ - className: "center-window-container", - spacing: 6, - children: [ - new Widget.Box({ - className: "left", - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Box({ - className: "datetime", - orientation: Gtk.Orientation.VERTICAL, - halign: Gtk.Align.CENTER, - valign: Gtk.Align.CENTER, - vexpand: true, - children: [ - new Widget.Label({ - className: "time", - label: getDateTime().as((dateTime: GLib.DateTime) => - dateTime.format("%H:%M")) - } as Widget.LabelProps), - new Widget.Label({ - className: "date", - label: getDateTime().as((dateTime: GLib.DateTime) => - dateTime.format("%A, %B %d")) - } as Widget.LabelProps) - ] - } as Widget.BoxProps), - new Widget.Box({ - className: "calendar-box", - vexpand: false, - hexpand: true, - valign: Gtk.Align.START, - child: new Gtk.Calendar({ - visible: true, - showHeading: true, - showDayNames: true, - showWeekNumbers: false - } as Gtk.Calendar.ConstructorProps) - } as Widget.BoxProps) - ] - } as Widget.BoxProps), - Separator({ - orientation: Gtk.Orientation.HORIZONTAL, - cssColor: "gray", - margin: 5, - spacing: 8, - alpha: .3, - visible: bind(AstalMpris.get_default(), "players").as(players => players.length > 0), - } as SeparatorProps), - BigMedia() - ] - } as Widget.BoxProps) -} as PopupWindowProps); diff --git a/ags/window/CenterWindow.tsx b/ags/window/CenterWindow.tsx new file mode 100644 index 0000000..13d2bf0 --- /dev/null +++ b/ags/window/CenterWindow.tsx @@ -0,0 +1,34 @@ +import { Astal, Gtk } from "ags/gtk4"; + +import { Separator } from "../widget/Separator"; +import { PopupWindow } from "../widget/PopupWindow"; +import { BigMedia } from "../widget/center-window/BigMedia"; +import { time } from "../scripts/utils"; +import { player } from "../widget/bar/Media"; + +export const CenterWindow = (mon: number) => + + + + + + + t.format("%H:%M")!)} /> + d.format("%A, %B %d")!)} /> + + + + + + + pl.available)} + /> + + + as Astal.Window; diff --git a/ags/window/ControlCenter.ts b/ags/window/ControlCenter.ts deleted file mode 100644 index e6a5fb4..0000000 --- a/ags/window/ControlCenter.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Astal, 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 { NotifHistory } from "../widget/control-center/NotifHistory"; -import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; - - -export const ControlCenter = (mon: number) => PopupWindow({ - namespace: "control-center", - className: "control-center", - halign: Gtk.Align.END, - valign: Gtk.Align.START, - layer: Astal.Layer.OVERLAY, - marginTop: 10, - marginRight: 10, - marginBottom: 10, - monitor: mon, - widthRequest: 395, - child: new Widget.Box({ - orientation: Gtk.Orientation.VERTICAL, - spacing: 16, - children: [ - new Widget.Box({ - className: "control-center-container", - orientation: Gtk.Orientation.VERTICAL, - vexpand: false, - children: [ - QuickActions(), - Sliders(), - Tiles() - ] - } as Widget.BoxProps), - NotifHistory() - ] - } as Widget.BoxProps) -} as PopupWindowProps); diff --git a/ags/window/ControlCenter.tsx b/ags/window/ControlCenter.tsx new file mode 100644 index 0000000..12fa5d1 --- /dev/null +++ b/ags/window/ControlCenter.tsx @@ -0,0 +1,28 @@ +import { Astal, Gtk } from "ags/gtk4"; +import { QuickActions } from "../widget/control-center/QuickActions"; +import { Tiles } from "../widget/control-center/Tiles"; +import { Sliders } from "../widget/control-center/Sliders"; +import { NotifHistory } from "../widget/control-center/NotifHistory"; +import { PopupWindow } from "../widget/PopupWindow"; + + +export const ControlCenter = (mon: number) => + + + + + + + + + + + + + + as Astal.Window; diff --git a/ags/window/FloatingNotifications.ts b/ags/window/FloatingNotifications.ts deleted file mode 100644 index ceb018e..0000000 --- a/ags/window/FloatingNotifications.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Astal, Gtk, Widget } from "astal/gtk3"; -import { bind } from "astal/binding"; -import { Notifications } from "../scripts/notifications"; -import { NotificationWidget } from "../widget/Notification"; - - -export const FloatingNotifications = (mon: number) => new Widget.Window({ - namespace: "floating-notifications", - canFocus: false, - anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT, - monitor: mon, - layer: Astal.Layer.OVERLAY, - widthRequest: 450, - exclusivity: Astal.Exclusivity.NORMAL, - child: new Widget.Box({ - className: "floating-notifications-container", - orientation: Gtk.Orientation.VERTICAL, - homogeneous: false, - spacing: 12, - visible: bind(Notifications.getDefault(), "notifications").as(notifs => notifs.length > 0), - children: bind(Notifications.getDefault(), "notifications").as((notifs) => - notifs.map((item) => new Widget.Box({ - className: "float-notification", - child: NotificationWidget(item, - () => Notifications.getDefault().removeNotification(item), - false, true) - } as Widget.BoxProps)) - ), - } as Widget.BoxProps) -} as Widget.WindowProps); diff --git a/ags/window/FloatingNotifications.tsx b/ags/window/FloatingNotifications.tsx new file mode 100644 index 0000000..bc9cb42 --- /dev/null +++ b/ags/window/FloatingNotifications.tsx @@ -0,0 +1,34 @@ +import { Astal, Gtk } from "ags/gtk4"; +import { createBinding, For } from "ags"; +import { Notifications } from "../scripts/notifications"; +import { NotificationWidget } from "../widget/Notification"; +import { variableToBoolean } from "../scripts/utils"; +import AstalNotifd from "gi://AstalNotifd?version=0.1"; + + +export const FloatingNotifications = (mon: number) => + + + + + + {(notif: AstalNotifd.Notification) => + + Notifications.getDefault().removeNotification(notif)} + holdOnHover={true} actionClicked={() => { + const viewAction = notif.actions.filter(action => + action.label.toLowerCase() === "view")?.[0]; + + viewAction && notif.invoke(viewAction.id); + }} + /> + + } + + + as Astal.Window; diff --git a/ags/window/LogoutMenu.ts b/ags/window/LogoutMenu.ts deleted file mode 100644 index 28f62a5..0000000 --- a/ags/window/LogoutMenu.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; -import { getDateTime } from "../scripts/time"; -import { execAsync, Gio, GLib } from "astal"; -import { AskPopup, AskPopupProps } from "../widget/AskPopup"; -import { Windows } from "../windows"; -import { Notifications } from "../scripts/notifications"; -import AstalNotifd from "gi://AstalNotifd"; -import { NightLight } from "../scripts/nightlight"; -import { Config } from "../scripts/config"; - - -const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; - -export const LogoutMenu = (mon: number) => new Widget.Window({ - namespace: "logout-menu", - anchor: TOP | LEFT | RIGHT | BOTTOM, - layer: Astal.Layer.OVERLAY, - exclusivity: Astal.Exclusivity.IGNORE, - keymode: Astal.Keymode.EXCLUSIVE, - monitor: mon, - onKeyPressEvent: (_, event: Gdk.Event) => { - event.get_keyval()[1] === Gdk.KEY_Escape && - _.close(); - }, - child: new Widget.EventBox({ - className: "logout-menu", - onClick: () => Windows.close("logout-menu"), - child: new Widget.Box({ - expand: true, - orientation: Gtk.Orientation.VERTICAL, - children: [ - new Widget.Box({ - className: "top", - hexpand: true, - vexpand: false, - orientation: Gtk.Orientation.VERTICAL, - valign: Gtk.Align.START, - children: [ - new Widget.Label({ - className: "time", - label: getDateTime().as((dateTime: GLib.DateTime) => - dateTime.format("%H:%M")) - } as Widget.LabelProps), - new Widget.Label({ - className: "date", - label: getDateTime().as((dateTime: GLib.DateTime) => - dateTime.format("%A, %B %d %Y")) - } as Widget.LabelProps) - ] - } as Widget.BoxProps), - new Widget.Box({ - className: "button-row", - homogeneous: true, - vexpand: true, - valign: Gtk.Align.CENTER, - height_request: 360, - children: [ - new Widget.Button({ - className: "poweroff", - image: new Widget.Icon({ - icon: "system-shutdown-symbolic" - } as Widget.IconProps), - onClick: () => AskPopup(poweroffAsk), - onActivate: () => AskPopup(poweroffAsk) - } as Widget.ButtonProps), - new Widget.Button({ - className: "reboot", - image: new Widget.Icon({ - icon: "arrow-circular-top-right-symbolic" - } as Widget.IconProps), - onClick: () => AskPopup(rebootAsk), - onActivate: () => AskPopup(rebootAsk) - } as Widget.ButtonProps), - new Widget.Button({ - className: "suspend", - image: new Widget.Icon({ - icon: "weather-clear-night-symbolic" - } as Widget.IconProps), - onClick: () => AskPopup(suspendAsk), - onActivate: () => AskPopup(suspendAsk) - } as Widget.ButtonProps), - new Widget.Button({ - className: "logout", - image: new Widget.Icon({ - icon: "system-log-out-symbolic" - } as Widget.IconProps), - onClick: () => AskPopup(logoutAsk), - onActivate: () => AskPopup(logoutAsk) - } as Widget.ButtonProps), - ] - } as Widget.BoxProps) - ] - }) - } as Widget.EventBoxProps) -} as Widget.WindowProps); - -const logoutAsk: AskPopupProps = { - title: "Log out", - text: "Are you sure you want to log out? Your session will be ended.", - onAccept: () => { - Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") && - NightLight.getDefault().saveData(); - - execAsync(`hyprctl dispatch exit`).catch((err: Gio.IOErrorEnum) => - Notifications.getDefault().sendNotification({ - appName: "colorshell", - summary: "Couldn't exit Hyprland", - body: `An error occurred and colorshell couldn't exit Hyprland. Stderr: \n${ - err.message ? `${err.message}\n` : ""}${err.stack}`, - urgency: AstalNotifd.Urgency.NORMAL, - actions: [{ - text: "Report Issue on colorshell", - onAction: () => execAsync( - `xdg-open https://github.com/retrozinndev/colorshell/issues/new` - ).catch((err: Gio.IOErrorEnum) => - Notifications.getDefault().sendNotification({ - appName: "colorshell", - summary: "Couldn't open link", - body: `Do you have \`xdg-utils\` installed? Stderr: \n${ - err.message ? `${err.message}\n` : ""}${err.stack}` - }) - ) - }] - }) - ) - } -}; - -const suspendAsk: AskPopupProps = { - title: "Suspend", - text: "Are you sure you want to Suspend?", - onAccept: () => execAsync("systemctl suspend") -}; - -const rebootAsk: AskPopupProps = { - title: "Reboot", - text: "Are you sure you want to Reboot? Unsaved work will be lost.", - onAccept: () => { - Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") && - NightLight.getDefault().saveData(); - - execAsync("systemctl reboot"); - } -}; - -const poweroffAsk: AskPopupProps = { - title: "Power Off", - text: "Are you sure you want to power off? Unsaved work will be lost.", - onAccept: () => { - Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") && - NightLight.getDefault().saveData(); - - execAsync("systemctl poweroff"); - } -}; diff --git a/ags/window/LogoutMenu.tsx b/ags/window/LogoutMenu.tsx new file mode 100644 index 0000000..3719e96 --- /dev/null +++ b/ags/window/LogoutMenu.tsx @@ -0,0 +1,131 @@ +import { Astal, Gdk, Gtk } from "ags/gtk4"; +import { execAsync } from "ags/process"; +import { AskPopup, AskPopupProps } from "../widget/AskPopup"; +import { Windows } from "../windows"; +import { Notifications } from "../scripts/notifications"; +import { NightLight } from "../scripts/nightlight"; +import { Config } from "../scripts/config"; + +import AstalNotifd from "gi://AstalNotifd"; +import Gio from "gi://Gio?version=2.0"; +import GObject from "gi://GObject?version=2.0"; +import { time } from "../scripts/utils"; + + +const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; + +export const LogoutMenu = (mon: number) => + { + const conns: Map = new Map(); + const controllerKey = Gtk.EventControllerKey.new(); + + self.add_controller(controllerKey); + conns.set(controllerKey, controllerKey.connect("key-released", (_, keyval) => { + if(keyval === Gdk.KEY_Escape) + self.close(); + })); + conns.set(self, self.connect("destroy", () => conns.forEach((id, obj) => + obj.disconnect(id)))); + }}> + + { + const conns: Map = new Map(); + const gestureClick = Gtk.GestureClick.new(); + + self.add_controller(gestureClick); + + conns.set(gestureClick, gestureClick.connect("released", (gesture) => { + if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) { + Windows.getDefault().close("logout-menu"); + return true; + } + })); + }}> + + + + t.format("%H:%M")!)} /> + d.format("%A, %B %d %Y")!)} /> + + + + AskPopup(poweroffAsk)} onActivate={() => + AskPopup(poweroffAsk)} + /> + AskPopup(rebootAsk)} onActivate={() => AskPopup(rebootAsk)} + /> + AskPopup(suspendAsk)} onActivate={() => AskPopup(suspendAsk)} + /> + AskPopup(logoutAsk)} onActivate={() => AskPopup(logoutAsk)} + /> + + + as Astal.Window; + +const logoutAsk: AskPopupProps = { + title: "Log out", + text: "Are you sure you want to log out? Your session will be ended.", + onAccept: () => { + Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") && + NightLight.getDefault().saveData(); + + execAsync(`hyprctl dispatch exit`).catch((err: Gio.IOErrorEnum) => + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "Couldn't exit Hyprland", + body: `An error occurred and colorshell couldn't exit Hyprland. Stderr: \n${ + err.message ? `${err.message}\n` : ""}${err.stack}`, + urgency: AstalNotifd.Urgency.NORMAL, + actions: [{ + text: "Report Issue on colorshell", + onAction: () => execAsync( + `xdg-open https://github.com/retrozinndev/colorshell/issues/new` + ).catch((err: Gio.IOErrorEnum) => + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "Couldn't open link", + body: `Do you have \`xdg-utils\` installed? Stderr: \n${ + err.message ? `${err.message}\n` : ""}${err.stack}` + }) + ) + }] + }) + ) + } +}; + +const suspendAsk: AskPopupProps = { + title: "Suspend", + text: "Are you sure you want to Suspend?", + onAccept: () => execAsync("systemctl suspend") +}; + +const rebootAsk: AskPopupProps = { + title: "Reboot", + text: "Are you sure you want to Reboot? Unsaved work will be lost.", + onAccept: () => { + Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") && + NightLight.getDefault().saveData(); + + execAsync("systemctl reboot"); + } +}; + +const poweroffAsk: AskPopupProps = { + title: "Power Off", + text: "Are you sure you want to power off? Unsaved work will be lost.", + onAccept: () => { + Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") && + NightLight.getDefault().saveData(); + + execAsync("systemctl poweroff"); + } +}; diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts deleted file mode 100644 index ddc8655..0000000 --- a/ags/window/OSD.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { bind, Variable } from "astal"; -import { Astal, Gtk, Widget } from "astal/gtk3"; -import { Wireplumber } from "../scripts/volume"; - -export enum OSDModes { - SINK, - BRIGHTNESS -} - -let osdMode: (Variable|null); - -export function setOSDMode(newMode: OSDModes): void { - if(!osdMode) return; - - osdMode.set(newMode); -} - -export const OSD = (mon: number) => { - osdMode = new Variable(OSDModes.SINK); - - return new Widget.Window({ - namespace: "osd", - className: "osd-window", - layer: Astal.Layer.OVERLAY, - anchor: Astal.WindowAnchor.BOTTOM, - canFocus: false, - clickThrough: true, - focusOnClick: false, - marginBottom: 80, - monitor: mon, - onDestroy: () => { - osdMode?.drop(); - osdMode = null; - }, - child: new Widget.Box({ - className: "osd", - expand: true, - children: [ - new Widget.Icon({ - className: "icon", - icon: bind(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon => - !Wireplumber.getDefault().isMutedSink() && Wireplumber.getDefault().getSinkVolume() > 0 ? icon : "audio-volume-muted-symbolic"), - } as Widget.IconProps), - new Widget.Box({ - className: "volume", - orientation: Gtk.Orientation.VERTICAL, - valign: Gtk.Align.CENTER, - expand: true, - children: [ - new Widget.Label({ - className: "device", - label: bind(Wireplumber.getDefault().getDefaultSink(), "description").as(description => - description ?? "Speaker"), - truncate: true, - } as Widget.LabelProps), - new Widget.Box({ - expand: true, - child: new Widget.LevelBar({ - className: "levelbar", - value: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) => - Math.floor(volume * 100)), - maxValue: bind(Wireplumber.getWireplumber(), "defaultSpeaker").as(() => - Wireplumber.getDefault().getMaxSinkVolume()), - expand: true - } as Widget.LevelBarProps) - } as Widget.BoxProps) - ] - } as Widget.BoxProps) - ] - } as Widget.BoxProps) - } as Widget.WindowProps); -} diff --git a/ags/window/OSD.tsx b/ags/window/OSD.tsx new file mode 100644 index 0000000..99e1a0c --- /dev/null +++ b/ags/window/OSD.tsx @@ -0,0 +1,40 @@ +import { Astal, Gtk } from "ags/gtk4"; +import { createBinding, createState } from "ags"; +import { Wireplumber } from "../scripts/volume"; +import Pango from "gi://Pango?version=1.0"; + + +export enum OSDModes { + SINK, + BRIGHTNESS, + NONE +} + +const [osdMode, setOSDMode] = createState(OSDModes.NONE); + +export const OSD = (mon: number) => { + if(osdMode.get() === OSDModes.NONE) + setOSDMode(OSDModes.SINK); + + return + + + !Wireplumber.getDefault().isMutedSink() && + Wireplumber.getDefault().getSinkVolume() > 0 ? icon : "audio-volume-muted-symbolic")} + /> + + description ?? "Speaker")} + ellipsize={Pango.EllipsizeMode.END} + /> + + Math.floor(volume * 100))} + maxValue={Wireplumber.getDefault().getMaxSinkVolume()} + /> + + + +}