#!/usr/bin/env python3 """ft-textinput: Frametop's input method, which tells the input relay when a text field on the desktop has keyboard focus. KWin starts it (--inputmethod, from session/frametop-session.sh) and activates it whenever the focused app enables text input on a field (text-input v1/v2/v3: Qt, GTK, and Firefox apps do; Xwayland apps and most Electron apps don't). This only passes that on: "textfield 1" or "textfield 0" to the input relay (@frametop_relay), which opens or closes Frametop's keyboard (ft-screens' own key panel), depending on the keyboard setting in Frametop Input Settings. Typing on it reaches the app as key presses from ft-screens, not through this input method, so nothing typed passes through here. Speaks the Wayland wire protocol itself (zwp_input_method_v1 only), so it needs nothing but Python on the SteamOS host. """ import os import socket import struct import sys RELAY = "\0frametop_relay" DISPLAY, REGISTRY, INPUT_METHOD = 1, 2, 3 # our object ids def string(text): data = text.encode() + b"\0" return struct.pack("=I", len(data)) + data + b"\0" * (-len(data) % 4) def read_string(body, at): """(text, offset after it) for a string argument at `at`.""" (length,) = struct.unpack_from("=I", body, at) text = body[at + 4:at + 4 + length - 1].decode(errors="replace") return text, at + 4 + length + (-length % 4) def main(): fd = os.environ.pop("WAYLAND_SOCKET", None) if fd is None: sys.exit("ft-textinput: no WAYLAND_SOCKET (KWin starts this as its input method)") conn = socket.socket(fileno=int(fd)) relay = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_NONBLOCK) def request(obj, opcode, payload=b""): conn.sendall(struct.pack("=II", obj, (8 + len(payload)) << 16 | opcode) + payload) def report(on): try: relay.sendto(f"textfield {int(on)}".encode(), RELAY) except OSError: pass # the relay isn't running request(DISPLAY, 1, struct.pack("=I", REGISTRY)) # wl_display.get_registry contexts = set() # KWin makes one per activation buf = b"" while True: data = conn.recv(65536) if not data: return # KWin went away buf += data while len(buf) >= 8: obj, word = struct.unpack_from("=II", buf) size, opcode = word >> 16, word & 0xFFFF if size < 8 or len(buf) < size: break body, buf = buf[8:size], buf[size:] if obj == DISPLAY and opcode == 0: # error bad, code = struct.unpack_from("=II", body) message, _ = read_string(body, 8) sys.exit(f"ft-textinput: Wayland error {code} on object {bad}: {message}") elif obj == REGISTRY and opcode == 0: # global (name,) = struct.unpack_from("=I", body) interface, _ = read_string(body, 4) if interface == "zwp_input_method_v1": request(REGISTRY, 0, struct.pack("=I", name) + string(interface) + struct.pack("=II", 1, INPUT_METHOD)) elif obj == INPUT_METHOD and opcode == 0: # activate: a text field has focus contexts.add(struct.unpack_from("=I", body)[0]) report(True) elif obj == INPUT_METHOD and opcode == 1: # deactivate: it lost focus (context,) = struct.unpack_from("=I", body) if context in contexts: contexts.discard(context) request(context, 0) # zwp_input_method_context_v1.destroy report(bool(contexts)) # Everything else (the contexts' surrounding text, content type, ...) is unused. if __name__ == "__main__": try: main() except KeyboardInterrupt: pass