From e3ca8dc85e214bb8c85d68412786b77d8732464c Mon Sep 17 00:00:00 2001 From: matt1432 Date: Wed, 16 Oct 2024 21:44:45 -0400 Subject: [PATCH] feat(agsV2): add MonitorClicks service --- nixosModules/ags/v2/app.ts | 4 + nixosModules/ags/v2/lib.ts | 42 +++- .../ags/v2/services/monitor-clicks.ts | 185 ++++++++++++++++++ .../ags/v2/widgets/misc/popup-window.tsx | 12 +- .../ags/v2/widgets/notifs/gesture.tsx | 23 +-- 5 files changed, 234 insertions(+), 32 deletions(-) create mode 100644 nixosModules/ags/v2/services/monitor-clicks.ts diff --git a/nixosModules/ags/v2/app.ts b/nixosModules/ags/v2/app.ts index f35a47d9..4ec28033 100644 --- a/nixosModules/ags/v2/app.ts +++ b/nixosModules/ags/v2/app.ts @@ -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(); }, }); diff --git a/nixosModules/ags/v2/lib.ts b/nixosModules/ags/v2/lib.ts index 927bb0b8..1aac07eb 100644 --- a/nixosModules/ags/v2/lib.ts +++ b/nixosModules/ags/v2/lib.ts @@ -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; +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((resolution = () => { /**/ }) => { - Hyprland.message_async(message, (_, asyncResult) => { - const result = Hyprland.message_finish(asyncResult); +export const hyprMessage = (message: string) => new Promise(( + resolution = () => { /**/ }, + rejection = () => { /**/ }, +) => { + try { + Hyprland.message_async(message, (_, asyncResult) => { + const result = Hyprland.message_finish(asyncResult); - resolution(result); - }); + resolution(result); + }); + } + catch (e) { + rejection(e); + } }); diff --git a/nixosModules/ags/v2/services/monitor-clicks.ts b/nixosModules/ags/v2/services/monitor-clicks.ts new file mode 100644 index 00000000..3562ffda --- /dev/null +++ b/nixosModules/ags/v2/services/monitor-clicks.ts @@ -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); + } + }); + } + } + }); + } +} diff --git a/nixosModules/ags/v2/widgets/misc/popup-window.tsx b/nixosModules/ags/v2/widgets/misc/popup-window.tsx index 25971837..81d47069 100644 --- a/nixosModules/ags/v2/widgets/misc/popup-window.tsx +++ b/nixosModules/ags/v2/widgets/misc/popup-window.tsx @@ -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; @@ -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) => { hyprMessage(`keyword layerrule animation ${t}, ${this.name}`); }; diff --git a/nixosModules/ags/v2/widgets/notifs/gesture.tsx b/nixosModules/ags/v2/widgets/notifs/gesture.tsx index 49a4dfb2..5f37dc79 100644 --- a/nixosModules/ags/v2/widgets/notifs/gesture.tsx +++ b/nixosModules/ags/v2/widgets/notifs/gesture.tsx @@ -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; -interface CursorPos { - x: number - y: number -} +import { CursorPos, LayerResult } from '../../lib'; const display = Gdk.Display.get_default();