From fc545d3889547513aebfe0db0e2457f64d74ddcc Mon Sep 17 00:00:00 2001 From: Fabien Cazenave Date: Thu, 8 Feb 2024 16:22:14 +0100 Subject: [PATCH 1/9] ensure `kalamine create` works out of the box --- .github/workflows/tests.yml | 1 + MANIFEST.in | 2 ++ README.rst | 2 +- kalamine/cli.py | 19 ++++++++----------- kalamine/template.py | 2 +- kalamine/www/x-keyboard.js | 2 +- layouts/README.md | 3 +-- layouts/ansi.toml | 2 +- layouts/intl.toml | 2 +- layouts/prog.toml | 2 +- 10 files changed, 18 insertions(+), 19 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 014bce4..2f6b47d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,4 +52,5 @@ jobs: - name: Run tests run: | python -m kalamine.cli make layouts/*.toml + python -m kalamine.cli create test.toml pytest diff --git a/MANIFEST.in b/MANIFEST.in index 267c2a7..d00ce28 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ include LICENSE +include docs/* include kalamine/data/* include kalamine/tpl/* include kalamine/www/* +include layouts/* diff --git a/README.rst b/README.rst index 2c6eb50..95579b2 100644 --- a/README.rst +++ b/README.rst @@ -58,7 +58,7 @@ Edit this layout with your preferred text editor: - the `user guide`_ is available at the end of the ``*.toml`` file - the layout can be rendered and emulated with ``kalamine watch`` (see next section) -.. _`user guide`: https://github.com/fabi1cazenave/kalamine/tree/master/docs +.. _`user guide`: https://github.com/OneDeadKey/kalamine/tree/master/docs Build your layout: diff --git a/kalamine/cli.py b/kalamine/cli.py index 31afd78..8f37463 100644 --- a/kalamine/cli.py +++ b/kalamine/cli.py @@ -169,7 +169,7 @@ def make( variant = "custom" # layout variant id author = "nobody" # author name description = "custom QWERTY layout" -url = "https://fabi1cazenave.github.com/kalamine" +url = "https://OneDeadKey.github.com/kalamine" version = "0.0.1" geometry = """ @@ -195,16 +195,13 @@ def get_layout(name: str) -> KeyboardLayout: layout.geometry = geometry return layout - def keymap(layout_name: str, layout_layer: str, layer_name: str = "") -> str: - return """ - -{} = ''' -{} -''' -""".format( - layer_name or layout_layer, - "\n".join(getattr(get_layout(layout_name), layout_layer)), - ) + def keymap(layout_name, layout_layer, layer_name=""): + layer = "\n" + layer += f"\n{layer_name or layout_layer} = '''" + layer += "\n" + layer += "\n".join(getattr(get_layout(layout_name), layout_layer)) + layer += "\n'''" + return layer content = f'{TOML_HEADER}"{geometry.upper()}"' if odk: diff --git a/kalamine/template.py b/kalamine/template.py index 71c4348..bd82c4d 100644 --- a/kalamine/template.py +++ b/kalamine/template.py @@ -482,7 +482,7 @@ def osx_terminators(layout: "KeyboardLayout") -> List[str]: ### # Web: JSON # To be used with the web component. -# https://github.com/fabi1cazenave/x-keyboard +# https://github.com/OneDeadKey/x-keyboard # diff --git a/kalamine/www/x-keyboard.js b/kalamine/www/x-keyboard.js index 7da2e37..28349f1 100644 --- a/kalamine/www/x-keyboard.js +++ b/kalamine/www/x-keyboard.js @@ -182,7 +182,7 @@ const KEY_RADIUS = 5; // 5px border radius /** * Deak Keys - * defined in the Kalamine project: https://github.com/fabi1cazenave/kalamine + * defined in the Kalamine project: https://github.com/OneDeadKey/kalamine * identifiers -> symbols dictionary, for presentation purposes */ diff --git a/layouts/README.md b/layouts/README.md index 7e66cee..1b4d23b 100644 --- a/layouts/README.md +++ b/layouts/README.md @@ -24,5 +24,4 @@ A qwerty-intl variant with an AltGr layer for dead diacritics and coding symbols ## See Also… -- [“One Dead Key”](https://github.com/fabi1cazenave/1dk) -- [Qwerty-Lafayette](https://github.com/fabi1cazenave/qwerty-lafayette) +- [“One Dead Key”](https://github.com/OneDeadKey/1dk) diff --git a/layouts/ansi.toml b/layouts/ansi.toml index 43eeca9..b9cbb3a 100644 --- a/layouts/ansi.toml +++ b/layouts/ansi.toml @@ -3,7 +3,7 @@ name8 = "q-ansi" locale = "us" variant = "ansi" description = "standard QWERTY-US layout" -url = "http://fabi1cazenave.github.com/kalamine/" +url = "http://OneDeadKey.github.com/kalamine/" version = "1.0.0" geometry = "ANSI" diff --git a/layouts/intl.toml b/layouts/intl.toml index cc39c1b..d98b125 100644 --- a/layouts/intl.toml +++ b/layouts/intl.toml @@ -3,7 +3,7 @@ name8 = "q-intl" locale = "us" variant = "intl" description = "QWERTY layout, international variant" -url = "http://fabi1cazenave.github.com/kalamine/" +url = "http://OneDeadKey.github.com/kalamine/" version = "1.0.0" geometry = "ISO" diff --git a/layouts/prog.toml b/layouts/prog.toml index 85c0776..99cac96 100644 --- a/layouts/prog.toml +++ b/layouts/prog.toml @@ -3,7 +3,7 @@ name8 = "q-prog" variant = "prog" locale = "us" description = "QWERTY-intl layout, developer variant" -url = "http://fabi1cazenave.github.com/kalamine/" +url = "http://OneDeadKey.github.com/kalamine/" version = "0.6.0" geometry = "ANSI" From 46f8eca22d50733dbef314667496efd63480d3ee Mon Sep 17 00:00:00 2001 From: Fabien Cazenave Date: Thu, 8 Feb 2024 16:24:37 +0100 Subject: [PATCH 2/9] test (should break) --- MANIFEST.in | 2 -- 1 file changed, 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index d00ce28..267c2a7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,4 @@ include LICENSE -include docs/* include kalamine/data/* include kalamine/tpl/* include kalamine/www/* -include layouts/* From 2d566a4845bdfc30bb9277162604dd75c4ed2a56 Mon Sep 17 00:00:00 2001 From: Fabien Cazenave Date: Thu, 8 Feb 2024 16:47:02 +0100 Subject: [PATCH 3/9] version bump --- MANIFEST.in | 4 ---- pyproject.toml | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 267c2a7..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include LICENSE -include kalamine/data/* -include kalamine/tpl/* -include kalamine/www/* diff --git a/pyproject.toml b/pyproject.toml index 55dc0f3..d653dc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,9 +2,12 @@ requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.build.targets.sdist] +include = [ "/docs", "/kalamine", "/layouts" ] + [project] name = "kalamine" -version = "0.25" +version = "0.26" description = "a cross-platform Keyboard Layout Maker" readme = "README.rst" From a4b8554b86d46d258edb0d6e0398e87a748f997f Mon Sep 17 00:00:00 2001 From: Fabien Cazenave Date: Thu, 8 Feb 2024 20:43:23 +0100 Subject: [PATCH 4/9] use pkgutil to load package resources --- kalamine/cli.py | 20 +++-- kalamine/cli_xkb.py | 4 +- kalamine/layout.py | 113 +++++++++++++++++++---------- kalamine/server.py | 5 +- kalamine/template.py | 4 +- kalamine/utils.py | 7 +- tests/test_parser.py | 6 +- tests/test_serializer_ahk.py | 8 +- tests/test_serializer_keylayout.py | 8 +- tests/test_serializer_klc.py | 8 +- tests/test_serializer_xkb.py | 8 +- tests/util.py | 11 ++- 12 files changed, 125 insertions(+), 77 deletions(-) diff --git a/kalamine/cli.py b/kalamine/cli.py index 8f37463..b675f26 100644 --- a/kalamine/cli.py +++ b/kalamine/cli.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 import json +import pkgutil from contextlib import contextmanager from importlib import metadata from pathlib import Path from typing import Iterator, List, Literal, Union import click +import tomli -from .layout import KeyboardLayout +from .layout import KeyboardLayout, load_layout from .server import keyboard_server @@ -113,7 +115,7 @@ def make( """Convert TOML/YAML descriptions into OS-specific keyboard drivers.""" for input_file in layout_descriptors: - layout = KeyboardLayout(input_file, angle_mod) + layout = KeyboardLayout(load_layout(input_file), angle_mod) # default: build all in the `dist` subdirectory if out == "all": @@ -187,15 +189,19 @@ def make( @click.option("--1dk/--no-1dk", "odk", default=False, help="Set a custom dead key.") def create(output_file: Path, geometry: str, altgr: bool, odk: bool) -> None: """Create a new TOML layout description.""" - base_dir_path = Path(__file__).resolve(strict=True).parent.parent + # base_dir_path = Path(__file__).resolve(strict=True).parent.parent def get_layout(name: str) -> KeyboardLayout: """Return a layout of type NAME with constrained geometry.""" - layout = KeyboardLayout(base_dir_path / "layouts" / f"{name}.toml") + + descriptor = pkgutil.get_data(__package__, f"../layouts/{name}.toml") + layout = KeyboardLayout(tomli.loads(descriptor.decode("utf-8"))) layout.geometry = geometry return layout def keymap(layout_name, layout_layer, layer_name=""): + """Return a multiline keymap ASCII art for the specified layout.""" + layer = "\n" layer += f"\n{layer_name or layout_layer} = '''" layer += "\n" @@ -216,8 +222,10 @@ def keymap(layout_name, layout_layer, layer_name=""): content += keymap("ansi", "base") # append user guide sections - with (base_dir_path / "docs" / "README.md").open() as f: - sections = "".join(f.readlines()).split("\n\n\n") + doc = pkgutil.get_data(__package__, "../docs/README.md").decode("utf-8") + sections = doc.split("\n\n\n") + # with (base_dir_path / "docs" / "README.md").open() as f: + # sections = "".join(f.readlines()).split("\n\n\n") for topic in sections[1:]: content += "\n\n" content += "\n# " diff --git a/kalamine/cli_xkb.py b/kalamine/cli_xkb.py index 982255a..6062f34 100644 --- a/kalamine/cli_xkb.py +++ b/kalamine/cli_xkb.py @@ -8,7 +8,7 @@ import click -from .layout import KeyboardLayout +from .layout import KeyboardLayout, load_layout from .xkb_manager import WAYLAND, Index, XKBManager @@ -35,7 +35,7 @@ def apply(filepath: Path, angle_mod: bool) -> None: "You appear to be running Wayland, which does not support this operation." ) - layout = KeyboardLayout(filepath, angle_mod) + layout = KeyboardLayout(load_layout(filepath), angle_mod) with tempfile.NamedTemporaryFile( mode="w+", suffix=".xkb", encoding="utf-8" ) as temp_file: diff --git a/kalamine/layout.py b/kalamine/layout.py index cbf6ee7..837f149 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -100,13 +100,32 @@ def load_tpl(layout: "KeyboardLayout", ext: str) -> str: return out -def load_descriptor(file_path: Path) -> Dict: - if file_path.suffix in [".yaml", ".yml"]: - with file_path.open(encoding="utf-8") as file: - return yaml.load(file, Loader=yaml.SafeLoader) - - with file_path.open(mode="rb") as dfile: - return tomli.load(dfile) +def load_layout(layout_path: Path) -> Dict: + """Load the TOML/YAML layout description data (and its ancessor, if any).""" + + def load_descriptor(file_path: Path) -> Dict: + if file_path.suffix in [".yaml", ".yml"]: + with file_path.open(encoding="utf-8") as file: + return yaml.load(file, Loader=yaml.SafeLoader) + + with file_path.open(mode="rb") as dfile: + return tomli.load(dfile) + + try: + cfg = load_descriptor(layout_path) + if "name" not in cfg: + cfg["name"] = layout_path.stem + if "extends" in cfg: + parent_path = filepath.parent / cfg["extends"] + ext = load_descriptor(parent_path) + ext.update(cfg) + cfg = ext + return cfg + + except Exception as exc: + click.echo("File could not be parsed.", err=True) + click.echo(f"Error: {exc}.", err=True) + sys.exit(1) ### @@ -114,6 +133,32 @@ def load_descriptor(file_path: Path) -> Dict: # +# fmt: off +@dataclass +class MetaDescr: + name: str = "custom" + name8: str = "custom" + variant: str = "custom" + fileName: str = "custom" + locale: str = "us" + geometry: str = "ISO" + description: str = "" + author: str = "nobody" + license: str = "" + version: str = "0.0.1" + lastChange: str = datetime.date.today().isoformat() + + +@dataclass +class SpacebarDescr: + shift: str = " " + altgr: str = " " + altgt_shift: str = " " + odk: str = "'" + odk_shift: str = "'" +# fmt: on + + CONFIG = { "author": "nobody", "license": "WTFPL - Do What The Fuck You Want Public License", @@ -150,9 +195,9 @@ def from_dict(cls: Type[T], src: Dict) -> T: ) -geometry_data = load_data("geometry.yaml") - -GEOMETRY = {key: GeometryDescr.from_dict(val) for key, val in geometry_data.items()} +GEOMETRY = { + key: GeometryDescr.from_dict(val) for key, val in load_data("geometry").items() +} ### @@ -163,42 +208,32 @@ def from_dict(cls: Type[T], src: Dict) -> T: class KeyboardLayout: """Lafayette-style keyboard layout: base + 1dk + altgr layers.""" - def __init__(self, filepath: Path, angle_mod: bool = False) -> None: + # self.meta = {key: MetaDescr.from_dict(val) for key, val in geometry_data.items()} + + def __init__(self, layout_data: Dict, angle_mod: bool = False) -> None: """Import a keyboard layout to instanciate the object.""" # initialize a blank layout self.layers: Dict[Layer, Dict[str, str]] = {layer: {} for layer in Layer} self.dk_set: Set[str] = set() self.dead_keys: Dict[str, Dict[str, str]] = {} # dictionary subset of DEAD_KEYS + # self.meta = Dict[str, str] = {} # default parameters, hardcoded self.meta = CONFIG.copy() # default parameters, hardcoded self.has_altgr = False self.has_1dk = False - # load the YAML data (and its ancessor, if any) - try: - cfg = load_descriptor(filepath) - if "extends" in cfg: - path = filepath.parent / cfg["extends"] - ext = load_descriptor(path) - ext.update(cfg) - cfg = ext - except Exception as exc: - click.echo("File could not be parsed.", err=True) - click.echo(f"Error: {exc}.", err=True) - sys.exit(1) - # metadata: self.meta - for k in cfg: + for k in layout_data: if ( k != "base" and k != "full" and k != "altgr" - and not isinstance(cfg[k], dict) + and not isinstance(layout_data[k], dict) ): - self.meta[k] = cfg[k] - filename = filepath.stem - self.meta["name"] = cfg["name"] if "name" in cfg else filename - self.meta["name8"] = cfg["name8"] if "name8" in cfg else self.meta["name"][0:8] + self.meta[k] = layout_data[k] + self.meta["name8"] = ( + layout_data["name8"] if "name8" in layout_data else self.meta["name"][0:8] + ) self.meta["fileName"] = self.meta["name8"].lower() self.meta["lastChange"] = datetime.date.today().isoformat() @@ -216,24 +251,26 @@ def __init__(self, filepath: Path, angle_mod: bool = False) -> None: "Warning: geometry does not support angle-mod; ignoring the --angle-mod argument" ) - if "full" in cfg: - full = text_to_lines(cfg["full"]) + if "full" in layout_data: + full = text_to_lines(layout_data["full"]) self._parse_template(full, rows, Layer.BASE) self._parse_template(full, rows, Layer.ALTGR) self.has_altgr = True else: - base = text_to_lines(cfg["base"]) + base = text_to_lines(layout_data["base"]) self._parse_template(base, rows, Layer.BASE) self._parse_template(base, rows, Layer.ODK) - if "altgr" in cfg: + if "altgr" in layout_data: self.has_altgr = True - self._parse_template(text_to_lines(cfg["altgr"]), rows, Layer.ALTGR) + self._parse_template( + text_to_lines(layout_data["altgr"]), rows, Layer.ALTGR + ) # space bar spc = SPACEBAR.copy() - if "spacebar" in cfg: - for k in cfg["spacebar"]: - spc[k] = cfg["spacebar"][k] + if "spacebar" in layout_data: + for k in layout_data["spacebar"]: + spc[k] = layout_data["spacebar"][k] self.layers[Layer.BASE]["spce"] = " " self.layers[Layer.SHIFT]["spce"] = spc["shift"] if True or self.has_1dk: # XXX self.has_1dk is not defined yet diff --git a/kalamine/server.py b/kalamine/server.py index 253a669..6170a9f 100644 --- a/kalamine/server.py +++ b/kalamine/server.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import json -import os import threading import webbrowser from http.server import HTTPServer, SimpleHTTPRequestHandler @@ -9,11 +8,11 @@ import click from livereload import Server # type: ignore -from .layout import KeyboardLayout +from .layout import KeyboardLayout, load_layout def keyboard_server(file_path: Path) -> None: - kb_layout = KeyboardLayout(file_path) + kb_layout = KeyboardLayout(load_layout(file_path)) host_name = "localhost" webserver_port = 1664 diff --git a/kalamine/template.py b/kalamine/template.py index bd82c4d..0e7b764 100644 --- a/kalamine/template.py +++ b/kalamine/template.py @@ -16,8 +16,8 @@ # Helpers # -KEY_CODES = load_data("key_codes.yaml") -XKB_KEY_SYM = load_data("key_sym.yaml") +KEY_CODES = load_data("key_codes") +XKB_KEY_SYM = load_data("key_sym") def hex_ord(char: str) -> str: diff --git a/kalamine/utils.py b/kalamine/utils.py index 000aa1a..edfe248 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import pkgutil from dataclasses import dataclass from enum import IntEnum from pathlib import Path @@ -28,8 +29,8 @@ def text_to_lines(text: str) -> List[str]: def load_data(filename: str) -> Dict: - filepath = Path(__file__).parent / "data" / filename - return yaml.load(filepath.open(encoding="utf-8"), Loader=yaml.SafeLoader) + descriptor = pkgutil.get_data(__package__, f"data/{filename}.yaml") + return yaml.safe_load(descriptor.decode("utf-8")) class Layer(IntEnum): @@ -65,7 +66,7 @@ class DeadKeyDescr: alt_self: str -DEAD_KEYS = [DeadKeyDescr(**data) for data in load_data("dead_keys.yaml")] +DEAD_KEYS = [DeadKeyDescr(**data) for data in load_data("dead_keys")] ODK_ID = "**" # must match the value in dead_keys.yaml LAYER_KEYS = [ diff --git a/tests/test_parser.py b/tests/test_parser.py index c7022a2..1c27de5 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,10 +1,10 @@ from kalamine import KeyboardLayout -from .util import get_layout_path +from .util import get_layout_dict -def load_layout(filename, angle_mod=False): - return KeyboardLayout(get_layout_path() / (filename + ".toml"), angle_mod) +def load_layout(filename: str, angle_mod: bool = False) -> KeyboardLayout: + return KeyboardLayout(get_layout_dict(filename), angle_mod) def test_ansi(): diff --git a/tests/test_serializer_ahk.py b/tests/test_serializer_ahk.py index a4b3090..ffdd286 100644 --- a/tests/test_serializer_ahk.py +++ b/tests/test_serializer_ahk.py @@ -3,14 +3,14 @@ from kalamine import KeyboardLayout from kalamine.template import ahk_keymap, ahk_shortcuts -from .util import get_layout_path +from .util import get_layout_dict -def load_layout(filename) -> KeyboardLayout: - return KeyboardLayout(get_layout_path() / (filename + ".toml")) +def load_layout(filename: str) -> KeyboardLayout: + return KeyboardLayout(get_layout_dict(filename)) -def split(multiline_str): +def split(multiline_str: str): return dedent(multiline_str).lstrip().splitlines() diff --git a/tests/test_serializer_keylayout.py b/tests/test_serializer_keylayout.py index ffe3d78..0aecf42 100644 --- a/tests/test_serializer_keylayout.py +++ b/tests/test_serializer_keylayout.py @@ -3,14 +3,14 @@ from kalamine import KeyboardLayout from kalamine.template import osx_actions, osx_keymap, osx_terminators -from .util import get_layout_path +from .util import get_layout_dict -def load_layout(filename): - return KeyboardLayout(get_layout_path() / (filename + ".toml")) +def load_layout(filename: str) -> KeyboardLayout: + return KeyboardLayout(get_layout_dict(filename)) -def split(multiline_str): +def split(multiline_str: str): return dedent(multiline_str).lstrip().rstrip().splitlines() diff --git a/tests/test_serializer_klc.py b/tests/test_serializer_klc.py index b7423bc..c0f3adc 100644 --- a/tests/test_serializer_klc.py +++ b/tests/test_serializer_klc.py @@ -1,20 +1,18 @@ -import os -from enum import Enum from textwrap import dedent from kalamine import KeyboardLayout from kalamine.template import klc_deadkeys, klc_dk_index, klc_keymap -from .util import get_layout_path +from .util import get_layout_dict -def split(multiline_str): +def split(multiline_str: str): return dedent(multiline_str).lstrip().rstrip().splitlines() LAYOUTS = {} for filename in ["ansi", "intl", "prog"]: - LAYOUTS[filename] = KeyboardLayout(get_layout_path() / (filename + ".toml")) + LAYOUTS[filename] = KeyboardLayout(get_layout_dict(filename)) def test_ansi_keymap(): diff --git a/tests/test_serializer_xkb.py b/tests/test_serializer_xkb.py index 704a259..ec47774 100644 --- a/tests/test_serializer_xkb.py +++ b/tests/test_serializer_xkb.py @@ -3,14 +3,14 @@ from kalamine import KeyboardLayout from kalamine.template import xkb_keymap -from .util import get_layout_path +from .util import get_layout_dict -def load_layout(filename): - return KeyboardLayout(get_layout_path() / (filename + ".toml")) +def load_layout(filename: str) -> KeyboardLayout: + return KeyboardLayout(get_layout_dict(filename)) -def split(multiline_str): +def split(multiline_str: str): return dedent(multiline_str).lstrip().rstrip().splitlines() diff --git a/tests/util.py b/tests/util.py index 21671e9..da97128 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,8 +1,13 @@ """Some util functions for tests.""" -from pathlib import Path +import pkgutil +from typing import Dict +import tomli -def get_layout_path() -> Path: + +def get_layout_dict(filename: str) -> Dict: """Return the layout directory path.""" - return Path(__file__).parent.parent / "layouts" + + descriptor = pkgutil.get_data(__package__, f"../layouts/{filename}.toml") + return tomli.loads(descriptor.decode("utf-8")) From 240a9587086d1206adc602823b5d70a4276a3683 Mon Sep 17 00:00:00 2001 From: Fabien Cazenave Date: Thu, 8 Feb 2024 20:43:23 +0100 Subject: [PATCH 5/9] use pkgutil to load package resources --- kalamine/cli.py | 15 ++-- kalamine/cli_xkb.py | 4 +- kalamine/layout.py | 113 +++++++++++++++++++---------- kalamine/server.py | 5 +- kalamine/template.py | 4 +- kalamine/utils.py | 7 +- tests/test_parser.py | 6 +- tests/test_serializer_ahk.py | 8 +- tests/test_serializer_keylayout.py | 8 +- tests/test_serializer_klc.py | 8 +- tests/test_serializer_xkb.py | 8 +- tests/util.py | 11 ++- 12 files changed, 120 insertions(+), 77 deletions(-) diff --git a/kalamine/cli.py b/kalamine/cli.py index 8f37463..929565d 100644 --- a/kalamine/cli.py +++ b/kalamine/cli.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 import json +import pkgutil from contextlib import contextmanager from importlib import metadata from pathlib import Path from typing import Iterator, List, Literal, Union import click +import tomli -from .layout import KeyboardLayout +from .layout import KeyboardLayout, load_layout from .server import keyboard_server @@ -113,7 +115,7 @@ def make( """Convert TOML/YAML descriptions into OS-specific keyboard drivers.""" for input_file in layout_descriptors: - layout = KeyboardLayout(input_file, angle_mod) + layout = KeyboardLayout(load_layout(input_file), angle_mod) # default: build all in the `dist` subdirectory if out == "all": @@ -187,15 +189,16 @@ def make( @click.option("--1dk/--no-1dk", "odk", default=False, help="Set a custom dead key.") def create(output_file: Path, geometry: str, altgr: bool, odk: bool) -> None: """Create a new TOML layout description.""" - base_dir_path = Path(__file__).resolve(strict=True).parent.parent def get_layout(name: str) -> KeyboardLayout: """Return a layout of type NAME with constrained geometry.""" - layout = KeyboardLayout(base_dir_path / "layouts" / f"{name}.toml") + descriptor = pkgutil.get_data(__package__, f"../layouts/{name}.toml") + layout = KeyboardLayout(tomli.loads(descriptor.decode("utf-8"))) layout.geometry = geometry return layout def keymap(layout_name, layout_layer, layer_name=""): + """Return a multiline keymap ASCII art for the specified layout.""" layer = "\n" layer += f"\n{layer_name or layout_layer} = '''" layer += "\n" @@ -216,8 +219,8 @@ def keymap(layout_name, layout_layer, layer_name=""): content += keymap("ansi", "base") # append user guide sections - with (base_dir_path / "docs" / "README.md").open() as f: - sections = "".join(f.readlines()).split("\n\n\n") + doc = pkgutil.get_data(__package__, "../docs/README.md").decode("utf-8") + sections = doc.split("\n\n\n") for topic in sections[1:]: content += "\n\n" content += "\n# " diff --git a/kalamine/cli_xkb.py b/kalamine/cli_xkb.py index 982255a..6062f34 100644 --- a/kalamine/cli_xkb.py +++ b/kalamine/cli_xkb.py @@ -8,7 +8,7 @@ import click -from .layout import KeyboardLayout +from .layout import KeyboardLayout, load_layout from .xkb_manager import WAYLAND, Index, XKBManager @@ -35,7 +35,7 @@ def apply(filepath: Path, angle_mod: bool) -> None: "You appear to be running Wayland, which does not support this operation." ) - layout = KeyboardLayout(filepath, angle_mod) + layout = KeyboardLayout(load_layout(filepath), angle_mod) with tempfile.NamedTemporaryFile( mode="w+", suffix=".xkb", encoding="utf-8" ) as temp_file: diff --git a/kalamine/layout.py b/kalamine/layout.py index cbf6ee7..837f149 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -100,13 +100,32 @@ def load_tpl(layout: "KeyboardLayout", ext: str) -> str: return out -def load_descriptor(file_path: Path) -> Dict: - if file_path.suffix in [".yaml", ".yml"]: - with file_path.open(encoding="utf-8") as file: - return yaml.load(file, Loader=yaml.SafeLoader) - - with file_path.open(mode="rb") as dfile: - return tomli.load(dfile) +def load_layout(layout_path: Path) -> Dict: + """Load the TOML/YAML layout description data (and its ancessor, if any).""" + + def load_descriptor(file_path: Path) -> Dict: + if file_path.suffix in [".yaml", ".yml"]: + with file_path.open(encoding="utf-8") as file: + return yaml.load(file, Loader=yaml.SafeLoader) + + with file_path.open(mode="rb") as dfile: + return tomli.load(dfile) + + try: + cfg = load_descriptor(layout_path) + if "name" not in cfg: + cfg["name"] = layout_path.stem + if "extends" in cfg: + parent_path = filepath.parent / cfg["extends"] + ext = load_descriptor(parent_path) + ext.update(cfg) + cfg = ext + return cfg + + except Exception as exc: + click.echo("File could not be parsed.", err=True) + click.echo(f"Error: {exc}.", err=True) + sys.exit(1) ### @@ -114,6 +133,32 @@ def load_descriptor(file_path: Path) -> Dict: # +# fmt: off +@dataclass +class MetaDescr: + name: str = "custom" + name8: str = "custom" + variant: str = "custom" + fileName: str = "custom" + locale: str = "us" + geometry: str = "ISO" + description: str = "" + author: str = "nobody" + license: str = "" + version: str = "0.0.1" + lastChange: str = datetime.date.today().isoformat() + + +@dataclass +class SpacebarDescr: + shift: str = " " + altgr: str = " " + altgt_shift: str = " " + odk: str = "'" + odk_shift: str = "'" +# fmt: on + + CONFIG = { "author": "nobody", "license": "WTFPL - Do What The Fuck You Want Public License", @@ -150,9 +195,9 @@ def from_dict(cls: Type[T], src: Dict) -> T: ) -geometry_data = load_data("geometry.yaml") - -GEOMETRY = {key: GeometryDescr.from_dict(val) for key, val in geometry_data.items()} +GEOMETRY = { + key: GeometryDescr.from_dict(val) for key, val in load_data("geometry").items() +} ### @@ -163,42 +208,32 @@ def from_dict(cls: Type[T], src: Dict) -> T: class KeyboardLayout: """Lafayette-style keyboard layout: base + 1dk + altgr layers.""" - def __init__(self, filepath: Path, angle_mod: bool = False) -> None: + # self.meta = {key: MetaDescr.from_dict(val) for key, val in geometry_data.items()} + + def __init__(self, layout_data: Dict, angle_mod: bool = False) -> None: """Import a keyboard layout to instanciate the object.""" # initialize a blank layout self.layers: Dict[Layer, Dict[str, str]] = {layer: {} for layer in Layer} self.dk_set: Set[str] = set() self.dead_keys: Dict[str, Dict[str, str]] = {} # dictionary subset of DEAD_KEYS + # self.meta = Dict[str, str] = {} # default parameters, hardcoded self.meta = CONFIG.copy() # default parameters, hardcoded self.has_altgr = False self.has_1dk = False - # load the YAML data (and its ancessor, if any) - try: - cfg = load_descriptor(filepath) - if "extends" in cfg: - path = filepath.parent / cfg["extends"] - ext = load_descriptor(path) - ext.update(cfg) - cfg = ext - except Exception as exc: - click.echo("File could not be parsed.", err=True) - click.echo(f"Error: {exc}.", err=True) - sys.exit(1) - # metadata: self.meta - for k in cfg: + for k in layout_data: if ( k != "base" and k != "full" and k != "altgr" - and not isinstance(cfg[k], dict) + and not isinstance(layout_data[k], dict) ): - self.meta[k] = cfg[k] - filename = filepath.stem - self.meta["name"] = cfg["name"] if "name" in cfg else filename - self.meta["name8"] = cfg["name8"] if "name8" in cfg else self.meta["name"][0:8] + self.meta[k] = layout_data[k] + self.meta["name8"] = ( + layout_data["name8"] if "name8" in layout_data else self.meta["name"][0:8] + ) self.meta["fileName"] = self.meta["name8"].lower() self.meta["lastChange"] = datetime.date.today().isoformat() @@ -216,24 +251,26 @@ def __init__(self, filepath: Path, angle_mod: bool = False) -> None: "Warning: geometry does not support angle-mod; ignoring the --angle-mod argument" ) - if "full" in cfg: - full = text_to_lines(cfg["full"]) + if "full" in layout_data: + full = text_to_lines(layout_data["full"]) self._parse_template(full, rows, Layer.BASE) self._parse_template(full, rows, Layer.ALTGR) self.has_altgr = True else: - base = text_to_lines(cfg["base"]) + base = text_to_lines(layout_data["base"]) self._parse_template(base, rows, Layer.BASE) self._parse_template(base, rows, Layer.ODK) - if "altgr" in cfg: + if "altgr" in layout_data: self.has_altgr = True - self._parse_template(text_to_lines(cfg["altgr"]), rows, Layer.ALTGR) + self._parse_template( + text_to_lines(layout_data["altgr"]), rows, Layer.ALTGR + ) # space bar spc = SPACEBAR.copy() - if "spacebar" in cfg: - for k in cfg["spacebar"]: - spc[k] = cfg["spacebar"][k] + if "spacebar" in layout_data: + for k in layout_data["spacebar"]: + spc[k] = layout_data["spacebar"][k] self.layers[Layer.BASE]["spce"] = " " self.layers[Layer.SHIFT]["spce"] = spc["shift"] if True or self.has_1dk: # XXX self.has_1dk is not defined yet diff --git a/kalamine/server.py b/kalamine/server.py index 253a669..6170a9f 100644 --- a/kalamine/server.py +++ b/kalamine/server.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import json -import os import threading import webbrowser from http.server import HTTPServer, SimpleHTTPRequestHandler @@ -9,11 +8,11 @@ import click from livereload import Server # type: ignore -from .layout import KeyboardLayout +from .layout import KeyboardLayout, load_layout def keyboard_server(file_path: Path) -> None: - kb_layout = KeyboardLayout(file_path) + kb_layout = KeyboardLayout(load_layout(file_path)) host_name = "localhost" webserver_port = 1664 diff --git a/kalamine/template.py b/kalamine/template.py index bd82c4d..0e7b764 100644 --- a/kalamine/template.py +++ b/kalamine/template.py @@ -16,8 +16,8 @@ # Helpers # -KEY_CODES = load_data("key_codes.yaml") -XKB_KEY_SYM = load_data("key_sym.yaml") +KEY_CODES = load_data("key_codes") +XKB_KEY_SYM = load_data("key_sym") def hex_ord(char: str) -> str: diff --git a/kalamine/utils.py b/kalamine/utils.py index 000aa1a..edfe248 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import pkgutil from dataclasses import dataclass from enum import IntEnum from pathlib import Path @@ -28,8 +29,8 @@ def text_to_lines(text: str) -> List[str]: def load_data(filename: str) -> Dict: - filepath = Path(__file__).parent / "data" / filename - return yaml.load(filepath.open(encoding="utf-8"), Loader=yaml.SafeLoader) + descriptor = pkgutil.get_data(__package__, f"data/{filename}.yaml") + return yaml.safe_load(descriptor.decode("utf-8")) class Layer(IntEnum): @@ -65,7 +66,7 @@ class DeadKeyDescr: alt_self: str -DEAD_KEYS = [DeadKeyDescr(**data) for data in load_data("dead_keys.yaml")] +DEAD_KEYS = [DeadKeyDescr(**data) for data in load_data("dead_keys")] ODK_ID = "**" # must match the value in dead_keys.yaml LAYER_KEYS = [ diff --git a/tests/test_parser.py b/tests/test_parser.py index c7022a2..1c27de5 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,10 +1,10 @@ from kalamine import KeyboardLayout -from .util import get_layout_path +from .util import get_layout_dict -def load_layout(filename, angle_mod=False): - return KeyboardLayout(get_layout_path() / (filename + ".toml"), angle_mod) +def load_layout(filename: str, angle_mod: bool = False) -> KeyboardLayout: + return KeyboardLayout(get_layout_dict(filename), angle_mod) def test_ansi(): diff --git a/tests/test_serializer_ahk.py b/tests/test_serializer_ahk.py index a4b3090..ffdd286 100644 --- a/tests/test_serializer_ahk.py +++ b/tests/test_serializer_ahk.py @@ -3,14 +3,14 @@ from kalamine import KeyboardLayout from kalamine.template import ahk_keymap, ahk_shortcuts -from .util import get_layout_path +from .util import get_layout_dict -def load_layout(filename) -> KeyboardLayout: - return KeyboardLayout(get_layout_path() / (filename + ".toml")) +def load_layout(filename: str) -> KeyboardLayout: + return KeyboardLayout(get_layout_dict(filename)) -def split(multiline_str): +def split(multiline_str: str): return dedent(multiline_str).lstrip().splitlines() diff --git a/tests/test_serializer_keylayout.py b/tests/test_serializer_keylayout.py index ffe3d78..0aecf42 100644 --- a/tests/test_serializer_keylayout.py +++ b/tests/test_serializer_keylayout.py @@ -3,14 +3,14 @@ from kalamine import KeyboardLayout from kalamine.template import osx_actions, osx_keymap, osx_terminators -from .util import get_layout_path +from .util import get_layout_dict -def load_layout(filename): - return KeyboardLayout(get_layout_path() / (filename + ".toml")) +def load_layout(filename: str) -> KeyboardLayout: + return KeyboardLayout(get_layout_dict(filename)) -def split(multiline_str): +def split(multiline_str: str): return dedent(multiline_str).lstrip().rstrip().splitlines() diff --git a/tests/test_serializer_klc.py b/tests/test_serializer_klc.py index b7423bc..c0f3adc 100644 --- a/tests/test_serializer_klc.py +++ b/tests/test_serializer_klc.py @@ -1,20 +1,18 @@ -import os -from enum import Enum from textwrap import dedent from kalamine import KeyboardLayout from kalamine.template import klc_deadkeys, klc_dk_index, klc_keymap -from .util import get_layout_path +from .util import get_layout_dict -def split(multiline_str): +def split(multiline_str: str): return dedent(multiline_str).lstrip().rstrip().splitlines() LAYOUTS = {} for filename in ["ansi", "intl", "prog"]: - LAYOUTS[filename] = KeyboardLayout(get_layout_path() / (filename + ".toml")) + LAYOUTS[filename] = KeyboardLayout(get_layout_dict(filename)) def test_ansi_keymap(): diff --git a/tests/test_serializer_xkb.py b/tests/test_serializer_xkb.py index 704a259..ec47774 100644 --- a/tests/test_serializer_xkb.py +++ b/tests/test_serializer_xkb.py @@ -3,14 +3,14 @@ from kalamine import KeyboardLayout from kalamine.template import xkb_keymap -from .util import get_layout_path +from .util import get_layout_dict -def load_layout(filename): - return KeyboardLayout(get_layout_path() / (filename + ".toml")) +def load_layout(filename: str) -> KeyboardLayout: + return KeyboardLayout(get_layout_dict(filename)) -def split(multiline_str): +def split(multiline_str: str): return dedent(multiline_str).lstrip().rstrip().splitlines() diff --git a/tests/util.py b/tests/util.py index 21671e9..da97128 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,8 +1,13 @@ """Some util functions for tests.""" -from pathlib import Path +import pkgutil +from typing import Dict +import tomli -def get_layout_path() -> Path: + +def get_layout_dict(filename: str) -> Dict: """Return the layout directory path.""" - return Path(__file__).parent.parent / "layouts" + + descriptor = pkgutil.get_data(__package__, f"../layouts/{filename}.toml") + return tomli.loads(descriptor.decode("utf-8")) From d4936a79958d62217e014a1cda6bf07bfdd8222d Mon Sep 17 00:00:00 2001 From: Fabien Cazenave Date: Thu, 8 Feb 2024 23:55:43 +0100 Subject: [PATCH 6/9] load template with pkgutil --- kalamine/layout.py | 7 ++++--- kalamine/tpl/base.ahk | 2 +- kalamine/tpl/base.keylayout | 6 ++++-- kalamine/tpl/base.klc | 6 +++--- kalamine/tpl/base.xkb | 2 +- kalamine/tpl/base.xkb_patch | 3 ++- kalamine/tpl/full.ahk | 2 +- kalamine/tpl/full.keylayout | 6 ++++-- kalamine/tpl/full.klc | 4 ++-- kalamine/tpl/full.xkb | 2 +- kalamine/tpl/full.xkb_patch | 3 ++- kalamine/tpl/full_1dk.xkb | 2 +- kalamine/tpl/full_1dk.xkb_patch | 3 ++- 13 files changed, 28 insertions(+), 20 deletions(-) diff --git a/kalamine/layout.py b/kalamine/layout.py index 837f149..4c8a9b6 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import copy import datetime +import pkgutil import re import sys from dataclasses import dataclass @@ -86,15 +87,17 @@ def substitute_token(text: str, token: str, value: str) -> str: def load_tpl(layout: "KeyboardLayout", ext: str) -> str: + date = datetime.date.today().isoformat() tpl = "base" if layout.has_altgr: tpl = "full" if layout.has_1dk and ext.startswith(".xkb"): tpl = "full_1dk" - out = (Path(__file__).parent / "tpl" / (tpl + ext)).read_text(encoding="utf-8") + out = pkgutil.get_data(__package__, f"tpl/{tpl}{ext}").decode("utf-8") out = substitute_lines(out, "GEOMETRY_base", layout.base) out = substitute_lines(out, "GEOMETRY_full", layout.full) out = substitute_lines(out, "GEOMETRY_altgr", layout.altgr) + out = substitute_token(out, "KALAMINE", f"Generated by kalamine on {date}") for key, value in layout.meta.items(): out = substitute_token(out, key, value) return out @@ -146,7 +149,6 @@ class MetaDescr: author: str = "nobody" license: str = "" version: str = "0.0.1" - lastChange: str = datetime.date.today().isoformat() @dataclass @@ -235,7 +237,6 @@ def __init__(self, layout_data: Dict, angle_mod: bool = False) -> None: layout_data["name8"] if "name8" in layout_data else self.meta["name"][0:8] ) self.meta["fileName"] = self.meta["name8"].lower() - self.meta["lastChange"] = datetime.date.today().isoformat() # keyboard layers: self.layers & self.dead_keys rows = copy.deepcopy(GEOMETRY[self.meta["geometry"]].rows) diff --git a/kalamine/tpl/base.ahk b/kalamine/tpl/base.ahk index dd9ff75..c6d98b8 100644 --- a/kalamine/tpl/base.ahk +++ b/kalamine/tpl/base.ahk @@ -1,4 +1,4 @@ -; Generated by kalamine. +; ${KALAMINE} ; This is an AutoHotKey 1.1 script. PKL and EPKL still rely on AHK 1.1, too. ; AutoHotKey 2.0 is way too slow to emulate keyboard layouts at the moment diff --git a/kalamine/tpl/base.keylayout b/kalamine/tpl/base.keylayout index a362f0b..767575c 100644 --- a/kalamine/tpl/base.keylayout +++ b/kalamine/tpl/base.keylayout @@ -1,14 +1,16 @@ -