ags: new window management system, adjustments, use adwaita sans

This commit is contained in:
retrozinndev
2025-04-01 10:47:40 -03:00
parent a555e89dc0
commit 53929db052
27 changed files with 505 additions and 344 deletions
+35 -11
View File
@@ -1,22 +1,25 @@
import { App } from "astal/gtk3" import { App } from "astal/gtk3"
import { Windows } from "./windows";
import { Wireplumber } from "./scripts/volume"; import { Wireplumber } from "./scripts/volume";
import { runStyleHandler } from "./scripts/style-handler"; import { runStyleHandler } from "./scripts/style-handler";
import { handleArguments } from "./scripts/arg-handler"; import { handleArguments } from "./scripts/arg-handler";
import { Time, timeout } from "astal/time"; import { Time, timeout } from "astal/time";
import { OSD, OSDModes, setOSDMode } from "./window/OSD"; import { OSDModes, setOSDMode } from "./window/OSD";
import { ControlCenter } from "./window/ControlCenter";
import { Runner } from "./runner/Runner"; import { Runner } from "./runner/Runner";
import { PluginApps } from "./runner/plugins/apps"; import { PluginApps } from "./runner/plugins/apps";
import { PluginShell } from "./runner/plugins/shell"; import { PluginShell } from "./runner/plugins/shell";
import { PluginWebSearch } from "./runner/plugins/websearch"; import { PluginWebSearch } from "./runner/plugins/websearch";
import { PluginMedia } from "./runner/plugins/media"; import { PluginMedia } from "./runner/plugins/media";
import { Windows } from "./windows";
import { Notifications } from "./scripts/notifications";
import AstalNotifd from "gi://AstalNotifd";
import { GObject } from "astal";
let osdTimer: (Time|undefined); let osdTimer: (Time|undefined);
let connections = new Map<GObject.Object, (Array<number> | number)>();
const runnerPlugins: Array<Runner.Plugin> = [ const runnerPlugins: Array<Runner.Plugin> = [
PluginApps, PluginApps,
@@ -27,19 +30,39 @@ const runnerPlugins: Array<Runner.Plugin> = [
App.start({ App.start({
instanceName: "astal", instanceName: "astal",
requestHandler(request: string, response: (result: any) => void) { async requestHandler(request: string, response: (result: any) => void) {
// console.log(`[LOG] Arguments received: ${request}`); // console.log(`[LOG] Arguments received: ${request}`);
response(handleArguments(request)); response(await handleArguments(request));
}, },
main() { main() {
console.log(`[LOG] Initialized astal instance as: ${ App.instanceName || "astal" }`); console.log(`[LOG] Initialized astal instance as: ${ App.instanceName || "astal" }`);
App.vfunc_quit = () => {
console.log("[LOG] Disconnecting stuff");
connections.forEach((v, k) => Array.isArray(v) ?
v.map(id => k.disconnect(id))
: k.disconnect(v));
};
console.log(`[LOG] Running Stylesheet handler`); console.log(`[LOG] Running Stylesheet handler`);
runStyleHandler(); runStyleHandler();
//console.log(`[LOG] Starting to monitor scripts to automatically reload instance`); //console.log(`[LOG] Starting to monitor scripts to automatically reload instance`);
//monitorPaths(); // Only for debugging purposes(testing new widgets and stuff) //monitorPaths(); // Only for debugging purposes(testing new widgets and stuff)
connections.set(Wireplumber.getDefault(), [
Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () =>
!Windows.isVisible(ControlCenter) && triggerOSD(OSDModes.SINK)); !Windows.isVisible("osd") && triggerOSD(OSDModes.SINK))
]);
connections.set(Notifications.getDefault(), [
Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => {
Windows.open("floating-notifications");
}),
Notifications.getDefault().connect("notification-removed", (_: Notifications, _id: number) => {
_.notifications.length === 0 && Windows.close("floating-notifications");
})
]);
console.log(`[LOG] Adding runner plugins`); console.log(`[LOG] Adding runner plugins`);
runnerPlugins.map(plugin => Runner.addPlugin(plugin)); runnerPlugins.map(plugin => Runner.addPlugin(plugin));
@@ -49,17 +72,18 @@ App.start({
function triggerOSD(osdModeParam: OSDModes) { function triggerOSD(osdModeParam: OSDModes) {
setOSDMode(osdModeParam); setOSDMode(osdModeParam);
Windows.open(OSD);
if(!osdTimer) { if(!osdTimer) {
Windows.open("osd");
osdTimer = timeout(3000, () => { osdTimer = timeout(3000, () => {
Windows.close(OSD); Windows.close("osd");
osdTimer = undefined; osdTimer = undefined;
}); });
} else { return;
}
osdTimer.cancel(); osdTimer.cancel();
osdTimer = timeout(3000, () => { osdTimer = timeout(3000, () => {
Windows.close(OSD); Windows.close("osd");
osdTimer = undefined; osdTimer = undefined;
}); });
}
} }
+19 -14
View File
@@ -3,8 +3,9 @@ import { Gdk, Gtk, Widget } from "astal/gtk3";
import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow";
import { updateApps } from "../scripts/apps"; import { updateApps } from "../scripts/apps";
import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget"; import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget";
import { Windows } from "../windows";
export let runnerInstance: (Widget.Window|null) = null; export let runnerInstance: (Gtk.Window|null) = null;
let onClickTimeout: (AstalIO.Time|undefined); let onClickTimeout: (AstalIO.Time|undefined);
export function startRunnerDefault() { export function startRunnerDefault() {
@@ -12,16 +13,21 @@ export function startRunnerDefault() {
entryPlaceHolder: "Start typing..." entryPlaceHolder: "Start typing..."
} as Runner.RunnerProps, } as Runner.RunnerProps,
() => [ () => [
new ResultWidget({
icon: "utilities-terminal-symbolic",
title: "Run shell commands",
description: "Start typing with '!' prefix to run shell commands"
} as ResultWidgetProps),
new ResultWidget({ new ResultWidget({
icon: "application-x-executable-symbolic", icon: "application-x-executable-symbolic",
title: "Run your applications", title: "Run your applications",
description: "Type the name of the application to search" description: "Type the name of the application to search"
} as ResultWidgetProps), } as ResultWidgetProps),
new ResultWidget({
icon: "media-playback-start-symbolic",
title: "Control media",
description: "Use prefix ':' to run"
} as ResultWidgetProps),
new ResultWidget({
icon: "utilities-terminal-symbolic",
title: "Run shell commands",
description: "Start typing with '!' prefix to run shell commands"
} as ResultWidgetProps),
new ResultWidget({ new ResultWidget({
icon: "applications-internet-symbolic", icon: "applications-internet-symbolic",
title: "Search the Web", title: "Search the Web",
@@ -39,13 +45,11 @@ export namespace Runner {
entryPlaceHolder?: string; entryPlaceHolder?: string;
}; };
export function close(gtkWindow?: Widget.Window) { export function close() {
const window = gtkWindow ? gtkWindow : runnerInstance;
[...plugins.values()].map(plugin => [...plugins.values()].map(plugin =>
plugin && plugin.onClose && plugin.onClose()); plugin && plugin.onClose && plugin.onClose());
window?.close(); runnerInstance?.close();
runnerInstance = null; runnerInstance = null;
} }
@@ -83,7 +87,7 @@ export namespace Runner {
return plugins.delete(plugin); return plugins.delete(plugin);
} }
export function openRunner(props?: RunnerProps, placeholder?: () => Array<ResultWidget>): (Widget.Window|null) { export function openRunner(props?: RunnerProps, placeholder?: () => Array<ResultWidget>): (Gtk.Window|null) {
let subs: Array<() => void> = []; let subs: Array<() => void> = [];
const entryText: Variable<string> = new Variable<string>(""); const entryText: Variable<string> = new Variable<string>("");
@@ -156,8 +160,9 @@ export namespace Runner {
})); }));
if(!runnerInstance) if(!runnerInstance)
runnerInstance = PopupWindow({ runnerInstance = Windows.createWindowForFocusedMonitor((mon: number): (Widget.Window) => PopupWindow({
namespace: "runner", namespace: "runner",
monitor: mon,
widthRequest: props?.width || 750, widthRequest: props?.width || 750,
heightRequest: props?.height || 0, heightRequest: props?.height || 0,
marginTop: 250, marginTop: 250,
@@ -176,8 +181,8 @@ export namespace Runner {
}, },
closeAction: (_) => { closeAction: (_) => {
close(_);
subs.map(sub => sub()); subs.map(sub => sub());
close();
}, },
child: new Widget.Box({ child: new Widget.Box({
className: "runner main", className: "runner main",
@@ -195,7 +200,7 @@ export namespace Runner {
}) })
] ]
} as Widget.BoxProps) } as Widget.BoxProps)
} as PopupWindowProps); } as PopupWindowProps))();
return runnerInstance; return runnerInstance;
} }
+22 -11
View File
@@ -1,5 +1,3 @@
import { Gtk } from "astal/gtk3";
import { Wireplumber } from "./volume"; import { Wireplumber } from "./volume";
import { Windows } from "../windows"; import { Windows } from "../windows";
@@ -7,9 +5,10 @@ import { restartInstance } from "./reload-handler";
import { startRunnerDefault } from "../runner/Runner"; import { startRunnerDefault } from "../runner/Runner";
import { showWorkspaceNumbers } from "../widget/bar/Workspaces"; import { showWorkspaceNumbers } from "../widget/bar/Workspaces";
import { timeout } from "astal"; import { timeout } from "astal";
import { App } from "astal/gtk3";
export function handleArguments(request: string): any { export async function handleArguments(request: string): Promise<any> {
const args: Array<string> = request.split(" "); const args: Array<string> = request.split(" ");
switch(args[0]) { switch(args[0]) {
case "open": case "open":
@@ -28,6 +27,10 @@ export function handleArguments(request: string): any {
restartInstance(); restartInstance();
return "Restarting instance..." return "Restarting instance..."
case "windows":
return Object.keys(Windows.windows).map(name =>
`${name}: ${Windows.isVisible(name) ? "open" : "closed" }`).join('\n');
case "runner": case "runner":
startRunnerDefault(); startRunnerDefault();
return "Opening runner..." return "Opening runner..."
@@ -39,6 +42,12 @@ export function handleArguments(request: string): any {
} }
return "Showing numbers"; return "Showing numbers";
case "c":
case "code":
const input = request.replace(args[0], "").trimStart();
console.log(input);
return await (App.eval(input).then((v) => v).catch((r) => r));
default: default:
return "command not found! try checking help"; return "command not found! try checking help";
} }
@@ -46,13 +55,13 @@ export function handleArguments(request: string): any {
// Didn't want to bloat the switch statement, so I just separated it into functions // Didn't want to bloat the switch statement, so I just separated it into functions
function handleWindowArgs(args: Array<string>): string { function handleWindowArgs(args: Array<string>): string {
const specifiedWindow: (Gtk.Window|undefined) = Windows.getWindow(args[1]); if(!args[1])
if(!specifiedWindow)
return "Window argument not specified!"; return "Window argument not specified!";
if(!Windows.getList().has(args[1])) const specifiedWindow: string = args[1];
return `Name "${args[1]}" not found windows map! Make sure to add new Windows on the Map!`
if(!Windows.hasWindow(specifiedWindow))
return `Name "${specifiedWindow}" not found windows map! Make sure to add new Windows on the Map!`
switch(args[0]) { switch(args[0]) {
case "open": case "open":
@@ -137,11 +146,11 @@ function handleVolumeArgs(args: Array<string>) {
return ` return `
Control speaker and microphone volumes easily! Control speaker and microphone volumes easily!
Options: Options:
sink-set [number]: set sink(speaker) volume with [number], 0 to ${Wireplumber.getDefault().getMaxSinkVolume()}. sink-set [number]: set sink(speaker) volume with [number], 0 to ${Wireplumber.getDefault().getMaxSinkVolume() || 100}.
sink-mute: toggle mute for the sink(speaker) device. sink-mute: toggle mute for the sink(speaker) device.
sink-increase [number]: increases sink(speaker) volume with [number]. sink-increase [number]: increases sink(speaker) volume with [number].
sink-decrease [number]: decreases sink(speaker) volume with [number]. sink-decrease [number]: decreases sink(speaker) volume with [number].
source-set [number]: set source(microphone) volume with [number], 0 to ${Wireplumber.getDefault().getMaxSourceVolume()}. source-set [number]: set source(microphone) volume with [number], 0 to ${Wireplumber.getDefault().getMaxSourceVolume() || 100}.
source-mute: toggle mute for the source(microphone) device. source-mute: toggle mute for the source(microphone) device.
source-increase [number]: increases source(microphone) volume with [number]. source-increase [number]: increases source(microphone) volume with [number].
source-decrease [number]: decreases source(microphone) volume with [number] source-decrease [number]: decreases source(microphone) volume with [number]
@@ -158,11 +167,13 @@ Options:
open [window_name]: sets specified window's visibility to true. open [window_name]: sets specified window's visibility to true.
close [window_name]: sets specified window's visibility to false. close [window_name]: sets specified window's visibility to false.
toggle [window_name]: toggles visibility of specified window. toggle [window_name]: toggles visibility of specified window.
windows: shows available windows to control.
reload: creates a new astal instance and removes this one. reload: creates a new astal instance and removes this one.
volume: wireplumber volume controller, see "volume help". volume: wireplumber volume controller, see "volume help".
runner: open the application runner. runner: open the application runner.
c, code [js]: runs provided js in args and returns result.
show-ws-numbers: show or hide workspace numbers in bar. show-ws-numbers: show or hide workspace numbers in bar.
help, -h, --help: shows this help message. h, help: shows this help message.
2025 (c) retrozinndev's Hyprland-Dots, licensed under the MIT License. 2025 (c) retrozinndev's Hyprland-Dots, licensed under the MIT License.
https://github.com/retrozinndev/Hyprland-Dots https://github.com/retrozinndev/Hyprland-Dots
+6 -1
View File
@@ -42,7 +42,12 @@ window.ask-popup {
& button { & button {
background: colors.$bg-primary; background: colors.$bg-primary;
border-radius: 12px; border-radius: 12px;
padding: 6px; padding: 9px 6px;
& label {
font-size: 16px;
font-weight: 600;
}
margin: { margin: {
left: 4px; left: 4px;
+1 -1
View File
@@ -6,7 +6,7 @@
@mixin reset-props { @mixin reset-props {
all: unset; all: unset;
transition: 120ms linear; transition: 120ms linear;
font-family: "Cantarell", "Noto Sans", font-family: "Adwaita Sans", "Cantarell", "Noto Sans",
"Noto Sans CJK JP", "Noto Sans CJK KR", "Noto Sans CJK JP", "Noto Sans CJK KR",
"Noto Sans CJK HK", "Noto Sans CJK SC", "Noto Sans CJK HK", "Noto Sans CJK SC",
"Noto Sans CJK TC", sans-serif, "Noto Sans CJK TC", sans-serif,
+9 -6
View File
@@ -3,6 +3,7 @@ import { PopupWindow, PopupWindowProps } from "./PopupWindow";
import { Astal, Gtk, Widget } from "astal/gtk3"; import { Astal, Gtk, Widget } from "astal/gtk3";
import { Separator } from "./Separator"; import { Separator } from "./Separator";
import { tr } from "../i18n/intl"; import { tr } from "../i18n/intl";
import { Windows } from "../windows";
export type AskPopupProps = { export type AskPopupProps = {
title?: string | Binding<string | undefined>; title?: string | Binding<string | undefined>;
@@ -17,15 +18,16 @@ export type AskPopupProps = {
* A Popup Widget that asks yes or no to a certain question. * A Popup Widget that asks yes or no to a certain question.
* Runs onAccept() when user accepts or else onDecline() when * Runs onAccept() when user accepts or else onDecline() when
* user doesn't accept or closes window. * user doesn't accept or closes window.
* This window isn't registered in this shell windowing stuff.
*/ */
export function AskPopup(props: AskPopupProps) { export function AskPopup(props: AskPopupProps): Gtk.Window {
const buttons = [ const buttons = [
new Widget.Button({ new Widget.Button({
className: "cancel", className: "cancel",
hexpand: true, hexpand: true,
label: props.cancelText || tr("ask_popup.options.cancel") || "Cancel", label: props.cancelText || tr("ask_popup.options.cancel") || "Cancel",
onClick: (_) => { onClick: (_) => {
window.destroy(); window.close();
props.onCancel && props.onCancel(); props.onCancel && props.onCancel();
} }
} as Widget.ButtonProps), } as Widget.ButtonProps),
@@ -34,15 +36,14 @@ export function AskPopup(props: AskPopupProps) {
hexpand: true, hexpand: true,
label: props.acceptText || tr("ask_popup.options.accept") || "Ok", label: props.acceptText || tr("ask_popup.options.accept") || "Ok",
onClick: (_) => { onClick: (_) => {
window.destroy(); window.close();
props.onAccept && props.onAccept(); props.onAccept && props.onAccept();
} }
} as Widget.ButtonProps) } as Widget.ButtonProps)
]; ];
const window = PopupWindow({ const window = Windows.createWindowForFocusedMonitor(PopupWindow({
namespace: "ask-popup", namespace: "ask-popup",
visible: true,
className: "ask-popup", className: "ask-popup",
exclusivity: Astal.Exclusivity.IGNORE, exclusivity: Astal.Exclusivity.IGNORE,
widthRequest: 350, widthRequest: 350,
@@ -80,5 +81,7 @@ export function AskPopup(props: AskPopupProps) {
} as Widget.BoxProps) } as Widget.BoxProps)
] ]
} as Widget.BoxProps) } as Widget.BoxProps)
} as PopupWindowProps); } as PopupWindowProps))();
return window;
} }
+9 -10
View File
@@ -2,7 +2,7 @@ import { Binding } from "astal";
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
const { TOP, BOTTOM, LEFT, RIGHT }: typeof Astal.WindowAnchor = Astal.WindowAnchor; const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor;
export type PopupWindowProps = Pick<Widget.WindowProps, export type PopupWindowProps = Pick<Widget.WindowProps,
"namespace" "namespace"
@@ -16,27 +16,26 @@ export type PopupWindowProps = Pick<Widget.WindowProps,
| "layer" | "layer"
| "widthRequest" | "widthRequest"
| "heightRequest" | "heightRequest"
| "child"
| "monitor" | "monitor"
| "setup" | "setup"
| "exclusivity"> & { | "exclusivity"> & {
child?: (Gtk.Widget | Binding<Gtk.Widget | undefined>);
children?: (Array<Gtk.Widget | Binding<Gtk.Widget | undefined>>);
marginTop?: number; marginTop?: number;
marginLeft?: number; marginLeft?: number;
marginBottom?: number; marginBottom?: number;
marginRight?: number; marginRight?: number;
onKeyPressEvent?: (self: Widget.Window, event: Gdk.Event) => void; onKeyPressEvent?: (self: Widget.Window, event: Gdk.Event) => void;
/** Do something else instead of hiding window on close action(clicking outside conent / pressing Escape) /** Do something else instead of closing window on close action(clicking outside conent/pressing Escape)
* Observation: onClose() function will still be ran after close action if defined. * Observation: onClose() function will still be ran after close action if defined.
*/ */
closeAction?: (self: Widget.Window) => void; closeAction?: (self: Widget.Window) => void;
onClose?: (self: Widget.Window) => void; onClose?: (self: Widget.Window) => void;
}; };
export function PopupWindow(props: PopupWindowProps): Widget.Window { export const PopupWindow = (props: PopupWindowProps): Widget.Window => {
if(!props.closeAction) if(!props.closeAction)
props.closeAction = (window) => { props.closeAction = (window) => window.close();
window.hide();
};
return new Widget.Window({ return new Widget.Window({
namespace: props?.namespace || "popup-window", namespace: props?.namespace || "popup-window",
@@ -47,9 +46,8 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window {
keymode: Astal.Keymode.EXCLUSIVE, keymode: Astal.Keymode.EXCLUSIVE,
layer: props?.layer || Astal.Layer.OVERLAY, layer: props?.layer || Astal.Layer.OVERLAY,
focusOnMap: true, focusOnMap: true,
visible: props?.visible,
monitor: props?.monitor || 0,
setup: props.setup, setup: props.setup,
monitor: props.monitor || 0,
onButtonPressEvent: (_, event: Gdk.Event) => { onButtonPressEvent: (_, event: Gdk.Event) => {
const [, posX, posY] = event.get_coords(); const [, posX, posY] = event.get_coords();
const childAllocation = _.get_child()!.get_allocation(); const childAllocation = _.get_child()!.get_allocation();
@@ -88,7 +86,8 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window {
widthRequest: props.widthRequest, widthRequest: props.widthRequest,
heightRequest: props.heightRequest, heightRequest: props.heightRequest,
onButtonPressEvent: () => true, onButtonPressEvent: () => true,
child: props.child ...(props.child ? { child: props.child } : {}),
...(props.children ? { children: props.children } : {})
} as Widget.BoxProps) } as Widget.BoxProps)
} as Widget.WindowProps); } as Widget.WindowProps);
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import { Windows } from "../../windows";
export function Apps(): Gtk.Widget { export function Apps(): Gtk.Widget {
return new Widget.EventBox({ return new Widget.EventBox({
onClickRelease: () => Windows.getWindow("apps-window")?.show(), onClickRelease: () => Windows.open("apps-window"),
className: "apps", className: "apps",
child: new Widget.Box({ child: new Widget.Box({
child: new Widget.Icon({ child: new Widget.Icon({
+3 -4
View File
@@ -2,14 +2,13 @@ import { Gtk, Widget } from "astal/gtk3";
import { getDateTime } from "../../scripts/time"; import { getDateTime } from "../../scripts/time";
import { bind, GLib } from "astal"; import { bind, GLib } from "astal";
import { Windows } from "../../windows"; import { Windows } from "../../windows";
import { CenterWindow } from "../../window/CenterWindow";
export function Clock(): Gtk.Widget { export function Clock(): Gtk.Widget {
return new Widget.Box({ return new Widget.Box({
className: bind(CenterWindow, "visible").as((visible: boolean) => className: bind(Windows, "openWindows").as((openWins) =>
visible ? "clock open" : "clock"), Object.hasOwn(openWins, "center-window") ? "open clock" : "clock"),
child: new Widget.Button({ child: new Widget.Button({
onClick: () => Windows.toggle(CenterWindow), onClick: () => Windows.toggle("center-window"),
label: getDateTime().as((dateTime: GLib.DateTime) => { label: getDateTime().as((dateTime: GLib.DateTime) => {
return dateTime.format("%A %d, %H:%M") return dateTime.format("%A %d, %H:%M")
}) })
+3 -4
View File
@@ -71,9 +71,8 @@ export function Media(): Gtk.Widget {
const mediaWidget = new Widget.EventBox({ const mediaWidget = new Widget.EventBox({
className: "media-eventbox", className: "media-eventbox",
visible: bind(mpris, "players").as((players: Array<AstalMpris.Player>) => { visible: bind(mpris, "players").as((players: Array<AstalMpris.Player>) =>
return players[0] && players[0].get_available() || CenterWindow.is_visible(); players[0] && players[0].get_available()),
}),
onDestroy: (_) => { onDestroy: (_) => {
hoverConnectionId !== undefined && hoverConnectionId !== undefined &&
_.disconnect(hoverConnectionId); _.disconnect(hoverConnectionId);
@@ -81,7 +80,7 @@ export function Media(): Gtk.Widget {
hoverLostConnectionId !== undefined && hoverLostConnectionId !== undefined &&
_.disconnect(hoverLostConnectionId); _.disconnect(hoverLostConnectionId);
}, },
onClick: () => Windows.toggle(CenterWindow), onClick: () => Windows.toggle("center-window"),
child: new Widget.Box({ child: new Widget.Box({
className: "media", className: "media",
children: [ children: [
+4 -4
View File
@@ -5,16 +5,15 @@ import AstalWp from "gi://AstalWp";
import { bind, Variable } from "astal"; import { bind, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import { Wireplumber } from "../../scripts/volume"; import { Wireplumber } from "../../scripts/volume";
import { ControlCenter } from "../../window/ControlCenter";
import { Notifications } from "../../scripts/notifications"; import { Notifications } from "../../scripts/notifications";
import { Windows } from "../../windows"; import { Windows } from "../../windows";
export function Status(): Gtk.Widget { export function Status(): Gtk.Widget {
return new Widget.EventBox({ return new Widget.EventBox({
className: bind(ControlCenter, "visible").as((visible: boolean) => className: bind(Windows, "openWindows").as((openWins) =>
visible ? "status open" : "status"), Object.hasOwn(openWins, "control-center") ? "open status" : "status"),
onClick: () => Windows.toggle(ControlCenter!), onClick: () => Windows.toggle("control-center"),
child: new Widget.Box({ child: new Widget.Box({
children: [ children: [
volumeStatusSlider({ volumeStatusSlider({
@@ -63,6 +62,7 @@ function volumeStatusSlider(props: { className?: string, endpoint: AstalWp.Endpo
revealer.add(new Widget.Slider({ revealer.add(new Widget.Slider({
className: "slider", className: "slider",
setup: (slider) => slider.set_value(Math.floor(props.endpoint.get_volume() * 100)),
onDragged: (slider) => props.endpoint.set_volume(slider.value / 100), onDragged: (slider) => props.endpoint.set_volume(slider.value / 100),
value: bind(props.endpoint, "volume").as((volume) => value: bind(props.endpoint, "volume").as((volume) =>
Math.floor(volume * 100)), Math.floor(volume * 100)),
+2 -1
View File
@@ -2,9 +2,10 @@ import { AstalIO, bind, Binding, execAsync, GLib, timeout } from "astal";
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import AstalMpris from "gi://AstalMpris"; import AstalMpris from "gi://AstalMpris";
let dragTimer: (AstalIO.Time|undefined);
export function BigMedia(): Gtk.Widget { export function BigMedia(): Gtk.Widget {
let dragTimer: (AstalIO.Time|undefined);
return new Widget.Box({ return new Widget.Box({
className: "big-media", className: "big-media",
orientation: Gtk.Orientation.VERTICAL, orientation: Gtk.Orientation.VERTICAL,
+1 -1
View File
@@ -4,7 +4,7 @@ import { HistoryNotification, Notifications } from "../../scripts/notifications"
import { NotificationWidget } from "../Notification"; import { NotificationWidget } from "../Notification";
export const NotifHistory: Gtk.Widget = new Widget.Box({ export const NotifHistory = () => new Widget.Box({
orientation: Gtk.Orientation.VERTICAL, orientation: Gtk.Orientation.VERTICAL,
className: "history", className: "history",
visible: bind(Notifications.getDefault(), "history").as(history => history.length > 0), visible: bind(Notifications.getDefault(), "history").as(history => history.length > 0),
+20 -7
View File
@@ -2,20 +2,29 @@ import { timeout, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import { Page } from "./pages/Page"; import { Page } from "./pages/Page";
const currentPage = new Variable<Page|undefined>(undefined); const currentPage = new Variable<Page | undefined>(undefined);
let pagesInstance: (Widget.Revealer | undefined);
export const PagesWidget: Widget.Revealer = new Widget.Revealer({ export const PagesWidget = () => {
const revealer = new Widget.Revealer({
revealChild: false, revealChild: false,
className: "pages", className: "pages",
transitionType: Gtk.RevealerTransitionType.SLIDE_DOWN, transitionType: Gtk.RevealerTransitionType.SLIDE_DOWN,
transitionDuration: 360, transitionDuration: 360,
child: currentPage((page: (Page|undefined)) => child: currentPage((page: (Page|undefined)) =>
!page ? new Widget.Box() : page.getPage()) !page ? new Widget.Box() : page.getPage())
} as Widget.RevealerProps); } as Widget.RevealerProps);
pagesInstance = revealer;
return revealer;
}
export function showPages(page: Page): void { export function showPages(page: Page): void {
if(!pagesInstance) return;
currentPage.set(page); currentPage.set(page);
PagesWidget.set_reveal_child(true); pagesInstance.set_reveal_child(true);
page.props.onOpen && page.props.onOpen(); page.props.onOpen && page.props.onOpen();
} }
@@ -24,7 +33,9 @@ export function getPage(): (Page|undefined) {
} }
export function togglePage(page: Page): void { export function togglePage(page: Page): void {
if(!PagesWidget.revealChild) { if(!pagesInstance) return;
if(!pagesInstance.revealChild) {
showPages(page); showPages(page);
return; return;
} }
@@ -33,10 +44,12 @@ export function togglePage(page: Page): void {
} }
export function hidePages() { export function hidePages() {
PagesWidget.set_reveal_child(false); if(!pagesInstance) return;
pagesInstance.set_reveal_child(false);
if(!currentPage.get()) return; if(!currentPage.get()) return;
timeout(500, () => { timeout(pagesInstance.transitionDuration || 500, () => {
if(currentPage.get() && currentPage.get()?.props.onClose) if(currentPage.get() && currentPage.get()?.props.onClose)
currentPage.get()!.props.onClose!(); currentPage.get()!.props.onClose!();
+1 -1
View File
@@ -57,7 +57,7 @@ function LogoutButton(): Widget.Button {
} as Widget.ButtonProps); } as Widget.ButtonProps);
} }
export const QuickActions: Widget.Box = new Widget.Box({ export const QuickActions = () => new Widget.Box({
className: "quickactions", className: "quickactions",
children: [ children: [
new Widget.Box({ new Widget.Box({
+3 -1
View File
@@ -2,7 +2,7 @@ import { bind } from "astal";
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import { Wireplumber } from "../../scripts/volume"; import { Wireplumber } from "../../scripts/volume";
export const Sliders: Gtk.Widget = new Widget.Box({ export const Sliders = () => new Widget.Box({
className: "sliders", className: "sliders",
orientation: Gtk.Orientation.VERTICAL, orientation: Gtk.Orientation.VERTICAL,
expand: true, expand: true,
@@ -17,6 +17,7 @@ export const Sliders: Gtk.Widget = new Widget.Box({
new Widget.Slider({ new Widget.Slider({
drawValue: false, drawValue: false,
hexpand: true, hexpand: true,
setup: (slider) => slider.set_value(Wireplumber.getDefault().getSinkVolume()),
value: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) => value: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) =>
Math.floor(volume * 100)), Math.floor(volume * 100)),
max: Wireplumber.getDefault().getMaxSinkVolume(), max: Wireplumber.getDefault().getMaxSinkVolume(),
@@ -34,6 +35,7 @@ export const Sliders: Gtk.Widget = new Widget.Box({
new Widget.Slider({ new Widget.Slider({
drawValue: false, drawValue: false,
hexpand: true, hexpand: true,
setup: (slider) => slider.set_value(Wireplumber.getDefault().getSourceVolume()),
value: bind(Wireplumber.getDefault().getDefaultSource(), "volume").as((volume: number) => value: bind(Wireplumber.getDefault().getDefaultSource(), "volume").as((volume: number) =>
Math.floor(volume * 100)), Math.floor(volume * 100)),
max: Wireplumber.getDefault().getMaxSourceVolume(), max: Wireplumber.getDefault().getMaxSourceVolume(),
+4 -6
View File
@@ -3,13 +3,13 @@ import { TileNetwork } from "./tiles/Network";
import { TileBluetooth } from "./tiles/Bluetooth"; import { TileBluetooth } from "./tiles/Bluetooth";
import { TileDND } from "./tiles/DoNotDisturb"; import { TileDND } from "./tiles/DoNotDisturb";
export const tileList: Array<any> = [ export const tileList: Array<() => Gtk.Widget> = [
TileNetwork, TileNetwork,
TileBluetooth, TileBluetooth,
TileDND TileDND
]; ];
export function TilesWidget(): Gtk.Widget { export function Tiles(): Gtk.Widget {
const tilesFlowBox: Gtk.FlowBox = new Gtk.FlowBox({ const tilesFlowBox: Gtk.FlowBox = new Gtk.FlowBox({
visible: true, visible: true,
orientation: Gtk.Orientation.HORIZONTAL, orientation: Gtk.Orientation.HORIZONTAL,
@@ -21,13 +21,11 @@ export function TilesWidget(): Gtk.Widget {
homogeneous: true, homogeneous: true,
} as Gtk.FlowBox.ConstructorProps); } as Gtk.FlowBox.ConstructorProps);
tileList.map((item: Gtk.Widget) => tileList.map((item: (() => Gtk.Widget)) =>
tilesFlowBox.insert(item, -1)); tilesFlowBox.insert(item(), -1));
return new Widget.Box({ return new Widget.Box({
className: "tiles-container", className: "tiles-container",
child: tilesFlowBox child: tilesFlowBox
} as Widget.BoxProps); } as Widget.BoxProps);
} }
export const Tiles: Gtk.Widget = TilesWidget();
+4 -4
View File
@@ -6,7 +6,7 @@ import { togglePage } from "../Pages";
import { PageNetwork } from "../pages/Network"; import { PageNetwork } from "../pages/Network";
import { tr } from "../../../i18n/intl"; import { tr } from "../../../i18n/intl";
export const TileNetwork = new Widget.Box({ export const TileNetwork = () => new Widget.Box({
child: Variable.derive([ child: Variable.derive([
bind(AstalNetwork.get_default(), "primary"), bind(AstalNetwork.get_default(), "primary"),
bind(AstalNetwork.get_default(), "wired"), bind(AstalNetwork.get_default(), "wired"),
@@ -36,7 +36,7 @@ export const TileNetwork = new Widget.Box({
icon: "󰤨", icon: "󰤨",
iconSize: 16, iconSize: 16,
toggleState: bind(wifi, "enabled") toggleState: bind(wifi, "enabled")
} as TileProps); } as TileProps)();
} else if(primary === AstalNetwork.Primary.WIRED) { } else if(primary === AstalNetwork.Primary.WIRED) {
return Tile({ return Tile({
@@ -69,7 +69,7 @@ export const TileNetwork = new Widget.Box({
internet === AstalNetwork.Internet.CONNECTING internet === AstalNetwork.Internet.CONNECTING
|| internet === AstalNetwork.Internet.CONNECTED || internet === AstalNetwork.Internet.CONNECTED
) )
} as TileProps); } as TileProps)();
} }
return Tile({ return Tile({
@@ -82,6 +82,6 @@ export const TileNetwork = new Widget.Box({
iconSize: 16, iconSize: 16,
toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) => toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED) internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)
} as TileProps); } as TileProps)();
})() })()
} as Widget.BoxProps); } as Widget.BoxProps);
+3 -3
View File
@@ -15,7 +15,7 @@ export type TileProps = {
onClickMore?: () => void; onClickMore?: () => void;
} }
export function Tile(props: TileProps): Widget.EventBox { export function Tile(props: TileProps): (() => Widget.EventBox) {
const toggled = new Variable<boolean>(props.toggleState instanceof Binding ? const toggled = new Variable<boolean>(props.toggleState instanceof Binding ?
(props.toggleState.get() || false) : (props.toggleState || false)); (props.toggleState.get() || false) : (props.toggleState || false));
@@ -24,7 +24,7 @@ export function Tile(props: TileProps): Widget.EventBox {
if(props?.toggleState instanceof Binding) if(props?.toggleState instanceof Binding)
subscription = props.toggleState.subscribe(val => toggled.set(val || false)); subscription = props.toggleState.subscribe(val => toggled.set(val || false));
return new Widget.EventBox({ return () => new Widget.EventBox({
className: toggled().as((state: boolean) => className: toggled().as((state: boolean) =>
state ? "tile-eventbox toggled" : "tile-eventbox"), state ? "tile-eventbox toggled" : "tile-eventbox"),
expand: true, expand: true,
@@ -39,7 +39,7 @@ export function Tile(props: TileProps): Widget.EventBox {
toggled.set(true); toggled.set(true);
props.onToggledOn && props.onToggledOn(); props.onToggledOn && props.onToggledOn();
}, },
onDestroy: () => subscription(), onDestroy: () => subscription?.(),
child: new Widget.Box({ child: new Widget.Box({
className: (props.className instanceof Binding) ? className: (props.className instanceof Binding) ?
props.className.as((clsName: (string|undefined)) => props.className.as((clsName: (string|undefined)) =>
+15 -9
View File
@@ -4,17 +4,22 @@ import { cleanExec, getAstalApps } from "../scripts/apps";
import AstalApps from "gi://AstalApps"; import AstalApps from "gi://AstalApps";
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
const searchString = new Variable<string>("");
let searchSubscription: () => void;
export const AppsWindow = new Widget.Window({ export const AppsWindow = (mon: number): (Widget.Window) => {
const searchString = new Variable<string>("");
let searchSubscription: () => void;
return new Widget.Window({
namespace: "apps-window", namespace: "apps-window",
layer: Astal.Layer.OVERLAY, layer: Astal.Layer.OVERLAY,
exclusivity: Astal.Exclusivity.IGNORE, exclusivity: Astal.Exclusivity.IGNORE,
anchor: TOP | LEFT | RIGHT | BOTTOM, anchor: TOP | LEFT | RIGHT | BOTTOM,
visible: false,
keymode: Astal.Keymode.EXCLUSIVE, keymode: Astal.Keymode.EXCLUSIVE,
onDestroy: () => searchSubscription(), monitor: mon,
onDestroy: () => {
searchString.set("");
searchSubscription()
},
setup: (window) => { setup: (window) => {
const flowbox = new Gtk.FlowBox({ const flowbox = new Gtk.FlowBox({
rowSpacing: 6, rowSpacing: 6,
@@ -46,7 +51,7 @@ export const AppsWindow = new Widget.Window({
if(event.button === Astal.MouseButton.PRIMARY) { if(event.button === Astal.MouseButton.PRIMARY) {
searchString.set(""); searchString.set("");
entry.text = ""; entry.text = "";
window.hide(); window.close();
cleanExec(app); cleanExec(app);
return; return;
@@ -83,13 +88,13 @@ export const AppsWindow = new Widget.Window({
onClick: () => { onClick: () => {
searchString.set(""); searchString.set("");
entry.text = ""; entry.text = "";
window.hide(); window.close();
}, },
onKeyPressEvent: (_, event: Gdk.Event) => { onKeyPressEvent: (_, event: Gdk.Event) => {
if(event.get_keyval()[1] === Gdk.KEY_Escape) { if(event.get_keyval()[1] === Gdk.KEY_Escape) {
searchString.set(""); searchString.set("");
entry.text = ""; entry.text = "";
window.hide(); window.close();
} }
}, },
child: new Widget.Box({ child: new Widget.Box({
@@ -108,4 +113,5 @@ export const AppsWindow = new Widget.Window({
} as Widget.BoxProps) } as Widget.BoxProps)
} as Widget.EventBoxProps)); } as Widget.EventBoxProps));
} }
} as Widget.WindowProps); });
}
+5 -5
View File
@@ -1,22 +1,22 @@
import { Astal, Gtk, Widget } from "astal/gtk3"; import { Astal, Gtk, Widget } from "astal/gtk3";
import { Clock } from "../widget/bar/Clock";
import { Tray } from "../widget/bar/Tray"; import { Tray } from "../widget/bar/Tray";
import { Workspaces } from "../widget/bar/Workspaces"; import { Workspaces } from "../widget/bar/Workspaces";
import { FocusedClient } from "../widget/bar/FocusedClient"; import { FocusedClient } from "../widget/bar/FocusedClient";
import { Media } from "../widget/bar/Media"; import { Media } from "../widget/bar/Media";
import { Status } from "../widget/bar/Status";
import { Apps } from "../widget/bar/Apps"; import { Apps } from "../widget/bar/Apps";
import { Clock } from "../widget/bar/Clock";
import { Status } from "../widget/bar/Status";
export const Bar: Widget.Window = new Widget.Window({ export const Bar = (mon: number) => new Widget.Window({
monitor: 0,
namespace: "top-bar", namespace: "top-bar",
anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT, anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT,
layer: Astal.Layer.TOP, layer: Astal.Layer.TOP,
exclusivity: Astal.Exclusivity.EXCLUSIVE, exclusivity: Astal.Exclusivity.EXCLUSIVE,
heightRequest: 46, heightRequest: 46,
canFocus: false, monitor: mon,
visible: true, visible: true,
canFocus: false,
child: new Widget.Box({ child: new Widget.Box({
className: "bar-container", className: "bar-container",
child: new Widget.CenterBox({ child: new Widget.CenterBox({
+9 -16
View File
@@ -1,23 +1,18 @@
import { Gtk, Widget } from "astal/gtk3"; import { Gtk, Widget } from "astal/gtk3";
import { bind, GLib } from "astal"; import { GLib } from "astal";
import { getDateTime } from "../scripts/time"; import { getDateTime } from "../scripts/time";
import { BigMedia } from "../widget/center-window/BigMedia";
import { Separator, SeparatorProps } from "../widget/Separator"; import { Separator, SeparatorProps } from "../widget/Separator";
import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow";
import { BigMedia } from "../widget/center-window/BigMedia";
const BigMediaWidget = BigMedia(); export const CenterWindow = (mon: number) => PopupWindow({
className: "center-window-container",
export const CenterWindow: Widget.Window = PopupWindow({
className: "center-window",
namespace: "center-window", namespace: "center-window",
monitor: 0,
visible: false,
marginTop: 10, marginTop: 10,
valign: Gtk.Align.START, valign: Gtk.Align.START,
halign: Gtk.Align.CENTER, halign: Gtk.Align.CENTER,
child: new Widget.Box({ monitor: mon,
className: "center-window-container",
children: [ children: [
new Widget.Box({ new Widget.Box({
className: "vertical left", className: "vertical left",
@@ -47,15 +42,14 @@ export const CenterWindow: Widget.Window = PopupWindow({
valign: Gtk.Align.START, valign: Gtk.Align.START,
child: new Gtk.Calendar({ child: new Gtk.Calendar({
visible: true, visible: true,
show_heading: true, showHeading: true,
show_day_names: true, showDayNames: true,
show_week_numbers: false showWeekNumbers: false
} as Gtk.Calendar.ConstructorProps) } as Gtk.Calendar.ConstructorProps)
} as Widget.BoxProps) } as Widget.BoxProps)
] ]
} as Widget.BoxProps), } as Widget.BoxProps),
Separator({ Separator({
visible: bind(BigMediaWidget, "visible"),
orientation: Gtk.Orientation.HORIZONTAL, orientation: Gtk.Orientation.HORIZONTAL,
alpha: .5, alpha: .5,
cssColor: "gray", cssColor: "gray",
@@ -65,9 +59,8 @@ export const CenterWindow: Widget.Window = PopupWindow({
className: "vertical right", className: "vertical right",
orientation: Gtk.Orientation.VERTICAL, orientation: Gtk.Orientation.VERTICAL,
children: [ children: [
BigMediaWidget BigMedia()
] ]
} as Widget.BoxProps) } as Widget.BoxProps)
] ]
} as Widget.BoxProps)
} as PopupWindowProps); } as PopupWindowProps);
+13 -16
View File
@@ -4,11 +4,11 @@ import { Tiles } from "../widget/control-center/Tiles";
import { Sliders } from "../widget/control-center/Sliders"; import { Sliders } from "../widget/control-center/Sliders";
import { hidePages, PagesWidget } from "../widget/control-center/Pages"; import { hidePages, PagesWidget } from "../widget/control-center/Pages";
import { NotifHistory } from "../widget/control-center/NotifHistory"; import { NotifHistory } from "../widget/control-center/NotifHistory";
import { Windows } from "../windows";
const connections: Array<number> = [];
const { TOP, LEFT, BOTTOM, RIGHT } = Astal.WindowAnchor; const { TOP, LEFT, BOTTOM, RIGHT } = Astal.WindowAnchor;
export const ControlCenter = new Widget.Window({ export const ControlCenter = (mon: number) => new Widget.Window({
namespace: "control-center", namespace: "control-center",
className: "control-center", className: "control-center",
anchor: TOP | BOTTOM | LEFT | RIGHT, anchor: TOP | BOTTOM | LEFT | RIGHT,
@@ -16,22 +16,23 @@ export const ControlCenter = new Widget.Window({
keymode: Astal.Keymode.EXCLUSIVE, keymode: Astal.Keymode.EXCLUSIVE,
layer: Astal.Layer.OVERLAY, layer: Astal.Layer.OVERLAY,
focusOnMap: true, focusOnMap: true,
visible: false, monitor: mon,
monitor: 0, onDestroy: (_) => {
onDestroy: (_) => connections.map(id => _.disconnect(id)), hidePages();
},
onButtonPressEvent: (_, event: Gdk.Event) => { onButtonPressEvent: (_, event: Gdk.Event) => {
const [, posX, posY] = event.get_coords(); const [, posX, posY] = event.get_coords();
const childAllocation = _.get_child()!.get_allocation(); const childAllocation = _.get_child()!.get_allocation();
if((posX < childAllocation.x || posX > (childAllocation.x + childAllocation.width)) || if((posX < childAllocation.x || posX > (childAllocation.x + childAllocation.width)) ||
(posY < childAllocation.y || posY > (childAllocation.y + childAllocation.height))) { (posY < childAllocation.y || posY > (childAllocation.y + childAllocation.height))) {
_.hide(); Windows.close("control-center");
hidePages(); hidePages();
} }
}, },
onKeyPressEvent: (_, event: Gdk.Event) => { onKeyPressEvent: (_, event: Gdk.Event) => {
if(event.get_keyval()[1] === Gdk.KEY_Escape) { if(event.get_keyval()[1] === Gdk.KEY_Escape) {
_.hide(); Windows.close("control-center");
hidePages(); hidePages();
} }
}, },
@@ -54,17 +55,13 @@ export const ControlCenter = new Widget.Window({
widthRequest: 400, widthRequest: 400,
vexpand: false, vexpand: false,
children: [ children: [
QuickActions, QuickActions(),
Sliders, Sliders(),
Tiles, Tiles(),
PagesWidget PagesWidget()
] ]
} as Widget.BoxProps), } as Widget.BoxProps),
NotifHistory NotifHistory()
] ]
} as Widget.BoxProps) } as Widget.BoxProps)
} as Widget.WindowProps); } as Widget.WindowProps);
connections.push(ControlCenter.connect("hide", (_) => {
hidePages();
}));
+3 -22
View File
@@ -1,42 +1,23 @@
import { Astal, Gtk, Widget } from "astal/gtk3"; import { Astal, Gtk, Widget } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
import { bind } from "astal/binding"; import { bind } from "astal/binding";
import { Notifications } from "../scripts/notifications"; import { Notifications } from "../scripts/notifications";
import { NotificationWidget } from "../widget/Notification"; import { NotificationWidget } from "../widget/Notification";
const connections: Array<number> = [];
export const FloatingNotifications: Widget.Window = new Widget.Window({ export const FloatingNotifications = (mon: number) => new Widget.Window({
namespace: "floating-notifications", namespace: "floating-notifications",
canFocus: false, canFocus: false,
anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT, anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT,
monitor: 0, monitor: mon,
layer: Astal.Layer.OVERLAY, layer: Astal.Layer.OVERLAY,
visible: false,
widthRequest: 450, widthRequest: 450,
exclusivity: Astal.Exclusivity.NORMAL, exclusivity: Astal.Exclusivity.NORMAL,
setup: (window) => {
connections.push(
Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => {
!window.is_visible() && window.show();
}),
Notifications.getDefault().connect("notification-removed", (_: Notifications, _id: number) => {
window.is_visible() && _.notifications.length === 0 && window.hide()
window.isFocus = false;
})
);
},
onDestroy: () => connections.map(id => Notifications.getDefault().disconnect(id)),
child: new Widget.Box({ child: new Widget.Box({
className: "floating-notifications-container", className: "floating-notifications-container",
orientation: Gtk.Orientation.VERTICAL, orientation: Gtk.Orientation.VERTICAL,
homogeneous: false, homogeneous: false,
visible: bind(Notifications.getDefault(), "notifications").as(notifs => notifs.length > 0), visible: bind(Notifications.getDefault(), "notifications").as(notifs => notifs.length > 0),
children: bind(Notifications.getDefault(), "notifications").as((notifs) => children: bind(Notifications.getDefault(), "notifications").as((notifs) =>
notifs.map((item) => notifs.map((item) => NotificationWidget(item, () => Notifications.getDefault().removeNotification(item)))),
NotificationWidget(item,
() => Notifications.getDefault().removeNotification(item))
)
),
} as Widget.BoxProps) } as Widget.BoxProps)
} as Widget.WindowProps); } as Widget.WindowProps);
+2 -11
View File
@@ -6,14 +6,13 @@ import { AskPopup } from "../widget/AskPopup";
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
export const LogoutMenu: Widget.Window = new Widget.Window({ export const LogoutMenu = (mon: number) => new Widget.Window({
namespace: "logout-menu", namespace: "logout-menu",
anchor: TOP | LEFT | RIGHT | BOTTOM, anchor: TOP | LEFT | RIGHT | BOTTOM,
layer: Astal.Layer.OVERLAY, layer: Astal.Layer.OVERLAY,
exclusivity: Astal.Exclusivity.IGNORE, exclusivity: Astal.Exclusivity.IGNORE,
keymode: Astal.Keymode.EXCLUSIVE, keymode: Astal.Keymode.EXCLUSIVE,
monitor: 0, monitor: mon,
visible: false,
onKeyPressEvent: (_, event: Gdk.Event) => { onKeyPressEvent: (_, event: Gdk.Event) => {
event.get_keyval()[1] === Gdk.KEY_Escape && event.get_keyval()[1] === Gdk.KEY_Escape &&
_.hide(); _.hide();
@@ -57,8 +56,6 @@ export const LogoutMenu: Widget.Window = new Widget.Window({
onClick: () => AskPopup({ onClick: () => AskPopup({
title: "Power Off", title: "Power Off",
text: "Are you sure you want to power off? Unsaved work will be lost.", text: "Are you sure you want to power off? Unsaved work will be lost.",
cancelText: "No! Let me go back",
acceptText: "Yes, shutdown",
onAccept: () => execAsync("systemctl poweroff") onAccept: () => execAsync("systemctl poweroff")
}) })
} as Widget.ButtonProps), } as Widget.ButtonProps),
@@ -68,8 +65,6 @@ export const LogoutMenu: Widget.Window = new Widget.Window({
onClick: () => AskPopup({ onClick: () => AskPopup({
title: "Reboot", title: "Reboot",
text: "Are you sure you want to Reboot? Unsaved work will be lost.", text: "Are you sure you want to Reboot? Unsaved work will be lost.",
cancelText: "No! Let me go back",
acceptText: "Yes, reboot",
onAccept: () => execAsync("systemctl reboot") onAccept: () => execAsync("systemctl reboot")
}) })
} as Widget.ButtonProps), } as Widget.ButtonProps),
@@ -79,8 +74,6 @@ export const LogoutMenu: Widget.Window = new Widget.Window({
onClick: () => AskPopup({ onClick: () => AskPopup({
title: "Suspend", title: "Suspend",
text: "Are you sure you want to Suspend?", text: "Are you sure you want to Suspend?",
cancelText: "No! Let me go back",
acceptText: "Yes, suspend",
onAccept: () => execAsync("systemctl suspend") onAccept: () => execAsync("systemctl suspend")
}) })
} as Widget.ButtonProps), } as Widget.ButtonProps),
@@ -90,8 +83,6 @@ export const LogoutMenu: Widget.Window = new Widget.Window({
onClick: () => AskPopup({ onClick: () => AskPopup({
title: "Log out", title: "Log out",
text: "Are you sure you want to log out? Your session will be ended.", text: "Are you sure you want to log out? Your session will be ended.",
cancelText: "No! Let me go back",
acceptText: "Yes, please log out",
onAccept: () => execAsync(`sh -c "loginctl terminate-user ${GLib.getenv("USER") || "$USER"}"`) onAccept: () => execAsync(`sh -c "loginctl terminate-user ${GLib.getenv("USER") || "$USER"}"`)
}) })
} as Widget.ButtonProps), } as Widget.ButtonProps),
+4 -4
View File
@@ -20,15 +20,15 @@ export function setOSDMode(newMode: OSDModes): void {
osdMode.set(newMode); osdMode.set(newMode);
} }
export const OSD: Widget.Window = new Widget.Window({ export const OSD = (mon: number) => new Widget.Window({
namespace: "osd", namespace: "osd",
layer: Astal.Layer.OVERLAY, layer: Astal.Layer.OVERLAY,
anchor: Astal.WindowAnchor.BOTTOM, anchor: Astal.WindowAnchor.BOTTOM,
canFocus: false, canFocus: false,
margin_bottom: 80, marginBottom: 80,
monitor: 0,
visible: false,
focusOnClick: false, focusOnClick: false,
clickThrough: true,
monitor: mon,
child: new Widget.Box({ child: new Widget.Box({
className: "osd", className: "osd",
children: [ children: [
+165 -31
View File
@@ -1,58 +1,192 @@
import { Gtk } from "astal/gtk3"; import { Widget } from "astal/gtk3";
import { Bar } from "./window/Bar"; import { Bar } from "./window/Bar";
import { OSD } from "./window/OSD"; import { OSD } from "./window/OSD";
import { ControlCenter } from "./window/ControlCenter"; import { ControlCenter } from "./window/ControlCenter";
import { CenterWindow } from "./window/CenterWindow"; import { CenterWindow } from "./window/CenterWindow";
import { FloatingNotifications } from "./window/FloatingNotifications";
import { GObject, register } from "astal";
import { LogoutMenu } from "./window/LogoutMenu"; import { LogoutMenu } from "./window/LogoutMenu";
import { FloatingNotifications } from "./window/FloatingNotifications";
import { AppsWindow } from "./window/AppsWindow"; import { AppsWindow } from "./window/AppsWindow";
import AstalHyprland from "gi://AstalHyprland";
import { GObject } from "astal";
/** /**
* get open windows / interact with windows(e.g.: close, open or toggle) * get open windows / interact with windows(e.g.: close, open or toggle)
*/ */
@register({ GTypeName: "Windows" }) export const Windows = GObject.registerClass({
class WindowsClass extends GObject.Object { GTypeName: "Windows",
private static windowsMap: Map<string, Gtk.Window> = new Map<string, Gtk.Window>(); Signals: {
"open": { param_types: [ GObject.TYPE_STRING ] },
"close": { param_types: [ GObject.TYPE_STRING ] }
},
Properties: {
"open-windows": GObject.ParamSpec.jsobject(
"open-windows",
"Open Windows",
"A Readonly object that stores open GTKLayerShell Windows",
GObject.ParamFlags.READABLE
)
}
}, class Windows extends GObject.Object {
#openWindows: Record<string, Widget.Window | Array<Widget.Window>> = {};
static #instance: (Windows | null);
static { #windows: Record<string, (() => (Widget.Window | Array<Widget.Window>))> = {
this.setWindow("bar", Bar); "bar": this.createWindowForMonitors(Bar),
this.setWindow("osd", OSD); "osd": this.createWindowForFocusedMonitor(OSD),
this.setWindow("control-center", ControlCenter); "control-center": this.createWindowForFocusedMonitor(ControlCenter),
this.setWindow("center-window", CenterWindow); "center-window": this.createWindowForFocusedMonitor(CenterWindow),
this.setWindow("logout-menu", LogoutMenu); "logout-menu": this.createWindowForFocusedMonitor(LogoutMenu),
this.setWindow("floating-notifications", FloatingNotifications); "floating-notifications": this.createWindowForFocusedMonitor(FloatingNotifications),
this.setWindow("apps-window", AppsWindow); "apps-window": this.createWindowForFocusedMonitor(AppsWindow)
};
#windowConnections: Record<string, (Array<number> | Array<Array<number>>)> = {};
get windows() { return this.#windows; }
get openWindows(): Record<string, Widget.Window | Array<Widget.Window>> { return this.#openWindows; };
vfunc_dispose() {
for(const name of Object.keys(this.#windowConnections)) {
const window = this.openWindows[name];
if(!window) continue;
this.disconnectWindow(name);
}
} }
public static setWindow(name: string, window: Gtk.Window): void { private disconnectWindow(name: keyof typeof this.windows) {
WindowsClass.windowsMap.set(name, window); const window = this.openWindows[name];
if(!window) {
console.log("couldn't disconnect, window is not open");
return;
} }
public static getWindow(name: string): (Gtk.Window|undefined) { this.#windowConnections[name].map((id: Array<number> | number) => {
return WindowsClass.windowsMap.get(name); if(Array.isArray(window)) {
window.map((win, i) => win.disconnect((id as Array<number>)[i]));
return;
} }
public static getList(): Map<string, Gtk.Window> { window.disconnect(id as number);
return WindowsClass.windowsMap; });
delete this.#windowConnections[name];
} }
public static open(window: Gtk.Window): void { private connectWindow(name: keyof typeof this.windows) {
!WindowsClass.isVisible(window) && window.show(); if(Object.hasOwn(this.#windowConnections, name)) return;
if(!this.openWindows?.[name]) {
console.log(`${name} is not open, will not connect`);
return;
} }
public static isVisible(window: Gtk.Window): boolean { if(Array.isArray(this.openWindows[name])) {
return window.get_visible(); this.#windowConnections[name] = this.openWindows[name].map(win => [
win.connect("map", (window) => {
this.#openWindows[name] = window;
}),
win.connect("destroy", () => {
this.disconnectWindow(name);
delete this.#openWindows[name];
})
]);
return;
} }
public static close(window: Gtk.Window): void { this.#windowConnections[name] = [
WindowsClass.isVisible(window) && window.hide(); this.openWindows[name].connect("map", (window) => {
this.#openWindows[name] = window;
}),
this.openWindows[name].connect("destroy", () => {
this.disconnectWindow(name);
delete this.#openWindows[name];
})
];
} }
public static toggle(window: Gtk.Window): void { public static getDefault(): Windows {
window.is_visible() ? WindowsClass.close(window) : WindowsClass.open(window); if(!this.#instance)
} this.#instance = new Windows();
}
export { WindowsClass as Windows }; return this.#instance;
}
public createWindowForMonitors(windowFun: (mon: number) => Widget.Window): (() => Array<Widget.Window>) {
return () => AstalHyprland.get_default().get_monitors().map(mon =>
windowFun(mon.id));
}
public createWindowForFocusedMonitor(windowFun: (mon: number) => Widget.Window): (() => Widget.Window) {
return () => windowFun(AstalHyprland.get_default().get_monitors().filter(mon => mon.focused)[0].id);
}
public addWindow(name: string, window: (() => (Widget.Window | Array<Widget.Window>))): void {
this.#windows[name] = window;
}
public hasWindow(name: keyof typeof this.windows): boolean {
return Boolean(this.windows?.[name as keyof typeof this.windows]);
}
public getWindow(name: (keyof typeof this.windows | string)): ((() => (Widget.Window | Array<Widget.Window>)) | undefined) {
return this.windows?.[name as keyof typeof this.windows];
}
public getOpenWindow(name: (keyof typeof this.openWindows)): (Widget.Window | Array<Widget.Window> | undefined) {
return this.openWindows?.[name as keyof typeof this.openWindows];
}
public getWindows(): Array<(() => (Widget.Window | Array<Widget.Window>))> {
return Object.values(this.windows);
}
public isVisible(name: keyof typeof this.windows): boolean {
return Object.hasOwn(this.#openWindows, name);
}
public open(name: keyof typeof this.windows): void {
if(this.isVisible(name)) return;
let window: (() => (Widget.Window | Array<Widget.Window>)) = this.getWindow(name)!;
const openWindows: (Array<Widget.Window> | Widget.Window) = window();
this.#openWindows[name] = openWindows;
this.connectWindow(name);
this.emit("open", name);
this.notify("open-windows");
if(Array.isArray(openWindows)) {
openWindows.map(win => win.show());
return;
}
openWindows.show();
}
public close(name: keyof typeof this.windows): void {
if(!this.isVisible(name)) return;
this.disconnectWindow(name);
const windows = this.#openWindows[name];
delete this.#openWindows[name];
if(Array.isArray(windows)) {
windows.map(win => win.close());
this.emit("close", name);
this.notify("open-windows");
return;
}
windows.close();
this.emit("close", name);
this.notify("open-windows");
}
public toggle(name: keyof typeof this.windows): void {
this.isVisible(name) ? this.close(name) : this.open(name);
}
}).getDefault();