diff --git a/modules/ags/gtk4/lib/cairo.ts b/modules/ags/gtk4/lib/cairo.ts new file mode 100644 index 00000000..b01c69b8 --- /dev/null +++ b/modules/ags/gtk4/lib/cairo.ts @@ -0,0 +1,47 @@ +type PointProps = [number, number] | { + x: number + y: number +} | number; + +export class Point { + public x = 0; + public y = 0; + + get values(): [number, number] { + return [this.x, this.y]; + } + + constructor(props?: PointProps, y?: number) { + if (typeof props === 'number') { + if (y) { + this.x = props; + this.y = y; + } + else { + throw new Error('Wrong props'); + } + } + else if (Array.isArray(props)) { + this.x = props[0]; + this.y = props[1]; + } + else if (props) { + this.x = props.x; + this.y = props.y; + } + } +} + +export type BezierPoints = [number, number, number, number]; + +export class Bezier { + private _points: BezierPoints; + + get points() { + return [...this._points] as BezierPoints; + } + + constructor(x1: number, y1: number, x2: number, y2: number) { + this._points = [x1, y1, x2, y2]; + } +} diff --git a/modules/ags/gtk4/lib/hypr.ts b/modules/ags/gtk4/lib/hypr.ts new file mode 100644 index 00000000..04f1bc30 --- /dev/null +++ b/modules/ags/gtk4/lib/hypr.ts @@ -0,0 +1,94 @@ +import { Gdk } from 'astal/gtk4'; + +import AstalHyprland from 'gi://AstalHyprland'; + + +export const get_hyprland_monitor = (monitor: Gdk.Monitor): AstalHyprland.Monitor | undefined => { + const hyprland = AstalHyprland.get_default(); + + const manufacturer = monitor.get_manufacturer()?.replace(',', ''); + const model = monitor.get_model()?.replace(',', ''); + const start = `${manufacturer} ${model}`; + + return hyprland.get_monitors().find((m) => m.get_description()?.startsWith(start)); +}; + +export const get_hyprland_monitor_desc = (monitor: Gdk.Monitor): string => { + const hyprland = AstalHyprland.get_default(); + + const manufacturer = monitor.get_manufacturer()?.replace(',', ''); + const model = monitor.get_model()?.replace(',', ''); + const start = `${manufacturer} ${model}`; + + return `desc:${hyprland + .get_monitors() + .find((m) => m.get_description()?.startsWith(start))?.get_description()}`; +}; + +export const get_gdkmonitor_from_desc = (desc: string): Gdk.Monitor => { + const display = Gdk.Display.get_default(); + + for (let m = 0; m < (display?.get_monitors().get_n_items() ?? 0); m++) { + const monitor = display?.get_monitors().get_item(m) as Gdk.Monitor; + + if (monitor && desc === get_hyprland_monitor_desc(monitor)) { + return monitor; + } + } + + throw Error(`Monitor ${desc} not found`); +}; + +export const get_monitor_desc = (mon: AstalHyprland.Monitor): string => { + return `desc:${mon.get_description()}`; +}; + +export const hyprMessage = (message: string) => new Promise(( + resolution = () => { /**/ }, + rejection = () => { /**/ }, +) => { + const hyprland = AstalHyprland.get_default(); + + try { + hyprland.message_async(message, (_, asyncResult) => { + const result = hyprland.message_finish(asyncResult); + + resolution(result); + }); + } + catch (e) { + rejection(e); + } +}); + +export const centerCursor = (): void => { + const hyprland = AstalHyprland.get_default(); + + let x: number; + let y: number; + const monitor = hyprland.get_focused_monitor(); + + switch (monitor.get_transform()) { + case 1: + x = monitor.get_x() - (monitor.get_height() / 2); + y = monitor.get_y() - (monitor.get_width() / 2); + break; + + case 2: + x = monitor.get_x() - (monitor.get_width() / 2); + y = monitor.get_y() - (monitor.get_height() / 2); + break; + + case 3: + x = monitor.get_x() + (monitor.get_height() / 2); + y = monitor.get_y() + (monitor.get_width() / 2); + break; + + default: + x = monitor.get_x() + (monitor.get_width() / 2); + y = monitor.get_y() + (monitor.get_height() / 2); + break; + } + + hyprMessage(`dispatch movecursor ${x} ${y}`); +}; diff --git a/modules/ags/gtk4/lib/index.ts b/modules/ags/gtk4/lib/index.ts new file mode 100644 index 00000000..0b00a1a5 --- /dev/null +++ b/modules/ags/gtk4/lib/index.ts @@ -0,0 +1,4 @@ +export * from './cairo'; +export * from './hypr'; +export * from './notify'; +export * from './windows'; diff --git a/modules/ags/gtk4/lib/notify.ts b/modules/ags/gtk4/lib/notify.ts new file mode 100644 index 00000000..3c29275b --- /dev/null +++ b/modules/ags/gtk4/lib/notify.ts @@ -0,0 +1,67 @@ +import { subprocess } from 'astal'; + +/* Types */ +interface NotifyAction { + id: string + label: string + callback: () => void +} +interface NotifySendProps { + actions?: NotifyAction[] + appName?: string + body?: string + category?: string + hint?: string + iconName: string + replaceId?: number + title: string + urgency?: 'low' | 'normal' | 'critical' +} + +const escapeShellArg = (arg: string | undefined): string => `'${arg?.replace(/'/g, '\'\\\'\'') ?? ''}'`; + +export const notifySend = ({ + actions = [], + appName, + body, + category, + hint, + iconName, + replaceId, + title, + urgency = 'normal', +}: NotifySendProps) => new Promise((resolve) => { + let printedId = false; + + const cmd = [ + 'notify-send', + '--print-id', + `--icon=${escapeShellArg(iconName)}`, + escapeShellArg(title), + escapeShellArg(body ?? ''), + // Optional params + appName ? `--app-name=${escapeShellArg(appName)}` : '', + category ? `--category=${escapeShellArg(category)}` : '', + hint ? `--hint=${escapeShellArg(hint)}` : '', + replaceId ? `--replace-id=${replaceId.toString()}` : '', + `--urgency=${urgency}`, + ].concat( + actions.map(({ id, label }) => `--action=${escapeShellArg(id)}=${escapeShellArg(label)}`), + ).join(' '); + + subprocess( + cmd, + (out) => { + if (!printedId) { + resolve(parseInt(out)); + printedId = true; + } + else { + actions.find((action) => action.id === out)?.callback(); + } + }, + (err) => { + console.error(`[Notify] ${err}`); + }, + ); +}); diff --git a/modules/ags/gtk4/lib/windows.ts b/modules/ags/gtk4/lib/windows.ts new file mode 100644 index 00000000..dc8a5c3f --- /dev/null +++ b/modules/ags/gtk4/lib/windows.ts @@ -0,0 +1,63 @@ +import { idle } from 'astal'; +import { App, Gdk, Gtk } from 'astal/gtk4'; + +/* Types */ +import PopupWindow from '../widget/misc/popup-window'; + +export interface Layer { + address: string + x: number + y: number + w: number + h: number + namespace: string +} +export interface Levels { + 0?: Layer[] | null + 1?: Layer[] | null + 2?: Layer[] | null + 3?: Layer[] | null +} +export interface Layers { + levels: Levels +} +export type LayerResult = Record; +export interface CursorPos { + x: number + y: number +} + +export const closeAll = () => { + (App.get_windows() as PopupWindow[]) + .filter((w) => w && + w.close_on_unfocus && + w.close_on_unfocus !== 'stay') + .forEach((w) => { + App.get_window(w.name)?.set_visible(false); + }); +}; + +export const perMonitor = (window: (monitor: Gdk.Monitor) => Gtk.Widget) => idle(() => { + const display = Gdk.Display.get_default(); + const windows = new Map(); + + const createWindow = (monitor: Gdk.Monitor) => { + windows.set(monitor, window(monitor)); + }; + + for (let m = 0; m < (display?.get_monitors().get_n_items() ?? 0); m++) { + const monitor = display?.get_monitors().get_item(m) as Gdk.Monitor; + + if (monitor) { + createWindow(monitor); + } + } + + display?.connect('monitor-added', (_, monitor) => { + createWindow(monitor); + }); + + display?.connect('monitor-removed', (_, monitor) => { + windows.delete(monitor); + }); +}); diff --git a/modules/ags/gtk4/widget/misc/popup-window.ts b/modules/ags/gtk4/widget/misc/popup-window.ts new file mode 100644 index 00000000..ae8edc11 --- /dev/null +++ b/modules/ags/gtk4/widget/misc/popup-window.ts @@ -0,0 +1,111 @@ +import { App, Astal, Gtk } from 'astal/gtk4'; +import { property, register } from 'astal/gobject'; +import { Binding, idle } from 'astal'; + +import { WindowClass, WindowProps } from '../subclasses'; +import { get_hyprland_monitor, hyprMessage } from '../../lib'; + +/* Types */ +type CloseType = 'none' | 'stay' | 'released' | 'clicked'; +type HyprTransition = 'slide' | 'slide top' | 'slide bottom' | 'slide left' | + 'slide right' | 'popin' | 'fade'; +type PopupCallback = (self?: WindowClass) => void; + +export type PopupWindowProps = WindowProps & { + transition?: HyprTransition | Binding + close_on_unfocus?: CloseType | Binding + on_open?: PopupCallback + on_close?: PopupCallback +}; + + +@register() +export class PopupWindow extends WindowClass { + @property(String) + declare transition: HyprTransition | Binding; + + @property(String) + declare close_on_unfocus: CloseType | Binding; + + on_open: PopupCallback; + on_close: PopupCallback; + + constructor({ + transition = 'slide top', + close_on_unfocus = 'released', + on_open = () => { /**/ }, + on_close = () => { /**/ }, + + name, + visible = false, + layer = Astal.Layer.OVERLAY, + ...rest + }: PopupWindowProps) { + super({ + ...rest, + name: `win-${name}`, + namespace: `win-${name}`, + visible: false, + layer, + setup: () => idle(() => { + // Add way to make window open on startup + if (visible) { + this.visible = true; + } + }), + } as WindowProps); + + App.add_window(this); + + const setTransition = (_: PopupWindow, t: HyprTransition | Binding) => { + hyprMessage(`keyword layerrule animation ${t}, ${this.name}`).catch(console.log); + }; + + this.connect('notify::transition', setTransition); + + this.close_on_unfocus = close_on_unfocus; + this.transition = transition; + this.on_open = on_open; + this.on_close = on_close; + + this.connect('notify::visible', () => { + // Make sure we have the right animation + setTransition(this, this.transition); + + if (this.visible) { + this.on_open(this); + } + else { + this.on_close(this); + } + }); + }; + + async set_x_pos( + alloc: Gtk.Allocation, + side = 'right' as 'left' | 'right', + ) { + const monitor = this.gdkmonitor ?? this.get_current_monitor(); + + const transform = get_hyprland_monitor(monitor)?.get_transform(); + + let width: number; + + if (transform && (transform === 1 || transform === 3)) { + width = monitor.get_geometry().height; + } + else { + width = monitor.get_geometry().width; + } + + this.margin_right = side === 'right' ? + (width - alloc.x - alloc.width) : + this.margin_right; + + this.margin_left = side === 'right' ? + this.margin_left : + (alloc.x - alloc.width); + } +} + +export default PopupWindow; diff --git a/modules/ags/gtk4/widget/misc/separator.ts b/modules/ags/gtk4/widget/misc/separator.ts new file mode 100644 index 00000000..59b54b1d --- /dev/null +++ b/modules/ags/gtk4/widget/misc/separator.ts @@ -0,0 +1,12 @@ +import { Box, BoxProps } from '../subclasses'; + + +export default ({ + size, + vertical = false, + css = '', + ...rest +}: { size: number } & BoxProps) => Box({ + css: `* { ${vertical ? 'min-height' : 'min-width'}: ${size}px; ${css} }`, + ...rest, +} as BoxProps);