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 all 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
201 changes: 119 additions & 82 deletions kalamine/cli.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
import json
import os
from contextlib import contextmanager
from importlib import metadata
from pathlib import Path
from typing import List, Literal, Union

import click

Expand All @@ -11,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("dist"))
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 @@ -144,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 @@ -180,31 +220,28 @@ 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__":
Expand Down
Loading