mirror of
https://github.com/zephrynis/nix-flake.git
synced 2026-02-18 20:21:53 +00:00
feat: make AGS colorshell configuration fully declarative
- Add complete colorshell v2.0.3 configuration to home/ags-config/ - Disable runner plugin and NightLight tile (incompatible with NixOS) - Customize SCSS with full opacity (no transparency) - Add dark pale blue color scheme in home/pywal-colors/ - Configure Papirus-Dark icon theme via home-manager - Make ~/.config/ags/ immutable and managed by Nix store - Auto-deploy pywal colors to ~/.cache/wal/colors.json All AGS configuration is now reproducible and version controlled.
This commit is contained in:
38
home/ags-config/widget/AskPopup.tsx
Normal file
38
home/ags-config/widget/AskPopup.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Accessor } from "ags";
|
||||
import { tr } from "../i18n/intl";
|
||||
import { CustomDialog } from "./CustomDialog";
|
||||
import { Astal } from "ags/gtk4";
|
||||
|
||||
|
||||
export type AskPopupProps = {
|
||||
title?: string | Accessor<string>;
|
||||
text: string | Accessor<string>;
|
||||
cancelText?: string;
|
||||
acceptText?: string;
|
||||
onAccept?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Popup Widget that asks yes or no to a defined promt.
|
||||
* Runs onAccept() when user accepts, or else onDecline() when
|
||||
* user doesn't accept / closes window.
|
||||
* This window isn't usually registered in this shell windowing
|
||||
* system.
|
||||
*/
|
||||
export function AskPopup(props: AskPopupProps): Astal.Window {
|
||||
let accepted: boolean = false;
|
||||
|
||||
return <CustomDialog namespace={"ask-popup"} widthRequest={400} heightRequest={250}
|
||||
title={props.title ?? tr("ask_popup.title")} text={props.text}
|
||||
onFinish={() => !accepted && props.onCancel?.()} options={[
|
||||
{ text: props.cancelText ?? tr("cancel") },
|
||||
{
|
||||
text: props.acceptText ?? tr("accept"),
|
||||
onClick: () => {
|
||||
accepted = true;
|
||||
props.onAccept?.();
|
||||
}
|
||||
}
|
||||
]} /> as Astal.Window;
|
||||
}
|
||||
73
home/ags-config/widget/AuthPopup.tsx
Normal file
73
home/ags-config/widget/AuthPopup.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { CustomDialog, getContainerCustomDialog } from "./CustomDialog";
|
||||
|
||||
import GLib from "gi://GLib?version=2.0";
|
||||
|
||||
|
||||
export type AuthPopupData = {
|
||||
user: string;
|
||||
hidePassword: boolean;
|
||||
passwd: string;
|
||||
};
|
||||
|
||||
export function AuthPopup(props: {
|
||||
/** hide password on showup. @default true */
|
||||
hidePassword?: boolean;
|
||||
/** icon name of the application that's requesting this popup */
|
||||
iconName?: string;
|
||||
/** popup body */
|
||||
text: string;
|
||||
/** selected user by default */
|
||||
user?: string;
|
||||
/** approve data after the user clicks the "grant permission" button */
|
||||
onContinue: (data: AuthPopupData, reject: (message: string) => void, approve: () => void) => void;
|
||||
}): Astal.Window {
|
||||
const data = {
|
||||
passwd: "",
|
||||
user: props.user ?? GLib.get_user_name(),
|
||||
hidePassword: props.hidePassword ?? true
|
||||
} satisfies AuthPopupData;
|
||||
const allowUserChange = props.user === undefined;
|
||||
|
||||
const dialog = <CustomDialog title={"Authentication"} text={props.text}
|
||||
namespace={"auth-popup"} options={[
|
||||
{ text: "Deny" }, // will close and call onFinish by default
|
||||
{
|
||||
text: "Grant permission",
|
||||
onClick: () => {
|
||||
if(allowUserChange)
|
||||
data.user = userEntry!.text;
|
||||
|
||||
data.passwd = passwordEntry.text;
|
||||
data.hidePassword = passwordEntry.showPeekIcon;
|
||||
|
||||
props.onContinue(data,
|
||||
// rejected by checker function
|
||||
(m) => {
|
||||
// show error to user
|
||||
!messageLabel.is_visible &&
|
||||
messageLabel.set_visible(true);
|
||||
messageLabel.set_label(m);
|
||||
|
||||
// clear password entry
|
||||
passwordEntry.set_text("");
|
||||
},
|
||||
// approved by the checker
|
||||
dialog.close
|
||||
);
|
||||
},
|
||||
closeOnClick: false
|
||||
}
|
||||
]}>
|
||||
|
||||
<Gtk.Entry class={"user"} placeholderText={"User"} visible={allowUserChange} />
|
||||
<Gtk.PasswordEntry class={"password"} showPeekIcon placeholderText={"Password"} />
|
||||
|
||||
<Gtk.Label class={"message"} label={""} />
|
||||
</CustomDialog> as Astal.Window;
|
||||
const messageLabel = getContainerCustomDialog(dialog).get_last_child() as Gtk.Label;
|
||||
const userEntry = allowUserChange ? getContainerCustomDialog(dialog).get_first_child() as Gtk.Entry : undefined;
|
||||
const passwordEntry = getContainerCustomDialog(dialog).get_first_child()?.get_next_sibling() as Gtk.PasswordEntry;
|
||||
|
||||
return dialog;
|
||||
}
|
||||
90
home/ags-config/widget/BackgroundWindow.tsx
Normal file
90
home/ags-config/widget/BackgroundWindow.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Accessor } from "ags";
|
||||
import { Astal, Gdk, Gtk } from "ags/gtk4";
|
||||
import GObject from "gi://GObject?version=2.0";
|
||||
|
||||
|
||||
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
|
||||
|
||||
export type BackgroundWindowProps = {
|
||||
/** GtkWindow Layer */
|
||||
layer?: Astal.Layer | Accessor<Astal.Layer>;
|
||||
/** Monitor number where the window should open */
|
||||
monitor: number | Accessor<number>;
|
||||
/** Custom stylesheet used in the window. default: `background: rgba(0, 0, 0, .2)` */
|
||||
css?: string | Accessor<string>;
|
||||
/* Function that is called when the user releases a key in the keyboard on the window
|
||||
* The `Escape` key is not passed to this function */
|
||||
actionKeyPressed?: (window: Astal.Window, keyval: number, keycode: number) => void;
|
||||
/** Function that is called when the user triggers a mouse-click or escape action on the window */
|
||||
actionFired?: (window: Astal.Window) => void;
|
||||
/** Function that is called when the user clicks on the window with primary mouse button */
|
||||
actionClickPrimary?: (window: Astal.Window) => void;
|
||||
/** Function that is called when the user clicks on the window with secodary mouse button */
|
||||
actionClickSecondary?: (window: Astal.Window) => void;
|
||||
onCloseRequest?: (window: Astal.Window) => void;
|
||||
keymode?: Astal.Keymode;
|
||||
exclusivity?: Astal.Exclusivity;
|
||||
|
||||
/** attach this window as a background for another window
|
||||
* background-window will close when the attached window triggers ::close-request) */
|
||||
attach?: Astal.Window;
|
||||
};
|
||||
|
||||
/** Creates a fullscreen GtkWindow that is used for making
|
||||
* the user focus on the content after this window(e.g.: AskPopup,
|
||||
* Authentication Window(futurely) or any PopupWindow)
|
||||
*
|
||||
* @param props Properties for background-window
|
||||
*
|
||||
* @returns The generated background-window
|
||||
*/
|
||||
export function BackgroundWindow(props: BackgroundWindowProps): Astal.Window {
|
||||
const conns: Map<GObject.Object, number> = new Map();
|
||||
|
||||
return <Astal.Window namespace={"background-window"} monitor={props.monitor} visible
|
||||
layer={props.layer ?? Astal.Layer.OVERLAY} keymode={props.keymode ?? Astal.Keymode.EXCLUSIVE}
|
||||
onCloseRequest={props.onCloseRequest} exclusivity={props.exclusivity ?? Astal.Exclusivity.IGNORE}
|
||||
anchor={TOP | LEFT | BOTTOM | RIGHT} css={props.css ?? "background: rgba(0, 0, 0, .2);"}
|
||||
$={(self) => {
|
||||
const gestureClick = Gtk.GestureClick.new(),
|
||||
eventControllerKey = Gtk.EventControllerKey.new();
|
||||
|
||||
gestureClick.set_button(0);
|
||||
|
||||
self.add_controller(gestureClick);
|
||||
self.add_controller(eventControllerKey);
|
||||
|
||||
conns.set(eventControllerKey, eventControllerKey.connect("key-released",
|
||||
(_, keyval, keycode) => {
|
||||
if(keyval === Gdk.KEY_Escape) {
|
||||
props.actionFired?.(self);
|
||||
return;
|
||||
}
|
||||
|
||||
props.actionKeyPressed?.(self, keyval, keycode);
|
||||
}
|
||||
));
|
||||
|
||||
conns.set(gestureClick, gestureClick.connect("released", (gesture) => {
|
||||
if(gesture.get_current_button() === Gdk.BUTTON_PRIMARY) {
|
||||
props.actionClickPrimary?.(self);
|
||||
return;
|
||||
}
|
||||
|
||||
if(gesture.get_current_button() === Gdk.BUTTON_SECONDARY) {
|
||||
props.actionClickSecondary?.(self);
|
||||
return;
|
||||
}
|
||||
|
||||
props.actionFired?.(self);
|
||||
}));
|
||||
|
||||
props.attach &&
|
||||
conns.set(props.attach, (props.attach as Gtk.Widget).connect("close-request", () =>
|
||||
self.close()
|
||||
));
|
||||
|
||||
conns.set(self, self.connect("destroy", () => conns.forEach((id, obj) =>
|
||||
obj.disconnect(id))));
|
||||
}} /> as Astal.Window;
|
||||
}
|
||||
39
home/ags-config/widget/Bar.tsx
Normal file
39
home/ags-config/widget/Bar.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import app from "ags/gtk4/app"
|
||||
import { Astal, Gtk, Gdk } from "ags/gtk4"
|
||||
import { execAsync } from "ags/process"
|
||||
import { createPoll } from "ags/time"
|
||||
|
||||
export default function Bar(gdkmonitor: Gdk.Monitor) {
|
||||
const time = createPoll("", 1000, "date")
|
||||
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
|
||||
|
||||
return (
|
||||
<window
|
||||
visible
|
||||
name="bar"
|
||||
class="Bar"
|
||||
gdkmonitor={gdkmonitor}
|
||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||
anchor={TOP | LEFT | RIGHT}
|
||||
application={app}
|
||||
>
|
||||
<centerbox cssName="centerbox">
|
||||
<button
|
||||
$type="start"
|
||||
onClicked={() => execAsync("echo hello").then(console.log)}
|
||||
hexpand
|
||||
halign={Gtk.Align.CENTER}
|
||||
>
|
||||
<label label="Welcome to AGS!" />
|
||||
</button>
|
||||
<box $type="center" />
|
||||
<menubutton $type="end" hexpand halign={Gtk.Align.CENTER}>
|
||||
<label label={time} />
|
||||
<popover>
|
||||
<Gtk.Calendar />
|
||||
</popover>
|
||||
</menubutton>
|
||||
</centerbox>
|
||||
</window>
|
||||
)
|
||||
}
|
||||
80
home/ags-config/widget/CustomDialog.tsx
Normal file
80
home/ags-config/widget/CustomDialog.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { Windows } from "../windows";
|
||||
import { getPopupWindowContainer, PopupWindow } from "./PopupWindow";
|
||||
import { Separator } from "./Separator";
|
||||
import { tr } from "../i18n/intl";
|
||||
import { Accessor, Node } from "ags";
|
||||
import { transformWidget, variableToBoolean } from "../modules/utils";
|
||||
|
||||
|
||||
export type CustomDialogProps = {
|
||||
namespace?: string | Accessor<string>;
|
||||
className?: string | Accessor<string>;
|
||||
cssBackground?: string;
|
||||
title?: string | Accessor<string>;
|
||||
text?: string | Accessor<string>;
|
||||
heightRequest?: number | Accessor<number>;
|
||||
widthRequest?: number | Accessor<number>;
|
||||
childOrientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
|
||||
children?: Node;
|
||||
onFinish?: () => void;
|
||||
options?: Array<CustomDialogOption> | Accessor<Array<CustomDialogOption>>;
|
||||
optionsOrientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
|
||||
};
|
||||
|
||||
export interface CustomDialogOption {
|
||||
onClick?: () => void;
|
||||
text: string | Accessor<string>;
|
||||
closeOnClick?: boolean | Accessor<boolean>;
|
||||
}
|
||||
|
||||
function CustomDialogOption({closeOnClick = true, ...props}: CustomDialogOption & {
|
||||
dialog: Astal.Window;
|
||||
}) {
|
||||
return <Gtk.Button class="option" hexpand label={props.text}
|
||||
onClicked={() => {
|
||||
props.onClick?.();
|
||||
closeOnClick &&
|
||||
props.dialog?.close();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
export function CustomDialog({ options = [{ text: tr("accept") }], ...props}: CustomDialogProps) {
|
||||
return Windows.getDefault().createWindowForFocusedMonitor((mon) => {
|
||||
const popup = <PopupWindow namespace={props.namespace ?? "custom-dialog"} monitor={mon}
|
||||
cssBackgroundWindow={props.cssBackground ?? "background: rgba(0, 0, 0, .3);"}
|
||||
exclusivity={Astal.Exclusivity.IGNORE} layer={Astal.Layer.OVERLAY}
|
||||
halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} actionClosed={() => props.onFinish?.()}
|
||||
widthRequest={props.widthRequest ?? 400} heightRequest={props.heightRequest ?? 220}>
|
||||
|
||||
<Gtk.Box class={props.className ?? "custom-dialog-container"}
|
||||
orientation={Gtk.Orientation.VERTICAL}>
|
||||
|
||||
<Gtk.Label class={"title"} visible={variableToBoolean(props.title)} label={props.title} />
|
||||
<Gtk.Label class={"text"} visible={variableToBoolean(props.text)} label={props.text}
|
||||
vexpand valign={Gtk.Align.START} />
|
||||
<Gtk.Box class={"custom-children custom-child"} visible={variableToBoolean(props.children)}
|
||||
orientation={props.childOrientation ?? Gtk.Orientation.VERTICAL}>
|
||||
{transformWidget(props.children, (child) => child as JSX.Element)}
|
||||
</Gtk.Box>
|
||||
<Separator alpha={.2} visible={options && options.length > 0}
|
||||
spacing={8} orientation={Gtk.Orientation.VERTICAL} />
|
||||
</Gtk.Box>
|
||||
</PopupWindow> as Astal.Window;
|
||||
|
||||
(popup.get_child()!.get_first_child()!.get_first_child() as Gtk.Box).append(
|
||||
<Gtk.Box class={"options"} orientation={props.optionsOrientation ?? Gtk.Orientation.HORIZONTAL}
|
||||
hexpand={true} heightRequest={38} homogeneous={true}>
|
||||
|
||||
{transformWidget(options, (props) => <CustomDialogOption {...props} dialog={popup} />)}
|
||||
</Gtk.Box> as Gtk.Box
|
||||
);
|
||||
|
||||
return popup;
|
||||
})();
|
||||
}
|
||||
|
||||
export function getContainerCustomDialog(dialog: Astal.Window): Gtk.Box {
|
||||
return getPopupWindowContainer(dialog).get_first_child()?.get_last_child()?.get_prev_sibling() as Gtk.Box;
|
||||
}
|
||||
61
home/ags-config/widget/EntryPopup.tsx
Normal file
61
home/ags-config/widget/EntryPopup.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Accessor } from "ags";
|
||||
import { tr } from "../i18n/intl";
|
||||
import { CustomDialog } from "./CustomDialog";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
|
||||
export type EntryPopupProps = {
|
||||
title: string | Accessor<string>;
|
||||
text?: string | Accessor<string>;
|
||||
cancelText?: string | Accessor<string>;
|
||||
acceptText?: string | Accessor<string>;
|
||||
closeOnAccept?: boolean;
|
||||
entryPlaceholder?: string | Accessor<string>;
|
||||
onAccept: (userInput: string) => void;
|
||||
onCancel?: () => void;
|
||||
onFinish?: () => void;
|
||||
isPassword?: boolean | Accessor<string>;
|
||||
};
|
||||
|
||||
export function EntryPopup(props: EntryPopupProps): Astal.Window {
|
||||
props.closeOnAccept = props.closeOnAccept ?? true;
|
||||
let entered: boolean = false;
|
||||
|
||||
function onActivate(entry: Gtk.Entry|Gtk.PasswordEntry) {
|
||||
props.closeOnAccept && window.close();
|
||||
entered = true;
|
||||
props.onAccept(entry.text);
|
||||
entry.text = "";
|
||||
}
|
||||
|
||||
const entry = props.isPassword ?
|
||||
<Gtk.PasswordEntry class={"password"} xalign={.5}
|
||||
placeholderText={props.entryPlaceholder}
|
||||
onActivate={onActivate}
|
||||
/> as Gtk.PasswordEntry
|
||||
: <Gtk.Entry xalign={.5} placeholderText={props.entryPlaceholder}
|
||||
onActivate={onActivate} /> as Gtk.Entry;
|
||||
|
||||
const window = <CustomDialog namespace={"entry-popup"} widthRequest={420}
|
||||
heightRequest={220} title={props.title} text={props.text}
|
||||
options={[
|
||||
{
|
||||
text: props.cancelText ?? tr("cancel"),
|
||||
onClick: props.onCancel
|
||||
},
|
||||
{
|
||||
text: props.acceptText ?? tr("accept"),
|
||||
closeOnClick: props.closeOnAccept,
|
||||
onClick: () => {
|
||||
entered = true;
|
||||
props.onAccept(entry.text);
|
||||
entry.text = "";
|
||||
}
|
||||
}
|
||||
]} onFinish={() => {
|
||||
!entered && props.onCancel?.()
|
||||
props.onFinish?.();
|
||||
}}
|
||||
/> as Astal.Window;
|
||||
|
||||
return window;
|
||||
}
|
||||
122
home/ags-config/widget/Notification.tsx
Normal file
122
home/ags-config/widget/Notification.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Gdk, Gtk } from "ags/gtk4";
|
||||
import { Separator } from "./Separator";
|
||||
import { HistoryNotification, Notifications } from "../modules/notifications";
|
||||
import { getAppIcon, getSymbolicIcon } from "../modules/apps";
|
||||
import { escapeUnintendedMarkup, pathToURI } from "../modules/utils";
|
||||
import { onCleanup } from "ags";
|
||||
|
||||
import GObject from "ags/gobject";
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
import Pango from "gi://Pango?version=1.0";
|
||||
import GLib from "gi://GLib?version=2.0";
|
||||
|
||||
|
||||
function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) {
|
||||
const img = notif.image || notif.appIcon;
|
||||
|
||||
if(!img || !img.includes('/'))
|
||||
return undefined;
|
||||
|
||||
return pathToURI(img);
|
||||
}
|
||||
|
||||
export function NotificationWidget({ notification, actionClicked, holdOnHover, showTime, actionClose }: {
|
||||
notification: AstalNotifd.Notification|number|HistoryNotification;
|
||||
actionClicked?: (notif: AstalNotifd.Notification|HistoryNotification) => void;
|
||||
actionClose?: (notif: AstalNotifd.Notification|HistoryNotification) => void;
|
||||
holdOnHover?: boolean;
|
||||
showTime?: boolean; // It's showTime :speaking_head: :boom: :bangbang:
|
||||
}): Gtk.Widget {
|
||||
|
||||
notification = (typeof notification === "number") ?
|
||||
AstalNotifd.get_default().get_notification(notification)
|
||||
: notification;
|
||||
|
||||
const actions: Array<AstalNotifd.Action>|undefined = ((notification instanceof AstalNotifd.Notification) &&
|
||||
notification.actions && notification.actions.filter(a => Boolean(a)).length > 0) ?
|
||||
notification.actions?.filter(a =>
|
||||
a?.id?.toLowerCase() !== "view" && a?.label?.toLowerCase() != "view"
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const conns: Map<GObject.Object, Array<number>> = new Map();
|
||||
|
||||
onCleanup(() => conns.forEach((ids, obj) =>
|
||||
ids.forEach(id => obj.disconnect(id))
|
||||
));
|
||||
|
||||
return <Gtk.Box hexpand class={`notification ${
|
||||
Notifications.getDefault().getUrgencyString(notification.urgency)
|
||||
}`} orientation={Gtk.Orientation.VERTICAL} spacing={5}>
|
||||
|
||||
<Gtk.EventControllerMotion onEnter={() => holdOnHover &&
|
||||
Notifications.getDefault().holdNotification(notification.id)
|
||||
} onLeave={() => holdOnHover &&
|
||||
Notifications.getDefault().releaseNotification(notification.id)
|
||||
}
|
||||
/>
|
||||
<Gtk.GestureClick onReleased={(gesture) =>
|
||||
gesture.get_current_button() === Gdk.BUTTON_PRIMARY &&
|
||||
actionClicked?.(notification)
|
||||
} />
|
||||
<Gtk.Box class={"top"} hexpand>
|
||||
<Gtk.Image class="app-icon" $={(self) => {
|
||||
const icon = getSymbolicIcon(notification.appIcon ?? notification.appName) ??
|
||||
getSymbolicIcon(notification.appName) ?? getAppIcon(notification.appName);
|
||||
|
||||
if(icon) {
|
||||
self.set_from_icon_name(icon);
|
||||
return;
|
||||
}
|
||||
|
||||
self.set_visible(false);
|
||||
}} />
|
||||
<Gtk.Label class={"app-name"} halign={Gtk.Align.START} hexpand
|
||||
label={notification.appName || "Application"} />
|
||||
|
||||
<Gtk.Label class={"time"} visible={showTime} xalign={1}
|
||||
label={GLib.DateTime.new_from_unix_local(notification.time).format("%H:%M") ?? ""} />
|
||||
|
||||
<Gtk.Button halign={Gtk.Align.END} iconName={"window-close-symbolic"}
|
||||
class={"close"} onClicked={() => actionClose?.(notification)}/>
|
||||
</Gtk.Box>
|
||||
<Separator alpha={.1} orientation={Gtk.Orientation.VERTICAL} />
|
||||
<Gtk.Box class={"content"}>
|
||||
{getNotificationImage(notification) &&
|
||||
<Gtk.Box class={"image"} hexpand={false}
|
||||
css={`background-image: url("${getNotificationImage(notification)}");`}
|
||||
valign={Gtk.Align.START}
|
||||
widthRequest={68}
|
||||
heightRequest={64}
|
||||
/>
|
||||
}
|
||||
<Gtk.Box class={"text"} orientation={Gtk.Orientation.VERTICAL}
|
||||
vexpand>
|
||||
|
||||
<Gtk.Label class={"summary"} useMarkup hexpand xalign={0}
|
||||
vexpand={false} ellipsize={Pango.EllipsizeMode.END} label={
|
||||
escapeUnintendedMarkup(notification.summary)}
|
||||
/>
|
||||
|
||||
<Gtk.Label class={"body"} useMarkup xalign={0} wrap hexpand
|
||||
vexpand wrapMode={Pango.WrapMode.WORD_CHAR} valign={Gtk.Align.START} label={
|
||||
escapeUnintendedMarkup(notification.body)}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
|
||||
{(notification instanceof AstalNotifd.Notification) && actions && actions.length > 0 &&
|
||||
<Gtk.Box class={"actions button-row"} hexpand>
|
||||
{
|
||||
actions.map(action =>
|
||||
<Gtk.Button class={"action"} label={action.label} hexpand
|
||||
onClicked={(_) => {
|
||||
notification.invoke(action.id);
|
||||
actionClose?.(notification);
|
||||
}}
|
||||
/>)
|
||||
}
|
||||
</Gtk.Box>
|
||||
}
|
||||
</Gtk.Box> as Gtk.Widget;
|
||||
}
|
||||
161
home/ags-config/widget/PopupWindow.tsx
Normal file
161
home/ags-config/widget/PopupWindow.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Astal, Gdk, Gtk } from "ags/gtk4";
|
||||
import { BackgroundWindow } from "./BackgroundWindow";
|
||||
import { Accessor, CCProps, createComputed, createRoot, getScope } from "ags";
|
||||
import { omitObjectKeys, WidgetNodeType } from "../modules/utils";
|
||||
|
||||
import GObject from "ags/gobject";
|
||||
|
||||
|
||||
type PopupWindowSpecificProps = {
|
||||
$?: (self: Astal.Window) => void;
|
||||
children?: WidgetNodeType;
|
||||
/** Stylesheet for the background of the popup-window */
|
||||
cssBackgroundWindow?: string;
|
||||
class?: string | Accessor<string>;
|
||||
actionClosed?: (self: Astal.Window) => void|boolean;
|
||||
orientation?: Gtk.Orientation | Accessor<Gtk.Orientation>;
|
||||
actionClickedOutside?: (self: Astal.Window) => void;
|
||||
actionKeyPressed?: (self: Astal.Window, keyval: number, keycode: number) => void;
|
||||
};
|
||||
|
||||
export type PopupWindowProps = Pick<Partial<CCProps<Astal.Window, Astal.Window.ConstructorProps>>,
|
||||
"monitor"
|
||||
| "layer"
|
||||
| "exclusivity"
|
||||
| "marginLeft"
|
||||
| "marginTop"
|
||||
| "marginRight"
|
||||
| "marginBottom"
|
||||
| "cursor"
|
||||
| "canFocus"
|
||||
| "hasFocus"
|
||||
| "tooltipMarkup"
|
||||
| "tooltipText"
|
||||
| "namespace"
|
||||
| "visible"
|
||||
| "widthRequest"
|
||||
| "heightRequest"
|
||||
| "halign"
|
||||
| "valign"
|
||||
| "anchor"
|
||||
| "vexpand"
|
||||
| "hexpand"> & PopupWindowSpecificProps;
|
||||
|
||||
|
||||
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
|
||||
|
||||
export function PopupWindow(props: PopupWindowProps): GObject.Object {
|
||||
props.visible ??= true;
|
||||
props.layer ??= Astal.Layer.OVERLAY;
|
||||
props.actionClickedOutside ??= (self: Astal.Window) => self.close();
|
||||
|
||||
let clickedInside: boolean = false;
|
||||
|
||||
return <Astal.Window {...omitObjectKeys(props, [
|
||||
"actionKeyPressed",
|
||||
"actionClickedOutside",
|
||||
"cssBackgroundWindow",
|
||||
"anchor",
|
||||
"halign",
|
||||
"valign",
|
||||
"namespace",
|
||||
"marginTop",
|
||||
"widthRequest",
|
||||
"heightRequest",
|
||||
"visible",
|
||||
"marginLeft",
|
||||
"marginRight",
|
||||
"marginBottom",
|
||||
"hexpand",
|
||||
"vexpand",
|
||||
"orientation",
|
||||
"actionClosed",
|
||||
"$"
|
||||
])} namespace={props.namespace ?? "popup-window"} class={
|
||||
(props.class instanceof Accessor) ?
|
||||
((props.namespace instanceof Accessor) ?
|
||||
createComputed([props.class, props.namespace], (clss, namespace) =>
|
||||
`popup-window ${clss} ${namespace}`)
|
||||
: props.class.as(clss => `popup-window ${clss} ${props.namespace ?? ""}`))
|
||||
: `popup-window ${props.class ?? ""} ${props.namespace ?? ""}`
|
||||
} keymode={Astal.Keymode.EXCLUSIVE} exclusivity={props.exclusivity ?? Astal.Exclusivity.NORMAL}
|
||||
anchor={TOP | LEFT | BOTTOM | RIGHT} visible={false}
|
||||
onCloseRequest={(self) => props.actionClosed?.(self)}
|
||||
$={(self) => {
|
||||
const scope = getScope();
|
||||
const conns: Map<GObject.Object, number> = new Map();
|
||||
const gestureClick = Gtk.GestureClick.new();
|
||||
const keyController = Gtk.EventControllerKey.new();
|
||||
|
||||
self.add_controller(gestureClick);
|
||||
self.add_controller(keyController);
|
||||
|
||||
props.cssBackgroundWindow && createRoot((dispose) =>
|
||||
<BackgroundWindow monitor={props.monitor ?? 0}
|
||||
layer={props.layer} css={props.cssBackgroundWindow}
|
||||
keymode={Astal.Keymode.NONE} attach={self}
|
||||
onCloseRequest={() => dispose()}
|
||||
/>
|
||||
);
|
||||
|
||||
props.visible && self.show();
|
||||
|
||||
conns.set(gestureClick, gestureClick.connect("released", () => {
|
||||
if(clickedInside) {
|
||||
clickedInside = false;
|
||||
return;
|
||||
}
|
||||
|
||||
props.actionClickedOutside!(self);
|
||||
}));
|
||||
|
||||
conns.set(keyController, keyController.connect("key-pressed", (_, keyval, keycode) => {
|
||||
if(keyval === Gdk.KEY_Escape) {
|
||||
conns.forEach((id, obj) => {
|
||||
obj.disconnect(id);
|
||||
});
|
||||
|
||||
props.actionClickedOutside!(self);
|
||||
return;
|
||||
}
|
||||
|
||||
props.actionKeyPressed?.(self, keyval, keycode);
|
||||
}));
|
||||
|
||||
scope.onCleanup(() => conns.forEach((id, obj) => obj.disconnect(id)));
|
||||
|
||||
props.$?.(self);
|
||||
}}>
|
||||
<Gtk.Box hexpand={false} vexpand={false}>
|
||||
<Gtk.Box class={"popup-window-container"} halign={props.halign}
|
||||
valign={props.valign} widthRequest={props.widthRequest}
|
||||
hexpand={props.hexpand} vexpand={props.vexpand}
|
||||
orientation={props.orientation}
|
||||
heightRequest={props.heightRequest} css={`
|
||||
margin-left: ${props.marginLeft ?? 0}px;
|
||||
margin-right: ${props.marginRight ?? 0}px;
|
||||
margin-top: ${props.marginTop ?? 0}px;
|
||||
margin-bottom: ${props.marginBottom ?? 0}px;
|
||||
`} $={(self) => {
|
||||
const conns = new Map<GObject.Object, number>(),
|
||||
gestureClick = Gtk.GestureClick.new();
|
||||
|
||||
gestureClick.set_button(0);
|
||||
|
||||
self.add_controller(gestureClick);
|
||||
conns.set(gestureClick, gestureClick.connect("released", () =>
|
||||
clickedInside = true
|
||||
));
|
||||
|
||||
conns.set(self, self.connect("destroy", () => conns.forEach((id, obj) =>
|
||||
obj.disconnect(id))));
|
||||
}}>
|
||||
{props.children}
|
||||
</Gtk.Box>
|
||||
</Gtk.Box>
|
||||
</Astal.Window> as Astal.Window;
|
||||
}
|
||||
|
||||
export function getPopupWindowContainer(popupWindow: Astal.Window): Gtk.Box {
|
||||
return popupWindow.get_child()!.get_first_child() as Gtk.Box;
|
||||
}
|
||||
48
home/ags-config/widget/Separator.tsx
Normal file
48
home/ags-config/widget/Separator.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Accessor } from "ags";
|
||||
import { Gtk } from "ags/gtk4";
|
||||
|
||||
|
||||
export interface SeparatorProps {
|
||||
class?: string;
|
||||
alpha?: number;
|
||||
cssColor?: string;
|
||||
orientation?: Gtk.Orientation;
|
||||
size?: number;
|
||||
spacing?: number;
|
||||
margin?: number;
|
||||
visible?: boolean | Accessor<boolean>;
|
||||
}
|
||||
|
||||
export function Separator(props: SeparatorProps = {
|
||||
orientation: Gtk.Orientation.HORIZONTAL
|
||||
}) {
|
||||
props.alpha = props.alpha ?
|
||||
(props.alpha > 1 ?
|
||||
props.alpha / 100
|
||||
: props.alpha)
|
||||
: 1;
|
||||
|
||||
props.orientation = props.orientation ?? Gtk.Orientation.HORIZONTAL;
|
||||
|
||||
return <Gtk.Box name={"separator"} vexpand={props.orientation === Gtk.Orientation.HORIZONTAL}
|
||||
hexpand={props.orientation === Gtk.Orientation.VERTICAL}
|
||||
class={`separator ${ props.orientation === Gtk.Orientation.VERTICAL ?
|
||||
"vertical" : "horizontal" }`} visible={props.visible}
|
||||
css={`.vertical { padding: ${props.spacing ?? 0}px ${props.margin ?? 7}px; }
|
||||
.horizontal { padding: ${props.margin ?? 4}px ${props.spacing ?? 0}px; }`}>
|
||||
|
||||
<Gtk.Box class={`${props.orientation === Gtk.Orientation.VERTICAL ?
|
||||
"vertical"
|
||||
: "horizontal"} ${props.class ?? ""}`}
|
||||
vexpand={props.orientation === Gtk.Orientation.HORIZONTAL}
|
||||
hexpand={props.orientation === Gtk.Orientation.VERTICAL}
|
||||
|
||||
css={`* {
|
||||
background: ${ props.cssColor ?? "lightgray" };
|
||||
opacity: ${props.alpha};
|
||||
}
|
||||
.horizontal { min-width: ${ props.size ?? 1 }px; }
|
||||
.vertical { min-height: ${ props.size ?? 1 }px; }`}
|
||||
/>
|
||||
</Gtk.Box>
|
||||
}
|
||||
Reference in New Issue
Block a user