keyd-application-mapper: Add proper wayland support

Replace hacky Sway and Hypr application detection logic with a generic
wayland implementation that works for wlroots based compositors.
This also provides the scaffolding for adding Gnome/KDE support
in the future should they decide to implement the requisite
protocols.
master
Raheman Vaiya 3 years ago
parent a8469b2dba
commit a21386595e
  1. 135
      scripts/keyd-application-mapper

@ -3,6 +3,8 @@
import subprocess import subprocess
import argparse import argparse
import select import select
import socket
import struct
import os import os
import shutil import shutil
import re import re
@ -101,99 +103,87 @@ def new_interruptible_generator(monfd, genfn):
if monfd in r: if monfd in r:
yield genfn() yield genfn()
class SwayMonitor():
def __init__(self, on_window_change):
assert_env('SWAYSOCK')
self.on_window_change = on_window_change # Just enough wayland wire protocol to listen for interesting events.
#
# Sadly most of the useful protocols haven't been standardized,
# so this only works for wlroots based compositors :(.
def init(self): class Wayland():
pass def __init__(self, interface_name):
path = os.getenv("WAYLAND_DISPLAY")
if path == None:
raise Exception("WAYLAND_DISPLAY not set (is wayland running?)")
def run(self): if path[0] != '/':
import json path = os.getenv("XDG_RUNTIME_DIR") + "/" + path
import subprocess
last_cls = '' self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
last_title = '' self.sock.connect(path)
self._bind_interface(interface_name)
swayproc = subprocess.Popen( def send_msg(self, object_id, opcode, payload):
['swaymsg', opcode |= (len(payload)+8) << 16
'--type',
'subscribe',
'--monitor',
'--raw',
'["window"]'], stdout=subprocess.PIPE)
for line in new_interruptible_generator(swayproc.stdout.fileno(), swayproc.stdout.readline): self.sock.sendall(struct.pack(b'II', object_id, opcode))
if line == None: self.sock.sendall(payload)
self.on_window_change(last_cls, last_title)
continue
data = json.loads(line) def recv_msg(self):
(object_id, evcode) = struct.unpack('II', self.sock.recv(8))
title = '' size = evcode >> 16
cls = '' evcode = evcode & 0xFFFF
try: data = self.sock.recv(size-8)
if data['container']['focused'] == True:
props = data['container']['window_properties']
cls = props['class'] return (object_id, evcode, data)
title = props['title']
except:
try:
title = ''
cls = data['container']['app_id']
except:
pass
if title == '' and cls == '': def read_string(self, b):
continue return b[4:4+struct.unpack('I', b[:4])[0]-1].decode('utf8')
# Will bind the requested interface to object id 4
def _bind_interface(self, name):
# bind the registry object to id 2
self.send_msg(1, 1, b'\x02\x00\x00\x00')
# solicit a sync message to bookend the registry events
self.send_msg(1, 0, b'\x03\x00\x00\x00')
while True:
(obj, event, payload) = self.recv_msg()
if obj == 2 and event == 0: # registry.global event
wl_name = struct.unpack('I', payload[0:4])[0]
if last_cls != cls or last_title != title: wl_interface = self.read_string(payload[4:])
last_cls = cls
last_title = title
self.on_window_change(cls, title) if wl_interface == name:
self.send_msg(2, 0, payload+b'\x04\x00\x00\x00')
return
class HyprMonitor(): if obj == 3: # sync message
def __init__(self, on_window_change): raise Exception(f"Could not find interface {name}")
assert_env('HYPRLAND_INSTANCE_SIGNATURE')
class Wlroots():
def __init__(self, on_window_change):
self.wl = Wayland('zwlr_foreign_toplevel_manager_v1')
self.on_window_change = on_window_change self.on_window_change = on_window_change
def init(self): def init(self):
pass pass
def run(self): def run(self):
import socket windows = {}
last_cls = ''
last_title = ''
s = "/tmp/hypr/" + str(os.getenv('HYPRLAND_INSTANCE_SIGNATURE')) + "/.socket2.sock"
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.connect(s)
while True: while True:
data = client.recv(4096) (obj, event, payload) = self.wl.recv_msg()
msg = data.decode('utf-8') if obj == 4 and event == 0:
tmp = msg.split('>>') windows[struct.unpack('I', payload)[0]] = {}
if tmp[0] == 'activewindow' and len(tmp) > 1:
cls, title = tmp[1].split(',') if obj in windows:
if event == 0:
if title == '' and cls == '': windows[obj]['title'] = self.wl.read_string(payload)
continue if event == 1:
windows[obj]['appid'] = self.wl.read_string(payload)
if last_cls != cls or last_title != title: if event == 4 and payload[0] > 0 and payload[4] == 2:
last_cls = cls self.on_window_change(windows[obj]['appid'], windows[obj]['title'])
last_title = title
self.on_window_change(cls, title)
client.close()
class XMonitor(): class XMonitor():
def __init__(self, on_window_change): def __init__(self, on_window_change):
@ -411,8 +401,7 @@ class GnomeMonitor():
def get_monitor(on_window_change): def get_monitor(on_window_change):
monitors = [ monitors = [
('Sway', SwayMonitor), ('wlroots', Wlroots),
('Hyprland', HyprMonitor),
('Gnome', GnomeMonitor), ('Gnome', GnomeMonitor),
('X', XMonitor), ('X', XMonitor),
] ]

Loading…
Cancel
Save