feat(agsV2): add MonitorClicks service
All checks were successful
Discord / discord commits (push) Has been skipped

This commit is contained in:
matt1432 2024-10-16 21:44:45 -04:00
parent e47f6ea0d7
commit e3ca8dc85e
5 changed files with 234 additions and 32 deletions

View file

@ -7,6 +7,8 @@ import BgFade from './widgets/bg-fade/main';
import Corners from './widgets/corners/main';
import { NotifPopups } from './widgets/notifs/main';
import MonitorClicks from './services/monitor-clicks';
App.start({
css: style,
@ -16,5 +18,7 @@ App.start({
BgFade();
Corners();
NotifPopups();
new MonitorClicks();
},
});

View file

@ -3,6 +3,30 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1';
const Hyprland = AstalHyprland.get_default();
/* Types */
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 get_hyprland_monitor = (monitor: Gdk.Monitor): AstalHyprland.Monitor | undefined => {
const manufacturer = monitor.manufacturer?.replace(',', '');
@ -38,10 +62,18 @@ export const get_monitor_desc = (mon: AstalHyprland.Monitor): string => {
return `desc:${mon.description}`;
};
export const hyprMessage = (message: string) => new Promise<string>((resolution = () => { /**/ }) => {
Hyprland.message_async(message, (_, asyncResult) => {
const result = Hyprland.message_finish(asyncResult);
export const hyprMessage = (message: string) => new Promise<string>((
resolution = () => { /**/ },
rejection = () => { /**/ },
) => {
try {
Hyprland.message_async(message, (_, asyncResult) => {
const result = Hyprland.message_finish(asyncResult);
resolution(result);
});
resolution(result);
});
}
catch (e) {
rejection(e);
}
});

View file

@ -0,0 +1,185 @@
import { subprocess } from 'astal';
import { App } from 'astal/gtk3';
import GObject, { register, signal } from 'astal/gobject';
import AstalIO from 'gi://AstalIO?version=0.1';
import { hyprMessage } from '../lib';
const ON_RELEASE_TRIGGERS = [
'released',
'TOUCH_UP',
'HOLD_END',
];
const ON_CLICK_TRIGGERS = [
'pressed',
'TOUCH_DOWN',
];
/* Types */
import { PopupWindow } from '../widgets/misc/popup-window';
import { CursorPos, Layer, LayerResult } from '../lib';
@register()
export default class MonitorClicks extends GObject.Object {
@signal(Boolean)
declare procStarted: (state: boolean) => void;
@signal(Boolean)
declare procDestroyed: (state: boolean) => void;
@signal(String)
declare released: (procLine: string) => void;
@signal(String)
declare clicked: (procLine: string) => void;
private process = null as AstalIO.Process | null;
constructor() {
super();
this.#initAppConnection();
}
startProc() {
if (this.process) {
return;
}
this.process = subprocess(
['libinput', 'debug-events'],
(output) => {
if (output.includes('cancelled')) {
return;
}
if (ON_RELEASE_TRIGGERS.some((p) => output.includes(p))) {
MonitorClicks.detectClickedOutside('released');
this.emit('released', output);
}
if (ON_CLICK_TRIGGERS.some((p) => output.includes(p))) {
MonitorClicks.detectClickedOutside('clicked');
this.emit('clicked', output);
}
},
);
this.emit('proc-started', true);
}
killProc() {
if (this.process) {
this.process.kill();
this.process = null;
this.emit('proc-destroyed', true);
}
}
#initAppConnection() {
App.connect('window-toggled', () => {
const anyVisibleAndClosable =
(App.get_windows() as PopupWindow[]).some((w) => {
const closable = w.close_on_unfocus &&
!(
w.close_on_unfocus === 'none' ||
w.close_on_unfocus === 'stay'
);
return w.visible && closable;
});
if (anyVisibleAndClosable) {
this.startProc();
}
else {
this.killProc();
}
});
}
static async detectClickedOutside(clickStage: string) {
const toClose = ((App.get_windows() as PopupWindow[])).some((w) => {
const closable = (
w.close_on_unfocus &&
w.close_on_unfocus === clickStage
);
return w.visible && closable;
});
if (!toClose) {
return;
}
const layers = JSON.parse(await hyprMessage('j/layers')) as LayerResult;
const pos = JSON.parse(await hyprMessage('j/cursorpos')) as CursorPos;
Object.values(layers).forEach((key) => {
const overlayLayer = key.levels['3'];
if (overlayLayer) {
const noCloseWidgetsNames = [
'bar-',
'osk',
];
const getNoCloseWidgets = (names: string[]) => {
const arr = [] as Layer[];
names.forEach((name) => {
arr.push(
overlayLayer.find(
(n) => n.namespace.startsWith(name),
) ||
// Return an empty Layer if widget doesn't exist
{
address: '',
x: 0,
y: 0,
w: 0,
h: 0,
namespace: '',
},
);
});
return arr;
};
const clickIsOnWidget = (w: Layer) => {
return pos.x > w.x && pos.x < w.x + w.w &&
pos.y > w.y && pos.y < w.y + w.h;
};
const noCloseWidgets = getNoCloseWidgets(noCloseWidgetsNames);
const widgets = overlayLayer.filter((n) => {
let window = null as null | PopupWindow;
if (App.get_windows().some((win) =>
win.name === n.namespace)) {
window = (App.get_window(n.namespace) as PopupWindow);
}
return window &&
window.close_on_unfocus &&
window.close_on_unfocus ===
clickStage;
});
if (noCloseWidgets.some(clickIsOnWidget)) {
// Don't handle clicks when on certain widgets
}
else {
widgets.forEach((w) => {
if (!(pos.x > w.x && pos.x < w.x + w.w &&
pos.y > w.y && pos.y < w.y + w.h)) {
App.get_window(w.namespace)?.set_visible(false);
}
});
}
}
});
}
}

View file

@ -1,4 +1,4 @@
import { Astal, Widget } from 'astal/gtk3';
import { App, Astal, Widget } from 'astal/gtk3';
import { register, property } from 'astal/gobject';
import { Binding, idle } from 'astal';
@ -19,7 +19,7 @@ type PopupWindowProps = Widget.WindowProps & {
@register()
class PopupWindow extends Widget.Window {
export class PopupWindow extends Widget.Window {
@property(String)
declare transition: HyprTransition | Binding<HyprTransition>;
@ -33,8 +33,8 @@ class PopupWindow extends Widget.Window {
declare on_close: PopupCallback;
constructor({
transition = 'fade',
close_on_unfocus = 'none',
transition = 'slide top',
close_on_unfocus = 'released',
on_open = () => { /**/ },
on_close = () => { /**/ },
@ -45,7 +45,7 @@ class PopupWindow extends Widget.Window {
}: PopupWindowProps) {
super({
...rest,
name,
name: `win-${name}`,
namespace: `win-${name}`,
visible: false,
layer,
@ -57,6 +57,8 @@ class PopupWindow extends Widget.Window {
}),
});
App.add_window(this);
const setTransition = (_: PopupWindow, t: HyprTransition | Binding<HyprTransition>) => {
hyprMessage(`keyword layerrule animation ${t}, ${this.name}`);
};

View file

@ -13,28 +13,7 @@ import { HasNotifs } from './notification';
import { get_hyprland_monitor } from '../../lib';
/* Types */
interface Layer {
address: string
x: number
y: number
w: number
h: number
namespace: string
}
interface Levels {
0?: Layer[] | null
1?: Layer[] | null
2?: Layer[] | null
3?: Layer[] | null
}
interface Layers {
levels: Levels
}
type LayerResult = Record<string, Layers>;
interface CursorPos {
x: number
y: number
}
import { CursorPos, LayerResult } from '../../lib';
const display = Gdk.Display.get_default();