✨ chore: restructure the project, make it not use the astal application stuff
now it's more organized and I have more control over the shell behaviour
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { Astal, Gdk, Gtk } from "ags/gtk4";
|
||||
import { execApp, getAppIcon, getApps, getAstalApps } from "../scripts/apps";
|
||||
import { getPopupWindowContainer, PopupWindow } from "../widget/PopupWindow";
|
||||
|
||||
import AstalApps from "gi://AstalApps";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
import { createState, For } from "ags";
|
||||
import { escapeUnintendedMarkup } from "../scripts/utils";
|
||||
|
||||
|
||||
const ignoredKeys = [
|
||||
Gdk.KEY_Right,
|
||||
Gdk.KEY_Down,
|
||||
Gdk.KEY_Up,
|
||||
Gdk.KEY_Shift_L,
|
||||
Gdk.KEY_Shift_R,
|
||||
Gdk.KEY_Shift_Lock,
|
||||
Gdk.KEY_Left,
|
||||
Gdk.KEY_Return,
|
||||
Gdk.KEY_space
|
||||
];
|
||||
|
||||
export const AppsWindow = (mon: number) => {
|
||||
const [results, setResults] = createState(getApps() as Array<AstalApps.Application>);
|
||||
|
||||
return <PopupWindow namespace="apps-window" layer={Astal.Layer.OVERLAY}
|
||||
exclusivity={Astal.Exclusivity.IGNORE} monitor={mon} marginTop={64}
|
||||
class={"apps-window"} orientation={Gtk.Orientation.VERTICAL}
|
||||
cssBackgroundWindow="background: rgba(0, 0, 0, .2);"
|
||||
actionKeyPressed={(self, key) => {
|
||||
const entry = getPopupWindowContainer(self).get_first_child()!
|
||||
.get_first_child()!.get_first_child()! as Gtk.SearchEntry;
|
||||
|
||||
for(const ignoredKey of ignoredKeys)
|
||||
if(key === ignoredKey) return
|
||||
|
||||
entry.grab_focus();
|
||||
}}>
|
||||
<Gtk.Box hexpand={false} halign={Gtk.Align.CENTER}>
|
||||
<Gtk.SearchEntry hexpand={false} onSearchChanged={(self) => {
|
||||
setResults(getAstalApps().fuzzy_query(self.text.trim()));
|
||||
}} onStopSearch={(self) => (self.get_root() as Astal.Window)?.close()} />
|
||||
</Gtk.Box>
|
||||
|
||||
<Gtk.ScrolledWindow vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
|
||||
hscrollbarPolicy={Gtk.PolicyType.NEVER} overlayScrolling
|
||||
propagateNaturalHeight={false} hexpand vexpand>
|
||||
|
||||
<Gtk.Box hexpand={false} vexpand={false}>
|
||||
<Gtk.FlowBox rowSpacing={60} columnSpacing={60} activateOnSingleClick
|
||||
minChildrenPerLine={1} homogeneous onChildActivated={(_, child) =>
|
||||
child.get_child()!.activate() // pass activation to button
|
||||
}>
|
||||
|
||||
<For each={results}>
|
||||
{(app) =>
|
||||
<Gtk.Button heightRequest={150} tooltipMarkup={`${
|
||||
escapeUnintendedMarkup(app.name)}${app.description ?
|
||||
`\n<span foreground="#7f7f7f">${
|
||||
escapeUnintendedMarkup(app.description)
|
||||
}</span>`
|
||||
: ""}`
|
||||
} onActivate={(self) => {
|
||||
execApp(app);
|
||||
(self.get_root() as Astal.Window)?.close();
|
||||
}} onClicked={(self) => {
|
||||
execApp(app);
|
||||
(self.get_root() as Astal.Window)?.close();
|
||||
}}>
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} valign={Gtk.Align.CENTER}
|
||||
hexpand={false} vexpand={false}>
|
||||
|
||||
<Gtk.Image iconName={getAppIcon(app) ?? "application-x-executable"}
|
||||
iconSize={Gtk.IconSize.LARGE} vexpand={false} class={"app-icon"} />
|
||||
<Gtk.Label ellipsize={Pango.EllipsizeMode.END} label={app.name}
|
||||
valign={Gtk.Align.END} maxWidthChars={30} class={"app-name"} />
|
||||
</Gtk.Box>
|
||||
</Gtk.Button>
|
||||
}
|
||||
</For>
|
||||
</Gtk.FlowBox>
|
||||
</Gtk.Box>
|
||||
</Gtk.ScrolledWindow>
|
||||
</PopupWindow>
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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 <Astal.Window namespace={"top-bar"} layer={Astal.Layer.TOP}
|
||||
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT}
|
||||
exclusivity={Astal.Exclusivity.EXCLUSIVE} heightRequest={46} monitor={mon}
|
||||
visible={true} canFocus={false}>
|
||||
<Gtk.Box class={"bar-container"}>
|
||||
<Gtk.CenterBox class={"bar-centerbox"} hexpand>
|
||||
<Gtk.Box class={"widgets-left"} homogeneous={false}
|
||||
halign={Gtk.Align.START} spacing={widgetSpacing}
|
||||
$type="start">
|
||||
|
||||
<Apps />
|
||||
<Workspaces />
|
||||
<FocusedClient />
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"widgets-center"} homogeneous={false}
|
||||
spacing={widgetSpacing} halign={Gtk.Align.CENTER}
|
||||
$type="center">
|
||||
|
||||
<Clock />
|
||||
<Media />
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"widgets-right"} homogeneous={false}
|
||||
spacing={widgetSpacing} halign={Gtk.Align.END}
|
||||
$type="end">
|
||||
<Tray />
|
||||
<Status />
|
||||
</Gtk.Box>
|
||||
</Gtk.CenterBox>
|
||||
</Gtk.Box>
|
||||
</Astal.Window>
|
||||
}
|
||||
@@ -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) =>
|
||||
<PopupWindow namespace={"center-window"} marginTop={10} monitor={mon}
|
||||
halign={Gtk.Align.CENTER} valign={Gtk.Align.START}>
|
||||
|
||||
<Gtk.Box class={"center-window-container"} spacing={6}>
|
||||
<Gtk.Box class={"left"} orientation={Gtk.Orientation.VERTICAL}>
|
||||
<Gtk.Box class={"datetime"} orientation={Gtk.Orientation.VERTICAL}
|
||||
halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}
|
||||
vexpand={true}>
|
||||
|
||||
<Gtk.Label class={"time"} label={time(t => t.format("%H:%M")!)} />
|
||||
<Gtk.Label class={"date"} label={time(d => d.format("%A, %B %d")!)} />
|
||||
</Gtk.Box>
|
||||
<Gtk.Box class={"calendar-box"} hexpand={true} valign={Gtk.Align.START}>
|
||||
<Gtk.Calendar showHeading={true} showDayNames={true}
|
||||
showWeekNumbers={false}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
|
||||
<Separator orientation={Gtk.Orientation.HORIZONTAL} cssColor="gray"
|
||||
margin={5} spacing={8} alpha={.3} visible={player(pl => pl.available)}
|
||||
/>
|
||||
<BigMedia />
|
||||
</Gtk.Box>
|
||||
</PopupWindow> as Astal.Window;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { PopupWindow } from "../widget/PopupWindow";
|
||||
import { QuickActions } from "../widget/control-center/QuickActions";
|
||||
import { NotifHistory } from "../widget/control-center/NotifHistory";
|
||||
import { Tiles } from "../widget/control-center/Tiles";
|
||||
import { Sliders } from "../widget/control-center/Sliders";
|
||||
|
||||
|
||||
export const ControlCenter = (mon: number) =>
|
||||
<PopupWindow namespace={"control-center"} class={"control-center"}
|
||||
halign={Gtk.Align.END} valign={Gtk.Align.START} layer={Astal.Layer.OVERLAY}
|
||||
marginTop={10} marginRight={10} marginBottom={10} monitor={mon}
|
||||
widthRequest={395}>
|
||||
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} spacing={16} vexpand={false}>
|
||||
<Gtk.Box class={"control-center-container"} vexpand={false}
|
||||
orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
||||
|
||||
<QuickActions />
|
||||
<Tiles />
|
||||
<Sliders />
|
||||
</Gtk.Box>
|
||||
<NotifHistory />
|
||||
</Gtk.Box>
|
||||
</PopupWindow> as Astal.Window;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { createBinding, For } from "ags";
|
||||
import { Notifications } from "../scripts/notifications";
|
||||
import { NotificationWidget } from "../widget/Notification";
|
||||
|
||||
import AstalNotifd from "gi://AstalNotifd?version=0.1";
|
||||
import Adw from "gi://Adw?version=1";
|
||||
|
||||
const size = 450;
|
||||
|
||||
export const FloatingNotifications = (mon: number) =>
|
||||
<Astal.Window namespace={"floating-notifications"} monitor={mon} layer={Astal.Layer.OVERLAY}
|
||||
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT} exclusivity={Astal.Exclusivity.NORMAL}
|
||||
resizable={false} widthRequest={450}>
|
||||
|
||||
<Gtk.Box class={"floating-notifications-container"} spacing={12}
|
||||
orientation={Gtk.Orientation.VERTICAL}>
|
||||
|
||||
<For each={createBinding(Notifications.getDefault(), "notifications")}>
|
||||
{(notif: AstalNotifd.Notification) =>
|
||||
<Adw.Clamp maximumSize={size}>
|
||||
<Gtk.Box class={"float-notification"} widthRequest={size} vexpand={false}>
|
||||
|
||||
{/*
|
||||
Why is holdOnHover disabled: the shell for some reason crashes
|
||||
when removing the notification on hover-lost 💔
|
||||
*/}
|
||||
<NotificationWidget notification={notif} showTime={false}
|
||||
actionClose={() => Notifications.getDefault().removeNotification(notif)}
|
||||
holdOnHover={false} actionClicked={() => {
|
||||
const viewAction = notif.actions.filter(action =>
|
||||
action.label.toLowerCase() === "view")?.[0];
|
||||
|
||||
viewAction && notif.invoke(viewAction.id);
|
||||
}}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Adw.Clamp>
|
||||
}
|
||||
</For>
|
||||
</Gtk.Box>
|
||||
</Astal.Window> as Astal.Window;
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Astal, Gdk, Gtk } from "ags/gtk4";
|
||||
import { execAsync } from "ags/process";
|
||||
import { generalConfig } from "../app";
|
||||
import { AskPopup } from "../widget/AskPopup";
|
||||
import { Notifications } from "../scripts/notifications";
|
||||
import { NightLight } from "../scripts/nightlight";
|
||||
import { time } from "../scripts/utils";
|
||||
|
||||
import GObject from "ags/gobject";
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
import Gio from "gi://Gio?version=2.0";
|
||||
|
||||
|
||||
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
|
||||
|
||||
export const LogoutMenu = (mon: number) =>
|
||||
<Astal.Window namespace={"logout-menu"} anchor={TOP | LEFT | RIGHT | BOTTOM}
|
||||
layer={Astal.Layer.OVERLAY} exclusivity={Astal.Exclusivity.IGNORE}
|
||||
keymode={Astal.Keymode.EXCLUSIVE} monitor={mon} $={(self) => {
|
||||
const conns: Map<GObject.Object, number> = 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("close-request", () => conns.forEach((id, obj) =>
|
||||
obj.disconnect(id))));
|
||||
}}>
|
||||
|
||||
<Gtk.Box class={"logout-menu-container"} orientation={Gtk.Orientation.VERTICAL}
|
||||
$={(self) => {
|
||||
const conns: Map<GObject.Object, number> = new Map();
|
||||
const gestureClick = Gtk.GestureClick.new();
|
||||
|
||||
self.add_controller(gestureClick);
|
||||
gestureClick.set_button(0);
|
||||
|
||||
conns.set(gestureClick, gestureClick.connect("released", (gesture) => {
|
||||
if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) {
|
||||
(self.get_root() as Astal.Window|null)?.close();
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
||||
conns.set(self, self.connect("destroy", () => conns.forEach((id, obj) =>
|
||||
obj.disconnect(id))));
|
||||
}}>
|
||||
|
||||
<Gtk.Box class={"top"} hexpand vexpand={false}
|
||||
orientation={Gtk.Orientation.VERTICAL} valign={Gtk.Align.START}>
|
||||
|
||||
<Gtk.Label class={"time"} label={time(t => t.format("%H:%M")!)} />
|
||||
<Gtk.Label class={"date"} label={time(d => d.format("%A, %B %d %Y")!)} />
|
||||
</Gtk.Box>
|
||||
|
||||
<Gtk.Box class={"button-row"} homogeneous heightRequest={360} valign={Gtk.Align.CENTER}
|
||||
vexpand>
|
||||
<Gtk.Button class={"poweroff"} iconName={"system-shutdown-symbolic"}
|
||||
onClicked={() => AskPopup({
|
||||
title: "Power Off",
|
||||
text: "Are you sure you want to power off? Unsaved work will be lost.",
|
||||
onAccept: () => {
|
||||
generalConfig.getProperty("night_light.save_on_shutdown", "boolean") &&
|
||||
NightLight.getDefault().saveData();
|
||||
|
||||
execAsync("systemctl poweroff");
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Gtk.Button class={"reboot"} iconName={"arrow-circular-top-right-symbolic"}
|
||||
onClicked={() => AskPopup({
|
||||
title: "Reboot",
|
||||
text: "Are you sure you want to Reboot? Unsaved work will be lost.",
|
||||
onAccept: () => {
|
||||
generalConfig.getProperty("night_light.save_on_shutdown", "boolean") &&
|
||||
NightLight.getDefault().saveData();
|
||||
|
||||
execAsync("systemctl reboot");
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Gtk.Button class={"suspend"} iconName={"weather-clear-night-symbolic"}
|
||||
onClicked={() => AskPopup({
|
||||
title: "Suspend",
|
||||
text: "Are you sure you want to Suspend?",
|
||||
onAccept: () => execAsync("systemctl suspend")
|
||||
})}
|
||||
/>
|
||||
<Gtk.Button class={"logout"} iconName={"system-log-out-symbolic"}
|
||||
onClicked={() => AskPopup({
|
||||
title: "Log out",
|
||||
text: "Are you sure you want to log out? Your session will be ended.",
|
||||
onAccept: () => {
|
||||
generalConfig.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}`
|
||||
})
|
||||
)
|
||||
}]
|
||||
})
|
||||
)
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
</Astal.Window> as Astal.Window;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { createBinding, createState } from "ags";
|
||||
import { Wireplumber } from "../scripts/volume";
|
||||
import { Windows } from "../windows";
|
||||
import { Time, timeout } from "ags/time";
|
||||
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
|
||||
|
||||
export enum OSDModes {
|
||||
SINK,
|
||||
BRIGHTNESS,
|
||||
NONE
|
||||
}
|
||||
|
||||
const [osdMode, setOSDMode] = createState(OSDModes.NONE);
|
||||
let osdTimer: (Time|undefined), osdTimeout = 3500;
|
||||
|
||||
export const OSD = (mon: number) => {
|
||||
if(osdMode.get() === OSDModes.NONE)
|
||||
setOSDMode(OSDModes.SINK);
|
||||
|
||||
return <Astal.Window namespace={"osd"} class={"osd-window"} layer={Astal.Layer.OVERLAY}
|
||||
anchor={Astal.WindowAnchor.BOTTOM} focusable={false} marginBottom={80} monitor={mon}>
|
||||
|
||||
<Gtk.Box class={"osd"}>
|
||||
<Gtk.Image class={"icon"} iconName={createBinding(Wireplumber.getDefault().getDefaultSink(),
|
||||
"volumeIcon").as(icon => !Wireplumber.getDefault().isMutedSink() &&
|
||||
Wireplumber.getDefault().getSinkVolume() > 0 ? icon : "audio-volume-muted-symbolic")}
|
||||
/>
|
||||
<Gtk.Box orientation={Gtk.Orientation.VERTICAL} class={"volume"} vexpand={true} hexpand={true}>
|
||||
<Gtk.Label class={"device"} label={createBinding(Wireplumber.getDefault().getDefaultSink(),
|
||||
"description").as(description => description ?? "Speaker")}
|
||||
ellipsize={Pango.EllipsizeMode.END}
|
||||
/>
|
||||
<Gtk.LevelBar class={"levelbar"} value={createBinding(
|
||||
Wireplumber.getDefault().getDefaultSink(), "volume")}
|
||||
maxValue={Wireplumber.getDefault().getMaxSinkVolume() / 100}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
</Astal.Window>
|
||||
}
|
||||
|
||||
export function triggerOSD() {
|
||||
if(Windows.getDefault().isOpen("control-center")) return;
|
||||
|
||||
Windows.getDefault().open("osd");
|
||||
|
||||
if(!osdTimer) {
|
||||
osdTimer = timeout(osdTimeout, () => {
|
||||
osdTimer = undefined;
|
||||
Windows.getDefault().close("osd");
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
osdTimer.cancel();
|
||||
osdTimer = timeout(osdTimeout, () => {
|
||||
Windows.getDefault().close("osd");
|
||||
osdTimer = undefined;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user