pax_global_header00006660000000000000000000000064146502617130014517gustar00rootroot0000000000000052 comment=df2b4f86ab94c1163136a2da0a1efd54e2bfde26 kalamine-0.38/000077500000000000000000000000001465026171300132325ustar00rootroot00000000000000kalamine-0.38/.git-blame-ignore-revs000066400000000000000000000000671465026171300173350ustar00rootroot00000000000000# Black+isort 251e99868478af19a02ccafa9e2f96a7e169eb51 kalamine-0.38/.github/000077500000000000000000000000001465026171300145725ustar00rootroot00000000000000kalamine-0.38/.github/workflows/000077500000000000000000000000001465026171300166275ustar00rootroot00000000000000kalamine-0.38/.github/workflows/tests.yml000066400000000000000000000031231465026171300205130ustar00rootroot00000000000000name: Test on: push: paths-ignore: - "layouts/**" - "*.md" - "*.rst" pull_request: paths-ignore: - "layouts/**" - "*.md" - "*.rst" permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true jobs: main: # We want to run on external PRs, but not on our own internal PRs as they'll be run # by the push to the branch. Without this if check, checks are duplicated since # internal PRs match both the push and pull_request events. if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install ".[dev]" - name: Run tests run: | python -m kalamine.cli build layouts/*.toml python -m kalamine.cli new test.toml pytest - name: Run black run: black kalamine --check - name: Run isort run: isort kalamine --check - name: Run ruff run: ruff check kalamine - name: Run mypy run: mypy kalamine kalamine-0.38/.gitignore000066400000000000000000000022511465026171300152220ustar00rootroot00000000000000*.vim* *.bak xkb/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # vscode .vscodekalamine-0.38/CONTRIBUTING.md000066400000000000000000000042241465026171300154650ustar00rootroot00000000000000Contributing to Kalamine ================================================================================ Setup -------------------------------------------------------------------------------- After checking out the repository, you can install kalamine and its development dependencies like this: ```bash python3 -m pip install --user .[dev] ``` Which is the equivalent of: ```bash python3 -m pip install --user -e . python3 -m pip install --user build black isort ruff pytest mypy types-PyYAML pre-commit ``` There’s also a Makefile recipe for that: ```bash make dev ``` Code Formating -------------------------------------------------------------------------------- We rely on [black][1] and [isort][2] for that, with their default configurations: ```bash black kalamine isort kalamine ``` Alternative: ```bash make format ``` [1]: https://black.readthedocs.io [2]: https://pycqa.github.io/isort/ Code Linting -------------------------------------------------------------------------------- We rely on [ruff][3] and [mypy][4] for that, with their default configurations: ```bash black --check --quiet kalamine isort --check --quiet kalamine ruff check kalamine mypy kalamine ``` Alternative: ```bash make lint ``` Many linting errors can be fixed automatically: ```bash ruff check --fix kalamine ``` [3]: https://docs.astral.sh/ruff/ [4]: https://mypy.readthedocs.io Unit Tests -------------------------------------------------------------------------------- We rely on [pytest][5] for that, but the sample layouts must be built by kalamine first: ```bash python3 -m kalamine.cli make layouts/*.toml pytest ``` Alternative: ```bash make test ``` [5]: https://docs.pytest.org Before Committing -------------------------------------------------------------------------------- You may ensure manually that your commit will pass the Github CI (continuous integration) with: ```bash make ``` But setting up a git pre-commit hook is strongly recommended. Just create an executable `.git/hooks/pre-commit` file containing: ```bash #!/bin/sh make ``` This is asking git to run the above command before any commit is created, and to abort the commit if it fails. kalamine-0.38/LICENSE000066400000000000000000000020601465026171300142350ustar00rootroot00000000000000MIT License Copyright (c) 2018 Fabien Cazenave Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kalamine-0.38/Makefile000066400000000000000000000015331465026171300146740ustar00rootroot00000000000000.PHONY: all dev test lint publish format clean PYTHON3?=python3 all: format lint test dev: ## Install a development environment $(PYTHON3) -m pip install --user --upgrade -e .[dev] # $(PYTHON3) -m pip install --user --upgrade build # $(PYTHON3) -m pip install --user --upgrade twine wheel format: ## Format sources black kalamine isort kalamine lint: ## Lint sources black --check --quiet kalamine isort --check --quiet kalamine ruff check kalamine mypy kalamine test: ## Run tests $(PYTHON3) -m kalamine.cli guide > docs/README.md $(PYTHON3) -m kalamine.cli build layouts/*.toml $(PYTHON3) -m pytest publish: test ## Publish package rm -rf dist/* $(PYTHON3) -m build twine check dist/* twine upload dist/* clean: ## Clean sources rm -rf build rm -rf dist rm -rf include rm -rf kalamine.egg-info rm -rf kalamine/__pycache__ kalamine-0.38/README.rst000066400000000000000000000252131465026171300147240ustar00rootroot00000000000000Kalamine ================================================================================ A text-based, cross-platform Keyboard Layout Maker. Install -------------------------------------------------------------------------------- To install kalamine, all you need is a Python 3.8+ environment and ``pip``: .. code-block:: bash # to install kalamine python3 -m pip install --user kalamine # to upgrade kalamine python3 -m pip install --user --upgrade kalamine # to uninstall kalamine python3 -m pip uninstall --user kalamine However, we recommend using pipx_ rather than ``pip`` as it provides ``pyenv`` containment, which is a much saner approach and is becoming mandatory on many operating systems (e.g. Arch Linux). It is even simpler from a user perspective: .. code-block:: bash # to install kalamine pipx install kalamine # to upgrade kalamine pipx upgrade kalamine # to uninstall kalamine pipx uninstall kalamine Arch Linux users may use the `AUR package`_: .. code-block:: bash yay -S kalamine-git Developer-specific installation instructions can be found in the CONTRIBUTING.md_ file. .. _pipx: https://pipx.pypa.io .. _`AUR package`: https://aur.archlinux.org/packages/kalamine-git .. _CONTRIBUTING.md: https://github.com/OneDeadKey/kalamine/blob/main/CONTRIBUTING.md Building Distributable Layouts -------------------------------------------------------------------------------- Create a keyboard layout with ``kalamine new``: .. code-block:: bash kalamine new layout.toml # basic layout kalamine new layout.toml --altgr # layout with an AltGr layer kalamine new layout.toml --1dk # layout with a custom dead key kalamine new layout.toml --geometry ERGO # apply an ortholinear geometry Edit this layout with your preferred text editor: - the `user guide`_ is available at the end of the ``*.toml`` file - the layout can be rendered and emulated with ``kalamine watch`` (see next section) .. _`user guide`: https://github.com/OneDeadKey/kalamine/tree/master/docs Build your layout: .. code-block:: bash kalamine build layout.toml Get all distributable keyboard drivers: .. code-block:: bash dist/ ├─ layout.ahk # Windows (user) ├─ layout.klc # Windows (admin) ├─ layout.keylayout # macOS ├─ layout.xkb_keymap # Linux (user) ├─ layout.xkb_symbols # Linux (root) ├─ layout.json # web └─ layout.svg You can also ask for a single target by specifying the file extension: .. code-block:: bash kalamine build layout.toml --out layout.xkb_symbols Emulating Layouts -------------------------------------------------------------------------------- Your layout can be emulated in a browser — including dead keys and an AltGr layer, if any. .. code-block:: bash $ kalamine watch layout.toml Server started: http://localhost:1664 Check your browser, type in the input area, test your layout. Changes on your TOML file are auto-detected and reloaded automatically. .. image:: watch.png Press Ctrl-C when you’re done, and kalamine will write all platform-specific files. Using Distributable Layouts -------------------------------------------------------------------------------- Windows (user): ``*.ahk`` ````````````````````````` * download the `AHK 1.1 archive`_ * load the ``*.ahk`` script with it. The keyboard layout appears in the notification area. It can be enabled/disabled by pressing both Alt keys. .. _`AHK 1.1 archive`: https://www.autohotkey.com/download/ahk.zip You may also use Ahk2Exe to turn your ``*.ahk`` script into an executable file. The ``U32 Unicode 32-bit.bin`` setting seems to work fine. Windows (admin): ``*.klc`` `````````````````````````` Note: this applies only if you want to use the ``*.klc`` file. A better approach is to use ``wkalamine`` (see below). * get a keyboard layout installer: MSKLC_ (freeware) or KbdEdit_ (shareware); * load the ``*.klc`` file with it; * run this installer to generate a setup program; * run the setup program; * :strong:`restart your session`, even if Windows doesn’t ask you to. The keyboard layout appears in the language bar. Note: in some cases, custom dead keys may not be supported any more by MSKLC on Windows 10/11. KbdEdit works fine, but its installers are not signed. WKalamine works fine as well (see below) and its installers are signed. Basic developer info available in Kalamine’s `KLC documentation page`_. .. _MSKLC: https://www.microsoft.com/en-us/download/details.aspx?id=102134 .. _KbdEdit: http://www.kbdedit.com/ .. _`KLC documentation page`: https://github.com/OneDeadKey/kalamine/tree/master/docs/klc.md macOS: ``*.keylayout`` `````````````````````` * copy your ``*.keylayout`` file into: * either ``~/Library/Keyboard Layouts`` for the current user only, * or ``/Library/Keyboard Layouts`` for all users; * restart your session. The keyboard layout appears in the “Language and Text” preferences, “Input Methods” tab. Linux (root): ``*.xkb_symbols`` ``````````````````````````````` :strong:`This is by far the simplest method to install a custom keyboard layout on Linux.` Recent versions of XKB allow *one* custom keyboard layout in root space: .. code-block:: bash sudo cp layout.xkb_symbols ${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}/symbols/custom Your keyboard layout will be listed as “Custom” in the keyboard settings. This works on both Wayland and X.Org. Depending on your system, you might have to relog to your session or to reboot X completely. On X.Org you can also select your keyboard layout from the command line: .. code-block:: bash setxkbmap custom # select your keyboard layout setxkbmap us # get back to QWERTY On Wayland, this depends on your compositor. For Sway, tweak your keyboard input section like this: .. code-block:: properties input type:keyboard { xkb_layout "custom" } Linux (user): ``*.xkb_keymap`` `````````````````````````````` ``*.xkb_keymap`` keyboard descriptions can be applied in user-space. The main limitation is that the keyboard layout won’t show up in the keyboard settings. On X.Org it is straight-forward with ``xkbcomp``: .. code-block:: bash xkbcomp -w10 layout.xkb_keymap $DISPLAY Again, ``setxkbmap`` can be used to get back to the standard us-qwerty layout on X.Org: .. code-block:: bash setxkbmap us On Wayland, this depends on your compositor. For Sway, tweak your keyboard input section like this: .. code-block:: properties input type:keyboard { xkb_file /path/to/layout.xkb_keymap } WKalamine -------------------------------------------------------------------------------- ``wkalamine`` is a Windows-specific CLI tool to create MSKLC_ setup packages. This is kind of a hack, but it provides an automatic way to build setup packages on Windows and more importantly, these setup packages overcome MSKLC’s limitations regarding chained dead keys and AltGr+Space combos. It is done by generating the C layout file, and tricking MSKLC to use it by setting it as read-only before. Make sure MSKLC is installed and build your installer: .. code-block:: bash wkalamine build layout.toml and you should get a ``[layout]\setup.exe`` executable to install the layout. Remember to log out and log back in to apply the changes. XKalamine -------------------------------------------------------------------------------- ``xkalamine`` is a Linux-specific CLI tool for installing and managing keyboard layouts with XKB, so that they can be listed in the system’s keyboard preferences. Wayland (user) `````````````` On *most* Wayland environments, keyboard layouts can be installed in user-space: .. code-block:: bash # Install a YAML/TOML keyboard layout into ~/.config/xkb xkalamine install layout.toml # Uninstall Kalamine layouts from ~/.config/xkb xkalamine remove us/prog # remove the kalamine 'prog' layout xkalamine remove fr # remove all kalamine layouts for French xkalamine remove "*" # remove all kalamine layouts # List available keyboard layouts xkalamine list # list all kalamine layouts xkalamine list fr # list all kalamine layouts for French xkalamine list us --all # list all layouts for US English xkalamine list --all # list all layouts, ordered by locale Once installed, layouts are selectable in the desktop environment’s keyboard preferences. On Sway, you can also select a layout like this: .. code-block:: properties input type:keyboard { xkb_layout "us" xkb_variant "prog" } Note: some desktops like KDE Plasma, despite using Wayland, do not support keyboards layouts in ``~/.config:xkb`` out of the box. In such cases, using ``xkalamine`` as ``sudo`` is required, as described below. X.Org (root) ```````````` On X.Org, a layout can be applied on the fly in user-space: .. code-block:: bash # Equivalent to `xkbcomp -w10 layout.xkb_keymap $DISPLAY` xkalamine apply layout.toml However, installing a layout so it can be selected in the keyboard preferences requires ``sudo`` privileges: .. code-block:: bash # Install a YAML/TOML keyboard layout into /usr/share/X11/xkb sudo env "PATH=$PATH" xkalamine install layout.toml # Uninstall Kalamine layouts from /usr/share/X11/xkb sudo env "PATH=$PATH" xkalamine remove us/prog sudo env "PATH=$PATH" xkalamine remove fr sudo env "PATH=$PATH" xkalamine remove "*" Once installed, you can apply a keyboard layout like this: .. code-block:: bash setxkbmap us -variant prog Note that updating XKB will delete all layouts installed using ``sudo xkalamine install``. Sadly, it seems there’s no way to install keyboard layouts in ``~/.config/xkb`` for X.Org. The system keyboard preferences will probably list user-space kayouts, but they won’t be usable on X.Org. If you want custom keymaps on your machine, switch to Wayland (and/or fix any remaining issues preventing you from doing so) instead of hoping this will ever work on X. -- `Peter Hutterer`_ .. _`Peter Hutterer`: https://who-t.blogspot.com/2020/09/no-user-specific-xkb-configuration-in-x.html Resources ````````` XKB is a tricky piece of software. The following resources might be helpful if you want to dig in: * https://www.charvolant.org/doug/xkb/html/ * https://wiki.archlinux.org/title/X_keyboard_extension * https://wiki.archlinux.org/title/Xorg/Keyboard_configuration * https://github.com/xkbcommon/libxkbcommon/blob/master/doc/keymap-format-text-v1.md Alternative -------------------------------------------------------------------------------- * https://github.com/39aldo39/klfc kalamine-0.38/docs/000077500000000000000000000000001465026171300141625ustar00rootroot00000000000000kalamine-0.38/docs/README.md000066400000000000000000000227421465026171300154500ustar00rootroot00000000000000Defining a Keyboard Layout ================================================================================ Kalamine keyboard layouts are defined with TOML files including this kind of ASCII-art layer templates: ``` full = ''' ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓ │ ~*~ │ ! │ @ │ # │ $ │ % │ ^ │ & │ * │ ( │ ) │ _ │ + ┃ ┃ │ `*` │ 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 ┃ ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛ ''' ``` Layers -------------------------------------------------------------------------------- ### base The `base` layer contains the base and shifted keys: +-----+ shift -------> | ? | base --------> | / | +-----+ When the base and shift keys correspond to the same character, you may only specify the uppercase char: +-----+ shift -------> | A | (base = a) --> | | +-----+ ### altgr The `altgr` layer contains the altgr and shift+altgr symbols: +-----+ | | <----- (altgr+shift+key is undefined) | { | <----- altgr+key = { +-----+ ### full The `full` view lets you specify the `base` and `altgr` levels together: +-----+ shift -------> | A | <----- (altgr+shift+key is undefined) (base = a) --> | { | <----- altgr+key = { +-----+ Dead Keys -------------------------------------------------------------------------------- ### Usage Dead keys are preceded by a `*` sign. They can be used in the `base` layer: +-----+ shift -------> |*" | = dead diaeresis base --------> |*´ | = dead acute accent +-----+ … as well as in the `altgr` layer: +-----+ | *" | <----- altgr+shift+key = dead diaeresis | *´ | <----- altgr+key = dead acute accent +-----+ … and combined in the `full` layer: +-----+ shift+key = A --> | A*" | <----- altgr+shift+key = dead diaeresis key = a --> | a*´ | <----- altgr+key = dead acute accent +-----+ ### Standard Dead Keys The following dead keys are supported, and their behavior cannot be customized: id XKB name base -> accented chars ---------------------------------------------------------------------------- *` grave AaEeIiNnOoUuWwYyЕеИи -> ÀàÈèÌìǸǹÒòÙùẀẁỲỳЀѐЍѝ *‟ doublegrave AaEeIiOoRrUuѴѴ -> ȀȁȄȅȈȉȌȍȐȑȔȕѶѷ *´ acute AaCcEeGgIiKkLlMmNnOoPpRrSsUuWwYyZzΑαΕεΗηΙιΟοΥυΩωГгКк -> ÁáĆćÉéǴǵÍíḰḱĹĺḾḿŃńÓóṔṕŔশÚúẂẃÝýŹźΆάΈέΉήΊίΌόΎύΏώЃѓЌќ *” doubleacute OoUuУу -> ŐőŰűӲӳ *^ circumflex AaCcEeGgHhIiJjOoSsUuWwYyZz0123456789()+-= -> ÂâĈĉÊêĜĝĤĥÎîĴĵÔôŜŝÛûŴŵŶŷẐẑ⁰¹²³⁴⁵⁶⁷⁸⁹⁽⁾⁺⁻⁼ *ˇ caron AaCcDdEeGgHhIiKkLlNnOoRrSsTtUuZzƷʒ0123456789()+-= -> ǍǎČčĎďĚěǦǧȞȟǏǐǨǩĽľŇňǑǒŘřŠšŤťǓǔŽžǮǯ₀₁₂₃₄₅₆₇₈₉₍₎₊₋₌ *˘ breve AaEeGgIiOoUuΑαΙιΥυАаЕеЖжИиУу -> ĂăĔĕĞğĬĭŎŏŬŭᾸᾰῘῐῨῠӐӑӖӗӁӂЙйЎў *⁻ invertedbreve AaEeIiOoUuRr -> ȂȃȆȇȊȋȎȏȖȗȒȓ *~ tilde AaEeIiNnOoUuVvYy<>= -> ÃãẼẽĨĩÑñÕõŨũṼṽỸỹ≲≳≃ *¯ macron AaÆæEeGgIiOoUuYy -> ĀāǢǣĒēḠḡĪīŌōŪūȲȳ *¨ diaeresis AaEeHhIiOotUuWwXxYyΙιΥυАаЕеӘәЖжЗзИиІіОоӨөУуЧчЫыЭэ -> ÄäËëḦḧÏïÖöẗÜüẄẅẌẍŸÿΪϊΫϋӒӓЁёӚӛӜӝӞӟӤӥЇїӦӧӪӫӰӱӴӵӸӹӬӭ *˚ abovering AaUuwy -> ÅåŮůẘẙ *¸ cedilla CcDdEeGgHhKkLlNnRrSsTt -> ÇçḐḑȨȩĢģḨḩĶķĻļŅņŖŗŞşŢţ *, belowcomma SsTt -> ȘșȚț *˛ ogonek AaEeIiOoUu -> ĄąĘęĮįǪǫŲų */ stroke AaBbCcDdEeGgHhIiJjLlOoPpRrTtUuYyZz<≤≥>= -> ȺⱥɃƀȻȼĐđɆɇǤǥĦħƗɨɈɉŁłØøⱣᵽɌɍŦŧɄʉɎɏƵƶ≮≰≱≯≠ *˙ abovedot AaBbCcDdEeFfGgHhIijLlMmNnOoPpRrSsTtWwXxYyZz -> ȦȧḂḃĊċḊḋĖėḞḟĠġḢḣİıȷĿŀṀṁṄṅȮȯṖṗṘṙṠṡṪṫẆẇẊẋẎẏŻż *. belowdot AaBbDdEeHhIiKkLlMmNnOoRrSsTtUuVvWwYyZz -> ẠạḄḅḌḍẸẹḤḥỊịḲḳḶḷṂṃṆṇỌọṚṛṢṣṬṭỤụṾṿẈẉỴỵẒẓ *µ greek AaBbDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuWwXxYyZz -> ΑαΒβΔδΕεΦφΓγΗηΙιΘθΚκΛλΜμΝνΟοΠπΧχΡρΣσΤτΥυΩωΞξΨψΖζ *¤ currency AaBbÇCçcDdEeFfGgHhIiKkLlMmNnOoPpRrSsTtþÞUuWwYy -> ₳؋₱฿₵₡₵¢₯₫₠€₣ƒ₲₲₴₴៛﷼₭₭₤£ℳ₥₦₦૱௹₧₰₨₢$₪₮৳৲৲圓元₩₩円¥ ### Custom Dead Key There is one dead key (1dk), noted `**`, that can be customized by specifying how it modifies each character in the `base` layer: +-----+ shift -------> | ? ¿ | <----- 1dk, shift+key base --------> | / ÷ | <----- 1dk, key +-----+ When the base and shift keys correspond to the same accented character, you may only specify the lowercase accented char in the `base` layer: +-----+ shift -------> | A | <----- (1dk, shift+key = À) (base = a) --> | à | <----- 1dk, key = à +-----+ You may also chain dead keys by specifying a dead key in the `1dk` layer: +-----+ shift -------> | G | (base = g) --> | *µ | <----- 1dk, key = dead Greek +-----+ **Warning:** chained dead keys are not supported by MSKLC, and KbdEdit will be required to build a Windows driver for such a keyboard layout. Space Bar -------------------------------------------------------------------------------- Kalamine descriptor files have an optional section to define specific behaviors of the space bar in non-base layers: [spacebar] shift = "\u202f" # NARROW NO-BREAK SPACE altgr = "\u0020" # SPACE altgr_shift = "\u00a0" # NO-BREAK SPACE 1dk = "\u2019" # RIGHT SINGLE QUOTATION MARK 1dk_shift = "\u2019" # RIGHT SINGLE QUOTATION MARK Kalamine doesn’t support non-space chars on the `base` layer for the space bar. Space characters outside of the space bar are not supported either. kalamine-0.38/docs/klc.md000066400000000000000000000070131465026171300152560ustar00rootroot00000000000000KLC Considerations ================================================================================ Kalamine’s `*.klc` outputs can be used: - either directly by the Microsoft Keyboard Layout Creator, a.k.a. [MSKLC][1], freeware and unmaintained; - or as an import format by [KbdEdit][2], shareware and maintained. The Premium version is required to build distributable installers. [1]: https://www.microsoft.com/en-us/download/details.aspx?id=102134 [2]: http://www.kbdedit.com/ File Format -------------------------------------------------------------------------------- Beware: KLC files are encoded in UTF16-LE and require a BOM mark. KbdEdit is a bit picky: additional line breaks in dead key sections is enough to break the KLC support. Limitations -------------------------------------------------------------------------------- KbdEdit has the most extensive support: - no particular limitation on the AltGr key; - dead keys work fine, including chained dead keys (CDK); - the installer is a single executable file — though unsigned, which raises a Windows security warning. MSKLC produces a signed installer (multiple files) but comes with a few limitations: - AltGr+Spacebar cannot be remapped, making it unsuitable for Bépo; - chained dead keys are not supported, making it unsuitable for Ergo‑L; - since Windows 10 (maybe 8), some `1dk` features stopped working — e.g. QWERTY-Lafayette’s dead key works fine in the preview or when installed on Windows 7, but not on Windows 10 and 11. Note: even when chained dead keys are properly supported by the keyboard driver, they can still cause bugs in other Windows apps — *even without using CDKs*: the CDK support alone has side-effects that may create bugs, like [this AHK bug][3] for instance. [3]: https://github.com/AutoHotkey/AutoHotkey/pull/331 Alternatives -------------------------------------------------------------------------------- MSKLC comes up with a `kbdutools` executable allowing to build a keyboard driver DLL that doesn’t have MSKLC’s limitations (AltGr, CDKs…). The [Windows Driver Kit (WDK)][4] looks like an interesting alternative. There are even [keyboard layout samples][5] on github. Installing the DLL onto the system is tricky and implies manipulating the register base, but it can be done: - either by replacing the DLL produced by MSKLC in its installer, as described [in this page][6]; - or by trying to do it manually, as described in the [WinKbdLayouts project][7]. [4]: https://learn.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk [5]: https://github.com/microsoft/Windows-driver-samples/blob/main/input/layout/README.md [6]: http://accentuez.mon.nom.free.fr/Clavier-CreationClavier.php [7]: https://github.com/lelegard/winkbdlayouts Last but not least, the [Neo][10] team has done an impressive job regarding their Windows drivers, both for the standalone version ([ReNeo][11]) and the installable version ([KbdNeo2][12]). Kudos, folks! [10]: https://neo-layout.org [11]: https://github.com/Rojetto/ReNeo [12]: https://git.neo-layout.org/neo/neo-layout/src/branch/master/windows/kbdneo2 Links -------------------------------------------------------------------------------- - https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input - [Michael Kaplan’s archives](https://archives.miloush.net/michkap/archive/) (author of MSKLC) - [GOTCHAS for keyboard developers using MSKLC](https://metacpan.org/pod/UI::KeyboardLayout#WINDOWS-GOTCHAS-for-keyboard-developers-using-MSKLC) - https://kbdlayout.info/features/deadkeys kalamine-0.38/docs/xkb.md000066400000000000000000000024551465026171300152760ustar00rootroot00000000000000XKB Considerations ================================================================================ > If you want custom keymaps on your machine, switch to Wayland (and/or fix any > remaining issues preventing you from doing so) instead of hoping this will > ever work on X. — [Peter Hutterer](https://who-t.blogspot.com/2020/09/no-user-specific-xkb-configuration-in-x.html) - [User-configuration for Wayland](https://xkbcommon.org/doc/current/md_doc_2user-configuration.html) Resources -------------------------------------------------------------------------------- XKB is a tricky piece of software. The following resources might be helpful if you want to dig in: - [Ivan Pascal's XKB documentation](https://web.archive.org/web/20190724015820/http://pascal.tsu.ru/en/xkb/) - [An Unreliable Guide to XKB Configuration](https://www.charvolant.org/doug/xkb/html/) - [How to enhance XKB configuration](How to enhance XKB configuration) - xkbcommon: - [XKB introduction](https://xkbcommon.org/doc/current/xkb-intro.html) - [The XKB keymap text format, V1](https://xkbcommon.org/doc/current/keymap-text-format-v1.html) - ArchLinux wiki: - [X keyboard extension](https://wiki.archlinux.org/title/X_keyboard_extension) - [Xorg/Keyboard configuration](https://wiki.archlinux.org/title/Xorg/Keyboard_configuration) kalamine-0.38/kalamine/000077500000000000000000000000001465026171300150135ustar00rootroot00000000000000kalamine-0.38/kalamine/__init__.py000066400000000000000000000001711465026171300171230ustar00rootroot00000000000000#!/usr/bin/env python3 from .layout import KeyboardLayout from .xkb_manager import XKBManager KeyboardLayout XKBManager kalamine-0.38/kalamine/cli.py000066400000000000000000000150671465026171300161450ustar00rootroot00000000000000#!/usr/bin/env python3 from contextlib import contextmanager from importlib import metadata from pathlib import Path from typing import Iterator, List, Literal, Union import click from .generators import ahk, keylayout, klc, web, xkb from .help import create_layout, user_guide from .layout import KeyboardLayout, load_layout from .server import keyboard_server @click.group() def cli() -> None: ... def build_all(layout: KeyboardLayout, output_dir_path: Path) -> None: """Generate all layout output files. Parameters ---------- layout : KeyboardLayout The layout to process. output_dir_path : Path The output directory. msklc_dir : Path The MSKLC installation directory. """ @contextmanager def file_creation_context(ext: str = "") -> Iterator[Path]: """Generate an output file path for extension EXT, return it and finally echo info.""" path = output_dir_path / (layout.meta["fileName"] + ext) yield path click.echo(f"... {path}") if not output_dir_path.exists(): output_dir_path.mkdir(parents=True) # AHK driver with file_creation_context(".ahk") as ahk_path: with ahk_path.open("w", encoding="utf-8", newline="\n") as file: file.write("\uFEFF") # AHK scripts require a BOM file.write(ahk.ahk(layout)) # Windows driver with file_creation_context(".klc") as klc_path: with klc_path.open("w", encoding="utf-16le", newline="\r\n") as file: try: file.write(klc.klc(layout)) except ValueError as err: print(err) # macOS driver with file_creation_context(".keylayout") as osx_path: with osx_path.open("w", encoding="utf-8", newline="\n") as file: file.write(keylayout.keylayout(layout)) # Linux driver, user-space with file_creation_context(".xkb_keymap") as xkb_path: with xkb_path.open("w", encoding="utf-8", newline="\n") as file: file.write(xkb.xkb_keymap(layout)) # Linux driver, root with file_creation_context(".xkb_symbols") as xkb_custom_path: with xkb_custom_path.open("w", encoding="utf-8", newline="\n") as file: file.write(xkb.xkb_symbols(layout)) # JSON data with file_creation_context(".json") as json_path: json_path.write_text(web.pretty_json(layout), encoding="utf8") # SVG data with file_creation_context(".svg") as svg_path: web.svg(layout).write(svg_path, encoding="utf-8", xml_declaration=True) @cli.command() @click.argument( "layout_descriptors", nargs=-1, type=click.Path(exists=True, dir_okay=False, path_type=Path), ) @click.option( "--out", default="all", type=click.Path(), help="Keyboard drivers to generate.", ) @click.option( "--angle-mod/--no-angle-mod", default=False, help="Apply Angle-Mod (which is a [ZXCVB] permutation with the LSGT key (a.k.a. ISO key))", ) @click.option( "--qwerty-shortcuts", default=False, is_flag=True, help="Keep shortcuts at their qwerty location", ) def build( layout_descriptors: List[Path], out: Union[Path, Literal["all"]], angle_mod: bool, qwerty_shortcuts: bool, ) -> None: """Convert TOML/YAML descriptions into OS-specific keyboard drivers.""" for input_file in layout_descriptors: layout = KeyboardLayout(load_layout(input_file), angle_mod, qwerty_shortcuts) # default: build all in the `dist` subdirectory if out == "all": build_all(layout, Path("dist")) continue # quick output: reuse the input name and change the file extension if out in ["keylayout", "klc", "xkb_keymap", "xkb_symbols", "svg"]: output_file = input_file.with_suffix(f".{out}") else: output_file = Path(out) # detailed output if output_file.suffix == ".ahk": with output_file.open("w", encoding="utf-8", newline="\n") as file: file.write("\uFEFF") # AHK scripts require a BOM file.write(ahk.ahk(layout)) elif output_file.suffix == ".klc": with output_file.open("w", encoding="utf-16le", newline="\r\n") as file: try: file.write(klc.klc(layout)) except ValueError as err: print(err) elif output_file.suffix == ".keylayout": with output_file.open("w", encoding="utf-8", newline="\n") as file: file.write(keylayout.keylayout(layout)) elif output_file.suffix == ".xkb_keymap": with output_file.open("w", encoding="utf-8", newline="\n") as file: file.write(xkb.xkb_keymap(layout)) elif output_file.suffix == ".xkb_symbols": with output_file.open("w", encoding="utf-8", newline="\n") as file: file.write(xkb.xkb_symbols(layout)) elif output_file.suffix == ".json": output_file.write_text(web.pretty_json(layout), encoding="utf8") elif output_file.suffix == ".svg": web.svg(layout).write(output_file, encoding="utf-8", xml_declaration=True) else: click.echo("Unsupported output format.", err=True) return # successfully converted, display file name click.echo(f"... {output_file}") # TODO: Provide geometry choices @cli.command() @click.argument("output_file", nargs=1, type=click.Path(exists=False, path_type=Path)) @click.option("--geometry", default="ISO", help="Specify keyboard geometry.") @click.option("--altgr/--no-altgr", default=False, help="Set an AltGr layer.") @click.option("--1dk/--no-1dk", "odk", default=False, help="Set a custom dead key.") def new(output_file: Path, geometry: str, altgr: bool, odk: bool) -> None: """Create a new TOML layout description.""" create_layout(output_file, geometry, altgr, odk) click.echo(f"... {output_file}") @cli.command() @click.argument("filepath", nargs=1, type=click.Path(exists=True, path_type=Path)) @click.option( "--angle-mod/--no-angle-mod", default=False, help="Apply Angle-Mod (which is a [ZXCVB] permutation with the LSGT key (a.k.a. ISO key))", ) def watch(filepath: Path, angle_mod: bool) -> None: """Watch a layout description file and display it in a web browser.""" keyboard_server(filepath, angle_mod) @cli.command() def guide() -> None: """Show user guide and exit.""" click.echo(user_guide()) @cli.command() def version() -> None: """Show version number and exit.""" click.echo(f"kalamine { metadata.version('kalamine') }") if __name__ == "__main__": cli() kalamine-0.38/kalamine/cli_msklc.py000066400000000000000000000106111465026171300173240ustar00rootroot00000000000000#!/usr/bin/env python3 import platform import sys from pathlib import Path from typing import List import click from .layout import KeyboardLayout, load_layout from .msklc_manager import MsklcManager @click.group() def cli() -> None: ... DEFAULT_MSKLC_DIR = "C:\\Program Files (x86)\\Microsoft Keyboard Layout Creator 1.4\\" @cli.command() @click.argument( "layout_descriptors", nargs=-1, type=click.Path(exists=True, dir_okay=False, path_type=Path), ) @click.option( "--angle-mod/--no-angle-mod", default=False, help="Apply Angle-Mod (which is a [ZXCVB] permutation with the LSGT key (a.k.a. ISO key))", ) @click.option( "--msklc", default=DEFAULT_MSKLC_DIR, type=click.Path(exists=True, file_okay=False, resolve_path=True), help="Directory where MSKLC is installed", ) @click.option( "--qwerty-shortcuts", default=False, is_flag=True, help="Keep shortcuts at their qwerty location", ) @click.option("--verbose", "-v", is_flag=True, help="Verbose mode") def build( layout_descriptors: List[Path], angle_mod: bool, msklc: Path, qwerty_shortcuts: bool, verbose: bool, ) -> None: """Convert TOML/YAML descriptions into Windows MSKLC keyboard drivers.""" if platform.system() != "Windows": sys.exit("This command is only compatible with Windows, sorry.") for input_file in layout_descriptors: layout = KeyboardLayout(load_layout(input_file), angle_mod, qwerty_shortcuts) msklc_mgr = MsklcManager(layout, msklc, install=False, verbose=verbose) if msklc_mgr.build_msklc_installer(): if msklc_mgr.build_msklc_dll(): output_dir = f'{msklc_mgr._working_dir}\\{layout.meta["name8"]}\\' click.echo( "MSKLC drivers successfully built.\n" f"Execute `{output_dir}setup.exe` to install.\n" "Log out and log back in to apply the changes." ) @cli.command() @click.argument( "layout_descriptors", nargs=-1, type=click.Path(exists=True, dir_okay=False, path_type=Path), ) @click.option( "--angle-mod/--no-angle-mod", default=False, help="Apply Angle-Mod (which is a [ZXCVB] permutation with the LSGT key (a.k.a. ISO key))", ) @click.option( "--msklc", default=DEFAULT_MSKLC_DIR, type=click.Path(exists=True, file_okay=False, resolve_path=True), help="Directory where MSKLC is installed", ) @click.option( "--qwerty-shortcuts", default=False, is_flag=True, help="Keep shortcuts at their qwerty location", ) @click.option("--verbose", "-v", is_flag=True, help="Verbose mode") def install( layout_descriptors: List[Path], angle_mod: bool, msklc: Path, qwerty_shortcuts: bool, verbose: bool, ) -> None: """Convert TOML/YAML descriptions into Windows MSKLC keyboard drivers and install them.""" if platform.system() != "Windows": sys.exit("This command is only compatible with Windows, sorry.") for input_file in layout_descriptors: layout = KeyboardLayout(load_layout(input_file), angle_mod, qwerty_shortcuts) msklc_mgr = MsklcManager(layout, msklc, install=True, verbose=verbose) if msklc_mgr.build_msklc_installer(): if msklc_mgr.build_msklc_dll(): if msklc_mgr.install(): click.echo( "MSKLC drivers successfully built and installed.\n" "Log out and log back in to apply the changes." ) else: output_dir = f'{msklc_mgr._working_dir}\\{layout.meta["name8"]}\\' click.echo( "MSKLC drivers successfully built but failed to installed.\n" f"Execute `{output_dir}setup.exe` to install manually.\n" "Log out and log back in to apply the changes." ) @cli.command() @click.argument( "input_file", nargs=1, type=click.Path(exists=True, dir_okay=False, path_type=Path), ) @click.option("--verbose", "-v", is_flag=True, help="Verbose mode") def dummy(input_file: Path, verbose: bool) -> None: """Dump a dummy TOML layout descriptor.""" input_layout = KeyboardLayout(load_layout(input_file)) msklc_mgr = MsklcManager(input_layout, Path(DEFAULT_MSKLC_DIR), verbose=verbose) print(msklc_mgr._create_dummy_layout()) if __name__ == "__main__": cli() kalamine-0.38/kalamine/cli_xkb.py000066400000000000000000000115701465026171300170040ustar00rootroot00000000000000#!/usr/bin/env python3 import os import platform import sys import tempfile from pathlib import Path from typing import Dict, List, Optional, Union import click from .generators import xkb from .layout import KeyboardLayout, load_layout from .xkb_manager import WAYLAND, KbdIndex, XKBManager @click.group() def cli() -> None: if platform.system() != "Linux": sys.exit("This command is only compatible with GNU/Linux, sorry.") @cli.command() @click.argument( "filepath", type=click.Path(exists=True, dir_okay=False, path_type=Path) ) @click.option( "--angle-mod/--no-angle-mod", default=False, help="Apply Angle-Mod (which is a [ZXCVB] permutation with the LSGT key (a.k.a. ISO key))", ) def apply(filepath: Path, angle_mod: bool) -> None: """Apply a Kalamine layout.""" if WAYLAND: sys.exit( "You appear to be running Wayland, which does not support this operation." ) layout = KeyboardLayout(load_layout(filepath), angle_mod) with tempfile.NamedTemporaryFile( mode="w+", suffix=".xkb_keymap", encoding="utf-8" ) as temp_file: temp_file.write(xkb.xkb_keymap(layout)) os.system(f"xkbcomp -w0 {temp_file.name} $DISPLAY") @cli.command() @click.argument( "layouts", nargs=-1, type=click.Path(exists=True, dir_okay=False, path_type=Path) ) @click.option( "--angle-mod/--no-angle-mod", default=False, help="Apply Angle-Mod (which is a [ZXCVB] permutation with the LSGT key (a.k.a. ISO key))", ) def install(layouts: List[Path], angle_mod: bool) -> None: """Install a list of Kalamine layouts.""" if not layouts: return kb_locales = set() kb_layouts = [] for file in layouts: layout_file = load_layout(file) layout = KeyboardLayout(layout_file, angle_mod) kb_layouts.append(layout) kb_locales.add(layout.meta["locale"]) def xkb_install(xkb: XKBManager) -> KbdIndex: for layout in kb_layouts: xkb.add(layout) index = xkb.index # gets erased with xkb.update() xkb.clean() xkb.update() print() print("Successfully installed.") return dict(index) # EAFP (Easier to Ask Forgiveness than Permission) try: xkb_root = XKBManager(root=True) xkb_index = xkb_install(xkb_root) print(f"On XOrg, you can try the layout{'s' if len(layouts) > 1 else ''} with:") for locale, variants in xkb_index.items(): for name in variants.keys(): print(f" setxkbmap {locale} -variant {name}") print() except PermissionError: print(xkb_root.path) print(" Not writable: switching to user-space.") print() if not WAYLAND: print( "You appear to be running XOrg. You need sudo privileges to install keyboard layouts:" ) for filepath in layouts: print(f' sudo env "PATH=$PATH" xkalamine install {filepath}') sys.exit(1) xkb_home = XKBManager() xkb_home.ensure_xkb_config_is_ready() xkb_install(xkb_home) print("Warning: user-space layouts only work with Wayland.") print() @cli.command() @click.argument("mask") # [locale]/[name] def remove(mask: str) -> None: """Remove a list of Kalamine layouts.""" def xkb_remove(root: bool = False) -> None: xkb = XKBManager(root=root) xkb.clean() for locale, variants in xkb.list(mask).items(): for name in variants.keys(): xkb.remove(locale, name) xkb.update() # EAFP (Easier to Ask Forgiveness than Permission) try: xkb_remove(root=True) except PermissionError: if not WAYLAND: print( "You appear to be running XOrg. You need sudo privileges to remove keyboard layouts:" ) print(f' sudo env "PATH=$PATH" xkalamine remove {mask}') sys.exit(1) xkb_remove() @cli.command(name="list") @click.option("-a", "--all", "all_flag", is_flag=True) @click.argument("mask", default="*") def list_command(mask: str, all_flag: bool) -> None: """List installed Kalamine layouts.""" for root in [True, False]: # XXX this very weird type means we've done something silly here filtered: Dict[str, Union[Optional[KeyboardLayout], str]] = {} xkb = XKBManager(root=root) layouts = xkb.list_all(mask) if all_flag else xkb.list(mask) for locale, variants in sorted(layouts.items()): for name, desc in sorted(variants.items()): filtered[f"{locale}/{name}"] = desc if mask == "*" and root and xkb.has_custom_symbols(): filtered["custom"] = "" if filtered: print(xkb.path) for key, value in filtered.items(): print(f" {key:<24} {value}") if __name__ == "__main__": cli() kalamine-0.38/kalamine/data/000077500000000000000000000000001465026171300157245ustar00rootroot00000000000000kalamine-0.38/kalamine/data/dead_keys.yaml000066400000000000000000000130161465026171300205410ustar00rootroot00000000000000# The dead keys below are used by the macOS and Windows drivers. In order to # work with the Linux driver, their name must match the XKB definition: # 'acute' <=> dead_acute, 'grave' <=> dead_grave, etc. # https://help.ubuntu.com/community/GtkDeadKeyTable # The only exception is the '1dk' key, which is: # - a standard dead key for the macOS and Windows drivers; # - a dead level key for the Linux driver: # - ISO_Level3_Latch if the layout has no AltGr layer # - ISO_Level5_Latch if the layout uses an AltGr key (= ISO_Level3_Shift). # All other dead keys have been grep’ed from my Ubuntu 18.04LTS box, # excluding multiple diacritic combinations and combining characters. # /usr/share/X11/locale/en_US.UTF-8/Compose --- - char: '**' name: 1dk base: '' # computed from the imported layout alt: '' # computed from the imported layout alt_space: "'" # apostrophe (can be overriden by the `space.1dk` key) alt_self: "'" # apostrophe (can be overriden from the imported layout) - char: '*`' name: grave base: AaEeIiNnOoUuWwYyЕеИи alt: ÀàÈèÌìǸǹÒòÙùẀẁỲỳЀѐЍѝ alt_space: '`' # U+0060 GRAVE ACCENT alt_self: '`' # U+0060 GRAVE ACCENT - char: '*‟' # there’s no “double grave accent” Unicode character :( name: doublegrave base: AaEeIiOoRrUuѴѴ alt: ȀȁȄȅȈȉȌȍȐȑȔȕѶѷ alt_space: '‟' # U+201F HIGH REVERSED-9 QUOTATION MARK alt_self: '‟' # U+201F HIGH REVERSED-9 QUOTATION MARK - char: '*´' name: acute base: AaCcEeGgIiKkLlMmNnOoPpRrSsUuWwYyZzΑαΕεΗηΙιΟοΥυΩωГгКк alt: ÁáĆćÉéǴǵÍíḰḱĹĺḾḿŃńÓóṔṕŔশÚúẂẃÝýŹźΆάΈέΉήΊίΌόΎύΏώЃѓЌќ alt_space: "'" # U+0027 APOSTROPHE alt_self: '´' # U+00B4 ACUTE ACCENT - char: '*”' # for the symmetry with the doublegrave ID name: doubleacute base: OoUuУу alt: ŐőŰűӲӳ alt_space: "”" # U+201D RIGHT DOUBLE QUOTATION MARK alt_self: "˝" # U+02DD DOUBLE ACUTE ACCENT - char: '*^' name: circumflex base: AaCcEeGgHhIiJjOoSsUuWwYyZz0123456789()+-= alt: ÂâĈĉÊêĜĝĤĥÎîĴĵÔôŜŝÛûŴŵŶŷẐẑ⁰¹²³⁴⁵⁶⁷⁸⁹⁽⁾⁺⁻⁼ alt_space: '^' # U+005E CIRCUMFLEX ACCENT alt_self: '^' # U+005E CIRCUMFLEX ACCENT - char: '*ˇ' name: caron base: AaCcDdEeGgHhIiKkLlNnOoRrSsTtUuZzƷʒ0123456789()+-= alt: ǍǎČčĎďĚěǦǧȞȟǏǐǨǩĽľŇňǑǒŘřŠšŤťǓǔŽžǮǯ₀₁₂₃₄₅₆₇₈₉₍₎₊₋₌ alt_space: 'ˇ' # U+02C7 CARON alt_self: 'ˇ' # U+02C7 CARON - char: '*˘' name: breve base: AaEeGgIiOoUuΑαΙιΥυАаЕеЖжИиУу alt: ĂăĔĕĞğĬĭŎŏŬŭᾸᾰῘῐῨῠӐӑӖӗӁӂЙйЎў alt_space: '˘' # U+02D8 BREVE alt_self: '˘' # U+02D8 BREVE - char: '*⁻' name: invertedbreve base: AaEeIiOoUuRr alt: ȂȃȆȇȊȋȎȏȖȗȒȓ alt_space: '˘' # U+02D8 BREVE alt_self: '˘' # U+02D8 BREVE - char: '*~' name: tilde base: AaEeIiNnOoUuVvYy<>= alt: ÃãẼẽĨĩÑñÕõŨũṼṽỸỹ≲≳≃ alt_space: '~' # U+007E TILDE alt_self: '~' # U+007E TILDE - char: '*¯' name: macron base: AaÆæEeGgIiOoUuYy alt: ĀāǢǣĒēḠḡĪīŌōŪūȲȳ alt_space: '¯' # U+00AF MACRON alt_self: 'ˉ' # U+02C9 MODIFIER LETTER MACRON - char: '*¨' name: diaeresis base: AaEeHhIiOotUuWwXxYyΙιΥυАаЕеӘәЖжЗзИиІіОоӨөУуЧчЫыЭэ alt: ÄäËëḦḧÏïÖöẗÜüẄẅẌẍŸÿΪϊΫϋӒӓЁёӚӛӜӝӞӟӤӥЇїӦӧӪӫӰӱӴӵӸӹӬӭ alt_space: '"' # U+0022 QUOTATION MARK alt_self: '¨' # U+00A8 DIAERESIS - char: '*˚' name: abovering base: AaUuwy alt: ÅåŮůẘẙ alt_space: '˚' # U+02DA RING ABOVE alt_self: '˚' # U+02DA RING ABOVE - char: '*¸' name: cedilla base: CcDdEeGgHhKkLlNnRrSsTt alt: ÇçḐḑȨȩĢģḨḩĶķĻļŅņŖŗŞşŢţ alt_space: '¸' # U+00B8 CEDILLA alt_self: '¸' # U+00B8 CEDILLA - char: '*,' name: belowcomma base: SsTt alt: ȘșȚț alt_space: ',' # U+002C COMMA alt_self: ',' # U+002C COMMA - char: '*˛' name: ogonek base: AaEeIiOoUu alt: ĄąĘęĮįǪǫŲų alt_space: '˛' # U+02DB OGONEK alt_self: '˛' # U+02DB OGONEK - char: '*/' name: stroke base: AaBbCcDdEeGgHhIiJjLlOoPpRrTtUuYyZz<≤≥>= alt: ȺⱥɃƀȻȼĐđɆɇǤǥĦħƗɨɈɉŁłØøⱣᵽɌɍŦŧɄʉɎɏƵƶ≮≰≱≯≠ alt_space: '/' # U+002F SOLIDUS alt_self: '/' # U+002F SOLIDUS - char: '*˙' name: abovedot base: AaBbCcDdEeFfGgHhIijLlMmNnOoPpRrSsTtWwXxYyZz alt: ȦȧḂḃĊċḊḋĖėḞḟĠġḢḣİıȷĿŀṀṁṄṅȮȯṖṗṘṙṠṡṪṫẆẇẊẋẎẏŻż alt_space: '˙' # U+02D9 DOT ABOVE alt_self: '˙' # U+02D9 DOT ABOVE - char: '*.' name: belowdot base: AaBbDdEeHhIiKkLlMmNnOoRrSsTtUuVvWwYyZz alt: ẠạḄḅḌḍẸẹḤḥỊịḲḳḶḷṂṃṆṇỌọṚṛṢṣṬṭỤụṾṿẈẉỴỵẒẓ alt_space: '.' # U+002E FULL STOP alt_self: '.' # U+002E FULL STOP - char: '*µ' name: greek base: AaBbDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuWwXxYyZz alt: ΑαΒβΔδΕεΦφΓγΗηΙιΘθΚκΛλΜμΝνΟοΠπΧχΡρΣσΤτΥυΩωΞξΨψΖζ alt_space: 'µ' # U+00B5 MICRO SIGN alt_self: 'µ' # U+00B5 MICRO SIGN - char: '*¤' name: currency base: AaBbÇCçcDdEeFfGgHhIiKkLlMmNnOoPpRrSsTtþÞUuWwYy alt: ₳؋₱฿₵₡₵¢₯₫₠€₣ƒ₲₲₴₴៛﷼₭₭₤£ℳ₥₦₦૱௹₧₰₨₢$₪₮৳৲৲圓元₩₩円¥ alt_space: '¤' # U+00A4 CURRENCY SIGN alt_self: '¤' # U+00A4 CURRENCY SIGN kalamine-0.38/kalamine/data/geometry.yaml000066400000000000000000000453531465026171300204550ustar00rootroot00000000000000ANSI: template: | ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ⌫ ┃ ┢━━━━━┷━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┺━━┯━━━━━━━┩ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ↹ ┃ │ │ │ │ │ │ │ │ │ │ │ │ │ ┣━━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┲━━━━┷━━━━━━━┪ ┃ ┃ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇬ ┃ │ │ │ │ │ │ │ │ │ │ ┃ ⏎ ┃ ┣━━━━━━━━━┻━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┻━━━━━━━━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇧ ┃ │ │ │ │ │ │ │ │ │ ┃ ⇧ ┃ ┣━━━━━━━┳━━━━┻━━┳━━┷━━━━┱┴─────┴─────┴─────┴─────┴─────┴─┲━━━┷━━━┳━┷━━━━━╋━━━━━━━┳━━━━━━━┫ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ Ctrl ┃ super ┃ Alt ┃ ␣ ┃ Alt ┃ super ┃ menu ┃ Ctrl ┃ ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛ rows: - offset: 2 keys: [ tlde, ae01, ae02, ae03, ae04, ae05, ae06, ae07, ae08, ae09, ae10, ae11, ae12 ] - offset: 11 keys: [ ad01, ad02, ad03, ad04, ad05, ad06, ad07, ad08, ad09, ad10, ad11, ad12, bksl ] - offset: 12 keys: [ ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11 ] - offset: 15 keys: [ ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10 ] ISO: template: | ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ⌫ ┃ ┢━━━━━┷━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┺━━┳━━━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ↹ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┣━━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┺┓ ⏎ ┃ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇬ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┣━━━━━━┳━━┹──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┷━━━━━┻━━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇧ ┃ │ │ │ │ │ │ │ │ │ │ ┃ ⇧ ┃ ┣━━━━━━┻┳━━━━┷━━┳━━┷━━━━┱┴─────┴─────┴─────┴─────┴─────┴─┲━━━┷━━━┳━┷━━━━━╋━━━━━━━┳━━━━━━━┫ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ Ctrl ┃ super ┃ Alt ┃ ␣ ┃ AltGr ┃ super ┃ menu ┃ Ctrl ┃ ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛ rows: - offset: 2 keys: [ tlde, ae01, ae02, ae03, ae04, ae05, ae06, ae07, ae08, ae09, ae10, ae11, ae12 ] - offset: 11 keys: [ ad01, ad02, ad03, ad04, ad05, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] - offset: 12 keys: [ ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11, bksl ] - offset: 9 keys: [ lsgt, ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10 ] ABNT: template: | ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ⌫ ┃ ┢━━━━━┷━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┺━━┳━━━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ↹ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┣━━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┺┓ ⏎ ┃ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇬ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┣━━━━━━┳━━┹──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┻━━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇧ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ⇧ ┃ ┣━━━━━━┻┳━━━━┷━━┳━━┷━━━━┱┴─────┴─────┴─────┴─────┴─────┴─┲━━━┷━━━┳━┷━━━━━╈━━━━━┻━┳━━━━━━━┫ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ Ctrl ┃ super ┃ Alt ┃ ␣ ┃ AltGr ┃ super ┃ menu ┃ Ctrl ┃ ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛ rows: - offset: 2 keys: [ tlde, ae01, ae02, ae03, ae04, ae05, ae06, ae07, ae08, ae09, ae10, ae11, ae12 ] - offset: 11 keys: [ ad01, ad02, ad03, ad04, ad05, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] - offset: 12 keys: [ ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11, bksl ] - offset: 9 keys: [ lsgt, ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10, ab11 ] JIS: template: | ┏━━━━━┱─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━┓ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ W. ┃ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ⌫ ┃ ┣━━━━━┻━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┻━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ↹ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┣━━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┺┓ ⏎ ┃ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇬ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┣━━━━━━━━━┻━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┻━━━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇧ ┃ │ │ │ │ │ │ │ │ │ │ ┃ ⇧ ┃ ┣━━━━━━━┳━━━━┻━━┳━━┷━━━━┳┷━━━━┱┴─────┴─────┴─┲━━━┷━┳━━━┷━┳━━━┷━━━┳━┷━━━━━╈━━━━━┻━┳━━━━━━━━┫ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ Ctrl ┃ super ┃ Alt ┃ NC. ┃ ␣ ┃ C. ┃ K. ┃ Alt ┃ super ┃ menu ┃ Ctrl ┃ ┗━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━┹──────────────┺━━━━━┻━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━━┛ rows: - offset: 8 keys: [ ae01, ae02, ae03, ae04, ae05, ae06, ae07, ae08, ae09, ae10, ae11, ae12, ae13 ] - offset: 11 keys: [ ad01, ad02, ad03, ad04, ad05, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] - offset: 12 keys: [ ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11, bksl ] - offset: 15 keys: [ ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10, ab11 ] ALT: template: | ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━┓ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ⌫ ┃ ┢━━━━━┷━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┻━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ↹ ┃ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┣━━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┲━━━━┛ ⏎ ┃ ┃ ┃ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇬ ┃ │ │ │ │ │ │ │ │ │ │ ┃ ┃ ┣━━━━━━━━━┻━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┻━━━━━━━━━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇧ ┃ │ │ │ │ │ │ │ │ │ ┃ ⇧ ┃ ┣━━━━━━━┳━━━━┻━━┳━━┷━━━━┱┴─────┴─────┴─────┴─────┴─────┴─┲━━━┷━━━┳━┷━━━━━╋━━━━━━━┳━━━━━━━━┫ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ Ctrl ┃ super ┃ Alt ┃ ␣ ┃ Alt ┃ super ┃ menu ┃ Ctrl ┃ ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━━┛ rows: - offset: 2 keys: [ tlde, ae01, ae02, ae03, ae04, ae05, ae06, ae07, ae08, ae09, ae10, ae11, ae12, bksl ] - offset: 11 keys: [ ad01, ad02, ad03, ad04, ad05, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] - offset: 12 keys: [ ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11 ] - offset: 15 keys: [ ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10 ] ERGO: template: | ╭╌╌╌╌╌┰─────┬─────┬─────┬─────┬─────┰─────┬─────┬─────┬─────┬─────┰╌╌╌╌╌┬╌╌╌╌╌╮ ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ ╰╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ ┠─────┼─────┼─────┼─────┼─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┼╌╌╌╌╌┤ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┆ ╭╌╌╌╌╌╂─────┼─────┼─────┼─────┼─────╂─────┼─────┼─────┼─────┼─────╂╌╌╌╌╌┴╌╌╌╌╌╯ ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ┆ ┃ │ │ │ │ ┃ │ │ │ │ ┃ ╰╌╌╌╌╌┸─────┴─────┴─────┴─────┴─────┸─────┴─────┴─────┴─────┴─────┚ rows: - offset: 2 keys: [ tlde, ae01, ae02, ae03, ae04, ae05, ae06, ae07, ae08, ae09, ae10, ae11, ae12 ] - offset: 8 keys: [ ad01, ad02, ad03, ad04, ad05, ad06, ad07, ad08, ad09, ad10, ad11, ad12 ] - offset: 8 keys: [ ac01, ac02, ac03, ac04, ac05, ac06, ac07, ac08, ac09, ac10, ac11, bksl ] - offset: 2 keys: [ lsgt, ab01, ab02, ab03, ab04, ab05, ab06, ab07, ab08, ab09, ab10 ] kalamine-0.38/kalamine/data/key_sym.yaml000066400000000000000000000211371465026171300202740ustar00rootroot00000000000000# extracted from /usr/include/X11/keysymdef.h # ascii "\u0020": space "\u0021": exclam "\u0022": quotedbl "\u0023": numbersign "\u0024": dollar "\u0025": percent "\u0026": ampersand "\u0027": apostrophe "\u0028": parenleft "\u0029": parenright "\u002a": asterisk "\u002b": plus "\u002c": comma "\u002d": minus "\u002e": period "\u002f": slash "\u0030": '0' "\u0031": '1' "\u0032": '2' "\u0033": '3' "\u0034": '4' "\u0035": '5' "\u0036": '6' "\u0037": '7' "\u0038": '8' "\u0039": '9' "\u003a": colon "\u003b": semicolon "\u003c": less "\u003d": equal "\u003e": greater "\u003f": question "\u0040": at "\u0041": A "\u0042": B "\u0043": C "\u0044": D "\u0045": E "\u0046": F "\u0047": G "\u0048": H "\u0049": I "\u004a": J "\u004b": K "\u004c": L "\u004d": M "\u004e": N "\u004f": O "\u0050": P "\u0051": Q "\u0052": R "\u0053": S "\u0054": T "\u0055": U "\u0056": V "\u0057": W "\u0058": X "\u0059": Y "\u005a": Z "\u005b": bracketleft "\u005c": backslash "\u005d": bracketright "\u005e": asciicircum "\u005f": underscore "\u0060": grave "\u0061": a "\u0062": b "\u0063": c "\u0064": d "\u0065": e "\u0066": f "\u0067": g "\u0068": h "\u0069": i "\u006a": j "\u006b": k "\u006c": l "\u006d": m "\u006e": n "\u006f": o "\u0070": p "\u0071": q "\u0072": r "\u0073": s "\u0074": t "\u0075": u "\u0076": v "\u0077": w "\u0078": x "\u0079": y "\u007a": z "\u007b": braceleft "\u007c": bar "\u007d": braceright "\u007e": asciitilde # latin-1 "\u00a0": nobreakspace "\u00a1": exclamdown "\u00a2": cent "\u00a3": sterling "\u00a4": currency "\u00a5": yen "\u00a6": brokenbar "\u00a7": section "\u00a8": diaeresis "\u00a9": copyright "\u00aa": ordfeminine "\u00ab": guillemotleft "\u00ac": notsign "\u00ad": hyphen "\u00ae": registered "\u00af": macron "\u00b0": degree "\u00b1": plusminus "\u00b2": twosuperior "\u00b3": threesuperior "\u00b4": acute "\u00b5": mu "\u00b6": paragraph "\u00b7": periodcentered "\u00b8": cedilla "\u00b9": onesuperior "\u00ba": masculine "\u00bb": guillemotright "\u00bc": onequarter "\u00bd": onehalf "\u00be": threequarters "\u00bf": questiondown "\u00c0": Agrave "\u00c1": Aacute "\u00c2": Acircumflex "\u00c3": Atilde "\u00c4": Adiaeresis "\u00c5": Aring "\u00c6": AE "\u00c7": Ccedilla "\u00c8": Egrave "\u00c9": Eacute "\u00ca": Ecircumflex "\u00cb": Ediaeresis "\u00cc": Igrave "\u00cd": Iacute "\u00ce": Icircumflex "\u00cf": Idiaeresis "\u00d0": ETH "\u00d0": Eth "\u00d1": Ntilde "\u00d2": Ograve "\u00d3": Oacute "\u00d4": Ocircumflex "\u00d5": Otilde "\u00d6": Odiaeresis "\u00d7": multiply "\u00d8": Oslash "\u00d8": Ooblique "\u00d9": Ugrave "\u00da": Uacute "\u00db": Ucircumflex "\u00dc": Udiaeresis "\u00dd": Yacute "\u00de": Thorn "\u00df": ssharp "\u00e0": agrave "\u00e1": aacute "\u00e2": acircumflex "\u00e3": atilde "\u00e4": adiaeresis "\u00e5": aring "\u00e6": ae "\u00e7": ccedilla "\u00e8": egrave "\u00e9": eacute "\u00ea": ecircumflex "\u00eb": ediaeresis "\u00ec": igrave "\u00ed": iacute "\u00ee": icircumflex "\u00ef": idiaeresis "\u00f0": eth "\u00f1": ntilde "\u00f2": ograve "\u00f3": oacute "\u00f4": ocircumflex "\u00f5": otilde "\u00f6": odiaeresis "\u00f7": division "\u00f8": oslash "\u00f8": ooblique "\u00f9": ugrave "\u00fa": uacute "\u00fb": ucircumflex "\u00fc": udiaeresis "\u00fd": yacute "\u00fe": thorn "\u00ff": ydiaeresis # latin-2 "\u0102": Abreve "\u0103": abreve "\u0104": Aogonek "\u0105": aogonek "\u0106": Cacute "\u0107": cacute "\u010c": Ccaron "\u010d": ccaron "\u010e": Dcaron "\u010f": dcaron "\u0110": Dstroke "\u0111": dstroke "\u0118": Eogonek "\u0119": eogonek "\u011a": Ecaron "\u011b": ecaron "\u0139": Lacute "\u013a": lacute "\u013d": Lcaron "\u013e": lcaron "\u0141": Lstroke "\u0142": lstroke "\u0143": Nacute "\u0144": nacute "\u0147": Ncaron "\u0148": ncaron "\u0150": Odoubleacute "\u0151": odoubleacute "\u0154": Racute "\u0155": racute "\u0158": Rcaron "\u0159": rcaron "\u015a": Sacute "\u015b": sacute "\u015e": Scedilla "\u015f": scedilla "\u0160": Scaron "\u0161": scaron "\u0162": Tcedilla "\u0163": tcedilla "\u0164": Tcaron "\u0165": tcaron "\u016e": Uring "\u016f": uring "\u0170": Udoubleacute "\u0171": udoubleacute "\u0179": Zacute "\u017a": zacute "\u017b": Zabovedot "\u017c": zabovedot "\u017d": Zcaron "\u017e": zcaron "\u02c7": caron "\u02d8": breve "\u02d9": abovedot "\u02db": ogonek "\u02dd": doubleacute # latin-3 "\u0108": Ccircumflex "\u0109": ccircumflex "\u010a": Cabovedot "\u010b": cabovedot "\u011c": Gcircumflex "\u011d": gcircumflex "\u011e": Gbreve "\u011f": gbreve "\u0120": Gabovedot "\u0121": gabovedot "\u0124": Hcircumflex "\u0125": hcircumflex "\u0126": Hstroke "\u0127": hstroke "\u0130": Iabovedot "\u0131": idotless "\u0134": Jcircumflex "\u0135": jcircumflex "\u015c": Scircumflex "\u015d": scircumflex "\u016c": Ubreve "\u016d": ubreve # latin-4 "\u0100": Amacron "\u0101": amacron "\u0112": Emacron "\u0113": emacron "\u0116": Eabovedot "\u0117": eabovedot "\u0122": Gcedilla "\u0123": gcedilla "\u0128": Itilde "\u0129": itilde "\u012a": Imacron "\u012b": imacron "\u012e": Iogonek "\u012f": iogonek "\u0136": Kcedilla "\u0137": kcedilla "\u0138": kra "\u013b": Lcedilla "\u013c": lcedilla "\u0145": Ncedilla "\u0146": ncedilla "\u014a": ENG "\u014b": eng "\u014c": Omacron "\u014d": omacron "\u0156": Rcedilla "\u0157": rcedilla "\u0166": Tslash "\u0167": tslash "\u0168": Utilde "\u0169": utilde "\u016a": Umacron "\u016b": umacron "\u0172": Uogonek "\u0173": uogonek # latin-8 "\u1000174": Wcircumflex "\u1000175": wcircumflex "\u1000176": Ycircumflex "\u1000177": ycircumflex "\u1001e02": Babovedot "\u1001e03": babovedot "\u1001e0a": Dabovedot "\u1001e0b": dabovedot "\u1001e1e": Fabovedot "\u1001e1f": fabovedot "\u1001e40": Mabovedot "\u1001e41": mabovedot "\u1001e56": Pabovedot "\u1001e57": pabovedot "\u1001e60": Sabovedot "\u1001e61": sabovedot "\u1001e6a": Tabovedot "\u1001e6b": tabovedot "\u1001e80": Wgrave "\u1001e81": wgrave "\u1001e82": Wacute "\u1001e83": wacute "\u1001e84": Wdiaeresis "\u1001e85": wdiaeresis "\u1001ef2": Ygrave "\u1001ef3": ygrave # latin-9 "\u0152": OE "\u0153": oe "\u0178": Ydiaeresis "\u20ac": EuroSign # technical "\u0192": function "\u2190": leftarrow "\u2191": uparrow "\u2192": rightarrow "\u2193": downarrow # "\u21d2": implies "\u21d4": ifonlyif "\u2202": partialderivative "\u2207": nabla "\u221a": radical "\u221d": variation "\u221e": infinity "\u2227": logicaland "\u2228": logicalor "\u2229": intersection "\u222a": union "\u222b": integral "\u2234": therefore "\u223c": approximate "\u2243": similarequal "\u2260": notequal "\u2261": identical "\u2264": lessthanequal "\u2265": greaterthanequal "\u2282": includedin "\u2283": includes "\u2320": topintegral "\u2321": botintegral "\u239b": topleftparens "\u239d": botleftparens "\u239e": toprightparens "\u23a0": botrightparens "\u23a1": topleftsqbracket "\u23a3": botleftsqbracket "\u23a4": toprightsqbracket "\u23a6": botrightsqbracket "\u23a8": leftmiddlecurlybrace "\u23ac": rightmiddlecurlybrace "\u23b7": leftradical "\u2500": horizconnector "\u2502": vertconnector "\u250c": topleftradical # publishing "\u2002": enspace "\u2003": emspace "\u2004": em3space "\u2005": em4space "\u2007": digitspace "\u2008": punctspace "\u2009": thinspace "\u200a": hairspace "\u2012": figdash "\u2013": endash "\u2014": emdash "\u2018": leftsinglequotemark "\u2019": rightsinglequotemark "\u201a": singlelowquotemark "\u201c": leftdoublequotemark "\u201d": rightdoublequotemark "\u201e": doublelowquotemark "\u2020": dagger "\u2021": doubledagger "\u2022": enfilledcircbullet "\u2025": doubbaselinedot "\u2026": ellipsis "\u2032": minutes "\u2033": seconds "\u2038": caret "\u2105": careof "\u2117": phonographcopyright "\u211e": prescription "\u2122": trademark "\u2153": onethird "\u2154": twothirds "\u2155": onefifth "\u2156": twofifths "\u2157": threefifths "\u2158": fourfifths "\u2159": onesixth "\u215a": fivesixths "\u215b": oneeighth "\u215c": threeeighths "\u215d": fiveeighths "\u215e": seveneighths "\u2315": telephonerecorder "\u2423": signifblank "\u25aa": enfilledsqbullet "\u25ab": enopensquarebullet "\u25ac": filledrectbullet "\u25ad": openrectbullet "\u25ae": emfilledrect "\u25af": emopenrectangle "\u25b2": filledtribulletup "\u25b3": opentribulletup "\u25b6": filledrighttribullet "\u25b7": rightopentriangle "\u25bc": filledtribulletdown "\u25bd": opentribulletdown "\u25c0": filledlefttribullet "\u25c1": leftopentriangle "\u25cb": emopencircle "\u25cf": emfilledcircle "\u25e6": enopencircbullet "\u2606": openstar "\u260e": telephone "\u2613": signaturemark "\u261c": leftpointer "\u261e": rightpointer "\u2640": femalesymbol "\u2642": malesymbol "\u2663": club "\u2665": heart "\u2666": diamond "\u266d": musicalflat "\u266f": musicalsharp "\u2713": checkmark "\u2717": ballotcross "\u271d": latincross "\u2720": maltesecross kalamine-0.38/kalamine/data/layout.yaml000066400000000000000000000210571465026171300201320ustar00rootroot00000000000000name: custom name8: custom locale: us variant: custom author: nobody description: QWERTY, custom variant url: https://OneDeadKey.github.com/kalamine/ version: 0.0.1 alpha: | ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓ │ ~ │ ! │ @ │ # │ $ │ % │ ^ │ & │ * │ ( │ ) │ _ │ + ┃ ┃ │ ` │ 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 ┃ ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛ 1dk: | ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓ │ ~ │ ! │ @ │ # │ $ │ % │ ^ │ & │ * │ ( │ ) │ _ │ + ┃ ┃ │ ` │ 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 ┃ ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛ altgr: | ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━━┓ │ *~ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ┃ │ *` │ │ │ │ │ │ *^ │ │ │ │ │ │ ┃ ⌫ ┃ ┢━━━━━┷━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┺━━┯━━━━━━━┩ ┃ ┃ │ │ │ │ │ │ │ │ │ │ │ │ │ ┃ ↹ ┃ @ │ < │ > │ $ │ % │ ^ │ & │ * │ ' │ ` │ │ │ │ ┣━━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┲━━━━┷━━━━━━━┪ ┃ ┃ │ │ │ │ │ │ │ │ │ │ *¨ ┃ ┃ ┃ ⇬ ┃ { │ ( │ ) │ } │ = │ \ │ + │ - │ / │ " │ *´ ┃ ⏎ ┃ ┣━━━━━━━━━┻━━┱──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┲━━┻━━━━━━━━━━━━┫ ┃ ┃ │ │ │ │ │ │ │ │ │ ┃ ┃ ┃ ⇧ ┃ ~ │ [ │ ] │ _ │ # │ | │ ! │ ; │ : │ ? ┃ ⇧ ┃ ┣━━━━━━━┳━━━━┻━━┳━━┷━━━━┱┴─────┴─────┴─────┴─────┴─────┴─┲━━━┷━━━┳━┷━━━━━╋━━━━━━━┳━━━━━━━┫ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ Ctrl ┃ super ┃ Alt ┃ ␣ ┃ AltGr ┃ super ┃ menu ┃ Ctrl ┃ ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┻━━━━━━━┛ kalamine-0.38/kalamine/data/qwerty_vk.yaml000066400000000000000000000013531465026171300206450ustar00rootroot00000000000000# Scancodes <-> Virtual Keys as in qwerty # this is to keep shortcuts at their qwerty location '39': 'SPACE' # digits '02': '1' '03': '2' '04': '3' '05': '4' '06': '5' '07': '6' '08': '7' '09': '8' '0a': '9' '0b': '0' # letters, first row '10': 'Q' '11': 'W' '12': 'E' '13': 'R' '14': 'T' '15': 'Y' '16': 'U' '17': 'I' '18': 'O' '19': 'P' # letters, second row '1e': 'A' '1f': 'S' '20': 'D' '21': 'F' '22': 'G' '23': 'H' '24': 'J' '25': 'K' '26': 'L' '27': 'OEM_1' # letters, third row '2c': 'Z' '2d': 'X' '2e': 'C' '2f': 'V' '30': 'B' '31': 'N' '32': 'M' '33': 'OEM_COMMA' '34': 'OEM_PERIOD' '35': 'OEM_2' # pinky keys '29': 'OEM_3' '0c': 'OEM_MINUS' '0d': 'OEM_PLUS' '1a': 'OEM_4' '1b': 'OEM_6' '28': 'OEM_7' '2b': 'OEM_5' '56': 'OEM_102'kalamine-0.38/kalamine/data/scan_codes.yaml000066400000000000000000000051541465026171300207160ustar00rootroot00000000000000klc: spce: '39' # digits ae01: '02' ae02: '03' ae03: '04' ae04: '05' ae05: '06' ae06: '07' ae07: '08' ae08: '09' ae09: '0a' ae10: '0b' # letters, first row ad01: '10' ad02: '11' ad03: '12' ad04: '13' ad05: '14' ad06: '15' ad07: '16' ad08: '17' ad09: '18' ad10: '19' # letters, second row ac01: '1e' ac02: '1f' ac03: '20' ac04: '21' ac05: '22' ac06: '23' ac07: '24' ac08: '25' ac09: '26' ac10: '27' # letters, third row ab01: '2c' ab02: '2d' ab03: '2e' ab04: '2f' ab05: '30' ab06: '31' ab07: '32' ab08: '33' ab09: '34' ab10: '35' # pinky keys tlde: '29' ae11: '0c' ae12: '0d' ae13: '0d' # XXX FIXME ad11: '1a' ad12: '1b' ac11: '28' ab11: '28' # XXX FIXME bksl: '2b' lsgt: '56' osx: spce: 49 # digits ae01: 18 # 1 ae02: 19 # 2 ae03: 20 # 3 ae04: 21 # 4 ae05: 23 # 5 ae06: 22 # 6 ae07: 26 # 7 ae08: 28 # 8 ae09: 25 # 9 ae10: 29 # 0 # letters, first row ad01: 12 # Q ad02: 13 # W ad03: 14 # E ad04: 15 # R ad05: 17 # T ad06: 16 # Y ad07: 32 # U ad08: 34 # I ad09: 31 # O ad10: 35 # P # letters, second row ac01: 0 # A ac02: 1 # S ac03: 2 # D ac04: 3 # F ac05: 5 # G ac06: 4 # H ac07: 38 # J ac08: 40 # K ac09: 37 # L ac10: 41 # ★ # letters, third row ab01: 6 # Z ab02: 7 # X ab03: 8 # C ab04: 9 # V ab05: 11 # B ab06: 45 # N ab07: 46 # M ab08: 43 # , ab09: 47 # . ab10: 44 # / # pinky keys tlde: 50 # ~ ae11: 27 # - ae12: 24 # = ae13: 42 # XXX FIXME ad11: 33 # [ ad12: 30 # ] ac11: 39 # ' ab11: 39 # XXX FIXME bksl: 42 # \ lsgt: 10 # < web: spce: 'Space' # digits ae01: 'Digit1' ae02: 'Digit2' ae03: 'Digit3' ae04: 'Digit4' ae05: 'Digit5' ae06: 'Digit6' ae07: 'Digit7' ae08: 'Digit8' ae09: 'Digit9' ae10: 'Digit0' # letters, 1st row ad01: 'KeyQ' ad02: 'KeyW' ad03: 'KeyE' ad04: 'KeyR' ad05: 'KeyT' ad06: 'KeyY' ad07: 'KeyU' ad08: 'KeyI' ad09: 'KeyO' ad10: 'KeyP' # letters, 2nd row ac01: 'KeyA' ac02: 'KeyS' ac03: 'KeyD' ac04: 'KeyF' ac05: 'KeyG' ac06: 'KeyH' ac07: 'KeyJ' ac08: 'KeyK' ac09: 'KeyL' ac10: 'Semicolon' # letters, 3rd row ab01: 'KeyZ' ab02: 'KeyX' ab03: 'KeyC' ab04: 'KeyV' ab05: 'KeyB' ab06: 'KeyN' ab07: 'KeyM' ab08: 'Comma' ab09: 'Period' ab10: 'Slash' # pinky keys tlde: 'Backquote' ae11: 'Minus' ae12: 'Equal' ae13: 'IntlYen' ad11: 'BracketLeft' ad12: 'BracketRight' bksl: 'Backslash' ac11: 'Quote' ab11: 'IntlRo' lsgt: 'IntlBackslash' kalamine-0.38/kalamine/data/user_guide.yaml000066400000000000000000000072041465026171300207460ustar00rootroot00000000000000Layers: base: | The `base` layer contains the base and shifted keys: +-----+ shift -------> | ? | base --------> | / | +-----+ When the base and shift keys correspond to the same character, you may only specify the uppercase char: +-----+ shift -------> | A | (base = a) --> | | +-----+ altgr: | The `altgr` layer contains the altgr and shift+altgr symbols: +-----+ | | <----- (altgr+shift+key is undefined) | { | <----- altgr+key = { +-----+ full: | The `full` view lets you specify the `base` and `altgr` levels together: +-----+ shift -------> | A | <----- (altgr+shift+key is undefined) (base = a) --> | { | <----- altgr+key = { +-----+ Dead_Keys: Usage: | Dead keys are preceded by a `*` sign. They can be used in the `base` layer: +-----+ shift -------> |*" | = dead diaeresis base --------> |*´ | = dead acute accent +-----+ … as well as in the `altgr` layer: +-----+ | *" | <----- altgr+shift+key = dead diaeresis | *´ | <----- altgr+key = dead acute accent +-----+ … and combined in the `full` layer: +-----+ shift+key = A --> | A*" | <----- altgr+shift+key = dead diaeresis key = a --> | a*´ | <----- altgr+key = dead acute accent +-----+ Standard_Dead_Keys: | The following dead keys are supported, and their behavior cannot be customized: Custom_Dead_Key: | There is one dead key (1dk), noted `**`, that can be customized by specifying how it modifies each character in the `base` layer: +-----+ shift -------> | ? ¿ | <----- 1dk, shift+key base --------> | / ÷ | <----- 1dk, key +-----+ When the base and shift keys correspond to the same accented character, you may only specify the lowercase accented char in the `base` layer: +-----+ shift -------> | A | <----- (1dk, shift+key = À) (base = a) --> | à | <----- 1dk, key = à +-----+ You may also chain dead keys by specifying a dead key in the `1dk` layer: +-----+ shift -------> | G | (base = g) --> | *µ | <----- 1dk, key = dead Greek +-----+ **Warning:** chained dead keys are not supported by MSKLC, and KbdEdit will be required to build a Windows driver for such a keyboard layout. Space_Bar: | Kalamine descriptor files have an optional section to define specific behaviors of the space bar in non-base layers: [spacebar] shift = "\u202f" # NARROW NO-BREAK SPACE altgr = "\u0020" # SPACE altgr_shift = "\u00a0" # NO-BREAK SPACE 1dk = "\u2019" # RIGHT SINGLE QUOTATION MARK 1dk_shift = "\u2019" # RIGHT SINGLE QUOTATION MARK Kalamine doesn’t support non-space chars on the `base` layer for the space bar. Space characters outside of the space bar are not supported either. kalamine-0.38/kalamine/data/win_locales.yaml000066400000000000000000000317241465026171300211160ustar00rootroot00000000000000# from https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c "aa": "1000" "aa-DJ": "1000" "aa-ER": "1000" "aa-ET": "1000" "af": "0036" "af-NA": "1000" "af-ZA": "0436" "agq": "1000" "agq-CM": "1000" "ak": "1000" "ak-GH": "1000" "am": "005E" "am-ET": "045E" "ar": "0001" "ar-001": "1000" "ar-AE": "3801" "ar-BH": "3C01" "ar-DJ": "1000" "ar-DZ": "1401" "ar-EG": "0c01" "ar-ER": "1000" "ar-IL": "1000" "ar-IQ": "0801" "ar-JO": "2C01" "ar-KM": "1000" "ar-KW": "3401" "ar-LB": "3001" "ar-LY": "1001" "ar-MA": "1801" "ar-MR": "1000" "ar-OM": "2001" "ar-PS": "1000" "ar-QA": "4001" "ar-SA": "0401" "ar-SD": "1000" "ar-SO": "1000" "ar-SS": "1000" "ar-SY": "2801" "ar-TD": "1000" "ar-TN": "1C01" "ar-YE": "2401" "arn": "007A" "arn-CL": "047A" "as": "004D" "as-IN": "044D" "asa": "1000" "asa-TZ": "1000" "ast": "1000" "ast-ES": "1000" "az": "002C" "az-Cyrl": "742C" "az-Cyrl-AZ": "082C" "az-Latn": "782C" "az-Latn-AZ": "042C" "ba": "006D" "ba-RU": "046D" "bas": "1000" "bas-CM": "1000" "be": "0023" "be-BY": "0423" "bem": "1000" "bem-ZM": "1000" "bez": "1000" "bez-TZ": "1000" "bg": "0002" "bg-BG": "0402" "bm": "1000" "bm-Latn-ML": "1000" "bn": "0045" "bn-BD": "0845" "bn-IN": "0445" "bo": "0051" "bo-CN": "0451" "bo-IN": "1000" "br": "007E" "br-FR": "047E" "brx": "1000" "brx-IN": "1000" "bs": "781A" "bs-Cyrl": "641A" "bs-Cyrl-BA": "201A" "bs-Latn": "681A" "bs-Latn-BA": "141A" "byn": "1000" "byn-ER": "1000" "ca": "0003" "ca-AD": "1000" "ca-ES": "0403" "ca-ES-valencia": "0803" "ca-FR": "1000" "ca-IT": "1000" "ccp": "1000" "ccp-Cakm": "1000" "ccp-Cakm-BD": "1000" "ccp-Cakm-IN": "1000" "cd-RU": "1000" "ceb": "1000" "ceb-Latn": "1000" "ceb-Latn-PH": "1000" "cgg": "1000" "cgg-UG": "1000" "chr": "005C" "chr-Cher": "7c5C" "chr-Cher-US": "045C" "co": "0083" "co-FR": "0483" "cs": "0005" "cs-CZ": "0405" "cu-RU": "1000" "cy": "0052" "cy-GB": "0452" "da": "0006" "da-DK": "0406" "da-GL": "1000" "dav": "1000" "dav-KE": "1000" "de": "0007" "de-AT": "0C07" "de-BE": "1000" "de-CH": "0807" "de-DE": "0407" "de-IT": "1000" "de-LI": "1407" "de-LU": "1007" "dje": "1000" "dje-NE": "1000" "dsb": "7C2E" "dsb-DE": "082E" "dua": "1000" "dua-CM": "1000" "dv": "0065" "dv-MV": "0465" "dyo": "1000" "dyo-SN": "1000" "dz": "1000" "dz-BT": "0C51" "ebu": "1000" "ebu-KE": "1000" "ee": "1000" "ee-GH": "1000" "ee-TG": "1000" "el": "0008" "el-CY": "1000" "el-GR": "0408" "en": "0009" "en-001": "1000" "en-029": "2409" "en-150": "1000" "en-AE": "4C09" "en-AG": "1000" "en-AI": "1000" "en-AS": "1000" "en-AT": "1000" "en-AU": "0C09" "en-BB": "1000" "en-BE": "1000" "en-BI": "1000" "en-BM": "1000" "en-BS": "1000" "en-BW": "1000" "en-BZ": "2809" "en-CA": "1009" "en-CC": "1000" "en-CH": "1000" "en-CK": "1000" "en-CM": "1000" "en-CX": "1000" "en-CY": "1000" "en-DE": "1000" "en-DK": "1000" "en-DM": "1000" "en-ER": "1000" "en-FI": "1000" "en-FJ": "1000" "en-FK": "1000" "en-FM": "1000" "en-GB": "0809" "en-GD": "1000" "en-GG": "1000" "en-GH": "1000" "en-GI": "1000" "en-GM": "1000" "en-GU": "1000" "en-GY": "1000" "en-HK": "3C09" "en-IE": "1809" "en-IL": "1000" "en-IM": "1000" "en-IN": "4009" "en-IO": "1000" "en-JE": "1000" "en-JM": "2009" "en-KE": "1000" "en-KI": "1000" "en-KN": "1000" "en-KY": "1000" "en-LC": "1000" "en-LR": "1000" "en-LS": "1000" "en-MG": "1000" "en-MH": "1000" "en-MO": "1000" "en-MP": "1000" "en-MS": "1000" "en-MT": "1000" "en-MU": "1000" "en-MW": "1000" "en-MY": "4409" "en-NA": "1000" "en-NF": "1000" "en-NG": "1000" "en-NL": "1000" "en-NR": "1000" "en-NU": "1000" "en-NZ": "1409" "en-PG": "1000" "en-PH": "3409" "en-PK": "1000" "en-PN": "1000" "en-PR": "1000" "en-PW": "1000" "en-RW": "1000" "en-SB": "1000" "en-SC": "1000" "en-SD": "1000" "en-SE": "1000" "en-SG": "4809" "en-SH": "1000" "en-SI": "1000" "en-SL": "1000" "en-SS": "1000" "en-SX": "1000" "en-SZ": "1000" "en-TC": "1000" "en-TK": "1000" "en-TO": "1000" "en-TT": "2c09" "en-TV": "1000" "en-TZ": "1000" "en-UG": "1000" "en-UM": "1000" "en-US": "0409" "en-VC": "1000" "en-VG": "1000" "en-VI": "1000" "en-VU": "1000" "en-WS": "1000" "en-ZA": "1C09" "en-ZM": "1000" "en-ZW": "3009" "eo": "1000" "eo-001": "1000" "es": "000A" "es-419": "580A" "es-AR": "2C0A" "es-BO": "400A" "es-BR": "1000" "es-BZ": "1000" "es-CL": "340A" "es-CO": "240A" "es-CR": "140A" "es-CU": "5c0A" "es-DO": "1c0A" "es-EC": "300A" "es-ES": "0c0A" "es-ES_tradnl": "040A" "es-GQ": "1000" "es-GT": "100A" "es-HN": "480A" "es-MX": "080A" "es-NI": "4C0A" "es-PA": "180A" "es-PE": "280A" "es-PH": "1000" "es-PR": "500A" "es-PY": "3C0A" "es-SV": "440A" "es-US": "540A" "es-UY": "380A" "es-VE": "200A" "et": "0025" "et-EE": "0425" "eu": "002D" "eu-ES": "042D" "ewo": "1000" "ewo-CM": "1000" "fa": "0029" "fa-AF": "1000" "fa-IR": "0429" "ff": "0067" "ff-CM": "1000" "ff-GN": "1000" "ff-Latn": "7C67" "ff-Latn-BF": "1000" "ff-Latn-CM": "1000" "ff-Latn-GH": "1000" "ff-Latn-GM": "1000" "ff-Latn-GN": "1000" "ff-Latn-GW": "1000" "ff-Latn-LR": "1000" "ff-Latn-MR": "1000" "ff-Latn-NE": "1000" "ff-Latn-NG": "1000" "ff-Latn-SL": "1000" "ff-Latn-SN": "0867" "ff-MR": "1000" "ff-NG": "1000" "fi": "000B" "fi-FI": "040B" "fil": "0064" "fil-PH": "0464" "fo": "0038" "fo-DK": "1000" "fo-FO": "0438" "fr": "000C" "fr-BE": "080C" "fr-BF": "1000" "fr-BI": "1000" "fr-BJ": "1000" "fr-BL": "1000" "fr-CA": "0c0C" "fr-CD": "240C" "fr-CF": "1000" "fr-CG": "1000" "fr-CH": "100C" "fr-CI": "300C" "fr-CM": "2c0C" "fr-DJ": "1000" "fr-DZ": "1000" "fr-FR": "040C" "fr-GA": "1000" "fr-GF": "1000" "fr-GN": "1000" "fr-GP": "1000" "fr-GQ": "1000" "fr-HT": "3c0C" "fr-KM": "1000" "fr-LU": "140C" "fr-MA": "380C" "fr-MC": "180C" "fr-MF": "1000" "fr-MG": "1000" "fr-ML": "340C" "fr-MQ": "1000" "fr-MR": "1000" "fr-MU": "1000" "fr-NC": "1000" "fr-NE": "1000" "fr-PF": "1000" "fr-PM": "1000" "fr-RE": "200C" "fr-RW": "1000" "fr-SC": "1000" "fr-SN": "280C" "fr-SY": "1000" "fr-TD": "1000" "fr-TG": "1000" "fr-TN": "1000" "fr-VU": "1000" "fr-WF": "1000" "fr-YT": "1000" "fur": "1000" "fur-IT": "1000" "fy": "0062" "fy-NL": "0462" "ga": "003C" "ga-IE": "083C" "gd": "0091" "gd-GB": "0491" "gl": "0056" "gl-ES": "0456" "gn": "0074" "gn-PY": "0474" "gsw": "0084" "gsw-CH": "1000" "gsw-FR": "0484" "gsw-LI": "1000" "gu": "0047" "gu-IN": "0447" "guz": "1000" "guz-KE": "1000" "gv": "1000" "gv-IM": "1000" "ha": "0068" "ha-Latn": "7C68" "ha-Latn-GH": "1000" "ha-Latn-NE": "1000" "ha-Latn-NG": "0468" "haw": "0075" "haw-US": "0475" "he": "000D" "he-IL": "040D" "hi": "0039" "hi-IN": "0439" "hr-BA": "101A" "hr-HR": "041A" "hr": "001A" "hsb": "002E" "hsb-DE": "042E" "hu": "000E" "hu-HU": "040E" "hy": "002B" "hy-AM": "042B" "ia": "1000" "ia-001": "1000" "ia-FR": "1000" "id": "0021" "id-ID": "0421" "ig": "0070" "ig-NG": "0470" "ii": "0078" "ii-CN": "0478" "is": "000F" "is-IS": "040F" "it": "0010" "it-CH": "0810" "it-IT": "0410" "it-SM": "1000" "it-VA": "1000" "iu": "005D" "iu-Cans": "785D" "iu-Cans-CA": "045d" "iu-Latn": "7C5D" "iu-Latn-CA": "085D" "ja": "0011" "ja-JP": "0411" "jgo": "1000" "jgo-CM": "1000" "jmc": "1000" "jmc-TZ": "1000" "jv": "1000" "jv-Latn": "1000" "jv-Latn-ID": "1000" "ka": "0037" "ka-GE": "0437" "kab": "1000" "kab-DZ": "1000" "kam": "1000" "kam-KE": "1000" "kde": "1000" "kde-TZ": "1000" "kea": "1000" "kea-CV": "1000" "khq": "1000" "khq-ML": "1000" "ki": "1000" "ki-KE": "1000" "kk": "003F" "kk-KZ": "043F" "kkj": "1000" "kkj-CM": "1000" "kl": "006F" "kl-GL": "046F" "kln": "1000" "kln-KE": "1000" "km": "0053" "km-KH": "0453" "kn": "004B" "kn-IN": "044B" "ko": "0012" "ko-KP": "1000" "ko-KR": "0412" "kok": "0057" "kok-IN": "0457" "ks": "0060" "ks-Arab": "0460" "ks-Arab-IN": "1000" "ksb": "1000" "ksb-TZ": "1000" "ksf": "1000" "ksf-CM": "1000" "ksh": "1000" "ksh-DE": "1000" "ku": "0092" "ku-Arab": "7c92" "ku-Arab-IQ": "0492" "ku-Arab-IR": "1000" "kw": "1000" "kw-GB": "1000" "ky": "0040" "ky-KG": "0440" "lag": "1000" "lag-TZ": "1000" "lb": "006E" "lb-LU": "046E" "lg": "1000" "lg-UG": "1000" "lkt": "1000" "lkt-US": "1000" "ln": "1000" "ln-AO": "1000" "ln-CD": "1000" "ln-CF": "1000" "ln-CG": "1000" "lo": "0054" "lo-LA": "0454" "lrc-IQ": "1000" "lrc-IR": "1000" "lt": "0027" "lt-LT": "0427" "lu": "1000" "lu-CD": "1000" "luo": "1000" "luo-KE": "1000" "luy": "1000" "luy-KE": "1000" "lv": "0026" "lv-LV": "0426" "mas": "1000" "mas-KE": "1000" "mas-TZ": "1000" "mer": "1000" "mer-KE": "1000" "mfe": "1000" "mfe-MU": "1000" "mg": "1000" "mg-MG": "1000" "mgh": "1000" "mgh-MZ": "1000" "mgo": "1000" "mgo-CM": "1000" "mi": "0081" "mi-NZ": "0481" "mk": "002F" "mk-MK": "042F" "ml": "004C" "ml-IN": "044C" "mn": "0050" "mn-Cyrl": "7850" "mn-MN": "0450" "mn-Mong": "7C50" "mn-Mong-CN": "0850" "mn-Mong-MN": "0C50" "moh": "007C" "moh-CA": "047C" "mr": "004E" "mr-IN": "044E" "ms": "003E" "ms-BN": "083E" "ms-MY": "043E" "mt": "003A" "mt-MT": "043A" "mua": "1000" "mua-CM": "1000" "my": "0055" "my-MM": "0455" "mzn-IR": "1000" "naq": "1000" "naq-NA": "1000" "nb": "7C14" "nb-NO": "0414" "nb-SJ": "1000" "nd": "1000" "nd-ZW": "1000" "nds": "1000" "nds-DE": "1000" "nds-NL": "1000" "ne": "0061" "ne-IN": "0861" "ne-NP": "0461" "nl": "0013" "nl-AW": "1000" "nl-BE": "0813" "nl-BQ": "1000" "nl-CW": "1000" "nl-NL": "0413" "nl-SR": "1000" "nl-SX": "1000" "nmg": "1000" "nmg-CM": "1000" "nn": "7814" "nn-NO": "0814" "nnh": "1000" "nnh-CM": "1000" "no": "0014" "nqo": "1000" "nqo-GN": "1000" "nr": "1000" "nr-ZA": "1000" "nso": "006C" "nso-ZA": "046C" "nus": "1000" "nus-SD": "1000" "nus-SS": "1000" "nyn": "1000" "nyn-UG": "1000" "oc": "0082" "oc-FR": "0482" "om": "0072" "om-ET": "0472" "om-KE": "1000" "or": "0048" "or-IN": "0448" "os": "1000" "os-GE": "1000" "os-RU": "1000" "pa": "0046" "pa-Arab": "7C46" "pa-Arab-PK": "0846" "pa-IN": "0446" "pl": "0015" "pl-PL": "0415" "prg-001": "1000" "prs": "008C" "prs-AF": "048C" "ps": "0063" "ps-AF": "0463" "ps-PK": "1000" "pt": "0016" "pt-AO": "1000" "pt-BR": "0416" "pt-CH": "1000" "pt-CV": "1000" "pt-GQ": "1000" "pt-GW": "1000" "pt-LU": "1000" "pt-MO": "1000" "pt-MZ": "1000" "pt-PT": "0816" "pt-ST": "1000" "pt-TL": "1000" "qps-ploc": "0501" "qps-ploca": "05FE" "qps-plocm": "09FF" "quc": "0086" "quc-Latn-GT": "0486" "quz": "006B" "quz-BO": "046B" "quz-EC": "086B" "quz-PE": "0C6B" "rm": "0017" "rm-CH": "0417" "rn": "1000" "rn-BI": "1000" "ro": "0018" "ro-MD": "0818" "ro-RO": "0418" "rof": "1000" "rof-TZ": "1000" "ru": "0019" "ru-BY": "1000" "ru-KG": "1000" "ru-KZ": "1000" "ru-MD": "0819" "ru-RU": "0419" "ru-UA": "1000" "rw": "0087" "rw-RW": "0487" "rwk": "1000" "rwk-TZ": "1000" "sa": "004F" "sa-IN": "044F" "sah": "0085" "sah-RU": "0485" "saq": "1000" "saq-KE": "1000" "sbp": "1000" "sbp-TZ": "1000" "sd": "0059" "sd-Arab": "7C59" "sd-Arab-PK": "0859" "se": "003B" "se-FI": "0C3B" "se-NO": "043B" "se-SE": "083B" "seh": "1000" "seh-MZ": "1000" "ses": "1000" "ses-ML": "1000" "sg": "1000" "sg-CF": "1000" "shi": "1000" "shi-Latn": "1000" "shi-Latn-MA": "1000" "shi-Tfng": "1000" "shi-Tfng-MA": "1000" "si": "005B" "si-LK": "045B" "sk": "001B" "sk-SK": "041B" "sl": "0024" "sl-SI": "0424" "sma": "783B" "sma-NO": "183B" "sma-SE": "1C3B" "smj": "7C3B" "smj-NO": "103B" "smj-SE": "143B" "smn": "703B" "smn-FI": "243B" "sms": "743B" "sms-FI": "203B" "sn": "1000" "sn-Latn": "1000" "sn-Latn-ZW": "1000" "so": "0077" "so-DJ": "1000" "so-ET": "1000" "so-KE": "1000" "so-SO": "0477" "sq": "001C" "sq-AL": "041C" "sq-MK": "1000" "sr": "7C1A" "sr-Cyrl": "6C1A" "sr-Cyrl-BA": "1C1A" "sr-Cyrl-CS": "0C1A" "sr-Cyrl-ME": "301A" "sr-Cyrl-RS": "281A" "sr-Latn": "701A" "sr-Latn-BA": "181A" "sr-Latn-CS": "081A" "sr-Latn-ME": "2c1A" "sr-Latn-RS": "241A" "ss": "1000" "ss-SZ": "1000" "ss-ZA": "1000" "ssy": "1000" "ssy-ER": "1000" "st": "0030" "st-LS": "1000" "st-ZA": "0430" "sv": "001D" "sv-AX": "1000" "sv-FI": "081D" "sv-SE": "041D" "sw": "0041" "sw-KE": "0441" "sw-TZ": "1000" "sw-UG": "1000" "swc": "1000" "swc-CD": "1000" "syr": "005A" "syr-SY": "045A" "ta": "0049" "ta-IN": "0449" "ta-LK": "0849" "ta-MY": "1000" "ta-SG": "1000" "te": "004A" "te-IN": "044A" "teo": "1000" "teo-KE": "1000" "teo-UG": "1000" "tg": "0028" "tg-Cyrl": "7C28" "tg-Cyrl-TJ": "0428" "th": "001E" "th-TH": "041E" "ti": "0073" "ti-ER": "0873" "ti-ET": "0473" "tig": "1000" "tig-ER": "1000" "tk": "0042" "tk-TM": "0442" "tn": "0032" "tn-BW": "0832" "tn-ZA": "0432" "to": "1000" "to-TO": "1000" "tr": "001F" "tr-CY": "1000" "tr-TR": "041F" "ts": "0031" "ts-ZA": "0431" "tt": "0044" "tt-RU": "0444" "twq": "1000" "twq-NE": "1000" "tzm": "005F" "tzm-Latn": "7C5F" "tzm-Latn-DZ": "085F" "tzm-Latn-MA": "1000" "ug": "0080" "ug-CN": "0480" "uk": "0022" "uk-UA": "0422" "ur": "0020" "ur-IN": "0820" "ur-PK": "0420" "uz": "0043" "uz-Arab": "1000" "uz-Arab-AF": "1000" "uz-Cyrl": "7843" "uz-Cyrl-UZ": "0843" "uz-Latn": "7C43" "uz-Latn-UZ": "0443" "vai": "1000" "vai-Latn": "1000" "vai-Latn-LR": "1000" "vai-Vaii": "1000" "vai-Vaii-LR": "1000" "ve": "0033" "ve-ZA": "0433" "vi": "002A" "vi-VN": "042A" "vo": "1000" "vo-001": "1000" "vun": "1000" "vun-TZ": "1000" "wae": "1000" "wae-CH": "1000" "wal": "1000" "wal-ET": "1000" "wo": "0088" "wo-SN": "0488" "xh": "0034" "xh-ZA": "0434" "xog": "1000" "xog-UG": "1000" "yav": "1000" "yav-CM": "1000" "yo": "006A" "yo-BJ": "1000" "yo-NG": "046A" "zgh": "1000" "zgh-Tfng": "1000" "zgh-Tfng-MA": "1000" "zh": "7804" "zh-CN": "0804" "zh-Hans": "0004" "zh-Hant": "7C04" "zh-HK": "0C04" "zh-MO": "1404" "zh-SG": "1004" "zh-TW": "0404" "zu": "0035" "zu-ZA": "0435"kalamine-0.38/kalamine/generators/000077500000000000000000000000001465026171300171645ustar00rootroot00000000000000kalamine-0.38/kalamine/generators/__init.py__000066400000000000000000000000001465026171300212630ustar00rootroot00000000000000kalamine-0.38/kalamine/generators/ahk.py000066400000000000000000000074551465026171300203140ustar00rootroot00000000000000""" Windows: AHK To be used by AutoHotKey v1.1: https://autohotkey.com During our tests, AHK 2.0 has raised serious performance and stability issues. FWIW, PKL and EPKL still rely on AHK 1.1, too. """ import json from typing import TYPE_CHECKING, Dict, List if TYPE_CHECKING: from ..layout import KeyboardLayout from ..template import load_tpl, substitute_lines from ..utils import LAYER_KEYS, SCAN_CODES, Layer, load_data def ahk_keymap(layout: "KeyboardLayout", altgr: bool = False) -> List[str]: """AHK layout, main and AltGr layers.""" prefixes = [" ", "+", "", "", " <^>!", "<^>!+"] specials = " \u00a0\u202f‘’'\"^`~" esc_all = True # set to False to ease the debug (more readable AHK script) def ahk_escape(key: str) -> str: if len(key) == 1: return f"U+{ord(key):04x}" if (esc_all or key in specials) else key return f"{key}`" if key.endswith("`") else key # deadkey identifier def ahk_actions(symbol: str) -> Dict[str, str]: actions = {} for key, dk in layout.dead_keys.items(): dk_id = ahk_escape(key) if symbol == "spce": actions[dk_id] = ahk_escape(dk[" "]) elif symbol in dk: actions[dk_id] = ahk_escape(dk[symbol]) return actions output = [] for key_name in LAYER_KEYS: if key_name.startswith("-"): output.append(f"; {key_name[1:]}") output.append("") continue if key_name in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet sc = f"SC{SCAN_CODES['klc'][key_name]}" for i in ( [Layer.ALTGR, Layer.ALTGR_SHIFT] if altgr else [Layer.BASE, Layer.SHIFT] ): layer = layout.layers[i] if key_name not in layer: continue symbol = layer[key_name] sym = ahk_escape(symbol) if symbol in layout.dead_keys: actions = {sym: layout.dead_keys[symbol][symbol]} elif key_name == "spce": actions = ahk_actions(key_name) else: actions = ahk_actions(symbol) desc = f" ; {symbol}" if symbol != sym else "" act = json.dumps(actions, ensure_ascii=False) output.append(f'{prefixes[i]}{sc}::SendKey("{sym}", {act}){desc}') if output[-1]: output.append("") return output def ahk_shortcuts(layout: "KeyboardLayout") -> List[str]: """AHK layout, shortcuts.""" prefixes = [" ^", "^+"] enabled = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" qwerty_vk = load_data("qwerty_vk") output = [] for key_name in LAYER_KEYS: if key_name.startswith("-"): output.append(f"; {key_name[1:]}") output.append("") continue if key_name in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet scan_code = SCAN_CODES["klc"][key_name] for i in [Layer.BASE, Layer.SHIFT]: layer = layout.layers[i] if key_name not in layer: continue symbol = layer[key_name] if layout.qwerty_shortcuts: symbol = qwerty_vk[scan_code] if symbol in enabled: output.append(f"{prefixes[i]}SC{scan_code}::Send {prefixes[i]}{symbol}") if output[-1]: output.append("") return output def ahk(layout: "KeyboardLayout") -> str: """Windows AHK driver""" # fmt: off out = load_tpl(layout, ".ahk") out = substitute_lines(out, "LAYOUT", ahk_keymap(layout)) out = substitute_lines(out, "ALTGR", ahk_keymap(layout, True)) out = substitute_lines(out, "SHORTCUTS", ahk_shortcuts(layout)) # fmt: on return out kalamine-0.38/kalamine/generators/keylayout.py000066400000000000000000000140331465026171300215650ustar00rootroot00000000000000""" macOS: keylayout output https://developer.apple.com/library/content/technotes/tn2056/ """ from typing import TYPE_CHECKING, List, Tuple if TYPE_CHECKING: from ..layout import KeyboardLayout from ..template import load_tpl, substitute_lines from ..utils import DK_INDEX, LAYER_KEYS, SCAN_CODES, Layer, hex_ord def _xml_proof(char: str) -> str: if char not in '<&"\u0020\u00a0\u202f>': return char return f"&#x{hex_ord(char)};" def _xml_proof_id(symbol: str) -> str: return symbol[2:-1] if symbol.startswith("&#x") else symbol def macos_keymap(layout: "KeyboardLayout") -> List[List[str]]: """macOS layout, main part.""" if layout.qwerty_shortcuts: print("WARN: keeping qwerty shortcuts is not yet supported for MacOS") ret_str = [] for index in range(5): layer = layout.layers[ [Layer.BASE, Layer.SHIFT, Layer.BASE, Layer.ALTGR, Layer.ALTGR_SHIFT][index] ] caps = index == 2 def has_dead_keys(letter: str) -> bool: if letter in "\u0020\u00a0\u202f": # space return True for k in layout.dead_keys: if letter in layout.dead_keys[k]: return True return False output: List[str] = [] for key_name in LAYER_KEYS: if key_name in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet if key_name.startswith("-"): if output: output.append("") output.append("") continue symbol = "" final_key = True if key_name in layer: key = layer[key_name] if key in layout.dead_keys: symbol = f"dead_{DK_INDEX[key].name}" final_key = False else: symbol = _xml_proof(key.upper() if caps else key) final_key = not has_dead_keys(key.upper()) char = f"code=\"{SCAN_CODES['osx'][key_name]}\"".ljust(10) if final_key: action = f'output="{symbol}"' elif symbol.startswith("dead_"): action = f'action="{_xml_proof_id(symbol)}"' else: action = f'action="{key_name}_{_xml_proof_id(symbol)}"' output.append(f"") ret_str.append(output) return ret_str def macos_actions(layout: "KeyboardLayout") -> List[str]: """macOS layout, dead key actions.""" ret_actions = [] def when(state: str, action: str) -> str: state_attr = f'state="{state}"'.ljust(18) if action in layout.dead_keys: action_attr = f'next="{DK_INDEX[action].name}"' elif action.startswith("dead_"): action_attr = f'next="{action[5:]}"' else: action_attr = f'output="{_xml_proof(action)}"' return f" " def append_actions(key: str, symbol: str, actions: List[Tuple[str, str]]) -> None: ret_actions.append(f'') ret_actions.append(when("none", symbol)) for state, out in actions: ret_actions.append(when(state, out)) ret_actions.append("") # dead key definitions for key in layout.dead_keys: name = DK_INDEX[key].name term = layout.dead_keys[key][key] ret_actions.append(f'') ret_actions.append(f' ') if name == "1dk" and term in layout.dead_keys: nested_dk = DK_INDEX[term].name ret_actions.append(f' ') ret_actions.append("") continue # normal key actions for key_name in LAYER_KEYS: if key_name.startswith("-"): ret_actions.append("") ret_actions.append(f"") continue for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: if key_name == "spce" or key_name not in layout.layers[i]: continue key = layout.layers[i][key_name] if i and key == layout.layers[Layer.BASE][key_name]: continue if key in layout.dead_keys: continue actions: List[Tuple[str, str]] = [] for k in DK_INDEX: if k in layout.dead_keys: if key in layout.dead_keys[k]: actions.append((DK_INDEX[k].name, layout.dead_keys[k][key])) if actions: append_actions(key_name, _xml_proof(key), actions) # spacebar actions actions = [] for k in DK_INDEX: if k in layout.dead_keys: actions.append((DK_INDEX[k].name, layout.dead_keys[k][" "])) append_actions("spce", " ", actions) # space append_actions("spce", " ", actions) # no-break space append_actions("spce", " ", actions) # fine no-break space return ret_actions def macos_terminators(layout: "KeyboardLayout") -> List[str]: """macOS layout, dead key terminators.""" ret_terminators = [] for key in DK_INDEX: if key not in layout.dead_keys: continue dk = layout.dead_keys[key] name = DK_INDEX[key].name term = dk[key] if name == "1dk" and term in layout.dead_keys: term = dk[" "] state = f'state="{name}"'.ljust(18) output = f'output="{_xml_proof(term)}"' ret_terminators.append(f"") return ret_terminators def keylayout(layout: "KeyboardLayout") -> str: """macOS driver""" out = load_tpl(layout, ".keylayout") for i, layer in enumerate(macos_keymap(layout)): out = substitute_lines(out, "LAYER_" + str(i), layer) out = substitute_lines(out, "ACTIONS", macos_actions(layout)) out = substitute_lines(out, "TERMINATORS", macos_terminators(layout)) return out kalamine-0.38/kalamine/generators/klc.py000066400000000000000000000311051465026171300203070ustar00rootroot00000000000000""" Windows: KLC To be used by the MS Keyboard Layout Creator to generate an installer. https://www.microsoft.com/en-us/download/details.aspx?id=102134 https://levicki.net/articles/2006/09/29/HOWTO_Build_keyboard_layouts_for_Windows_x64.php Also supported by KbdEdit: http://www.kbdedit.com (non-free). Note: blank lines and comments in KLC sections are removed from the output file because they are not recognized by KBDEdit (as of v19.8.0). """ import re from typing import TYPE_CHECKING, List if TYPE_CHECKING: from ..layout import KeyboardLayout from ..template import load_tpl, substitute_lines, substitute_token from ..utils import DK_INDEX, LAYER_KEYS, SCAN_CODES, Layer, hex_ord, load_data # return the corresponding char for a symbol def _get_chr(symbol: str) -> str: if len(symbol) > 1 and symbol.endswith("@"): # remove dead key symbol for dict access key = symbol[:-1] else: key = symbol if len(symbol) == 4: char = chr(int(key, base=16)) else: char = symbol return char def _get_langid(locale: str) -> str: locale_codes = load_data("win_locales") if locale not in locale_codes: raise ValueError(f"`{locale}` is not a valid locale") return locale_codes[locale] oem_idx = 0 def klc_virtual_key(layout: "KeyboardLayout", symbols: list, scan_code: str) -> str: oem_102_scan_code = "56" if layout.angle_mod: oem_102_scan_code = "30" if scan_code == oem_102_scan_code: # manage the ISO key (between shift and Z on ISO keyboards). # We're assuming that its scancode is always 56 # https://www.win.tue.nl/~aeb/linux/kbd/scancodes.html return "OEM_102" base = _get_chr(symbols[0]) shifted = _get_chr(symbols[1]) # Can’t use `isdigit()` because `²` is a digit but we don't want that as a VK allowed_digit = "0123456789" # We assume that digit row always have digit as VK if base in allowed_digit: return base elif shifted in allowed_digit: return shifted if shifted.isascii() and shifted.isalpha(): return shifted # VK_OEM_* case if base == "," or shifted == ",": return "OEM_COMMA" elif base == "." or shifted == ".": return "OEM_PERIOD" elif base == "+" or shifted == "+": return "OEM_PLUS" elif base == "-" or shifted == "-": return "OEM_MINUS" elif base == " ": return "SPACE" else: MAX_OEM = 9 # We affect abitrary OEM VK and it will not match the one # in distributed layout. It can cause issue if a application # is awaiting a particular OEM_ for a hotkey global oem_idx oem_idx += 1 if oem_idx <= MAX_OEM: return "OEM_" + str(oem_idx) else: raise Exception("Too many OEM keys") def klc_keymap(layout: "KeyboardLayout") -> List[str]: """Windows layout, main part.""" supported_symbols = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" global oem_idx oem_idx = 0 # Python trick to do equivalent of C static variable output = [] qwerty_vk = load_data("qwerty_vk") for key_name in LAYER_KEYS: if key_name.startswith("-"): continue if key_name in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet symbols = [] description = "//" is_alpha = False for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: layer = layout.layers[i] if key_name in layer: symbol = layer[key_name] desc = symbol if symbol in layout.dead_keys: desc = layout.dead_keys[symbol][" "] symbol = hex_ord(desc) + "@" else: if i == Layer.BASE: is_alpha = symbol.upper() != symbol if symbol not in supported_symbols: symbol = hex_ord(symbol) symbols.append(symbol) else: desc = " " symbols.append("-1") description += " " + desc scan_code = SCAN_CODES["klc"][key_name] virtual_key = qwerty_vk[scan_code] if not layout.qwerty_shortcuts: virtual_key = klc_virtual_key(layout, symbols, scan_code) if layout.has_altgr: output.append( "\t".join( [ scan_code, virtual_key, "1" if is_alpha else "0", # affected by CapsLock? symbols[0], symbols[1], # base layer "-1", "-1", # ctrl layer symbols[2], symbols[3], # altgr layer description.strip(), ] ) ) else: output.append( "\t".join( [ scan_code, virtual_key, "1" if is_alpha else "0", # affected by CapsLock? symbols[0], symbols[1], # base layer "-1", "-1", # ctrl layer description.strip(), ] ) ) return output def klc_deadkeys(layout: "KeyboardLayout") -> List[str]: """Windows layout, dead keys.""" output = [] for k in DK_INDEX: if k not in layout.dead_keys: continue dk = layout.dead_keys[k] output.append(f"// DEADKEY: {DK_INDEX[k].name.upper()} //" + "{{{") output.append(f"DEADKEY\t{hex_ord(dk[' '])}") for base, alt in dk.items(): if base == k and alt in base: continue if base in layout.dead_keys: base = layout.dead_keys[base][" "] if alt in layout.dead_keys: alt = layout.dead_keys[alt][" "] ext = hex_ord(alt) + "@" else: ext = hex_ord(alt) output.append(f"{hex_ord(base)}\t{ext}\t// {base} -> {alt}") output.append("//}}}") output.append("") return output[:-1] def klc_dk_index(layout: "KeyboardLayout") -> List[str]: """Windows layout, dead key index.""" output = [] for k in DK_INDEX: if k not in layout.dead_keys: continue dk = layout.dead_keys[k] output.append(f"{hex_ord(dk[' '])}\t\"{DK_INDEX[k].name.upper()}\"") return output def c_keymap(layout: "KeyboardLayout") -> List[str]: """Windows C layout, main part.""" supported_symbols = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" qwerty_vk = load_data("qwerty_vk") global oem_idx oem_idx = 0 # Python trick to do equivalent of C static variable output = [] for key_name in LAYER_KEYS: if key_name.startswith("-"): continue if key_name in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet symbols = [] dead_symbols = [] is_alpha = False has_dead_key = False for i in [Layer.BASE, Layer.SHIFT, Layer.ALTGR, Layer.ALTGR_SHIFT]: layer = layout.layers[i] if key_name in layer: symbol = layer[key_name] desc = symbol dead = "WCH_NONE" if symbol in layout.dead_keys: desc = layout.dead_keys[symbol][" "] symbol = "WCH_DEAD" dead = hex_ord(desc) has_dead_key = True else: if i == Layer.BASE: is_alpha = symbol.upper() != symbol if symbol not in supported_symbols: symbol = hex_ord(symbol) symbols.append(symbol) dead_symbols.append(dead) else: desc = " " symbols.append("WCH_NONE") dead_symbols.append("WCH_NONE") scan_code = SCAN_CODES["klc"][key_name] virtual_key = qwerty_vk[scan_code] if not layout.qwerty_shortcuts: virtual_key = klc_virtual_key(layout, symbols, scan_code) if len(virtual_key) == 1: virtual_key_id = f"'{virtual_key}'" else: virtual_key_id = f"VK_{virtual_key}" def process_symbol(symbol: str) -> str: if len(symbol) == 4: return f"0x{symbol}" if len(symbol) == 1: return f"'{symbol}'" return symbol symbols[:] = [process_symbol(symbol) for symbol in symbols] dead_symbols[:] = [process_symbol(symbol) for symbol in dead_symbols] def key_list( key_syms: List[str], virt_key: str = "0xff", has_altgr: bool = False, is_alpha: bool = False, ) -> str: cols = [ virt_key, "CAPLOK" if is_alpha else "0", # affected by CapsLock? key_syms[0], key_syms[1], # base layer "WCH_NONE", "WCH_NONE", # ctrl layer ] if has_altgr: cols += [ key_syms[2], key_syms[3], ] return "\t,".join(cols) output.append( f"\t{{{key_list(symbols, virtual_key_id, layout.has_altgr, is_alpha)}}}," ) if has_dead_key: output.append( f"\t{{{key_list(dead_symbols, has_altgr=layout.has_altgr, is_alpha=is_alpha)}}}," ) return output def c_deadkeys(layout: "KeyboardLayout") -> List[str]: """Windows C layout, dead keys.""" output = [] for k in DK_INDEX: if k not in layout.dead_keys: continue dk = layout.dead_keys[k] output.append(f"// DEADKEY: {DK_INDEX[k].name.upper()}") for base, alt in dk.items(): if base == k and alt in base: continue if base in layout.dead_keys: base = layout.dead_keys[base][" "] if alt in layout.dead_keys: alt = layout.dead_keys[alt][" "] dead_alt = "0x0001" else: dead_alt = "0x0000" ext = hex_ord(alt) output.append( f"DEADTRANS(0x{hex_ord(base)}\t, 0x{hex_ord(dk[' '])}\t, 0x{ext}\t, {dead_alt}), /* {base} -> {alt} */" ) output.append("") return output[:-1] def c_dk_index(layout: "KeyboardLayout") -> List[str]: """Windows layout, dead key index.""" output = [] for k in DK_INDEX: if k not in layout.dead_keys: continue term = layout.dead_keys[k][" "] output.append(f'L"\\\\x{hex_ord(term)}"\tL"{DK_INDEX[k].name.upper()}",') return output def klc(layout: "KeyboardLayout") -> str: """Windows driver (warning: requires CR/LF + UTF16LE encoding)""" if len(layout.meta["name8"]) > 8: raise ValueError("`name8` max length is 8 charaters") # check `version` format # it must be `a.b.c[.d]` version = re.compile(r"^\d+\.\d+\.\d+(\.\d+)?$") if version.match(layout.meta["version"]) is None: raise ValueError("`version` must be in `a.b.c[.d]` form") locale = layout.meta["locale"] langid = _get_langid(locale) # fmt: off out = load_tpl(layout, ".klc") out = substitute_lines(out, "LAYOUT", klc_keymap(layout)) out = substitute_lines(out, "DEAD_KEYS", klc_deadkeys(layout)) out = substitute_lines(out, "DEAD_KEY_INDEX", klc_dk_index(layout)) out = substitute_token(out, "localeid", f"0000{langid}") out = substitute_token(out, "locale", locale) out = substitute_token(out, "encoding", "utf-16le") # fmt: on return out def klc_rc(layout: "KeyboardLayout") -> str: """Windows resource file for C drivers""" out = load_tpl(layout, ".RC") # version numbers are in "a,b,c,d" format version = layout.meta["version"].replace(".", ",") out = substitute_token(out, "rc_version", version) return out def klc_c(layout: "KeyboardLayout") -> str: """Windows keymap file for C drivers""" out = load_tpl(layout, ".C") out = substitute_lines(out, "LAYOUT", c_keymap(layout)) out = substitute_lines(out, "DEAD_KEYS", c_deadkeys(layout)) out = substitute_lines(out, "DEAD_KEY_INDEX", c_dk_index(layout)) return out kalamine-0.38/kalamine/generators/web.py000066400000000000000000000073421465026171300203210ustar00rootroot00000000000000""" JSON & SVG outputs To be used with the 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.py000066400000000000000000000071521465026171300203270ustar00rootroot00000000000000""" 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.py000066400000000000000000000110751465026171300163210ustar00rootroot00000000000000from 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.py000066400000000000000000000303331465026171300167040ustar00rootroot00000000000000import 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.py000066400000000000000000000230111465026171300201650ustar00rootroot00000000000000import 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.py000066400000000000000000000124511465026171300166760ustar00rootroot00000000000000import 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.py000066400000000000000000000030461465026171300172030ustar00rootroot00000000000000import 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/000077500000000000000000000000001465026171300170115ustar00rootroot00000000000000kalamine-0.38/kalamine/templates/base.C000066400000000000000000000305451465026171300200360ustar00rootroot00000000000000#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.ahk000066400000000000000000000053721465026171300204170ustar00rootroot00000000000000; ${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.keylayout000066400000000000000000000416541465026171300217050ustar00rootroot00000000000000 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.klc000066400000000000000000000031741465026171300204230ustar00rootroot00000000000000// ${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_keymap000066400000000000000000000014751465026171300220060ustar00rootroot00000000000000// ${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_symbols000066400000000000000000000010751465026171300222040ustar00rootroot00000000000000// ${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.C000066400000000000000000000313241465026171300200620ustar00rootroot00000000000000#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.RC000066400000000000000000000021441465026171300202020ustar00rootroot00000000000000#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.ahk000066400000000000000000000055741465026171300204530ustar00rootroot00000000000000; ${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.keylayout000066400000000000000000000416031465026171300217270ustar00rootroot00000000000000 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.klc000066400000000000000000000033761465026171300204570ustar00rootroot00000000000000// ${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_keymap000066400000000000000000000015371465026171300220350ustar00rootroot00000000000000// ${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_symbols000066400000000000000000000011401465026171300222250ustar00rootroot00000000000000// ${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_keymap000066400000000000000000000031331465026171300225660ustar00rootroot00000000000000// ${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_symbols000066400000000000000000000020141465026171300227650ustar00rootroot00000000000000// ${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.svg000066400000000000000000001304641465026171300216070ustar00rootroot00000000000000 半角 全角 漢字 英数 Ctrl Win Super Alt 한자 無変換 変換 カタカナ ひらがな ローマ字 한/영 AltGr Win Super Ctrl kalamine-0.38/kalamine/utils.py000066400000000000000000000067601465026171300165360ustar00rootroot00000000000000import 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/000077500000000000000000000000001465026171300156375ustar00rootroot00000000000000kalamine-0.38/kalamine/www/demo.js000066400000000000000000000054771465026171300171360ustar00rootroot00000000000000window.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.css000066400000000000000000000005211465026171300175070ustar00rootroot00000000000000body { 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.js000066400000000000000000001064211465026171300202460ustar00rootroot00000000000000/** * 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')}`; 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 = ` ${numberRow} ${letterRow1} ${letterRow2} ${letterRow3} ${baseRow} `; 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.py000066400000000000000000000362031465026171300176470ustar00rootroot00000000000000""" 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/000077500000000000000000000000001465026171300147325ustar00rootroot00000000000000kalamine-0.38/layouts/README.md000066400000000000000000000007201465026171300162100ustar00rootroot00000000000000# 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.toml000066400000000000000000000057211465026171300165660ustar00rootroot00000000000000name = "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.toml000066400000000000000000000057401465026171300166030ustar00rootroot00000000000000name = "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.toml000066400000000000000000000057371465026171300166120ustar00rootroot00000000000000name = "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.toml000066400000000000000000000024631465026171300161530ustar00rootroot00000000000000[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/000077500000000000000000000000001465026171300143745ustar00rootroot00000000000000kalamine-0.38/tests/__init__.py000066400000000000000000000000001465026171300164730ustar00rootroot00000000000000kalamine-0.38/tests/test_macos.py000066400000000000000000000054421465026171300171140ustar00rootroot00000000000000from 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.py000066400000000000000000000055611465026171300173100ustar00rootroot00000000000000from 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.py000066400000000000000000000660611465026171300210120ustar00rootroot00000000000000from 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.py000066400000000000000000001624751465026171300223030ustar00rootroot00000000000000from 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.py000066400000000000000000000361231465026171300210140ustar00rootroot00000000000000from 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.py000066400000000000000000000425071465026171300210320ustar00rootroot00000000000000from 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.py000066400000000000000000000005261465026171300157260ustar00rootroot00000000000000"""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.png000066400000000000000000002434641465026171300150630ustar00rootroot00000000000000PNG  IHDR'4!o IDATx^e@ LDQ1Q lQ;>[n  ,DBTTTTP ?̞={~uyggg޹y~- d" " " " " " " "3b&݉84D@D@D@D@D@D@D@!PsàAfmژc"SjNtXw]3Cf" " " " " " " ?f,mW"WO;(KtxN30l%DVD@D@D@D@D@D@@$с 8D(}g';8gRHy mt" H)1l2i瞛i ƙnoH h_cȇ|+VH6tIp=ZԃkVz3F 3{`Rh\pnDEdM7eΟp-Ėc=Dj4F D|iȶH>6GfL@1G Mh]ԬME_̈ uDQD9dP(3ȤJ!C2)gKOiB",i}H@dс&b„LDgg D;!<]#(ʈp'<h+3嗙D X1!a=^~9ﲋ(H-# cxD ʃz[qAJ5#ӹA&Hـg n1E&" " " " " " B,ѡ.CCC@A WȇbbB7fI2)ydc@e D| !R`2[t(]eO{,.[LBqrn٢C{Pt ځ(3HU!UD&" " " " " " B P(fwm"/Q"H v1YϞ,~RD@D@D@D@D@D@@ 6! '1D\ DD@D@D@D@D@D@jD:HT￿}'3\4gM.:\xO>9_ &^}U[{mĉA}, 2,5ut2" " " " " " "P"}osϞ=myr=6i$kK.veYn{۰alȑo-n.6`xw1؝wiwutI;{mW_}?~G׮]SO}O?&LyYgc:W5ոE@D@D@D@D@D@D =X馛 /_ަNjz]w Kzg\s>vu]gFrb٥^j+:~VrFD@AHxꩧl[?sXm})Sif<$:b~j" " " " " " UL 1au׵%Xnx[n9{0EcǺty+ r‹\r :4G8/6RKmOD ?ѣ#ѡ'." " " " " "Gyp ƿ7tS޽n:?|4hKFzQ /Bζ/ вe˺m@l;s\ŷ~k76l.eK/mHtLPBD@D@D@D@D@Dz $&:.{?te]sNG !5d;v~h@iӦY-\z5H z"zFAtAl#cPsZ#H Ȣ>H[9@E]d_~=vG۞{j5P VÇuY'Gthժ1"gx=c9 "&OlڵSCJ&!" " " " " "P"g}@AL3wu+BC:qgć+@t%_|qW\2CCÉ'"'hߟ1cF]1cƸ}CNj@:D(Hz 7~o^tp G kIj)l&.2o!7߸gy@H:_p Jb+lӦ586iC q)[m^}믿ꫯ"3d" " " " " " " ,:pzȉP~׻^t]%;@{ᄊ_~ڷoZX)"P@U&],pEy+$:9)?x6lQ[o߾9Ǻ:&LΝ;ԜxwqGTM+Kth,{u2e2, lSi#NtːOIDbID[P4:Y@*}WlR&" " " " " " "P>f+:v;.sc9gϞv2,S>Q}SD@D@D@D@D@D@DH"" " " " " " "P$:uY@HtH%рD@D@D@D@D@D@D6Ht먳([txK.^{~g[dEl-N:ڷo_we\xmذa9'u]g\stA9?ۃ>Nw =6d c9v-ZXc ;3lr1RnٝwLql:uuVXa2dH]n .0cD3fYgen9_Ƭj?x]Kzȑ# i7ؾv ':r:`gaYǬ4L>nc4hi4DK/Ԏ>h޽Ndxw쪫g9K-;}.Yyt嗷O?k1ǎ+;0=ò.kGqD=R+-u;pǍ`_ڏ?h~a>y b;Bvm ^{e<{֩Sza7tsشiӬuve;0lr]}n^i⤙ܾV^ye7y~$a뮻{nMbڧ@HEW^yŭ߉-[;lux #;?/k2èpW_}U믿8{뭷lVry':{Gl뭷vB*D\=p߷UVYEa `>mFY>C"DpJm%ѡ!?feƼ?cw!v'!)گD#Ytׯ=S_XvT3<Ӟ~ix?w~](?%\Ə.O(/h4Ft4i-Bv衇ڕW^3b~N0!;?,%%hR1Z#h8Am۶ogP E: B)0K,!bZrCX!59ӧs"B $Dxc__|9`Hl#9~m'\1Ej3]tQNzӝt-isui++cdGT˜Obp3_| ݺusBG;{キ#"z衜K-Q>Co\oR}&L`sm^f_^z9_1ßa̍lW@e DXgyА!r:#u1;#"i N0C=ow9眳΁nH.re[)сW_9tv[οy(vRAp逸Zǂc9r9q蓳>9#hϵF] Y#Xf9_uɓxSMˆmxb8|7 ,jar>,ƮꃐvPB lPW7Z+?-k׮nH N,o͹0|!,?DҔ_|q7߸vkPCZguܽqcDas - L4R!Xp]}w;qp#2U>rca߈'\:U>"c"r }'n D[m;`GO2$:PLJU 狕> Q d ℰҏ;;G Tb$ '|M5V_}U7l+%:-5Qjs>B B7V"AJW 0FXFAp'wp0ƎOˆlؚ8ws4X¹ ~uݯgİϊO!qkO$܆ u=A k/labDm&u`i0qDǟϸcƌqƜgCl :z?]s}#Np-0!Ɖ C $ SH2}?y;N+^j}(" " " " #ItiD9;찃 ppLGT pdpqYs iV _]nH`%hˆpmq+.>&+8RDsb5VIuHBt `Ǝc̱pLq$/ D 0 [p5o/pY*%: 9Nt@ 4;A bQ> ?itlqG]Lusňl! ?D3>/p_T$foD wcA(bu닱?"$_vBOΏ«>b */B sQ?b0\s\gCع>QSdG8S"ӎ#GW VYqqq(51gyaER!8N ')QD{}-:P'8{fs>}}>M:F|h(E$R–JoPD^BB@C!zx`._Ӄ[fQ! tpݲؠҧ(~[;0~t` a=1 Ńjxp{C,+K~`cPl!\-Oxp6$:FD@D@D@D@'It`8c圈|#G?!۰ @r1 V);{׋":w=8KсyرO`D?FoAyd| 76B|Vj%[lsOMv;ƦW}E^':puؘ[>D ":^coDNbQ"tRE[zaHΕ2/pEZ?\'xbNu(F|OҐU ^nQD@D@D@D@RK +N+WggM}rWaVDTBBq ϶ƈD?n*ѡ-Wt`>=/ꄐ@vX<޽+/:PXZ;hÐ2 u.q ٢t@@(H<|z gB>s"ߧ&3.++,EˆD6=ߥ "G="!O)7;QjXсyHgBY9}GD@D@D@D@D :Ȣ zD ȱM:+6j7;C0 ]XMš-LR|1ߨUHgDL@ ű?'B/:Pj^~‹D&/:ށÍfu! J< SL^A*'1&Ǘ4lсws1FV c%) \WV1WPQV-vZg2nvi%A6Wv3*a^4߽x{)\RAHq@,|rDufb1|D;q=GdQe(Gt2WÊ)a,ԉ!ʊh*E&" " " " "PYe رJO^}5!8GIU@!Ͽ/ֽ' g + ?!&: ,Nr"8?PB'glsK N IDATVcXEG7qoOAPďlс'1I3:&9|"…'[t@p!}/7ܵ[S;E8pqu9i+EC, ?/ qn \ )A)+V!/2 . !D-.Gt2WÊԦ a2([t& 8d8`"H "A~qJ D77+G$hnt" " " " " C 6с|R&hOIAҼFJ@B "*X)*D# ^¬8 " " " " " qMtsPWZ"O:Hth::@$:T{Ht4HDDj" " " " " " " 4D@D@D@D@D@D@D@! !کDDHtHv*" " " " " " " As@D@D@D@D@D@D@D HtHDDj" " " " " " " 4D@D@D@D@D@D@D@! !کDDHtHv*" " " " " " " As@D@D@D@D@D@D@D DMa>:>+hf:uZضj#kٲe3$: m:6sڹ6?=k6:H#+Ԏ@ Ht3HtH v," " " " " " ͛D}u" " " " " " "ՎE@D@D@D@D@D@Dyм^D@DBV^ye{{w1Q9;SFnԍ7pC:t]uUv!D4!D@D@Df̘aguaf;o{:l0{mW Ztml>h m-nݺ%sUD@D@RH@C /$" "P=Mf[v?~-䒱]w]~&W\(^yY.$zv~)عw&uH]wu_ogH" " "P$:TE@D@@%D&?*D*`@MPӗW''" "4Iљ" " "I~E@D@ja7|s,yww}6qDgyl7>:uTo IvڽkoD]w\sעX!ɓ'ۀ' &ӭ}袋l}Q|`_~h[l1۷uQ6|ݝ}ֱcǢO4ɱɶ~ب0rHm۶ֽ{w;Cm6jp]|6|pꫯlgv,Yfõm\znVpLlϹD@D@DV Ht+H?bks4!.8c^tm6P[pmԨQVHtGp"FmvԩS?p}[o ͢Cx0ik.C8`-b{Gcpos=u<y[b%B퓱lvNho-\w\WF2=P6O?T'x駟v!/gƍ0e3䓮m&\AAMEv'a5j|D@D@*J C ~O?9$ma落:cyQO?u+/Bø\eU7l;{vBF78.-M@X#"v@&ѡ}j% ѡZ-" " 8>6K-TSth耿AZBP Zg6ȥEPXm PqEPZNYgRC9Į1~xkٲe)4>QzaÆ}G.:WD@D@D\SD@D 1zWLt] ECF5pڇ D0VHt(vW_}lѢu5suQ"N<ĺ s+aՅ^R< m2} U=qI'9/r.B."U&" " ͅDru" " X=tJt~흰@5\ӵrĨUN_|Ez `@b~\Hx=g_ 8 0yq>8;}#@JɈ#r:dikIm4;u6!1SLz%Za6ksJ1Lj<n̤Vb arE-ՎE@D@D a݋>믿8w"첋%!:ANaƟ~(9"|;K?!# ٭>KtwwN=ǏN ;oȢ (RIJi&mڴq9ro7!&MOvzVt S!AGp"5@XQ qp<%с'|bzgX : 7x+ɶ"Jn\dǖ[n"F0D#BW_9ωS ` .1j(:uWih *Z% ѡVKD@D@D@D@D@D@D&:*zeu^" " " " " " "$:4E@D@D@D@D@D@DV Ht+&& ѡ//" " " " " " J@C^Y41D^ֲm3}e" " " " " " " ͇w`Çn[oInO` 61c=ȳg_:-l[ml2EBQ@@@@"$:$U;9 " " " " " " "`NE@D@D@D@D@D@D@$:h$B@C"XS" " " " " " " VTD@D@D@D@D@D@D@@"$:$U;9 " " " " " " "`NE@D@D@D@D@D@D@$:h$B@C"XS" " " " " " " VTD@D@D@D@D@D@D@@"$:$U;9 " " " " " " "`NE@D@D@D@D@D@D@$:h$B@C"XS" " " " " " " VTD@D@D@D@D@D@D@@"$:$U;9 " " " " " " "Dƍg?_&2JtfyYd[tE>[oe/nmڴr-Wp>o~_413[۶mmV-[5/&N3f,<Ν;[vsww}yZ <#e믿~ɽT iݺ˺wrߗ>{5a;v^Ә>}=j#3û;s\W_U{_㝌^e&m<Լ40ju΋ d:kvSO=ecs?DG6!/ma:0?ѪU_Ov1?~g5jm.) / :G 2,!==1sW3sdʔ)O?^3$R1s<?Cx bXÆ s((Ϡǩv-ZpBsyo{&#LZ!` t5~_y's V8

