chore: make workspaces fit in a single widget, add keyboard support for the logout menu

This commit is contained in:
retrozinndev
2025-06-28 13:29:33 -03:00
parent dece2776fe
commit 2bca31e601
9 changed files with 276 additions and 239 deletions
+10 -2
View File
@@ -69,9 +69,13 @@ entry {
right: 4px; right: 4px;
}; };
&:hover, &:focus { &:hover {
background: colors.$bg-secondary; background: colors.$bg-secondary;
} }
&:focus {
box-shadow: inset 0 0 0 2px colors.$fg-primary;
}
} }
} }
@@ -114,9 +118,13 @@ entry {
padding: 2px; padding: 2px;
border-radius: 8px; border-radius: 8px;
&:hover, &:focus { &:hover {
background: colors.$bg-secondary; background: colors.$bg-secondary;
} }
&:focus {
box-shadow: inset 0 0 0 1px colors.$fg-primary;
}
} }
& icon.close { & icon.close {
+6 -11
View File
@@ -5,8 +5,6 @@
@use "./functions"; @use "./functions";
.bar-container { .bar-container {
@include mixins.reset-props;
padding: 6px; padding: 6px;
padding-bottom: 0px; padding-bottom: 0px;
@@ -29,7 +27,7 @@
& > eventbox { & > eventbox {
&:hover { &:hover {
& > box:not(.workspaces):not(.special-workspaces) { & > box {
background: $color-hover; background: $color-hover;
} }
} }
@@ -38,7 +36,7 @@
margin: $padding 0; margin: $padding 0;
} }
& > box:not(.workspaces):not(.special-workspaces) { & > box {
padding: 0 8px; padding: 0 8px;
} }
} }
@@ -55,16 +53,15 @@
} }
} }
.workspaces, .special-workspaces { .workspaces-row {
@include mixins.reset-props; padding: 4px;
padding: 0 4px;
& > eventbox { & eventbox > box > eventbox {
& > box { & > box {
margin: 3px 0; margin: 3px 0;
border-radius: 16px; border-radius: 16px;
transition: 80ms linear; transition: 80ms linear;
min-width: 15px; min-width: 16px;
padding: 0 6px; padding: 0 6px;
background: colors.$bg-tertiary; background: colors.$bg-tertiary;
@@ -182,8 +179,6 @@
padding: 0 6px; padding: 0 6px;
& .item { & .item {
all: unset;
&:hover { &:hover {
background: none; background: none;
} }
+8
View File
@@ -23,6 +23,10 @@
} }
} }
/*& eventbox:focus, & button:focus {
box-shadow: inset 0 0 0 1px colors.$fg-primary;
}*/
& .quickactions { & .quickactions {
margin-bottom: .8em; margin-bottom: .8em;
@@ -188,6 +192,10 @@ box.history {
& button { & button {
padding: 6px; padding: 6px;
&:focus {
box-shadow: inset 0 0 0 1px colors.$fg-primary;
}
& icon { & icon {
font-size: 16px; font-size: 16px;
} }
+4
View File
@@ -24,6 +24,10 @@
font-size: 128px; font-size: 128px;
} }
&:focus {
box-shadow: inset 0 0 0 5px colors.$fg-primary;
}
margin: { margin: {
left: 4px; left: 4px;
right: 4px; right: 4px;
+20 -20
View File
@@ -1,26 +1,26 @@
// SCSS Variables // SCSS Variables
// Generated by 'wal' // Generated by 'wal'
$wallpaper: "/home/joaov/wallpapers/Gumi Forest Sunlight.jpg"; $wallpaper: "/home/joaov/wallpapers/Frieren Ring.jpeg";
// Special // Special
$background: #2a2825; $background: #523c42;
$foreground: #c9c9c8; $foreground: #d3cecf;
$cursor: #c9c9c8; $cursor: #d3cecf;
// Colors // Colors
$color0: #2a2825; $color0: #523c42;
$color1: #6a6a3b; $color1: #6c839d;
$color2: #7b7b48; $color2: #7a84a4;
$color3: #908a45; $color3: #9f8a9d;
$color4: #7e876d; $color4: #84a2b5;
$color5: #8a9680; $color5: #9f9cab;
$color6: #a5a679; $color6: #b7a1b2;
$color7: #a29f98; $color7: #b0a7a9;
$color8: #7d7667; $color8: #937b81;
$color9: #8E8E4F; $color9: #90AFD2;
$color10: #A5A560; $color10: #A3B0DB;
$color11: #C0B85C; $color11: #D4B9D2;
$color12: #A9B592; $color12: #B0D9F2;
$color13: #B9C8AB; $color13: #D5D0E5;
$color14: #DDDEA2; $color14: #F5D7EE;
$color15: #c9c9c8; $color15: #d3cecf;
-44
View File
@@ -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);
+158 -92
View File
@@ -4,6 +4,7 @@ import AstalHyprland from "gi://AstalHyprland";
import { getAppIcon, getSymbolicIcon } from "../../scripts/apps"; import { getAppIcon, getSymbolicIcon } from "../../scripts/apps";
import { Windows } from "../../windows"; import { Windows } from "../../windows";
import { Config } from "../../scripts/config"; import { Config } from "../../scripts/config";
import { Separator, SeparatorProps } from "../Separator";
let showWsNum: (Variable<boolean>|undefined); let showWsNum: (Variable<boolean>|undefined);
export const showWorkspaceNumber = (show: boolean) => export const showWorkspaceNumber = (show: boolean) =>
@@ -13,103 +14,168 @@ export const showWorkspaceNumber = (show: boolean) =>
export function Workspaces(): Gtk.Widget { export function Workspaces(): Gtk.Widget {
showWsNum ??= new Variable<boolean>(false); showWsNum ??= new Variable<boolean>(false);
return new Widget.EventBox({ return new Widget.Box({
onScroll: (_, event) => className: "workspaces-row",
event.delta_y > 0 ? orientation: Gtk.Orientation.HORIZONTAL,
AstalHyprland.get_default().dispatch("workspace", "e-1") children: [
: AstalHyprland.get_default().dispatch("workspace", "e+1"), new Widget.EventBox({
onHover: () => showWorkspaceNumber(true), className: "special",
onHoverLost: () => showWorkspaceNumber(false), visible: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) =>
onDestroy: () => { workspaces.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id).length > 0),
// check if the current widgets is from the only bar child: new Widget.Box({
if((Windows.openWindows["bar"] as (Array<Widget.Window>|undefined))?.length === 1) { className: "special-workspaces",
showWsNum?.drop(); spacing: 4,
showWsNum = undefined; 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({
child: new Widget.Box({ className: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusWs =>
className: "workspaces", `${focusWs.id === workspace.id ? "focus" : ""}`),
spacing: 4, tooltipText: bind(workspace, "name").as((name) => {
children: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) => name = name.replace(/^special\:/, "");
workspaces.filter((ws) => ws.id > 0).sort((a, b) => a.id - b.id).map((workspace, wsIndex, workspaces) => { 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<Widget.Window>|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<boolean> = Variable.derive([ const showIds: Variable<boolean> = Variable.derive([
Config.getDefault().bindProperty("workspaces.always_show_id", "boolean").as(Boolean), Config.getDefault().bindProperty("workspaces.always_show_id", "boolean").as(Boolean),
Config.getDefault().bindProperty("workspaces.enable_helper", "boolean").as(Boolean), Config.getDefault().bindProperty("workspaces.enable_helper", "boolean").as(Boolean),
showWsNum!() showWsNum!()
], (alwaysShowIds, enableHelper, showIds) => { ], (alwaysShowIds, enableHelper, showIds) => {
if(enableHelper && !alwaysShowIds) { if(enableHelper && !alwaysShowIds) {
const previousWorkspace = workspaces[wsIndex-1]; const previousWorkspace = workspaces[wsIndex-1];
const nextWorkspace = workspaces[wsIndex+1]; const nextWorkspace = workspaces[wsIndex+1];
if((workspaces.filter((_, i) => i < wsIndex).length > 0 && if((workspaces.filter((_, i) => i < wsIndex).length > 0 &&
previousWorkspace?.id < (workspace.id-1)) || previousWorkspace?.id < (workspace.id-1)) ||
(workspaces.filter((_, i) => i > wsIndex).length > 0 && (workspaces.filter((_, i) => i > wsIndex).length > 0 &&
nextWorkspace?.id > (workspace.id+1))) { nextWorkspace?.id > (workspace.id+1))) {
return true; return true;
} }
} }
return alwaysShowIds || showIds; return alwaysShowIds || showIds;
}); });
const className = Variable.derive([ const className = Variable.derive([
bind(AstalHyprland.get_default(), "focusedWorkspace"), bind(AstalHyprland.get_default(), "focusedWorkspace"),
showIds!() showIds!()
], (focusedWs, showWsNumbers) => ], (focusedWs, showWsNumbers) =>
`${focusedWs.id === workspace.id ? "focus" : ""} ${ `${focusedWs.id === workspace.id ? "focus" : ""} ${
showWsNumbers ? "show" : ""}` showWsNumbers ? "show" : ""}`
); );
const tooltipText = Variable.derive([ const tooltipText = Variable.derive([
bind(workspace, "lastClient"), bind(workspace, "lastClient"),
bind(AstalHyprland.get_default(), "focusedWorkspace") bind(AstalHyprland.get_default(), "focusedWorkspace")
], (lastClient, focusWs) => focusWs.id === workspace.id ? "" : ], (lastClient, focusWs) => focusWs.id === workspace.id ? "" :
`Workspace ${workspace.id}${ lastClient ? ` - ${ `Workspace ${workspace.id}${ lastClient ? ` - ${
!lastClient.title.toLowerCase().includes(lastClient.class) ? !lastClient.title.toLowerCase().includes(lastClient.class) ?
`${lastClient.get_class()}: ` `${lastClient.get_class()}: `
: "" : ""
} ${lastClient.title}` : "" }` } ${lastClient.title}` : "" }`
); );
return new Widget.EventBox({ return new Widget.EventBox({
className: className(), className: className(),
onClickRelease: () => workspace.focus(), onClickRelease: () => workspace.focus(),
tooltipText: tooltipText(), tooltipText: tooltipText(),
onDestroy: () => { onDestroy: () => {
showIds.drop(); showIds.drop();
className.drop(); className.drop();
tooltipText.drop(); tooltipText.drop();
}, },
child: new Widget.Box({ child: new Widget.Box({
children: bind(workspace, "lastClient").as((lastClient) => [ hexpand: true,
new Widget.Revealer({ children: bind(workspace, "lastClient").as((lastClient) => {
transitionDuration: 200, const widgets: Array<Gtk.Widget> = [
transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT, new Widget.Revealer({
revealChild: showIds!(), transitionDuration: 200,
child: new Widget.Label({ transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT,
label: bind(workspace, "id").as(String), revealChild: showIds!(),
className: "id", hexpand: true,
hexpand: true child: new Widget.Label({
} as Widget.LabelProps) label: bind(workspace, "id").as(String),
} as Widget.RevealerProps), className: "id",
new Widget.Icon({ } as Widget.LabelProps)
className: "last-app-icon", } as Widget.RevealerProps),
visible: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusedWorkspace => ];
workspace.id === focusedWorkspace.id ?
false if(lastClient) {
: Boolean(lastClient)), widgets.push(new Widget.Icon({
icon: lastClient ? className: "last-app-icon",
bind(lastClient, "class").as((clss) => halign: Gtk.Align.CENTER,
getSymbolicIcon(clss) ?? getAppIcon(clss) ?? "application-x-executable-symbolic") expand: true,
: undefined visible: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusedWorkspace =>
} as Widget.IconProps) workspace.id === focusedWorkspace.id ?
]) false
} as Widget.BoxProps) : Boolean(lastClient)),
} as Widget.EventBoxProps); icon: lastClient ?
}) bind(lastClient, "initialClass").as((clss) =>
) getSymbolicIcon(clss) ?? getAppIcon(clss) ?? "application-x-executable-symbolic")
} as Widget.BoxProps) : undefined
} as Widget.EventBoxProps); } as Widget.IconProps));
}
return widgets;
})
} as Widget.BoxProps)
} as Widget.EventBoxProps);
})
)
} as Widget.BoxProps)
} as Widget.EventBoxProps)
]
} as Widget.BoxProps);
} }
-12
View File
@@ -7,10 +7,6 @@ import { Media } from "../widget/bar/Media";
import { Apps } from "../widget/bar/Apps"; import { Apps } from "../widget/bar/Apps";
import { Clock } from "../widget/bar/Clock"; import { Clock } from "../widget/bar/Clock";
import { Status } from "../widget/bar/Status"; 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) => { export const Bar = (mon: number) => {
const widgetSpacing = 4; const widgetSpacing = 4;
@@ -36,14 +32,6 @@ export const Bar = (mon: number) => {
spacing: widgetSpacing, spacing: widgetSpacing,
children: [ children: [
Apps(), 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(), Workspaces(),
FocusedClient() FocusedClient()
] ]
+69 -57
View File
@@ -1,7 +1,7 @@
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { getDateTime } from "../scripts/time"; import { getDateTime } from "../scripts/time";
import { execAsync, Gio, GLib } from "astal"; import { execAsync, Gio, GLib } from "astal";
import { AskPopup } from "../widget/AskPopup"; import { AskPopup, AskPopupProps } from "../widget/AskPopup";
import { Windows } from "../windows"; import { Windows } from "../windows";
import { Notifications } from "../scripts/notifications"; import { Notifications } from "../scripts/notifications";
import AstalNotifd from "gi://AstalNotifd"; import AstalNotifd from "gi://AstalNotifd";
@@ -60,80 +60,32 @@ export const LogoutMenu = (mon: number) => new Widget.Window({
image: new Widget.Icon({ image: new Widget.Icon({
icon: "system-shutdown-symbolic" icon: "system-shutdown-symbolic"
} as Widget.IconProps), } as Widget.IconProps),
onClick: () => AskPopup({ onClick: () => AskPopup(poweroffAsk),
title: "Power Off", onActivate: () => AskPopup(poweroffAsk)
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");
}
})
} as Widget.ButtonProps), } as Widget.ButtonProps),
new Widget.Button({ new Widget.Button({
className: "reboot", className: "reboot",
image: new Widget.Icon({ image: new Widget.Icon({
icon: "arrow-circular-top-right-symbolic" icon: "arrow-circular-top-right-symbolic"
} as Widget.IconProps), } as Widget.IconProps),
onClick: () => AskPopup({ onClick: () => AskPopup(rebootAsk),
title: "Reboot", onActivate: () => AskPopup(rebootAsk)
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");
}
})
} as Widget.ButtonProps), } as Widget.ButtonProps),
new Widget.Button({ new Widget.Button({
className: "suspend", className: "suspend",
image: new Widget.Icon({ image: new Widget.Icon({
icon: "weather-clear-night-symbolic" icon: "weather-clear-night-symbolic"
} as Widget.IconProps), } as Widget.IconProps),
onClick: () => AskPopup({ onClick: () => AskPopup(suspendAsk),
title: "Suspend", onActivate: () => AskPopup(suspendAsk)
text: "Are you sure you want to Suspend?",
onAccept: () => execAsync("systemctl suspend")
})
} as Widget.ButtonProps), } as Widget.ButtonProps),
new Widget.Button({ new Widget.Button({
className: "logout", className: "logout",
image: new Widget.Icon({ image: new Widget.Icon({
icon: "system-log-out-symbolic" icon: "system-log-out-symbolic"
} as Widget.IconProps), } as Widget.IconProps),
onClick: () => AskPopup({ onClick: () => AskPopup(logoutAsk),
title: "Log out", onActivate: () => AskPopup(logoutAsk)
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}`
})
)
}]
})
)
}
})
} as Widget.ButtonProps), } as Widget.ButtonProps),
] ]
} as Widget.BoxProps) } as Widget.BoxProps)
@@ -141,3 +93,63 @@ export const LogoutMenu = (mon: number) => new Widget.Window({
}) })
} as Widget.EventBoxProps) } as Widget.EventBoxProps)
} as Widget.WindowProps); } 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");
}
};