From 7aa73be09d9a857fc18ed591f4c02d2d39cdbbf8 Mon Sep 17 00:00:00 2001 From: Stephen Early Date: Sat, 6 Jul 2024 00:33:12 +0100 Subject: [PATCH] Implement compose table iterator Added in libxkbcommon-1.6.0 --- tests/test_xkb.py | 20 +++++++++++-- xkbcommon/ffi_build.py | 25 +++++++++++++++- xkbcommon/xkb.py | 68 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/tests/test_xkb.py b/tests/test_xkb.py index 3a8294d..0df7d02 100644 --- a/tests/test_xkb.py +++ b/tests/test_xkb.py @@ -598,9 +598,25 @@ class TestCompose(TestCase): @classmethod def setUpClass(cls): ctx = xkb.Context() - ct = ctx.compose_table_new_from_buffer( + cls.ct = ctx.compose_table_new_from_buffer( sample_compose_bytes, "en_US.UTF-8") - cls.compose = ct.compose_state_new() + cls.compose = cls.ct.compose_state_new() + + def test_compose_table_iterator(self): + # Our sample compose table has 5125 entries + self.assertEqual(len(list(self.ct)), 5125) + + # Check that we can find the compose sequence DEAD_TILDE, + # DEAD_HORN, O and its results + seq = (self.XKB_KEYSYM_DEAD_TILDE, self.XKB_KEYSYM_DEAD_HORN, + self.XKB_KEYSYM_O) + for te in self.ct: + if te.sequence == seq: + self.assertEqual(te.keysym, self.XKB_KEYSYM_OHORNTILDE) + self.assertEqual(te.utf8, self.UTF8_OHORNTILDE) + break + else: + self.fail("Did not find test sequence in compose table") def test_compose_initial_status(self): self.compose.reset() diff --git a/xkbcommon/ffi_build.py b/xkbcommon/ffi_build.py index 3acf507..6b5c49f 100644 --- a/xkbcommon/ffi_build.py +++ b/xkbcommon/ffi_build.py @@ -1,7 +1,7 @@ from cffi import FFI ffibuilder = FFI() -# Currently implemented with reference to libxkbcommon-1.5.0 +# Currently implemented with reference to libxkbcommon-1.6.0 ffibuilder.set_source("xkbcommon._ffi", """ #include @@ -443,6 +443,29 @@ void xkb_compose_table_unref(struct xkb_compose_table *table); +struct xkb_compose_table_entry; + +const xkb_keysym_t * +xkb_compose_table_entry_sequence(struct xkb_compose_table_entry *entry, + size_t *sequence_length); + +xkb_keysym_t +xkb_compose_table_entry_keysym(struct xkb_compose_table_entry *entry); + +const char * +xkb_compose_table_entry_utf8(struct xkb_compose_table_entry *entry); + +struct xkb_compose_table_iterator; + +struct xkb_compose_table_iterator * +xkb_compose_table_iterator_new(struct xkb_compose_table *table); + +void +xkb_compose_table_iterator_free(struct xkb_compose_table_iterator *iter); + +struct xkb_compose_table_entry * +xkb_compose_table_iterator_next(struct xkb_compose_table_iterator *iter); + enum xkb_compose_state_flags { XKB_COMPOSE_STATE_NO_FLAGS = ... }; diff --git a/xkbcommon/xkb.py b/xkbcommon/xkb.py index afa7362..e044910 100644 --- a/xkbcommon/xkb.py +++ b/xkbcommon/xkb.py @@ -1,6 +1,8 @@ import enum import mmap import sys +from dataclasses import dataclass +from typing import Tuple from xkbcommon._ffi import ffi, lib @@ -115,6 +117,11 @@ class XKBComposeTableCreationFailure(XKBError): pass +class XKBComposeTableIteratorCreationFailure(XKBError): + """Unable to create a compose table iterator.""" + pass + + class XKBComposeStateCreationFailure(XKBError): """Unable to create a compose state.""" pass @@ -1266,8 +1273,29 @@ def __init__(self, context, pointer, load_method): self._table = ffi.gc( pointer, _keepref(lib, lib.xkb_compose_table_unref)) - # Methods to access and iterate over the compose table will be - # added for release 1.6 + def __iter__(self): + local_lib = lib + iterator = local_lib.xkb_compose_table_iterator_new(self._table) + if not iterator: + raise XKBComposeTableIteratorCreationFailure() + # This allocates a single size_t + sequence_length_ptr = ffi.new("size_t *") + try: + while True: + entry = local_lib.xkb_compose_table_iterator_next(iterator) + if entry == ffi.NULL: + return + sequence = local_lib.xkb_compose_table_entry_sequence( + entry, sequence_length_ptr) + keysym = local_lib.xkb_compose_table_entry_keysym(entry) + utf8 = local_lib.xkb_compose_table_entry_utf8(entry) + yield ComposeTableEntry( + sequence=tuple(sequence[0:sequence_length_ptr[0]]), + keysym=keysym, + string=ffi.string(utf8).decode('utf8')) + finally: + del sequence_length_ptr + local_lib.xkb_compose_table_iterator_free(iterator) def compose_state_new(self, flags=None): pointer = lib.xkb_compose_state_new(self._table, flags if flags else 0) @@ -1277,6 +1305,29 @@ def compose_state_new(self, flags=None): return ComposeState(self, pointer) +@dataclass +class ComposeTableEntry: + """A Compose table entry + + Enables access to the left-hand keysym sequence, right-hand result + keysym and right-hand result string of a compose table entry. + + Do not instantiate this object directly. Instead, obtain a compose + table iterator by calling iter() on a ComposeTable; the iterator + will yield ComposeTableEntry instances + """ + sequence: Tuple[int] + keysym: int + string: str + + # Someone reading the libxkbcommon documentation may expect the + # right hand result string to be called "utf8". This is just an + # alias. + @property + def utf8(self): + return self.string + + class ComposeState: """A Compose state object. @@ -1366,6 +1417,19 @@ def get_utf8(self): lib.xkb_compose_state_get_utf8(self._state, buffer, buffer_size) return ffi.string(buffer).decode("utf8") + # This is an alias for get_utf8() — the name is more logical but + # may be unexpected to somebody just reading the libxkbcommon + # documentation + def get_string(self): + """Get the result string for a compose sequence. + + This function is only useful when the status is + ComposeStatus.XKB_COMPOSE_COMPOSED. + + Returns string for composed sequence or empty string if not viable. + """ + return self.get_utf8() + def get_one_sym(self): """Get the result keysym for a composed sequence.