iz#v^j^~e#K:=~ Yݻwm#mҤI=ԤR-%:z]%Ӄ^J)8u0Ժ=ֳgOW$4_~)8K,Ds2d{YZ^|xAjLa5pde_{\cVucuRo_8^`ŶK.52HcǺߙbFjY4&Ͽ&a8FVY Q ^h8F j JmU:q/ •JM"SN";`s/VP̨i5#\jL?eEbPRGwwڰQϰQCsqʏV߾}]X! "q0?ZɁ K#;K"v7/_^Y}jj:bB1'șZwu w楑H@QTsߢXo&YX.oQB (5üs-jSDA!Z:&YX.oQB #[DtPԔ62>ӒQ݄r5"D:K@(1k[(0΋U9zOz,W; ?~RNb0Zys,/䒪Eb-Voh#n/DC[Ǐ[ {E1jq =Qs<ӁߢM74ɡfߤ!j9_#VE:R9 kߞyPh <H(BM)QQVm=X'Yj1ZH"PYDkF jP#\Z;}cX83WꫯN Aub92\Np1'vqQ[?Ax/̀4CⲬd#2ޫ|i: Du \Һ9D\"lS@ݥZ*WWËCXItIUC.8EoZސR.{2/3Is ,ENi4Wa2 IDAT1pN</q뮻@aS#qhI‰? -"W4ٌSt9YBdv衇qZIC%JRt`nPg,)сs?]T7\5aZL$:TÓ(ѡYC2]8 W†Q 1g?nVxy!b6mvLKH7B# _֥[@45 $4Vo14/ sO mРAv'rۋ/Vqj&6Ep FnAd0Gg}vj8E{`ڧ9&Zό):pqa0D=}K8pe9TʦIl=c\`*+:^ b"4w$:jo,RrM$:/:]tE2 i^x2MD^i'I!=)0VHCVSt TViIQ ǛZrO*-pj &op @~u1a{}8 8u+0lz!{KFq5DFSt`>W- G^x#4\xvy9Q^̟bݛr$%:o 5HW)Q)d;IсEj#5PW,6-<$:TVt@7[Jv I萖+qHtHuH8}ɡyw}wj$%:BA*+.>NJȏl{gJÈSt`LhBrXX5Ft vyoQ#|D; J R+ xq{}Gyhr0O"m|au]]cB"&Fwx&%:l"nN/qI'dt AFNItL;BJ@CTb m/!lcj:𢇳D++ i#%LV`q(C},.с7Zщ*ӬBVcƈ [nvi~zW+,zJΝo\CFe~BsG(4Ykb@{{Z,ijI ѡԽHRRr%:':"JAve^#U(*tlQ\m^}VqF MCMD^+FЮj}""dq8 )*DKqDlVه iJ8svCR-!e|q?QU܍2[tIdikiEDP!hRi%qq7iсDFkhv ѡPPى$EB`y/R[167έv$SJ~n;B>qE RƦD ]GJt 4iDgt"i&9t$kLDZRk\L0ҎO@ [#S4M&!j):>@}7`#AՐ@+DФSty]]9W#ȷ䷈g,&FKJtv"h ^-@tW^y%>P*"Djy58Zt?][7bG^lx?lZ] @^|^ XH[w$EV.Uf?jNNz_~EEȒMUi$DF-FlSfڇHq㐦IcDO(R hH "&t@/7K~R3e$:$#:{vi~Sq*Y6 Rl_<5):`q~ A_*كIBt@Ĥf g:DU%-:>x.ܴ ѡܔPPjE;{Xdp:S^QcEPⴅ&):CI:U}yڧ}ĉ#‰>gd@tR@w$DD(9`jmݖr (I /WنE/Z:7RXK>/'44$}L VE<$:^8"x[('섩|GwޮrC/:=$Dj,VÃE]P1&-:d!2&P+ DDi$:jo,U+:s*$ϯ2Ϥ5j\"ϊ577/@i$Egm?<|v(#c*ץK띺O|mByinjCt`u ocǎuuV7>G#ć1m?l 8nC`ueYƨ@T/M='򏟔yJD Q7yw i0W!LjZtq$ D< 8s% sÏ!H"Zwu]XN %ZWSҢq6dwÆ snMCS"*VұJ]D4M?oƦ$):ˆIkم$ɻu(/lQVd$D Qq~6NW^u}aFC5K5s3RK1&눈I%):\Ì')сB!jz)iсS اhuy"!ѡs 6DjEjfIamHaenz#$P_ zi&tY#G:.2G?!i8D3{ڭR|M 01nсz#^Ii۶W]unᆜ(Nj7\K=H/8*uj GBʕF{U'&?1s"{LIVNPP:KtH4DrA+:j0 4nidŅ [LHqCt <^XN=REB & rJO1ִ[\u S~L+_JEZ}vI'6lSw(bIRۼn5 3h$:^8Dx>sv >LKXCRC.DFXҢ/yᇧDHC.W6nJthR9tǓ*lG;^#(IE:S k 8Нo5J'`qOo;e^)8JaDS?v9L):D5 !/H@1`q+ɯ<hj./øD.vvc=H7d׊'r]bbrD\{ޠ>-w1s:j%(0ʋi&P9/hiDX`uPDU5%:'):# n 8P|p]rAZAkD*vn'N! w}b.rW%:T| PCrCB⻕P9HKVi+~#0)сDl6 aPY!TMͦRs)R9),r/DcE6uTRK+VW]uz!jL.r퍢WdJ[}iϞ= g|@Zƞ8(9rHI] WDRLCDDơjiM$)с.ҍMDDRRr%:/:iӦ kFB~'Nhƍs 7DC+ !"ryHtP-ѡGC巷2'%: <-P7muN3$:zJt(Ey.!%_C2oO?>aа +ln+VPYс€O ѡ\C)ѡCkϓtQ@b|grHM+M$+~21PC kxRrq%:$+:2= ʾPME$:D%:Ht(6c$:҉StXk\{0֩S'{衇lZm$:"P)XU萒%AC(ACGDJ%Kt !=RDttNCJD݌It6tl-ADhD3곒 !-kFtxmB Y߾}>f8)gV.˱$:HtVKCmӾZ3#8jZv֪PD?N$:Ht(5[^QPfDrw_W_mkf2WOG×%Y97Xj_tO{Y~ѦL-Wc[u9Z/릑 ѡġ% 72ZP>K]PDR7DR5#:pL?c4£)6/fL?yf2Z3/Y֍Iq=f#۬j/RA7,̖nk֮]on;L3d]t)>~'֭[0ɉywߵZha:tݻ.b /pAF+Xp|771ڗ Gy`hfmT/Gu \x$e :vەw8#_<Ȣk젃*oE; Y2{>~$%:| vϔI&g ]pVYeyml?n}(tV}^5o[TcW]uQ46l?X˖;_~qyvЎŐyV\q@dV[m҃ŏ?Z,ێYp`X -Ǎ&-:L>{1{Ǎk~\pAի6sD<қ,ǧZ~\[f 'J?{$D[oՆEڵkI'JY -և;D^DC{nUS={\u'xtm;YN{St?{spp*sƌu\Ywu/OogS6@H Yf㏺!1pt/u/[omHRtyIo2x)nvm7{˻xEfaĈv]ƳTWU%mW|`&gqh0p B?O<# {kmfX>Im0"=3Bm`>Д-kݿ[k!njABݹŊ"ŜsYᦛnr]\mhNlӂzA6,]Rl0uTCݽbǏj '|r(&l+L$:5':p*g%s{9q㢇%:q^+B=mȑn l'PSRN¦%:MTlb@]|;g[lqVl A&Ղ0u0暠-.뮫wwwfso8Ax)ACUC)Bs)Nt%hrq6ZAoMU*):N׺V&eD(7" сA  9o|x8`un nո4WO4i`up EJ^pEZr 2(/yi5LC H\&IDQ9RM꒬ HthkF !BT*ҁ}IZ䘖ʛ@X0X1ҊWC엺Jt( [_%:7ܐix*GU CItN47wЂ!!6IJtY6sdHZ8P!Rl~B_{kC1tuyݻw/u8E M&.;0:^frEq( <:49;hҢaD!Nr%f)nс%D|D] Rxi&:褓NU̥^CP&ˆo-84ACwv-y58gLs)phy.Ɉ hD 3ajF): rKAhؗDPC6[t@h E;|D﫧~ٳD"!!]GCJGb e}*!i;F֚[(1ȷEtH BeIك!dyH Wm&.' 6VgIthxR`9Vhq 8cU6?P対OҢA: /Dİ Am!NсۃJDyBO6К1h,VIGtYG>8 `p}|" 7<D` v7bBD ߙJpf8)6x8E4D:0'}dעD$:2StA ]=)RwG@_*s%{DwyKtHoך@<--Iс erL~-$ 0aۖ54J?8Ex}Ƌ!ı= pG]^=rNUCWVXC-N$!#JcbqiEV qr+awmfA9 &Y~!M`3VkYXﰃQi{$,w`10H+}8EszȐ!.|J!~Yѧ߸"wPÁڝ03~҂॒ࣥkH/bP*PR5 X7R^|RUppN;<'F,uK,h7D !%!JlRkC!"eYR/(QIk> rAUԎ_JD< ߈ XPݽsDD2H8t?x>H[|FB5JWXFStOˆX7d$/=b,Iс{ZL\_"Ie$YYJxMZ ݐy4>qk~՚GQ!< 4%fQw JKRnW Ի(eqQW1Ӆ5,.!.'%:de2kj 8Dj)$ڢA! 4D8A2Q&ѡ1;JtiY{5ѡ,Y_JBt'VPF?rWcEV=a+`EqaG acۮ@Z hqadՍj =yL%):t5u=ӮUbt)\ )&:& 3j:!ʁhRV)с`KhQC("cHK!"LOb5+ Rݤ!/qbkNmTK,HOf @AHaI8G:x5-\XanFw VaD<. OC}*r$!:p5yf=X>wvu21Ox+!^e-nс^*b8X`ٽQOD^Tώjq8O8Gz`BK2+,G8EڠA@E~ry +VKPe K%NсPEoK5zo4#>OnLJ-nсVG.!0)x <)?O(H A%tmS*WGKt`o )kyv*7iҤg+)P4 '2C0sϿwAmܰ%h#( s qk[ Q1Q~h^{bWRr6YSd0nKۻRduO~-߮pr>St`5SQ! )@n:Tzؘ`VTJQC.8E ݽ\$waCl`z(q7 +nс#!rC@^h~!:Pϭ%!:p2㙂IȱqtqTlD=v5?-'VD|@!#khwA+#=,( 5 v G{"ְ)' C[t823_b08һuexD kꓓ$Iѡy磊D\EVÈ3ҡAEWqX܉i+^[sCY'D_SthSI;KBt)~EX):Tx. !)@4 j1J5%:KtȽ&:pvADeP3! %$⣠VC.6<$:P1#!D\ :2;l&"AC55%:Kt}с3$ Ө)$:kAno% ѡ|PP\CJD!:;jIt(A$:HtnRC}V$:Ht ѡlPP<բC-r}QWB,\+'ɳRiڱcǢ΃^FRQ?,WN~i[*BԩSD79DTus"L%:0 p.JI aqؕ6’)z]51ȳy0q?0~؈vmd8(BJSŠcO_*hOԢ`|T/&2xOޛo^նȿd4t&TNH5:I1QxsuҥdH0|#_,BNs 'j. vM+`b׶mÁ%b'/@ڵg9wӼ! 1)}чvQ7Znի鷶^O?y98^0Q/7Y 3҇!7ikl#(/A+\]@?u#=W$%Z4r&L ,]5:-&0Ub>ЋU!r0׭[7rQ|M}fUB*NQ9gx91@1|W5aF? СC9t}p >Ea/@>I}*u/}jE!M 2U!S}(}~@nmWbA 3)drLpkX ! @ۡ͸܇-Zwzp{g,]Cq Xb`aItyOԪU8r1.P ø:ܟ6 }!sT< wCڜo{Ƹ d-Gwb`'О(40R0QѻlL5iCpJrNuO;G܋uecB3} J#p,obeOMj爉1qvxkgzP&&ID}7꧟~R-VuQWM4Qϟ8o3]>zW疯ZiӦqP-R_}aηw8sѣEYsu嶻+ͬz%_g~Aq;痻jA߱:$}׺~|NAjiM5kWxb_jM\CVXa݇%!駟>m\֭~ڼyYC8M5jW=7ou<w7o[Ї0h_,r}\){b̠4q±cǪ{Ű@xKƳ5”)S4r-_w Mfx[jv2^{OYCo Gr+J 0yV%yW<< dұcǒ*QްgékO6M{(o?"*g}y{0wWLhC10QJt[ 9-Eߊ;s^HEAHWe'fM6E@&:.`M֍| zfn@ѧ~ZI\nx0 ?#Ȳgٹ%(1yG=\Cxt?4#e=xzH^͈].W#[ȉA;ZLf`W}Y g|rK x կ_/|xÄ aEu5Xy0MI(IBG}w pU3qv+jPa%yhw\[:rG>:`Ha+0A;xPOˡjFVx4 ð6:L 8~6R@ݛo's{X IDATѥkU{%l /0ʔ ٧Myz7fEkK*w7`_%4G5_s9QoS@ $ M:#~W<HcEpTC"qvm7Z;*\Y.[kgׁqs_0} 7sw^3T5f4cHsNNߓI2#8ź?ߙAЁETБpc9;H*>X?K۶mI\N`wI'Eԧn!w%?7⊢L;Cu~38#EwPخ^΂d5tLva:'QG?𴋒:AIvmYhn  npcR?l,rxo~1k׉{^ ÂE͹ k̙ ;9YDd1$U" MbN>9 Z4-iz ٸ.fq:՞7>AC\*$ܰa>'埁KxeY2:skP|M 쬳Ϊq@{Յ^xrV:([΢?+ ?f),8v_< ,jZ,bqaLU ,)@_$6HP,~{U\s$p-UB'I@¯#СPSOT$ĀޒWMn5D2-@/Wc/6=Mp*^.Jt4t_zA(upᱎ\~ !S*W%^,{qiȮ;L11Cq뭷&'$q AW9rd5N 5A%-pmr1곒Uwn٥û>8ؚ4 P0Ѽu.XRЁrE]=I(Uw|`"kXJg: СTOvjc 7t³ ;CTͤz[{^ڬ&t׿^|EkCMu0-|= j"С=U3M;|@UwI@8*dL~krbˠ :Lv=jYA]wݥj7JW:TiCdݣ !0 M)wPxЁ;IpK= W#MC\*RRb&q Jv_<d%\d@s=uq-=>;|i!|:8٤`{Cp-q6@A`2t:D%@C~(kV-䢒سM':8Yu\-V/g̘o?gDi>oηe >hw KBM,MOAHL_t*Wߕ x8~xu 'IYSV ɡY#/YYMGfwi@ѧz';؀d7&KõnZ!\6Z@#1+.œdW^yed@BW[{9`$f3tt(ӡP85]({}UW={Zӌ&9/-t! tk׮]A/&t 82[L_t*Wߕ M<@j*$,RX|cU8 J$;vwuM#!I`o߾o,$LUNY } 6ZСX9Ģ'tҲ6!Сx%7,t1tt(R p޽~'ׯMeӂAGfBL?HЁI_>*5$U+g/ <Оh[mt_/b:}{qJt;v - )v#0` p2iFPF8 cs\ XZATڛcџ,$d<LIx:~4 /0X\7x7I@/)СP0 {9 7jfӶ_nNV fF-[U_,MaQ8V`1㾽@UwB!A*ˮP=T0sL E$oO|{?EOc9K:ĚV{ r(v~&v~UЁ ĢpZ()ShW-\YdI@oZi0!4<*:T:;C/8~$WuԱw СЁg{iobah~ P6&%/7߬ƍ_O&@BB_ɡbm&t I#IdM? t+ tT쭒`ĉ3"8ŋQo`%ayȐ!ءĈ۾~Y%KZ!LL0i'Z3DM&СkH"B=L'a9 . X6]:~$r:w7oN `lРΣ֭Th4=:-СP#'lᆊpr1sL;^)9*W% (:Grp,ɼpy1bIJc#+T&xr#&rt+vm.첰*rCv[zzAe t( 0tB>}t=*v x<]Gx6@¯$t΀23;lHI@x4l*"СI:=\tt8묳ԨQo6`g5㾽@UwICAH~6Ir9tLIB_B7L :Tl!- ӄexa:20MP$򸤡C"/U$ *X&tIct7J:<er\tHpk?#:UB+thtvm!ɸoٔ0QCPCu]:j"СPzN::F믯5kVA+СPk^OM.H'ucW& ?I&#m`W2㾽@Uw>xkeg&Q@I@:b: СSNE9dMº$.СЁrܮo49f% vqG}oA'٨@i%`w/V.H#ACX=OB:tk]+ OB=::46XXϚ5K{4pwְxuЁ#tk׮:$PQ"U.P:tk: (Tt!IxEP^ԑ]v%l pQ俳@;!r@AUO+#A@t!AWx:T_:j"СP4h'WVUt@Uw:t:t`fL':tPg !h`>&J?; t\~'A@hO@mt!A@Vt(IY)UW N@@یC@ ƏN8ᄂGr\СC; ]t𾭹_P)+\r^i:}'q:t t ͘N:\O(9k=Ԃ=e7$KW|R3fw[WVjw*YpJ䫕Fx8amяׯ޽هUiAnM}41cƨ 8/:j>t[ŠCK_>׺ t !Z Ay1t__K VZIyA.M=a>8},Q H J~YP]4&lncUiAx4hРZ u /PN|y^t[@~M9Rj_GVƍSgyZuU㖡VN@њ@lCvˮd 92~N:\?+<{^~(լYp#KrKZȁ`ϕL+סï*3Jykyղe&wܧRm(ն& uPG:s8Ģ+`:Gse?y{u-_ pՊ+>loh@ ~YWMWfWdJկ|Cm/MըQ#!a(SjȐjb[I/\s*JիR"C;&MRjʔtCTW/+J5lV[):fZ tTSҀ{=76 ֗b$txo&]!N9[DS_-СvAv]>T3bԺ򼅲fV ۍl>RDWI@!CCv^0c*խJs@tPs jmԩ W`"|37u NȴT- xYgFyyj63rݟ d:ow}+" W: Ѐ'ɭWVjM[@yٱ\Y;&(ժU8d.A@ta{xa=[869:%Gx ;,&ߝ;B ˞;ry<0=H R L-+Ё XzysS%NCf^QG<SO=U{ןtI+n-WW`Kc/o[_f'BIF5b-۵+,P||͋^D6VlkXl13G&˽(o|Mi 2͟ԩ|@8 k۞ZkiZ<8wڴiEiPsەW$+v oCcӡ˜ҳg55_1t蠔^XlQhgɹS=h7x4i{ &СP*z{Ԍ܁O>i\_œ\ 8cL>_3&uXQ!vs:/|0Gס99wv_0'-Q \~+2|&3݉P:!zN:lr o߿W[kB7%[\>^fmL ʯ%.C?¸G \3)' u@U(IRg?S`"AC\?-3=X5qD//Ƀ+Q"Z?e$$yfR;vTÆ S~Nö)>*7Ք5r:ĭas:,\ȷ;04QVC~x 0x3b٩"v tT?:d :r9~c܎o:6W80le@(/.8FMHH~o@u@ЅMzfLCq:t(#M0bu饗>ZzaW@W,03W4vO;45}tu1x &.̣^ WlԽR[zߗE萯 !:+&СPW=PX?L'z:+KMC!9{A> \~rQJIDVy<%cPUCU.P(@B=҄{;Zuk\s\N8A/6_矯_}/l tLɑGveU@BA\Lj [7nsxqiHW̖z[9<\ˮLq}ukp!C&WU}wWלmиW5\9 t(Tta#tX@) iZ:TěnĊa?.2syk1G*_ ޽{/vW_Z xGiRj*@BU:&tITGs=aNͅFYw4JYC͛-.f M YT!rOt:[0UܰЁr]Gz0EMYUAKԯV{;o'SzAI>d{oF(Nj }I$l5t|y'7!n>nĮ />t8,( H:yt{KrUWZkM:Á3S[ 9r |{d:ee]!/k IDATY8;(ɤ_ lERnXd@8&СP5zt(ta#t$P!9pr!))KLyuQ{Ly^F<k(СPz xL53ƃ%-ut[ز@Nn`+B[imCxNCa%t/ qb1N^\1f]t(CC^{Jq$"c`?> q*V,AM}Ё8PAhF)f9]&^z10&!|t(ԣ/VSx: ~( t0}9LIel덐{N.)9RPC%%@<2ng :|ip`Cx#AC$ACIz\ K#㈟)EIA3w?ntx7OY#wĂ0htx<*9:t !t֣t(THC ƢrСaMO :BHokzs-rt@~Z{r!c\ʿ^7 AC6*A@6c:OBMm 0W tV[H$9Klu< 6|שӂrPSN9 {rA*`::O- AC.A@6c: СSNE+G}dM+WCtpzN :@;KO>γPNϗETD;wV͚5+)+y)ۛ^6]>;kG<82hq[omӫ.-zS' 2\!XbѣGmF'*fӤI$/:-[,ZY^9/ڵk;wU݋^ƹy=~2+jw ,+z]+?;vtM쭷z뭗:*~3V@;cK)b["1zXB;3fw197<7 PwNx֭3!!3l޼y?uD< qߦ$@P QլAajsYhe&N'zZ*Z%aO=2dH,Xn`ecLvМIYuӧOUcǎUmڴitС*io.\wuAc|QtL3x՗]i*05, vMvfc HLZx$J2~@/dxԀX~CPḻCl+bҥڋ7kpXkB=Mb{;(<%kZ@>$,vh"KjMvBs92}PC`BG zPWS)OT#hxGЇe^L<,h+!ծA+Bj*'-p8E;rЃ}H!z`n&8}R c.>N!2NX/^Sͨ[!qꘋ+wNb~@tt> y=48:RwUePc.uϤ=`jt>RJ9h ¸vY$0ɧxK`O;~I)}6ÄU͠#LM~0dfCgrge^^N0 } (]bVLմ,! HfCPW\wW>0k5}`Mbh/]zӄymmJS MDQ@DQ@DQ@DQ t7( ( ( Q@CEd( ( ( W@CPDQ@DQ@DQ@*"/39+sѦLi v`G:u"! nݺR`fTVXA!R~m>-W?փth+ԑ$ eCV[bo|w}c j ƙ%Kz9%}z0a~P?K_UQ/ЃY)8rR $5781kO>DO)TnOut8 Ht[ljܸqΛ7Oz@kԨQ@W_}',m۶UlAcp7 ߠA/_~ef͚6,/ԃ$1ۿdǂ3gTfҋ =6vOnƪiӦ_믿V}c N`?֭[;F@7qD=dꪫF-?[.ZHOj4irX;?\6g…ӇāgV3fqe=gϟХKբEu='M%}*0Uŋ-[]F. Z뺞^\wS?|ĜGs:427tSk2] wD-\_}V[+J4\",ZTN筷R[oZwu_NkΜ9gϞN5=mx@mv%{٦{ァC6'P}5Map]w5ԫ1c]v6/1^P6~@?",Jr\M6o7hϸ[NCt)S4cej@-^zH2}N%cxW#y06@Jl;VSG}u >Gˆ8%/:9RdmGL~gb ̘@ҧOK; DH2/a@O}2dHإɓ'=Zm&zW!裏vȕ@ Y5&@x5oaӵY5 Syi}rQ3Y_e+xS;Nw૖ C1B <8Uף8!XVx~Wfqbm(;Ne -&Yr5e` ်.+u:u VO="ZeULꀗa^rj#2~ЇG[w}k2~A7Y7w[6~I"͚¼䘿)N~._ӻR=(ĈlqG]޷< 6v}qKXw`7"  (@Q~&zČa@0+;. nAwޙ,I4c,ڐˀ<AvGwЄg}XdAA;hWOm1zgC=ظ )![ҧ6ihW~k&<O PG;PGk1SChEm0Y)k]h5 ˟GM6!}?O!iXIĵc(ܳ$"$&(kLYג.z3O@\.k[O?:,JI(A%6$U!)x;#( \1C HcNC~z죏{B6UN t:diA2p s藽5<0ipkvٙoO )k4tϸddi ?^ 4F9;Λ? -IѰa|CeպBS t^K:jR.@hR/T"СPyڢ@0* СTH :_ id$g7n $ XC3{G.g2HfXh1뮻&iPӮ-;ٓ&MR9Se\ک.IA&~A.Õ>]*@a/>{W_l4x:*$!KU@C W&DG\r:SuHGj% .Bww)h.~1W^yEp/|z55)pq)[n%8Tzo4:tiCiƍ5:tk3:{ tHVO@3 Zsܱ>`uO:Pމ'_^Ёw ;LBIx7[@Ô)S xD买bB:4})hOaǷo ! tVG* !Y=n)A

