Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to pathlib and implement annotations #80

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 120 additions & 84 deletions kalamine/cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
import json
import os
import shutil
from contextlib import contextmanager
from importlib import metadata
from pathlib import Path
from typing import List, Literal, Union

import click

Expand All @@ -12,120 +12,155 @@


@click.group()
def cli():
pass
def cli() -> None:
...


def pretty_json(layout, path):
"""Pretty-prints the JSON layout."""
def pretty_json(layout: KeyboardLayout, output_path: Path) -> None:
"""Pretty-print the JSON layout.

Parameters
----------
layout : KeyboardLayout
The layout to be exported.
output_path : Path
The output file path.
"""
text = (
json.dumps(layout.json, indent=2, ensure_ascii=False)
.replace("\n ", " ")
.replace("\n ]", " ]")
.replace("\n }", " }")
)
with open(path, "w", encoding="utf8") as file:
file.write(text)
output_path.write_text(text, encoding="utf8")


def make_all(layout: KeyboardLayout, output_dir_path: Path) -> None:
"""Generate all layout output files.

def make_all(layout, subdir):
def out_path(ext=""):
return os.path.join(subdir, layout.meta["fileName"] + ext)
Parameters
----------
layout : KeyboardLayout
The layout to process.
output_dir_path : Path
The output directory.
"""

if not os.path.exists(subdir):
os.makedirs(subdir)
@contextmanager
def file_creation_context(ext: str = "") -> Path:
"""Generate an output file path for extension EXT, return it and finally echo info."""
path = output_dir_path / (layout.meta["fileName"] + ext)
yield path
click.echo(f"... {path}")

if not output_dir_path.exists():
output_dir_path.mkdir(parents=True)

# AHK driver
ahk_path = out_path(".ahk")
with open(ahk_path, "w", encoding="utf-8", newline="\n") as file:
file.write("\uFEFF") # AHK scripts require a BOM
file.write(layout.ahk)
print("... " + ahk_path)
with file_creation_context(".ahk") as ahk_path:
with ahk_path.open("w", encoding="utf-8", newline="\n") as file:
file.write("\uFEFF") # AHK scripts require a BOM
file.write(layout.ahk)

# Windows driver
klc_path = out_path(".klc")
with open(klc_path, "w", encoding="utf-16le", newline="\r\n") as file:
file.write(layout.klc)
print("... " + klc_path)
with file_creation_context(".klc") as klc_path:
with klc_path.open("w", encoding="utf-16le", newline="\r\n") as file:
file.write(layout.klc)

# macOS driver
osx_path = out_path(".keylayout")
with open(osx_path, "w", encoding="utf-8", newline="\n") as file:
file.write(layout.keylayout)
print("... " + osx_path)
with file_creation_context(".keylayout") as osx_path:
with osx_path.open("w", encoding="utf-8", newline="\n") as file:
file.write(layout.keylayout)

# Linux driver, user-space
xkb_path = out_path(".xkb")
with open(xkb_path, "w", encoding="utf-8", newline="\n") as file:
file.write(layout.xkb)
print("... " + xkb_path)
with file_creation_context(".xkb") as xkb_path:
with xkb_path.open("w", encoding="utf-8", newline="\n") as file:
file.write(layout.xkb)

# Linux driver, root
xkb_custom_path = out_path(".xkb_custom")
with open(xkb_custom_path, "w", encoding="utf-8", newline="\n") as file:
file.write(layout.xkb_patch)
print("... " + xkb_custom_path)
with file_creation_context(".xkb_custom") as xkb_custom_path:
with xkb_custom_path.open("w", encoding="utf-8", newline="\n") as file:
file.write(layout.xkb_patch)

# JSON data
json_path = out_path(".json")
pretty_json(layout, json_path)
print("... " + json_path)
with file_creation_context(".json") as json_path:
pretty_json(layout, json_path)

# SVG data
svg_path = out_path(".svg")
layout.svg.write(svg_path, pretty_print=True, encoding="utf-8")
print("... " + svg_path)
with file_creation_context(".svg") as svg_path:
layout.svg.write(svg_path, pretty_print=True, encoding="utf-8")


@cli.command()
@click.argument("layout_descriptors", nargs=-1, type=click.Path(exists=True))
@click.argument(
"layout_descriptors",
nargs=-1,
type=click.Path(exists=True, dir_okay=False, path_type=Path),
)
@click.option(
"--out", default="all", type=click.Path(), help="Keyboard drivers to generate."
"--out",
default="all",
type=click.Path(),
help="Keyboard drivers to generate.",
)
def make(layout_descriptors, out):
def make(layout_descriptors: List[Path], out: Union[Path, Literal["all"]]):
"""Convert TOML/YAML descriptions into OS-specific keyboard drivers."""

for input_file in layout_descriptors:
layout = KeyboardLayout(input_file)

# default: build all in the `dist` subdirectory
if out == "all":
make_all(layout, "dist")
make_all(layout, Path(__file__).parent.parent / "dist")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ce changement a l'air d'aller au-delà de l'utilisation de pathlib et des annotations. Je n'ai pas vérifié ce qu'il fait.

Copy link
Collaborator

@fabi1cazenave fabi1cazenave Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci du heads-up. En fait on ne veut pas de cette ligne.

