add interactive keyboard viewer, fix parser, move spec PDF

interactive layout viewer with version tabs, modifier key highlighting,
dead key compositions, ISO enter spanning two rows, arrow cluster.
fix keylayout parser mapSet range handling, update PDF build scripts,
move eurkey-layout-complete.pdf to spec/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 12:47:48 +01:00
parent a8f5cf4097
commit 18718e3424
9 changed files with 702 additions and 67 deletions

View File

@@ -35,6 +35,13 @@ jobs:
run: |
pip install fpdf2
python3 scripts/generate_layout_pdf.py -o eurkey-macos.eu/pdf/
- name: Generate layout JSON
run: |
mkdir -p eurkey-macos.eu/data
for ver in v1.2 v1.3 v1.4 v2.0; do
python3 scripts/parse_keylayout.py "src/keylayouts/EurKEY ${ver}.keylayout" \
-o "eurkey-macos.eu/data/eurkey-${ver}.json"
done
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:

4
.gitignore vendored
View File

@@ -4,6 +4,10 @@ __pycache__/
build/
*.dmg
# Generated site assets
eurkey-macos.eu/data/
eurkey-macos.eu/pdf/
# macOS
.DS_Store
.AppleDouble

View File

@@ -17,8 +17,8 @@
<span>EurKEY</span>
</a>
<div class="nav-links">
<a href="#features">Features</a>
<a href="#layout">Layout</a>
<a href="#features">Features</a>
<a href="#install">Install</a>
<a href="https://github.com/felixfoertsch/EurKEY-macOS" class="nav-github" aria-label="GitHub">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
@@ -37,6 +37,34 @@
<a href="https://github.com/felixfoertsch/EurKEY-macOS/releases" class="btn">Download on GitHub</a>
</section>
<!-- LAYOUT PREVIEW -->
<section id="layout" class="layout-preview">
<div class="container">
<h2>Layout Preview</h2>
<p class="section-sub">The complete key map for each version, showing all modifier layers and dead key compositions.</p>
<div class="layout-controls">
<div class="version-tabs">
<button class="version-tab active" data-version="v2.0">v2.0</button>
<button class="version-tab" data-version="v1.4">v1.4</button>
<button class="version-tab" data-version="v1.3">v1.3</button>
<button class="version-tab" data-version="v1.2">v1.2</button>
</div>
</div>
<figure class="keyboard" id="keyboard"></figure>
<div class="dead-key-panel" id="dead-key-panel" hidden>
<button class="dead-key-close" id="dead-key-close" aria-label="Close">&times;</button>
<h4 id="dead-key-title"></h4>
<div class="dead-key-grid" id="dead-key-grid"></div>
</div>
<div class="layout-pdf-links">
<a href="pdf/eurkey-v1.3-layout.pdf">v1.3 PDF</a>
<a href="pdf/eurkey-v1.2-layout.pdf">v1.2 PDF</a>
<a href="pdf/eurkey-v1.4-layout.pdf">v1.4 PDF</a>
<a href="pdf/eurkey-v2.0-layout.pdf">v2.0 PDF</a>
</div>
</div>
</section>
<!-- FEATURES -->
<section id="features" class="features">
<div class="container">
@@ -66,20 +94,6 @@
</div>
</section>
<!-- LAYOUT PREVIEW -->
<section id="layout" class="layout-preview">
<div class="container">
<h2>Layout Preview</h2>
<p class="section-sub">The complete key map for each version, showing all modifier layers and dead key compositions.</p>
<div class="layout-downloads">
<a href="pdf/eurkey-v1.3-layout.pdf" class="layout-link">v1.3 <span>PDF</span></a>
<a href="pdf/eurkey-v1.2-layout.pdf" class="layout-link">v1.2 <span>PDF</span></a>
<a href="pdf/eurkey-v1.4-layout.pdf" class="layout-link">v1.4 <span>PDF</span></a>
<a href="pdf/eurkey-v2.0-layout.pdf" class="layout-link">v2.0 <span>PDF</span></a>
</div>
</div>
</section>
<!-- INSTALLATION -->
<section id="install" class="install">
<div class="container">
@@ -136,5 +150,6 @@
</div>
</footer>
<script src="keyboard.js"></script>
</body>
</html>

302
eurkey-macos.eu/keyboard.js Normal file
View File

@@ -0,0 +1,302 @@
/* EurKEY interactive keyboard viewer */
const KEYBOARD_ROWS = [
[
[10, 1.0, "§"], [18, 1.0, "1"], [19, 1.0, "2"], [20, 1.0, "3"],
[21, 1.0, "4"], [23, 1.0, "5"], [22, 1.0, "6"], [26, 1.0, "7"],
[28, 1.0, "8"], [25, 1.0, "9"], [29, 1.0, "0"], [27, 1.0, "-"],
[24, 1.0, "="], [null, 1.5, "⌫"],
],
[
[null, 1.5, "⇥"], [12, 1.0, "Q"], [13, 1.0, "W"], [14, 1.0, "E"],
[15, 1.0, "R"], [17, 1.0, "T"], [16, 1.0, "Y"], [32, 1.0, "U"],
[34, 1.0, "I"], [31, 1.0, "O"], [35, 1.0, "P"], [33, 1.0, "["],
[30, 1.0, "]"], ["spacer", 1.0, ""],
],
[
[null, 1.75, "⇪"], [0, 1.0, "A"], [1, 1.0, "S"], [2, 1.0, "D"],
[3, 1.0, "F"], [5, 1.0, "G"], [4, 1.0, "H"], [38, 1.0, "J"],
[40, 1.0, "K"], [37, 1.0, "L"], [41, 1.0, ";"], [39, 1.0, "'"],
[42, 1.0, "\\"], ["enter", 0.75, "⏎"],
],
[
[null, 1.25, "⇧"], [50, 1.0, "`"], [6, 1.0, "Z"], [7, 1.0, "X"],
[8, 1.0, "C"], [9, 1.0, "V"], [11, 1.0, "B"], [45, 1.0, "N"],
[46, 1.0, "M"], [43, 1.0, ","], [47, 1.0, "."], [44, 1.0, "/"],
[null, 2.25, "⇧"],
],
[
[null, 1.0, "fn"], [null, 1.0, "⌃"], [null, 1.0, "⌥"], [null, 1.25, "⌘"],
[null, 5.0, ""], [null, 1.25, "⌘"], [null, 1.0, "⌥"],
["arrow-cluster", 3.0, ""],
],
];
const MOD_BASE = "0";
const MOD_SHIFT = "1";
const MOD_OPTION = "3";
const MOD_SHIFT_OPTION = "4";
const LAYERS = [
{ mod: MOD_SHIFT, cls: "key-char--shift" },
{ mod: MOD_SHIFT_OPTION, cls: "key-char--shift-option" },
{ mod: MOD_BASE, cls: "key-char--base" },
{ mod: MOD_OPTION, cls: "key-char--option" },
];
const MOD_LABELS = { "\u21e7": "shift", "\u2325": "option" };
const cache = new Map();
let currentVersion = "v2.0";
let currentData = null;
async function loadVersion(version) {
if (cache.has(version)) return cache.get(version);
const resp = await fetch("data/eurkey-" + version + ".json");
if (!resp.ok) throw new Error("Failed to load " + version);
const data = await resp.json();
cache.set(version, data);
return data;
}
function charForKey(data, modIndex, keyCode) {
const keyMap = data.keyMaps[modIndex];
if (!keyMap) return null;
const key = keyMap.keys[String(keyCode)];
if (!key) return null;
if (key.deadKey) {
const terminator = data.terminators[key.deadKey] || data.deadKeys[key.deadKey]?.terminator;
return { char: terminator || "\u25c6", deadKey: key.deadKey };
}
return { char: key.output || "" };
}
function displayChar(ch) {
if (!ch || ch === " ") return "\u00a0";
if (ch === "\u00a0") return "\u237d";
return ch;
}
function clearElement(el) {
while (el.firstChild) el.removeChild(el.firstChild);
}
function renderKeyboard(data) {
const kb = document.getElementById("keyboard");
clearElement(kb);
for (const row of KEYBOARD_ROWS) {
const rowEl = document.createElement("div");
rowEl.className = "keyboard-row";
for (const [keyCode, width, label] of row) {
const keyEl = document.createElement("div");
keyEl.className = "key";
keyEl.style.setProperty("--w", width);
if (keyCode === "arrow-cluster") {
keyEl.classList.add("key--arrow-cluster");
function makeArrowKey(sym) {
const ak = document.createElement("div");
ak.className = "arrow-key key--mod";
const lbl = document.createElement("span");
lbl.className = "key-mod-label";
lbl.textContent = sym;
ak.appendChild(lbl);
return ak;
}
const topRow = document.createElement("div");
topRow.className = "arrow-row";
const spacerL = document.createElement("div");
spacerL.className = "arrow-key arrow-key--spacer";
const spacerR = document.createElement("div");
spacerR.className = "arrow-key arrow-key--spacer";
topRow.append(spacerL, makeArrowKey("▲"), spacerR);
const bottomRow = document.createElement("div");
bottomRow.className = "arrow-row";
for (const sym of ["◀", "▼", "▶"]) {
bottomRow.appendChild(makeArrowKey(sym));
}
keyEl.append(topRow, bottomRow);
} else if (keyCode === "spacer") {
keyEl.classList.add("key--spacer");
} else if (keyCode === "enter") {
keyEl.classList.add("key--mod", "key--enter");
const span = document.createElement("span");
span.className = "key-mod-label";
span.textContent = label;
keyEl.appendChild(span);
} else if (keyCode === null) {
keyEl.classList.add("key--mod");
if (MOD_LABELS[label]) keyEl.dataset.mod = MOD_LABELS[label];
const span = document.createElement("span");
span.className = "key-mod-label";
span.textContent = label;
keyEl.appendChild(span);
} else {
let hasDead = false;
let deadState = null;
for (const layer of LAYERS) {
const info = charForKey(data, layer.mod, keyCode);
const span = document.createElement("span");
span.className = "key-char " + layer.cls;
if (info) {
span.textContent = displayChar(info.char);
if (info.deadKey) {
hasDead = true;
deadState = info.deadKey;
span.classList.add("key-char--is-dead");
}
}
keyEl.appendChild(span);
}
if (hasDead) {
keyEl.classList.add("key--dead");
keyEl.dataset.deadKey = deadState;
keyEl.addEventListener("click", () => showDeadKeyPanel(data, deadState));
}
}
rowEl.appendChild(keyEl);
}
kb.appendChild(rowEl);
}
}
function showDeadKeyPanel(data, state) {
const panel = document.getElementById("dead-key-panel");
const title = document.getElementById("dead-key-title");
const grid = document.getElementById("dead-key-grid");
const dk = data.deadKeys[state];
if (!dk) return;
const terminator = dk.terminator || data.terminators[state] || "";
title.textContent = state + " \u2192 " + displayChar(terminator);
clearElement(grid);
const compositions = dk.compositions;
if (!compositions) return;
for (const [actionId, composed] of Object.entries(compositions)) {
const action = data.actions[actionId];
const base = action?.none || actionId;
const pair = document.createElement("div");
pair.className = "dead-key-pair";
const baseSpan = document.createElement("span");
baseSpan.className = "dead-key-base";
baseSpan.textContent = displayChar(base);
const arrow = document.createElement("span");
arrow.className = "dead-key-arrow";
arrow.textContent = "\u2192";
const composedSpan = document.createElement("span");
composedSpan.className = "dead-key-composed";
composedSpan.textContent = displayChar(composed);
pair.appendChild(baseSpan);
pair.appendChild(arrow);
pair.appendChild(composedSpan);
grid.appendChild(pair);
}
panel.hidden = false;
panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
function showError(msg) {
const kb = document.getElementById("keyboard");
clearElement(kb);
const p = document.createElement("p");
p.className = "keyboard-error";
p.textContent = msg;
kb.appendChild(p);
}
/* --- Modifier key detection --- */
const activeModifiers = new Set();
function updateActiveLayer() {
const kb = document.getElementById("keyboard");
const shift = activeModifiers.has("shift");
const option = activeModifiers.has("option");
let layer = null;
if (shift && option) layer = "shift-option";
else if (shift) layer = "shift";
else if (option) layer = "option";
if (layer) {
kb.dataset.activeLayer = layer;
} else {
delete kb.dataset.activeLayer;
}
}
document.addEventListener("keydown", (e) => {
if (e.key === "Shift") activeModifiers.add("shift");
if (e.key === "Alt") activeModifiers.add("option");
updateActiveLayer();
});
document.addEventListener("keyup", (e) => {
if (e.key === "Shift") activeModifiers.delete("shift");
if (e.key === "Alt") activeModifiers.delete("option");
updateActiveLayer();
});
window.addEventListener("blur", () => {
activeModifiers.clear();
updateActiveLayer();
});
/* --- Version tabs --- */
function initTabs() {
const tabs = document.querySelectorAll(".version-tab");
tabs.forEach(tab => {
tab.addEventListener("click", async () => {
const version = tab.dataset.version;
if (version === currentVersion) return;
tabs.forEach(t => t.classList.remove("active"));
tab.classList.add("active");
currentVersion = version;
document.getElementById("dead-key-panel").hidden = true;
try {
currentData = await loadVersion(version);
renderKeyboard(currentData);
} catch (e) {
console.error("Failed to load layout:", e);
showError("Failed to load layout data.");
}
});
});
}
/* --- Init --- */
document.getElementById("dead-key-close").addEventListener("click", () => {
document.getElementById("dead-key-panel").hidden = true;
});
initTabs();
loadVersion(currentVersion).then(data => {
currentData = data;
renderKeyboard(data);
}).catch(e => {
console.error("Failed to load initial layout:", e);
showError("Failed to load layout data.");
});

View File

@@ -207,40 +207,338 @@ img {
margin-bottom: 2rem;
}
.layout-downloads {
/* Version tabs */
.layout-controls {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.layout-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
.version-tabs {
display: flex;
gap: 0.25rem;
background: #e0e4ed;
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
padding: 3px;
}
.version-tab {
border: none;
background: transparent;
padding: 0.4rem 1rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
color: #1a1a2e;
text-decoration: none;
color: #5a6178;
cursor: pointer;
transition: all 0.15s;
}
.layout-link span {
font-size: 0.75rem;
font-weight: 500;
color: #5a6178;
text-transform: uppercase;
letter-spacing: 0.05em;
.version-tab:hover {
color: #1a1a2e;
}
.layout-link:hover {
border-color: #003399;
.version-tab.active {
background: #fff;
color: #1a1a2e;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Keyboard */
.keyboard {
--key-unit: 44px;
--key-gap: 6px;
--row-gap: 6px;
width: fit-content;
margin: 0 auto;
padding: 12px;
background: linear-gradient(to bottom, #dcdcdc, #d8d8d8 95%, #e2e2e2);
border: 1px solid #a0a0a0;
border-radius: 7px;
box-shadow:
0 1px 0 #8c8c8c,
0 2px 0 #969696,
0 4px 12px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
gap: var(--row-gap);
}
.keyboard-row {
display: flex;
gap: var(--key-gap);
}
.key {
width: calc(var(--w) * var(--key-unit) + (var(--w) - 1) * var(--key-gap));
height: var(--key-unit);
flex-shrink: 0;
background: linear-gradient(to bottom, #f9f9f9, #efefef 95%, #e2e2e2);
border-radius: 4px;
box-shadow:
0 1px 0 #fafafa,
0 2px 0 1px #828282,
0 2px 0 2px #464646;
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
padding: 3px 5px;
font-size: 12px;
font-family: "SF Mono", "Menlo", "Consolas", monospace;
cursor: default;
transition: transform 0.08s, box-shadow 0.08s;
}
.key:active {
transform: translateY(1px);
box-shadow:
0 0.5px 0 1px #828282,
0 0.5px 0 2px #464646;
}
/* Character positions */
.key-char {
display: flex;
align-items: center;
line-height: 1;
overflow: hidden;
white-space: nowrap;
}
.key-char--shift {
justify-content: flex-start;
color: #0028aa;
}
.key-char--shift-option {
justify-content: flex-end;
color: #780078;
}
.key-char--base {
justify-content: flex-start;
color: #000;
}
.key-char--option {
justify-content: flex-end;
color: #aa0000;
}
.key-char--is-dead {
font-weight: 700;
text-decoration: underline;
text-decoration-style: dotted;
}
/* Dead key highlight */
.key--dead {
background: linear-gradient(to bottom, #fef9e7, #fef3cd 95%, #f5e6a8);
cursor: pointer;
}
.key--dead:hover {
background: linear-gradient(to bottom, #fef3cd, #fce9a0 95%, #f0da88);
}
/* Modifier keys */
.key--mod {
background: linear-gradient(to bottom, #eeeeee, #e4e4e4 95%, #d8d8d8);
display: flex;
align-items: center;
justify-content: center;
}
.key-mod-label {
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #555;
font-weight: 500;
}
/* Invisible spacer (reserves space next to spanning enter key) */
.key--spacer {
background: none;
box-shadow: none;
pointer-events: none;
}
/* ISO Enter — single tall block in row 3, extending upward into row 2 */
.key--enter {
height: calc(2 * var(--key-unit) + var(--row-gap));
margin-top: calc(-1 * var(--key-unit) - var(--row-gap));
z-index: 1;
}
/* Arrow cluster — inverted T, all half-height */
.key--arrow-cluster {
display: flex;
flex-direction: column;
gap: 2px;
background: transparent;
box-shadow: none;
padding: 0;
}
.key--arrow-cluster:active {
transform: none;
box-shadow: none;
}
.arrow-row {
display: flex;
gap: var(--key-gap);
flex: 1;
}
.arrow-key {
flex: 1;
border-radius: 3px;
background: linear-gradient(to bottom, #eeeeee, #e4e4e4 95%, #d8d8d8);
box-shadow:
0 0.5px 0 #fafafa,
0 1px 0 1px #828282,
0 1px 0 2px #464646;
display: flex;
align-items: center;
justify-content: center;
}
.arrow-key:active {
transform: translateY(1px);
box-shadow:
0 0.5px 0 1px #828282,
0 0.5px 0 2px #464646;
}
.arrow-key .key-mod-label {
font-size: 9px;
}
.arrow-key--spacer {
visibility: hidden;
background: none;
box-shadow: none;
}
/* Active modifier layer — dim inactive characters, emphasize active */
.keyboard[data-active-layer] .key-char {
opacity: 0.15;
transition: opacity 0.1s;
}
.keyboard[data-active-layer="shift"] .key-char--shift,
.keyboard[data-active-layer="option"] .key-char--option,
.keyboard[data-active-layer="shift-option"] .key-char--shift-option {
opacity: 1;
font-weight: 700;
font-size: 15px;
}
/* Pressed modifier key visual */
.keyboard[data-active-layer="shift"] .key--mod[data-mod="shift"],
.keyboard[data-active-layer="option"] .key--mod[data-mod="option"],
.keyboard[data-active-layer="shift-option"] .key--mod[data-mod="shift"],
.keyboard[data-active-layer="shift-option"] .key--mod[data-mod="option"] {
transform: translateY(1px);
background: linear-gradient(to bottom, #d4d4d4, #ccc);
box-shadow:
0 0.5px 0 1px #828282,
0 0.5px 0 2px #464646;
}
/* Dead key panel */
.dead-key-panel {
max-width: fit-content;
margin: 1.5rem auto 0;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 1.25rem;
position: relative;
}
.dead-key-panel h4 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 1rem;
color: #1a1a2e;
}
.dead-key-close {
position: absolute;
top: 0.75rem;
right: 0.75rem;
border: none;
background: none;
font-size: 1.25rem;
cursor: pointer;
color: #5a6178;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.dead-key-close:hover {
background: #f0f0f0;
color: #1a1a2e;
}
.dead-key-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 0.5rem;
}
.dead-key-pair {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
background: #f5f7fa;
border-radius: 4px;
padding: 0.375rem 0.5rem;
font-family: "SF Mono", "Menlo", "Consolas", monospace;
font-size: 0.875rem;
}
.dead-key-base {
color: #5a6178;
}
.dead-key-arrow {
color: #aaa;
font-size: 0.75rem;
}
.dead-key-composed {
color: #1a1a2e;
font-weight: 600;
}
/* PDF links */
.layout-pdf-links {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
.layout-pdf-links a {
font-size: 0.8rem;
color: #5a6178;
}
.layout-pdf-links a:hover {
color: #003399;
text-decoration: none;
}
/* Error */
.keyboard-error {
text-align: center;
padding: 2rem;
color: #aa0000;
}
/* Installation */
@@ -344,6 +642,13 @@ img {
}
/* Responsive */
@media (max-width: 830px) {
.keyboard {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 768px) {
.hero h1 {
font-size: 1.75rem;

View File

@@ -1,8 +1,10 @@
#!/usr/bin/env bash
# Generate keyboard layout PDFs from .keylayout files.
# Requires: fpdf2 (pip install fpdf2)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# auto-install Python dependencies
python3 -c "import fpdf" 2>/dev/null || pip3 install --quiet fpdf2
exec python3 "${SCRIPT_DIR}/generate_layout_pdf.py" "$@"

View File

@@ -54,7 +54,7 @@ KEYBOARD_ROWS = [
],
# Row 3: Bottom row
[
(None, 1.25, "Shift"), (93, 1.0, "§"), (6, 1.0, "Z"), (7, 1.0, "X"),
(None, 1.25, "Shift"), (50, 1.0, "`"), (6, 1.0, "Z"), (7, 1.0, "X"),
(8, 1.0, "C"), (9, 1.0, "V"), (11, 1.0, "B"), (45, 1.0, "N"),
(46, 1.0, "M"), (43, 1.0, ","), (47, 1.0, "."), (44, 1.0, "/"),
(None, 2.75, "Shift"),
@@ -352,7 +352,7 @@ def generate_pdf(version, output_dir):
return False
print(f"Generating PDF for EurKEY {version}...")
data = parse_keylayout(str(keylayout))
data = parse_keylayout(str(keylayout), keyboard_type=0)
pdf = LayoutPDF(f"EurKEY {version}")
pdf.generate(data)

View File

@@ -128,8 +128,13 @@ def _restore_control_chars(text):
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."""
def parse_keylayout(filepath, keyboard_type=0):
"""Parse a .keylayout XML file and return a structured dict.
keyboard_type selects which mapSet to use. Each <layout> element
covers a range of hardware keyboard types (first..last). The mapSet
matching the requested type is used. Default 0 = MacBook built-in.
"""
xml_content = _read_keylayout_xml(filepath)
root = ET.fromstring(xml_content)
@@ -160,32 +165,25 @@ def parse_keylayout(filepath):
terminators[state] = output
result["terminators"] = terminators
# resolve layouts
layouts = root.findall(".//layout")
# find the mapSet for the requested keyboard type
target_map_set = None
for layout in root.findall(".//layout"):
first = int(layout.get("first", "0"))
last = int(layout.get("last", "0"))
if first <= keyboard_type <= last:
target_map_set = layout.get("mapSet")
break
# build resolved key maps from all layout entries
# first pass: load ALL keys from each keyMapSet (base definitions)
# second pass: override with keys from layout entries that specify ranges
if target_map_set is None:
# fall back to first layout entry
first_layout = root.find(".//layout")
target_map_set = first_layout.get("mapSet") if first_layout is not None else None
# resolve keys from the selected mapSet
resolved = {}
seen_map_sets = set()
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 map_set_id not in seen_map_sets:
# first time seeing this mapSet: include all keys
resolved[idx_str][code_str] = entry
elif first_code <= code <= last_code:
# subsequent layout with same mapSet: only override in range
resolved[idx_str][code_str] = entry
seen_map_sets.add(map_set_id)
kms = key_map_sets.get(target_map_set, {})
for idx_str, keys in kms.items():
resolved[idx_str] = dict(keys)
# build the final keyMaps output
key_maps = {}
@@ -341,9 +339,11 @@ def main():
parser.add_argument("--output", "-o", help="Output JSON file path")
parser.add_argument("--summary", "-s", action="store_true",
help="Print human-readable summary")
parser.add_argument("--keyboard-type", "-k", type=int, default=0,
help="Hardware keyboard type ID (default: 0 = MacBook built-in)")
args = parser.parse_args()
data = parse_keylayout(args.keylayout)
data = parse_keylayout(args.keylayout, keyboard_type=args.keyboard_type)
if args.summary:
print_summary(data)