feat(ags4): migrate lib and misc widgets
All checks were successful
Discord / discord commits (push) Has been skipped
All checks were successful
Discord / discord commits (push) Has been skipped
This commit is contained in:
parent
345d51b6ab
commit
a1caad618b
7 changed files with 398 additions and 0 deletions
47
modules/ags/gtk4/lib/cairo.ts
Normal file
47
modules/ags/gtk4/lib/cairo.ts
Normal file
|
@ -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];
|
||||
}
|
||||
}
|
94
modules/ags/gtk4/lib/hypr.ts
Normal file
94
modules/ags/gtk4/lib/hypr.ts
Normal file
|
@ -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<string>((
|
||||
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}`);
|
||||
};
|
4
modules/ags/gtk4/lib/index.ts
Normal file
4
modules/ags/gtk4/lib/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './cairo';
|
||||
export * from './hypr';
|
||||
export * from './notify';
|
||||
export * from './windows';
|
67
modules/ags/gtk4/lib/notify.ts
Normal file
67
modules/ags/gtk4/lib/notify.ts
Normal file
|
@ -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<number>((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}`);
|
||||
},
|
||||
);
|
||||
});
|
63
modules/ags/gtk4/lib/windows.ts
Normal file
63
modules/ags/gtk4/lib/windows.ts
Normal file
|
@ -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<string, Layers>;
|
||||
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<Gdk.Monitor, Gtk.Widget>();
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
111
modules/ags/gtk4/widget/misc/popup-window.ts
Normal file
111
modules/ags/gtk4/widget/misc/popup-window.ts
Normal file
|
@ -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<HyprTransition>
|
||||
close_on_unfocus?: CloseType | Binding<CloseType>
|
||||
on_open?: PopupCallback
|
||||
on_close?: PopupCallback
|
||||
};
|
||||
|
||||
|
||||
@register()
|
||||
export class PopupWindow extends WindowClass {
|
||||
@property(String)
|
||||
declare transition: HyprTransition | Binding<HyprTransition>;
|
||||
|
||||
@property(String)
|
||||
declare close_on_unfocus: CloseType | Binding<CloseType>;
|
||||
|
||||
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<HyprTransition>) => {
|
||||
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;
|
12
modules/ags/gtk4/widget/misc/separator.ts
Normal file
12
modules/ags/gtk4/widget/misc/separator.ts
Normal file
|
@ -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);
|
Loading…
Reference in a new issue