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:
retrozinndev
2025-08-06 15:25:21 -03:00
parent 5a6d5b47c6
commit d549ad9596
191 changed files with 529 additions and 1000 deletions
+85
View File
@@ -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>
}
+43
View File
@@ -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>
}
+34
View File
@@ -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;
+25
View File
@@ -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;
+42
View File
@@ -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;
+128
View File
@@ -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;
+64
View File
@@ -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;
});
}