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 Corners from './widgets/corners/main';
|
||||||
import { NotifPopups } from './widgets/notifs/main';
|
import { NotifPopups } from './widgets/notifs/main';
|
||||||
|
|
||||||
|
import MonitorClicks from './services/monitor-clicks';
|
||||||
|
|
||||||
|
|
||||||
App.start({
|
App.start({
|
||||||
css: style,
|
css: style,
|
||||||
|
@ -16,5 +18,7 @@ App.start({
|
||||||
BgFade();
|
BgFade();
|
||||||
Corners();
|
Corners();
|
||||||
NotifPopups();
|
NotifPopups();
|
||||||
|
|
||||||
|
new MonitorClicks();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,30 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||||
|
|
||||||
const Hyprland = AstalHyprland.get_default();
|
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 => {
|
export const get_hyprland_monitor = (monitor: Gdk.Monitor): AstalHyprland.Monitor | undefined => {
|
||||||
const manufacturer = monitor.manufacturer?.replace(',', '');
|
const manufacturer = monitor.manufacturer?.replace(',', '');
|
||||||
|
@ -38,10 +62,18 @@ export const get_monitor_desc = (mon: AstalHyprland.Monitor): string => {
|
||||||
return `desc:${mon.description}`;
|
return `desc:${mon.description}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hyprMessage = (message: string) => new Promise<string>((resolution = () => { /**/ }) => {
|
export const hyprMessage = (message: string) => new Promise<string>((
|
||||||
Hyprland.message_async(message, (_, asyncResult) => {
|
resolution = () => { /**/ },
|
||||||
const result = Hyprland.message_finish(asyncResult);
|
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 { register, property } from 'astal/gobject';
|
||||||
import { Binding, idle } from 'astal';
|
import { Binding, idle } from 'astal';
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ type PopupWindowProps = Widget.WindowProps & {
|
||||||
|
|
||||||
|
|
||||||
@register()
|
@register()
|
||||||
class PopupWindow extends Widget.Window {
|
export class PopupWindow extends Widget.Window {
|
||||||
@property(String)
|
@property(String)
|
||||||
declare transition: HyprTransition | Binding<HyprTransition>;
|
declare transition: HyprTransition | Binding<HyprTransition>;
|
||||||
|
|
||||||
|
@ -33,8 +33,8 @@ class PopupWindow extends Widget.Window {
|
||||||
declare on_close: PopupCallback;
|
declare on_close: PopupCallback;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
transition = 'fade',
|
transition = 'slide top',
|
||||||
close_on_unfocus = 'none',
|
close_on_unfocus = 'released',
|
||||||
on_open = () => { /**/ },
|
on_open = () => { /**/ },
|
||||||
on_close = () => { /**/ },
|
on_close = () => { /**/ },
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ class PopupWindow extends Widget.Window {
|
||||||
}: PopupWindowProps) {
|
}: PopupWindowProps) {
|
||||||
super({
|
super({
|
||||||
...rest,
|
...rest,
|
||||||
name,
|
name: `win-${name}`,
|
||||||
namespace: `win-${name}`,
|
namespace: `win-${name}`,
|
||||||
visible: false,
|
visible: false,
|
||||||
layer,
|
layer,
|
||||||
|
@ -57,6 +57,8 @@ class PopupWindow extends Widget.Window {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
App.add_window(this);
|
||||||
|
|
||||||
const setTransition = (_: PopupWindow, t: HyprTransition | Binding<HyprTransition>) => {
|
const setTransition = (_: PopupWindow, t: HyprTransition | Binding<HyprTransition>) => {
|
||||||
hyprMessage(`keyword layerrule animation ${t}, ${this.name}`);
|
hyprMessage(`keyword layerrule animation ${t}, ${this.name}`);
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,28 +13,7 @@ import { HasNotifs } from './notification';
|
||||||
import { get_hyprland_monitor } from '../../lib';
|
import { get_hyprland_monitor } from '../../lib';
|
||||||
|
|
||||||
/* Types */
|
/* Types */
|
||||||
interface Layer {
|
import { CursorPos, LayerResult } from '../../lib';
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const display = Gdk.Display.get_default();
|
const display = Gdk.Display.get_default();
|
||||||
|
|
Loading…
Reference in a new issue