From a8764181e51b1b3711954ba79c68da9c5eae82fe Mon Sep 17 00:00:00 2001 From: mats cronqvist Date: Sat, 16 May 2026 17:20:00 +0200 Subject: [PATCH 1/3] scripts: fix Wayland socket reliability and message buffering sock.recv() is not guaranteed to return all requested bytes in one call; replace bare recv() calls with a recvall() loop throughout. Split recv_msg() into _recv_raw() (direct socket read) and recv_msg() (drains a _pending buffer first). _bind_interfaces() now uses _recv_raw() and buffers any non-registry, non-sync messages into _pending so they are not silently discarded before run() starts. This is necessary for compositors that send initial toplevel state during the bind phase. Also track the next client-side object ID (_next_id / alloc_id()) for use by monitor classes that need to create server-side objects. --- scripts/keyd-application-mapper | 36 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/scripts/keyd-application-mapper b/scripts/keyd-application-mapper index 91a365b..47359bd 100755 --- a/scripts/keyd-application-mapper +++ b/scripts/keyd-application-mapper @@ -207,6 +207,7 @@ class Wayland(): self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(path) + self._pending = [] self._bind_interfaces(interfaces) def send_msg(self, object_id, opcode, payload): @@ -215,16 +216,27 @@ class Wayland(): self.sock.sendall(struct.pack(b'II', object_id, opcode)) self.sock.sendall(payload) - def recv_msg(self): - (object_id, evcode) = struct.unpack('II', self.sock.recv(8)) - + def recvall(self, n): + data = b'' + while len(data) < n: + chunk = self.sock.recv(n - len(data)) + if not chunk: + raise ConnectionError('wayland socket closed') + data += chunk + return data + + def _recv_raw(self): + (object_id, evcode) = struct.unpack('II', self.recvall(8)) size = evcode >> 16 evcode = evcode & 0xFFFF - - data = self.sock.recv(size-8) - + data = self.recvall(size-8) return (object_id, evcode, data) + def recv_msg(self): + if self._pending: + return self._pending.pop(0) + return self._recv_raw() + def read_string(self, b): return b[4:4+struct.unpack('I', b[:4])[0]-1].decode('utf8') @@ -237,7 +249,7 @@ class Wayland(): interface_object_number = 4 while True: - (obj, event, payload) = self.recv_msg() + (obj, event, payload) = self._recv_raw() if obj == 2 and event == 0: # registry.global event wl_name = struct.unpack('I', payload[0:4])[0] wl_interface = self.read_string(payload[4:]) @@ -249,11 +261,19 @@ class Wayland(): setattr(self, interface, interface_object_number) interface_object_number += 1 - if obj == 3: # sync message + elif obj == 3: # sync message for interface in interfaces: if not hasattr(self, interface): raise Exception(f"Could not find interface {interface}") + self._next_id = interface_object_number return + else: + self._pending.append((obj, event, payload)) + + def alloc_id(self): + obj_id = self._next_id + self._next_id += 1 + return obj_id class Wlroots(): From 9a85b48ded645f6d7bb4e8ef75e839ed1e39d959 Mon Sep 17 00:00:00 2001 From: mats cronqvist Date: Sat, 16 May 2026 17:22:04 +0200 Subject: [PATCH 2/3] scripts: fix application mapper for modern COSMIC DE (fixes #1147) COSMIC DE changed its Wayland protocol architecture: zcosmic_toplevel_info_v1 no longer acts as a standalone toplevel manager. It now works as a supplementary interface alongside the standardised ext_foreign_toplevel_list_v1. Update the Cosmic class to: - Bind both ext_foreign_toplevel_list_v1 and zcosmic_toplevel_info_v1 - On each new ext_foreign_toplevel_handle_v1, call get_cosmic_toplevel() to obtain a zcosmic_toplevel_handle_v1 for activation state - Read title and app_id from ext_foreign_toplevel_handle_v1 (events 2/3) - Detect window activation from zcosmic_toplevel_handle_v1 state (event 8) --- scripts/keyd-application-mapper | 50 +++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/scripts/keyd-application-mapper b/scripts/keyd-application-mapper index 47359bd..380f17f 100755 --- a/scripts/keyd-application-mapper +++ b/scripts/keyd-application-mapper @@ -333,25 +333,57 @@ class Wlroots(): class Cosmic(): def __init__(self, on_window_change): - self.wl = Wayland('zcosmic_toplevel_info_v1') + self.wl = Wayland('ext_foreign_toplevel_list_v1', 'zcosmic_toplevel_info_v1') self.on_window_change = on_window_change def init(self): pass def run(self): + ext_list_id = self.wl.ext_foreign_toplevel_list_v1 + cosmic_info_id = self.wl.zcosmic_toplevel_info_v1 + # ext_handle_id -> cosmic_handle_id + ext_to_cosmic = {} + # cosmic_handle_id -> {title, appid, active} windows = {} + while True: (obj, event, payload) = self.wl.recv_msg() + + if obj == ext_list_id: + if event == 0: # ext_foreign_toplevel_list_v1::toplevel — new ext handle + ext_handle_id = struct.unpack('I', payload)[0] + cosmic_handle_id = self.wl.alloc_id() + self.wl.send_msg(cosmic_info_id, 1, struct.pack('II', cosmic_handle_id, ext_handle_id)) + windows[cosmic_handle_id] = {'title': '', 'appid': '', 'active': False} + ext_to_cosmic[ext_handle_id] = cosmic_handle_id + continue + + # ext_foreign_toplevel_handle_v1 events — title and appid live here + if obj in ext_to_cosmic: + w = windows[ext_to_cosmic[obj]] + if event == 2: # title + w['title'] = self.wl.read_string(payload) + elif event == 3: # app_id + w['appid'] = self.wl.read_string(payload) + elif event == 0: # closed + cosmic_id = ext_to_cosmic.pop(obj) + windows.pop(cosmic_id, None) + continue + + # zcosmic_toplevel_handle_v1 events — state (activation) lives here if obj not in windows: - windows[obj]={} - - if event == 2: - windows[obj]['title'] = self.wl.read_string(payload) - if event == 3: - windows[obj]['appid'] = self.wl.read_string(payload) - if event == 8 and payload[0] > 0 and payload[4] == 2: - self.on_window_change(windows[obj].get('appid', ''), windows[obj].get('title', '')) + continue + + if event == 8 and len(payload) >= 4: + array_size = struct.unpack('I', payload[0:4])[0] + activated = any( + struct.unpack('I', payload[4+i:8+i])[0] == 2 + for i in range(0, array_size, 4) + ) + windows[obj]['active'] = activated + if activated: + self.on_window_change(windows[obj]['appid'], windows[obj]['title']) class XMonitor(): def __init__(self, on_window_change): From 09860ed1fb5584b20b768f201ca139e0fefbe7ea Mon Sep 17 00:00:00 2001 From: mats cronqvist Date: Sat, 16 May 2026 17:23:30 +0200 Subject: [PATCH 3/3] scripts: support KEYD_APP_CONF env var to override the config file path. --- scripts/keyd-application-mapper | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/keyd-application-mapper b/scripts/keyd-application-mapper index 380f17f..651da77 100755 --- a/scripts/keyd-application-mapper +++ b/scripts/keyd-application-mapper @@ -23,7 +23,7 @@ from fnmatch import fnmatch # Consider reimplmenting in perl or C. # Produce more useful error messages :P. -CONFIG_PATH = os.getenv('HOME')+'/.config/keyd/app.conf' +CONFIG_PATH = os.getenv('KEYD_APP_CONF', os.getenv('HOME')+'/.config/keyd/app.conf') LOCKFILE = os.getenv('HOME')+'/.config/keyd/app.lock' LOGFILE = os.getenv('HOME')+'/.config/keyd/app.log'