Skip to content

Commit

Permalink
Merge branch 'refactor/pathlib-and-annotations' of github.com:etienne…
Browse files Browse the repository at this point in the history
…-monier/kalamine into etienne-monier-refactor/pathlib-and-annotations
  • Loading branch information
fabi1cazenave committed Jan 31, 2024
2 parents 097609a + 79bf0b0 commit 41ba7ce
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 218 deletions.
197 changes: 112 additions & 85 deletions kalamine/cli.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
import json
import os
from importlib import metadata
from pathlib import Path
from typing import List, Literal, Union

import click

Expand All @@ -11,77 +11,96 @@


@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:
klc_path.write_text(layout.klc, encoding="utf-16le", newline="\r\n")

# 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:
osx_path.write_text(layout.keylayout, encoding="utf-8", newline="\n")

# 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:
xkb_path.write_text(layout.xkb, encoding="utf-8", newline="\n")

# 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:
xkb_custom_path.write_text(
layout.xkb_patch, "w", encoding="utf-8", newline="\n"
)

# 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(path_type=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:
Expand All @@ -94,37 +113,44 @@ def make(layout_descriptors, 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:
file.write(layout.klc)
elif output_file.endswith(".keylayout"):
with open(output_file, "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:
file.write(layout.xkb)
elif output_file.endswith(".xkb_custom"):
with open(output_file, "w", encoding="utf-8", newline="\n") as file:
file.write(layout.xkb_patch)
elif output_file.endswith(".json"):

elif output_file.suffix == ".klc":
output_file.write_text(layout.klc, "w", encoding="utf-16le", newline="\r\n")

elif output_file.suffix == ".keylayout":
output_file.write_text(
layout.keylayout, "w", encoding="utf-8", newline="\n"
)

elif output_file.suffix == ".xkb":
output_file.write_text(layout.xkb, "w", encoding="utf-8", newline="\n")

elif output_file.suffix == ".xkb_custom":
output_file.write_text(
layout.xkb_patch, "w", encoding="utf-8", newline="\n"
)

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("... " + output_file)


TOML_HEADER = """# kalamine keyboard layout descriptor
Expand All @@ -144,28 +170,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)),
)

content = f'{TOML_HEADER}"{geometry.upper()}"'
if odk:
Expand All @@ -180,31 +210,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
44 changes: 26 additions & 18 deletions kalamine/cli_xkb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import platform
import sys
import tempfile
from pathlib import Path
from typing import List

import click

Expand All @@ -17,29 +19,30 @@ def cli():


@cli.command()
@click.argument("input", nargs=1, type=click.Path(exists=True))
def apply(input):
@click.argument(
"filepath", type=click.Path(exists=True, dir_okay=False, path_type=Path)
)
def apply(filepath: Path):
"""Apply a Kalamine layout."""

if WAYLAND:
sys.exit(
"You appear to be running Wayland, which does not support this operation."
)

layout = KeyboardLayout(input)
layout = KeyboardLayout(filepath)
with tempfile.NamedTemporaryFile(
mode="w+", suffix=".xkb", encoding="utf-8"
) as temp_file:
try:
temp_file.write(layout.xkb)
os.system(f"xkbcomp -w0 {temp_file.name} $DISPLAY")
finally:
temp_file.close()
temp_file.write(layout.xkb)
os.system(f"xkbcomp -w0 {temp_file.name} $DISPLAY")


@cli.command()
@click.argument("layouts", nargs=-1, type=click.Path(exists=True))
def install(layouts):
@click.argument(
"layouts", nargs=-1, type=click.Path(exists=True, dir_okay=False, path_type=Path)
)
def install(layouts: List[Path]):
"""Install a list of Kalamine layouts."""

if not layouts:
Expand Down Expand Up @@ -84,7 +87,7 @@ def xkb_install(xkb):

@cli.command()
@click.argument("mask") # [locale]/[name]
def remove(mask):
def remove(mask: str):
"""Remove a list of Kalamine layouts."""

def xkb_remove(root=False):
Expand All @@ -102,25 +105,30 @@ def xkb_remove(root=False):
xkb_remove()


@cli.command()
@cli.command(name="list")
@click.option("-a", "--all", "all_flag", is_flag=True)
@click.argument("mask", default="*")
@click.option("--all", "-a", is_flag=True)
def list(mask, all):
def list_command(mask, all_flag):
"""List installed Kalamine layouts."""

for root in [True, False]:
filtered = {}

xkb = XKBManager(root=root)
layouts = xkb.list_all(mask) if all else xkb.list(mask)
layouts = xkb.list_all(mask) if all_flag else xkb.list(mask)
for locale, variants in sorted(layouts.items()):
for name, desc in sorted(variants.items()):
filtered[f"{locale}/{name}"] = desc

if mask == "*" and root and xkb.has_custom_symbols():
filtered["custom"] = ""

if bool(filtered):
if filtered:
print(xkb.path)
for id, desc in filtered.items():
print(f" {id:<24} {desc}")
for key, value in filtered.items():
print(f" {key:<24} {value}")


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

0 comments on commit 41ba7ce

Please sign in to comment.