6 Commits

Author SHA1 Message Date
felixfoertsch 859c26f64c add build workflow, remove redundant validate workflow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:07:50 +01:00
felixfoertsch 5e29ef63d4 rewrite README: fix typos, add versions table, validation docs
- fix "verison" → "version", "pyhiscal" → "physical", "distiction" → "distinction"
- add versions table explaining each layout version
- add installation from DMG section
- add validation section with usage examples
- restructure changelog with clearer per-version descriptions
- fix dead key table formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:06:48 +01:00
felixfoertsch 079ff0a872 add build, DMG creation scripts, CI/CD workflows
- build-bundle.sh: regenerates Info.plist with KLInfo entries for all 4
  layout versions, sets CalVer bundle version, validates plists
- create-dmg.sh: packages bundle into a DMG with drag-and-drop symlink
  to /Library/Keyboard Layouts/
- release.yml: GitHub Actions workflow that validates, builds, creates DMG,
  publishes GitHub Release on CalVer tag push
- validate.yml: CI validation on push/PR for layout and script changes
- Info.plist now declares all 4 layouts (was only v2.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:06:48 +01:00
felixfoertsch 7084817dab add validation infrastructure, fix "6" key bug in v1.3
- add keylayout XML parser (parse_keylayout.py) that extracts all key
  mappings, dead key compositions, modifier layers from .keylayout files
- add validation script (validate_layouts.py) that compares layouts against
  v1.3 reference with per-version exception support
- fix action id="6" outputting "p" instead of "6" in v1.3
- generate reference JSON for all 4 layout versions
- document known intentional differences: v1.2 (no ¬ dead key, § vs ẞ),
  v1.4 (ẞ on §/` caps, extra ¬ composition)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:06:48 +01:00
felixfoertsch e16b92051d add v1.3 to the bundle, implements official spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:06:48 +01:00
felixfoertsch b5e2de535e create bundle, add v1.4, v2.0 to the bundle
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:06:48 +01:00
56 changed files with 17735 additions and 1781 deletions
+36
View File
@@ -0,0 +1,36 @@
name: Build
on:
push:
branches: [main]
paths:
- 'EurKey-macOS.bundle/**'
- 'scripts/**'
- 'spec/**'
pull_request:
paths:
- 'EurKey-macOS.bundle/**'
- 'scripts/**'
- 'spec/**'
jobs:
build:
runs-on: macos-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: validate layouts
run: python3 scripts/validate_layouts.py
- name: build bundle
run: bash scripts/build-bundle.sh
- name: create DMG
run: bash scripts/create-dmg.sh
- name: upload DMG
uses: actions/upload-artifact@v4
with:
name: EurKEY-macOS-DMG
path: build/*.dmg
+82
View File
@@ -0,0 +1,82 @@
# Sample workflow for building and deploying a Hugo site to GitHub Pages
name: Deploy Hugo site to GitHub Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["master"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
# Default to bash
defaults:
run:
shell: bash
jobs:
# Build job
build:
runs-on: ubuntu-latest
env:
HUGO_VERSION: 0.120.4
steps:
- name: Install Hugo CLI
run: |
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
&& sudo dpkg -i ${{ runner.temp }}/hugo.deb
- name: Install Dart Sass
run: sudo snap install dart-sass
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Pages
id: pages
uses: actions/configure-pages@v4
- name: Install Node.js dependencies
run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
- name: Build with Hugo
env:
# For maximum backward compatibility with Hugo modules
HUGO_ENVIRONMENT: production
HUGO_ENV: production
run: |
hugo \
--source /home/runner/work/EurKEY-macOS/EurKEY-macOS/eurkey-macos.eu/ \
--minify \
--baseURL "${{ steps.pages.outputs.base_url }}" # Adjust baseURL according to your directory structure
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: eurkey-macos.eu/public
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+51
View File
@@ -0,0 +1,51 @@
name: Release
on:
push:
tags:
- '[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9]*'
permissions:
contents: write
jobs:
release:
runs-on: macos-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
- name: validate layouts
run: python3 scripts/validate_layouts.py
- name: build bundle
run: bash scripts/build-bundle.sh --version "${{ steps.version.outputs.version }}"
- name: create DMG
run: bash scripts/create-dmg.sh --version "${{ steps.version.outputs.version }}"
- name: create GitHub release
uses: softprops/action-gh-release@v2
with:
name: EurKEY-macOS ${{ steps.version.outputs.version }}
files: build/EurKEY-macOS-${{ steps.version.outputs.version }}.dmg
body: |
## Installation
1. Download `EurKEY-macOS-${{ steps.version.outputs.version }}.dmg`
2. Open the DMG
3. Drag `EurKey-macOS.bundle` to the `Install Here (Keyboard Layouts)` folder
4. Log out and back in (or restart)
5. System Settings → Keyboard → Input Sources → Add → Select EurKEY
## Included layouts
- **EurKEY v1.2** — legacy version
- **EurKEY v1.3** — official spec implementation
- **EurKEY v1.4** — v1.3 with ẞ on Caps+§
- **EurKEY v2.0** — custom edition
generate_release_notes: true
+31
View File
@@ -0,0 +1,31 @@
**/public/
__pycache__/
*.pyc
build/
*.dmg
# macOS
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# IDEs
.idea/
.vscode/
*.iws
*.iml
*.ipr
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

-1767
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>de.felixfoertsch.keyboardlayout.EurKEY-macOS</string>
<key>CFBundleName</key>
<string>EurKEY-macOS</string>
<key>CFBundleVersion</key>
<string>2026.03.03</string>
<key>KLInfo_EurKEY v1.2</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISIconIsTemplate</key>
<true/>
<key>TISInputSourceID</key>
<string>de.felixfoertsch.keyboardlayout.EurKEY-macOS.eurkeyv1.2</string>
<key>TISIntendedLanguage</key>
<string>en</string>
</dict>
<key>KLInfo_EurKEY v1.3</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISIconIsTemplate</key>
<true/>
<key>TISInputSourceID</key>
<string>de.felixfoertsch.keyboardlayout.EurKEY-macOS.eurkeyv1.3</string>
<key>TISIntendedLanguage</key>
<string>en</string>
</dict>
<key>KLInfo_EurKEY v1.4</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISIconIsTemplate</key>
<true/>
<key>TISInputSourceID</key>
<string>de.felixfoertsch.keyboardlayout.EurKEY-macOS.eurkeyv1.4</string>
<key>TISIntendedLanguage</key>
<string>en</string>
</dict>
<key>KLInfo_EurKEY v2.0</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISIconIsTemplate</key>
<true/>
<key>TISInputSourceID</key>
<string>de.felixfoertsch.keyboardlayout.EurKEY-macOS.eurkeyv2.0</string>
<key>TISIntendedLanguage</key>
<string>en</string>
</dict>
</dict>
</plist>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildVersion</key>
<string>2026.03.03</string>
<key>ProjectName</key>
<string>EurKEY-macOS</string>
<key>SourceVersion</key>
<string>2026.03.03</string>
</dict>
</plist>
+109 -14
View File
@@ -1,23 +1,118 @@
EurKEY-Mac
==========
# EurKEY-macOS
The Keyboard Layout for Europeans, Coders and Translators - Version for Mac OS X.
The keyboard layout for Europeans, coders, and translators. This repo contains a **modified version** of the EurKEY base layout for macOS, bundling multiple versions so users can pick what they need.
This is a port of the [EurKEY Keyboard layout](http://eurkey.steffen.bruentjen.eu/), which features a QWERTY baseline layout (=good access to braces etc.) with quick access to commonly used accented characters and Umlauts.
EurKEY-macOS targets MacBooks with the physical English International keyboard (ISO) instead of the ANSI layout from the official upstream. Since it is an ISO layout, it has one additional key (`` ` ``) and the big Enter key.
**Status**: The whole layout should be mapped now. Please report if you find any missing characters.
The keyboard layout should be compatible with the other ISO layouts typically available in Europe (e.g., German ISO). However, the printed keys will obviously be different. I tested the layout on the current tenkeyless MacBook keyboard (MacBook Air 2024). Working numpad keys are therefore not guaranteed.
## Versions
Install
=======
The bundle ships 4 layout versions:
Copy the two files `EurKEY.keylayout` and `EurKEY.icns` to your library, either system-wide (`/Library/Keyboard Layouts`) or for your local user (`~/Library/Keyboard Layouts`).
| Version | Description |
| ------- | ----------- |
| **v1.3** | Official EurKEY spec implementation. **Recommended for most users.** |
| **v1.2** | Legacy version based on [Leonardo Schenkel's port](https://github.com/lbschenkel/EurKEY-Mac). Predates the v1.3 spec (no `¬` dead key, `§` instead of `` on Shift+Option+S). |
| **v1.4** | v1.3 with `` (capital sharp s) on Caps+`§` key. |
| **v2.0** | Custom edition — complete rework. Every key configures exactly as printed on the MacBook keyboard. Removes left/right modifier key distinction. New monochrome template icon. |
## Installation
License
=======
### From DMG (recommended)
The Layout itself is licensed under [GPLv3](http://www.gnu.org/licenses/gpl-3.0.html).
The EU flag icon is taken from [Iconspedia](http://www.iconspedia.com/pack/european-flags-1631/),
created by [Alpak](http://alpak.deviantart.com/) and
licensed under [CC](http://creativecommons.org/licenses/by-nc-nd/3.0)
1. Download the latest `EurKEY-macOS-YYYY.MM.DD.dmg` from [Releases](https://github.com/felixfoertsch/EurKEY-macOS/releases).
2. Open the DMG.
3. Drag `EurKey-macOS.bundle` to the `Install Here (Keyboard Layouts)` folder.
4. Log out and back in (or restart).
5. System Settings → Keyboard → Input Sources → click `+` → select the EurKEY version you want.
### Manual
1. Download or clone this repo.
2. Copy `EurKey-macOS.bundle` to `/Library/Keyboard Layouts/` (system-wide) or `~/Library/Keyboard Layouts/` (user-only).
3. Log out and back in.
4. System Settings → Keyboard → Input Sources → click `+` → select EurKEY.
<img src="eurkey-macos.eu/static/img/1-input-sources.png" width="300" alt="System preferences showing the edit button for input sources.">
<img src="eurkey-macos.eu/static/img/2-add-layout.png" width="300" alt="Dialogue to add a new input source.">
<img src="eurkey-macos.eu/static/img/3-select-eurkey.png" width="300" alt="EurKEY in the input sources list.">
<img src="eurkey-macos.eu/static/img/4-select-input-method.png" width="300" alt="Selecting EurKEY from the menu bar dropdown.">
## Validation
The project includes automated validation to catch regressions. The validation script parses each `.keylayout` XML file and compares key mappings and dead key compositions against the v1.3 reference.
```bash
# validate all layouts
python3 scripts/validate_layouts.py
# parse a single layout to JSON
python3 scripts/parse_keylayout.py "EurKey-macOS.bundle/Contents/Resources/EurKEY v1.3.keylayout" --summary
# build the bundle (validates + generates Info.plist)
bash scripts/build-bundle.sh
# create a DMG installer
bash scripts/create-dmg.sh
```
## Dead key compositions (v2.0)
v2.0 renames all dead key states to their initializing key combination:
| Key combination | Dead key symbol |
| --------------- | --------------- |
| ⌥`` ` `` | `` ` `` |
| ⌥⇧`` ` `` | ~ |
| ⌥' | ´ |
| ⌥⇧' | ¨ |
| ⌥6 | ^ |
| ⌥⇧6 | ˇ |
| ⌥7 | ˚ |
| ⌥⇧7 | ¯ |
| ⌥m | Ω |
| ⌥⇧m | √ |
| ⌥\ | ¬ |
## Notes on Ukelele and template icons
Template icons switch color with the system theme (dark/light). Ukelele's GUI checkbox for template icons does not save correctly — the `TISIconIsTemplate` flag must be set manually in `Info.plist`:
```xml
<key>TISIconIsTemplate</key>
<true/>
```
The build script (`scripts/build-bundle.sh`) generates `Info.plist` with this flag set correctly for all layout versions.
## Changelog
### v2.0 (WIP)
- Configures every key exactly as it is printed on the MacBook keyboard (ISO, English International).
- Removes distinction between left/right modifier keys.
- Uses the `*.bundle` format to group the layout versions.
- Adds new monochrome macOS template icon that switches color with the system theme.
- Renames all dead key states to their initializing key combination for easier identification.
### v1.4
- Adds `` (capital sharp s) on Caps+`§` key.
### v1.3
- Implements the layout according to [spec](https://eurkey.steffen.bruentjen.eu/changelog.html). Based on [Leonardo Schenkel's version 1.2](https://github.com/lbschenkel/EurKEY-Mac).
### v1.2
- Original macOS port by [Leonardo Brondani Schenkel](https://github.com/lbschenkel/EurKEY-Mac).
## Attribution
The original EurKEY layout is by [Steffen Brüntjen](https://eurkey.steffen.bruentjen.eu/start.html). The macOS port is originally based on the work of [Leonardo Brondani Schenkel](https://github.com/lbschenkel/EurKEY-Mac).
## License
- The EurKEY Layout is licensed under [GPLv3](http://www.gnu.org/licenses/gpl-3.0.html). See: [eurkey.steffen.bruentjen.eu/license.html](https://eurkey.steffen.bruentjen.eu/license.html).
- The EU flag icon is from [Iconspedia](http://www.iconspedia.com/pack/european-flags-1631/), created by [Alpak](http://alpak.deviantart.com/) and licensed under [CC BY-NC-ND 3.0](http://creativecommons.org/licenses/by-nc-nd/3.0).
View File
+5
View File
@@ -0,0 +1,5 @@
+++
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
date = {{ .Date }}
draft = true
+++
+58
View File
@@ -0,0 +1,58 @@
# EurKEY-macOS
The Keyboard Layout for Europeans, Coders and Translators. This repo is a fork and contains a **modified verison** of the EurKEY base layout.
I start versioning my customized edition from 2, since the layout is based on my modified EurKEY v1.4 that I have been using the last few years. The version with slight fixes is now available is called v1.5.
EurKEY-macOS is a rework targeted at MacBooks with the with pyhiscal English International keyboard (ISO). Since it is an ISO layout, it has one additional key (`) and the big Enter key.
The keyboard layout should be compatible with the other ISO layouts typically available in Europe (e.g. German ISO). I tested the layout on the current tenkeyless MacBook keyboard (MacBook Air 2024). Working numpad keys are therefore not guaranteed.
## Installation
- Download the `EurKEY.bundle` file.
- Copy the `EurKEY.bundle` file to `/Library/Keyboard Layouts/` (for global installation) or `~/Library/Keyboard Layouts/` (for user installation).
- Open System Settings > Keyboard > Input Sources <br><img src="eurkey-macos.eu/static/img/1-input-sources.png" width="300">
- Click the `+` button <br><img src="eurkey-macos.eu/static/img/2-add-layout.png" width="300">
- Add `EurKEY` from the list of available input sources <br><img src="eurkey-macos.eu/static/img/3-select-eurkey.png" width="300">
- Select `EurKEY` as the input method <br><img src="eurkey-macos.eu/static/img/4-select-input-method.png" width="300">
## Changelog
### v2.0 (WIP)
| Key Combinations | Dead Key Symbol |
| ---------------- | --------------- |
| ⌥` | ` |
| ⌥⇧` | ~ |
| ⌥' | ´ |
| ⌥⇧' | ¨ |
| ⌥6 | ^ |
| ⌥⇧6 | ˇ |
| ⌥7 | ˚ |
| ⌥⇧7 | ¯ |
| ⌥m | Ω |
| ⌥⇧m | √ |
| ⌥\ | ¬ |
### v1.5
- Configures every key exactly as it is printed on the keyboard (English - International).
- Fixes §-Key.
- Fixes German ẞ-Character ("Großes scharfes S"). Now correctly available via ⌥⇧s.
- Removes distiction between left/right modifier keys.
- Uses the `*.bundle` format to group the layout versions.
- Adds new nicer flag icon from upstream.
### v1.4
- Switches behaviour of superscript and subscript numbers: The subscript numbers are the default; the superscript numbers are available via `⌥⇧<number>`.
## Attribution
You can find the original EurKEY layout on [Steffen Brüntjens Website](https://eurkey.steffen.bruentjen.eu/start.html). My modified versions are originally based on the work of [Leonardo Brondani Schenkel](https://github.com/lbschenkel/EurKEY-Mac).
## License
- The EurKEY Layout is licensed under [GPLv3](http://www.gnu.org/licenses/gpl-3.0.html). See: [https://eurkey.steffen.bruentjen.eu/license.html](https://eurkey.steffen.bruentjen.eu/license.html).
- The EU flag icon is taken from [Iconspedia](http://www.iconspedia.com/pack/european-flags-1631/), created by [Alpak](http://alpak.deviantart.com/) and licensed under [CC](http://creativecommons.org/licenses/by-nc-nd/3.0).
+4
View File
@@ -0,0 +1,4 @@
baseURL = 'https://eurkey-macos.eu'
languageCode = 'en'
title = 'EurKEY-macOS'
theme = 'blank'
Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2016 Vimux
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.
+31
View File
@@ -0,0 +1,31 @@
# Blank
Blank — starter [Hugo](https://gohugo.io/) theme for developers. Use it to make your own theme.
**[Demo](https://blank-demo.netlify.app/)**
![Blank theme screenshot](https://github.com/Vimux/blank/blob/master/images/splash.png)
## Installation
In your Hugo site `themes` directory, run:
```
git clone https://github.com/vimux/blank
```
Next, open `config.toml` in the base of the Hugo site and ensure the theme option is set to `blank`.
```
theme = "blank"
```
For more information read the official [quick start guide](https://gohugo.io/getting-started/quick-start/) of Hugo.
## Contributing
Have you found a bug or got an idea for a new feature? Feel free to use the [issue tracker](https://github.com/Vimux/blank/issues) to let me know. Or make directly a [pull request](https://github.com/Vimux/blank/pulls).
## License
This theme is released under the [MIT license](https://github.com/Vimux/blank/blob/master/LICENSE).
@@ -0,0 +1,4 @@
+++
title = "{{ replace .Name "-" " " | title }}"
date = {{ .Date }}
+++
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode | default "en-us" }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .Title }}</title>
{{ with .Site.Params.description }}<meta name="description" content="{{ . }}">{{ end }}
{{ with .Site.Params.author }}<meta name="author" content="{{ . }}">{{ end }}
<link rel="stylesheet" href="{{ "css/style.css" | relURL }}">
{{ with .OutputFormats.Get "RSS" -}}
{{ printf `<link rel="%s" type="%s" href="%s" title="%s">` .Rel .MediaType.Type .RelPermalink $.Site.Title | safeHTML }}
{{- end }}
</head>
<body>
{{ partial "header" . }}
{{ block "main" . }}{{ end }}
{{ partial "footer" . }}
</body>
</html>
@@ -0,0 +1,16 @@
{{ define "main" }}
<main>
{{ if or .Title .Content }}
<div>
{{ with .Title }}<h1>{{ . }}</h1>{{ end }}
{{ with .Content }}<div>{{ . }}</div>{{ end }}
</div>
{{ end }}
{{ range .Paginator.Pages }}
{{ .Render "summary" }}
{{ end }}
{{ partial "pagination.html" . }}
</main>
{{ partial "sidebar.html" . }}
{{ end }}
@@ -0,0 +1,26 @@
{{ define "main" }}
<main>
<article>
<h1>{{ .Title }}</h1>
<time datetime="{{ .Date.Format "2006-01-02T15:04:05" }}">{{ .Date.Format "02.01.2006 15:04" }}</time>
<div>
{{ .Content }}
</div>
{{ with .Params.tags }}
<div>
<ul id="tags">
{{ range . }}
<li><a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a></li>
{{ end }}
</ul>
</div>
{{ end }}
{{ with .Site.DisqusShortname }}
<div>
{{ template "_internal/disqus.html" . }}
</div>
{{ end }}
</article>
</main>
{{ partial "sidebar.html" . }}
{{ end }}
@@ -0,0 +1,13 @@
<article>
<h1><a href="{{ .Permalink }}">{{ .Title }}</a></h1>
<time datetime="{{ .Date.Format "2006-01-02T15:04:05" }}">{{ .Date.Format "02.01.2006 15:04" }}</time>
{{ range .Params.tags }}
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
{{ end }}
<div>
{{ .Summary }}
{{ if .Truncated }}
<a href="{{ .Permalink }}">Read more...</a>
{{ end }}
</div>
</article>
@@ -0,0 +1,7 @@
{{ define "main" }} {{ readFile "content/README.md" | markdownify }}
<main>
{{ $paginator := .Paginate (where .Site.RegularPages "Type" "in"
.Site.Params.mainSections) }} {{ range $paginator.Pages }} {{ .Render
"summary" }} {{ end }} {{ partial "pagination.html" . }}
</main>
{{ partial "sidebar.html" . }} {{ end }}
@@ -0,0 +1,3 @@
<footer>
<p>Last Update: {{ now.Format "2006-01-02" }}</p>
</footer>
@@ -0,0 +1,12 @@
<header>
<h1><a href="{{ .Site.BaseURL }}">{{ .Site.Title }}</a></h1>
{{ with .Site.Menus.main }}
<nav>
<ul>
{{ range . }}
<li><a href="{{ .URL | relURL }}">{{ .Name }}</a></li>
{{ end }}
</ul>
</nav>
{{ end }}
</header>
@@ -0,0 +1,9 @@
<div>
{{ if .Paginator.HasPrev }}
<a href="{{ .Paginator.Prev.URL }}">Previous Page</a>
{{ end }}
{{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}
{{ if .Paginator.HasNext }}
<a href="{{ .Paginator.Next.URL }}">Next Page</a>
{{ end }}
</div>
@@ -0,0 +1,14 @@
<aside>
<div>
<div>
<h3>LATEST POSTS</h3>
</div>
<div>
<ul>
{{ range first 5 (where .Site.RegularPages "Type" "in" .Site.Params.mainSections) }}
<li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</div>
</div>
</aside>
+13
View File
@@ -0,0 +1,13 @@
name = "Blank"
license = "MIT"
licenselink = "https://github.com/vimux/blank/blob/master/LICENSE"
description = "Starter Hugo theme for developers."
homepage = "https://github.com/vimux/blank/"
demosite = "https://blank-demo.netlify.app/"
tags = ["blog", "plain", "blank", "starter", "development"]
features = ["blog"]
min_version = "0.20"
[author]
name = "Vimux"
homepage = "https://github.com/vimux"
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
# Build and validate the EurKEY-macOS keyboard layout bundle.
#
# Regenerates Info.plist with correct KLInfo entries for all layout versions,
# sets the bundle version, and validates the bundle structure.
#
# Usage: bash scripts/build-bundle.sh [--version YYYY.MM.DD]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
BUNDLE_DIR="${PROJECT_DIR}/EurKey-macOS.bundle"
CONTENTS_DIR="${BUNDLE_DIR}/Contents"
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
# parse arguments
VERSION="${1:-$(date +%Y.%m.%d)}"
if [[ "${1:-}" == "--version" ]]; then
VERSION="${2:?missing version argument}"
fi
BUNDLE_ID="de.felixfoertsch.keyboardlayout.EurKEY-macOS"
BUNDLE_NAME="EurKEY-macOS"
# layout versions to include
VERSIONS=("v1.2" "v1.3" "v1.4" "v2.0")
echo "Building ${BUNDLE_NAME} ${VERSION}"
echo "Bundle: ${BUNDLE_DIR}"
echo
# --- validate that all required files exist ---
errors=0
for ver in "${VERSIONS[@]}"; do
keylayout="${RESOURCES_DIR}/EurKEY ${ver}.keylayout"
icns="${RESOURCES_DIR}/EurKEY ${ver}.icns"
if [[ ! -f "${keylayout}" ]]; then
echo "ERROR: missing ${keylayout}"
errors=$((errors + 1))
fi
if [[ ! -f "${icns}" ]]; then
echo "ERROR: missing ${icns}"
errors=$((errors + 1))
fi
done
for lang in en de es; do
strings="${RESOURCES_DIR}/${lang}.lproj/InfoPlist.strings"
if [[ ! -f "${strings}" ]]; then
echo "ERROR: missing ${strings}"
errors=$((errors + 1))
fi
done
if [[ $errors -gt 0 ]]; then
echo "FAILED: ${errors} missing file(s)"
exit 1
fi
# --- generate Info.plist ---
cat > "${CONTENTS_DIR}/Info.plist" << 'PLIST_HEADER'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
PLIST_HEADER
echo " <string>${BUNDLE_ID}</string>" >> "${CONTENTS_DIR}/Info.plist"
cat >> "${CONTENTS_DIR}/Info.plist" << 'PLIST_NAME'
<key>CFBundleName</key>
PLIST_NAME
echo " <string>${BUNDLE_NAME}</string>" >> "${CONTENTS_DIR}/Info.plist"
cat >> "${CONTENTS_DIR}/Info.plist" << 'PLIST_VERSION'
<key>CFBundleVersion</key>
PLIST_VERSION
echo " <string>${VERSION}</string>" >> "${CONTENTS_DIR}/Info.plist"
# add KLInfo for each version
for ver in "${VERSIONS[@]}"; do
layout_name="EurKEY ${ver}"
# generate input source ID: bundle id + layout name with spaces removed, lowercased
source_id_suffix=$(echo "eurkey${ver}" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
source_id="${BUNDLE_ID}.${source_id_suffix}"
cat >> "${CONTENTS_DIR}/Info.plist" << KLINFO_ENTRY
<key>KLInfo_${layout_name}</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISIconIsTemplate</key>
<true/>
<key>TISInputSourceID</key>
<string>${source_id}</string>
<key>TISIntendedLanguage</key>
<string>en</string>
</dict>
KLINFO_ENTRY
done
cat >> "${CONTENTS_DIR}/Info.plist" << 'PLIST_FOOTER'
</dict>
</plist>
PLIST_FOOTER
echo "Generated Info.plist with ${#VERSIONS[@]} layout entries"
# --- generate version.plist ---
cat > "${CONTENTS_DIR}/version.plist" << VPLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildVersion</key>
<string>${VERSION}</string>
<key>ProjectName</key>
<string>${BUNDLE_NAME}</string>
<key>SourceVersion</key>
<string>${VERSION}</string>
</dict>
</plist>
VPLIST
echo "Generated version.plist (${VERSION})"
# --- validate with plutil ---
if command -v plutil &> /dev/null; then
plutil -lint "${CONTENTS_DIR}/Info.plist" || exit 1
plutil -lint "${CONTENTS_DIR}/version.plist" || exit 1
echo "plist validation passed"
fi
# --- run layout validation ---
if [[ -f "${SCRIPT_DIR}/validate_layouts.py" ]]; then
echo
python3 "${SCRIPT_DIR}/validate_layouts.py" || exit 1
fi
echo
echo "Bundle build complete: ${BUNDLE_DIR}"
echo "Version: ${VERSION}"
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Create a .dmg installer for EurKEY-macOS keyboard layouts.
#
# The DMG contains the keyboard layout bundle and a symlink to
# /Library/Keyboard Layouts/ for drag-and-drop installation.
#
# Usage: bash scripts/create-dmg.sh [--version YYYY.MM.DD]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
BUNDLE_DIR="${PROJECT_DIR}/EurKey-macOS.bundle"
BUILD_DIR="${PROJECT_DIR}/build"
# parse arguments
VERSION="${1:-$(date +%Y.%m.%d)}"
if [[ "${1:-}" == "--version" ]]; then
VERSION="${2:?missing version argument}"
fi
DMG_NAME="EurKEY-macOS-${VERSION}"
DMG_PATH="${BUILD_DIR}/${DMG_NAME}.dmg"
STAGING_DIR="${BUILD_DIR}/dmg-staging"
echo "Creating DMG: ${DMG_NAME}"
# --- ensure bundle is built ---
if [[ ! -f "${BUNDLE_DIR}/Contents/Info.plist" ]]; then
echo "ERROR: bundle not found at ${BUNDLE_DIR}"
echo "Run scripts/build-bundle.sh first"
exit 1
fi
# --- prepare staging directory ---
rm -rf "${STAGING_DIR}"
mkdir -p "${STAGING_DIR}"
# copy the bundle
cp -R "${BUNDLE_DIR}" "${STAGING_DIR}/"
# create symlink to installation target
ln -s "/Library/Keyboard Layouts" "${STAGING_DIR}/Install Here (Keyboard Layouts)"
echo "Staged files:"
ls -la "${STAGING_DIR}/"
# --- create DMG ---
mkdir -p "${BUILD_DIR}"
rm -f "${DMG_PATH}"
hdiutil create \
-volname "${DMG_NAME}" \
-srcfolder "${STAGING_DIR}" \
-ov \
-format UDZO \
"${DMG_PATH}"
# --- clean up ---
rm -rf "${STAGING_DIR}"
echo
echo "DMG created: ${DMG_PATH}"
echo "Size: $(du -h "${DMG_PATH}" | cut -f1)"
+355
View File
@@ -0,0 +1,355 @@
#!/usr/bin/env python3
"""Parse Apple .keylayout XML files into a flat JSON representation.
Extracts all key mappings across modifier layers (base, Shift, Caps, Option,
Shift+Option, Caps+Option, Command+Option) and resolves dead key states to
their composed outputs.
Usage:
python3 scripts/parse_keylayout.py <file.keylayout> [--output file.json]
Output JSON structure:
{
"name": "EurKEY v1.3",
"modifierMap": { ... },
"keyMaps": {
"0": { "label": "Base", "keys": { "0": {"output": "a", ...}, ... } },
...
},
"actions": {
"a": {
"none": "a",
"dead: ^": "â",
...
},
...
},
"deadKeys": {
"dead: ^": { "terminator": "^", "compositions": { "a": "â", "A": "Â", ... } },
...
}
}
"""
import json
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
# macOS key code → physical key name (US ANSI/ISO layout)
KEY_CODE_NAMES = {
0: "A", 1: "S", 2: "D", 3: "F", 4: "H", 5: "G",
6: "Z", 7: "X", 8: "C", 9: "V", 10: "§/`",
11: "B", 12: "Q", 13: "W", 14: "E", 15: "R",
16: "Y", 17: "T", 18: "1", 19: "2", 20: "3",
21: "4", 22: "6", 23: "5", 24: "=", 25: "9",
26: "7", 27: "-", 28: "8", 29: "0", 30: "]",
31: "O", 32: "U", 33: "[", 34: "I", 35: "P",
36: "Return", 37: "L", 38: "J", 39: "'", 40: "K",
41: ";", 42: "\\", 43: ",", 44: "/", 45: "N",
46: "M", 47: ".", 48: "Tab", 49: "Space", 50: "`",
51: "Delete", 52: "Enter", 53: "Escape",
# numpad
65: "KP.", 67: "KP*", 69: "KP+", 75: "KP/",
76: "KPEnter", 78: "KP-", 81: "KP=",
82: "KP0", 83: "KP1", 84: "KP2", 85: "KP3",
86: "KP4", 87: "KP5", 88: "KP6", 89: "KP7",
91: "KP8", 92: "KP9",
# iso extra key
93: "ISO§", 94: "ISO_backslash", 95: "ISO_comma",
# function/navigation keys
96: "F5", 97: "F6", 98: "F7", 99: "F3",
100: "F8", 101: "F9", 103: "F11", 105: "F13",
107: "F14", 109: "F10", 111: "F12", 113: "F15",
114: "Help/Insert", 115: "Home", 116: "PageUp",
117: "ForwardDelete", 118: "F4", 119: "End",
120: "F2", 121: "PageDown", 122: "F1",
123: "Left", 124: "Right", 125: "Down", 126: "Up",
}
# modifier map index → human-readable label
MODIFIER_LABELS = {
0: "Base",
1: "Shift",
2: "Caps",
3: "Option",
4: "Shift+Option",
5: "Caps+Option",
6: "Command+Option",
7: "Control",
}
# key codes that are "typing" keys (not function/navigation/control)
TYPING_KEY_CODES = set(range(0, 50)) | {50, 93, 94, 95}
def _read_keylayout_xml(filepath):
"""Read a .keylayout file, working around XML 1.1 control character references.
Apple .keylayout files declare XML 1.1 and use numeric character references
for control characters (&#x0001; through &#x001F;) that are invalid in XML 1.0.
Python's ElementTree only supports XML 1.0, so we convert control character
references to placeholder tokens, parse, then restore them.
"""
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# downgrade XML declaration from 1.1 to 1.0
content = content.replace('version="1.1"', 'version="1.0"')
# strip the DOCTYPE (references local DTD that may not exist)
content = re.sub(r'<!DOCTYPE[^>]*>', '', content)
# replace control character references with placeholder strings
# &#x0001; through &#x001F; and &#x007F; are problematic in XML 1.0
def replace_control_ref(m):
code_point = int(m.group(1), 16)
return f"__CTRL_U{code_point:04X}__"
content = re.sub(
r'&#x(000[0-9A-Fa-f]|001[0-9A-Fa-f]|007[Ff]);',
replace_control_ref,
content,
)
return content
def _restore_control_chars(text):
"""Restore placeholder tokens back to actual characters."""
if text is None:
return None
def restore(m):
code_point = int(m.group(1), 16)
return chr(code_point)
return re.sub(r'__CTRL_U([0-9A-F]{4})__', restore, text)
def parse_keylayout(filepath):
"""Parse a .keylayout XML file and return a structured dict."""
xml_content = _read_keylayout_xml(filepath)
root = ET.fromstring(xml_content)
result = {
"name": root.get("name", ""),
"group": root.get("group", ""),
"id": root.get("id", ""),
}
# parse modifier map
result["modifierMap"] = parse_modifier_map(root)
# parse all keyMapSets
key_map_sets = {}
for kms in root.findall(".//keyMapSet"):
kms_id = kms.get("id")
key_map_sets[kms_id] = parse_key_map_set(kms, key_map_sets)
# parse actions (dead key compositions)
actions = parse_actions(root)
result["actions"] = actions
# parse terminators
terminators = {}
for term in root.findall(".//terminators/when"):
state = term.get("state", "")
output = _restore_control_chars(term.get("output", ""))
terminators[state] = output
result["terminators"] = terminators
# resolve layouts
layouts = root.findall(".//layout")
# build resolved key maps with all key codes from all layout entries
resolved = {}
for layout in layouts:
map_set_id = layout.get("mapSet")
first_code = int(layout.get("first", "0"))
last_code = int(layout.get("last", "0"))
kms = key_map_sets.get(map_set_id, {})
for idx_str, keys in kms.items():
if idx_str not in resolved:
resolved[idx_str] = {}
for code_str, entry in keys.items():
code = int(code_str)
if first_code <= code <= last_code:
resolved[idx_str][code_str] = entry
# build the final keyMaps output
key_maps = {}
for idx_str in sorted(resolved.keys(), key=int):
idx = int(idx_str)
label = MODIFIER_LABELS.get(idx, f"Index {idx}")
keys = {}
for code_str in sorted(resolved[idx_str].keys(), key=int):
code = int(code_str)
entry = resolved[idx_str][code_str]
key_name = KEY_CODE_NAMES.get(code, f"code{code}")
key_info = {"code": code, "keyName": key_name}
if "output" in entry:
key_info["output"] = entry["output"]
if "action" in entry:
action_id = entry["action"]
key_info["action"] = action_id
# resolve the action's base output
if action_id in actions:
action_data = actions[action_id]
if "none" in action_data:
key_info["output"] = action_data["none"]
elif "next" in action_data:
key_info["deadKey"] = action_data["next"]
keys[code_str] = key_info
key_maps[idx_str] = {"label": label, "keys": keys}
result["keyMaps"] = key_maps
# build dead key summary
dead_keys = {}
for state_name, terminator in terminators.items():
compositions = {}
for action_id, action_data in actions.items():
if state_name in action_data:
compositions[action_id] = action_data[state_name]
dead_keys[state_name] = {
"terminator": terminator,
"compositions": compositions,
}
result["deadKeys"] = dead_keys
return result
def parse_modifier_map(root):
"""Parse the modifierMap element."""
mod_map = {}
for mm in root.findall(".//modifierMap"):
mm_id = mm.get("id")
default_index = mm.get("defaultIndex", "")
selects = []
for kms in mm.findall("keyMapSelect"):
map_index = kms.get("mapIndex", "")
modifiers = []
for mod in kms.findall("modifier"):
modifiers.append(mod.get("keys", ""))
selects.append({"mapIndex": map_index, "modifiers": modifiers})
mod_map[mm_id] = {"defaultIndex": default_index, "selects": selects}
return mod_map
def parse_key_map_set(kms_element, all_key_map_sets):
"""Parse a keyMapSet element, resolving baseMapSet/baseIndex references."""
result = {}
for km in kms_element.findall("keyMap"):
index = km.get("index")
keys = {}
# resolve base map set if specified
base_map_set_id = km.get("baseMapSet")
base_index = km.get("baseIndex")
if base_map_set_id and base_index:
base_kms = all_key_map_sets.get(base_map_set_id, {})
base_keys = base_kms.get(base_index, {})
keys.update(base_keys)
# parse keys in this keyMap (override base)
for key in km.findall("key"):
code = key.get("code")
entry = {}
if key.get("output") is not None:
entry["output"] = _restore_control_chars(key.get("output"))
if key.get("action") is not None:
entry["action"] = key.get("action")
keys[code] = entry
result[index] = keys
return result
def parse_actions(root):
"""Parse all action elements into a dict of action_id → {state → output/next}."""
actions = {}
for action in root.findall(".//actions/action"):
action_id = action.get("id")
states = {}
for when in action.findall("when"):
state = when.get("state", "none")
if when.get("output") is not None:
states[state] = _restore_control_chars(when.get("output"))
elif when.get("next") is not None:
if state == "none":
states["next"] = when.get("next")
else:
states[state] = f"{when.get('next')}"
actions[action_id] = states
return actions
def format_char(c):
"""Format a character for display, showing control chars as hex."""
if len(c) == 1:
cp = ord(c)
if cp < 0x20 or cp == 0x7F:
return f"U+{cp:04X}"
if cp == 0xA0:
return "NBSP"
return c
def print_summary(data):
"""Print a human-readable summary of the parsed layout."""
print(f"Layout: {data['name']}")
print(f"Dead key states: {', '.join(data['deadKeys'].keys())}")
print()
for idx_str in sorted(data["keyMaps"].keys(), key=int):
km = data["keyMaps"][idx_str]
print(f"--- {km['label']} (index {idx_str}) ---")
for code_str in sorted(km["keys"].keys(), key=int):
code = int(code_str)
if code not in TYPING_KEY_CODES:
continue
ki = km["keys"][code_str]
key_name = ki["keyName"]
output = ki.get("output", "")
dead = ki.get("deadKey", "")
formatted = format_char(output) if output else ""
extra = f" [dead: {dead}]" if dead else ""
action = f" (action: {ki['action']})" if "action" in ki else ""
print(f" {key_name:>12s} (code {code:>3d}): {formatted}{extra}{action}")
print()
def main():
import argparse
parser = argparse.ArgumentParser(description="Parse Apple .keylayout XML files")
parser.add_argument("keylayout", help="Path to .keylayout file")
parser.add_argument("--output", "-o", help="Output JSON file path")
parser.add_argument("--summary", "-s", action="store_true",
help="Print human-readable summary")
args = parser.parse_args()
data = parse_keylayout(args.keylayout)
if args.summary:
print_summary(data)
if args.output:
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent="\t")
print(f"Written to {output_path}")
elif not args.summary:
json.dump(data, sys.stdout, ensure_ascii=False, indent="\t")
print()
if __name__ == "__main__":
main()
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Validate all EurKEY keylayout files against the v1.3 reference spec.
# Exit code 0 if all pass, 1 if any unexpected mismatches.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 "${SCRIPT_DIR}/validate_layouts.py" "$@"
+306
View File
@@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""Validate .keylayout files against the EurKEY v1.3 reference spec.
Compares each layout version's key mappings and dead key compositions against
the reference, reporting mismatches. Supports per-version exception lists for
intentional differences.
Usage:
python3 scripts/validate_layouts.py [--verbose]
Exit code 0 if all layouts pass validation, 1 if any unexpected mismatches.
"""
import sys
from pathlib import Path
# import the parser
sys.path.insert(0, str(Path(__file__).parent))
from parse_keylayout import parse_keylayout, TYPING_KEY_CODES, MODIFIER_LABELS, KEY_CODE_NAMES
BUNDLE_DIR = Path(__file__).parent.parent / "EurKey-macOS.bundle" / "Contents" / "Resources"
# modifier indices that contain meaningful typing output
# (exclude index 6 = Command+Option and 7 = Control — these are system shortcuts)
VALIDATED_MODIFIER_INDICES = {"0", "1", "2", "3", "4", "5"}
def load_layout(version):
"""Parse a keylayout file for the given version string."""
path = BUNDLE_DIR / f"EurKEY {version}.keylayout"
if not path.exists():
print(f"ERROR: {path} not found")
sys.exit(2)
return parse_keylayout(str(path))
def compare_key_maps(reference, target, exceptions):
"""Compare key maps between reference and target layouts.
Returns a list of (modifier_label, key_name, code, ref_output, target_output) tuples
for each mismatch that is not in the exceptions list.
"""
mismatches = []
# build terminator→state name maps for dead key comparison
# build state_name→terminator for both versions
ref_state_to_term = {name: dk["terminator"] for name, dk in reference["deadKeys"].items()}
tgt_state_to_term = {name: dk["terminator"] for name, dk in target["deadKeys"].items()}
for idx_str in VALIDATED_MODIFIER_INDICES:
ref_km = reference["keyMaps"].get(idx_str, {}).get("keys", {})
tgt_km = target["keyMaps"].get(idx_str, {}).get("keys", {})
mod_label = MODIFIER_LABELS.get(int(idx_str), f"Index {idx_str}")
for code_str in ref_km:
code = int(code_str)
if code not in TYPING_KEY_CODES:
continue
ref_key = ref_km[code_str]
tgt_key = tgt_km.get(code_str, {})
ref_output = ref_key.get("output", "")
tgt_output = tgt_key.get("output", "")
ref_dead = ref_key.get("deadKey", "")
tgt_dead = tgt_key.get("deadKey", "")
key_name = KEY_CODE_NAMES.get(code, f"code{code}")
# check for exception
exc_key = f"{idx_str}:{code_str}"
if exc_key in exceptions:
expected = exceptions[exc_key]
if expected.get("output") == tgt_output or expected.get("deadKey") == tgt_dead:
continue
# exception exists but value doesn't match → still a mismatch
mismatches.append((
mod_label, key_name, code,
f"{ref_output or ref_dead} (ref)",
f"{tgt_output or tgt_dead} (got, expected exception: {expected})",
))
continue
# compare dead keys by terminator (state names may differ)
if ref_dead or tgt_dead:
ref_term = ref_state_to_term.get(ref_dead, ref_dead)
tgt_term = tgt_state_to_term.get(tgt_dead, tgt_dead)
if ref_term == tgt_term:
continue # same dead key, different name
# dead keys differ
ref_display = f"[dead: {ref_dead}{ref_term}]"
tgt_display = f"[dead: {tgt_dead}{tgt_term}]" if tgt_dead else tgt_output or "[missing]"
mismatches.append((mod_label, key_name, code, ref_display, tgt_display))
continue
# compare regular outputs
if ref_output != tgt_output:
if not tgt_output and not tgt_dead:
tgt_display = "[missing]"
else:
tgt_display = tgt_output
mismatches.append((mod_label, key_name, code, ref_output, tgt_display))
return mismatches
def _build_terminator_map(data):
"""Build a mapping from terminator character → dead key data.
Different layout versions may use different state names (e.g., "dead: ^" vs "7")
but the same terminator character. This allows matching by terminator.
"""
return {dk["terminator"]: (name, dk) for name, dk in data["deadKeys"].items()}
def _composition_output_set(compositions):
"""Extract the set of output characters from a dead key's compositions.
Since action IDs differ between versions (e.g., "a" vs "a61"), we compare
by the set of output characters produced, not by action ID.
"""
return set(compositions.values())
def compare_dead_keys(reference, target, exceptions):
"""Compare dead key compositions between reference and target.
Matches dead key states by their terminator character (since state names
may differ between versions). Compares composition output sets.
Returns a list of (dead_key_state, detail, ref_value, target_value) tuples.
"""
mismatches = []
ref_by_term = _build_terminator_map(reference)
tgt_by_term = _build_terminator_map(target)
for terminator, (ref_name, ref_dk) in ref_by_term.items():
if ref_name in exceptions.get("_dead_key_skip", []):
continue
if terminator not in tgt_by_term:
mismatches.append((ref_name, "*", "present", "missing"))
continue
_, tgt_dk = tgt_by_term[terminator]
# compare composition output sets
ref_outputs = _composition_output_set(ref_dk["compositions"])
tgt_outputs = _composition_output_set(tgt_dk["compositions"])
only_ref = ref_outputs - tgt_outputs
only_tgt = tgt_outputs - ref_outputs
for out in sorted(only_ref):
exc_key = f"dead:{ref_name}:output:{out}"
if exc_key not in exceptions:
mismatches.append((ref_name, f"output {out}", "present", "missing"))
for out in sorted(only_tgt):
exc_key = f"dead:{ref_name}:extra:{out}"
if exc_key not in exceptions:
mismatches.append((ref_name, f"output {out}", "missing", "present"))
return mismatches
def format_char_display(c):
"""Format a character for display."""
if not c or c in ("[missing]", "missing", "present"):
return c
if len(c) == 1:
cp = ord(c)
if cp < 0x20 or cp == 0x7F:
return f"U+{cp:04X}"
return c
# --- per-version exception definitions ---
# format: {"modifierIndex:keyCode": {"output": "expected_value"}}
# or {"_dead_key_skip": ["state_name", ...]} to skip entire dead key states
# v1.2 predates v1.3 — known differences documented here
V1_2_EXCEPTIONS = {
# Shift+Option S: v1.2 has § where v1.3 has ẞ (capital sharp s)
"4:1": {"output": "§"},
# v1.2 does not have the ¬ (negation) dead key — added in v1.3
"_dead_key_skip": ["dead: ¬"],
}
# v1.4 differences from v1.3:
# - §/` key (code 10) in Caps/Caps+Option outputs ẞ instead of §
# - ¬ dead key has an extra ¬ composition (self-referencing)
V1_4_EXCEPTIONS = {
"2:10": {"output": ""}, # Caps: §/` → ẞ (capital sharp s)
"5:10": {"output": ""}, # Caps+Option: §/` → ẞ
"dead:dead: ¬:extra:¬": True, # extra ¬ composition in negation dead key
}
# v2.0 is a custom edition — skip validation for now, just document diffs
V2_0_EXCEPTIONS = {
"_skip_validation": True,
}
VERSIONS = {
"v1.2": {"file": "v1.2", "exceptions": V1_2_EXCEPTIONS, "label": "EurKEY v1.2"},
"v1.3": {"file": "v1.3", "exceptions": {}, "label": "EurKEY v1.3 (reference)"},
"v1.4": {"file": "v1.4", "exceptions": V1_4_EXCEPTIONS, "label": "EurKEY v1.4"},
"v2.0": {"file": "v2.0", "exceptions": V2_0_EXCEPTIONS, "label": "EurKEY v2.0 (custom)"},
}
def validate_version(version_key, reference):
"""Validate a single version against the reference. Returns (pass, mismatch_count)."""
config = VERSIONS[version_key]
exceptions = config["exceptions"]
if exceptions.get("_skip_validation"):
print(f"\n{'='*60}")
print(f" {config['label']} — SKIPPED (custom edition)")
print(f"{'='*60}")
return True, 0
target = load_layout(config["file"])
print(f"\n{'='*60}")
print(f" Validating {config['label']} against v1.3 reference")
print(f"{'='*60}")
# compare key maps
key_mismatches = compare_key_maps(reference, target, exceptions)
dk_mismatches = compare_dead_keys(reference, target, exceptions)
total = len(key_mismatches) + len(dk_mismatches)
if key_mismatches:
print(f"\n Key mapping mismatches ({len(key_mismatches)}):")
for mod_label, key_name, code, ref_out, tgt_out in key_mismatches:
print(f" {mod_label:>14s} | {key_name:>12s} (code {code:>3d}): "
f"ref={format_char_display(ref_out)} got={format_char_display(tgt_out)}")
if dk_mismatches:
print(f"\n Dead key mismatches ({len(dk_mismatches)}):")
for state, action_id, ref_out, tgt_out in dk_mismatches:
print(f" {state:>12s} + {action_id}: "
f"ref={format_char_display(ref_out)} got={format_char_display(tgt_out)}")
if total == 0:
print(f"\n PASS — no unexpected mismatches")
else:
print(f"\n FAIL — {total} unexpected mismatch(es)")
return total == 0, total
def self_validate(reference):
"""Validate that v1.3 matches itself (sanity check)."""
target = load_layout("v1.3")
key_mismatches = compare_key_maps(reference, target, {})
dk_mismatches = compare_dead_keys(reference, target, {})
total = len(key_mismatches) + len(dk_mismatches)
if total > 0:
print("INTERNAL ERROR: v1.3 does not match itself!")
for m in key_mismatches:
print(f" key: {m}")
for m in dk_mismatches:
print(f" dead: {m}")
return False
print(" Self-check: v1.3 matches itself ✓")
return True
def main():
print("EurKEY-macOS Layout Validation")
print("Reference: EurKEY v1.3")
# load reference
reference = load_layout("v1.3")
# sanity check
if not self_validate(reference):
sys.exit(2)
all_pass = True
total_mismatches = 0
for version_key in VERSIONS:
if version_key == "v1.3":
continue # skip self-comparison
passed, count = validate_version(version_key, reference)
if not passed:
all_pass = False
total_mismatches += count
print(f"\n{'='*60}")
if all_pass:
print(" ALL LAYOUTS PASS ✓")
else:
print(f" VALIDATION FAILED — {total_mismatches} total mismatch(es)")
print(f"{'='*60}")
sys.exit(0 if all_pass else 1)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff