Compare commits
6 Commits
b3cfa280df
...
2026.03.03
| Author | SHA1 | Date | |
|---|---|---|---|
| 859c26f64c | |||
| 5e29ef63d4 | |||
| 079ff0a872 | |||
| 7084817dab | |||
| e16b92051d | |||
| b5e2de535e |
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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).
|
1. Download the latest `EurKEY-macOS-YYYY.MM.DD.dmg` from [Releases](https://github.com/felixfoertsch/EurKEY-macOS/releases).
|
||||||
The EU flag icon is taken from [Iconspedia](http://www.iconspedia.com/pack/european-flags-1631/),
|
2. Open the DMG.
|
||||||
created by [Alpak](http://alpak.deviantart.com/) and
|
3. Drag `EurKey-macOS.bundle` to the `Install Here (Keyboard Layouts)` folder.
|
||||||
licensed under [CC](http://creativecommons.org/licenses/by-nc-nd/3.0)
|
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).
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
+++
|
||||||
|
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||||
|
date = {{ .Date }}
|
||||||
|
draft = true
|
||||||
|
+++
|
||||||
@@ -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).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
baseURL = 'https://eurkey-macos.eu'
|
||||||
|
languageCode = 'en'
|
||||||
|
title = 'EurKEY-macOS'
|
||||||
|
theme = 'blank'
|
||||||
|
After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -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.
|
||||||
@@ -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/)**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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 }}
|
||||||
|
+++
|
||||||
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
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>
|
||||||
@@ -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"
|
||||||
@@ -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}"
|
||||||
@@ -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)"
|
||||||
@@ -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 ( through ) 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
|
||||||
|
#  through  and  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()
|
||||||
@@ -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" "$@"
|
||||||
@@ -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()
|
||||||