From 2bca31e6012090065b1a40295e90a66f86749f7e Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Sat, 28 Jun 2025 13:29:33 -0300 Subject: [PATCH] :sparkles: chore: make workspaces fit in a single widget, add keyboard support for the logout menu --- ags/style.scss | 12 +- ags/style/_bar.scss | 17 +- ags/style/_control-center.scss | 8 + ags/style/_logout-menu.scss | 4 + ags/style/_wal.scss | 40 ++--- ags/widget/bar/SpecialWorkspaces.ts | 44 ----- ags/widget/bar/Workspaces.ts | 252 ++++++++++++++++++---------- ags/window/Bar.ts | 12 -- ags/window/LogoutMenu.ts | 126 +++++++------- 9 files changed, 276 insertions(+), 239 deletions(-) delete mode 100644 ags/widget/bar/SpecialWorkspaces.ts diff --git a/ags/style.scss b/ags/style.scss index 771af10..1eab054 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -69,9 +69,13 @@ entry { right: 4px; }; - &:hover, &:focus { + &:hover { background: colors.$bg-secondary; } + + &:focus { + box-shadow: inset 0 0 0 2px colors.$fg-primary; + } } } @@ -114,9 +118,13 @@ entry { padding: 2px; border-radius: 8px; - &:hover, &:focus { + &:hover { background: colors.$bg-secondary; } + + &:focus { + box-shadow: inset 0 0 0 1px colors.$fg-primary; + } } & icon.close { diff --git a/ags/style/_bar.scss b/ags/style/_bar.scss index 2c74743..6d9b9b3 100644 --- a/ags/style/_bar.scss +++ b/ags/style/_bar.scss @@ -5,8 +5,6 @@ @use "./functions"; .bar-container { - @include mixins.reset-props; - padding: 6px; padding-bottom: 0px; @@ -29,7 +27,7 @@ & > eventbox { &:hover { - & > box:not(.workspaces):not(.special-workspaces) { + & > box { background: $color-hover; } } @@ -38,7 +36,7 @@ margin: $padding 0; } - & > box:not(.workspaces):not(.special-workspaces) { + & > box { padding: 0 8px; } } @@ -55,16 +53,15 @@ } } - .workspaces, .special-workspaces { - @include mixins.reset-props; - padding: 0 4px; + .workspaces-row { + padding: 4px; - & > eventbox { + & eventbox > box > eventbox { & > box { margin: 3px 0; border-radius: 16px; transition: 80ms linear; - min-width: 15px; + min-width: 16px; padding: 0 6px; background: colors.$bg-tertiary; @@ -182,8 +179,6 @@ padding: 0 6px; & .item { - all: unset; - &:hover { background: none; } diff --git a/ags/style/_control-center.scss b/ags/style/_control-center.scss index 26a547a..f454ebe 100644 --- a/ags/style/_control-center.scss +++ b/ags/style/_control-center.scss @@ -23,6 +23,10 @@ } } + /*& eventbox:focus, & button:focus { + box-shadow: inset 0 0 0 1px colors.$fg-primary; + }*/ + & .quickactions { margin-bottom: .8em; @@ -188,6 +192,10 @@ box.history { & button { padding: 6px; + &:focus { + box-shadow: inset 0 0 0 1px colors.$fg-primary; + } + & icon { font-size: 16px; } diff --git a/ags/style/_logout-menu.scss b/ags/style/_logout-menu.scss index 41e4637..8d57aae 100644 --- a/ags/style/_logout-menu.scss +++ b/ags/style/_logout-menu.scss @@ -24,6 +24,10 @@ font-size: 128px; } + &:focus { + box-shadow: inset 0 0 0 5px colors.$fg-primary; + } + margin: { left: 4px; right: 4px; diff --git a/ags/style/_wal.scss b/ags/style/_wal.scss index 0c75ac5..a94b042 100644 --- a/ags/style/_wal.scss +++ b/ags/style/_wal.scss @@ -1,26 +1,26 @@ // SCSS Variables // Generated by 'wal' -$wallpaper: "/home/joaov/wallpapers/Gumi Forest Sunlight.jpg"; +$wallpaper: "/home/joaov/wallpapers/Frieren Ring.jpeg"; // Special -$background: #2a2825; -$foreground: #c9c9c8; -$cursor: #c9c9c8; +$background: #523c42; +$foreground: #d3cecf; +$cursor: #d3cecf; // Colors -$color0: #2a2825; -$color1: #6a6a3b; -$color2: #7b7b48; -$color3: #908a45; -$color4: #7e876d; -$color5: #8a9680; -$color6: #a5a679; -$color7: #a29f98; -$color8: #7d7667; -$color9: #8E8E4F; -$color10: #A5A560; -$color11: #C0B85C; -$color12: #A9B592; -$color13: #B9C8AB; -$color14: #DDDEA2; -$color15: #c9c9c8; +$color0: #523c42; +$color1: #6c839d; +$color2: #7a84a4; +$color3: #9f8a9d; +$color4: #84a2b5; +$color5: #9f9cab; +$color6: #b7a1b2; +$color7: #b0a7a9; +$color8: #937b81; +$color9: #90AFD2; +$color10: #A3B0DB; +$color11: #D4B9D2; +$color12: #B0D9F2; +$color13: #D5D0E5; +$color14: #F5D7EE; +$color15: #d3cecf; diff --git a/ags/widget/bar/SpecialWorkspaces.ts b/ags/widget/bar/SpecialWorkspaces.ts deleted file mode 100644 index e757ae5..0000000 --- a/ags/widget/bar/SpecialWorkspaces.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { bind, Variable } from "astal"; -import { Gtk, Widget } from "astal/gtk3" -import AstalHyprland from "gi://AstalHyprland"; -import { getAppIcon, getSymbolicIcon } from "../../scripts/apps"; - -export const SpecialWorkspaces: (() => Gtk.Widget) = () => new Widget.EventBox({ - className: "special-ws-eventbox", - 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({ - child: bind(workspace, "lastClient").as(lastClient => - new Widget.Icon({ - className: "last-app-icon", - 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); diff --git a/ags/widget/bar/Workspaces.ts b/ags/widget/bar/Workspaces.ts index d7da499..217ed60 100644 --- a/ags/widget/bar/Workspaces.ts +++ b/ags/widget/bar/Workspaces.ts @@ -4,6 +4,7 @@ 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) => @@ -13,103 +14,168 @@ export const showWorkspaceNumber = (show: boolean) => export function Workspaces(): Gtk.Widget { showWsNum ??= new Variable(false); - return 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]; + 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))) { + 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 true; + } + } - return alwaysShowIds || showIds; - }); + return alwaysShowIds || showIds; + }); - const className = Variable.derive([ - bind(AstalHyprland.get_default(), "focusedWorkspace"), - showIds!() - ], (focusedWs, showWsNumbers) => - `${focusedWs.id === workspace.id ? "focus" : ""} ${ - showWsNumbers ? "show" : ""}` - ); + 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}` : "" }` - ); + 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({ - children: bind(workspace, "lastClient").as((lastClient) => [ - new Widget.Revealer({ - transitionDuration: 200, - transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT, - revealChild: showIds!(), - child: new Widget.Label({ - label: bind(workspace, "id").as(String), - className: "id", - hexpand: true - } as Widget.LabelProps) - } as Widget.RevealerProps), - new Widget.Icon({ - className: "last-app-icon", - visible: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusedWorkspace => - workspace.id === focusedWorkspace.id ? - false - : Boolean(lastClient)), - icon: lastClient ? - bind(lastClient, "class").as((clss) => - getSymbolicIcon(clss) ?? getAppIcon(clss) ?? "application-x-executable-symbolic") - : undefined - } as Widget.IconProps) - ]) - } as Widget.BoxProps) - } as Widget.EventBoxProps); - }) - ) - } as Widget.BoxProps) - } as Widget.EventBoxProps); + 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/window/Bar.ts b/ags/window/Bar.ts index ed796d9..d10d5b9 100644 --- a/ags/window/Bar.ts +++ b/ags/window/Bar.ts @@ -7,10 +7,6 @@ import { Media } from "../widget/bar/Media"; import { Apps } from "../widget/bar/Apps"; import { Clock } from "../widget/bar/Clock"; import { Status } from "../widget/bar/Status"; -import { SpecialWorkspaces } from "../widget/bar/SpecialWorkspaces"; -import { Separator, SeparatorProps } from "../widget/Separator"; -import AstalHyprland from "gi://AstalHyprland?version=0.1"; -import { bind } from "astal"; export const Bar = (mon: number) => { const widgetSpacing = 4; @@ -36,14 +32,6 @@ export const Bar = (mon: number) => { spacing: widgetSpacing, children: [ Apps(), - SpecialWorkspaces(), - Separator({ - alpha: .2, - orientation: Gtk.Orientation.HORIZONTAL, - margin: 14, - visible: bind(AstalHyprland.get_default(), "workspaces").as(wss => - wss.filter(ws => ws.id < 0).length > 0) - } as SeparatorProps), Workspaces(), FocusedClient() ] diff --git a/ags/window/LogoutMenu.ts b/ags/window/LogoutMenu.ts index 8b505c2..28f62a5 100644 --- a/ags/window/LogoutMenu.ts +++ b/ags/window/LogoutMenu.ts @@ -1,7 +1,7 @@ import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; import { getDateTime } from "../scripts/time"; import { execAsync, Gio, GLib } from "astal"; -import { AskPopup } from "../widget/AskPopup"; +import { AskPopup, AskPopupProps } from "../widget/AskPopup"; import { Windows } from "../windows"; import { Notifications } from "../scripts/notifications"; import AstalNotifd from "gi://AstalNotifd"; @@ -60,80 +60,32 @@ export const LogoutMenu = (mon: number) => new Widget.Window({ image: new Widget.Icon({ icon: "system-shutdown-symbolic" } as Widget.IconProps), - onClick: () => AskPopup({ - 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"); - } - }) + 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({ - 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"); - } - }) + 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({ - title: "Suspend", - text: "Are you sure you want to Suspend?", - onAccept: () => execAsync("systemctl suspend") - }) + 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({ - 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}` - }) - ) - }] - }) - ) - } - }) + onClick: () => AskPopup(logoutAsk), + onActivate: () => AskPopup(logoutAsk) } as Widget.ButtonProps), ] } as Widget.BoxProps) @@ -141,3 +93,63 @@ export const LogoutMenu = (mon: number) => new Widget.Window({ }) } 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"); + } +};