web component.
https://github.com/OneDeadKey/x-keyboard
"""
import json
import pkgutil
from typing import TYPE_CHECKING, Dict, List, Optional
from xml.etree import ElementTree as ET
if TYPE_CHECKING:
from ..layout import KeyboardLayout
from ..utils import LAYER_KEYS, ODK_ID, SCAN_CODES, Layer, upper_key
def raw_json(layout: "KeyboardLayout") -> Dict:
"""JSON layout descriptor"""
# flatten the keymap: each key has an array of 2-4 characters
# correcponding to Base, Shift, AltGr, AltGr+Shift
keymap: Dict[str, List[str]] = {}
for key_name in LAYER_KEYS:
if key_name.startswith("-"):
continue
chars = list("")
for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]:
if key_name in layout.layers[i]:
chars.append(layout.layers[i][key_name])
if chars:
keymap[SCAN_CODES["web"][key_name]] = chars
return {
# fmt: off
"name": layout.meta["name"],
"description": layout.meta["description"],
"geometry": layout.meta["geometry"].lower(),
"keymap": keymap,
"deadkeys": layout.dead_keys,
"altgr": layout.has_altgr,
# fmt: on
}
def pretty_json(layout: "KeyboardLayout") -> str:
"""Pretty-print the JSON layout."""
return (
json.dumps(raw_json(layout), indent=2, ensure_ascii=False)
.replace("\n ", " ")
.replace("\n ]", " ]")
.replace("\n }", " }")
)
def svg(layout: "KeyboardLayout") -> ET.ElementTree:
"""SVG drawing"""
svg_ns = "http://www.w3.org/2000/svg"
ET.register_namespace("", svg_ns)
ns = {"": svg_ns}
def set_key_label(key: Optional[ET.Element], lvl: int, char: str) -> None:
if not key:
return
for label in key.findall(f'g/text[@class="level{lvl}"]', ns):
if char not in layout.dead_keys:
label.text = char
else: # only show last char for deadkeys
if char == ODK_ID:
label.text = "★"
elif char == "*µ":
label.text = "α"
else:
label.text = char[-1]
label.set("class", f"{label.get('class')} deadKey")
def same_symbol(key_name: str, lower: Layer, upper: Layer):
up = layout.layers[upper]
low = layout.layers[lower]
if key_name not in up or key_name not in low:
return False
return up[key_name] == upper_key(low[key_name], blank_if_obvious=False)
# Parse the SVG template
# res = pkgutil.get_data(__package__, "templates/x-keyboard.svg")
res = pkgutil.get_data("kalamine", "templates/x-keyboard.svg")
if res is None:
return ET.ElementTree()
svg = ET.ElementTree(ET.fromstring(res.decode("utf-8")))
for key_name in LAYER_KEYS:
if key_name.startswith("-"):
continue
level = 0
for i in [
Layer.BASE,
Layer.SHIFT,
Layer.ALTGR,
Layer.ALTGR_SHIFT,
Layer.ODK,
Layer.ODK_SHIFT,
]:
level += 1
if key_name not in layout.layers[i]:
continue
if level == 1 and same_symbol(key_name, Layer.BASE, Layer.SHIFT):
continue
if level == 4 and same_symbol(key_name, Layer.ALTGR, Layer.ALTGR_SHIFT):
continue
if level == 6 and same_symbol(key_name, Layer.ODK, Layer.ODK_SHIFT):
continue
key = svg.find(f".//g[@id=\"{SCAN_CODES['web'][key_name]}\"]", ns)
set_key_label(key, level, layout.layers[i][key_name])
return svg
kalamine-0.38/kalamine/generators/xkb.py 0000664 0000000 0000000 00000007152 14650261713 0020327 0 ustar 00root root 0000000 0000000 """
GNU/Linux: XKB
- standalone xkb keymap file to be used by `xkbcomp` (XOrg only)
- xkb symbols/patch for XOrg (system-wide) & Wayland (system-wide/user-space)
"""
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from ..layout import KeyboardLayout
from ..template import load_tpl, substitute_lines
from ..utils import DK_INDEX, LAYER_KEYS, ODK_ID, hex_ord, load_data
XKB_KEY_SYM = load_data("key_sym")
def xkb_table(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]:
"""GNU/Linux layout."""
if layout.qwerty_shortcuts:
print("WARN: keeping qwerty shortcuts is not yet supported for xkb")
show_description = True
eight_level = layout.has_altgr and layout.has_1dk and not xkbcomp
odk_symbol = "ISO_Level5_Latch" if eight_level else "ISO_Level3_Latch"
max_length = 16 # `ISO_Level3_Latch` should be the longest symbol name
output: List[str] = []
for key_name in LAYER_KEYS:
if key_name.startswith("-"): # separator
if output:
output.append("")
output.append("//" + key_name[1:])
continue
descs = []
symbols = []
for layer in layout.layers.values():
if key_name in layer:
keysym = layer[key_name]
desc = keysym
# dead key?
if keysym in DK_INDEX:
name = DK_INDEX[keysym].name
desc = layout.dead_keys[keysym][keysym]
symbol = odk_symbol if keysym == ODK_ID else f"dead_{name}"
# regular key: use a keysym if possible, utf-8 otherwise
elif keysym in XKB_KEY_SYM and len(XKB_KEY_SYM[keysym]) <= max_length:
symbol = XKB_KEY_SYM[keysym]
else:
symbol = f"U{hex_ord(keysym).upper()}"
else:
desc = " "
symbol = "VoidSymbol"
descs.append(desc)
symbols.append(symbol.ljust(max_length))
key = "{{[ {0}, {1}, {2}, {3}]}}" # 4-level layout by default
description = "{0} {1} {2} {3}"
if layout.has_altgr and layout.has_1dk:
# 6 layers are needed: they won't fit on the 4-level format.
if xkbcomp: # user-space XKB keymap file (standalone)
# standalone XKB files work best with a dual-group solution:
# one 4-level group for base+1dk, one two-level group for AltGr
key = "{{[ {}, {}, {}, {}],[ {}, {}]}}"
description = "{} {} {} {} {} {}"
else: # eight_level XKB symbols (Neo-like)
key = "{{[ {0}, {1}, {4}, {5}, {2}, {3}]}}"
description = "{0} {1} {4} {5} {2} {3}"
elif layout.has_altgr:
del symbols[3]
del symbols[2]
del descs[3]
del descs[2]
line = f"key <{key_name.upper()}> {key.format(*symbols)};"
if show_description:
line += (" // " + description.format(*descs)).rstrip()
if line.endswith("\\"):
line += " " # escape trailing backslash
output.append(line)
return output
def xkb_keymap(self) -> str: # will not work with Wayland
"""GNU/Linux driver (standalone / user-space)"""
out = load_tpl(self, ".xkb_keymap")
out = substitute_lines(out, "LAYOUT", xkb_table(self, xkbcomp=True))
return out
def xkb_symbols(self) -> str:
"""GNU/Linux driver (xkb patch, system or user-space)"""
out = load_tpl(self, ".xkb_symbols")
out = substitute_lines(out, "LAYOUT", xkb_table(self, xkbcomp=False))
return out.replace("//#", "//")
kalamine-0.38/kalamine/help.py 0000664 0000000 0000000 00000011075 14650261713 0016321 0 ustar 00root root 0000000 0000000 from pathlib import Path
from typing import Dict, List
from .layout import KeyboardLayout
from .template import SCAN_CODES
from .utils import Layer, load_data
SEPARATOR = (
"--------------------------------------------------------------------------------"
)
MARKDOWN_HEADER = """Defining a Keyboard Layout
================================================================================
Kalamine keyboard layouts are defined with TOML files including this kind of
ASCII-art layer templates:
```KALAMINE_LAYOUT```
"""
TOML_HEADER = """# kalamine keyboard layout descriptor
name = "qwerty-custom" # full layout name, displayed in the keyboard settings
name8 = "custom" # short Windows filename: no spaces, no special chars
locale = "us" # locale/language id
variant = "custom" # layout variant id
author = "nobody" # author name
description = "custom QWERTY layout"
url = "https://OneDeadKey.github.com/kalamine"
version = "0.0.1"
geometry = """
TOML_FOOTER = """
[spacebar]
1dk = "'" # apostrophe
1dk_shift = "'" # apostrophe"""
def dead_key_table() -> str:
out = f"\n id XKB name base -> accented chars\n {SEPARATOR[4:]}"
for item in load_data("dead_keys"):
if (item["char"]) != "**":
out += f"\n {item['char']} {item['name']:<17} {item['base']}"
out += f"\n -> {item['alt']}"
return out
def core_guide() -> List[str]:
sections: List[str] = []
for title, content in load_data("user_guide").items():
out = f"\n{title.replace('_', ' ')}\n{SEPARATOR}"
if isinstance(content, dict):
for subtitle, subcontent in content.items():
out += f"\n\n### {subtitle.replace('_', ' ')}"
out += f"\n\n{subcontent}"
if subtitle == "Standard_Dead_Keys":
out += dead_key_table()
else:
out += f"\n\n{content}"
sections.append(out)
return sections
def dummy_layout(
geometry: str = "ISO",
altgr: bool = False,
odk: bool = False,
meta: Dict[str, str] = {},
) -> KeyboardLayout:
"""Create a dummy (QWERTY) layout with the given characteristics."""
# load the descriptor, but only keep the layers we need
descriptor = load_data("layout")
if not altgr:
del descriptor["altgr"]
if not odk:
del descriptor["1dk"]
descriptor["base"] = descriptor.pop("alpha")
else:
del descriptor["alpha"]
descriptor["base"] = descriptor.pop("1dk")
# XXX this should be a dataclass
for key, val in meta.items():
descriptor[key] = val
# make a KeyboardLayout matching the input parameters
descriptor["geometry"] = "ANSI" # layout.yaml has an ANSI geometry
layout = KeyboardLayout(descriptor)
layout.geometry = geometry
# ensure there is no empty keys (XXX maybe this should be in layout.py)
for key in SCAN_CODES["web"].keys():
if key not in layout.layers[Layer.BASE].keys():
layout.layers[Layer.BASE][key] = "\\"
layout.layers[Layer.SHIFT][key] = "|"
return layout
def draw_layout(geometry: str = "ISO", altgr: bool = False, odk: bool = False) -> str:
"""Draw a ASCII art description of a default layout."""
# make a KeyboardLayout, just to get the ASCII arts
layout = dummy_layout(geometry, altgr, odk)
def keymap(layer_name: str) -> str:
layer = "\n".join(getattr(layout, layer_name))
return f"\n{layer_name} = '''\n{layer}\n'''\n"
content = ""
if odk:
content += keymap("base")
if altgr:
content += keymap("altgr")
elif altgr:
content += keymap("full")
else:
content += keymap("base")
return content
###
# Public API
##
def user_guide() -> str:
"""Create a user guide with a sample layout description."""
header = MARKDOWN_HEADER.replace(
"KALAMINE_LAYOUT", draw_layout(geometry="ANSI", altgr=True)
)
return header + "\n" + "\n".join(core_guide())
def create_layout(output_file: Path, geometry: str, altgr: bool, odk: bool) -> None:
"""Create a new TOML layout description."""
content = f'{TOML_HEADER}"{geometry.upper()}"\n'
content += draw_layout(geometry, altgr, odk)
if odk:
content += TOML_FOOTER
for topic in core_guide():
content += f"\n\n\n# {SEPARATOR}"
content += "\n# ".join(topic.rstrip().split("\n"))
with open(output_file, "w", encoding="utf-8", newline="\n") as file:
file.write(content.replace(" \n", "\n"))
kalamine-0.38/kalamine/layout.py 0000664 0000000 0000000 00000030333 14650261713 0016704 0 ustar 00root root 0000000 0000000 import copy
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Set, Type, TypeVar
import click
import tomli
import yaml
from .utils import (
DEAD_KEYS,
LAYER_KEYS,
ODK_ID,
Layer,
load_data,
text_to_lines,
upper_key,
)
###
# Helpers
#
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 = layout_path.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)
###
# Constants
#
# 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"
@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",
"geometry": "ISO",
}
SPACEBAR = {
"shift": " ",
"altgr": " ",
"altgr_shift": " ",
"1dk": "'",
"1dk_shift": "'",
}
@dataclass
class RowDescr:
offset: int
keys: List[str]
T = TypeVar("T", bound="GeometryDescr")
@dataclass
class GeometryDescr:
template: str
rows: List[RowDescr]
@classmethod
def from_dict(cls: Type[T], src: Dict) -> T:
return cls(
template=src["template"], rows=[RowDescr(**row) for row in src["rows"]]
)
GEOMETRY = {
key: GeometryDescr.from_dict(val) for key, val in load_data("geometry").items()
}
###
# Main
#
class KeyboardLayout:
"""Lafayette-style keyboard layout: base + 1dk + altgr layers."""
# self.meta = {key: MetaDescr.from_dict(val) for key, val in geometry_data.items()}
def __init__(
self, layout_data: Dict, angle_mod: bool = False, qwerty_shortcuts: 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
self.qwerty_shortcuts = qwerty_shortcuts
self.angle_mod = angle_mod
# metadata: self.meta
for k in layout_data:
if (
k != "base"
and k != "full"
and k != "altgr"
and not isinstance(layout_data[k], dict)
):
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()
# keyboard layers: self.layers & self.dead_keys
rows = copy.deepcopy(GEOMETRY[self.meta["geometry"]].rows)
# Angle Mod permutation
if angle_mod:
last_row = rows[3]
if last_row.keys[0] == "lsgt":
# should bevome ['ab05', 'lsgt', 'ab01', 'ab02', 'ab03', 'ab04']
last_row.keys[:6] = [last_row.keys[5]] + last_row.keys[:5]
else:
click.echo(
"Warning: geometry does not support angle-mod; ignoring the --angle-mod argument"
)
self.angle_mod = False
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(layout_data["base"])
self._parse_template(base, rows, Layer.BASE)
self._parse_template(base, rows, Layer.ODK)
if "altgr" in layout_data:
self.has_altgr = True
self._parse_template(
text_to_lines(layout_data["altgr"]), rows, Layer.ALTGR
)
# space bar
spc = SPACEBAR.copy()
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
self.layers[Layer.ODK]["spce"] = spc["1dk"]
self.layers[Layer.ODK_SHIFT]["spce"] = (
spc["shift_1dk"] if "shift_1dk" in spc else spc["1dk"]
)
if self.has_altgr:
self.layers[Layer.ALTGR]["spce"] = spc["altgr"]
self.layers[Layer.ALTGR_SHIFT]["spce"] = spc["altgr_shift"]
self._parse_dead_keys(spc)
def _parse_dead_keys(self, spc: Dict[str, str]) -> None:
"""Build a deadkey dict."""
def layout_has_char(char: str) -> bool:
all_layers = [Layer.BASE, Layer.SHIFT]
if self.has_altgr:
all_layers += [Layer.ALTGR, Layer.ALTGR_SHIFT]
for layer_index in all_layers:
for id in self.layers[layer_index]:
if self.layers[layer_index][id] == char:
return True
return False
all_spaces: List[str] = []
for space in ["\u0020", "\u00a0", "\u202f"]:
if layout_has_char(space):
all_spaces.append(space)
self.dead_keys = {}
for dk in DEAD_KEYS:
id = dk.char
if id not in self.dk_set:
continue
self.dead_keys[id] = {}
deadkey = self.dead_keys[id]
deadkey[id] = dk.alt_self
if id == ODK_ID:
self.has_1dk = True
for key_name in LAYER_KEYS:
if key_name.startswith("-"):
continue
for layer in [Layer.ODK_SHIFT, Layer.ODK]:
if key_name in self.layers[layer]:
deadkey[self.layers[layer.necromance()][key_name]] = (
self.layers[layer][key_name]
)
for space in all_spaces:
deadkey[space] = spc["1dk"]
else:
base = dk.base
alt = dk.alt
for i in range(len(base)):
if layout_has_char(base[i]):
deadkey[base[i]] = alt[i]
for space in all_spaces:
deadkey[space] = dk.alt_space
def _parse_template(
self, template: List[str], rows: List[RowDescr], layer_number: Layer
) -> None:
"""Extract a keyboard layer from a template."""
j = 0
col_offset = 0 if layer_number == Layer.BASE else 2
for row in rows:
i = row.offset + col_offset
keys = row.keys
base = list(template[2 + j * 3])
shift = list(template[1 + j * 3])
for key in keys:
base_key = ("*" if base[i - 1] == "*" else "") + base[i]
shift_key = ("*" if shift[i - 1] == "*" else "") + shift[i]
# in the BASE layer, if the base character is undefined, shift prevails
if base_key == " ":
if layer_number == Layer.BASE:
base_key = shift_key.lower()
# in other layers, if the shift character is undefined, base prevails
elif shift_key == " ":
if layer_number == Layer.ALTGR:
shift_key = upper_key(base_key)
elif layer_number == Layer.ODK:
shift_key = upper_key(base_key)
# shift_key = upper_key(base_key, blank_if_obvious=False)
if base_key != " ":
self.layers[layer_number][key] = base_key
if shift_key != " ":
self.layers[layer_number.next()][key] = shift_key
for dk in DEAD_KEYS:
if base_key == dk.char or shift_key == dk.char:
self.dk_set.add(dk.char)
i += 6
j += 1
###
# Geometry: base, full, altgr
#
def _fill_template(
self, template: List[str], rows: List[RowDescr], layer_number: Layer
) -> List[str]:
"""Fill a template with a keyboard layer."""
if layer_number == Layer.BASE:
col_offset = 0
shift_prevails = True
else: # AltGr or 1dk
col_offset = 2
shift_prevails = False
j = 0
for row in rows:
i = row.offset + col_offset
keys = row.keys
base = list(template[2 + j * 3])
shift = list(template[1 + j * 3])
for key in keys:
base_key = " "
if key in self.layers[layer_number]:
base_key = self.layers[layer_number][key]
shift_key = " "
if key in self.layers[layer_number.next()]:
shift_key = self.layers[layer_number.next()][key]
dead_base = len(base_key) == 2 and base_key[0] == "*"
dead_shift = len(shift_key) == 2 and shift_key[0] == "*"
if shift_prevails:
shift[i] = shift_key[-1]
if dead_shift:
shift[i - 1] = "*"
if upper_key(base_key) != shift_key:
base[i] = base_key[-1]
if dead_base:
base[i - 1] = "*"
else:
base[i] = base_key[-1]
if dead_base:
base[i - 1] = "*"
if upper_key(base_key) != shift_key:
shift[i] = shift_key[-1]
if dead_shift:
shift[i - 1] = "*"
i += 6
template[2 + j * 3] = "".join(base)
template[1 + j * 3] = "".join(shift)
j += 1
return template
def _get_geometry(self, layers: Optional[List[Layer]] = None) -> List[str]:
"""`geometry` view of the requested layers."""
layers = layers or [Layer.BASE]
rows = GEOMETRY[self.geometry].rows
template = GEOMETRY[self.geometry].template.split("\n")[:-1]
for i in layers:
template = self._fill_template(template, rows, i)
return template
@property
def geometry(self) -> str:
"""ANSI, ISO, ERGO."""
return self.meta["geometry"].upper()
@geometry.setter
def geometry(self, value: str) -> None:
"""ANSI, ISO, ERGO."""
shape = value.upper()
if shape not in ["ANSI", "ISO", "ERGO"]:
shape = "ISO"
self.meta["geometry"] = shape
@property
def base(self) -> List[str]:
"""Base + 1dk layers."""
return self._get_geometry([Layer.BASE, Layer.ODK])
@property
def full(self) -> List[str]:
"""Base + AltGr layers."""
return self._get_geometry([Layer.BASE, Layer.ALTGR])
@property
def altgr(self) -> List[str]:
"""AltGr layer only."""
return self._get_geometry([Layer.ALTGR])
kalamine-0.38/kalamine/msklc_manager.py 0000664 0000000 0000000 00000023011 14650261713 0020165 0 ustar 00root root 0000000 0000000 import ctypes
import os
import platform
import subprocess
import sys
import winreg
from pathlib import Path
from shutil import move, rmtree
from stat import S_IREAD, S_IWUSR
from progress.bar import ChargingBar
from .generators import klc
from .help import dummy_layout
from .layout import KeyboardLayout
class MsklcManager:
def __init__(
self,
layout: "KeyboardLayout",
msklc_dir: Path,
working_dir: Path = Path(os.getcwd()),
install: bool = False,
verbose: bool = False,
) -> None:
self._layout = layout
self._msklc_dir = msklc_dir
self._verbose = verbose
self._working_dir = working_dir
nb_steps = 14
if install:
nb_steps = 15
self._progress = ChargingBar(
f"Creating MSKLC driver for `{layout.meta['name']}`", max=nb_steps
)
def create_c_files(self):
"""Call kbdutool on the KLC descriptor to generate C files."""
kbdutool = self._msklc_dir / Path("bin/i386/kbdutool.exe")
cur = os.getcwd()
os.chdir(self._working_dir)
ret = subprocess.run(
[kbdutool, "-u", "-s", f"{self._layout.meta['name8']}.klc"],
capture_output=not self._verbose,
)
os.chdir(cur)
ret.check_returncode()
def _is_already_installed(self) -> bool:
"""Check if the keyboard driver is already installed,
which would cause MSKLC to launch the GUI instead of creating the installer."""
# check if the DLL is present
sys32 = Path(os.environ["WINDIR"]) / Path("System32")
sysWow = Path(os.environ["WINDIR"]) / Path("SysWOW64")
dll_name = f'{self._layout.meta["name8"]}.dll'
dll_exists = (sys32 / dll_name).exists() or (sysWow / Path(dll_name)).exists()
if dll_exists:
print(f"Warning: {dll_name} is already installed")
return True
if sys.platform != "win32": # let mypy know this is win32-specific
return False
# check if the registry still has it
# that can happen after a botch uninstall of the driver
kbd_layouts_handle = winreg.OpenKeyEx(
winreg.HKEY_LOCAL_MACHINE,
"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts",
)
# [0] is the number of sub keys
for i in range(0, winreg.QueryInfoKey(kbd_layouts_handle)[0]):
sub_key = winreg.EnumKey(kbd_layouts_handle, i)
sub_handle = winreg.OpenKey(kbd_layouts_handle, sub_key)
layout_file = winreg.QueryValueEx(sub_handle, "Layout File")[0]
if layout_file == dll_name:
print(
f"Error: The registry still have reference to `{dll_name}` in"
f"`HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\{sub_key}`"
)
return True
return False
def _create_dummy_layout(self) -> str:
return klc.klc(
dummy_layout(
self._layout.geometry,
self._layout.has_altgr,
self._layout.has_1dk,
self._layout.meta,
)
)
def build_msklc_installer(self) -> bool:
def installer_exists(installer: Path) -> bool:
return (
installer.exists()
and installer.is_dir()
and (installer / Path("setup.exe")).exists()
and (installer / Path("amd64")).exists()
and (installer / Path("i386")).exists()
and (installer / Path("ia64")).exists()
and (installer / Path("wow64")).exists()
)
name8 = self._layout.meta["name8"]
installer_dir = self._working_dir / Path(name8)
if installer_exists(installer_dir):
self._progress.next(4)
return True
if self._is_already_installed():
self._progress.finish()
print(
"Error: layout already installed and "
"installer package not found in the current directory.\n"
"Either uninstall the layout manually, or put the installer "
f"folder in the current directory: ({self._working_dir})"
)
return False
self._progress.message = "Creating installer package"
self._progress.next()
# Create a dummy klc file to generate the installer.
# The file must have a correct name to be reflected in the installer.
dummy_klc = self._create_dummy_layout()
klc_file = Path(self._working_dir) / Path(f"{name8}.klc")
with klc_file.open("w", encoding="utf-16le", newline="\r\n") as file:
file.write(dummy_klc)
self._progress.next()
msklc = self._msklc_dir / Path("MSKLC.exe")
result = subprocess.run(
[msklc, klc_file, "-build"], capture_output=not self._verbose, text=True
)
self._progress.next()
# move the installer from "My Documents" to current dir
if sys.platform != "win32": # let mypy know this is win32-specific
return False
CSIDL_PERSONAL = 5 # My Documents
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
ctypes.windll.shell32.SHGetFolderPathW(
None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf
)
my_docs = Path(buf.value)
installer = my_docs / Path(name8)
self._progress.next()
if not installer_exists(installer):
self._progress.finish()
print(f"MSKLC Exit code: {result.returncode}")
print(result.stdout)
print(result.stderr)
print("Error: installer was not created.")
return False
move(str(installer), str(self._working_dir / Path(name8)))
return True
def build_msklc_dll(self) -> bool:
self._progress.next()
name8 = self._layout.meta["name8"]
prev = os.getcwd()
os.chdir(self._working_dir)
INST_DIR = self._working_dir / Path(name8)
dll_dirs = ["i386", "amd64", "ia64", "wow64"]
for dll_dir in dll_dirs:
full_dir = INST_DIR / Path(dll_dir)
if not full_dir.exists():
raise Exception(f"{full_dir} doesn't exist")
else:
rmtree(full_dir)
os.mkdir(full_dir)
self._progress.next()
# create correct klc
klc_file = self._working_dir / Path(f"{name8}.klc")
with klc_file.open("w", encoding="utf-16le", newline="\r\n") as file:
try:
file.write(klc.klc(self._layout))
except ValueError as err:
print(f"ERROR: {err}")
return False
self._progress.next()
self.create_c_files()
self._progress.next()
rc_file = klc_file.with_suffix(".RC")
with rc_file.open("w", encoding="utf-16le", newline="\r\n") as file:
file.write(klc.klc_rc(self._layout))
self._progress.next()
c_file = klc_file.with_suffix(".C")
with c_file.open("w", encoding="utf-16le", newline="\r\n") as file:
file.write(klc.klc_c(self._layout))
c_files = [".C", ".RC", ".H", ".DEF"]
self._progress.next()
# Make files read-only to prevent MSKLC from overwriting them.
for suffix in c_files:
os.chmod(klc_file.with_suffix(suffix), S_IREAD)
# build correct DLLs
kbdutool = self._msklc_dir / Path("bin/i386/kbdutool.exe")
dll = klc_file.with_suffix(".dll")
self._progress.message = "Creating driver DLLs"
for arch_flag, arch in [
("-x", "i386"),
("-m", "amd64"),
("-i", "ia64"),
("-o", "wow64"),
]:
self._progress.next()
result = subprocess.run(
[kbdutool, "-u", arch_flag, klc_file],
text=True,
capture_output=not self._verbose,
)
if result.returncode == 0:
move(str(dll), str(INST_DIR / Path(arch)))
else:
# Restore write permission
for suffix in c_files:
os.chmod(klc_file.with_suffix(suffix), S_IWUSR)
self._progress.finish()
print(f"Error while creating DLL for arch {arch}:")
print(result.stdout)
print(result.stderr)
return False
# Restore write permission
for suffix in c_files:
os.chmod(klc_file.with_suffix(suffix), S_IWUSR)
os.chdir(prev)
self._progress.finish()
return True
def install(self) -> bool:
self._progress.message = "Installing drivers"
arch = platform.machine().lower()
valid_archs = ["i386", "amd64", "ia64", "wow64"]
if arch not in valid_archs:
print(f"Unsupported architecture: {arch}")
self._progress.finish()
return False
os.chdir(self._layout.meta["name8"])
msi = f'{self._layout.meta["name8"]}_{arch}.msi'
if not Path(msi).exists():
print(f"`{msi}` not found")
self._progress.finish()
return False
self._progress.next()
flag = "/i"
if self._is_already_installed():
flag = "/fa"
result = subprocess.run(
["msiexec.exe", flag, msi], text=True, capture_output=not self._verbose
)
self._progress.finish()
return result.returncode == 0
kalamine-0.38/kalamine/server.py 0000664 0000000 0000000 00000012451 14650261713 0016676 0 ustar 00root root 0000000 0000000 import threading
import webbrowser
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from xml.etree import ElementTree as ET
import click
from livereload import Server # type: ignore
from .generators import ahk, keylayout, klc, web, xkb
from .layout import KeyboardLayout, load_layout
def keyboard_server(file_path: Path, angle_mod: bool = False) -> None:
kb_layout = KeyboardLayout(load_layout(file_path), angle_mod)
host_name = "localhost"
webserver_port = 1664
lr_server_port = 5500
def main_page(layout: KeyboardLayout, angle_mod: bool = False) -> str:
return f"""
Kalamine
{layout.meta['name']}
{layout.meta['locale']}/{layout.meta['variant']}
{layout.meta['description']}
json
| keylayout
| klc
| rc
| c
| xkb_keymap
| xkb_symbols
| svg
"""
class LayoutHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs) -> None: # type: ignore
kwargs["directory"] = str(Path(__file__).parent / "www")
super().__init__(*args, **kwargs)
def do_GET(self) -> None:
self.send_response(200)
def send(
page: str, content: str = "text/plain", charset: str = "utf-8"
) -> None:
self.send_header("Content-type", f"{content}; charset={charset}")
self.end_headers()
self.wfile.write(bytes(page, charset))
# self.wfile.write(page.encode(charset))
# XXX always reloads the layout on the root page, never in sub pages
nonlocal kb_layout
nonlocal angle_mod
if self.path == "/favicon.ico":
pass
elif self.path == "/json":
send(web.pretty_json(kb_layout), content="application/json")
elif self.path == "/keylayout":
# send(keylayout.keylayout(kb_layout), content='application/xml')
send(keylayout.keylayout(kb_layout))
elif self.path == "/ahk":
send(ahk.ahk(kb_layout))
elif self.path == "/klc":
send(klc.klc(kb_layout), charset="utf-16-le", content="text")
elif self.path == "/rc":
send(klc.klc_rc(kb_layout), content="text")
elif self.path == "/c":
send(klc.klc_c(kb_layout), content="text")
elif self.path == "/xkb_keymap":
send(xkb.xkb_keymap(kb_layout))
elif self.path == "/xkb_symbols":
send(xkb.xkb_symbols(kb_layout))
elif self.path == "/svg":
utf8 = ET.tostring(web.svg(kb_layout).getroot(), encoding="unicode")
send(utf8, content="image/svg+xml")
elif self.path == "/":
kb_layout = KeyboardLayout(load_layout(file_path), angle_mod) # refresh
send(main_page(kb_layout, angle_mod), content="text/html")
else:
return SimpleHTTPRequestHandler.do_GET(self)
webserver = HTTPServer((host_name, webserver_port), LayoutHandler)
thread = threading.Thread(None, webserver.serve_forever)
try:
thread.start()
url = f"http://{host_name}:{webserver_port}"
print(f"Server started: {url}")
print("Hit Ctrl-C to stop.")
webbrowser.open(url)
# livereload
lr_server = Server()
lr_server.watch(str(file_path))
lr_server.serve(host=host_name, port=lr_server_port)
except KeyboardInterrupt:
pass
webserver.shutdown()
webserver.server_close()
thread.join()
click.echo("Server stopped.")
kalamine-0.38/kalamine/template.py 0000664 0000000 0000000 00000003046 14650261713 0017203 0 ustar 00root root 0000000 0000000 import datetime
import pkgutil
import re
from typing import TYPE_CHECKING, List
from .utils import lines_to_text, load_data
if TYPE_CHECKING:
from .layout import KeyboardLayout
SCAN_CODES = load_data("scan_codes")
def substitute_lines(text: str, variable: str, lines: List[str]) -> str:
prefix = "KALAMINE::"
exp = re.compile(".*" + prefix + variable + ".*")
indent = ""
for line in text.split("\n"):
m = exp.match(line)
if m:
indent = m.group().split(prefix)[0]
break
return exp.sub(lines_to_text(lines, indent), text)
def substitute_token(text: str, token: str, value: str) -> str:
exp = re.compile("\\$\\{" + token + "(=[^\\}]*){0,1}\\}")
return exp.sub(value, text)
def load_tpl(layout: "KeyboardLayout", ext: str, tpl: str = "base") -> str:
date = datetime.date.today().isoformat()
if tpl == "base":
if layout.has_altgr or ext.startswith(".RC"):
tpl = "full"
if layout.has_1dk and ext.startswith(".xkb"):
tpl = "full_1dk"
bin = pkgutil.get_data(__package__, f"templates/{tpl}{ext}")
if bin is None:
return ""
out = bin.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
kalamine-0.38/kalamine/templates/ 0000775 0000000 0000000 00000000000 14650261713 0017011 5 ustar 00root root 0000000 0000000 kalamine-0.38/kalamine/templates/base.C 0000664 0000000 0000000 00000030545 14650261713 0020036 0 ustar 00root root 0000000 0000000 #include
#include "kbd.h"
#include "${name8}.h"
#if defined(_M_IA64)
#pragma section(".data")
#define ALLOC_SECTION_LDATA __declspec(allocate(".data"))
#else
#pragma data_seg(".data")
#define ALLOC_SECTION_LDATA
#endif
/***************************************************************************\
* ausVK[] - Virtual Scan Code to Virtual Key conversion table
\***************************************************************************/
static ALLOC_SECTION_LDATA USHORT ausVK[] = {
T00, T01, T02, T03, T04, T05, T06, T07,
T08, T09, T0A, T0B, T0C, T0D, T0E, T0F,
T10, T11, T12, T13, T14, T15, T16, T17,
T18, T19, T1A, T1B, T1C, T1D, T1E, T1F,
T20, T21, T22, T23, T24, T25, T26, T27,
T28, T29, T2A, T2B, T2C, T2D, T2E, T2F,
T30, T31, T32, T33, T34, T35,
/*
* Right-hand Shift key must have KBDEXT bit set.
*/
T36 | KBDEXT,
T37 | KBDMULTIVK, // numpad_* + Shift/Alt -> SnapShot
T38, T39, T3A, T3B, T3C, T3D, T3E,
T3F, T40, T41, T42, T43, T44,
/*
* NumLock Key:
* KBDEXT - VK_NUMLOCK is an Extended key
* KBDMULTIVK - VK_NUMLOCK or VK_PAUSE (without or with CTRL)
*/
T45 | KBDEXT | KBDMULTIVK,
T46 | KBDMULTIVK,
/*
* Number Pad keys:
* KBDNUMPAD - digits 0-9 and decimal point.
* KBDSPECIAL - require special processing by Windows
*/
T47 | KBDNUMPAD | KBDSPECIAL, // Numpad 7 (Home)
T48 | KBDNUMPAD | KBDSPECIAL, // Numpad 8 (Up),
T49 | KBDNUMPAD | KBDSPECIAL, // Numpad 9 (PgUp),
T4A,
T4B | KBDNUMPAD | KBDSPECIAL, // Numpad 4 (Left),
T4C | KBDNUMPAD | KBDSPECIAL, // Numpad 5 (Clear),
T4D | KBDNUMPAD | KBDSPECIAL, // Numpad 6 (Right),
T4E,
T4F | KBDNUMPAD | KBDSPECIAL, // Numpad 1 (End),
T50 | KBDNUMPAD | KBDSPECIAL, // Numpad 2 (Down),
T51 | KBDNUMPAD | KBDSPECIAL, // Numpad 3 (PgDn),
T52 | KBDNUMPAD | KBDSPECIAL, // Numpad 0 (Ins),
T53 | KBDNUMPAD | KBDSPECIAL, // Numpad . (Del),
T54, T55, T56, T57, T58, T59, T5A, T5B,
T5C, T5D, T5E, T5F, T60, T61, T62, T63,
T64, T65, T66, T67, T68, T69, T6A, T6B,
T6C, T6D, T6E, T6F, T70, T71, T72, T73,
T74, T75, T76, T77, T78, T79, T7A, T7B,
T7C, T7D, T7E
};
static ALLOC_SECTION_LDATA VSC_VK aE0VscToVk[] = {
{ 0x10, X10 | KBDEXT }, // Speedracer: Previous Track
{ 0x19, X19 | KBDEXT }, // Speedracer: Next Track
{ 0x1D, X1D | KBDEXT }, // RControl
{ 0x20, X20 | KBDEXT }, // Speedracer: Volume Mute
{ 0x21, X21 | KBDEXT }, // Speedracer: Launch App 2
{ 0x22, X22 | KBDEXT }, // Speedracer: Media Play/Pause
{ 0x24, X24 | KBDEXT }, // Speedracer: Media Stop
{ 0x2E, X2E | KBDEXT }, // Speedracer: Volume Down
{ 0x30, X30 | KBDEXT }, // Speedracer: Volume Up
{ 0x32, X32 | KBDEXT }, // Speedracer: Browser Home
{ 0x35, X35 | KBDEXT }, // Numpad Divide
{ 0x37, X37 | KBDEXT }, // Snapshot
{ 0x38, X38 | KBDEXT }, // RMenu
{ 0x47, X47 | KBDEXT }, // Home
{ 0x48, X48 | KBDEXT }, // Up
{ 0x49, X49 | KBDEXT }, // Prior
{ 0x4B, X4B | KBDEXT }, // Left
{ 0x4D, X4D | KBDEXT }, // Right
{ 0x4F, X4F | KBDEXT }, // End
{ 0x50, X50 | KBDEXT }, // Down
{ 0x51, X51 | KBDEXT }, // Next
{ 0x52, X52 | KBDEXT }, // Insert
{ 0x53, X53 | KBDEXT }, // Delete
{ 0x5B, X5B | KBDEXT }, // Left Win
{ 0x5C, X5C | KBDEXT }, // Right Win
{ 0x5D, X5D | KBDEXT }, // Application
{ 0x5F, X5F | KBDEXT }, // Speedracer: Sleep
{ 0x65, X65 | KBDEXT }, // Speedracer: Browser Search
{ 0x66, X66 | KBDEXT }, // Speedracer: Browser Favorites
{ 0x67, X67 | KBDEXT }, // Speedracer: Browser Refresh
{ 0x68, X68 | KBDEXT }, // Speedracer: Browser Stop
{ 0x69, X69 | KBDEXT }, // Speedracer: Browser Forward
{ 0x6A, X6A | KBDEXT }, // Speedracer: Browser Back
{ 0x6B, X6B | KBDEXT }, // Speedracer: Launch App 1
{ 0x6C, X6C | KBDEXT }, // Speedracer: Launch Mail
{ 0x6D, X6D | KBDEXT }, // Speedracer: Launch Media Selector
{ 0x1C, X1C | KBDEXT }, // Numpad Enter
{ 0x46, X46 | KBDEXT }, // Break (Ctrl + Pause)
{ 0, 0 }
};
static ALLOC_SECTION_LDATA VSC_VK aE1VscToVk[] = {
{ 0x1D, Y1D }, // Pause
{ 0 , 0 }
};
/***************************************************************************\
* aVkToBits[] - map Virtual Keys to Modifier Bits
*
* See kbd.h for a full description.
*
* The keyboard has only three shifter keys:
* SHIFT (L & R) affects alphabnumeric keys,
* CTRL (L & R) is used to generate control characters
* ALT (L & R) used for generating characters by number with numpad
\***************************************************************************/
static ALLOC_SECTION_LDATA VK_TO_BIT aVkToBits[] = {
{ VK_SHIFT , KBDSHIFT },
{ VK_CONTROL , KBDCTRL },
{ VK_MENU , KBDALT },
{ 0 , 0 }
};
/***************************************************************************\
* aModification[] - map character modifier bits to modification number
*
* See kbd.h for a full description.
*
\***************************************************************************/
static ALLOC_SECTION_LDATA MODIFIERS CharModifiers = {
&aVkToBits[0],
3,
{
// Modification# // Keys Pressed
// ============= // =============
0, //
1, // Shift
2, // Control
3 // Shift + Control
}
};
/***************************************************************************\
*
* aVkToWch2[] - Virtual Key to WCHAR translation for 2 shift states
* aVkToWch3[] - Virtual Key to WCHAR translation for 3 shift states
* aVkToWch4[] - Virtual Key to WCHAR translation for 4 shift states
*
* Table attributes: Unordered Scan, null-terminated
*
* Search this table for an entry with a matching Virtual Key to find the
* corresponding unshifted and shifted WCHAR characters.
*
* Special values for VirtualKey (column 1)
* 0xff - dead chars for the previous entry
* 0 - terminate the list
*
* Special values for Attributes (column 2)
* CAPLOK bit - CAPS-LOCK affect this key like SHIFT
*
* Special values for wch[*] (column 3 & 4)
* WCH_NONE - No character
* WCH_DEAD - Dead Key (diaresis) or invalid (US keyboard has none)
* WCH_LGTR - Ligature (generates multiple characters)
*
\***************************************************************************/
static ALLOC_SECTION_LDATA VK_TO_WCHARS2 aVkToWch2[] = {
// | | Shift |
// |=========|=========|
{VK_TAB ,0 ,'\t' ,'\t' },
{VK_ADD ,0 ,'+' ,'+' },
{VK_DIVIDE ,0 ,'/' ,'/' },
{VK_MULTIPLY ,0 ,'*' ,'*' },
{VK_SUBTRACT ,0 ,'-' ,'-' },
{0 ,0 ,0 ,0 }
};
static ALLOC_SECTION_LDATA VK_TO_WCHARS3 aVkToWch3[] = {
// | | Shift | Ctrl |
// |=========|=========|=========|
{VK_BACK ,0 ,'\b' ,'\b' ,0x007f },
{VK_ESCAPE ,0 ,0x001b ,0x001b ,0x001b },
{VK_RETURN ,0 ,'\r' ,'\r' ,'\n' },
{VK_CANCEL ,0 ,0x0003 ,0x0003 ,0x0003 },
{0 ,0 ,0 ,0 ,0 }
};
static ALLOC_SECTION_LDATA VK_TO_WCHARS4 aVkToWch4[] = {
// | | Shift | Ctrl |S+Ctrl |
// |=========|=========|=========|=========|
KALAMINE::LAYOUT
{0 ,0 ,0 ,0 ,0 ,0 }
};
// Put this last so that VkKeyScan interprets number characters
// as coming from the main section of the kbd (aVkToWch2 and
// aVkToWch5) before considering the numpad (aVkToWch1).
static ALLOC_SECTION_LDATA VK_TO_WCHARS1 aVkToWch1[] = {
{ VK_NUMPAD0 , 0 , '0' },
{ VK_NUMPAD1 , 0 , '1' },
{ VK_NUMPAD2 , 0 , '2' },
{ VK_NUMPAD3 , 0 , '3' },
{ VK_NUMPAD4 , 0 , '4' },
{ VK_NUMPAD5 , 0 , '5' },
{ VK_NUMPAD6 , 0 , '6' },
{ VK_NUMPAD7 , 0 , '7' },
{ VK_NUMPAD8 , 0 , '8' },
{ VK_NUMPAD9 , 0 , '9' },
{ 0 , 0 , '\0' }
};
static ALLOC_SECTION_LDATA VK_TO_WCHAR_TABLE aVkToWcharTable[] = {
{ (PVK_TO_WCHARS1)aVkToWch3, 3, sizeof(aVkToWch3[0]) },
{ (PVK_TO_WCHARS1)aVkToWch4, 4, sizeof(aVkToWch4[0]) },
{ (PVK_TO_WCHARS1)aVkToWch2, 2, sizeof(aVkToWch2[0]) },
{ (PVK_TO_WCHARS1)aVkToWch1, 1, sizeof(aVkToWch1[0]) },
{ NULL, 0, 0 },
};
/***************************************************************************\
* aKeyNames[], aKeyNamesExt[] - Virtual Scancode to Key Name tables
*
* Table attributes: Ordered Scan (by scancode), null-terminated
*
* Only the names of Extended, NumPad, Dead and Non-Printable keys are here.
* (Keys producing printable characters are named by that character)
\***************************************************************************/
static ALLOC_SECTION_LDATA VSC_LPWSTR aKeyNames[] = {
0x01, L"Esc",
0x0e, L"Backspace",
0x0f, L"Tab",
0x1c, L"Enter",
0x1d, L"Ctrl",
0x2a, L"Shift",
0x36, L"Right Shift",
0x37, L"Num *",
0x38, L"Alt",
0x39, L"Space",
0x3a, L"Caps Lock",
0x3b, L"F1",
0x3c, L"F2",
0x3d, L"F3",
0x3e, L"F4",
0x3f, L"F5",
0x40, L"F6",
0x41, L"F7",
0x42, L"F8",
0x43, L"F9",
0x44, L"F10",
0x45, L"Pause",
0x46, L"Scroll Lock",
0x47, L"Num 7",
0x48, L"Num 8",
0x49, L"Num 9",
0x4a, L"Num -",
0x4b, L"Num 4",
0x4c, L"Num 5",
0x4d, L"Num 6",
0x4e, L"Num +",
0x4f, L"Num 1",
0x50, L"Num 2",
0x51, L"Num 3",
0x52, L"Num 0",
0x53, L"Num Del",
0x54, L"Sys Req",
0x57, L"F11",
0x58, L"F12",
0x7c, L"F13",
0x7d, L"F14",
0x7e, L"F15",
0x7f, L"F16",
0x80, L"F17",
0x81, L"F18",
0x82, L"F19",
0x83, L"F20",
0x84, L"F21",
0x85, L"F22",
0x86, L"F23",
0x87, L"F24",
0 , NULL
};
static ALLOC_SECTION_LDATA VSC_LPWSTR aKeyNamesExt[] = {
0x1c, L"Num Enter",
0x1d, L"Right Ctrl",
0x35, L"Num /",
0x37, L"Prnt Scrn",
0x38, L"Right Alt",
0x45, L"Num Lock",
0x46, L"Break",
0x47, L"Home",
0x48, L"Up",
0x49, L"Page Up",
0x4b, L"Left",
0x4d, L"Right",
0x4f, L"End",
0x50, L"Down",
0x51, L"Page Down",
0x52, L"Insert",
0x53, L"Delete",
0x54, L"<00>",
0x56, L"Help",
0x5b, L"Left Windows",
0x5c, L"Right Windows",
0x5d, L"Application",
0 , NULL
};
static ALLOC_SECTION_LDATA DEADKEY_LPWSTR aKeyNamesDead[] = {
KALAMINE::DEAD_KEY_INDEX
NULL
};
static ALLOC_SECTION_LDATA DEADKEY aDeadKey[] = {
KALAMINE::DEAD_KEYS
0, 0
};
static ALLOC_SECTION_LDATA KBDTABLES KbdTables = {
/*
* Modifier keys
*/
&CharModifiers,
/*
* Characters tables
*/
aVkToWcharTable,
/*
* Diacritics
*/
aDeadKey,
/*
* Names of Keys
*/
aKeyNames,
aKeyNamesExt,
aKeyNamesDead,
/*
* Scan codes to Virtual Keys
*/
ausVK,
sizeof(ausVK) / sizeof(ausVK[0]),
aE0VscToVk,
aE1VscToVk,
/*
* Locale-specific special processing
*/
MAKELONG(0, KBD_VERSION),
/*
* Ligatures
*/
0,
0,
NULL
};
PKBDTABLES KbdLayerDescriptor(VOID)
{
return &KbdTables;
}
kalamine-0.38/kalamine/templates/base.ahk 0000664 0000000 0000000 00000005372 14650261713 0020417 0 ustar 00root root 0000000 0000000 ; ${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
; — or maybe we’ve missed the proper options to speed it up.
#NoEnv
#Persistent
#InstallKeybdHook
#SingleInstance, force
#MaxThreadsBuffer
#MaxThreadsPerHotKey 3
#MaxHotkeysPerInterval 300
#MaxThreads 20
SendMode Event ; either Event or Input
SetKeyDelay, -1
SetBatchLines, -1
Process, Priority, , R
SetWorkingDir, %A_ScriptDir%
StringCaseSense, On
;-------------------------------------------------------------------------------
; On/Off Switch
;-------------------------------------------------------------------------------
global Active := True
HideTrayTip() {
TrayTip ; Attempt to hide it the normal way.
if SubStr(A_OSVersion,1,3) = "10." {
Menu Tray, NoIcon
Sleep 200 ; It may be necessary to adjust this sleep.
Menu Tray, Icon
}
}
ShowTrayTip() {
title := "${name}"
text := Active ? "ON" : "OFF"
HideTrayTip()
TrayTip, %title% , %text%, 1, 0x31
SetTimer, HideTrayTip, -1500
}
RAlt & Alt::
Alt & RAlt::
global Active
Active := !Active
ShowTrayTip()
return
#If Active
SetTimer, ShowTrayTip, -1000 ; not working
;-------------------------------------------------------------------------------
; DeadKey Helpers
;-------------------------------------------------------------------------------
global DeadKey := ""
; Check CapsLock status, upper the char if needed and send the char
SendChar(char) {
if % GetKeyState("CapsLock", "T") {
if (StrLen(char) == 6) {
; we have something in the form of `U+NNNN `
; Change it to 0xNNNN so it can be passed to `Chr` function
char := Chr("0x" SubStr(char, 3, 4))
}
StringUpper, char, char
}
Send, {%char%}
}
DoTerm(base:="") {
global DeadKey
term := SubStr(DeadKey, 2, 1)
Send, {%term%}
SendChar(base)
DeadKey := ""
}
DoAction(action:="") {
global DeadKey
if (action == "U+0020") {
Send, {SC39}
DeadKey := ""
}
else if (StrLen(action) != 2) {
SendChar(action)
DeadKey := ""
}
else if (action == DeadKey) {
DoTerm(SubStr(DeadKey, 2, 1))
}
else {
DeadKey := action
}
}
SendKey(base, deadkeymap) {
if (!DeadKey) {
DoAction(base)
}
else if (deadkeymap.HasKey(DeadKey)) {
DoAction(deadkeymap[DeadKey])
}
else {
DoTerm(base)
}
}
;-------------------------------------------------------------------------------
; Base
;-------------------------------------------------------------------------------
KALAMINE::LAYOUT
;-------------------------------------------------------------------------------
; Ctrl
;-------------------------------------------------------------------------------
KALAMINE::SHORTCUTS
kalamine-0.38/kalamine/templates/base.keylayout 0000664 0000000 0000000 00000041654 14650261713 0021705 0 ustar 00root root 0000000 0000000
KALAMINE::LAYER_0
KALAMINE::LAYER_1
KALAMINE::LAYER_2
KALAMINE::LAYER_3
KALAMINE::LAYER_4
KALAMINE::ACTIONS
KALAMINE::TERMINATORS
kalamine-0.38/kalamine/templates/base.klc 0000664 0000000 0000000 00000003174 14650261713 0020423 0 ustar 00root root 0000000 0000000 // ${KALAMINE}
//
// File : ${fileName}.klc
// Encoding : ${encoding=utf-8, with BOM}
// Project page : ${url}
// Author : ${author}
// Version : ${version}
// License : ${license}
//
// ${description}
//
KBD ${name8} "${name}"
COPYRIGHT "(c) 2010-2024 ${author}"
COMPANY "${author}"
LOCALENAME "${locale}"
LOCALEID "${localeid}"
VERSION ${version}
// Base layer + dead key
// KALAMINE::GEOMETRY_base
SHIFTSTATE
0 // Column 4
1 // Column 5: Shift
2 // Column 6: Ctrl
3 // Column 7: Shift Ctrl
LAYOUT //{{{
// an extra '@' at the end is a dead key
//SC VK_ Cap 0 1 2 3
KALAMINE::LAYOUT
53 DECIMAL 0 002e 002e -1 -1 // FULL STOP, FULL STOP,
//}}}
KALAMINE::DEAD_KEYS
KEYNAME //{{{
01 Esc
0e Backspace
0f Tab
1c Enter
1d Ctrl
2a Shift
36 "Right Shift"
37 "Num *"
38 Alt
39 Space
3a "Caps Lock"
3b F1
3c F2
3d F3
3e F4
3f F5
40 F6
41 F7
42 F8
43 F9
44 F10
45 Pause
46 "Scroll Lock"
47 "Num 7"
48 "Num 8"
49 "Num 9"
4a "Num -"
4b "Num 4"
4c "Num 5"
4d "Num 6"
4e "Num +"
4f "Num 1"
50 "Num 2"
51 "Num 3"
52 "Num 0"
53 "Num Del"
54 "Sys Req"
57 F11
58 F12
7c F13
7d F14
7e F15
7f F16
80 F17
81 F18
82 F19
83 F20
84 F21
85 F22
86 F23
87 F24
//}}}
KEYNAME_EXT //{{{
1c "Num Enter"
1d "Right Ctrl"
35 "Num /"
37 "Prnt Scrn"
38 "Right Alt"
45 "Num Lock"
46 Break
47 Home
48 Up
49 "Page Up"
4b Left
4d Right
4f End
50 Down
51 "Page Down"
52 Insert
53 Delete
54 <00>
56 Help
5b "Left Windows"
5c "Right Windows"
5d Application
//}}}
KEYNAME_DEAD //{{{
KALAMINE::DEAD_KEY_INDEX
//}}}
DESCRIPTIONS
0409 ${description}
LANGUAGENAMES
0409 French (France)
ENDKBD
// vim: ft=xkb:ts=12:fdm=marker:fmr=//{{{,}}}:nowrap
kalamine-0.38/kalamine/templates/base.xkb_keymap 0000664 0000000 0000000 00000001475 14650261713 0022006 0 ustar 00root root 0000000 0000000 // ${KALAMINE}
//
// This is a standalone XKB keymap file. To apply this keymap, use:
// xkbcomp -w9 ${fileName}.xkb_keymap $DISPLAY
//
// DO NOT COPY THIS INTO xkb/symbols: THIS WOULD MESS UP YOUR XKB CONFIG.
//
// File : ${fileName}.xkb_keymap
// Project page : ${url}
// Author : ${author}
// Version : ${version}
// License : ${license}
//
// ${description}
//
xkb_keymap {
xkb_keycodes { include "evdev" };
xkb_types { include "complete" };
xkb_compatibility { include "complete" };
// KALAMINE::GEOMETRY_base
partial alphanumeric_keys modifier_keys
xkb_symbols "${variant}" {
include "pc"
include "inet(evdev)"
name[group1]= "${description}";
key.type[group1] = "FOUR_LEVEL";
KALAMINE::LAYOUT
};
};
// vim: ft=xkb:fdm=indent:ts=2:nowrap
kalamine-0.38/kalamine/templates/base.xkb_symbols 0000664 0000000 0000000 00000001075 14650261713 0022204 0 ustar 00root root 0000000 0000000 // ${KALAMINE}
//
//# This XKB symbols file should be copied to:
//# /usr/share/X11/xkb/symbols/custom
//# or
//# $XKB_CONFIG_ROOT/symbols/custom
//#
//# File : ${fileName}.xkb_symbols
// Project page : ${url}
// Author : ${author}
// Version : ${version}
// License : ${license}
//
// ${description}
//
// KALAMINE::GEOMETRY_base
partial alphanumeric_keys modifier_keys
xkb_symbols "${variant}" {
name[group1]= "${description}";
key.type[group1] = "FOUR_LEVEL";
KALAMINE::LAYOUT
};
//# vim: ft=xkb:fdm=indent:ts=4:nowrap
kalamine-0.38/kalamine/templates/full.C 0000664 0000000 0000000 00000031324 14650261713 0020062 0 ustar 00root root 0000000 0000000 #include
#include "kbd.h"
#include "${name8}.h"
#if defined(_M_IA64)
#pragma section(".data")
#define ALLOC_SECTION_LDATA __declspec(allocate(".data"))
#else
#pragma data_seg(".data")
#define ALLOC_SECTION_LDATA
#endif
/***************************************************************************\
* ausVK[] - Virtual Scan Code to Virtual Key conversion table
\***************************************************************************/
static ALLOC_SECTION_LDATA USHORT ausVK[] = {
T00, T01, T02, T03, T04, T05, T06, T07,
T08, T09, T0A, T0B, T0C, T0D, T0E, T0F,
T10, T11, T12, T13, T14, T15, T16, T17,
T18, T19, T1A, T1B, T1C, T1D, T1E, T1F,
T20, T21, T22, T23, T24, T25, T26, T27,
T28, T29, T2A, T2B, T2C, T2D, T2E, T2F,
T30, T31, T32, T33, T34, T35,
/*
* Right-hand Shift key must have KBDEXT bit set.
*/
T36 | KBDEXT,
T37 | KBDMULTIVK, // numpad_* + Shift/Alt -> SnapShot
T38, T39, T3A, T3B, T3C, T3D, T3E,
T3F, T40, T41, T42, T43, T44,
/*
* NumLock Key:
* KBDEXT - VK_NUMLOCK is an Extended key
* KBDMULTIVK - VK_NUMLOCK or VK_PAUSE (without or with CTRL)
*/
T45 | KBDEXT | KBDMULTIVK,
T46 | KBDMULTIVK,
/*
* Number Pad keys:
* KBDNUMPAD - digits 0-9 and decimal point.
* KBDSPECIAL - require special processing by Windows
*/
T47 | KBDNUMPAD | KBDSPECIAL, // Numpad 7 (Home)
T48 | KBDNUMPAD | KBDSPECIAL, // Numpad 8 (Up),
T49 | KBDNUMPAD | KBDSPECIAL, // Numpad 9 (PgUp),
T4A,
T4B | KBDNUMPAD | KBDSPECIAL, // Numpad 4 (Left),
T4C | KBDNUMPAD | KBDSPECIAL, // Numpad 5 (Clear),
T4D | KBDNUMPAD | KBDSPECIAL, // Numpad 6 (Right),
T4E,
T4F | KBDNUMPAD | KBDSPECIAL, // Numpad 1 (End),
T50 | KBDNUMPAD | KBDSPECIAL, // Numpad 2 (Down),
T51 | KBDNUMPAD | KBDSPECIAL, // Numpad 3 (PgDn),
T52 | KBDNUMPAD | KBDSPECIAL, // Numpad 0 (Ins),
T53 | KBDNUMPAD | KBDSPECIAL, // Numpad . (Del),
T54, T55, T56, T57, T58, T59, T5A, T5B,
T5C, T5D, T5E, T5F, T60, T61, T62, T63,
T64, T65, T66, T67, T68, T69, T6A, T6B,
T6C, T6D, T6E, T6F, T70, T71, T72, T73,
T74, T75, T76, T77, T78, T79, T7A, T7B,
T7C, T7D, T7E
};
static ALLOC_SECTION_LDATA VSC_VK aE0VscToVk[] = {
{ 0x10, X10 | KBDEXT }, // Speedracer: Previous Track
{ 0x19, X19 | KBDEXT }, // Speedracer: Next Track
{ 0x1D, X1D | KBDEXT }, // RControl
{ 0x20, X20 | KBDEXT }, // Speedracer: Volume Mute
{ 0x21, X21 | KBDEXT }, // Speedracer: Launch App 2
{ 0x22, X22 | KBDEXT }, // Speedracer: Media Play/Pause
{ 0x24, X24 | KBDEXT }, // Speedracer: Media Stop
{ 0x2E, X2E | KBDEXT }, // Speedracer: Volume Down
{ 0x30, X30 | KBDEXT }, // Speedracer: Volume Up
{ 0x32, X32 | KBDEXT }, // Speedracer: Browser Home
{ 0x35, X35 | KBDEXT }, // Numpad Divide
{ 0x37, X37 | KBDEXT }, // Snapshot
{ 0x38, X38 | KBDEXT }, // RMenu
{ 0x47, X47 | KBDEXT }, // Home
{ 0x48, X48 | KBDEXT }, // Up
{ 0x49, X49 | KBDEXT }, // Prior
{ 0x4B, X4B | KBDEXT }, // Left
{ 0x4D, X4D | KBDEXT }, // Right
{ 0x4F, X4F | KBDEXT }, // End
{ 0x50, X50 | KBDEXT }, // Down
{ 0x51, X51 | KBDEXT }, // Next
{ 0x52, X52 | KBDEXT }, // Insert
{ 0x53, X53 | KBDEXT }, // Delete
{ 0x5B, X5B | KBDEXT }, // Left Win
{ 0x5C, X5C | KBDEXT }, // Right Win
{ 0x5D, X5D | KBDEXT }, // Application
{ 0x5F, X5F | KBDEXT }, // Speedracer: Sleep
{ 0x65, X65 | KBDEXT }, // Speedracer: Browser Search
{ 0x66, X66 | KBDEXT }, // Speedracer: Browser Favorites
{ 0x67, X67 | KBDEXT }, // Speedracer: Browser Refresh
{ 0x68, X68 | KBDEXT }, // Speedracer: Browser Stop
{ 0x69, X69 | KBDEXT }, // Speedracer: Browser Forward
{ 0x6A, X6A | KBDEXT }, // Speedracer: Browser Back
{ 0x6B, X6B | KBDEXT }, // Speedracer: Launch App 1
{ 0x6C, X6C | KBDEXT }, // Speedracer: Launch Mail
{ 0x6D, X6D | KBDEXT }, // Speedracer: Launch Media Selector
{ 0x1C, X1C | KBDEXT }, // Numpad Enter
{ 0x46, X46 | KBDEXT }, // Break (Ctrl + Pause)
{ 0, 0 }
};
static ALLOC_SECTION_LDATA VSC_VK aE1VscToVk[] = {
{ 0x1D, Y1D }, // Pause
{ 0 , 0 }
};
/***************************************************************************\
* aVkToBits[] - map Virtual Keys to Modifier Bits
*
* See kbd.h for a full description.
*
* The keyboard has only three shifter keys:
* SHIFT (L & R) affects alphabnumeric keys,
* CTRL (L & R) is used to generate control characters
* ALT (L & R) used for generating characters by number with numpad
\***************************************************************************/
static ALLOC_SECTION_LDATA VK_TO_BIT aVkToBits[] = {
{ VK_SHIFT , KBDSHIFT },
{ VK_CONTROL , KBDCTRL },
{ VK_MENU , KBDALT },
{ 0 , 0 }
};
/***************************************************************************\
* aModification[] - map character modifier bits to modification number
*
* See kbd.h for a full description.
*
\***************************************************************************/
static ALLOC_SECTION_LDATA MODIFIERS CharModifiers = {
&aVkToBits[0],
7,
{
// Modification# // Keys Pressed
// ============= // =============
0, //
1, // Shift
2, // Control
3, // Shift + Control
SHFT_INVALID, // Menu
SHFT_INVALID, // Shift + Menu
4, // Control + Menu
5 // Shift + Control + Menu
}
};
/***************************************************************************\
*
* aVkToWch2[] - Virtual Key to WCHAR translation for 2 shift states
* aVkToWch3[] - Virtual Key to WCHAR translation for 3 shift states
* aVkToWch4[] - Virtual Key to WCHAR translation for 4 shift states
* aVkToWch5[] - Virtual Key to WCHAR translation for 5 shift states
* aVkToWch6[] - Virtual Key to WCHAR translation for 6 shift states
*
* Table attributes: Unordered Scan, null-terminated
*
* Search this table for an entry with a matching Virtual Key to find the
* corresponding unshifted and shifted WCHAR characters.
*
* Special values for VirtualKey (column 1)
* 0xff - dead chars for the previous entry
* 0 - terminate the list
*
* Special values for Attributes (column 2)
* CAPLOK bit - CAPS-LOCK affect this key like SHIFT
*
* Special values for wch[*] (column 3 & 4)
* WCH_NONE - No character
* WCH_DEAD - Dead Key (diaresis) or invalid (US keyboard has none)
* WCH_LGTR - Ligature (generates multiple characters)
*
\***************************************************************************/
static ALLOC_SECTION_LDATA VK_TO_WCHARS2 aVkToWch2[] = {
// | | Shift |
// |=========|=========|
{VK_TAB ,0 ,'\t' ,'\t' },
{VK_ADD ,0 ,'+' ,'+' },
{VK_DIVIDE ,0 ,'/' ,'/' },
{VK_MULTIPLY ,0 ,'*' ,'*' },
{VK_SUBTRACT ,0 ,'-' ,'-' },
{0 ,0 ,0 ,0 }
};
static ALLOC_SECTION_LDATA VK_TO_WCHARS3 aVkToWch3[] = {
// | | Shift | Ctrl |
// |=========|=========|=========|
{VK_BACK ,0 ,'\b' ,'\b' ,0x007f },
{VK_ESCAPE ,0 ,0x001b ,0x001b ,0x001b },
{VK_RETURN ,0 ,'\r' ,'\r' ,'\n' },
{VK_CANCEL ,0 ,0x0003 ,0x0003 ,0x0003 },
{0 ,0 ,0 ,0 ,0 }
};
static ALLOC_SECTION_LDATA VK_TO_WCHARS6 aVkToWch6[] = {
// | | Shift | Ctrl |S+Ctrl | Ctl+Alt|S+Ctl+Alt|
// |=========|=========|=========|=========|=========|=========|
KALAMINE::LAYOUT
{0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 }
};
// Put this last so that VkKeyScan interprets number characters
// as coming from the main section of the kbd (aVkToWch2 and
// aVkToWch5) before considering the numpad (aVkToWch1).
static ALLOC_SECTION_LDATA VK_TO_WCHARS1 aVkToWch1[] = {
{ VK_NUMPAD0 , 0 , '0' },
{ VK_NUMPAD1 , 0 , '1' },
{ VK_NUMPAD2 , 0 , '2' },
{ VK_NUMPAD3 , 0 , '3' },
{ VK_NUMPAD4 , 0 , '4' },
{ VK_NUMPAD5 , 0 , '5' },
{ VK_NUMPAD6 , 0 , '6' },
{ VK_NUMPAD7 , 0 , '7' },
{ VK_NUMPAD8 , 0 , '8' },
{ VK_NUMPAD9 , 0 , '9' },
{ 0 , 0 , '\0' }
};
static ALLOC_SECTION_LDATA VK_TO_WCHAR_TABLE aVkToWcharTable[] = {
{ (PVK_TO_WCHARS1)aVkToWch3, 3, sizeof(aVkToWch3[0]) },
{ (PVK_TO_WCHARS1)aVkToWch6, 6, sizeof(aVkToWch6[0]) },
{ (PVK_TO_WCHARS1)aVkToWch2, 2, sizeof(aVkToWch2[0]) },
{ (PVK_TO_WCHARS1)aVkToWch1, 1, sizeof(aVkToWch1[0]) },
{ NULL, 0, 0 },
};
/***************************************************************************\
* aKeyNames[], aKeyNamesExt[] - Virtual Scancode to Key Name tables
*
* Table attributes: Ordered Scan (by scancode), null-terminated
*
* Only the names of Extended, NumPad, Dead and Non-Printable keys are here.
* (Keys producing printable characters are named by that character)
\***************************************************************************/
static ALLOC_SECTION_LDATA VSC_LPWSTR aKeyNames[] = {
0x01, L"Esc",
0x0e, L"Backspace",
0x0f, L"Tab",
0x1c, L"Enter",
0x1d, L"Ctrl",
0x2a, L"Shift",
0x36, L"Right Shift",
0x37, L"Num *",
0x38, L"Alt",
0x39, L"Space",
0x3a, L"Caps Lock",
0x3b, L"F1",
0x3c, L"F2",
0x3d, L"F3",
0x3e, L"F4",
0x3f, L"F5",
0x40, L"F6",
0x41, L"F7",
0x42, L"F8",
0x43, L"F9",
0x44, L"F10",
0x45, L"Pause",
0x46, L"Scroll Lock",
0x47, L"Num 7",
0x48, L"Num 8",
0x49, L"Num 9",
0x4a, L"Num -",
0x4b, L"Num 4",
0x4c, L"Num 5",
0x4d, L"Num 6",
0x4e, L"Num +",
0x4f, L"Num 1",
0x50, L"Num 2",
0x51, L"Num 3",
0x52, L"Num 0",
0x53, L"Num Del",
0x54, L"Sys Req",
0x57, L"F11",
0x58, L"F12",
0x7c, L"F13",
0x7d, L"F14",
0x7e, L"F15",
0x7f, L"F16",
0x80, L"F17",
0x81, L"F18",
0x82, L"F19",
0x83, L"F20",
0x84, L"F21",
0x85, L"F22",
0x86, L"F23",
0x87, L"F24",
0 , NULL
};
static ALLOC_SECTION_LDATA VSC_LPWSTR aKeyNamesExt[] = {
0x1c, L"Num Enter",
0x1d, L"Right Ctrl",
0x35, L"Num /",
0x37, L"Prnt Scrn",
0x38, L"Right Alt",
0x45, L"Num Lock",
0x46, L"Break",
0x47, L"Home",
0x48, L"Up",
0x49, L"Page Up",
0x4b, L"Left",
0x4d, L"Right",
0x4f, L"End",
0x50, L"Down",
0x51, L"Page Down",
0x52, L"Insert",
0x53, L"Delete",
0x54, L"<00>",
0x56, L"Help",
0x5b, L"Left Windows",
0x5c, L"Right Windows",
0x5d, L"Application",
0 , NULL
};
static ALLOC_SECTION_LDATA DEADKEY_LPWSTR aKeyNamesDead[] = {
KALAMINE::DEAD_KEY_INDEX
NULL
};
static ALLOC_SECTION_LDATA DEADKEY aDeadKey[] = {
KALAMINE::DEAD_KEYS
0, 0
};
static ALLOC_SECTION_LDATA KBDTABLES KbdTables = {
/*
* Modifier keys
*/
&CharModifiers,
/*
* Characters tables
*/
aVkToWcharTable,
/*
* Diacritics
*/
aDeadKey,
/*
* Names of Keys
*/
aKeyNames,
aKeyNamesExt,
aKeyNamesDead,
/*
* Scan codes to Virtual Keys
*/
ausVK,
sizeof(ausVK) / sizeof(ausVK[0]),
aE0VscToVk,
aE1VscToVk,
/*
* Locale-specific special processing
*/
MAKELONG(KLLF_ALTGR, KBD_VERSION),
/*
* Ligatures
*/
0,
0,
NULL
};
PKBDTABLES KbdLayerDescriptor(VOID)
{
return &KbdTables;
}
kalamine-0.38/kalamine/templates/full.RC 0000664 0000000 0000000 00000002144 14650261713 0020202 0 ustar 00root root 0000000 0000000 #include "winver.h"
1 VERSIONINFO
FILEVERSION ${rc_version}
PRODUCTVERSION ${rc_version}
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x40004L
FILETYPE VFT_DLL
FILESUBTYPE VFT2_DRV_KEYBOARD
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "000004B0"
BEGIN
VALUE "CompanyName", "${author}\0"
VALUE "FileDescription", "${name} Keyboard Layout\0"
VALUE "FileVersion", "${rc_version}\0"
VALUE "InternalName", "${name8} (3.40)\0"
VALUE "ProductName","Created by Kalamine\0"
VALUE "Release Information","Created by Kalamine\0"
VALUE "LegalCopyright", "(c) 2024 ${author}\0"
VALUE "OriginalFilename","${name8}\0"
VALUE "ProductVersion", "${rc_version}\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0000, 0x04B0
END
END
STRINGTABLE DISCARDABLE
LANGUAGE 9, 1
BEGIN
1200 "${locale}"
END
STRINGTABLE DISCARDABLE
LANGUAGE 9, 1
BEGIN
1000 "${name}"
END
STRINGTABLE DISCARDABLE
LANGUAGE 9, 1
BEGIN
1100 "${description} ${version}"
END
kalamine-0.38/kalamine/templates/full.ahk 0000664 0000000 0000000 00000005574 14650261713 0020453 0 ustar 00root root 0000000 0000000 ; ${KALAMINE}
#NoEnv
#Persistent
#InstallKeybdHook
#SingleInstance, force
#MaxThreadsBuffer
#MaxThreadsPerHotKey 3
#MaxHotkeysPerInterval 300
#MaxThreads 20
SendMode Event ; either Event or Input
SetKeyDelay, -1
SetBatchLines, -1
Process, Priority, , R
SetWorkingDir, %A_ScriptDir%
StringCaseSense, On
;-------------------------------------------------------------------------------
; On/Off Switch
;-------------------------------------------------------------------------------
global Active := True
HideTrayTip() {
TrayTip ; Attempt to hide it the normal way.
if SubStr(A_OSVersion,1,3) = "10." {
Menu Tray, NoIcon
Sleep 200 ; It may be necessary to adjust this sleep.
Menu Tray, Icon
}
}
ShowTrayTip() {
title := "${name}"
text := Active ? "ON" : "OFF"
HideTrayTip()
TrayTip, %title% , %text%, 1, 0x31
SetTimer, HideTrayTip, -1500
}
RAlt & Alt::
Alt & RAlt::
global Active
Active := !Active
ShowTrayTip()
return
#If Active
SetTimer, ShowTrayTip, -1000 ; not working
;-------------------------------------------------------------------------------
; DeadKey Helpers
;-------------------------------------------------------------------------------
global DeadKey := ""
; Check CapsLock status, upper the char if needed and send the char
SendChar(char) {
if % GetKeyState("CapsLock", "T") {
if (StrLen(char) == 6) {
; we have something in the form of `U+NNNN `
; Change it to 0xNNNN so it can be passed to `Chr` function
char := Chr("0x" SubStr(char, 3, 4))
}
StringUpper, char, char
}
Send, {%char%}
}
DoTerm(base:="") {
global DeadKey
term := SubStr(DeadKey, 2, 1)
Send, {%term%}
SendChar(base)
DeadKey := ""
}
DoAction(action:="") {
global DeadKey
if (action == "U+0020") {
Send, {SC39}
DeadKey := ""
}
else if (StrLen(action) != 2) {
SendChar(action)
DeadKey := ""
}
else if (action == DeadKey) {
DoTerm(SubStr(DeadKey, 2, 1))
}
else {
DeadKey := action
}
}
SendKey(base, deadkeymap) {
if (!DeadKey) {
DoAction(base)
}
else if (deadkeymap.HasKey(DeadKey)) {
DoAction(deadkeymap[DeadKey])
}
else {
DoTerm(base)
}
}
;-------------------------------------------------------------------------------
; Base
;-------------------------------------------------------------------------------
KALAMINE::LAYOUT
;-------------------------------------------------------------------------------
; AltGr
;-------------------------------------------------------------------------------
KALAMINE::ALTGR
; Special Keys
$<^>!Esc:: Send {SC01}
$<^>!End:: Send {SC4f}
$<^>!Home:: Send {SC47}
$<^>!Delete:: Send {SC53}
$<^>!Backspace:: Send {SC0e}
;-------------------------------------------------------------------------------
; Ctrl
;-------------------------------------------------------------------------------
KALAMINE::SHORTCUTS
kalamine-0.38/kalamine/templates/full.keylayout 0000664 0000000 0000000 00000041603 14650261713 0021727 0 ustar 00root root 0000000 0000000
KALAMINE::LAYER_0
KALAMINE::LAYER_1
KALAMINE::LAYER_2
KALAMINE::LAYER_3
KALAMINE::LAYER_4
KALAMINE::ACTIONS
KALAMINE::TERMINATORS
kalamine-0.38/kalamine/templates/full.klc 0000664 0000000 0000000 00000003376 14650261713 0020457 0 ustar 00root root 0000000 0000000 // ${KALAMINE}
//
// File : ${fileName}.klc
// Encoding : ${encoding=utf-8, with BOM}
// Project page : ${url}
// Author : ${author}
// Version : ${version}
// License : ${license}
//
// ${description}
//
KBD ${name8} "${name}"
COPYRIGHT "(c) 2010-2024 ${author}"
COMPANY "${author}"
LOCALENAME "${locale}"
LOCALEID "${localeid}"
VERSION ${version}
// Base layer + dead key
// KALAMINE::GEOMETRY_base
// AltGr layer
// KALAMINE::GEOMETRY_altgr
SHIFTSTATE
0 // Column 4
1 // Column 5: Shift
2 // Column 6: Ctrl
3 // Column 7: Shift Ctrl
6 // Column 8: Ctrl Alt
7 // Column 9: Shift Ctrl Alt
LAYOUT //{{{
// an extra '@' at the end is a dead key
//SC VK_ Cap 0 1 2 3 6 7
KALAMINE::LAYOUT
53 DECIMAL 0 002e 002e -1 -1 -1 -1 // FULL STOP, FULL STOP, , ,
//}}}
KALAMINE::DEAD_KEYS
KEYNAME //{{{
01 Esc
0e Backspace
0f Tab
1c Enter
1d Ctrl
2a Shift
36 "Right Shift"
37 "Num *"
38 Alt
39 Space
3a "Caps Lock"
3b F1
3c F2
3d F3
3e F4
3f F5
40 F6
41 F7
42 F8
43 F9
44 F10
45 Pause
46 "Scroll Lock"
47 "Num 7"
48 "Num 8"
49 "Num 9"
4a "Num -"
4b "Num 4"
4c "Num 5"
4d "Num 6"
4e "Num +"
4f "Num 1"
50 "Num 2"
51 "Num 3"
52 "Num 0"
53 "Num Del"
54 "Sys Req"
57 F11
58 F12
7c F13
7d F14
7e F15
7f F16
80 F17
81 F18
82 F19
83 F20
84 F21
85 F22
86 F23
87 F24
//}}}
KEYNAME_EXT //{{{
1c "Num Enter"
1d "Right Ctrl"
35 "Num /"
37 "Prnt Scrn"
38 "Right Alt"
45 "Num Lock"
46 Break
47 Home
48 Up
49 "Page Up"
4b Left
4d Right
4f End
50 Down
51 "Page Down"
52 Insert
53 Delete
54 <00>
56 Help
5b "Left Windows"
5c "Right Windows"
5d Application
//}}}
KEYNAME_DEAD //{{{
KALAMINE::DEAD_KEY_INDEX
//}}}
DESCRIPTIONS
0409 ${description}
LANGUAGENAMES
0409 French (France)
ENDKBD
// vim: ft=xkb:ts=12:fdm=marker:fmr=//{{{,}}}:nowrap
kalamine-0.38/kalamine/templates/full.xkb_keymap 0000664 0000000 0000000 00000001537 14650261713 0022035 0 ustar 00root root 0000000 0000000 // ${KALAMINE}
//
// This is a standalone XKB keymap file. To apply this keymap, use:
// xkbcomp -w9 ${fileName}.xkb_keymap $DISPLAY
//
// DO NOT COPY THIS INTO xkb/symbols: THIS WOULD MESS UP YOUR XKB CONFIG.
//
// File : ${fileName}.xkb_keymap
// Project page : ${url}
// Author : ${author}
// Version : ${version}
// License : ${license}
//
// ${description}
//
xkb_keymap {
xkb_keycodes { include "evdev" };
xkb_types { include "complete" };
xkb_compatibility { include "complete" };
// KALAMINE::GEOMETRY_full
partial alphanumeric_keys modifier_keys
xkb_symbols "${variant}" {
include "pc"
include "inet(evdev)"
include "level3(ralt_switch)"
name[group1]= "${description}";
key.type[group1] = "FOUR_LEVEL";
KALAMINE::LAYOUT
};
};
// vim: ft=xkb:fdm=indent:ts=2:nowrap
kalamine-0.38/kalamine/templates/full.xkb_symbols 0000664 0000000 0000000 00000001140 14650261713 0022225 0 ustar 00root root 0000000 0000000 // ${KALAMINE}
//
//# This XKB symbols file should be copied to:
//# /usr/share/X11/xkb/symbols/custom
//# or
//# $XKB_CONFIG_ROOT/symbols/custom
//#
//# File : ${fileName}.xkb_symbols
// Project page : ${url}
// Author : ${author}
// Version : ${version}
// License : ${license}
//
// ${description}
//
// KALAMINE::GEOMETRY_full
partial alphanumeric_keys modifier_keys
xkb_symbols "${variant}" {
name[group1]= "${description}";
key.type[group1] = "FOUR_LEVEL";
KALAMINE::LAYOUT
include "level3(ralt_switch)"
};
//# vim: ft=xkb:fdm=indent:ts=4:nowrap
kalamine-0.38/kalamine/templates/full_1dk.xkb_keymap 0000664 0000000 0000000 00000003133 14650261713 0022566 0 ustar 00root root 0000000 0000000 // ${KALAMINE}
//
// This is a standalone XKB keymap file. To apply this keymap, use:
// xkbcomp -w9 ${fileName}.xkb_keymap $DISPLAY
//
// DO NOT COPY THIS INTO xkb/symbols: THIS WOULD MESS UP YOUR XKB CONFIG.
//
// File : ${fileName}.xkb_keymap
// Project page : ${url}
// Author : ${author}
// Version : ${version}
// License : ${license}
//
// ${description}
//
xkb_keymap {
xkb_keycodes { include "evdev" };
xkb_types { include "complete" };
xkb_compatibility { include "complete" };
// Base layer + dead key
// KALAMINE::GEOMETRY_base
// AltGr layer
// KALAMINE::GEOMETRY_altgr
partial alphanumeric_keys modifier_keys
xkb_symbols "${variant}" {
include "pc"
include "inet(evdev)"
// The “OneDeadKey” is an ISO_Level3_Latch, i.e. a “dead AltGr” key.
// This is the only way to have a multi-purpose dead key with XKB.
// The real AltGr key should be an ISO_Level5_Switch; however,
// ISO_Level5_Switch does not work as expected when applying this layout
// with xkbcomp, so let’s use two groups instead and make the AltGr key a
// group selector.
name[group1]= "${description}";
name[group2]= "AltGr";
key.type[group1] = "FOUR_LEVEL";
key.type[group2] = "TWO_LEVEL";
KALAMINE::LAYOUT
// AltGr
// Note: the `ISO_Level5_Latch` here is meaningless but helps with Chromium.
key {
type = "TWO_LEVEL",
symbols = [ ISO_Level5_Latch, ISO_Level5_Latch ],
actions = [ SetGroup(group=2), SetGroup(group=2) ]
};
};
};
// vim: ft=xkb:fdm=indent:ts=2:nowrap
kalamine-0.38/kalamine/templates/full_1dk.xkb_symbols 0000664 0000000 0000000 00000002014 14650261713 0022765 0 ustar 00root root 0000000 0000000 // ${KALAMINE}
//
//# This XKB symbols file should be copied to:
//# /usr/share/X11/xkb/symbols/custom
//# or
//# $XKB_CONFIG_ROOT/symbols/custom
//#
//# File : ${fileName}.xkb_symbols
// Project page : ${url}
// Author : ${author}
// Version : ${version}
// License : ${license}
//
// ${description}
//
// Base layer + dead key
// KALAMINE::GEOMETRY_base
//
// AltGr layer
// KALAMINE::GEOMETRY_altgr
partial alphanumeric_keys modifier_keys
xkb_symbols "${variant}" {
name[group1]= "${description}";
key.type[group1] = "EIGHT_LEVEL";
KALAMINE::LAYOUT
// The AltGr key is an ISO_Level3_Shift:
include "level3(ralt_switch)"
// The “OneDeadKey” is an ISO_Level5_Latch, which is activated by this:
// (note: MDSW [Mode_switch] is an alias for LVL5 on recent versions of XKB)
replace key {
type[Group1] = "ONE_LEVEL",
symbols[Group1] = [ ISO_Level5_Shift ]
};
modifier_map Mod3 { };
};
//# vim: ft=xkb:fdm=indent:ts=4:nowrap
kalamine-0.38/kalamine/templates/x-keyboard.svg 0000664 0000000 0000000 00000130464 14650261713 0021607 0 ustar 00root root 0000000 0000000
kalamine-0.38/kalamine/utils.py 0000664 0000000 0000000 00000006760 14650261713 0016536 0 ustar 00root root 0000000 0000000 import pkgutil
from dataclasses import dataclass
from enum import IntEnum
from typing import Dict, List, Optional
import yaml
def hex_ord(char: str) -> str:
return hex(ord(char))[2:].zfill(4)
def lines_to_text(lines: List[str], indent: str = "") -> str:
"""
From a list lines of string, produce a string concatenating the elements
of lines indented by prepending indent and followed by a new line.
Example: lines_to_text(["one", "two", "three"], " ") returns
' one\n two\n three'
"""
out = ""
for line in lines:
if len(line):
out += indent + line
out += "\n"
return out[:-1]
def text_to_lines(text: str) -> List[str]:
"""Split given text into lines"""
return text.split("\n")
def load_data(filename: str) -> Dict:
descriptor = pkgutil.get_data(__package__, f"data/{filename}.yaml")
if not descriptor:
return {}
return yaml.safe_load(descriptor.decode("utf-8"))
class Layer(IntEnum):
"""A layer designation."""
BASE = 0
SHIFT = 1
ODK = 2
ODK_SHIFT = 3
ALTGR = 4
ALTGR_SHIFT = 5
def next(self) -> "Layer":
"""The next layer in the layer ordering."""
return Layer(int(self) + 1)
def necromance(self) -> "Layer":
"""Remove the effect of the dead key if any."""
if self == Layer.ODK:
return Layer.BASE
elif self == Layer.ODK_SHIFT:
return Layer.SHIFT
return self
def upper_key(letter: Optional[str], blank_if_obvious: bool = True) -> str:
"""This is used for presentation purposes: in a key, the upper character
becomes blank if it's an obvious uppercase version of the base character."""
if letter is None:
return " "
custom_alpha = {
"\u00df": "\u1e9e", # ß ẞ
"\u007c": "\u00a6", # | ¦
"\u003c": "\u2264", # < ≤
"\u003e": "\u2265", # > ≥
"\u2020": "\u2021", # † ‡
"\u2190": "\u21d0", # ← ⇐
"\u2191": "\u21d1", # ↑ ⇑
"\u2192": "\u21d2", # → ⇒
"\u2193": "\u21d3", # ↓ ⇓
"\u00b5": " ", # µ (to avoid getting `Μ` as uppercase)
}
if letter in custom_alpha:
return custom_alpha[letter]
if len(letter) == 1 and letter.upper() != letter.lower():
return letter.upper()
# dead key or non-letter character
return " " if blank_if_obvious else letter
@dataclass
class DeadKeyDescr:
char: str
name: str
base: str
alt: str
alt_space: str
alt_self: str
DEAD_KEYS = [DeadKeyDescr(**data) for data in load_data("dead_keys")]
DK_INDEX = {}
for dk in DEAD_KEYS:
DK_INDEX[dk.char] = dk
SCAN_CODES = load_data("scan_codes")
ODK_ID = "**" # must match the value in dead_keys.yaml
LAYER_KEYS = [
"- Digits",
"ae01",
"ae02",
"ae03",
"ae04",
"ae05",
"ae06",
"ae07",
"ae08",
"ae09",
"ae10",
"- Letters, first row",
"ad01",
"ad02",
"ad03",
"ad04",
"ad05",
"ad06",
"ad07",
"ad08",
"ad09",
"ad10",
"- Letters, second row",
"ac01",
"ac02",
"ac03",
"ac04",
"ac05",
"ac06",
"ac07",
"ac08",
"ac09",
"ac10",
"- Letters, third row",
"ab01",
"ab02",
"ab03",
"ab04",
"ab05",
"ab06",
"ab07",
"ab08",
"ab09",
"ab10",
"- Pinky keys",
"ae11",
"ae12",
"ae13",
"ad11",
"ad12",
"ac11",
"ab11",
"tlde",
"bksl",
"lsgt",
"- Space bar",
"spce",
]
kalamine-0.38/kalamine/www/ 0000775 0000000 0000000 00000000000 14650261713 0015637 5 ustar 00root root 0000000 0000000 kalamine-0.38/kalamine/www/demo.js 0000664 0000000 0000000 00000005477 14650261713 0017136 0 ustar 00root root 0000000 0000000 window.addEventListener('DOMContentLoaded', () => {
'use strict'; // eslint-disable-line
const keyboard = document.querySelector('x-keyboard');
const input = document.querySelector('input');
const geometry = document.querySelector('select');
if (!keyboard.layout) {
console.warn('web components are not supported');
return; // the web component has not been loaded
}
fetch(keyboard.getAttribute('src'))
.then(response => response.json())
.then(data => {
const shape = angle_mod ? "iso" : data.geometry.replace('ergo', 'ol60').toLowerCase();
keyboard.setKeyboardLayout(data.keymap, data.deadkeys, shape);
geometry.value = shape;
});
geometry.onchange = (event) => {
keyboard.geometry = event.target.value;
};
/**
* Keyboard highlighting & layout emulation
*/
// required to work around a Chrome bug, see the `keyup` listener below
const pressedKeys = {};
// highlight keyboard keys and emulate the selected layout
input.onkeydown = (event) => {
pressedKeys[event.code] = true;
const value = keyboard.keyDown(event);
if (value) {
event.target.value += value;
} else if (event.code === 'Enter') { // clear text input on
event.target.value = '';
} else if ((event.code === 'Tab') || (event.code === 'Escape')) {
setTimeout(close, 100);
} else {
return true; // don't intercept special keys or key shortcuts
}
return false; // event has been consumed, stop propagation
};
input.addEventListener('keyup', (event) => {
if (pressedKeys[event.code]) { // expected behavior
keyboard.keyUp(event);
delete pressedKeys[event.code];
} else {
/**
* We got a `keyup` event for a key that did not trigger any `keydown`
* event first: this is a known bug with "real" dead keys on Chrome.
* As a workaround, emulate a keydown + keyup. This introduces some lag,
* which can result in a typo (especially when the "real" dead key is used
* for an emulated dead key) -- but there's not much else we can do.
*/
event.target.value += keyboard.keyDown(event);
setTimeout(() => keyboard.keyUp(event), 100);
}
});
/**
* When pressing a "real" dead key + key sequence, Firefox and Chrome will
* add the composed character directly to the text input (and nicely trigger
* an `insertCompositionText` or `insertText` input event, respectively).
* Not sure wether this is a bug or not -- but this is not the behavior we
* want for a keyboard layout emulation. The code below works around that.
*/
input.addEventListener('input', (event) => {
if (event.inputType === 'insertCompositionText'
|| event.inputType === 'insertText') {
event.target.value = event.target.value.slice(0, -event.data.length);
}
});
input.focus();
});
kalamine-0.38/kalamine/www/style.css 0000664 0000000 0000000 00000000521 14650261713 0017507 0 ustar 00root root 0000000 0000000 body {
max-width: 64em;
margin: 0 auto;
font-family: sans-serif;
}
input {
width: 100%;
text-align: center;
font-size: 1.5em;
margin-bottom: 1em;
}
a { color: blue; }
input, select { color-scheme: light dark; }
@media (prefers-color-scheme: dark) {
html { background-color: #222; color: #ddd; }
a { color: #99f; }
}
kalamine-0.38/kalamine/www/x-keyboard.js 0000664 0000000 0000000 00000106421 14650261713 0020246 0 ustar 00root root 0000000 0000000 /**
* Keyboard Layout Data
* {
* keymap: {
* 'KeyQ': [ 'q', 'Q' ], // normal, shift, [altGr], [shift+altGr]
* 'KeyP': [ 'p', 'P' ],
* 'Quote': [ '*´', '*¨' ], // dead keys: acute, diaeresis
* ...
* },
* deadkeys: {
* '*´': { 'a': 'á', 'A': 'Á', ... },
* '*¨': { 'a': 'ä', 'A': 'Ä', ... },
* ...
* },
* geometry: 'ansi' // 'ansi', 'iso', 'alt', 'abnt', 'jis', 'ks' (standard)
* // or 'ol60', 'ol50', 'ol40' (ortholinear)
* }
*/
// dead keys are identified with a `*` prefix + the diacritic sign
function isDeadKey(value) {
return value && value.length === 2 && value[0] === '*';
}
/**
* Keyboard hints:
* suggest the most efficient way to type a character or a string.
*/
// return the list of all keys that can output the requested char
function getKeyList(keyMap, char) {
const rv = [];
Object.entries(keyMap).forEach(([ keyID, value ]) => {
const level = value.indexOf(char);
if (level >= 0) {
rv.push({ id: keyID, level });
}
});
return rv.sort((a, b) => a.level > b.level);
}
// return a dictionary of all characters that can be done with a dead key
function getDeadKeyDict(deadKeys) {
const dict = {};
Object.entries(deadKeys).forEach(([ id, dkObj ]) => {
Object.entries(dkObj).forEach(([ base, alt ]) => {
if (!(alt in dict)) {
dict[alt] = [];
}
dict[alt].push({ id, base });
});
});
return dict;
}
// return a sequence of keys that can output the requested string
function getKeySequence(keyMap, dkDict, str = '') {
const rv = [];
Array.from(str).forEach((char) => {
const keys = getKeyList(keyMap, char);
if (keys.length) { // direct access (possibly with Shift / AltGr)
rv.push(keys[0]);
} else if (char in dkDict) { // available with a dead key
const dk = dkDict[char][0];
rv.push(getKeyList(keyMap, dk.id)[0]);
rv.push(getKeyList(keyMap, dk.base)[0]);
} else { // not available
rv.push({});
console.error('char not found:', char); // eslint-disable-line
}
});
return rv;
}
/**
* Modifiers
*/
const MODIFIERS = {
ShiftLeft: false,
ShiftRight: false,
ControlLeft: false,
ControlRight: false,
AltLeft: false,
AltRight: false,
OSLeft: false,
OSRight: false,
};
function getShiftState(modifiers) {
return modifiers.ShiftRight || modifiers.ShiftLeft;
}
function getAltGrState(modifiers, platform) {
if (platform === 'win') {
return modifiers.AltRight || (modifiers.ControlLeft && modifiers.AltLeft);
}
if (platform === 'mac') {
return modifiers.AltRight || modifiers.AltLeft;
}
return modifiers.AltRight;
}
function getModifierLevel(modifiers, platform) {
return (getShiftState(modifiers) ? 1 : 0)
+ (getAltGrState(modifiers, platform) ? 2 : 0);
}
/**
* Keyboard Layout API (public)
*/
function newKeyboardLayout(keyMap = {}, deadKeys = {}, geometry = '') {
const modifiers = { ...MODIFIERS };
const deadKeyDict = getDeadKeyDict(deadKeys);
let pendingDK;
let platform = '';
return {
get keyMap() { return keyMap; },
get deadKeys() { return deadKeys; },
get pendingDK() { return pendingDK; },
get geometry() { return geometry; },
get platform() { return platform; },
set platform(value) { platform = value; },
// modifier state
get modifiers() {
return {
get shift() { return getShiftState(modifiers); },
get altgr() { return getAltGrState(modifiers, platform); },
get level() { return getModifierLevel(modifiers, platform); },
};
},
// keyboard hints
getKey: char => getKeyList(keyMap, char)[0],
getKeySequence: str => getKeySequence(keyMap, deadKeyDict, str),
// keyboard emulation
keyUp: (keyCode) => {
if (keyCode in modifiers) {
modifiers[keyCode] = false;
}
},
keyDown: (keyCode) => {
if (keyCode in modifiers) {
modifiers[keyCode] = true;
}
const key = keyMap[keyCode];
if (!key) {
return '';
}
let value = key[getModifierLevel(modifiers, platform)];
if (pendingDK) {
value = pendingDK[value] || '';
pendingDK = undefined;
}
if (isDeadKey(value)) {
pendingDK = deadKeys[value];
return '';
}
return value || '';
},
};
}
/**
* Styling: colors & dimensions
*/
const KEY_BG = '#f8f8f8';
const SPECIAL_KEY_BG = '#e4e4e4';
const KEY_COLOR = '#333';
const KEY_COLOR_L3 = 'blue';
const KEY_COLOR_L5 = 'green';
const DEAD_KEY_COLOR = 'red';
const KEY_WIDTH = 60; // 1U = 0.75" = 19.05mm = 60px
const KEY_PADDING = 4; // 8px between two key edges
const KEY_RADIUS = 5; // 5px border radius
/**
* Deak Keys
* defined in the Kalamine project: https://github.com/OneDeadKey/kalamine
* identifiers -> symbols dictionary, for presentation purposes
*/
const symbols = {
// diacritics, represented by a space + a combining character
'*`': ' \u0300', // grave
'*´': ' \u0301', // acute
'*^': ' \u0302', // circumflex
'*~': ' \u0303', // tilde
'*¯': ' \u0304', // macron
'*˘': ' \u0306', // breve
'*˙': ' \u0307', // dot above
'*¨': ' \u0308', // diaeresis
'*˚': ' \u030a', // ring above
'*”': ' \u030b', // double acute
'*ˇ': ' \u030c', // caron
'*‟': ' \u030f', // double grave
'*⁻': ' \u0311', // inverted breve
'*.': ' \u0323', // dot below
'*,': ' \u0326', // comma below
'*¸': ' \u0327', // cedilla
'*˛': ' \u0328', // ogonek
// special keys, represented by a smaller single character
// '*/': stroke (no special glyph needed)
// '*µ': greek (no special glyph needed)
// '*¤': currency (no special glyph needed)
'**': '\u2605', // 1dk = Kalamine "one dead key" = multi-purpose dead key
// other dead key identifiers (= two-char strings starting with a `*` sign)
// are not supported by Kalamine, but can still be used with
};
/**
* Enter Key: ISO & ALT
*/
const arc = (xAxisRotation, x, y) => [
`a${KEY_RADIUS},${KEY_RADIUS}`,
xAxisRotation ? '1 0 0' : '0 0 1',
`${KEY_RADIUS * x},${KEY_RADIUS * y}`,
].join(' ');
const lineLength = (length, gap) => {
const offset = 2 * (KEY_PADDING + KEY_RADIUS) - 2 * gap * KEY_PADDING;
return KEY_WIDTH * length - Math.sign(length) * offset;
};
const h = (length, gap = 0, ccw = 0) => {
const l = lineLength(length, gap);
const sign = Math.sign(length);
return `h${l} ${ccw ? arc(1, sign, -sign) : arc(0, sign, sign)}`;
};
const v = (length, gap = 0, ccw = 0) => {
const l = lineLength(length, gap);
const sign = Math.sign(length);
return `v${l} ${ccw ? arc(1, sign, sign) : arc(0, -sign, sign)}`;
};
const M = `M${0.75 * KEY_WIDTH + KEY_RADIUS},-${KEY_WIDTH}`;
const altEnterPath = [
M, h(1.5), v(2.0), h(-2.25), v(-1.0), h(0.75, 1, 1), v(-1.0, 1), 'z',
].join(' ');
const isoEnterPath = [
M, h(1.5), v(2.0), h(-1.25), v(-1.0, 1, 1), h(-0.25, 1), v(-1.0), 'z',
].join(' ');
/**
* DOM-to-Text Utils
*/
const sgml = (nodeName, attributes = {}, children = []) => `<${nodeName} ${
Object.entries(attributes)
.map(([ id, value ]) => {
if (id === 'x' || id === 'y') {
return `${id}="${KEY_WIDTH * Number(value)
- (nodeName === 'text' ? KEY_PADDING : 0)}"`;
}
if (id === 'width' || id === 'height') {
return `${id}="${KEY_WIDTH * Number(value) - 2 * KEY_PADDING}"`;
}
if (id === 'translateX') {
return `transform="translate(${KEY_WIDTH * Number(value)}, 0)"`;
}
return `${id}="${value}"`;
})
.join(' ')
}>${children.join('\n')}${nodeName}>`;
const path = (cname = '', d) => sgml('path', { class: cname, d });
const rect = (cname = '', attributes) => sgml('rect', {
class: cname,
width: 1,
height: 1,
rx: KEY_RADIUS,
ry: KEY_RADIUS,
...attributes,
});
const text = (content, cname = '', attributes) => sgml('text', {
class: cname,
width: 0.50,
height: 0.50,
x: 0.34,
y: 0.78,
...attributes,
}, [content]);
const g = (className, children) => sgml('g', { class: className }, children);
const emptyKey = [ rect(), g('key') ];
const gKey = (className, finger, x, id, children = emptyKey) => sgml('g', {
class: className, finger, id, transform: `translate(${x * KEY_WIDTH}, 0)`,
}, children);
/**
* Keyboard Layout Utils
*/
const keyLevel = (level, label, position) => {
const attrs = { ...position };
const symbol = symbols[label] || '';
const content = symbol || (label || '').slice(-1);
let className = '';
if (level > 4) {
className = 'dk';
} else if (isDeadKey(label)) {
className = `deadKey ${symbol.startsWith(' ') ? 'diacritic' : ''}`;
}
return text(content, `level${level} ${className}`, attrs);
};
// In order not to overload the `alt` layers visually (AltGr & dead keys),
// the `shift` key is displayed only if its lowercase is not `base`.
const altUpperChar = (base, shift) => (shift && base !== shift.toLowerCase()
? shift : '');
function drawKey(element, keyMap) {
const keyChars = keyMap[element.parentNode.id];
if (!keyChars) {
element.innerHTML = '';
return;
}
/**
* What key label should we display when the `base` and `shift` layers have
* the lowercase and uppercase versions of the same letter?
* Most of the time we want the uppercase letter, but there are tricky cases:
* - German:
* 'ß'.toUpperCase() == 'SS'
* 'ẞ'.toLowerCase() == 'ß'
* - Greek:
* 'ς'.toUpperCase() == 'Σ'
* 'σ'.toUpperCase() == 'Σ'
* 'Σ'.toLowerCase() == 'σ'
* 'µ'.toUpperCase() == 'Μ' // micro sign => capital letter MU
* 'μ'.toUpperCase() == 'Μ' // small letter MU => capital letter MU
* 'Μ'.toLowerCase() == 'μ' // capital letter MU => small letter MU
* So if the lowercase version of the `shift` layer does not match the `base`
* layer, we'll show the lowercase letter (e.g. Greek 'ς').
*/
const [ l1, l2, l3, l4 ] = keyChars;
const base = l1.toUpperCase() !== l2 ? l1 : '';
const shift = base || l2.toLowerCase() === l1 ? l2 : l1;
const salt = altUpperChar(l3, l4);
element.innerHTML = `
${keyLevel(1, base, { x: 0.28, y: 0.79 })}
${keyLevel(2, shift, { x: 0.28, y: 0.41 })}
${keyLevel(3, l3, { x: 0.70, y: 0.79 })}
${keyLevel(4, salt, { x: 0.70, y: 0.41 })}
${keyLevel(5, '', { x: 0.70, y: 0.79 })}
${keyLevel(6, '', { x: 0.70, y: 0.41 })}
`;
}
function drawDK(element, keyMap, deadKey) {
const drawChar = (element, content) => {
if (isDeadKey(content)) {
element.classList.add('deadKey', 'diacritic');
element.textContent = content[1];
} else {
element.classList.remove('deadKey', 'diacritic');
element.textContent = content || '';
}
};
const keyChars = keyMap[element.parentNode.id];
if (!keyChars) return;
const alt0 = deadKey[keyChars[0]];
const alt1 = deadKey[keyChars[1]];
drawChar(element.querySelector('.level5'), alt0);
drawChar(element.querySelector('.level6'), altUpperChar(alt0, alt1));
}
/**
* SVG Content
* https://www.w3.org/TR/uievents-code/
* https://commons.wikimedia.org/wiki/File:Physical_keyboard_layouts_comparison_ANSI_ISO_KS_ABNT_JIS.png
*/
const numberRow = g('left', [
gKey('specialKey', 'l5', 0, 'Escape', [
rect('ergo', { width: 1.25 }),
text('⎋', 'ergo'),
]),
gKey('pinkyKey', 'l5', 0, 'Backquote', [
rect('specialKey jis', { width: 1 }),
rect('ansi alt iso', { width: 1 }),
rect('ol60', { width: 1.25 }),
text('半角', 'jis', { x: 0.5, y: 0.4 }), // half-width (hankaku)
text('全角', 'jis', { x: 0.5, y: 0.6 }), // full-width (zenkaku)
text('漢字', 'jis', { x: 0.5, y: 0.8 }), // kanji
g('ansi key'),
]),
gKey('numberKey', 'l5', 1, 'Digit1'),
gKey('numberKey', 'l4', 2, 'Digit2'),
gKey('numberKey', 'l3', 3, 'Digit3'),
gKey('numberKey', 'l2', 4, 'Digit4'),
gKey('numberKey', 'l2', 5, 'Digit5'),
]) + g('right', [
gKey('numberKey', 'r2', 6, 'Digit6'),
gKey('numberKey', 'r2', 7, 'Digit7'),
gKey('numberKey', 'r3', 8, 'Digit8'),
gKey('numberKey', 'r4', 9, 'Digit9'),
gKey('numberKey', 'r5', 10, 'Digit0'),
gKey('pinkyKey', 'r5', 11, 'Minus'),
gKey('pinkyKey', 'r5', 12, 'Equal', [
rect('ansi', { width: 1.00 }),
rect('ol60', { width: 1.25 }),
g('key'),
]),
gKey('pinkyKey', 'r5', 13, 'IntlYen'),
gKey('specialKey', 'r5', 13, 'Backspace', [
rect('ansi', { width: 2 }),
rect('ol60', { width: 1.25, height: 2, y: -1 }),
rect('ol40 ol50', { width: 1.25 }),
rect('alt', { x: 1 }),
text('⌫', 'ansi'),
text('⌫', 'ergo'),
text('⌫', 'alt', { translateX: 1 }),
]),
]);
const letterRow1 = g('left', [
gKey('specialKey', 'l5', 0, 'Tab', [
rect('', { width: 1.5 }),
rect('ergo', { width: 1.25 }),
text('↹'),
text('↹', 'ergo'),
]),
gKey('letterKey', 'l5', 1.5, 'KeyQ'),
gKey('letterKey', 'l4', 2.5, 'KeyW'),
gKey('letterKey', 'l3', 3.5, 'KeyE'),
gKey('letterKey', 'l2', 4.5, 'KeyR'),
gKey('letterKey', 'l2', 5.5, 'KeyT'),
]) + g('right', [
gKey('letterKey', 'r2', 6.5, 'KeyY'),
gKey('letterKey', 'r2', 7.5, 'KeyU'),
gKey('letterKey', 'r3', 8.5, 'KeyI'),
gKey('letterKey', 'r4', 9.5, 'KeyO'),
gKey('letterKey', 'r5', 10.5, 'KeyP'),
gKey('pinkyKey', 'r5', 11.5, 'BracketLeft'),
gKey('pinkyKey', 'r5', 12.5, 'BracketRight', [
rect('ansi', { width: 1.00 }),
rect('ol60', { width: 1.25 }),
g('key'),
]),
gKey('pinkyKey', 'r5', 13.5, 'Backslash', [
rect('ansi', { width: 1.5 }),
rect('iso ol60'),
g('key'),
]),
]);
const letterRow2 = g('left', [
gKey('specialKey', 'l5', 0, 'CapsLock', [
rect('', { width: 1.75 }),
text('⇪', 'ansi'),
text('英数', 'jis', { x: 0.45 }), // alphanumeric (eisū)
]),
gKey('letterKey homeKey', 'l5', 1.75, 'KeyA'),
gKey('letterKey homeKey', 'l4', 2.75, 'KeyS'),
gKey('letterKey homeKey', 'l3', 3.75, 'KeyD'),
gKey('letterKey homeKey', 'l2', 4.75, 'KeyF'),
gKey('letterKey', 'l2', 5.75, 'KeyG'),
]) + g('right', [
gKey('letterKey', 'r2', 6.75, 'KeyH'),
gKey('letterKey homeKey', 'r2', 7.75, 'KeyJ'),
gKey('letterKey homeKey', 'r3', 8.75, 'KeyK'),
gKey('letterKey homeKey', 'r4', 9.75, 'KeyL'),
gKey('letterKey homeKey', 'r5', 10.75, 'Semicolon'),
gKey('pinkyKey', 'r5', 11.75, 'Quote'),
gKey('specialKey', 'r5', 12.75, 'Enter', [
path('alt', altEnterPath),
path('iso', isoEnterPath),
rect('ansi', { width: 2.25 }),
rect('ol60', { width: 1.25, height: 2, y: -1 }),
rect('ol40 ol50', { width: 1.25 }),
text('⏎', 'ansi alt ergo'),
text('⏎', 'iso', { translateX: 1 }),
]),
]);
const letterRow3 = g('left', [
gKey('specialKey', 'l5', 0, 'ShiftLeft', [
rect('ansi alt', { width: 2.25 }),
rect('iso', { width: 1.25 }),
rect('ol50 ol60', { width: 1.25, height: 2, y: -1 }),
rect('ol40', { width: 1.25 }),
text('⇧'),
text('⇧', 'ergo'),
]),
gKey('letterKey', 'l5', 1.25, 'IntlBackslash'),
gKey('letterKey', 'l5', 2.25, 'KeyZ'),
gKey('letterKey', 'l4', 3.25, 'KeyX'),
gKey('letterKey', 'l3', 4.25, 'KeyC'),
gKey('letterKey', 'l2', 5.25, 'KeyV'),
gKey('letterKey', 'l2', 6.25, 'KeyB'),
]) + g('right', [
gKey('letterKey', 'r2', 7.25, 'KeyN'),
gKey('letterKey', 'r2', 8.25, 'KeyM'),
gKey('letterKey', 'r3', 9.25, 'Comma'),
gKey('letterKey', 'r4', 10.25, 'Period'),
gKey('letterKey', 'r5', 11.25, 'Slash'),
gKey('pinkyKey', 'r5', 12.25, 'IntlRo'),
gKey('specialKey', 'r5', 12.25, 'ShiftRight', [
rect('ansi', { width: 2.75 }),
rect('abnt', { width: 1.75, x: 1 }),
rect('ol50 ol60', { width: 1.25, height: 2, y: -1 }),
rect('ol40', { width: 1.25 }),
text('⇧', 'ansi'),
text('⇧', 'ergo'),
text('⇧', 'abnt', { translateX: 1 }),
]),
]);
const nonIcon = { x: 0.25, 'text-anchor': 'start' };
const baseRow = g('left', [
gKey('specialKey', 'l5', 0, 'ControlLeft', [
rect('', { width: 1.25 }),
rect('ergo', { width: 1.25 }),
text('Ctrl', 'win gnu', nonIcon),
text('⌃', 'mac'),
]),
gKey('specialKey', 'l1', 1.25, 'MetaLeft', [
rect('', { width: 1.25 }),
rect('ergo', { width: 1.50 }),
text('Win', 'win', nonIcon),
text('Super', 'gnu', nonIcon),
text('⌘', 'mac'),
]),
gKey('specialKey', 'l1', 2.50, 'AltLeft', [
rect('', { width: 1.25 }),
rect('ergo', { width: 1.50 }),
text('Alt', 'win gnu', nonIcon),
text('⌥', 'mac'),
]),
gKey('specialKey', 'l1', 3.75, 'Lang2', [
rect(),
text('한자', '', { x: 0.4 }), // hanja
]),
gKey('specialKey', 'l1', 3.75, 'NonConvert', [
rect(),
text('無変換', '', { x: 0.5 }), // muhenkan
]),
]) + gKey('homeKey', 'm1', 3.75, 'Space', [
rect('ansi', { width: 6.25 }),
rect('ol60', { width: 5.50, x: -1 }),
rect('ol50 ol40', { width: 4.50 }),
rect('ks', { width: 4.25, x: 1 }),
rect('jis', { width: 3.25, x: 1 }),
]) + g('right', [
gKey('specialKey', 'r1', 8.00, 'Convert', [
rect(),
text('変換', '', { x: 0.5 }), // henkan
]),
gKey('specialKey', 'r1', 9.00, 'KanaMode', [
rect(),
text('カタカナ', '', { x: 0.5, y: 0.4 }), // katakana
text('ひらがな', '', { x: 0.5, y: 0.6 }), // hiragana
text('ローマ字', '', { x: 0.5, y: 0.8 }), // romaji
]),
gKey('specialKey', 'r1', 9.00, 'Lang1', [
rect(),
text('한/영', '', { x: 0.4 }), // han/yeong
]),
gKey('specialKey', 'r1', 10.00, 'AltRight', [
rect('', { width: 1.25 }),
rect('ergo', { width: 1.50 }),
text('Alt', 'win gnu', nonIcon),
text('⌥', 'mac'),
]),
gKey('specialKey', 'r1', 11.50, 'MetaRight', [
rect('', { width: 1.25 }),
rect('ergo', { width: 1.50 }),
text('Win', 'win', nonIcon),
text('Super', 'gnu', nonIcon),
text('⌘', 'mac'),
]),
gKey('specialKey', 'r5', 12.50, 'ContextMenu', [
rect('', { width: 1.25 }),
rect('ergo'),
text('☰'),
text('☰', 'ol60'),
]),
gKey('specialKey', 'r5', 13.75, 'ControlRight', [
rect('', { width: 1.25 }),
rect('ergo', { width: 1.25 }),
text('Ctrl', 'win gnu', nonIcon),
text('⌃', 'mac'),
]),
]);
const svgContent = `
`;
const translate = (x = 0, y = 0, offset) => {
const dx = KEY_WIDTH * x + (offset ? KEY_PADDING : 0);
const dy = KEY_WIDTH * y + (offset ? KEY_PADDING : 0);
return `{ transform: translate(${dx}px, ${dy}px); }`;
};
const main = `
rect, path {
stroke: #666;
stroke-width: .5px;
fill: ${KEY_BG};
}
.specialKey,
.specialKey rect,
.specialKey path {
fill: ${SPECIAL_KEY_BG};
}
text {
fill: ${KEY_COLOR};
font: normal 20px sans-serif;
text-align: center;
}
#Backspace text {
font-size: 12px;
}
`;
// keyboard geometry: ANSI, ISO, ABNT, ALT
const classicGeometry = `
#Escape { display: none; }
#row_AE ${translate(0, 0, true)}
#row_AD ${translate(0, 1, true)}
#row_AC ${translate(0, 2, true)}
#row_AB ${translate(0, 3, true)}
#row_AA ${translate(0, 4, true)}
/* Backslash + Enter */
#Enter path.alt,
#Enter .iso,
#Backslash .iso,
.alt #Enter rect.ansi,
.iso #Enter rect.ansi,
.iso #Enter text.ansi,
.alt #Backslash .ansi,
.iso #Backslash .ansi { display: none; }
#Enter text.ansi,
.alt #Enter .alt,
.iso #Enter .iso,
.iso #Backslash .iso { display: block; }
.iso #Backslash ${translate(12.75, 1)}
.alt #Backslash ${translate(13.0, -1)}
/* Backspace + IntlYen */
#IntlYen, #Backspace .alt,
.intlYen #Backspace .ansi { display: none; }
.intlYen #Backspace .alt,
.intlYen #IntlYen { display: block; }
/* ShiftLeft + IntlBackslash */
#IntlBackslash, #ShiftLeft .iso,
.intlBackslash #ShiftLeft .ansi { display: none; }
.intlBackslash #ShiftLeft .iso,
.intlBackslash #IntlBackslash { display: block; }
/* ShiftRight + IntlRo */
#IntlRo, #ShiftRight .abnt,
.intlRo #ShiftRight .ansi { display: none; }
.intlRo #ShiftRight .abnt,
.intlRo #IntlRo { display: block; }
`;
// ortholinear geometry: TypeMatrix (60%), OLKB (50%, 40%)
const orthoGeometry = `
.specialKey .ergo,
.specialKey .ol60,
.specialKey .ol50,
.specialKey .ol40,
#Space .ol60,
#Space .ol50,
#Space .ol40,
#Backquote .ol60,
#BracketRight .ol60,
#Equal .ol60,
.ergo #CapsLock,
.ergo #Space rect,
.ergo #Backslash rect,
.ergo .specialKey rect,
.ergo .specialKey text { display: none; }
.ol50 #Escape,
.ol40 #Escape,
.ol60 #Space .ol60,
.ol50 #Space .ol50,
.ol40 #Space .ol40,
.ol60 #Backquote .ol60,
.ol60 #BracketRight .ol60,
.ol60 #Backslash .ol60,
.ol60 #Equal .ol60,
.ol60 .specialKey .ol60,
.ol50 .specialKey .ol50,
.ol40 .specialKey .ol40,
.ergo .specialKey .ergo { display: block; }
.ol50 .pinkyKey, .ol50 #ContextMenu,
.ol40 .pinkyKey, .ol40 #ContextMenu,
.ol40 #row_AE .numberKey { display: none; }
.ergo #row_AE ${translate(1.50, 0, true)}
.ergo #row_AD ${translate(1.00, 1, true)}
.ergo #row_AC ${translate(0.75, 2, true)}
.ergo #row_AB ${translate(0.25, 3, true)}
.ergo #Tab ${translate(0.25)}
.ergo #ShiftLeft ${translate(1.00)}
.ergo #ControlLeft ${translate(1.25)}
.ergo #MetaLeft ${translate(2.50)}
.ergo #AltLeft ${translate(4.00)}
.ergo #Space ${translate(5.25)}
.ergo #AltRight ${translate(9.00)}
.ergo #MetaRight ${translate(10.5)}
.ergo #ControlRight ${translate(12.5)}
.ergo .left ${translate(-0.25)}
.ergo .right ${translate(0.25)}
.ol60 .left ${translate(-1.25)}
.ol60 #ControlRight ${translate(13.50)}
.ol60 #Backquote ${translate(-0.25)}
.ol60 #ShiftRight ${translate(13.25)}
.ol60 #ContextMenu ${translate(12.50)}
.ol60 #Backslash ${translate(11.50, 2)}
.ol60 #Backspace ${translate(4.625, 1)}
.ol60 #Enter ${translate(5.375, 1)}
.ol50 #Escape ${translate(-0.25)}
.ol50 #Backspace ${translate(11.00)}
.ol50 #Enter ${translate(11.75, -1)}
.ol40 #Escape ${translate(-0.25, 2)}
.ol40 #Backspace ${translate(11.00, 1)}
.ol40 #Enter ${translate(11.75, 0)}
[platform="gnu"].ergo .specialKey .win,
[platform="gnu"].ergo .specialKey .mac,
[platform="win"].ergo .specialKey .gnu,
[platform="win"].ergo .specialKey .mac { display: none; }
.ergo .specialKey .mac,
[platform="gnu"].ergo .specialKey .gnu,
[platform="win"].ergo .specialKey .win { display: block; }
/* swap Alt/Meta for MacOSX */
[platform="gnu"].ergo #MetaLeft,
[platform="win"].ergo #MetaLeft,
.ergo #AltLeft ${translate(2.5)}
[platform="gnu"].ergo #AltLeft,
[platform="win"].ergo #AltLeft,
.ergo #MetaLeft ${translate(4.0)}
[platform="gnu"].ergo #AltRight,
[platform="win"].ergo #AltRight,
.ergo #MetaRight ${translate(9.5)}
[platform="gnu"].ergo #MetaRight,
[platform="win"].ergo #MetaRight,
.ergo #AltRight ${translate(11.0)}
`;
// Korean + Japanese input systems
const cjkKeys = `
#NonConvert, #Convert, #KanaMode,
#Lang1, #Lang2,
#Space .jis,
#Space .ks,
.ks #Space .ansi,
.ks #Space .jis,
.jis #Space .ansi,
.jis #Space .ks { display: none; }
.ks #Space .ks,
.jis #NonConvert, .jis #Convert, .jis #KanaMode,
.ks #Lang1, .ks #Lang2,
.jis #Space .jis { display: block; }
#Backquote .jis,
#CapsLock .jis,
.jis #Backquote .ansi,
.jis #CapsLock .ansi { display: none; }
.jis #Backquote .jis,
.jis #CapsLock .jis { display: block; }
#Lang1 text,
#Lang2 text,
#Convert text,
#NonConvert text,
.jis #CapsLock text { font-size: 14px; }
#KanaMode text,
.jis #Backquote text { font-size: 10px; }
`;
// Windows / MacOSX / Linux modifiers
const modifiers = `
.specialKey .win,
.specialKey .gnu {
display: none;
font-size: 14px;
}
/* display MacOSX by default */
[platform="gnu"] .specialKey .win,
[platform="gnu"] .specialKey .mac,
[platform="win"] .specialKey .gnu,
[platform="win"] .specialKey .mac { display: none; }
[platform="mac"] .specialKey .mac,
[platform="gnu"] .specialKey .gnu,
[platform="win"] .specialKey .win { display: block; }
/* swap Alt/Meta for MacOSX */
[platform="gnu"] #MetaLeft,
[platform="win"] #MetaLeft, #AltLeft ${translate(1.25)}
[platform="gnu"] #AltLeft,
[platform="win"] #AltLeft, #MetaLeft ${translate(2.50)}
[platform="gnu"] #AltRight,
[platform="win"] #AltRight, #MetaRight ${translate(10.00)}
[platform="gnu"] #MetaRight,
[platform="win"] #MetaRight, #AltRight ${translate(11.25)}
`;
// color themes
const themes = `
g:target rect, .press rect,
g:target path, .press path {
fill: #aad;
}
[theme="reach"] .pinkyKey rect { fill: hsl( 0, 100%, 90%); }
[theme="reach"] .numberKey rect { fill: hsl( 42, 100%, 90%); }
[theme="reach"] .letterKey rect { fill: hsl(122, 100%, 90%); }
[theme="reach"] .homeKey rect { fill: hsl(122, 100%, 75%); }
[theme="reach"] .press rect { fill: #aaf; }
[theme="hints"] [finger="m1"] rect { fill: hsl( 0, 100%, 95%); }
[theme="hints"] [finger="l2"] rect { fill: hsl( 42, 100%, 85%); }
[theme="hints"] [finger="r2"] rect { fill: hsl( 61, 100%, 85%); }
[theme="hints"] [finger="l3"] rect,
[theme="hints"] [finger="r3"] rect { fill: hsl(136, 100%, 85%); }
[theme="hints"] [finger="l4"] rect,
[theme="hints"] [finger="r4"] rect { fill: hsl(200, 100%, 85%); }
[theme="hints"] [finger="l5"] rect,
[theme="hints"] [finger="r5"] rect { fill: hsl(230, 100%, 85%); }
[theme="hints"] .specialKey rect,
[theme="hints"] .specialKey path { fill: ${SPECIAL_KEY_BG}; }
[theme="hints"] .hint rect { fill: #a33; }
[theme="hints"] .press rect { fill: #335; }
[theme="hints"] .press text { fill: #fff; }
[theme="hints"] .hint text {
font-weight: bold;
fill: white;
}
/* dimmed AltGr + bold dead keys */
.level3, .level4 { fill: ${KEY_COLOR_L3}; opacity: .5; }
.level5, .level6 { fill: ${KEY_COLOR_L5}; }
.deadKey {
fill: ${DEAD_KEY_COLOR};
font-size: 14px;
}
.diacritic {
font-size: 20px;
font-weight: bolder;
}
/* hide Level4 (Shift+AltGr) unless AltGr is pressed */
.level4 { display: none; }
.altgr .level4 { display: block; }
/* highlight AltGr + Dead Keys */
.dk .level1, .altgr .level1,
.dk .level2, .altgr .level2 { opacity: 0.25; }
.dk .level5, .altgr .level3,
.dk .level6, .altgr .level4 { opacity: 1; }
.dk .level3,
.dk .level4 { display: none; }
@media (prefers-color-scheme: dark) {
rect, path { stroke: #777; fill: #444; }
.specialKey, .specialKey rect, .specialKey path { fill: #333; }
g:target rect, .press rect, g:target path, .press path { fill: #558; }
text { fill: #bbb; }
.level3, .level4 { fill: #99f; }
.level5, .level6 { fill: #6d6; }
.deadKey { fill: #f44; }
[theme="reach"] .pinkyKey rect { fill: hsl( 0, 20%, 30%); }
[theme="reach"] .numberKey rect { fill: hsl( 35, 25%, 30%); }
[theme="reach"] .letterKey rect { fill: hsl( 61, 30%, 30%); }
[theme="reach"] .homeKey rect { fill: hsl(136, 30%, 30%); }
[theme="reach"] .press rect { fill: #449; }
[theme="hints"] [finger="m1"] rect { fill: hsl( 0, 25%, 30%); }
[theme="hints"] [finger="l2"] rect { fill: hsl( 31, 30%, 30%); }
[theme="hints"] [finger="r2"] rect { fill: hsl( 61, 30%, 30%); }
[theme="hints"] [finger="l3"] rect,
[theme="hints"] [finger="r3"] rect { fill: hsl(136, 30%, 30%); }
[theme="hints"] [finger="l4"] rect,
[theme="hints"] [finger="r4"] rect { fill: hsl(200, 30%, 30%); }
[theme="hints"] [finger="l5"] rect,
[theme="hints"] [finger="r5"] rect { fill: hsl(230, 30%, 30%); }
[theme="hints"] .specialKey rect,
[theme="hints"] .specialKey path { fill: #333; }
[theme="hints"] .hint rect { fill: #a33; }
[theme="hints"] .press rect { fill: #335; }
[theme="hints"] .press text { fill: #fff; }
[theme="hints"] .hint text {
font-weight: bold;
fill: white;
}
}
`;
// export full stylesheet
const style = `
${main}
${classicGeometry}
${orthoGeometry}
${cjkKeys}
${modifiers}
${themes}
`;
/**
* Custom Element
*/
const setFingerAssignment = (root, ansiStyle) => {
(ansiStyle
? ['l5', 'l4', 'l3', 'l2', 'l2', 'r2', 'r2', 'r3', 'r4', 'r5']
: ['l5', 'l5', 'l4', 'l3', 'l2', 'l2', 'r2', 'r2', 'r3', 'r4'])
.forEach((attr, i) => {
root.getElementById(`Digit${(i + 1) % 10}`).setAttribute('finger', attr);
});
};
const getKeyChord = (root, key) => {
if (!key || !key.id) {
return [];
}
const element = root.getElementById(key.id);
const chord = [ element ];
if (key.level > 1) { // altgr
chord.push(root.getElementById('AltRight'));
}
if (key.level % 2) { // shift
chord.push(root.getElementById(element.getAttribute('finger')[0] === 'l'
? 'ShiftRight' : 'ShiftLeft'));
}
return chord;
};
const guessPlatform = () => {
const p = navigator.platform.toLowerCase();
if (p.startsWith('win')) {
return 'win';
}
if (p.startsWith('mac')) {
return 'mac';
}
if (p.startsWith('linux')) {
return 'linux';
}
return '';
};
const template = document.createElement('template');
template.innerHTML = `${svgContent}`;
class Keyboard extends HTMLElement {
constructor() {
super();
this.root = this.attachShadow({ mode: 'open' });
this.root.appendChild(template.content.cloneNode(true));
this._state = {
geometry: this.getAttribute('geometry') || '',
platform: this.getAttribute('platform') || '',
theme: this.getAttribute('theme') || '',
layout: newKeyboardLayout(),
};
this.geometry = this._state.geometry;
this.platform = this._state.platform;
this.theme = this._state.theme;
}
/**
* User Interface: color theme, shape, layout.
*/
get theme() {
return this._state.theme;
}
set theme(value) {
this._state.theme = value;
this.root.querySelector('svg').setAttribute('theme', value);
}
get geometry() {
return this._state.geometry;
}
set geometry(value) {
/**
* Supported geometries (besides ANSI):
* - Euro-style [Enter] key:
* ISO = ANSI + IntlBackslash
* ABNT = ISO + IntlRo + NumpadComma
* JIS = ISO + IntlRo + IntlYen - IntlBackslash
* + NonConvert + Convert + KanaMode
* - Russian-style [Enter] key:
* ALT = ANSI - Backslash + IntlYen
* KS = ALT + Lang1 + Lang2
* - Ortholinear:
* OL60 = TypeMatrix 2030
* OL50 = OLKB Preonic
* OL40 = OLKB Planck
*/
const supportedShapes = {
alt: 'alt intlYen',
ks: 'alt intlYen ks',
jis: 'iso intlYen intlRo jis',
abnt: 'iso intlBackslash intlRo',
iso: 'iso intlBackslash',
ansi: '',
ol60: 'ergo ol60',
ol50: 'ergo ol50',
ol40: 'ergo ol40',
};
if (value && !(value in supportedShapes)) {
return;
}
this._state.geometry = value;
const geometry = value || this.layout.geometry || 'ansi';
const shape = supportedShapes[geometry];
this.root.querySelector('svg').className.baseVal = shape;
setFingerAssignment(this.root, !shape.startsWith('iso'));
}
get platform() {
return this._state.platform;
}
set platform(value) {
const supportedPlatforms = {
win: 'win',
mac: 'mac',
linux: 'gnu',
};
this._state.platform = value in supportedPlatforms ? value : '';
const platform = this._state.platform || guessPlatform();
this.layout.platform = platform;
this.root.querySelector('svg')
.setAttribute('platform', supportedPlatforms[platform]);
}
get layout() {
return this._state.layout;
}
set layout(value) {
this._state.layout = value;
this._state.layout.platform = this.platform;
this.geometry = this._state.geometry;
Array.from(this.root.querySelectorAll('.key'))
.forEach(key => drawKey(key, value.keyMap));
}
setKeyboardLayout(keyMap, deadKeys, geometry) {
this.layout = newKeyboardLayout(keyMap, deadKeys, geometry);
}
/**
* KeyboardEvent helpers
*/
keyDown(event) {
const code = event.code.replace(/^OS/, 'Meta'); // https://bugzil.la/1264150
if (!code) {
return '';
}
const element = this.root.getElementById(code);
if (!element) {
return '';
}
element.classList.add('press');
const dk = this.layout.pendingDK;
const rv = this.layout.keyDown(code); // updates `this.layout.pendingDK`
const alt = this.layout.modifiers.altgr;
if (alt) {
this.root.querySelector('svg').classList.add('altgr');
}
if (dk) { // a dead key has just been unlatched, hide all key hints
if (!element.classList.contains('specialKey')) {
this.root.querySelector('svg').classList.remove('dk');
Array.from(this.root.querySelectorAll('.dk'))
.forEach((span) => {
span.textContent = '';
});
}
}
if (this.layout.pendingDK) { // show hints for this dead key
Array.from(this.root.querySelectorAll('.key')).forEach((key) => {
drawDK(key, this.layout.keyMap, this.layout.pendingDK);
});
this.root.querySelector('svg').classList.add('dk');
}
return (!alt && (event.ctrlKey || event.altKey || event.metaKey))
? '' : rv; // don't steal ctrl/alt/meta shortcuts
}
keyUp(event) {
const code = event.code.replace(/^OS/, 'Meta'); // https://bugzil.la/1264150
if (!code) {
return;
}
const element = this.root.getElementById(code);
if (!element) {
return;
}
element.classList.remove('press');
this.layout.keyUp(code);
if (!this.layout.modifiers.altgr) {
this.root.querySelector('svg').classList.remove('altgr');
}
}
/**
* Keyboard hints
*/
clearStyle() {
Array.from(this.root.querySelectorAll('[style]'))
.forEach(element => element.removeAttribute('style'));
Array.from(this.root.querySelectorAll('.press'))
.forEach(element => element.classList.remove('press'));
}
showKeys(chars, cssText) {
this.clearStyle();
this.layout.getKeySequence(chars)
.forEach((key) => {
this.root.getElementById(key.id).style.cssText = cssText;
});
}
showHint(keyObj) {
let hintClass = '';
Array.from(this.root.querySelectorAll('.hint'))
.forEach(key => key.classList.remove('hint'));
getKeyChord(this.root, keyObj).forEach((key) => {
key.classList.add('hint');
hintClass += `${key.getAttribute('finger')} `;
});
return hintClass;
}
pressKey(keyObj) {
this.clearStyle();
getKeyChord(this.root, keyObj)
.forEach((key) => {
key.classList.add('press');
});
}
pressKeys(str, duration = 250) {
function* pressKeys(keys) {
for (const key of keys) { // eslint-disable-line
yield key;
}
}
const it = pressKeys(this.layout.getKeySequence(str));
const send = setInterval(() => {
const { value, done } = it.next();
// this.showHint(value);
this.pressKey(value);
if (done) {
clearInterval(send);
}
}, duration);
}
}
customElements.define('x-keyboard', Keyboard);
kalamine-0.38/kalamine/xkb_manager.py 0000664 0000000 0000000 00000036203 14650261713 0017647 0 ustar 00root root 0000000 0000000 """
Helper to list, add and remove keyboard layouts from XKB config.
This MUST remain dependency-free in order to be usable as a standalone installer.
"""
import datetime
import re
import sys
import traceback
from os import environ
from pathlib import Path
from textwrap import dedent
from typing import Dict, ItemsView, Optional
from xml.etree import ElementTree as ET
from .generators import xkb
from .layout import KeyboardLayout
def xdg_config_home() -> Path:
xdg_config = environ.get("XDG_CONFIG_HOME")
if xdg_config:
return Path(xdg_config)
return Path.home() / ".config"
def wayland_running() -> bool:
xdg_session = environ.get("XDG_SESSION_TYPE")
if xdg_session:
return xdg_session.startswith("wayland")
return False
XKB_HOME = xdg_config_home() / "xkb"
XKB_ROOT = Path(environ.get("XKB_CONFIG_ROOT") or "/usr/share/X11/xkb/")
WAYLAND = wayland_running()
KALAMINE_MARK = f"Generated by kalamine on {datetime.date.today().isoformat()}"
XKB_RULES_HEADER = f"""\
"""
LayoutName = str
LocaleName = str
KbdVariant = Dict[LayoutName, Optional[KeyboardLayout]]
KbdIndex = Dict[LocaleName, KbdVariant]
XmlIndex = Dict[LocaleName, Dict[LayoutName, str]]
class XKBManager:
"""Wrapper to list/add/remove keyboard drivers to XKB."""
def __init__(self, root: bool = False) -> None:
self._as_root = root
self._rootdir = XKB_ROOT if root else XKB_HOME
self._index: KbdIndex = {}
@property
def index(self) -> ItemsView[LocaleName, KbdVariant]:
return self._index.items()
@property
def path(self) -> Path:
return self._rootdir
def add(self, layout: KeyboardLayout) -> None:
locale = layout.meta["locale"]
variant = layout.meta["variant"]
if locale not in self._index:
self._index[locale] = {}
self._index[locale][variant] = layout
def remove(self, locale: str, variant: str) -> None:
if locale not in self._index:
self._index[locale] = {}
self._index[locale][variant] = None
def update(self) -> None:
update_rules(self._rootdir, self._index) # XKB/rules/{base,evdev}.xml
update_symbols(self._rootdir, self._index) # XKB/symbols/{locales}
self._index = {}
def clean(self) -> None:
"""Drop the obsolete 'type' attributes Kalamine used to add."""
for filename in ["base.xml", "evdev.xml"]:
filepath = self._rootdir / "rules" / filename
if not filepath.exists():
continue
tree = ET.parse(str(filepath))
for variant in tree.findall(".//variant[@type]"):
variant.attrib.pop("type")
def list(self, mask: str = "") -> XmlIndex:
layouts = list_rules(self._rootdir, mask)
return list_symbols(self._rootdir, layouts)
def list_all(self, mask: str = "") -> XmlIndex:
return list_rules(self._rootdir, mask)
def has_custom_symbols(self) -> bool:
"""Check if there is a usable xkb/symbols/custom file."""
custom_path = self._rootdir / "symbols" / "custom"
if not custom_path.exists():
return False
for filename in ["base.xml", "evdev.xml"]:
filepath = self._rootdir / "rules" / filename
if not filepath.exists():
continue
tree = ET.parse(str(filepath))
if tree.find('.//layout/configItem/name[.="custom"]'):
return True
return False
def ensure_xkb_config_is_ready(self) -> None:
"""Ensure there is an XKB configuration in user-space."""
# See xkblayout.py for a more extensive version of this feature:
# https://gitlab.freedesktop.org/whot/xkblayout
if self._as_root:
return
# ensure all expected directories exist (don't care about 'geometry')
XKB_HOME.mkdir(exist_ok=True)
for subdir in ["compat", "keycodes", "rules", "symbols", "types"]:
(XKB_HOME / subdir).mkdir(exist_ok=True)
# ensure there are XKB rules
# (new locales and symbols will be added by XKBManager)
for ruleset in ["evdev"]: # add 'base', too?
# xkb/rules/evdev
rules = XKB_HOME / "rules" / ruleset
if not rules.exists():
rules.write_text(
dedent(
f"""\
// {KALAMINE_MARK}
// Include the system '{ruleset}' file
! include %S/{ruleset}
"""
)
)
# xkb/rules/evdev.xml
xmlpath = XKB_HOME / "rules" / f"{ruleset}.xml"
if not xmlpath.exists():
xmlpath.write_text(
XKB_RULES_HEADER
+ dedent(
"""\
"""
)
)
""" On GNU/Linux, keyboard layouts must be installed in /usr/share/X11/xkb. To
be able to revert a layout installation, Kalamine marks layouts like this:
- XKB/symbols/[locale]: layout definitions
// KALAMINE::[NAME]::BEGIN
xkb_symbols "[name]" { ... }
// KALAMINE::[NAME]::END
Earlier versions of XKalamine used to mark index files as well but recent
versions of Gnome do not support the custom `type` attribute any more, which
must be removed:
- XKB/rules/{base,evdev}.xml: layout references
lafayette42
French (Lafayette42)
Even worse, the Lafayette project has released a first installer before
the XKalamine installer was developed, so we have to handle this situation
too:
- XKB/symbols/[locale]: layout definitions
// LAFAYETTE::BEGIN
xkb_symbols "lafayette" { ... }
xkb_symbols "lafayette42" { ... }
// LAFAYETTE::END
- XKB/rules/{base,evdev}.xml: layout references
lafayette
French (Lafayette)
lafayette42
French (Lafayette42)
Consequence: these two Lafayette layouts must be uninstalled together.
Because of the way they are grouped in symbols/fr, it is impossible to
remove one without removing the other.
"""
def clean_legacy_lafayette() -> None:
return
###############################################################################
# Helpers: XKB/symbols
#
LEGACY_MARK = {"begin": "// LAFAYETTE::BEGIN\n", "end": "// LAFAYETTE::END\n"}
def get_symbol_mark(name: str) -> Dict[str, str]:
return {
"begin": "// KALAMINE::" + name.upper() + "::BEGIN\n",
"end": "// KALAMINE::" + name.upper() + "::END\n",
}
def is_new_symbol_mark(line: str) -> Optional[str]:
if not line.endswith("::BEGIN\n"):
return None
if line.startswith("// KALAMINE::"):
return line[13:-8].lower() # XXX Kalamine expects lowercase names
return "lafayette" # line.startswith("// LAFAYETTE::"): # obsolete marker
def update_symbols_locale(path: Path, named_layouts: KbdVariant) -> None:
"""Update Kalamine layouts in an xkb/symbols/[locale] file."""
text = ""
modified_text = False
with path.open("r+", encoding="utf-8") as symbols:
# look for Kalamine layouts to be updated or removed
between_marks = False
closing_mark = ""
for line in symbols:
name = is_new_symbol_mark(line)
if name:
if name in named_layouts.keys():
closing_mark = line[:-6] + "END\n"
modified_text = True
between_marks = True
text = text.rstrip()
else:
text += line
elif line.endswith("::END\n"):
if between_marks and line.startswith(closing_mark):
between_marks = False
closing_mark = ""
else:
text += line
elif not between_marks:
text += line
# clear previous Kalamine layouts if needed
if modified_text:
symbols.seek(0)
symbols.write(text.rstrip() + "\n")
symbols.truncate()
# add new Kalamine layouts
locale = path.name
for name, layout in named_layouts.items():
if layout is None:
print(f" - {locale}/{name}")
else:
print(f" + {locale}/{name}")
mark = get_symbol_mark(name)
symbols.write("\n")
symbols.write(mark["begin"])
symbols.write(
re.sub( # drop lines starting with '//#'
r"^//#.*\n", "", xkb.xkb_symbols(layout), flags=re.MULTILINE
).rstrip()
+ "\n"
)
symbols.write(mark["end"])
symbols.close()
def update_symbols(xkb_root: Path, kbd_index: KbdIndex) -> None:
"""Update Kalamine layouts in all xkb/symbols files."""
for locale, named_layouts in kbd_index.items():
path = xkb_root / "symbols" / locale
if not path.exists():
with path.open("w") as file:
file.write("// {KALAMINE_MARK}")
file.close()
try:
print(f"... {path}")
update_symbols_locale(path, named_layouts)
except Exception as exc:
exit_FileNotWritable(exc, path)
def list_symbols(xkb_root: Path, xml_index: XmlIndex) -> XmlIndex:
"""Filter input layouts: only keep the ones defined with Kalamine."""
filtered_index: XmlIndex = {}
for locale, variants in sorted(xml_index.items()):
path = xkb_root / "symbols" / locale
if not path.exists():
continue
with open(path, "r", encoding="utf-8") as symbols:
for line in symbols:
name = is_new_symbol_mark(line)
if name is None:
continue
if name in variants.keys():
if locale not in filtered_index:
filtered_index[locale] = {}
filtered_index[locale][name] = variants[name]
return filtered_index
###############################################################################
# Helpers: XKB/rules
#
def get_rules_variant_list(
tree: ET.ElementTree, locale: LocaleName
) -> Optional[ET.Element]:
"""Find the item matching the locale."""
query = f'.//layout/configItem/name[.="{locale}"]/../../variantList'
if tree.find(query) is None: # create the locale if needed
layout_list = tree.find(".//layoutList")
if layout_list is None:
raise Exception
layout = ET.SubElement(layout_list, "layout")
config = ET.SubElement(layout, "configItem")
ET.SubElement(config, "name").text = locale
ET.SubElement(layout, "variantList")
return tree.find(query)
def remove_rules_variant(variant_list: ET.Element, name: str) -> None:
"""Remove a named item from ."""
for variant in variant_list.findall(f'variant/configItem/name[.="{name}"]/../..'):
variant_list.remove(variant)
def add_rules_variant(variant_list: ET.Element, name: str, description: str) -> None:
"""Add a item to ."""
variant = ET.SubElement(variant_list, "variant")
config = ET.SubElement(variant, "configItem")
ET.SubElement(config, "name").text = name
ET.SubElement(config, "description").text = description
def update_rules(xkb_root: Path, kbd_index: KbdIndex) -> None:
"""Update references in XKB/rules/{base,evdev}.xml."""
for filename in ["base.xml", "evdev.xml"]:
filepath = xkb_root / "rules" / filename
if not filepath.exists():
continue
try:
tree = ET.parse(filepath)
for locale, named_layouts in kbd_index.items():
vlist = get_rules_variant_list(tree, locale)
if vlist is None:
exit(f"Error: unexpected xml format in {filepath}.")
for name, layout in named_layouts.items():
remove_rules_variant(vlist, name)
if layout is not None:
description = layout.meta["description"]
add_rules_variant(vlist, name, description)
if hasattr(ET, "indent"): # Python 3.9+
ET.indent(tree)
with filepath.open("w") as file:
file.write(XKB_RULES_HEADER)
file.write(ET.tostring(tree.getroot(), encoding="unicode"))
file.close()
print(f"... {filepath}")
except Exception as exc:
exit_FileNotWritable(exc, filepath)
def list_rules(xkb_root: Path, mask: str = "*") -> XmlIndex:
"""List all matching XKB layouts."""
if mask in ("", "*"):
locale_mask = "*"
variant_mask = "*"
else:
m = mask.split("/")
if len(m) == 2:
locale_mask, variant_mask = m
else:
locale_mask = mask
variant_mask = "*"
xml_index: XmlIndex = {}
for filename in ["base.xml", "evdev.xml"]:
filepath = xkb_root / "rules" / filename
if not filepath.exists():
continue
tree = ET.parse(filepath)
locales = [str(name.text) for name in tree.findall(".//layout/configItem/name")]
for locale in locales:
for variant in tree.findall(
f'.//layout/configItem/name[.="{locale}"]/../../variantList/variant'
):
name = variant.find("configItem/name")
desc = variant.find("configItem/description")
if name is None or name.text is None or desc is None:
continue
if locale_mask in ("*", locale) and variant_mask in ("*", name.text):
if locale not in xml_index:
xml_index[(locale)] = {}
xml_index[locale][name.text] = str(desc.text)
return xml_index
###############################################################################
# Exception Handling (there must be a better way...)
#
def exit_FileNotWritable(exception: Exception, path: Path) -> None:
if isinstance(exception, PermissionError): # noqa: F821
raise exception
if isinstance(exception, IOError):
print("")
sys.exit(f"Error: could not write to file {path}.")
else:
print("")
sys.exit(f"Error: {exception}.\n{traceback.format_exc()}")
kalamine-0.38/layouts/ 0000775 0000000 0000000 00000000000 14650261713 0014732 5 ustar 00root root 0000000 0000000 kalamine-0.38/layouts/README.md 0000664 0000000 0000000 00000000720 14650261713 0016210 0 ustar 00root root 0000000 0000000 # Sample Layouts
## Qwerty-ANSI
The standard Qwerty-US layout.
## Qwerty-intl
Same layout, but ``'"^`~`` are turned into dead keys:
- `"`, `a` = ä
- `'`, `e` = è
## Qwerty-prog
A qwerty-intl variant with an AltGr layer for dead diacritics and coding symbols.
- `AltGr`+`a` = {
- `AltGr`+`s` = [
- `AltGr`+`d` = ]
- `AltGr`+`f` = }
- `AltGr`+`"`, `a` = ä
- `AltGr`+`'`, `e` = è
## See Also…
- [“One Dead Key”](https://github.com/OneDeadKey/1dk)
kalamine-0.38/layouts/ansi.toml 0000664 0000000 0000000 00000005721 14650261713 0016566 0 ustar 00root root 0000000 0000000 name = "qwerty-ansi"
name8 = "q-ansi"
locale = "us"
variant = "ansi"
description = "standard QWERTY-US layout"
url = "https://OneDeadKey.github.com/kalamine/"
version = "1.0.0"
geometry = "ANSI"
base = '''
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓
│ ~ │ ! │ @ │ # │ $ │ % │ ^ │ & │ * │ ( │ ) │ _ │ + ┃ ┃
│ ` │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 0 │ - │ = ┃ ⌫ ┃
┢━━━━━┷━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┺━━┯━━━━━━━┩
┃ ┃ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │ { │ } │ | │
┃ ↹ ┃ │ │ │ │ │ │ │ │ │ │ [ │ ] │ \ │
┣━━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┲━━━━┷━━━━━━━┪
┃ ┃ A │ S │ D │ F │ G │ H │ J │ K │ L │ : │ " ┃ ┃
┃ ⇬ ┃ │ │ │ │ │ │ │ │ │ ; │ ' ┃ ⏎ ┃
┣━━━━━━━━━┻━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┻━━━━━━━━━━━━┫
┃ ┃ Z │ X │ C │ V │ B │ N │ M │ < │ > │ ? ┃ ┃
┃ ⇧ ┃ │ │ │ │ │ │ │ , │ . │ / ┃ ⇧ ┃
┣━━━━━━━┳━━━━┻━━┳━━┷━━━━┱┴─────┴─────┴─────┴─────┴─────┴─┲━━━┷━━━┳━┷━━━━━╋━━━━━━━┳━━━━━━━┫
┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┃ Ctrl ┃ super ┃ Alt ┃ ␣ ┃ Alt ┃ super ┃ menu ┃ Ctrl ┃
┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛
'''
kalamine-0.38/layouts/intl.toml 0000664 0000000 0000000 00000005740 14650261713 0016603 0 ustar 00root root 0000000 0000000 name = "qwerty-intl"
name8 = "q-intl"
locale = "us"
variant = "intl"
description = "QWERTY layout, international variant"
url = "https://OneDeadKey.github.com/kalamine/"
version = "1.0.0"
geometry = "ISO"
base = '''
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓
│*~ │ ! │ @ │ # │ $ │ % │*^ │ & │ * │ ( │ ) │ _ │ + ┃ ┃
│*` │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 0 │ - │ = ┃ ⌫ ┃
┢━━━━━┷━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┺━━┳━━━━━━━┫
┃ ┃ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │ { │ } ┃ ┃
┃ ↹ ┃ │ │ é │ │ │ │ ú │ í │ ó │ │ [ │ ] ┃ ┃
┣━━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┺┓ ⏎ ┃
┃ ┃ A │ S │ D │ F │ G │ H │ J │ K │ L │ : │*¨ │ | ┃ ┃
┃ ⇬ ┃ á │ │ │ │ │ │ │ │ │ ; │** ' │ \ ┃ ┃
┣━━━━━━┳━━┹──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┷━━━━━┻━━━━━━┫
┃ ┃ | │ Z │ X │ C │ V │ B │ N │ M │ < │ > │ ? ┃ ┃
┃ ⇧ ┃ \ │ │ │ ç │ │ │ │ │ , │ . … │ / ┃ ⇧ ┃
┣━━━━━━┻┳━━━━┷━━┳━━┷━━━━┱┴─────┴─────┴─────┴─────┴─────┴─┲━━━┷━━━┳━┷━━━━━╋━━━━━━━┳━━━━━━━┫
┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┃ Ctrl ┃ super ┃ Alt ┃ ␣ ┃ Alt ┃ super ┃ menu ┃ Ctrl ┃
┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛
'''
kalamine-0.38/layouts/prog.toml 0000664 0000000 0000000 00000005737 14650261713 0016612 0 ustar 00root root 0000000 0000000 name = "qwerty-prog"
name8 = "q-prog"
variant = "prog"
locale = "us"
description = "QWERTY-intl layout, developer variant"
url = "https://OneDeadKey.github.com/kalamine/"
version = "0.6.0"
geometry = "ANSI"
full = '''
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓
│ ~*~ │ ! │ @ │ # │ $ │ % │ ^ │ & │ * │ ( │ ) │ _ │ + ┃ ┃
│ `*` │ 1 ! │ 2 ( │ 3 ) │ 4 ' │ 5 " │ 6*^ │ 7 7 │ 8 8 │ 9 9 │ 0 / │ - │ = ┃ ⌫ ┃
┢━━━━━┷━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┺━━┯━━━━━━━┩
┃ ┃ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │ { │ } │ | │
┃ ↹ ┃ = │ < │ > │ - │ + │ │ 4 │ 5 │ 6 │ * │ [ │ ] │ \ │
┣━━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┲━━━━┷━━━━━━━┪
┃ ┃ A │ S │ D │ F │ G │ H │ J │ K │ L │ : │ "*¨ ┃ ┃
┃ ⇬ ┃ { │ [ │ ] │ } │ / │ │ 1 │ 2 │ 3 │ ; - │ '*´ ┃ ⏎ ┃
┣━━━━━━━━━┻━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┻━━━━━━━━━━━━┫
┃ ┃ Z │ X │ C │ V │ B │ N │ M │ < │ > │ ? ┃ ┃
┃ ⇧ ┃ ~ │ ` │ | │ _ │ \ │ │ 0 │ , , │ . . │ / + ┃ ⇧ ┃
┣━━━━━━━┳━━━━┻━━┳━━┷━━━━┱┴─────┴─────┴─────┴─────┴─────┴─┲━━━┷━━━┳━┷━━━━━╋━━━━━━━┳━━━━━━━┫
┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃
┃ Ctrl ┃ super ┃ Alt ┃ ␣ ┃ AltGr ┃ super ┃ menu ┃ Ctrl ┃
┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛
'''
kalamine-0.38/pyproject.toml 0000664 0000000 0000000 00000002463 14650261713 0016153 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "kalamine"
version = "0.38"
description = "a cross-platform Keyboard Layout Maker"
readme = "README.rst"
authors = [{ name = "Fabien Cazenave", email = "fabien@cazenave.cc" }]
license = { text = "MIT License" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Build Tools",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
requires-python = ">= 3.8"
dependencies = [
"click",
"livereload",
"pyyaml",
"tomli",
"progress",
]
[project.optional-dependencies]
dev = [
"black",
"isort",
"ruff",
"pytest",
"lxml",
"mypy",
"types-PyYAML",
]
[project.urls]
Homepage = "https://github.com/OneDeadKey/kalamine"
[project.scripts]
kalamine = "kalamine.cli:cli"
xkalamine = "kalamine.cli_xkb:cli"
wkalamine = "kalamine.cli_msklc:cli"
[tool.isort]
profile = "black"
[[tool.mypy.overrides]]
module = ["progress.bar"]
ignore_missing_imports = true
kalamine-0.38/tests/ 0000775 0000000 0000000 00000000000 14650261713 0014374 5 ustar 00root root 0000000 0000000 kalamine-0.38/tests/__init__.py 0000664 0000000 0000000 00000000000 14650261713 0016473 0 ustar 00root root 0000000 0000000 kalamine-0.38/tests/test_macos.py 0000664 0000000 0000000 00000005442 14650261713 0017114 0 ustar 00root root 0000000 0000000 from pathlib import Path
from lxml import etree
def check_keylayout(filename: str):
path = Path(__file__).parent.parent / f"dist/{filename}.keylayout"
tree = etree.parse(path, etree.XMLParser(recover=True))
dead_keys = []
# check all keymaps/layers: base, shift, caps, option, option+shift
for keymap_index in range(5):
keymap_query = f'//keyMap[@index="{keymap_index}"]'
keymap = tree.xpath(keymap_query)
assert len(keymap) == 1, f"{keymap_query} should be unique"
# check all key codes for this keymap / layer
# (the key codes below are not used, I don't know why)
excluded_keys = [
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
68,
73,
74,
90,
93,
94,
95,
]
for key_index in range(126):
if key_index in excluded_keys:
continue
# ensure the key is defined and unique
key_query = f'{keymap_query}/key[@code="{key_index}"]'
key = tree.xpath(key_query)
assert len(key) == 1, f"{key_query} should be unique"
# ensure the key has either a direct output or a valid action
action_id = key[0].get("action")
if action_id:
if action_id.startswith("dead_"):
dead_keys.append(action_id[5:])
action_query = f'//actions/action[@id="{action_id}"]'
action = tree.xpath(action_query)
assert len(action) == 1, f"{action_query} should be unique"
assert (
len(action_id) > 1
), f"{key_query} should have a multi-char action ID"
else:
assert (
len(key[0].get("output")) <= 1
), f"{key_query} should have a one-char output"
# check all dead keys
# TODO: ensure there are no unused actions or terminators
for dk in dead_keys:
# ensure all 'when' definitions are defined and unique
when_query = f'//actions/action[@id="dead_{dk}"]/when'
when = tree.xpath(when_query)
assert len(when) == 1, f"{when_query} should be unique"
assert when[0].get("state") == "none"
assert when[0].get("next") == dk
# ensure all terminators are defined and unique
terminator_query = f'//terminators/when[@state="{dk}"]'
terminator = tree.xpath(terminator_query)
assert len(terminator) == 1, f"{terminator_query} should be unique"
assert len(terminator[0].get("output")) == 1
def test_keylayouts():
check_keylayout("q-ansi")
check_keylayout("q-intl")
check_keylayout("q-prog")
kalamine-0.38/tests/test_parser.py 0000664 0000000 0000000 00000005561 14650261713 0017310 0 ustar 00root root 0000000 0000000 from kalamine import KeyboardLayout
from .util import get_layout_dict
def load_layout(filename: str, angle_mod: bool = False) -> KeyboardLayout:
return KeyboardLayout(get_layout_dict(filename), angle_mod)
def test_ansi():
layout = load_layout("ansi")
assert layout.layers[0]["ad01"] == "q"
assert layout.layers[1]["ad01"] == "Q"
assert layout.layers[0]["tlde"] == "`"
assert layout.layers[1]["tlde"] == "~"
assert not layout.has_altgr
assert not layout.has_1dk
assert "**" not in layout.dead_keys
# ensure angle mod is NOT applied
layout = load_layout("ansi", angle_mod=True)
assert layout.layers[0]["ab01"] == "z"
assert layout.layers[1]["ab01"] == "Z"
def test_prog(): # AltGr + dead keys
layout = load_layout("prog")
assert layout.layers[0]["ad01"] == "q"
assert layout.layers[1]["ad01"] == "Q"
assert layout.layers[0]["tlde"] == "`"
assert layout.layers[1]["tlde"] == "~"
assert layout.layers[4]["tlde"] == "*`"
assert layout.layers[5]["tlde"] == "*~"
assert layout.has_altgr
assert not layout.has_1dk
assert "**" not in layout.dead_keys
assert len(layout.dead_keys["*`"]) == 18
assert len(layout.dead_keys["*~"]) == 21
def test_intl(): # 1dk + dead keys
layout = load_layout("intl")
assert layout.layers[0]["ad01"] == "q"
assert layout.layers[1]["ad01"] == "Q"
assert layout.layers[0]["tlde"] == "*`"
assert layout.layers[1]["tlde"] == "*~"
assert not layout.has_altgr
assert layout.has_1dk
assert "**" in layout.dead_keys
assert len(layout.dead_keys) == 5
assert "**" in layout.dead_keys
assert "*`" in layout.dead_keys
assert "*^" in layout.dead_keys
assert "*¨" in layout.dead_keys
assert "*~" in layout.dead_keys
assert len(layout.dead_keys["**"]) == 15
assert len(layout.dead_keys["*`"]) == 18
assert len(layout.dead_keys["*^"]) == 43
assert len(layout.dead_keys["*¨"]) == 21
assert len(layout.dead_keys["*~"]) == 21
# ensure the 1dk parser does not accumulate values from a previous run
layout = load_layout("intl")
assert len(layout.dead_keys["*`"]) == 18
assert len(layout.dead_keys["*~"]) == 21
assert len(layout.dead_keys) == 5
assert "**" in layout.dead_keys
assert "*`" in layout.dead_keys
assert "*^" in layout.dead_keys
assert "*¨" in layout.dead_keys
assert "*~" in layout.dead_keys
assert len(layout.dead_keys["**"]) == 15
assert len(layout.dead_keys["*`"]) == 18
assert len(layout.dead_keys["*^"]) == 43
assert len(layout.dead_keys["*¨"]) == 21
assert len(layout.dead_keys["*~"]) == 21
# ensure angle mod is working correctly
layout = load_layout("intl", angle_mod=True)
assert layout.layers[0]["lsgt"] == "z"
assert layout.layers[1]["lsgt"] == "Z"
assert layout.layers[0]["ab01"] == "x"
assert layout.layers[1]["ab01"] == "X"
kalamine-0.38/tests/test_serializer_ahk.py 0000664 0000000 0000000 00000066061 14650261713 0021012 0 ustar 00root root 0000000 0000000 from textwrap import dedent
from kalamine import KeyboardLayout
from kalamine.generators.ahk import ahk_keymap, ahk_shortcuts
from .util import get_layout_dict
def load_layout(filename: str) -> KeyboardLayout:
return KeyboardLayout(get_layout_dict(filename))
def split(multiline_str: str):
return dedent(multiline_str).lstrip().splitlines()
QWERTY_INTL = split(
"""
; Digits
SC02::SendKey("U+0031", {"*^": "U+00b9"}) ; 1
+SC02::SendKey("U+0021", {}) ; !
SC03::SendKey("U+0032", {"*^": "U+00b2"}) ; 2
+SC03::SendKey("U+0040", {}) ; @
SC04::SendKey("U+0033", {"*^": "U+00b3"}) ; 3
+SC04::SendKey("U+0023", {}) ; #
SC05::SendKey("U+0034", {"*^": "U+2074"}) ; 4
+SC05::SendKey("U+0024", {}) ; $
SC06::SendKey("U+0035", {"*^": "U+2075"}) ; 5
+SC06::SendKey("U+0025", {}) ; %
SC07::SendKey("U+0036", {"*^": "U+2076"}) ; 6
+SC07::SendKey("*^", {"*^": "^"})
SC08::SendKey("U+0037", {"*^": "U+2077"}) ; 7
+SC08::SendKey("U+0026", {}) ; &
SC09::SendKey("U+0038", {"*^": "U+2078"}) ; 8
+SC09::SendKey("U+002a", {}) ; *
SC0a::SendKey("U+0039", {"*^": "U+2079"}) ; 9
+SC0a::SendKey("U+0028", {"*^": "U+207d"}) ; (
SC0b::SendKey("U+0030", {"*^": "U+2070"}) ; 0
+SC0b::SendKey("U+0029", {"*^": "U+207e"}) ; )
; Letters, first row
SC10::SendKey("U+0071", {}) ; q
+SC10::SendKey("U+0051", {}) ; Q
SC11::SendKey("U+0077", {"*``": "U+1e81", "*^": "U+0175", "*¨": "U+1e85"}) ; w
+SC11::SendKey("U+0057", {"*``": "U+1e80", "*^": "U+0174", "*¨": "U+1e84"}) ; W
SC12::SendKey("U+0065", {"*``": "U+00e8", "*~": "U+1ebd", "*^": "U+00ea", "**": "U+00e9", "*¨": "U+00eb"}) ; e
+SC12::SendKey("U+0045", {"*``": "U+00c8", "*~": "U+1ebc", "*^": "U+00ca", "**": "U+00c9", "*¨": "U+00cb"}) ; E
SC13::SendKey("U+0072", {}) ; r
+SC13::SendKey("U+0052", {}) ; R
SC14::SendKey("U+0074", {"*¨": "U+1e97"}) ; t
+SC14::SendKey("U+0054", {}) ; T
SC15::SendKey("U+0079", {"*``": "U+1ef3", "*~": "U+1ef9", "*^": "U+0177", "*¨": "U+00ff"}) ; y
+SC15::SendKey("U+0059", {"*``": "U+1ef2", "*~": "U+1ef8", "*^": "U+0176", "*¨": "U+0178"}) ; Y
SC16::SendKey("U+0075", {"*``": "U+00f9", "*~": "U+0169", "*^": "U+00fb", "**": "U+00fa", "*¨": "U+00fc"}) ; u
+SC16::SendKey("U+0055", {"*``": "U+00d9", "*~": "U+0168", "*^": "U+00db", "**": "U+00da", "*¨": "U+00dc"}) ; U
SC17::SendKey("U+0069", {"*``": "U+00ec", "*~": "U+0129", "*^": "U+00ee", "**": "U+00ed", "*¨": "U+00ef"}) ; i
+SC17::SendKey("U+0049", {"*``": "U+00cc", "*~": "U+0128", "*^": "U+00ce", "**": "U+00cd", "*¨": "U+00cf"}) ; I
SC18::SendKey("U+006f", {"*``": "U+00f2", "*~": "U+00f5", "*^": "U+00f4", "**": "U+00f3", "*¨": "U+00f6"}) ; o
+SC18::SendKey("U+004f", {"*``": "U+00d2", "*~": "U+00d5", "*^": "U+00d4", "**": "U+00d3", "*¨": "U+00d6"}) ; O
SC19::SendKey("U+0070", {}) ; p
+SC19::SendKey("U+0050", {}) ; P
; Letters, second row
SC1e::SendKey("U+0061", {"*``": "U+00e0", "*~": "U+00e3", "*^": "U+00e2", "**": "U+00e1", "*¨": "U+00e4"}) ; a
+SC1e::SendKey("U+0041", {"*``": "U+00c0", "*~": "U+00c3", "*^": "U+00c2", "**": "U+00c1", "*¨": "U+00c4"}) ; A
SC1f::SendKey("U+0073", {"*^": "U+015d"}) ; s
+SC1f::SendKey("U+0053", {"*^": "U+015c"}) ; S
SC20::SendKey("U+0064", {}) ; d
+SC20::SendKey("U+0044", {}) ; D
SC21::SendKey("U+0066", {}) ; f
+SC21::SendKey("U+0046", {}) ; F
SC22::SendKey("U+0067", {"*^": "U+011d"}) ; g
+SC22::SendKey("U+0047", {"*^": "U+011c"}) ; G
SC23::SendKey("U+0068", {"*^": "U+0125", "*¨": "U+1e27"}) ; h
+SC23::SendKey("U+0048", {"*^": "U+0124", "*¨": "U+1e26"}) ; H
SC24::SendKey("U+006a", {"*^": "U+0135"}) ; j
+SC24::SendKey("U+004a", {"*^": "U+0134"}) ; J
SC25::SendKey("U+006b", {}) ; k
+SC25::SendKey("U+004b", {}) ; K
SC26::SendKey("U+006c", {}) ; l
+SC26::SendKey("U+004c", {}) ; L
SC27::SendKey("U+003b", {}) ; ;
+SC27::SendKey("U+003a", {}) ; :
; Letters, third row
SC2c::SendKey("U+007a", {"*^": "U+1e91"}) ; z
+SC2c::SendKey("U+005a", {"*^": "U+1e90"}) ; Z
SC2d::SendKey("U+0078", {"*¨": "U+1e8d"}) ; x
+SC2d::SendKey("U+0058", {"*¨": "U+1e8c"}) ; X
SC2e::SendKey("U+0063", {"*^": "U+0109", "**": "U+00e7"}) ; c
+SC2e::SendKey("U+0043", {"*^": "U+0108", "**": "U+00c7"}) ; C
SC2f::SendKey("U+0076", {"*~": "U+1e7d"}) ; v
+SC2f::SendKey("U+0056", {"*~": "U+1e7c"}) ; V
SC30::SendKey("U+0062", {}) ; b
+SC30::SendKey("U+0042", {}) ; B
SC31::SendKey("U+006e", {"*``": "U+01f9", "*~": "U+00f1"}) ; n
+SC31::SendKey("U+004e", {"*``": "U+01f8", "*~": "U+00d1"}) ; N
SC32::SendKey("U+006d", {}) ; m
+SC32::SendKey("U+004d", {}) ; M
SC33::SendKey("U+002c", {}) ; ,
+SC33::SendKey("U+003c", {"*~": "U+2272"}) ; <
SC34::SendKey("U+002e", {"**": "U+2026"}) ; .
+SC34::SendKey("U+003e", {"*~": "U+2273"}) ; >
SC35::SendKey("U+002f", {}) ; /
+SC35::SendKey("U+003f", {}) ; ?
; Pinky keys
SC0c::SendKey("U+002d", {"*^": "U+207b"}) ; -
+SC0c::SendKey("U+005f", {}) ; _
SC0d::SendKey("U+003d", {"*~": "U+2243", "*^": "U+207c"}) ; =
+SC0d::SendKey("U+002b", {"*^": "U+207a"}) ; +
SC1a::SendKey("U+005b", {}) ; [
+SC1a::SendKey("U+007b", {}) ; {
SC1b::SendKey("U+005d", {}) ; ]
+SC1b::SendKey("U+007d", {}) ; }
SC28::SendKey("**", {"**": "´"})
+SC28::SendKey("*¨", {"*¨": "¨"})
SC29::SendKey("*``", {"*``": "`"}) ; *`
+SC29::SendKey("*~", {"*~": "~"})
SC2b::SendKey("U+005c", {}) ; \\
+SC2b::SendKey("U+007c", {}) ; |
SC56::SendKey("U+005c", {}) ; \\
+SC56::SendKey("U+007c", {}) ; |
; Space bar
SC39::SendKey("U+0020", {"*``": "U+0060", "*~": "U+007e", "*^": "U+005e", "**": "U+0027", "*¨": "U+0022"}) ;
+SC39::SendKey("U+0020", {"*``": "U+0060", "*~": "U+007e", "*^": "U+005e", "**": "U+0027", "*¨": "U+0022"}) ;
"""
)
QWERTY_SHORTCUTS = split(
"""
; Digits
; Letters, first row
^SC10::Send ^q
^+SC10::Send ^+Q
^SC11::Send ^w
^+SC11::Send ^+W
^SC12::Send ^e
^+SC12::Send ^+E
^SC13::Send ^r
^+SC13::Send ^+R
^SC14::Send ^t
^+SC14::Send ^+T
^SC15::Send ^y
^+SC15::Send ^+Y
^SC16::Send ^u
^+SC16::Send ^+U
^SC17::Send ^i
^+SC17::Send ^+I
^SC18::Send ^o
^+SC18::Send ^+O
^SC19::Send ^p
^+SC19::Send ^+P
; Letters, second row
^SC1e::Send ^a
^+SC1e::Send ^+A
^SC1f::Send ^s
^+SC1f::Send ^+S
^SC20::Send ^d
^+SC20::Send ^+D
^SC21::Send ^f
^+SC21::Send ^+F
^SC22::Send ^g
^+SC22::Send ^+G
^SC23::Send ^h
^+SC23::Send ^+H
^SC24::Send ^j
^+SC24::Send ^+J
^SC25::Send ^k
^+SC25::Send ^+K
^SC26::Send ^l
^+SC26::Send ^+L
; Letters, third row
^SC2c::Send ^z
^+SC2c::Send ^+Z
^SC2d::Send ^x
^+SC2d::Send ^+X
^SC2e::Send ^c
^+SC2e::Send ^+C
^SC2f::Send ^v
^+SC2f::Send ^+V
^SC30::Send ^b
^+SC30::Send ^+B
^SC31::Send ^n
^+SC31::Send ^+N
^SC32::Send ^m
^+SC32::Send ^+M
; Pinky keys
; Space bar
"""
)
def test_ansi():
layout = load_layout("ansi")
keymap = ahk_keymap(layout)
assert len(keymap) == 156
assert keymap == split(
"""
; Digits
SC02::SendKey("U+0031", {}) ; 1
+SC02::SendKey("U+0021", {}) ; !
SC03::SendKey("U+0032", {}) ; 2
+SC03::SendKey("U+0040", {}) ; @
SC04::SendKey("U+0033", {}) ; 3
+SC04::SendKey("U+0023", {}) ; #
SC05::SendKey("U+0034", {}) ; 4
+SC05::SendKey("U+0024", {}) ; $
SC06::SendKey("U+0035", {}) ; 5
+SC06::SendKey("U+0025", {}) ; %
SC07::SendKey("U+0036", {}) ; 6
+SC07::SendKey("U+005e", {}) ; ^
SC08::SendKey("U+0037", {}) ; 7
+SC08::SendKey("U+0026", {}) ; &
SC09::SendKey("U+0038", {}) ; 8
+SC09::SendKey("U+002a", {}) ; *
SC0a::SendKey("U+0039", {}) ; 9
+SC0a::SendKey("U+0028", {}) ; (
SC0b::SendKey("U+0030", {}) ; 0
+SC0b::SendKey("U+0029", {}) ; )
; Letters, first row
SC10::SendKey("U+0071", {}) ; q
+SC10::SendKey("U+0051", {}) ; Q
SC11::SendKey("U+0077", {}) ; w
+SC11::SendKey("U+0057", {}) ; W
SC12::SendKey("U+0065", {}) ; e
+SC12::SendKey("U+0045", {}) ; E
SC13::SendKey("U+0072", {}) ; r
+SC13::SendKey("U+0052", {}) ; R
SC14::SendKey("U+0074", {}) ; t
+SC14::SendKey("U+0054", {}) ; T
SC15::SendKey("U+0079", {}) ; y
+SC15::SendKey("U+0059", {}) ; Y
SC16::SendKey("U+0075", {}) ; u
+SC16::SendKey("U+0055", {}) ; U
SC17::SendKey("U+0069", {}) ; i
+SC17::SendKey("U+0049", {}) ; I
SC18::SendKey("U+006f", {}) ; o
+SC18::SendKey("U+004f", {}) ; O
SC19::SendKey("U+0070", {}) ; p
+SC19::SendKey("U+0050", {}) ; P
; Letters, second row
SC1e::SendKey("U+0061", {}) ; a
+SC1e::SendKey("U+0041", {}) ; A
SC1f::SendKey("U+0073", {}) ; s
+SC1f::SendKey("U+0053", {}) ; S
SC20::SendKey("U+0064", {}) ; d
+SC20::SendKey("U+0044", {}) ; D
SC21::SendKey("U+0066", {}) ; f
+SC21::SendKey("U+0046", {}) ; F
SC22::SendKey("U+0067", {}) ; g
+SC22::SendKey("U+0047", {}) ; G
SC23::SendKey("U+0068", {}) ; h
+SC23::SendKey("U+0048", {}) ; H
SC24::SendKey("U+006a", {}) ; j
+SC24::SendKey("U+004a", {}) ; J
SC25::SendKey("U+006b", {}) ; k
+SC25::SendKey("U+004b", {}) ; K
SC26::SendKey("U+006c", {}) ; l
+SC26::SendKey("U+004c", {}) ; L
SC27::SendKey("U+003b", {}) ; ;
+SC27::SendKey("U+003a", {}) ; :
; Letters, third row
SC2c::SendKey("U+007a", {}) ; z
+SC2c::SendKey("U+005a", {}) ; Z
SC2d::SendKey("U+0078", {}) ; x
+SC2d::SendKey("U+0058", {}) ; X
SC2e::SendKey("U+0063", {}) ; c
+SC2e::SendKey("U+0043", {}) ; C
SC2f::SendKey("U+0076", {}) ; v
+SC2f::SendKey("U+0056", {}) ; V
SC30::SendKey("U+0062", {}) ; b
+SC30::SendKey("U+0042", {}) ; B
SC31::SendKey("U+006e", {}) ; n
+SC31::SendKey("U+004e", {}) ; N
SC32::SendKey("U+006d", {}) ; m
+SC32::SendKey("U+004d", {}) ; M
SC33::SendKey("U+002c", {}) ; ,
+SC33::SendKey("U+003c", {}) ; <
SC34::SendKey("U+002e", {}) ; .
+SC34::SendKey("U+003e", {}) ; >
SC35::SendKey("U+002f", {}) ; /
+SC35::SendKey("U+003f", {}) ; ?
; Pinky keys
SC0c::SendKey("U+002d", {}) ; -
+SC0c::SendKey("U+005f", {}) ; _
SC0d::SendKey("U+003d", {}) ; =
+SC0d::SendKey("U+002b", {}) ; +
SC1a::SendKey("U+005b", {}) ; [
+SC1a::SendKey("U+007b", {}) ; {
SC1b::SendKey("U+005d", {}) ; ]
+SC1b::SendKey("U+007d", {}) ; }
SC28::SendKey("U+0027", {}) ; '
+SC28::SendKey("U+0022", {}) ; "
SC29::SendKey("U+0060", {}) ; `
+SC29::SendKey("U+007e", {}) ; ~
SC2b::SendKey("U+005c", {}) ; \\
+SC2b::SendKey("U+007c", {}) ; |
; Space bar
SC39::SendKey("U+0020", {}) ;
+SC39::SendKey("U+0020", {}) ;
"""
)
assert len(ahk_keymap(layout, True)) == 12
shortcuts = ahk_shortcuts(layout)
assert len(shortcuts) == len(QWERTY_SHORTCUTS)
assert shortcuts == QWERTY_SHORTCUTS
def test_intl():
layout = load_layout("intl")
keymap = ahk_keymap(layout)
assert len(keymap) == 159
assert keymap == split(
"""
; Digits
SC02::SendKey("U+0031", {"*^": "U+00b9"}) ; 1
+SC02::SendKey("U+0021", {}) ; !
SC03::SendKey("U+0032", {"*^": "U+00b2"}) ; 2
+SC03::SendKey("U+0040", {}) ; @
SC04::SendKey("U+0033", {"*^": "U+00b3"}) ; 3
+SC04::SendKey("U+0023", {}) ; #
SC05::SendKey("U+0034", {"*^": "U+2074"}) ; 4
+SC05::SendKey("U+0024", {}) ; $
SC06::SendKey("U+0035", {"*^": "U+2075"}) ; 5
+SC06::SendKey("U+0025", {}) ; %
SC07::SendKey("U+0036", {"*^": "U+2076"}) ; 6
+SC07::SendKey("*^", {"*^": "^"})
SC08::SendKey("U+0037", {"*^": "U+2077"}) ; 7
+SC08::SendKey("U+0026", {}) ; &
SC09::SendKey("U+0038", {"*^": "U+2078"}) ; 8
+SC09::SendKey("U+002a", {}) ; *
SC0a::SendKey("U+0039", {"*^": "U+2079"}) ; 9
+SC0a::SendKey("U+0028", {"*^": "U+207d"}) ; (
SC0b::SendKey("U+0030", {"*^": "U+2070"}) ; 0
+SC0b::SendKey("U+0029", {"*^": "U+207e"}) ; )
; Letters, first row
SC10::SendKey("U+0071", {}) ; q
+SC10::SendKey("U+0051", {}) ; Q
SC11::SendKey("U+0077", {"*``": "U+1e81", "*^": "U+0175", "*¨": "U+1e85"}) ; w
+SC11::SendKey("U+0057", {"*``": "U+1e80", "*^": "U+0174", "*¨": "U+1e84"}) ; W
SC12::SendKey("U+0065", {"**": "U+00e9", "*``": "U+00e8", "*^": "U+00ea", "*~": "U+1ebd", "*¨": "U+00eb"}) ; e
+SC12::SendKey("U+0045", {"**": "U+00c9", "*``": "U+00c8", "*^": "U+00ca", "*~": "U+1ebc", "*¨": "U+00cb"}) ; E
SC13::SendKey("U+0072", {}) ; r
+SC13::SendKey("U+0052", {}) ; R
SC14::SendKey("U+0074", {"*¨": "U+1e97"}) ; t
+SC14::SendKey("U+0054", {}) ; T
SC15::SendKey("U+0079", {"*``": "U+1ef3", "*^": "U+0177", "*~": "U+1ef9", "*¨": "U+00ff"}) ; y
+SC15::SendKey("U+0059", {"*``": "U+1ef2", "*^": "U+0176", "*~": "U+1ef8", "*¨": "U+0178"}) ; Y
SC16::SendKey("U+0075", {"**": "U+00fa", "*``": "U+00f9", "*^": "U+00fb", "*~": "U+0169", "*¨": "U+00fc"}) ; u
+SC16::SendKey("U+0055", {"**": "U+00da", "*``": "U+00d9", "*^": "U+00db", "*~": "U+0168", "*¨": "U+00dc"}) ; U
SC17::SendKey("U+0069", {"**": "U+00ed", "*``": "U+00ec", "*^": "U+00ee", "*~": "U+0129", "*¨": "U+00ef"}) ; i
+SC17::SendKey("U+0049", {"**": "U+00cd", "*``": "U+00cc", "*^": "U+00ce", "*~": "U+0128", "*¨": "U+00cf"}) ; I
SC18::SendKey("U+006f", {"**": "U+00f3", "*``": "U+00f2", "*^": "U+00f4", "*~": "U+00f5", "*¨": "U+00f6"}) ; o
+SC18::SendKey("U+004f", {"**": "U+00d3", "*``": "U+00d2", "*^": "U+00d4", "*~": "U+00d5", "*¨": "U+00d6"}) ; O
SC19::SendKey("U+0070", {}) ; p
+SC19::SendKey("U+0050", {}) ; P
; Letters, second row
SC1e::SendKey("U+0061", {"**": "U+00e1", "*``": "U+00e0", "*^": "U+00e2", "*~": "U+00e3", "*¨": "U+00e4"}) ; a
+SC1e::SendKey("U+0041", {"**": "U+00c1", "*``": "U+00c0", "*^": "U+00c2", "*~": "U+00c3", "*¨": "U+00c4"}) ; A
SC1f::SendKey("U+0073", {"*^": "U+015d"}) ; s
+SC1f::SendKey("U+0053", {"*^": "U+015c"}) ; S
SC20::SendKey("U+0064", {}) ; d
+SC20::SendKey("U+0044", {}) ; D
SC21::SendKey("U+0066", {}) ; f
+SC21::SendKey("U+0046", {}) ; F
SC22::SendKey("U+0067", {"*^": "U+011d"}) ; g
+SC22::SendKey("U+0047", {"*^": "U+011c"}) ; G
SC23::SendKey("U+0068", {"*^": "U+0125", "*¨": "U+1e27"}) ; h
+SC23::SendKey("U+0048", {"*^": "U+0124", "*¨": "U+1e26"}) ; H
SC24::SendKey("U+006a", {"*^": "U+0135"}) ; j
+SC24::SendKey("U+004a", {"*^": "U+0134"}) ; J
SC25::SendKey("U+006b", {}) ; k
+SC25::SendKey("U+004b", {}) ; K
SC26::SendKey("U+006c", {}) ; l
+SC26::SendKey("U+004c", {}) ; L
SC27::SendKey("U+003b", {}) ; ;
+SC27::SendKey("U+003a", {}) ; :
; Letters, third row
SC2c::SendKey("U+007a", {"*^": "U+1e91"}) ; z
+SC2c::SendKey("U+005a", {"*^": "U+1e90"}) ; Z
SC2d::SendKey("U+0078", {"*¨": "U+1e8d"}) ; x
+SC2d::SendKey("U+0058", {"*¨": "U+1e8c"}) ; X
SC2e::SendKey("U+0063", {"**": "U+00e7", "*^": "U+0109"}) ; c
+SC2e::SendKey("U+0043", {"**": "U+00c7", "*^": "U+0108"}) ; C
SC2f::SendKey("U+0076", {"*~": "U+1e7d"}) ; v
+SC2f::SendKey("U+0056", {"*~": "U+1e7c"}) ; V
SC30::SendKey("U+0062", {}) ; b
+SC30::SendKey("U+0042", {}) ; B
SC31::SendKey("U+006e", {"*``": "U+01f9", "*~": "U+00f1"}) ; n
+SC31::SendKey("U+004e", {"*``": "U+01f8", "*~": "U+00d1"}) ; N
SC32::SendKey("U+006d", {}) ; m
+SC32::SendKey("U+004d", {}) ; M
SC33::SendKey("U+002c", {}) ; ,
+SC33::SendKey("U+003c", {"*~": "U+2272"}) ; <
SC34::SendKey("U+002e", {"**": "U+2026"}) ; .
+SC34::SendKey("U+003e", {"*~": "U+2273"}) ; >
SC35::SendKey("U+002f", {}) ; /
+SC35::SendKey("U+003f", {}) ; ?
; Pinky keys
SC0c::SendKey("U+002d", {"*^": "U+207b"}) ; -
+SC0c::SendKey("U+005f", {}) ; _
SC0d::SendKey("U+003d", {"*^": "U+207c", "*~": "U+2243"}) ; =
+SC0d::SendKey("U+002b", {"*^": "U+207a"}) ; +
SC1a::SendKey("U+005b", {}) ; [
+SC1a::SendKey("U+007b", {}) ; {
SC1b::SendKey("U+005d", {}) ; ]
+SC1b::SendKey("U+007d", {}) ; }
SC28::SendKey("**", {"**": "\'"})
+SC28::SendKey("*¨", {"*¨": "¨"})
SC29::SendKey("*``", {"*``": "`"}) ; *`
+SC29::SendKey("*~", {"*~": "~"})
SC2b::SendKey("U+005c", {}) ; \\
+SC2b::SendKey("U+007c", {}) ; |
SC56::SendKey("U+005c", {}) ; \\
+SC56::SendKey("U+007c", {}) ; |
; Space bar
SC39::SendKey("U+0020", {"**": "U+0027", "*``": "U+0060", "*^": "U+005e", "*~": "U+007e", "*¨": "U+0022"}) ;
+SC39::SendKey("U+0020", {"**": "U+0027", "*``": "U+0060", "*^": "U+005e", "*~": "U+007e", "*¨": "U+0022"}) ;
"""
)
assert len(ahk_keymap(layout, True)) == 12
shortcuts = ahk_shortcuts(layout)
assert len(shortcuts) == 90
assert shortcuts == QWERTY_SHORTCUTS
def test_prog():
layout = load_layout("prog")
keymap = ahk_keymap(layout)
assert len(keymap) == 156
assert keymap == split(
"""
; Digits
SC02::SendKey("U+0031", {"*^": "U+00b9"}) ; 1
+SC02::SendKey("U+0021", {}) ; !
SC03::SendKey("U+0032", {"*^": "U+00b2"}) ; 2
+SC03::SendKey("U+0040", {}) ; @
SC04::SendKey("U+0033", {"*^": "U+00b3"}) ; 3
+SC04::SendKey("U+0023", {}) ; #
SC05::SendKey("U+0034", {"*^": "U+2074"}) ; 4
+SC05::SendKey("U+0024", {}) ; $
SC06::SendKey("U+0035", {"*^": "U+2075"}) ; 5
+SC06::SendKey("U+0025", {}) ; %
SC07::SendKey("U+0036", {"*^": "U+2076"}) ; 6
+SC07::SendKey("U+005e", {}) ; ^
SC08::SendKey("U+0037", {"*^": "U+2077"}) ; 7
+SC08::SendKey("U+0026", {}) ; &
SC09::SendKey("U+0038", {"*^": "U+2078"}) ; 8
+SC09::SendKey("U+002a", {}) ; *
SC0a::SendKey("U+0039", {"*^": "U+2079"}) ; 9
+SC0a::SendKey("U+0028", {"*^": "U+207d"}) ; (
SC0b::SendKey("U+0030", {"*^": "U+2070"}) ; 0
+SC0b::SendKey("U+0029", {"*^": "U+207e"}) ; )
; Letters, first row
SC10::SendKey("U+0071", {}) ; q
+SC10::SendKey("U+0051", {}) ; Q
SC11::SendKey("U+0077", {"*``": "U+1e81", "*´": "U+1e83", "*^": "U+0175", "*¨": "U+1e85"}) ; w
+SC11::SendKey("U+0057", {"*``": "U+1e80", "*´": "U+1e82", "*^": "U+0174", "*¨": "U+1e84"}) ; W
SC12::SendKey("U+0065", {"*``": "U+00e8", "*´": "U+00e9", "*^": "U+00ea", "*~": "U+1ebd", "*¨": "U+00eb"}) ; e
+SC12::SendKey("U+0045", {"*``": "U+00c8", "*´": "U+00c9", "*^": "U+00ca", "*~": "U+1ebc", "*¨": "U+00cb"}) ; E
SC13::SendKey("U+0072", {"*´": "U+0155"}) ; r
+SC13::SendKey("U+0052", {"*´": "U+0154"}) ; R
SC14::SendKey("U+0074", {"*¨": "U+1e97"}) ; t
+SC14::SendKey("U+0054", {}) ; T
SC15::SendKey("U+0079", {"*``": "U+1ef3", "*´": "U+00fd", "*^": "U+0177", "*~": "U+1ef9", "*¨": "U+00ff"}) ; y
+SC15::SendKey("U+0059", {"*``": "U+1ef2", "*´": "U+00dd", "*^": "U+0176", "*~": "U+1ef8", "*¨": "U+0178"}) ; Y
SC16::SendKey("U+0075", {"*``": "U+00f9", "*´": "U+00fa", "*^": "U+00fb", "*~": "U+0169", "*¨": "U+00fc"}) ; u
+SC16::SendKey("U+0055", {"*``": "U+00d9", "*´": "U+00da", "*^": "U+00db", "*~": "U+0168", "*¨": "U+00dc"}) ; U
SC17::SendKey("U+0069", {"*``": "U+00ec", "*´": "U+00ed", "*^": "U+00ee", "*~": "U+0129", "*¨": "U+00ef"}) ; i
+SC17::SendKey("U+0049", {"*``": "U+00cc", "*´": "U+00cd", "*^": "U+00ce", "*~": "U+0128", "*¨": "U+00cf"}) ; I
SC18::SendKey("U+006f", {"*``": "U+00f2", "*´": "U+00f3", "*^": "U+00f4", "*~": "U+00f5", "*¨": "U+00f6"}) ; o
+SC18::SendKey("U+004f", {"*``": "U+00d2", "*´": "U+00d3", "*^": "U+00d4", "*~": "U+00d5", "*¨": "U+00d6"}) ; O
SC19::SendKey("U+0070", {"*´": "U+1e55"}) ; p
+SC19::SendKey("U+0050", {"*´": "U+1e54"}) ; P
; Letters, second row
SC1e::SendKey("U+0061", {"*``": "U+00e0", "*´": "U+00e1", "*^": "U+00e2", "*~": "U+00e3", "*¨": "U+00e4"}) ; a
+SC1e::SendKey("U+0041", {"*``": "U+00c0", "*´": "U+00c1", "*^": "U+00c2", "*~": "U+00c3", "*¨": "U+00c4"}) ; A
SC1f::SendKey("U+0073", {"*´": "U+015b", "*^": "U+015d"}) ; s
+SC1f::SendKey("U+0053", {"*´": "U+015a", "*^": "U+015c"}) ; S
SC20::SendKey("U+0064", {}) ; d
+SC20::SendKey("U+0044", {}) ; D
SC21::SendKey("U+0066", {}) ; f
+SC21::SendKey("U+0046", {}) ; F
SC22::SendKey("U+0067", {"*´": "U+01f5", "*^": "U+011d"}) ; g
+SC22::SendKey("U+0047", {"*´": "U+01f4", "*^": "U+011c"}) ; G
SC23::SendKey("U+0068", {"*^": "U+0125", "*¨": "U+1e27"}) ; h
+SC23::SendKey("U+0048", {"*^": "U+0124", "*¨": "U+1e26"}) ; H
SC24::SendKey("U+006a", {"*^": "U+0135"}) ; j
+SC24::SendKey("U+004a", {"*^": "U+0134"}) ; J
SC25::SendKey("U+006b", {"*´": "U+1e31"}) ; k
+SC25::SendKey("U+004b", {"*´": "U+1e30"}) ; K
SC26::SendKey("U+006c", {"*´": "U+013a"}) ; l
+SC26::SendKey("U+004c", {"*´": "U+0139"}) ; L
SC27::SendKey("U+003b", {}) ; ;
+SC27::SendKey("U+003a", {}) ; :
; Letters, third row
SC2c::SendKey("U+007a", {"*´": "U+017a", "*^": "U+1e91"}) ; z
+SC2c::SendKey("U+005a", {"*´": "U+0179", "*^": "U+1e90"}) ; Z
SC2d::SendKey("U+0078", {"*¨": "U+1e8d"}) ; x
+SC2d::SendKey("U+0058", {"*¨": "U+1e8c"}) ; X
SC2e::SendKey("U+0063", {"*´": "U+0107", "*^": "U+0109"}) ; c
+SC2e::SendKey("U+0043", {"*´": "U+0106", "*^": "U+0108"}) ; C
SC2f::SendKey("U+0076", {"*~": "U+1e7d"}) ; v
+SC2f::SendKey("U+0056", {"*~": "U+1e7c"}) ; V
SC30::SendKey("U+0062", {}) ; b
+SC30::SendKey("U+0042", {}) ; B
SC31::SendKey("U+006e", {"*``": "U+01f9", "*´": "U+0144", "*~": "U+00f1"}) ; n
+SC31::SendKey("U+004e", {"*``": "U+01f8", "*´": "U+0143", "*~": "U+00d1"}) ; N
SC32::SendKey("U+006d", {"*´": "U+1e3f"}) ; m
+SC32::SendKey("U+004d", {"*´": "U+1e3e"}) ; M
SC33::SendKey("U+002c", {}) ; ,
+SC33::SendKey("U+003c", {"*~": "U+2272"}) ; <
SC34::SendKey("U+002e", {}) ; .
+SC34::SendKey("U+003e", {"*~": "U+2273"}) ; >
SC35::SendKey("U+002f", {}) ; /
+SC35::SendKey("U+003f", {}) ; ?
; Pinky keys
SC0c::SendKey("U+002d", {"*^": "U+207b"}) ; -
+SC0c::SendKey("U+005f", {}) ; _
SC0d::SendKey("U+003d", {"*^": "U+207c", "*~": "U+2243"}) ; =
+SC0d::SendKey("U+002b", {"*^": "U+207a"}) ; +
SC1a::SendKey("U+005b", {}) ; [
+SC1a::SendKey("U+007b", {}) ; {
SC1b::SendKey("U+005d", {}) ; ]
+SC1b::SendKey("U+007d", {}) ; }
SC28::SendKey("U+0027", {}) ; '
+SC28::SendKey("U+0022", {}) ; "
SC29::SendKey("U+0060", {}) ; `
+SC29::SendKey("U+007e", {}) ; ~
SC2b::SendKey("U+005c", {}) ; \\
+SC2b::SendKey("U+007c", {}) ; |
; Space bar
SC39::SendKey("U+0020", {"*``": "U+0060", "*´": "U+0027", "*^": "U+005e", "*~": "U+007e", "*¨": "U+0022"}) ;
+SC39::SendKey("U+0020", {"*``": "U+0060", "*´": "U+0027", "*^": "U+005e", "*~": "U+007e", "*¨": "U+0022"}) ;
"""
)
altgr = ahk_keymap(layout, True)
assert len(altgr) == 98
assert altgr == split(
"""
; Digits
<^>!SC02::SendKey("U+0021", {}) ; !
<^>!SC03::SendKey("U+0028", {"*^": "U+207d"}) ; (
<^>!SC04::SendKey("U+0029", {"*^": "U+207e"}) ; )
<^>!SC05::SendKey("U+0027", {}) ; '
<^>!SC06::SendKey("U+0022", {}) ; "
<^>!SC07::SendKey("*^", {"*^": "^"})
<^>!SC08::SendKey("U+0037", {"*^": "U+2077"}) ; 7
<^>!SC09::SendKey("U+0038", {"*^": "U+2078"}) ; 8
<^>!SC0a::SendKey("U+0039", {"*^": "U+2079"}) ; 9
<^>!SC0b::SendKey("U+002f", {}) ; /
; Letters, first row
<^>!SC10::SendKey("U+003d", {"*^": "U+207c", "*~": "U+2243"}) ; =
<^>!SC11::SendKey("U+003c", {"*~": "U+2272"}) ; <
<^>!+SC11::SendKey("U+2264", {}) ; ≤
<^>!SC12::SendKey("U+003e", {"*~": "U+2273"}) ; >
<^>!+SC12::SendKey("U+2265", {}) ; ≥
<^>!SC13::SendKey("U+002d", {"*^": "U+207b"}) ; -
<^>!SC14::SendKey("U+002b", {"*^": "U+207a"}) ; +
<^>!SC16::SendKey("U+0034", {"*^": "U+2074"}) ; 4
<^>!SC17::SendKey("U+0035", {"*^": "U+2075"}) ; 5
<^>!SC18::SendKey("U+0036", {"*^": "U+2076"}) ; 6
<^>!SC19::SendKey("U+002a", {}) ; *
; Letters, second row
<^>!SC1e::SendKey("U+007b", {}) ; {
<^>!SC1f::SendKey("U+005b", {}) ; [
<^>!SC20::SendKey("U+005d", {}) ; ]
<^>!SC21::SendKey("U+007d", {}) ; }
<^>!SC22::SendKey("U+002f", {}) ; /
<^>!SC24::SendKey("U+0031", {"*^": "U+00b9"}) ; 1
<^>!SC25::SendKey("U+0032", {"*^": "U+00b2"}) ; 2
<^>!SC26::SendKey("U+0033", {"*^": "U+00b3"}) ; 3
<^>!SC27::SendKey("U+002d", {"*^": "U+207b"}) ; -
; Letters, third row
<^>!SC2c::SendKey("U+007e", {}) ; ~
<^>!SC2d::SendKey("U+0060", {}) ; `
<^>!SC2e::SendKey("U+007c", {}) ; |
<^>!+SC2e::SendKey("U+00a6", {}) ; ¦
<^>!SC2f::SendKey("U+005f", {}) ; _
<^>!SC30::SendKey("U+005c", {}) ; \\
<^>!SC32::SendKey("U+0030", {"*^": "U+2070"}) ; 0
<^>!SC33::SendKey("U+002c", {}) ; ,
<^>!SC34::SendKey("U+002e", {}) ; .
<^>!SC35::SendKey("U+002b", {"*^": "U+207a"}) ; +
; Pinky keys
<^>!SC28::SendKey("*´", {"*´": "´"})
<^>!+SC28::SendKey("*¨", {"*¨": "¨"})
<^>!SC29::SendKey("*``", {"*``": "`"}) ; *`
<^>!+SC29::SendKey("*~", {"*~": "~"})
; Space bar
<^>!SC39::SendKey("U+0020", {"*``": "U+0060", "*´": "U+0027", "*^": "U+005e", "*~": "U+007e", "*¨": "U+0022"}) ;
<^>!+SC39::SendKey("U+0020", {"*``": "U+0060", "*´": "U+0027", "*^": "U+005e", "*~": "U+007e", "*¨": "U+0022"}) ;
"""
)
shortcuts = ahk_shortcuts(layout)
assert len(shortcuts) == 90
assert shortcuts == QWERTY_SHORTCUTS
kalamine-0.38/tests/test_serializer_keylayout.py 0000664 0000000 0000000 00000162475 14650261713 0022303 0 ustar 00root root 0000000 0000000 from textwrap import dedent
from kalamine import KeyboardLayout
from kalamine.generators.keylayout import macos_actions, macos_keymap, macos_terminators
from .util import get_layout_dict
def load_layout(filename: str) -> KeyboardLayout:
return KeyboardLayout(get_layout_dict(filename))
def split(multiline_str: str):
return dedent(multiline_str).lstrip().rstrip().splitlines()
EMPTY_KEYMAP = split(
"""
"""
)
def test_ansi():
layout = load_layout("ansi")
keymap = macos_keymap(layout)
assert len(keymap[0]) == 60
assert keymap[0] == split(
"""
"""
)
assert len(keymap[1]) == 60
assert keymap[1] == split(
"""
"""
)
assert len(keymap[2]) == 60
assert keymap[2] == split(
"""
"""
)
assert len(keymap[3]) == 60
assert keymap[3] == EMPTY_KEYMAP
assert len(keymap[4]) == 60
assert keymap[4] == EMPTY_KEYMAP
actions = macos_actions(layout)
assert actions[1:] == split(
"""
"""
)
terminators = macos_terminators(layout)
assert len(terminators) == 0
def test_intl():
layout = load_layout("intl")
keymap = macos_keymap(layout)
assert len(keymap[0]) == 60
assert keymap[0] == split(
"""
"""
)
assert len(keymap[1]) == 60
assert keymap[1] == split(
"""
"""
)
assert len(keymap[2]) == 60
assert keymap[2] == split(
"""
"""
)
assert len(keymap[3]) == 60
assert keymap[3] == EMPTY_KEYMAP
assert len(keymap[4]) == 60
assert keymap[4] == EMPTY_KEYMAP
actions = macos_actions(layout)
assert actions == split(
"""
"""
)
terminators = macos_terminators(layout)
assert len(terminators) == 5
assert terminators == split(
"""
"""
)
def test_prog():
layout = load_layout("prog")
keymap = macos_keymap(layout)
assert len(keymap[0]) == 60
assert keymap[0] == split(
"""
"""
)
assert len(keymap[1]) == 60
assert keymap[1] == split(
"""
"""
)
assert len(keymap[2]) == 60
assert keymap[2] == split(
"""
"""
)
assert len(keymap[3]) == 60
assert keymap[3] == split(
"""
"""
)
assert len(keymap[4]) == 60
assert keymap[4] == split(
"""
"""
)
actions = macos_actions(layout)
assert actions == split(
"""
"""
)
terminators = macos_terminators(layout)
assert len(terminators) == 5
assert terminators == split(
"""
"""
)
kalamine-0.38/tests/test_serializer_klc.py 0000664 0000000 0000000 00000036123 14650261713 0021014 0 ustar 00root root 0000000 0000000 from textwrap import dedent
from kalamine import KeyboardLayout
from kalamine.generators.klc import klc_deadkeys, klc_dk_index, klc_keymap
from .util import get_layout_dict
def split(multiline_str: str):
return dedent(multiline_str).lstrip().rstrip().splitlines()
LAYOUTS = {}
for filename in ["ansi", "intl", "prog"]:
LAYOUTS[filename] = KeyboardLayout(get_layout_dict(filename))
def test_ansi_keymap():
keymap = klc_keymap(LAYOUTS["ansi"])
assert len(keymap) == 49
assert keymap == split(
"""
02 1 0 1 0021 -1 -1 // 1 !
03 2 0 2 0040 -1 -1 // 2 @
04 3 0 3 0023 -1 -1 // 3 #
05 4 0 4 0024 -1 -1 // 4 $
06 5 0 5 0025 -1 -1 // 5 %
07 6 0 6 005e -1 -1 // 6 ^
08 7 0 7 0026 -1 -1 // 7 &
09 8 0 8 002a -1 -1 // 8 *
0a 9 0 9 0028 -1 -1 // 9 (
0b 0 0 0 0029 -1 -1 // 0 )
10 Q 1 q Q -1 -1 // q Q
11 W 1 w W -1 -1 // w W
12 E 1 e E -1 -1 // e E
13 R 1 r R -1 -1 // r R
14 T 1 t T -1 -1 // t T
15 Y 1 y Y -1 -1 // y Y
16 U 1 u U -1 -1 // u U
17 I 1 i I -1 -1 // i I
18 O 1 o O -1 -1 // o O
19 P 1 p P -1 -1 // p P
1e A 1 a A -1 -1 // a A
1f S 1 s S -1 -1 // s S
20 D 1 d D -1 -1 // d D
21 F 1 f F -1 -1 // f F
22 G 1 g G -1 -1 // g G
23 H 1 h H -1 -1 // h H
24 J 1 j J -1 -1 // j J
25 K 1 k K -1 -1 // k K
26 L 1 l L -1 -1 // l L
27 OEM_1 0 003b 003a -1 -1 // ; :
2c Z 1 z Z -1 -1 // z Z
2d X 1 x X -1 -1 // x X
2e C 1 c C -1 -1 // c C
2f V 1 v V -1 -1 // v V
30 B 1 b B -1 -1 // b B
31 N 1 n N -1 -1 // n N
32 M 1 m M -1 -1 // m M
33 OEM_COMMA 0 002c 003c -1 -1 // , <
34 OEM_PERIOD 0 002e 003e -1 -1 // . >
35 OEM_2 0 002f 003f -1 -1 // / ?
0c OEM_MINUS 0 002d 005f -1 -1 // - _
0d OEM_PLUS 0 003d 002b -1 -1 // = +
1a OEM_3 0 005b 007b -1 -1 // [ {
1b OEM_4 0 005d 007d -1 -1 // ] }
28 OEM_5 0 0027 0022 -1 -1 // ' "
29 OEM_6 0 0060 007e -1 -1 // ` ~
2b OEM_7 0 005c 007c -1 -1 // \\ |
56 OEM_102 0 -1 -1 -1 -1 //
39 SPACE 0 0020 0020 -1 -1 //
"""
)
def test_ansi_deadkeys():
assert len(klc_dk_index(LAYOUTS["ansi"])) == 0
assert len(klc_deadkeys(LAYOUTS["ansi"])) == 0
def test_intl_keymap():
keymap = klc_keymap(LAYOUTS["intl"])
assert len(keymap) == 49
assert keymap == split(
"""
02 1 0 1 0021 -1 -1 // 1 !
03 2 0 2 0040 -1 -1 // 2 @
04 3 0 3 0023 -1 -1 // 3 #
05 4 0 4 0024 -1 -1 // 4 $
06 5 0 5 0025 -1 -1 // 5 %
07 6 0 6 005e@ -1 -1 // 6 ^
08 7 0 7 0026 -1 -1 // 7 &
09 8 0 8 002a -1 -1 // 8 *
0a 9 0 9 0028 -1 -1 // 9 (
0b 0 0 0 0029 -1 -1 // 0 )
10 Q 1 q Q -1 -1 // q Q
11 W 1 w W -1 -1 // w W
12 E 1 e E -1 -1 // e E
13 R 1 r R -1 -1 // r R
14 T 1 t T -1 -1 // t T
15 Y 1 y Y -1 -1 // y Y
16 U 1 u U -1 -1 // u U
17 I 1 i I -1 -1 // i I
18 O 1 o O -1 -1 // o O
19 P 1 p P -1 -1 // p P
1e A 1 a A -1 -1 // a A
1f S 1 s S -1 -1 // s S
20 D 1 d D -1 -1 // d D
21 F 1 f F -1 -1 // f F
22 G 1 g G -1 -1 // g G
23 H 1 h H -1 -1 // h H
24 J 1 j J -1 -1 // j J
25 K 1 k K -1 -1 // k K
26 L 1 l L -1 -1 // l L
27 OEM_1 0 003b 003a -1 -1 // ; :
2c Z 1 z Z -1 -1 // z Z
2d X 1 x X -1 -1 // x X
2e C 1 c C -1 -1 // c C
2f V 1 v V -1 -1 // v V
30 B 1 b B -1 -1 // b B
31 N 1 n N -1 -1 // n N
32 M 1 m M -1 -1 // m M
33 OEM_COMMA 0 002c 003c -1 -1 // , <
34 OEM_PERIOD 0 002e 003e -1 -1 // . >
35 OEM_2 0 002f 003f -1 -1 // / ?
0c OEM_MINUS 0 002d 005f -1 -1 // - _
0d OEM_PLUS 0 003d 002b -1 -1 // = +
1a OEM_3 0 005b 007b -1 -1 // [ {
1b OEM_4 0 005d 007d -1 -1 // ] }
28 OEM_5 0 0027@ 0022@ -1 -1 // ' "
29 OEM_6 0 0060@ 007e@ -1 -1 // ` ~
2b OEM_7 0 005c 007c -1 -1 // \\ |
56 OEM_102 0 005c 007c -1 -1 // \\ |
39 SPACE 0 0020 0020 -1 -1 //
"""
)
def test_intl_deadkeys():
dk_index = klc_dk_index(LAYOUTS["intl"])
assert len(dk_index) == 5
assert dk_index == split(
"""
0027 "1DK"
0060 "GRAVE"
005e "CIRCUMFLEX"
007e "TILDE"
0022 "DIAERESIS"
"""
)
deadkeys = klc_deadkeys(LAYOUTS["intl"])
# assert len(deadkeys) == 138
assert deadkeys == split(
"""
// DEADKEY: 1DK //{{{
DEADKEY 0027
0027 0027 // ' -> '
0045 00c9 // E -> É
0065 00e9 // e -> é
0055 00da // U -> Ú
0075 00fa // u -> ú
0049 00cd // I -> Í
0069 00ed // i -> í
004f 00d3 // O -> Ó
006f 00f3 // o -> ó
0041 00c1 // A -> Á
0061 00e1 // a -> á
0043 00c7 // C -> Ç
0063 00e7 // c -> ç
002e 2026 // . -> …
0020 0027 // -> '
//}}}
// DEADKEY: GRAVE //{{{
DEADKEY 0060
0041 00c0 // A -> À
0061 00e0 // a -> à
0045 00c8 // E -> È
0065 00e8 // e -> è
0049 00cc // I -> Ì
0069 00ec // i -> ì
004e 01f8 // N -> Ǹ
006e 01f9 // n -> ǹ
004f 00d2 // O -> Ò
006f 00f2 // o -> ò
0055 00d9 // U -> Ù
0075 00f9 // u -> ù
0057 1e80 // W -> Ẁ
0077 1e81 // w -> ẁ
0059 1ef2 // Y -> Ỳ
0079 1ef3 // y -> ỳ
0020 0060 // -> `
//}}}
// DEADKEY: CIRCUMFLEX //{{{
DEADKEY 005e
0041 00c2 // A -> Â
0061 00e2 // a -> â
0043 0108 // C -> Ĉ
0063 0109 // c -> ĉ
0045 00ca // E -> Ê
0065 00ea // e -> ê
0047 011c // G -> Ĝ
0067 011d // g -> ĝ
0048 0124 // H -> Ĥ
0068 0125 // h -> ĥ
0049 00ce // I -> Î
0069 00ee // i -> î
004a 0134 // J -> Ĵ
006a 0135 // j -> ĵ
004f 00d4 // O -> Ô
006f 00f4 // o -> ô
0053 015c // S -> Ŝ
0073 015d // s -> ŝ
0055 00db // U -> Û
0075 00fb // u -> û
0057 0174 // W -> Ŵ
0077 0175 // w -> ŵ
0059 0176 // Y -> Ŷ
0079 0177 // y -> ŷ
005a 1e90 // Z -> Ẑ
007a 1e91 // z -> ẑ
0030 2070 // 0 -> ⁰
0031 00b9 // 1 -> ¹
0032 00b2 // 2 -> ²
0033 00b3 // 3 -> ³
0034 2074 // 4 -> ⁴
0035 2075 // 5 -> ⁵
0036 2076 // 6 -> ⁶
0037 2077 // 7 -> ⁷
0038 2078 // 8 -> ⁸
0039 2079 // 9 -> ⁹
0028 207d // ( -> ⁽
0029 207e // ) -> ⁾
002b 207a // + -> ⁺
002d 207b // - -> ⁻
003d 207c // = -> ⁼
0020 005e // -> ^
//}}}
// DEADKEY: TILDE //{{{
DEADKEY 007e
0041 00c3 // A -> Ã
0061 00e3 // a -> ã
0045 1ebc // E -> Ẽ
0065 1ebd // e -> ẽ
0049 0128 // I -> Ĩ
0069 0129 // i -> ĩ
004e 00d1 // N -> Ñ
006e 00f1 // n -> ñ
004f 00d5 // O -> Õ
006f 00f5 // o -> õ
0055 0168 // U -> Ũ
0075 0169 // u -> ũ
0056 1e7c // V -> Ṽ
0076 1e7d // v -> ṽ
0059 1ef8 // Y -> Ỹ
0079 1ef9 // y -> ỹ
003c 2272 // < -> ≲
003e 2273 // > -> ≳
003d 2243 // = -> ≃
0020 007e // -> ~
//}}}
// DEADKEY: DIAERESIS //{{{
DEADKEY 0022
0041 00c4 // A -> Ä
0061 00e4 // a -> ä
0045 00cb // E -> Ë
0065 00eb // e -> ë
0048 1e26 // H -> Ḧ
0068 1e27 // h -> ḧ
0049 00cf // I -> Ï
0069 00ef // i -> ï
004f 00d6 // O -> Ö
006f 00f6 // o -> ö
0074 1e97 // t -> ẗ
0055 00dc // U -> Ü
0075 00fc // u -> ü
0057 1e84 // W -> Ẅ
0077 1e85 // w -> ẅ
0058 1e8c // X -> Ẍ
0078 1e8d // x -> ẍ
0059 0178 // Y -> Ÿ
0079 00ff // y -> ÿ
0020 0022 // -> "
//}}}
"""
)
def test_prog_keymap():
keymap = klc_keymap(LAYOUTS["prog"])
assert len(keymap) == 49
assert keymap == split(
"""
02 1 0 1 0021 -1 -1 0021 -1 // 1 ! !
03 2 0 2 0040 -1 -1 0028 -1 // 2 @ (
04 3 0 3 0023 -1 -1 0029 -1 // 3 # )
05 4 0 4 0024 -1 -1 0027 -1 // 4 $ '
06 5 0 5 0025 -1 -1 0022 -1 // 5 % "
07 6 0 6 005e -1 -1 005e@ -1 // 6 ^ ^
08 7 0 7 0026 -1 -1 7 -1 // 7 & 7
09 8 0 8 002a -1 -1 8 -1 // 8 * 8
0a 9 0 9 0028 -1 -1 9 -1 // 9 ( 9
0b 0 0 0 0029 -1 -1 002f -1 // 0 ) /
10 Q 1 q Q -1 -1 003d -1 // q Q =
11 W 1 w W -1 -1 003c 2264 // w W < ≤
12 E 1 e E -1 -1 003e 2265 // e E > ≥
13 R 1 r R -1 -1 002d -1 // r R -
14 T 1 t T -1 -1 002b -1 // t T +
15 Y 1 y Y -1 -1 -1 -1 // y Y
16 U 1 u U -1 -1 4 -1 // u U 4
17 I 1 i I -1 -1 5 -1 // i I 5
18 O 1 o O -1 -1 6 -1 // o O 6
19 P 1 p P -1 -1 002a -1 // p P *
1e A 1 a A -1 -1 007b -1 // a A {
1f S 1 s S -1 -1 005b -1 // s S [
20 D 1 d D -1 -1 005d -1 // d D ]
21 F 1 f F -1 -1 007d -1 // f F }
22 G 1 g G -1 -1 002f -1 // g G /
23 H 1 h H -1 -1 -1 -1 // h H
24 J 1 j J -1 -1 1 -1 // j J 1
25 K 1 k K -1 -1 2 -1 // k K 2
26 L 1 l L -1 -1 3 -1 // l L 3
27 OEM_1 0 003b 003a -1 -1 002d -1 // ; : -
2c Z 1 z Z -1 -1 007e -1 // z Z ~
2d X 1 x X -1 -1 0060 -1 // x X `
2e C 1 c C -1 -1 007c 00a6 // c C | ¦
2f V 1 v V -1 -1 005f -1 // v V _
30 B 1 b B -1 -1 005c -1 // b B \\
31 N 1 n N -1 -1 -1 -1 // n N
32 M 1 m M -1 -1 0 -1 // m M 0
33 OEM_COMMA 0 002c 003c -1 -1 002c -1 // , < ,
34 OEM_PERIOD 0 002e 003e -1 -1 002e -1 // . > .
35 OEM_2 0 002f 003f -1 -1 002b -1 // / ? +
0c OEM_MINUS 0 002d 005f -1 -1 -1 -1 // - _
0d OEM_PLUS 0 003d 002b -1 -1 -1 -1 // = +
1a OEM_3 0 005b 007b -1 -1 -1 -1 // [ {
1b OEM_4 0 005d 007d -1 -1 -1 -1 // ] }
28 OEM_5 0 0027 0022 -1 -1 0027@ 0022@ // ' " ' "
29 OEM_6 0 0060 007e -1 -1 0060@ 007e@ // ` ~ ` ~
2b OEM_7 0 005c 007c -1 -1 -1 -1 // \\ |
56 OEM_102 0 -1 -1 -1 -1 -1 -1 //
39 SPACE 0 0020 0020 -1 -1 0020 0020 //
"""
)
def test_prog_deadkeys():
dk_index = klc_dk_index(LAYOUTS["prog"])
assert len(dk_index) == 5
assert dk_index == split(
"""
0060 "GRAVE"
0027 "ACUTE"
005e "CIRCUMFLEX"
007e "TILDE"
0022 "DIAERESIS"
"""
)
deadkeys = klc_deadkeys(LAYOUTS["prog"])
assert len(deadkeys) == 153
assert deadkeys == split(
"""
// DEADKEY: GRAVE //{{{
DEADKEY 0060
0041 00c0 // A -> À
0061 00e0 // a -> à
0045 00c8 // E -> È
0065 00e8 // e -> è
0049 00cc // I -> Ì
0069 00ec // i -> ì
004e 01f8 // N -> Ǹ
006e 01f9 // n -> ǹ
004f 00d2 // O -> Ò
006f 00f2 // o -> ò
0055 00d9 // U -> Ù
0075 00f9 // u -> ù
0057 1e80 // W -> Ẁ
0077 1e81 // w -> ẁ
0059 1ef2 // Y -> Ỳ
0079 1ef3 // y -> ỳ
0020 0060 // -> `
//}}}
// DEADKEY: ACUTE //{{{
DEADKEY 0027
0041 00c1 // A -> Á
0061 00e1 // a -> á
0043 0106 // C -> Ć
0063 0107 // c -> ć
0045 00c9 // E -> É
0065 00e9 // e -> é
0047 01f4 // G -> Ǵ
0067 01f5 // g -> ǵ
0049 00cd // I -> Í
0069 00ed // i -> í
004b 1e30 // K -> Ḱ
006b 1e31 // k -> ḱ
004c 0139 // L -> Ĺ
006c 013a // l -> ĺ
004d 1e3e // M -> Ḿ
006d 1e3f // m -> ḿ
004e 0143 // N -> Ń
006e 0144 // n -> ń
004f 00d3 // O -> Ó
006f 00f3 // o -> ó
0050 1e54 // P -> Ṕ
0070 1e55 // p -> ṕ
0052 0154 // R -> Ŕ
0072 0155 // r -> ŕ
0053 015a // S -> Ś
0073 015b // s -> ś
0055 00da // U -> Ú
0075 00fa // u -> ú
0057 1e82 // W -> Ẃ
0077 1e83 // w -> ẃ
0059 00dd // Y -> Ý
0079 00fd // y -> ý
005a 0179 // Z -> Ź
007a 017a // z -> ź
0020 0027 // -> '
//}}}
// DEADKEY: CIRCUMFLEX //{{{
DEADKEY 005e
0041 00c2 // A -> Â
0061 00e2 // a -> â
0043 0108 // C -> Ĉ
0063 0109 // c -> ĉ
0045 00ca // E -> Ê
0065 00ea // e -> ê
0047 011c // G -> Ĝ
0067 011d // g -> ĝ
0048 0124 // H -> Ĥ
0068 0125 // h -> ĥ
0049 00ce // I -> Î
0069 00ee // i -> î
004a 0134 // J -> Ĵ
006a 0135 // j -> ĵ
004f 00d4 // O -> Ô
006f 00f4 // o -> ô
0053 015c // S -> Ŝ
0073 015d // s -> ŝ
0055 00db // U -> Û
0075 00fb // u -> û
0057 0174 // W -> Ŵ
0077 0175 // w -> ŵ
0059 0176 // Y -> Ŷ
0079 0177 // y -> ŷ
005a 1e90 // Z -> Ẑ
007a 1e91 // z -> ẑ
0030 2070 // 0 -> ⁰
0031 00b9 // 1 -> ¹
0032 00b2 // 2 -> ²
0033 00b3 // 3 -> ³
0034 2074 // 4 -> ⁴
0035 2075 // 5 -> ⁵
0036 2076 // 6 -> ⁶
0037 2077 // 7 -> ⁷
0038 2078 // 8 -> ⁸
0039 2079 // 9 -> ⁹
0028 207d // ( -> ⁽
0029 207e // ) -> ⁾
002b 207a // + -> ⁺
002d 207b // - -> ⁻
003d 207c // = -> ⁼
0020 005e // -> ^
//}}}
// DEADKEY: TILDE //{{{
DEADKEY 007e
0041 00c3 // A -> Ã
0061 00e3 // a -> ã
0045 1ebc // E -> Ẽ
0065 1ebd // e -> ẽ
0049 0128 // I -> Ĩ
0069 0129 // i -> ĩ
004e 00d1 // N -> Ñ
006e 00f1 // n -> ñ
004f 00d5 // O -> Õ
006f 00f5 // o -> õ
0055 0168 // U -> Ũ
0075 0169 // u -> ũ
0056 1e7c // V -> Ṽ
0076 1e7d // v -> ṽ
0059 1ef8 // Y -> Ỹ
0079 1ef9 // y -> ỹ
003c 2272 // < -> ≲
003e 2273 // > -> ≳
003d 2243 // = -> ≃
0020 007e // -> ~
//}}}
// DEADKEY: DIAERESIS //{{{
DEADKEY 0022
0041 00c4 // A -> Ä
0061 00e4 // a -> ä
0045 00cb // E -> Ë
0065 00eb // e -> ë
0048 1e26 // H -> Ḧ
0068 1e27 // h -> ḧ
0049 00cf // I -> Ï
0069 00ef // i -> ï
004f 00d6 // O -> Ö
006f 00f6 // o -> ö
0074 1e97 // t -> ẗ
0055 00dc // U -> Ü
0075 00fc // u -> ü
0057 1e84 // W -> Ẅ
0077 1e85 // w -> ẅ
0058 1e8c // X -> Ẍ
0078 1e8d // x -> ẍ
0059 0178 // Y -> Ÿ
0079 00ff // y -> ÿ
0020 0022 // -> "
//}}}
"""
)
kalamine-0.38/tests/test_serializer_xkb.py 0000664 0000000 0000000 00000042507 14650261713 0021032 0 ustar 00root root 0000000 0000000 from textwrap import dedent
from kalamine import KeyboardLayout
from kalamine.generators.xkb import xkb_table
from .util import get_layout_dict
def load_layout(filename: str) -> KeyboardLayout:
return KeyboardLayout(get_layout_dict(filename))
def split(multiline_str: str):
return dedent(multiline_str).lstrip().rstrip().splitlines()
def test_ansi():
layout = load_layout("ansi")
expected = split(
"""
// Digits
key {[ 1 , exclam , VoidSymbol , VoidSymbol ]}; // 1 !
key {[ 2 , at , VoidSymbol , VoidSymbol ]}; // 2 @
key {[ 3 , numbersign , VoidSymbol , VoidSymbol ]}; // 3 #
key {[ 4 , dollar , VoidSymbol , VoidSymbol ]}; // 4 $
key {[ 5 , percent , VoidSymbol , VoidSymbol ]}; // 5 %
key {[ 6 , asciicircum , VoidSymbol , VoidSymbol ]}; // 6 ^
key {[ 7 , ampersand , VoidSymbol , VoidSymbol ]}; // 7 &
key {[ 8 , asterisk , VoidSymbol , VoidSymbol ]}; // 8 *
key {[ 9 , parenleft , VoidSymbol , VoidSymbol ]}; // 9 (
key {[ 0 , parenright , VoidSymbol , VoidSymbol ]}; // 0 )
// Letters, first row
key {[ q , Q , VoidSymbol , VoidSymbol ]}; // q Q
key {[ w , W , VoidSymbol , VoidSymbol ]}; // w W
key {[ e , E , VoidSymbol , VoidSymbol ]}; // e E
key {[ r , R , VoidSymbol , VoidSymbol ]}; // r R
key {[ t , T , VoidSymbol , VoidSymbol ]}; // t T
key {[ y , Y , VoidSymbol , VoidSymbol ]}; // y Y
key {[ u , U , VoidSymbol , VoidSymbol ]}; // u U
key {[ i , I , VoidSymbol , VoidSymbol ]}; // i I
key {[ o , O , VoidSymbol , VoidSymbol ]}; // o O
key {[ p , P , VoidSymbol , VoidSymbol ]}; // p P
// Letters, second row
key {[ a , A , VoidSymbol , VoidSymbol ]}; // a A
key {[ s , S , VoidSymbol , VoidSymbol ]}; // s S
key {[ d , D , VoidSymbol , VoidSymbol ]}; // d D
key {[ f , F , VoidSymbol , VoidSymbol ]}; // f F
key {[ g , G , VoidSymbol , VoidSymbol ]}; // g G
key {[ h , H , VoidSymbol , VoidSymbol ]}; // h H
key {[ j , J , VoidSymbol , VoidSymbol ]}; // j J
key {[ k , K , VoidSymbol , VoidSymbol ]}; // k K
key {[ l , L , VoidSymbol , VoidSymbol ]}; // l L
key {[ semicolon , colon , VoidSymbol , VoidSymbol ]}; // ; :
// Letters, third row
key {[ z , Z , VoidSymbol , VoidSymbol ]}; // z Z
key {[ x , X , VoidSymbol , VoidSymbol ]}; // x X
key {[ c , C , VoidSymbol , VoidSymbol ]}; // c C
key {[ v , V , VoidSymbol , VoidSymbol ]}; // v V
key {[ b , B , VoidSymbol , VoidSymbol ]}; // b B
key {[ n , N , VoidSymbol , VoidSymbol ]}; // n N
key {[ m , M , VoidSymbol , VoidSymbol ]}; // m M
key {[ comma , less , VoidSymbol , VoidSymbol ]}; // , <
key {[ period , greater , VoidSymbol , VoidSymbol ]}; // . >
key {[ slash , question , VoidSymbol , VoidSymbol ]}; // / ?
// Pinky keys
key {[ minus , underscore , VoidSymbol , VoidSymbol ]}; // - _
key {[ equal , plus , VoidSymbol , VoidSymbol ]}; // = +
key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; //
key {[ bracketleft , braceleft , VoidSymbol , VoidSymbol ]}; // [ {
key {[ bracketright , braceright , VoidSymbol , VoidSymbol ]}; // ] }
key {[ apostrophe , quotedbl , VoidSymbol , VoidSymbol ]}; // ' "
key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; //
key {[ grave , asciitilde , VoidSymbol , VoidSymbol ]}; // ` ~
key {[ backslash , bar , VoidSymbol , VoidSymbol ]}; // \\ |
key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; //
// Space bar
key {[ space , space , apostrophe , apostrophe ]}; // ' '
"""
)
xkbcomp = xkb_table(layout, xkbcomp=True)
assert len(xkbcomp) == len(expected)
assert xkbcomp == expected
xkbpatch = xkb_table(layout, xkbcomp=False)
assert len(xkbpatch) == len(expected)
assert xkbpatch == expected
def test_intl():
layout = load_layout("intl")
expected = split(
"""
// Digits
key {[ 1 , exclam , VoidSymbol , VoidSymbol ]}; // 1 !
key {[ 2 , at , VoidSymbol , VoidSymbol ]}; // 2 @
key {[ 3 , numbersign , VoidSymbol , VoidSymbol ]}; // 3 #
key {[ 4 , dollar , VoidSymbol , VoidSymbol ]}; // 4 $
key {[ 5 , percent , VoidSymbol , VoidSymbol ]}; // 5 %
key {[ 6 , dead_circumflex , VoidSymbol , VoidSymbol ]}; // 6 ^
key {[ 7 , ampersand , VoidSymbol , VoidSymbol ]}; // 7 &
key {[ 8 , asterisk , VoidSymbol , VoidSymbol ]}; // 8 *
key {[ 9 , parenleft , VoidSymbol , VoidSymbol ]}; // 9 (
key {[ 0 , parenright , VoidSymbol , VoidSymbol ]}; // 0 )
// Letters, first row
key {[ q , Q , VoidSymbol , VoidSymbol ]}; // q Q
key {[ w , W , VoidSymbol , VoidSymbol ]}; // w W
key {[ e , E , eacute , Eacute ]}; // e E é É
key {[ r , R , VoidSymbol , VoidSymbol ]}; // r R
key {[ t , T , VoidSymbol , VoidSymbol ]}; // t T
key {[ y , Y , VoidSymbol , VoidSymbol ]}; // y Y
key {[ u , U , uacute , Uacute ]}; // u U ú Ú
key {[ i , I , iacute , Iacute ]}; // i I í Í
key {[ o , O , oacute , Oacute ]}; // o O ó Ó
key {[ p , P , VoidSymbol , VoidSymbol ]}; // p P
// Letters, second row
key {[ a , A , aacute , Aacute ]}; // a A á Á
key {[ s , S , VoidSymbol , VoidSymbol ]}; // s S
key {[ d , D , VoidSymbol , VoidSymbol ]}; // d D
key {[ f , F , VoidSymbol , VoidSymbol ]}; // f F
key {[ g , G , VoidSymbol , VoidSymbol ]}; // g G
key {[ h , H , VoidSymbol , VoidSymbol ]}; // h H
key {[ j , J , VoidSymbol , VoidSymbol ]}; // j J
key {[ k , K , VoidSymbol , VoidSymbol ]}; // k K
key {[ l , L , VoidSymbol , VoidSymbol ]}; // l L
key {[ semicolon , colon , VoidSymbol , VoidSymbol ]}; // ; :
// Letters, third row
key {[ z , Z , VoidSymbol , VoidSymbol ]}; // z Z
key {[ x , X , VoidSymbol , VoidSymbol ]}; // x X
key {[ c , C , ccedilla , Ccedilla ]}; // c C ç Ç
key {[ v , V , VoidSymbol , VoidSymbol ]}; // v V
key {[ b , B , VoidSymbol , VoidSymbol ]}; // b B
key {[ n , N , VoidSymbol , VoidSymbol ]}; // n N
key {[ m , M , VoidSymbol , VoidSymbol ]}; // m M
key {[ comma , less , VoidSymbol , VoidSymbol ]}; // , <
key {[ period , greater , ellipsis , VoidSymbol ]}; // . > …
key {[ slash , question , VoidSymbol , VoidSymbol ]}; // / ?
// Pinky keys
key {[ minus , underscore , VoidSymbol , VoidSymbol ]}; // - _
key {[ equal , plus , VoidSymbol , VoidSymbol ]}; // = +
key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; //
key {[ bracketleft , braceleft , VoidSymbol , VoidSymbol ]}; // [ {
key {[ bracketright , braceright , VoidSymbol , VoidSymbol ]}; // ] }
key {[ ISO_Level3_Latch, dead_diaeresis , apostrophe , VoidSymbol ]}; // ' ¨ '
key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; //
key {[ dead_grave , dead_tilde , VoidSymbol , VoidSymbol ]}; // ` ~
key {[ backslash , bar , VoidSymbol , VoidSymbol ]}; // \\ |
key {[ backslash , bar , VoidSymbol , VoidSymbol ]}; // \\ |
// Space bar
key {[ space , space , apostrophe , apostrophe ]}; // ' '
"""
)
xkbcomp = xkb_table(layout, xkbcomp=True)
assert len(xkbcomp) == len(expected)
assert xkbcomp == expected
xkbpatch = xkb_table(layout, xkbcomp=False)
assert len(xkbpatch) == len(expected)
assert xkbpatch == expected
def test_prog():
layout = load_layout("prog")
expected = split(
"""
// Digits
key {[ 1 , exclam , exclam , VoidSymbol ]}; // 1 ! !
key {[ 2 , at , parenleft , VoidSymbol ]}; // 2 @ (
key {[ 3 , numbersign , parenright , VoidSymbol ]}; // 3 # )
key {[ 4 , dollar , apostrophe , VoidSymbol ]}; // 4 $ '
key {[ 5 , percent , quotedbl , VoidSymbol ]}; // 5 % "
key {[ 6 , asciicircum , dead_circumflex , VoidSymbol ]}; // 6 ^ ^
key {[ 7 , ampersand , 7 , VoidSymbol ]}; // 7 & 7
key {[ 8 , asterisk , 8 , VoidSymbol ]}; // 8 * 8
key {[ 9 , parenleft , 9 , VoidSymbol ]}; // 9 ( 9
key {[ 0 , parenright , slash , VoidSymbol ]}; // 0 ) /
// Letters, first row
key {[ q , Q , equal , VoidSymbol ]}; // q Q =
key {[ w , W , less , lessthanequal ]}; // w W < ≤
key {[ e , E , greater , greaterthanequal]}; // e E > ≥
key {[ r , R , minus , VoidSymbol ]}; // r R -
key {[ t , T , plus , VoidSymbol ]}; // t T +
key {[ y , Y , VoidSymbol , VoidSymbol ]}; // y Y
key {[ u , U , 4 , VoidSymbol ]}; // u U 4
key {[ i , I , 5 , VoidSymbol ]}; // i I 5
key {[ o , O , 6 , VoidSymbol ]}; // o O 6
key {[ p , P , asterisk , VoidSymbol ]}; // p P *
// Letters, second row
key {[ a , A , braceleft , VoidSymbol ]}; // a A {
key {[ s , S , bracketleft , VoidSymbol ]}; // s S [
key {[ d , D , bracketright , VoidSymbol ]}; // d D ]
key {[ f , F , braceright , VoidSymbol ]}; // f F }
key {[ g , G , slash , VoidSymbol ]}; // g G /
key {[ h , H , VoidSymbol , VoidSymbol ]}; // h H
key {[ j , J , 1 , VoidSymbol ]}; // j J 1
key {[ k , K , 2 , VoidSymbol ]}; // k K 2
key {[ l , L , 3 , VoidSymbol ]}; // l L 3
key {[ semicolon , colon , minus , VoidSymbol ]}; // ; : -
// Letters, third row
key {[ z , Z , asciitilde , VoidSymbol ]}; // z Z ~
key {[ x , X , grave , VoidSymbol ]}; // x X `
key {[ c , C , bar , brokenbar ]}; // c C | ¦
key {[ v , V , underscore , VoidSymbol ]}; // v V _
key {[ b , B , backslash , VoidSymbol ]}; // b B \\
key {[ n , N , VoidSymbol , VoidSymbol ]}; // n N
key {[ m , M , 0 , VoidSymbol ]}; // m M 0
key {[ comma , less , comma , VoidSymbol ]}; // , < ,
key {[ period , greater , period , VoidSymbol ]}; // . > .
key {[ slash , question , plus , VoidSymbol ]}; // / ? +
// Pinky keys
key {[ minus , underscore , VoidSymbol , VoidSymbol ]}; // - _
key {[ equal , plus , VoidSymbol , VoidSymbol ]}; // = +
key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; //
key {[ bracketleft , braceleft , VoidSymbol , VoidSymbol ]}; // [ {
key {[ bracketright , braceright , VoidSymbol , VoidSymbol ]}; // ] }
key {[ apostrophe , quotedbl , dead_acute , dead_diaeresis ]}; // ' " ´ ¨
key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; //
key {[ grave , asciitilde , dead_grave , dead_tilde ]}; // ` ~ ` ~
key {[ backslash , bar , VoidSymbol , VoidSymbol ]}; // \\ |
key {[ VoidSymbol , VoidSymbol , VoidSymbol , VoidSymbol ]}; //
// Space bar
key {[ space , space , space , space ]}; //
"""
)
xkbcomp = xkb_table(layout, xkbcomp=True)
assert len(xkbcomp) == len(expected)
assert xkbcomp == expected
xkbpatch = xkb_table(layout, xkbcomp=False)
assert len(xkbpatch) == len(expected)
assert xkbpatch == expected
kalamine-0.38/tests/util.py 0000664 0000000 0000000 00000000526 14650261713 0015726 0 ustar 00root root 0000000 0000000 """Some util functions for tests."""
from pathlib import Path
from typing import Dict
import tomli
def get_layout_dict(filename: str) -> Dict:
"""Return the layout directory path."""
file_path = Path(__file__).parent.parent / f"layouts/{filename}.toml"
with file_path.open(mode="rb") as file:
return tomli.load(file)
kalamine-0.38/watch.png 0000664 0000000 0000000 00000243464 14650261713 0015063 0 ustar 00root root 0000000 0000000 PNG
IHDR ' 4!o IDATx^e@LDQ1QlQ;>[n,DBTTTTP?̞={~uygggy~- d" " " " " " " "3b&݉84D@D@D@D@D@D@D@!PsàAfmژc"SjNtXw]3Cf" " " " " " " ?f,mW"WO;(KtxN30l%