C’est bourrin mais par défaut, kalamine génère tous les pilotes dans un sous-répertoire dist du répertoire courant. Ça mériterait certainement une option (e.g. --output-dir), mais on ne veut pas générer les fichiers dans le dossier de kalamine.

continue

# Transform out into Path.
out = Path(out)

# quick output: reuse the input name and change the file extension
if out in ["keylayout", "klc", "xkb", "xkb_custom", "svg"]:
output_file = os.path.splitext(input_file)[0] + "." + out
output_file = input_file.with_suffix("." + out)
else:
output_file = out

# detailed output
if output_file.endswith(".ahk"):
with open(output_file, "w", encoding="utf-8", newline="\n") as file:
if output_file.suffix == ".ahk":
with output_file.open("w", encoding="utf-8", newline="\n") as file:
file.write("\uFEFF") # AHK scripts require a BOM
file.write(layout.ahk)
elif output_file.endswith(".klc"):
with open(output_file, "w", encoding="utf-16le", newline="\r\n") as file:

elif output_file.suffix == ".klc":
with output_file.open("w", encoding="utf-16le", newline="\r\n") as file:
file.write(layout.klc)
elif output_file.endswith(".keylayout"):
with open(output_file, "w", encoding="utf-8", newline="\n") as file:

elif output_file.suffix == ".keylayout":
with output_file.open(
"w", encoding="utf-8", newline="\n"
) as file:
file.write(layout.keylayout)
elif output_file.endswith(".xkb"):
with open(output_file, "w", encoding="utf-8", newline="\n") as file:

elif output_file.suffix == ".xkb":
with output_file.open("w", encoding="utf-8", newline="\n") as file:
file.write(layout.xkb)
elif output_file.endswith(".xkb_custom"):
with open(output_file, "w", encoding="utf-8", newline="\n") as file:

elif output_file.suffix == ".xkb_custom":
with output_file.open(
"w", encoding="utf-8", newline="\n"
) as file:
file.write(layout.xkb_patch)
elif output_file.endswith(".json"):

elif output_file.suffix == ".json":
pretty_json(layout, output_file)
elif output_file.endswith(".svg"):

elif output_file.suffix == ".svg":
layout.svg.write(output_file, pretty_print=True, encoding="utf-8")

else:
print("Unsupported output format.")
click.echo("Unsupported output format.", err=True)
return

# successfully converted, display file name
print("... " + output_file)
click.echo(f"... {output_file}")


TOML_HEADER = """# kalamine keyboard layout descriptor
Expand All @@ -145,28 +180,32 @@ def make(layout_descriptors, out):
1dk_shift = "'" # apostrophe"""


# TODO: Provide geometry choices
@cli.command()
@click.argument("output_file", nargs=1, type=click.Path(exists=False))
@click.argument("output_file", nargs=1, type=click.Path(exists=False, path_type=Path))
@click.option("--geometry", default="ISO", help="Specify keyboard geometry.")
@click.option("--altgr/--no-altgr", default=False, help="Set an AltGr layer.")
@click.option("--1dk/--no-1dk", "odk", default=False, help="Set a custom dead key.")
def create(output_file, geometry, altgr, odk):
def create(output_file: Path, geometry: str, altgr: bool, odk: bool):
"""Create a new TOML layout description."""
base_dir_path = Path(__file__).resolve(strict=True).parent.parent

root = Path(__file__).resolve(strict=True).parent.parent

def get_layout(name):
layout = KeyboardLayout(str(root / "layouts" / f"{name}.toml"))
def get_layout(name: str) -> KeyboardLayout:
"""Return a layout of type NAME with constrained geometry."""
layout = KeyboardLayout(base_dir_path / "layouts" / f"{name}.toml")
layout.geometry = geometry
return layout

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
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)),
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Chipotage…)

Là j’avoue que j’avais fait à coups de += pare que je n’avais rien trouvé qui fonctionne sans péter l’indentation — ce qui, sur mon environnement de boulot, pète le repliement de code. Il y a peut-être une ruse de dedent qu’on pourrait utiliser ici ?


content = f'{TOML_HEADER}"{geometry.upper()}"'
if odk:
Expand All @@ -181,32 +220,29 @@ def keymap(layout_name, layout_layer, layer_name=""):
content += keymap("ansi", "base")

# append user guide sections
with (root / "docs" / "README.md").open() as f:
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# "
content += "\n# ".join(topic.rstrip().split("\n"))

with open(output_file, "w", encoding="utf-8", newline="\n") as file:
file.write(content)
print("... " + output_file)
output_file.write_text(content, "w", encoding="utf-8", newline="\n")
click.echo(f"... {output_file}")


@cli.command()
@click.argument("input", nargs=1, type=click.Path(exists=True))
def watch(input):
@click.argument("filepath", nargs=1, type=click.Path(exists=True, path_type=Path))
def watch(filepath: Path) -> None:
"""Watch a TOML/YAML layout description and display it in a web server."""

keyboard_server(input)
keyboard_server(filepath)


@cli.command()
def version():
def version() -> None:
"""Show version number and exit."""

print(f"kalamine { metadata.version('kalamine') }")
click.echo(f"kalamine { metadata.version('kalamine') }")


if __name__ == "__main__":
cli()
cli()
Loading