.ɬl$[ϮVnFq9hvѱ:$ 2Jww !Τ'x8kq'(۰,::OSboљ#H:t(W$) AjB $0g{dؤI4|y:VK:5JuY P{СC9ӶR 萟 Cr9p$}`\6o1B8%iuQ7t3G tfҀrmVdN sT&9l:x7V"iQCw}w5xv_h:ǃ$t GЁ}#?CvEtݻwBT:SO҂SPl$r:%I(J.[!rdf(rU4'ָqcּy/::skСjs|UҀQM tkd]:dVTIB첋^3}J:G2bFq@t:~)92zP@Wz7wS7O:y ѣGSy9W6Nc m& DСj9'Om߿wr+M%üyul<j3ts:_Av~J tR.4+:(79sZk$g:t:{ tHVO[<$2$YlРuߠЁ_~ꡇ Y%J˫cU*Dxz 7: ~N|Gٳg넒rjѢս@aT@:"OV)wK:}.v 7--$@(H~]Ot.\8駟f͚֕.{Fr:T 5vgy1$  ,Ž#Δ P۶mՃ>hIЁ{ze`P}JtP3tΟ?_{7 IDAT?@}_]@(ACXg%9@RWS@@:t(K@$txe],YKNMjݺukXw:DkP@hG@/B ACnB@#I:p+Bڪ{HHt !ڸ+AC#W@RWS@@::Dk3IBhOj:Dk:D1ru t(UA@: $ +@   AC>$ka?}EXn5kI $\CZ5|մiO*EOO?Uj.T_jV޽͔ t72ܷUY'*5yraKyn4q:Lԫ*/Rkk]D jPWTK:0rzE:HzmtWcK~RYMΛX' ,!\rq 27|>V۷!$Yޱ35RjZȦU)СC\hPNͯ駟Vᣏ>vm7J+E{@c>,ٳNR3GB) 6Jucd:R+uޜpE3}:d:xd(ƖI˕n~5>t<_~%1YG 6P]h=J~P꧟rszk"СViB}G͚5K3ƛ3zƔmɜj^YZyj* X7vmoLB . b)r4tûJV#GPژ5J^nҳv"t8:tW{s u:5cF,o1B-\ك^HV7(1er9 a"츣R+`n\o;UV.J2&&СJYK;W-ԫ0˯y8B+AJB -;L(<ҋeFB|)kmx)W߬A^P=1x 5P^_"v~XVSj~UoJmm4!p ȕ5T#Zlk1Om$ov (uPugyBSPt^p~ Oe1?S>kЁ\t_)&СbӡPAxp;GYUk+5ir2ln"Сz"!=0{lu)#< 2{YgKeGLCaW]uUjaÇg ZXס ,]!nZBMstLh7x;Ж5ʝ3NixR֠V-z#ACX%$6m U׃WOr fYZKYmF1A%񁗌 /Nmm`"Х}L!c5A;~Lx֠ÈJ}Ut`حT߾a5 tVssn~T:t9SqZ{m+\@%~:sUp饗za^ܲ-:] t0ˡK:8\y,A4QHf5Z@%@1$oe\mRڅ IxERYx8̟Qv5w N:t tWr{PVCxIͯ.t zw w qdžq] /_LC (Y>ܹ;J6mk*ACXt`#0:urq,Kx}'s1t@lAèQF#:tx꩜VI<2kP]C|<3['V+4t0aDYd'|ܝJZaP/tAa%Y)uv !q b.C١R-Z(9%3|95o^Λl@\9iB௻:CUZ*ӕ.^e=(GQÆ “$wqu"K/$Сk t־:*R;6kװZP8фDscj Le0gRGJIV(^b< Wup-$$&ohđۇkiB睡ַoZ_\H6pC/xZ7xCz^1/X pI> 5l^ƕ# K>R?{GЯOl .TlI)rzgmpW_}UF{G;1;2zQ?,%#bކZ#V"Ɔn5W^ 0$l9xƢy^|tx_%oz4hP_~Ds]֍NT8߽{1{f]ipԩcǪ.]x.WV{챇w,w.Z2ewrq>&M-kJ$>sDA28N;~^g7k,udז V,0@L1c!N03;x9fߎ;V bH6? 2Pmu\68񈹙(Uo=wq6b/ :@"1w`BJ'L;a1^ֹ-R6vmQ`)ٕawEC~PoV?3^7{;LA;Ԍ \7`RoفZguB'x7yɹsjxٽ{+M\=Cq) :P&QkW\1{ʠLqFMzv}JxY ;! xsL q⢁i 믿Q$ 3ΚT'ǠmWub1w_>%}jTO~;sSs&,&dOR!PܥPOr¸K˻+<5 fD_D˕i)dΗdQ'Db*']5v#KK+vXHuN@:Rs3#@mIO)&d^f"qՀj$.D0̄21>0ralj>~e@XdPTv@2=RjPdc } U/ER&x(үއm>68㏻.!<=JJ!  wW6C¼}*\C\67j:7-gr( ( ( ( d_RBQ@DQ@DQ@DQ" tPQ@DQ@DQ@DQ t7( ( ( Q@CEd( ( ( W@CPDQ@DQ@DQ@*"pBk; 4N@:u͛-Rc7oX2jo:묣:w5j(3fP֖ !׋֭[ 6 _}6mֶAO뮻tMc6_kmXGN5nXmƱS/4Ir[# zޱcG]OٳլY;;vYnٲԩS9t1uT-ŋcdžQiM4QoyWԤIԗ_~7\oVm3￯>3dɒs8M7s}U-b=O?UE:˜.UvK2on3l.b ?^es]tfږtx7q; * DLlL iϓO>6l3@ܽ2,z{=VZEzW^yEk _L J="}Μ9I`⺱2m"ƎzkFB8qfm465O?WIz^C^ Yht9?`ivtk,6df\R~'=?0a}#-{9݇0B\:oݻ g}V/9YC(<?TxF]h3aQ&YC֞?SxȘ{W5~$yer(EC̋np>d@N@]벥 vIOdŒT{Wf;p`@ebƍS͚50 0bݻĚh#~gT<@}3"3hi%ɚ IDAT d!6iI.PĨ,7pC˝ɢ<|GzO>ΕTA&/>zgTD6>0Q@э- 0Ā,tY4<'z-Ifb9l+& h[mUj^t`77ĬNZRS]n]>ٝcQF״iL+ʸZks̙zg!ꎄkMe`/ð3튅IA.I6@.o- }! hiy 脙&& DU`隨 Xjl܁vX@YTgb1clA׮'G:h',2qi]> @Hg'~!X~*B]L8.'t OI&MXו3CjuRZP"D6 K2MX|fQ6bI9#זa%(/9G3IlѺbC 6A@WZ;r) С\Jsʮ@ЁyX`N@I--0o<} ax(<7o :-ЇYfͪuaډ#wMy0*ݎ ūE*,F/…#H8Ao $$ : t+;CPYQ҄ f,,oF:.0>@ZЁ{sW^3-]}I:$eK:Do [ 1jt  6m,;a V+8,972=xR!~X ТE ]wFO)ʐ6tQȁa{. tqwJCi*B4?Afs}ZQ^"M/ H.XЁSة.% ~wnD,zݦ1c8[vV@tXc5 ͲmbAo!7&t`z=&" (3!jtw^Pf:Y8n_ c >u66) ji@;p?۶mk,iB ȑ# ems9:>QY~2b}ǑGOU,m萯8Ef'LHwUW:?^g:R@CRJ}S 0i$8]\g능 1k>|ޕb^zV˓t?1m߽e7$mŵ\С*! e]f]I:|jРA\A־}Zxb~D 6qD`Y;M6ЩS' ^ĵ9Lv?}5~diB/\=CG5ۤ"/"RV[qEފ t[iӦ2pHtb_WA $pd6w( x6m,?7 yG.;rCߋL$ͳ҆,ꅟ`6z:Ùg$Qnf:PHƫ{N'1c.7t' ի#ٴC~bIS<lJ:2x^! tpK{@R[@aɒ%o2@A[[^,i@&uUd-.]]m?I:Pf&8,ٵ۝ti@#FK/TH{Nj:"Y99@E''9p*Ђ q[o ]ǟĒI"{"ĒXcKVQJQ]Ъ}',vb=AbIwsϽ~ɇ̙Μ93yM IP |G3D zjS &~6ǏYi Yt($K:_"Ȕ+B EڢÛoQje|L JSLjcǎ sO4J@th@t @CL?t[KStp"N;d'xbɧvmzu蠫:ЯEuD"S A 2/Nzx"Bz?pAٿXSo"E\|- eBQu$KbƎ1cƄ7!(2Hf"!:ȊA▄YEAWdJhбM!TSth+#C%K C -oGFpX"3^:\Nx~<@OZ&-A?ϰÆ M pҎv5!::f#O:u we#zf͚ZOITW+Z/1SxY:{7Utg"C%@):C=/PDFi)Y)DJ n %:Hd"Z%뮡z iv}p(Mڅ+tv4mA,fΜiFSfL]t ;\w&QVKt̉8W%4[c V1 :L>vi'[wu.5]+G=\c=ꫯ}mˬ/8|ɶ2˄W\$!j&k2=lujDDr=' nufޓ(C6E{1{\ݖX,0ٳsqhgAt+va~9X !=X Z}I-: Q)Ȝ wq#}!!-B8 {l',Hm!N CxcuI`Ж]#_3o^qaa$cs{C~ޢLg뭷>6}t骫5w=:+\,Yd~͘14r!H| 9A/+"xL$4(Lkݸӹk/] " : :40fLNaVpOt|SH5jtkw-bU2k{vBtռ"z2>ێ@tH)9 F` }fӦYA55+cf+hڹk,At@th&ATK5CD/|: )j':nنnhs{#9FD\ϴ9MJu  :4A#7kUt͐=θqBOdK]"b 5uÚJk6ئnP䬤CDn~ :Nt$}B?fݒp"%d*:TCkBYT;Y:!zGtȖfݖۡu\xO]6ӡ5#Dډ95pVһwΚB D E@thn":nsDDQ!|:LX}GtȖ Gr.*@on6p`Md6vMf?Qѡ\OO?0kUV I* :Ԋ4̋^{a[isϵ/8 mGb : :Ľ$thM!üyK![jf*lms#) BNlr~\B5Eɓ'lwLrK%ПRn9IǏO>9,_\]l߂#9.5~j7t̃:(2VD;/l L Ng}vxZȼ?ǥjѵK}ѡ5DuQa$G-8;j#7ZÑGXpe-pLF XD;ٕ$iсvonѡК CkY_6q/-'ڲʂZbnru gBݢ):r-ޖ\r nСO X+mUCt,S|ĈwGpl-ETF{.) >Dc@t@t늈HVE]sMݑ#ޒ+:1):tIv7/DŽ +&|b뮹#oY!~5D~؎8h;ꡚ^e{:C/PJYq,!-D@ :Ԏ5O1DD.ЌԜ9fgzܛѡ-X:XFeκAGL?|'К[5DSN9%82vy6|d Վ>)Da$Ut{7o%:L9N_kb/=@e :T_tH1DvϣkJEy]?kˣk5*)ew}%86`Fsg͚^g¼~{|9newᄚt:ڵT{,4\Qv%3<W؀kd o̙3CRw1#(ÌsLu5]W_v+ A 6(4}.z?2dO?XYNJ*kk|>R*hxCҬ/PP@*nStj={DqW]Wd v?qZBAN4N.1O3 /`/81ciuQ eEU/߫gDj#߱VX=4D9g[l+b~zeeo`r}1ҥ|g%C׋iX*|>܋YW1{[. <$9᪫w߽l5'^;%8{ޗrSoqq&묳NɺΟ??\:#(o 7z`5/e}E-ۃmBX0Ȥ\.ǂ"" nVCH&Y70aB(kB.nѻ4tK(A5^#PeM9}ܷC'|26:ud,۴2t9\ʟ]e)'%P蠇j ֵe JZ52j2Esɴ^zDo8{oh*#'Μv'j krpY{VwPQ;q"Vý,Ae%]hUqH:n]gxZ;H/y#{.*e%.8e﴾w)$ZGH\'7EV-cjNYꛫ#Æ sh|'!F>!hHyh]!_?q[ncU,:([唉;49J7cZ2_d3"C }1ga{xhV/vzUR2 B5ijN ' ӦM *tT14$F%d"AcǎU Glw~P"j 3UpP^.M5~O9ULj :rg"^0MҢaV ϊ ^# $ʖsXrZ,s{bRu*VxHpټ8%|d\s;}{}W\*d hC<|&G\7NPTm jT^]}wǛT# 4nf^Rbnջk9s4wXW3":K|Fey&@ "W"n#LhШAZM4[kb͡KSξ$#cd6bxș/ZHiy%' ;ud.\sMߓϗ^z)d7!i @8[:wl={&]-\'~=K&d>Ӵ":6n8۷uqh׬YO> y 6 qv[ֽ{wСWYĭv{#BK,2ՂÜ9s¾ɘ2tO> 0Zj)[|} K`̙6{l{7lĈ|cG-c.I IOS[E)S C>i+ Ýo-(O?C=d|󍭲* -A`ĉKx_%:q @fΘ1cl_։}3g/1[cU$:oVtf%IX6x4k}ȑ^x>}zEfH~ؾGYN9Հ F k{Iޢ>n\q߇{\=ELJ&,HZFK'Y=1w}מx 4hPU]SOV6:R*Ⱥk 6Ydv)g#=T ;v y =-ֿFe ;O1]ݙ3wg-[nmcUd Zpl ی3:2צq3wgxfB/+CR^cЩ_ :3utgO FO,?UD?Znq :~xfquAtJl@HCz,\3 %8_-KA'$ :3utgO FO,?U,hf'g~\DCtH !}h19:5o@@tH+~3_/ז؈I%HǠCjΔB@\~rB/+CR^cЩ_ :3uttH=9BD7N>\>T9^G-67N>\{r 7n|b~|":rߍO,'OD*LP!"tg: :Ϟ!@'XD~b3w!) !:T!}Erttj<@tH[~3_/זHJ.A:b0uAtH=9BD7N>\>Tt'XOqK!4D: :Լix  |K!GtcߏKlDJ~q\\[b#:$% NhCCt9S@@tHrs]t :g%6 %8_IyUA"|E3u!}@n8r#:Px-8r}}̏CRr !:$A'}C :qGtG-67N>\<Ҫ0C$gI렃>{r 7n|b~|":rߍO,'OD*LP!@DuAtIs@(B!nGtcߏKlDJ~q\\[bs")t@I >SA!}@n8r#:P[ln|b>yEtHJ.A:D҇렃P -D=?^.\(q}rm萔\t : !: ѡLy  K!]uсǞ/؈.⸾~":$UQ:M̠>SAKٓ# @tpC~b3w3?.I%HZL:'G@p#'*-8r}}DtHJt,A'}C :qGtG-67N>\<Ҫ0CX$GAѡ&C @D=?^.\(q}rmcI-& NL]Dٓ# @tpCK?Znq!)@CtHZLC͛B D?i{13g_!bw[o!!C'}2CRr ҥ=$(B0ߤC :qGt𡚾s:u_!bjvg$N_ '#:$% Ch1Itg: :Ϟ!@'XDm~ȋݿ;co?K!4D: :Լix  |K!GtcߏKlDJ~q\\[b#:$% NhCCt9S@@tHrs]t :g%6 %8_u":v7駟Z׮]M猶n;:thXJ=6rHѣS=5r-v;ůUj:fͲk`fֹ̙sgݻ暶{B -T9tO~bwo-wqo>+뮻֌GrttH6y@@mJDQo'm%UVY%篽IBݧT3f{o,ꧣF2SQ{K.6|pV1?iAbE]d-zeg϶~ƏoG?β|;NlF>?<ovaܹsMGy$dݩS''~xL44@(l_ .;F C#*e -':}v96,lf?n>!f?裏7}h3\~|6vpSQ`m{Ƹ\I}/ /lymv_~e[tECGt/l+,z!_:_;tՍW%Zj䥗^K.$XDr Dj%:OOt8C_Wرc5'\dEꮃk#Fe$-:L<͛Hey a\sMixAN;Ԧ +:D %dsٿo[|K>UJҟ6_pڴ{؁hӦM (/ f;^qwy+rIbӟ~ڐ!C{'줓N VX!wz+>ы: d믷)S p-Ra(b%ڃ%?qXk£?꿺h뭷nUϷo,]xIZtX: 5 j!:/8Iox:-EW_J?i*'wmǵ5~*6n0#di0ܰOsdyӧOWIH[tLjWWkvZ٦/[m/ش),Kt Z#]k D7*ɠ_YV_'qo[x~Ӥ*fm?7uS&:{G؀B o1eWtБwvqGOc.s̱>{  R4XiH@-P޳ѫ9Dy.9"p CKSN9 V(K/tj4 CMq0@H@tESO=5 4Owm C?qꫯs_? ga&LѣGG94BPZ 0cРAxwnpQG>1 ?k9Ts/K_סp.$zCC j3U Jڈ]qMr-Zis>h+iF)oh wUW o}WXc G" nh)!9 =jHr4 rzaVnѡX{v29Dॳ[:R!!$׿ZY|p$- @@-D#_9<똪6%1"+:nBxpga)֢P+yv"ˉ=3 ii_ۧ~z#Ř Aڤt~%t~~V%oVAd F&f=AʲZlQ- G"QvWsу`ʲA*$(DV%l*iIB T[t_-sS=39䟝,[Q裏]jYť^n^YJ`홮])r_Tߒ +tkA#> .[neo١](k9sYr2+ %PH %'/5nſu~9s駟WDWiFN|4sx뜛m[ og(oENy睡\F7|ᇡ.(:HY9,aYD1 &)Pt΄i`ũr-xDZP P j|#:h>/?crry|9<6v6 mj)h,].tݥLյ)Y(:h^{5(9it+mjBG$]G{кAk (CN{99T[t&K2ĆnjWU\{TD3%\2Yve\΢/g7tS_-XՋnSB-5h5ѡejZeClXeUDxpqS=X E>!Bn8H޽{P%]7H&:'k 52+&NzB!K>h^xaݏuD@B I-$hK€6$BQFo}@۴) Emi=yDM74ܰnns47{7o ZP6gX Z#ILJQOFr:+"Z*Jҡuj7K镴TK$Hf.Z :iCt| P-ѡɺ~iHv9ivݫɺI萔\t4 :$h$qttfpg"# 9DǾ~5k؈w}>!)iAtHI: :d $~~7߯f!}'#:$% Nh1Itg: :Ϟ!@'Xz|֞qOL]OFtHJ.A:҇렃P -D=?^.\(q}rm] Oku%*C< 򼫛1r6VERLbZ7M3f Ç/[gGxСMæN8vqVw;O=zXϞ=+y\SnU4/oEIRLI̙3M&@{-:u >3";w=W_/RNs5xD ڵlMdE2p@>"C@Йqꫯn}q4JsejzEd)r%"":(?-cǎ֭[po2=&lhۓO>iqx,@^E9HY8E@%k3 kF'Iл/ڧj myc/8=%?`_y,́Lz}J=t١f;w.{K-tf:H9^}iك&q]vM$oD!OL#2O)L~]JP@ @ }ocj@ @h킝B @O!mL !@ @Bѡ]P@ @@ :d! @ @] : v @ @>D15 @  DvC!@ @'6 @ v!.y( @ @  @ .; @ dCۘB @څC` @ @t~SC@ @@@th< @ }ocj@ @h킝B @O!mL !@ @Bѡ]P@ @@ :d! @ @] : v @ @>D15 @ >8&O~͛;[QvlloaCtH'A @@#;mVZҺtԈUZfئLyFޮ#: @ @ 3޺uf}nZSۜ9s F2 @ F%poٔm&cg*͉ F2 @ F%pڹQz/F/H%_DT0  @ 4*D-Ш=rC @@p:t͚=Q. 1is6b6pYǎߦMfm<2V]u߮:;眿믿f:u ]v{[i7N톞C @@=jsTlrvnxصйsg ~~Q=-7g}fO?}v!":dcQ@ @pۊ+ D>•W^Ft8C{ -Zh6'{go+ K# @ @ CDykns-2:kڸqwB {FƎ?c>L~S`gBqHp\{g͚5~] P* @ оD뮻&\yaA2d};[/]}vEGh3fhaO1cFml[,C믿i aԩSW@th߮!@ @ Dv5p'? QtӍ׿cۈ-m_vۍlHR xu;vl`-h ة)+MA2ʆ n\r}ᆛmFtz~ @ оʉӧO[',`]wЂtٳYgnaVWTVzHy[vީzر {챧2|Q*p}#O @ (':gX6ݪƧЩ)FtmMG'v?0ax5jaʩ*W^aaa:Q. :dsS@ @h_D6$XX#Qcgy&O~)8~E]fw͟?pi9sؖ[ne-S? jtر*s3F!6C,""@ @zI~mlǬZe6gl[cvyX߾+KtM_[ :Å^`O>}Q7cMG04}:վm,DH@ @( Hـ!:4[S_@ @Dh @Cy @ @5"pvma]t1ͷc\ t52 @ @Y#0f[b%lzgj:u͝;6lwb RH& @ nlelJY8|L{ۦ֔$#@ @hdcLɓ_ v5r5k.6pʩY8D@tH$ @ @ C!~ @ @ DDH@ @@D8B@ @@" @ 8q @ Da# @ @q; @ @ :$F"@ @ :w@ @@tHD @ @t# @ $" @ @#G!@ @HD!6A @ G!C @ C"l$ @ @ C!~ @ @ DDH@ @@D8B@N|i͞]'Ǐ7z-[aJ[v~ylM{lJpf'{[No} /,@u_e+,-;Z.ZK,avfk85gGevefk{u7Ǹw/% !8mEI!&'p,0s z 6dj4?vຸwY#:ċfnDFlm̞zl Wă !: @"[ѡ-G Kt_\.qJ@to@hv/6"};fEяZ*\2y1=rqEf6ug6&wY~U":}ru,Z:|Ojvf_of/f\;:0dٙg?RI6 g/Q83;a; ʩObv饹U[rDVk{\x晖wbtu1b k=ҡ~\z#˗WO̽/zV\l~\L/dST;4#eE^87yWoC55*Ђ^csq#A -3-:w-E[R;Z쾻Z| f暹ŗ:Vrf>[rnѫ3{R2-\~cɓV]-W_ݬkBL|_=OxìoߖC ;{ǎfܓ[0mn6gNnfO?ݲ0u>0? " j2.2eq|3?Z+$\w]n!9!JWSKrISS9QM=G .}&i$Ph7/N۵j,!des$m$ AO"7ܐ||SH9QEBRp$Jr]fU>D5=Og0v$SDgC1 Fx  ~^N!@@c@th@(\ꨀm޽ZygQЄ^Zk74ZXrZTTp]E HD˲A}tGA/[xhAؗ u"SV 1Cz9 DJAG+QC.ҹZ29b#QABvӿ) ADZ*ԃ NWPCaߒ"1HJ|D3]lƌ\, g/꿲lD/RFuG|Rόwq%:f浶ev-*GCJAi%BDV&3ZuDqFm$nQXc6⦬iT? 46Dn?J4b&:o r k1]ȢAZx-%R\qEˢЬ[Zk9E[ym5οzOȿBP %#@A/3m O~b&F;mP(:(ґ l\G7tCi QeY!aGdREΤkg""?h3AtxM3-Շ$¾%7i#:(N8g~3B]Y+:Hxqo!?Ht-AǁCg]DK5;9$[o%hDG]J391ј-K[_ @ :{ Q>@(w|0hw94.{ȩ[)dJEѮC,K)$DᤓrT,$ǖ E1m"?GZtP.ɫ6IDATkU%(Y:HDq jv>^'Wîd~([9ږZȊ|u/hB$:3_yoD#rBR1!:^Qwd Ǐ_K/k GM]"1?2HN'#G߹Y%tgZ  EYlUd@9!]Z0jQ,ZoF!ڙ^|1EqZP.|tڙ~}+OqעIg%0<[Q(d "K9|Kv9'CV 2y| w=WG2$tIttEHeGmb:_tLv.բ ߱D ZߒPط~8P\n2)ktBwE|%h'[tt(gj6?MjY^>di$k(|:R) OYStP$_齊Tu+b"'#$Q(|"ID7xC%@PA"1 KvPO> ",non -d n 2ŃvkuX@tߟԺѵU@$C]eYhW]<$ɦNʥٴik0]9b,pYAy8b: ]k* ό|!S!a@vh.\W8jqI n" -nע+nKN-%.ɏgh$:2  o(g"AK}]Ew!+$Zovș]fOQp{?BFQn@_bD$/TQT(:ҪJt9Y7r)QRG@tCϐP!SNZZc%IPSFqѡ\  l@tF;R @ ha̺E;Z4Kxu:vpY9Z@aF7Dr@ɲ5+ BN xCT71𠅓vC1GZj%ǓS,dpmEE8FYCɁ;z$b"kkġ(HSv/3.L@MѡۏCMD@;}EC=E[ʻ9qBL OVLH \EZ^UC{Z σ D3!xv Cq <(x .jW7Ȝ[;ڥM}U94wmR̕eq_2kq/A %иmG!&! sX;K.CC<<=LpqZXudᕘT;P2˿^UE{h @h/EBmӐr5(9U]S z'  tpIENDB`