import ctypes import ctypes.util import os import xml.etree.ElementTree as ET class XkbHelper: def __init__(self): self.lib = None self.ctx = None self.keymap = None self.state = None path = ctypes.util.find_library("xkbcommon") if not path: for p in [ "/usr/lib/libxkbcommon.so.0", "/usr/lib64/libxkbcommon.so.0", "/lib/x86_64-linux-gnu/libxkbcommon.so.0", "/usr/lib/x86_64-linux-gnu/libxkbcommon.so.0", "/usr/lib/libxkbcommon.so", "/usr/lib64/libxkbcommon.so", "/lib/libxkbcommon.so.0", "/run/current-system/sw/lib/libxkbcommon.so.0", ]: if os.path.exists(p): path = p break if not path: return try: self.lib = ctypes.CDLL(path) # Prototypes self.lib.xkb_context_new.restype = ctypes.c_void_p self.lib.xkb_keymap_new_from_names.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] self.lib.xkb_keymap_new_from_names.restype = ctypes.c_void_p self.lib.xkb_state_new.argtypes = [ctypes.c_void_p] self.lib.xkb_state_new.restype = ctypes.c_void_p self.lib.xkb_state_key_get_utf8.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_size_t] self.lib.xkb_state_key_get_utf8.restype = ctypes.c_int self.lib.xkb_state_key_get_one_sym.argtypes = [ctypes.c_void_p, ctypes.c_uint32] self.lib.xkb_state_key_get_one_sym.restype = ctypes.c_uint32 self.lib.xkb_keysym_get_name.argtypes = [ctypes.c_uint32, ctypes.c_char_p, ctypes.c_size_t] self.lib.xkb_keysym_get_name.restype = ctypes.c_int self.lib.xkb_keymap_unref.argtypes = [ctypes.c_void_p] self.lib.xkb_keymap_unref.restype = None self.lib.xkb_state_unref.argtypes = [ctypes.c_void_p] self.lib.xkb_state_unref.restype = None self.ctx = self.lib.xkb_context_new(0) except Exception: self.lib = None class XkbRuleNames(ctypes.Structure): _fields_ = [ ("rules", ctypes.c_char_p), ("model", ctypes.c_char_p), ("layout", ctypes.c_char_p), ("variant", ctypes.c_char_p), ("options", ctypes.c_char_p), ] def set_layout(self, layout_id: str): if not self.lib or not self.ctx: return parts = layout_id.split(":", 1) layout_name = parts[0] variant_name = parts[1] if len(parts) > 1 else "" self._layout_bytes = layout_name.encode() self._variant_bytes = variant_name.encode() if variant_name else None names = self.XkbRuleNames(None, None, self._layout_bytes, self._variant_bytes, None) if self.state: self.lib.xkb_state_unref(self.state) self.state = None if self.keymap: self.lib.xkb_keymap_unref(self.keymap) self.keymap = None self.keymap = self.lib.xkb_keymap_new_from_names(self.ctx, ctypes.byref(names), 0) if self.keymap: self.state = self.lib.xkb_state_new(self.keymap) def get_label(self, keycode: int) -> str | None: if not self.state: return None xkb_keycode = keycode + 8 buf = ctypes.create_string_buffer(32) res = self.lib.xkb_state_key_get_utf8(self.state, xkb_keycode, buf, 32) if res > 0: return buf.value.decode('utf-8') return None def get_keysym_name(self, keycode: int) -> str | None: if not self.state: return None xkb_keycode = keycode + 8 sym = self.lib.xkb_state_key_get_one_sym(self.state, xkb_keycode) if sym == 0: return None buf = ctypes.create_string_buffer(64) res = self.lib.xkb_keysym_get_name(sym, buf, 64) if res >= 0: return buf.value.decode('utf-8') return None @staticmethod def get_available_layouts() -> list[tuple[str, str]]: paths = [ "/usr/share/X11/xkb/rules/evdev.xml", "/usr/share/X11/xkb/rules/base.xml", "/usr/share/xkb/rules/evdev.xml", "/usr/share/xkb/rules/base.xml", "/run/current-system/sw/share/X11/xkb/rules/evdev.xml", ] layouts = [] for p in paths: if os.path.exists(p): try: tree = ET.parse(p) root = tree.getroot() for layout in root.findall(".//layout"): config = layout.find("configItem") if config is not None: name = config.findtext("name") desc = config.findtext("description") if name and desc: layouts.append((name, desc)) variant_list = layout.find("variantList") if variant_list is not None: for variant in variant_list.findall("variant"): v_config = variant.find("configItem") if v_config is not None: v_name = v_config.findtext("name") v_desc = v_config.findtext("description") if name and v_name and v_desc: layouts.append((f"{name}:{v_name}", v_desc)) if layouts: # Sort by description layouts.sort(key=lambda x: x[1]) return layouts except Exception: continue return [("us", "English (US)"), ("us:dvorak", "English (Dvorak)"), ("it", "Italian"), ("fr", "French"), ("de", "German"), ("es", "Spanish")]