feat(agsV2): add MonitorClicks service
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
e47f6ea0d7
commit
e3ca8dc85e
5 changed files with 234 additions and 32 deletions
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
185
nixosModules/ags/v2/services/monitor-clicks.ts
Normal file
185
nixosModules/ags/v2/services/monitor-clicks.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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}`);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue