From 6517a2e83c77b376b0c8a397ea389a77353a7874 Mon Sep 17 00:00:00 2001 From: Alejandro Martin Perez Date: Tue, 13 Oct 2026 23:51:57 +0200 Subject: [PATCH 1/5] fix(kbd): real .kbd layouts and locale-tag names (en-US/es-ES) (#394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kbdcompile never parsed the real KLC layout table, so US.kbd/Spanish.kbd shipped with 256 empty entries. Only the kernel's built-in US fallback worked; switching to Spanish changed the index but typed nothing. - rewrite the KLC parser: shift-state columns, `@` dead keys and the DEADKEY/COMPOSE tables; regenerate the layouts from the sources. - rename the layouts to locale tags `en-US`/`es-ES` (files `en-US.kbd`/`es-ES.kbd`) and migrate legacy `US`/`Spanish` Registry values on load. - add kernel tests for switching by name (case-insensitive) and typing with the Spanish layout (ñ/Ñ/ç, dead keys, composition). --- data/keyboard/{Spanish.kbd => en-US.kbd} | Bin 4160 -> 4160 bytes data/keyboard/{KBDUS.klc => en-US.klc} | 0 data/keyboard/{US.kbd => es-ES.kbd} | Bin 4160 -> 4484 bytes data/keyboard/{KBDSP.klc => es-ES.klc} | 0 docs/architecture/source-of-truth.md | 2 +- neodos-kernel/src/input/kbd/config.rs | 17 +- neodos-kernel/src/input/kbd/layout.rs | 46 +++-- neodos-kernel/src/input/kbd/mod.rs | 79 +++++++- neodos-kernel/src/input/mod.rs | 2 + tools/kbdcompile/src/main.rs | 226 +++++++++++++---------- 10 files changed, 249 insertions(+), 123 deletions(-) rename data/keyboard/{Spanish.kbd => en-US.kbd} (72%) rename data/keyboard/{KBDUS.klc => en-US.klc} (100%) rename data/keyboard/{US.kbd => es-ES.kbd} (65%) rename data/keyboard/{KBDSP.klc => es-ES.klc} (100%) diff --git a/data/keyboard/Spanish.kbd b/data/keyboard/en-US.kbd similarity index 72% rename from data/keyboard/Spanish.kbd rename to data/keyboard/en-US.kbd index 29adde99bb21bcf33708e66365ab4f121cd35fe2..c420d9083e951422ec3db304fa0aaf5444bc45c8 100644 GIT binary patch literal 4160 zcmeIy*G|Jg6h+a^cL0*bQ2|CbNumU2E{FuiCZZbw^1Q(XH(okrMQz?aTnF% zMry<%w#0qZiOqUUUokK9A6OPA84+KzAiiZ${KTsGj=1=k3GoH9;!EblYrJ?a^y6oP zH_yd^&3a7lFeLlk5EJhc5g#xrK4eUM#JKo`N%1LB@flO%9=gR~LHaj@pdMBta+k88oNx%;* z*Lk?OfBL>=zvlXUcK`hLgU#FKolYQM_mAy2+i$kt?Ehx(n@j@sKK$Q(m`?E%Z=y<+ delta 48 zcmX@0a6p0I+sTE2k%56BxF9hvvp8d-pg0=?Lu#?E>qH%c&3jlIxPU|gD+^Es0{}fY B3`qb0 diff --git a/data/keyboard/KBDUS.klc b/data/keyboard/en-US.klc similarity index 100% rename from data/keyboard/KBDUS.klc rename to data/keyboard/en-US.klc diff --git a/data/keyboard/US.kbd b/data/keyboard/es-ES.kbd similarity index 65% rename from data/keyboard/US.kbd rename to data/keyboard/es-ES.kbd index c9cca891d493de551859d65b9bfe1d2438188d3b..62db261a2d194a1a268d9ab61b1afe22adfeae42 100644 GIT binary patch literal 4484 zcmeH~*J~6}6o)_2|3Zba*Q`DEGBKmB(X1L*6H83gQP+}DW{AqUFJtGoClBXHC-}3D__i*o-yBD^itehTz-&o>R>GN;TA8}1;&p+Xu zsE?r^2b2}Y$I_o!MLv!r3@GyP45UfkD?ecZgJ^LI^Gg{_OHuwrM#&2CNeneUnPJ99 zGu`+QW*R@lG2_wwM12Q~P5vI18~5?d`ggL##CNgOc#az5S<=QcB#j58jBjU=@j7Z* zNpE`Q`#FN)tfp`HI)3FA_bImS0K*Z3yp8Q;u&<6EdOzLf>W z+qi1Hm1g$qn{av!*Y zi2EJ)`)3Eb>;8$)TYTQ)^A`W!;`^qf1Mz+MfA?Xj+E_0xF9CEBBr<{rnx6N8cnM6SZtt84EQLzoDwVbKekEvH$_L|2jl0MqW}N^ delta 44 xcmZosKA^zm?c~D1$iTo58az=@e4+w7OKP5O=tLcZ&3jlIxPU|gD+^GT0RZ)53Z4J} diff --git a/data/keyboard/KBDSP.klc b/data/keyboard/es-ES.klc similarity index 100% rename from data/keyboard/KBDSP.klc rename to data/keyboard/es-ES.klc diff --git a/docs/architecture/source-of-truth.md b/docs/architecture/source-of-truth.md index 0a48761c..e54ec32a 100644 --- a/docs/architecture/source-of-truth.md +++ b/docs/architecture/source-of-truth.md @@ -628,7 +628,7 @@ layout translation. NeoKBD handles scancode → Unicode conversion, dead key com modifier tracking, and hotkey dispatch (Ctrl+Alt+Del → poweroff, Alt+F1-F8 → VT switch). **Rule 11.6.3**: Layout switching is via `ob_set_info(KeyboardSetLayout=43)` on -`\Device\Keyboard` with a layout name string (e.g., "Spanish", "US"). The `neokey.nxe` +`\Device\Keyboard` with a layout name string (e.g., "es-ES", "en-US"). The `neokey.nxe` binary provides the user interface: `NEOKEY layout `, `NEOKEY layouts`, `NEOKEY show`. **Rule 11.6.4**: Keyboard configuration (active layout, repeat delay/rate, NumLock/CapsLock diff --git a/neodos-kernel/src/input/kbd/config.rs b/neodos-kernel/src/input/kbd/config.rs index 39e1687c..7c47b8cc 100644 --- a/neodos-kernel/src/input/kbd/config.rs +++ b/neodos-kernel/src/input/kbd/config.rs @@ -3,6 +3,21 @@ use crate::kbd::KbdConfig; const REG_PATH: &str = "\\Registry\\Machine\\System\\Keyboard"; +/// Map legacy layout names (`US`, `Spanish`, `SP`) to their locale-tag form. +fn normalize_layout_name(name: &str) -> &str { + if name.eq_ignore_ascii_case("us") || name.eq_ignore_ascii_case("en-us") { + "en-US" + } else if name.eq_ignore_ascii_case("spanish") + || name.eq_ignore_ascii_case("sp") + || name.eq_ignore_ascii_case("es") + || name.eq_ignore_ascii_case("es-es") + { + "es-ES" + } else { + name + } +} + pub fn kbd_load_config() -> KbdConfig { let mut config = KbdConfig::default(); @@ -13,7 +28,7 @@ pub fn kbd_load_config() -> KbdConfig { if let Some(vc) = crate::cm::cm_query_value(native_id, "Layout").ok() { if let Some(s) = vc.as_str() { if !s.is_empty() { - config.layout_name = s.to_string(); + config.layout_name = normalize_layout_name(s).to_string(); } } } diff --git a/neodos-kernel/src/input/kbd/layout.rs b/neodos-kernel/src/input/kbd/layout.rs index 6098d54f..660b40f6 100644 --- a/neodos-kernel/src/input/kbd/layout.rs +++ b/neodos-kernel/src/input/kbd/layout.rs @@ -157,7 +157,7 @@ pub fn load_layouts() -> usize { let fallback = KbdLayout { name: { let mut n = [0u8; 32]; - n[..2].copy_from_slice(b"US"); + n[..5].copy_from_slice(b"en-US"); n }, lang_tag: { @@ -170,20 +170,12 @@ pub fn load_layouts() -> usize { }; layouts.push(fallback); - // Try loading from C:\System\Keyboard\*.kbd - let dir_path = "C:\\System\\Keyboard"; - let has_dir = crate::globals::with_vfs(|vfs| -> bool { - vfs.resolve_path(dir_path).is_ok() - }); - - if has_dir { - // Try Spanish.kbd - if let Ok(layout) = load_kbd("C:\\System\\Keyboard\\Spanish.kbd") { - // Replace the fallback US if it's Spanish (or keep both) - if layouts.iter().any(|l| l.name_str() == "Spanish") { - // already loaded - } else { - layouts.push(layout); + // Disk layouts (locale-tagged) override the built-in fallback when present. + for path in ["C:\\System\\Keyboard\\en-US.kbd", "C:\\System\\Keyboard\\es-ES.kbd"] { + if let Ok(layout) = load_kbd(path) { + match layouts.iter().position(|l| l.name_str() == layout.name_str()) { + Some(i) => layouts[i] = layout, + None => layouts.push(layout), } } } @@ -243,19 +235,23 @@ pub fn is_dead_key(layout: &KbdLayout, scancode: u8, modifiers: u8) -> bool { flags == 1 } +/// Compose `base` with a pending `dead` key using `layout`'s compose table. +pub fn compose_in(layout: &KbdLayout, dead: u16, base: u16) -> u16 { + for entry in &layout.compose { + if entry.dead == dead && entry.base == base { + return entry.result; + } + } + // No compose match found; return the base character (or itself). + if dead == base { dead } else { b'?' as u16 } +} + pub fn compose(dead: u16, base: u16) -> u16 { - // Try to find a compose entry matching this dead+base combination let kbd = crate::kbd::KBD.lock(); - if let Some(layout) = kbd.active_layout() { - for entry in &layout.compose { - if entry.dead == dead && entry.base == base { - return entry.result; - } - } + match kbd.active_layout() { + Some(layout) => compose_in(layout, dead, base), + None => if dead == base { dead } else { b'?' as u16 }, } - // No compose match found; return '?' or the base character - if dead == base { dead } - else { b'?' as u16 } } fn builtin_us_layout() -> [KeyEntry; 256] { diff --git a/neodos-kernel/src/input/kbd/mod.rs b/neodos-kernel/src/input/kbd/mod.rs index a9b825c4..c04b05ac 100644 --- a/neodos-kernel/src/input/kbd/mod.rs +++ b/neodos-kernel/src/input/kbd/mod.rs @@ -67,7 +67,7 @@ pub struct KbdConfig { impl Default for KbdConfig { fn default() -> Self { KbdConfig { - layout_name: "US".to_string(), + layout_name: "en-US".to_string(), repeat_delay: 500, repeat_rate: 30, numlock_on_boot: true, @@ -121,7 +121,8 @@ impl NeoKbd { pub fn set_layout_by_name(&mut self, name: &str) -> Result<(), ()> { let idx = self.find_layout_index(name).ok_or(())?; self.state.active_layout_index = idx; - self.config.layout_name = name.to_string(); + // Canonicalise to the layout's own name (e.g. "es-ES", not "spanish"). + self.config.layout_name = self.layouts[idx as usize].name_str().to_string(); let _ = config::kbd_save_config(&self.config); let _ = crate::eventbus::EVENT_BUS.push_event( crate::eventbus::EVENT_KEYB_LAYOUT, @@ -357,3 +358,77 @@ pub fn kbd_init() { kinfo!(LogSubsys::Kbd, "Ready (layout: {})", layout_name); } + +// ── Tests ────────────────────────────────────────────────────────── + +pub fn register_tests() { + use crate::{test_case, test_eq, test_true}; + + // Layout switching: lookup is case-insensitive, switching updates the + // active index and canonicalises the persisted config name. + test_case!("kbd_switch_layout_by_name", { + let mut kbd = NeoKbd::new(); + let mk = |name: &str| { + let mut lay = KbdLayout { + name: [0u8; 32], + lang_tag: [0u8; 16], + entries: [layout::KeyEntry::empty(); 256], + compose: Vec::new(), + }; + lay.name[..name.len()].copy_from_slice(name.as_bytes()); + lay + }; + kbd.layouts.push(mk("en-US")); + kbd.layouts.push(mk("es-ES")); + + test_eq!(kbd.find_layout_index("en-US"), Some(0)); + test_eq!(kbd.find_layout_index("EN-us"), Some(0)); + test_eq!(kbd.find_layout_index("es-ES"), Some(1)); + test_eq!(kbd.find_layout_index("de-DE"), None); + test_eq!(kbd.active_layout().map(|l| l.name_str()), Some("en-US")); + + test_true!(kbd.set_layout_by_name("es-ES").is_ok()); + test_eq!(kbd.state.active_layout_index, 1); + test_eq!(kbd.active_layout().map(|l| l.name_str()), Some("es-ES")); + test_eq!(kbd.config.layout_name.as_str(), "es-ES"); + }); + + // Both locale-tagged layouts must be present (disk image provides them). + test_case!("kbd_locale_layouts_loaded", { + let kbd = KBD.lock(); + test_true!(kbd.find_layout_index("en-US").is_some()); + test_true!(kbd.find_layout_index("es-ES").is_some()); + }); + + // #394: typing with the Spanish layout produces Spanish characters. + test_case!("kbd_spanish_layout_types", { + let idx = KBD.lock().find_layout_index("es-ES"); + test_true!(idx.is_some()); + let es = KBD.lock().layouts[idx.unwrap_or(0) as usize].clone(); + + // ñ / Ñ / ç and the shifted ; on the Spanish table. + test_eq!(layout::lookup_codepoint(&es, 0x27, 0), Some('ñ' as u16)); + test_eq!(layout::lookup_codepoint(&es, 0x27, KBD_SHIFT), Some('Ñ' as u16)); + test_eq!(layout::lookup_codepoint(&es, 0x2b, 0), Some('ç' as u16)); + test_eq!(layout::lookup_codepoint(&es, 0x33, KBD_SHIFT), Some(';' as u16)); + + // Dead keys: ` (grave), ´ (acute) and ~ (AltGr+4). + test_true!(layout::is_dead_key(&es, 0x1a, 0)); + test_true!(layout::is_dead_key(&es, 0x28, 0)); + test_true!(layout::is_dead_key(&es, 0x05, KBD_ALTGR)); + + // Composition: acute+a, tilde+n, diaeresis+u. + test_eq!(layout::compose_in(&es, '´' as u16, 'a' as u16), 'á' as u16); + test_eq!(layout::compose_in(&es, '~' as u16, 'n' as u16), 'ñ' as u16); + test_eq!(layout::compose_in(&es, '¨' as u16, 'u' as u16), 'ü' as u16); + }); + + // The English layout keeps its US punctuation. + test_case!("kbd_us_layout_types", { + let idx = KBD.lock().find_layout_index("en-US"); + test_true!(idx.is_some()); + let en = KBD.lock().layouts[idx.unwrap_or(0) as usize].clone(); + test_eq!(layout::lookup_codepoint(&en, 0x27, 0), Some(';' as u16)); + test_eq!(layout::lookup_codepoint(&en, 0x27, KBD_SHIFT), Some(':' as u16)); + }); +} diff --git a/neodos-kernel/src/input/mod.rs b/neodos-kernel/src/input/mod.rs index 8c18fcce..e9e63e7e 100644 --- a/neodos-kernel/src/input/mod.rs +++ b/neodos-kernel/src/input/mod.rs @@ -15,6 +15,8 @@ pub fn register_tests() { // Raw keyboard event ring + VK mapping (#466). crate::input::kbd::ring::register_tests(); + // Layout switching + locale layouts (#394). + crate::input::kbd::register_tests(); test_case!("input_empty_pop", { let buf = InputBuffer::new(); diff --git a/tools/kbdcompile/src/main.rs b/tools/kbdcompile/src/main.rs index cfa380b0..687d40ab 100644 --- a/tools/kbdcompile/src/main.rs +++ b/tools/kbdcompile/src/main.rs @@ -1,3 +1,19 @@ +//! kbdcompile — convert a Microsoft KLC layout into the kernel `.kbd` format. +//! +//! Usage: `kbdcompile [name] [langtag]` +//! +//! KLC sections handled: +//! - `SHIFTSTATE`: ordered list of shift states (e.g. `0 1 2 6 3`). +//! - `LAYOUT`: `SC VK Cap ...` rows. Values are either a +//! single literal character (`q`, `1`) or a 4-hex-digit Unicode codepoint +//! (`00f1`); `-1` means "no mapping"; a trailing `@` marks a dead key. +//! - `DEADKEY `: `base hex -> composed hex` rows. +//! - `COMPOSE`: `dead base result` hex rows. +//! +//! Shift-state columns map onto the kernel `KeyEntry` fields: +//! `0 -> normal`, `1 -> shift`, `2 -> ctrl`, `6 -> altgr`. Dead-key flags use +//! bit0 (normal), bit1 (shift) and bit2 (altgr), matching `layout::is_dead_key`. + use std::fs; const KBD_MAGIC: &[u8; 4] = b"KBD\0"; @@ -18,13 +34,40 @@ impl KeyEntry { } } -#[repr(C)] struct ComposeEntry { dead: u16, base: u16, result: u16, } +/// Parse a KLC layout cell. Returns `(codepoint, is_dead)` or `None` for `-1`. +fn parse_value(tok: &str) -> Option<(u16, bool)> { + let mut s = tok.trim(); + let mut dead = false; + if let Some(stripped) = s.strip_suffix('@') { + dead = true; + s = stripped; + } + if s.is_empty() || s == "-1" { + return None; + } + // Single literal character (`q`, `Q`, `1`, `%`). + if s.chars().count() == 1 { + let c = s.chars().next().unwrap(); + return Some((c as u16, dead)); + } + // 4-hex-digit Unicode codepoint. + if let Ok(v) = u16::from_str_radix(s, 16) { + return Some((v, dead)); + } + // Fallback: first ASCII character. + s.chars().next().filter(char::is_ascii).map(|c| (c as u16, dead)) +} + +fn hex_u16(tok: &str) -> Option { + u16::from_str_radix(tok.trim().trim_start_matches("0x"), 16).ok() +} + fn main() { let args: Vec = std::env::args().collect(); if args.len() < 3 { @@ -34,132 +77,126 @@ fn main() { let input_path = &args[1]; let output_path = &args[2]; - let name = if args.len() > 3 { &args[3] } else { "Unknown" }; - let lang_tag = if args.len() > 4 { &args[4] } else { "xx-XX" }; + let name = if args.len() > 3 { args[3].clone() } else { "Unknown".to_string() }; + let lang_tag = if args.len() > 4 { args[4].clone() } else { "xx-XX".to_string() }; let content = fs::read_to_string(input_path).unwrap_or_default(); + // Strip a UTF-8 BOM if present. + let content = content.strip_prefix('\u{feff}').unwrap_or(&content); - let mut entries: Vec = Vec::with_capacity(256); - for _ in 0..256 { - entries.push(KeyEntry::empty()); - } - let compose: Vec = Vec::new(); + let mut entries: Vec = (0..256).map(|_| KeyEntry::empty()).collect(); + let mut compose: Vec = Vec::new(); + + // Ordered shift states from the SHIFTSTATE block (0 1 2 6 3 ...). + let mut shift_states: Vec = Vec::new(); + let mut current_dead: Option = None; - // Parse KLC format — very simple parser - let mut in_layout = false; - let mut in_shift = false; - let mut in_altgr = false; - let mut shift_map: Vec<(u8, u16)> = Vec::new(); - let mut altgr_map: Vec<(u8, u16)> = Vec::new(); - let mut normal_map: Vec<(u8, u16)> = Vec::new(); + #[derive(PartialEq)] + enum Mode { Scan, ShiftState, Layout, DeadKey, Compose, Skip } + let mut mode = Mode::Scan; - for line in content.lines() { + for raw in content.lines() { + let line = raw.trim_end(); let trimmed = line.trim(); - if trimmed.starts_with("//") || trimmed.is_empty() { + + if trimmed.is_empty() { + // Blank lines are separators inside blocks (e.g. between a + // `DEADKEY` header and its rows); blocks end at the next header. + continue; + } + if trimmed.starts_with("//") { continue; } - match trimmed { - "LAYOUT" => { in_layout = true; continue; } - "SHIFTSTATE 0" => { in_shift = false; in_altgr = false; continue; } - "SHIFTSTATE 1" => { in_shift = true; in_altgr = false; continue; } - "SHIFTSTATE 2" => { in_shift = false; in_altgr = true; continue; } - "COMPOSE" => { - in_layout = false; - continue; - } - "ENDKBD" => break, - "KEYNAME" | "KEYNAME_EXT" | "DEADKEY" | "KEYNAME_DEAD" | "LIGATURE" | "DESCRIPTION" | "CODEPAGE" => { + // Section headers are the first whitespace-delimited token. + let first = trimmed.split_whitespace().next().unwrap_or(""); + match first { + "SHIFTSTATE" => { mode = Mode::ShiftState; continue; } + "LAYOUT" => { mode = Mode::Layout; continue; } + "DEADKEY" => { + let dead = trimmed.split_whitespace().nth(1).and_then(hex_u16); + current_dead = dead; + mode = if dead.is_some() { Mode::DeadKey } else { Mode::Skip }; continue; } - "ENDLAYOUT" => { in_layout = false; continue; } + "COMPOSE" => { mode = Mode::Compose; continue; } + "KEYNAME" | "KEYNAME_EXT" | "KEYNAME_DEAD" | "LIGATURE" | "DESCRIPTIONS" + | "ENDKBD" | "ENDLAYOUT" | "LOCALENAME" | "LOCALEID" | "VERSION" + | "ATTRIBUTES" | "ALTGR" => { mode = Mode::Skip; continue; } _ => {} } - if in_layout && trimmed.starts_with("SC") { - // Parse SCxxxx = hex_value - let parts: Vec<&str> = trimmed.split('=').collect(); - if parts.len() != 2 { continue; } - let sc_str = parts[0].trim().trim_start_matches("SC"); - let val_str = parts[1].trim(); - if let (Ok(sc), Ok(val)) = (u8::from_str_radix(sc_str, 16), u16::from_str_radix(val_str, 16)) { - normal_map.push((sc, val)); + match mode { + Mode::ShiftState => { + if let Ok(state) = first.parse::() { + shift_states.push(state); + } } - } - - if trimmed.starts_with("SC") && !in_layout && !in_shift && !in_altgr { - // Another format: SCxxx=value outside LAYOUT block - let parts: Vec<&str> = trimmed.split('=').collect(); - if parts.len() == 2 { - let sc_str = parts[0].trim().trim_start_matches("SC"); - let val_str = parts[1].trim(); - if let (Ok(sc), Ok(val)) = (u8::from_str_radix(sc_str, 16), u16::from_str_radix(val_str, 16)) { - normal_map.push((sc, val)); + Mode::Layout => { + let cols: Vec<&str> = trimmed.split_whitespace().collect(); + if cols.len() < 3 { continue; } + let sc = match u8::from_str_radix(cols[0], 16) { + Ok(v) => v, + Err(_) => continue, + }; + for (i, state) in shift_states.iter().enumerate() { + let idx = 3 + i; + if idx >= cols.len() { break; } + let (cp, dead) = match parse_value(cols[idx]) { + Some(v) => v, + None => continue, + }; + let e = &mut entries[sc as usize]; + match state { + 0 => { e.normal = cp; if dead { e.flags |= 0x01; } } + 1 => { e.shift = cp; if dead { e.flags |= 0x02; } } + 2 => { e.ctrl = cp; } + 6 => { e.altgr = cp; if dead { e.flags |= 0x04; } } + _ => {} // 3 (shift+ctrl): no kernel column + } } } - } - - if in_shift && trimmed.starts_with("SC") { - let parts: Vec<&str> = trimmed.split('=').collect(); - if parts.len() == 2 { - let sc_str = parts[0].trim().trim_start_matches("SC"); - let val_str = parts[1].trim(); - if let (Ok(sc), Ok(val)) = (u8::from_str_radix(sc_str, 16), u16::from_str_radix(val_str, 16)) { - shift_map.push((sc, val)); + Mode::DeadKey => { + if let Some(dead) = current_dead { + let cols: Vec<&str> = trimmed.split_whitespace().collect(); + if cols.len() >= 2 { + if let (Some(base), Some(result)) = (hex_u16(cols[0]), hex_u16(cols[1])) { + compose.push(ComposeEntry { dead, base, result }); + } + } } } - } - - if in_altgr && trimmed.starts_with("SC") { - let parts: Vec<&str> = trimmed.split('=').collect(); - if parts.len() == 2 { - let sc_str = parts[0].trim().trim_start_matches("SC"); - let val_str = parts[1].trim(); - if let (Ok(sc), Ok(val)) = (u8::from_str_radix(sc_str, 16), u16::from_str_radix(val_str, 16)) { - altgr_map.push((sc, val)); + Mode::Compose => { + let cols: Vec<&str> = trimmed.split_whitespace().collect(); + if cols.len() >= 3 { + if let (Some(dead), Some(base), Some(result)) = + (hex_u16(cols[0]), hex_u16(cols[1]), hex_u16(cols[2])) + { + compose.push(ComposeEntry { dead, base, result }); + } } } + Mode::Scan | Mode::Skip => {} } } - // Fill entries - for (sc, val) in &normal_map { - if (*sc as usize) < 256 { - entries[*sc as usize].normal = *val; - } - } - for (sc, val) in &shift_map { - if (*sc as usize) < 256 { - entries[*sc as usize].shift = *val; - } - } - for (sc, val) in &altgr_map { - if (*sc as usize) < 256 { - entries[*sc as usize].altgr = *val; - } - } + // Write output. + let mut out = Vec::with_capacity(60 + 256 * 16 + 4 + compose.len() * 6); + out.extend_from_slice(KBD_MAGIC); + out.extend_from_slice(&1u32.to_le_bytes()); - // Write output - let mut out = Vec::new(); - out.extend_from_slice(KBD_MAGIC); // Magic: 4 bytes - out.extend_from_slice(&1u32.to_le_bytes()); // Version: 4 bytes - - // Name: 32 bytes let mut name_bytes = [0u8; 32]; let nlen = name.len().min(31); name_bytes[..nlen].copy_from_slice(&name.as_bytes()[..nlen]); out.extend_from_slice(&name_bytes); - // Lang tag: 16 bytes let mut tag_bytes = [0u8; 16]; let tlen = lang_tag.len().min(15); tag_bytes[..tlen].copy_from_slice(&lang_tag.as_bytes()[..tlen]); out.extend_from_slice(&tag_bytes); - // Scancode count: 4 bytes out.extend_from_slice(&256u32.to_le_bytes()); - // Key entries: 256 * 16 = 4096 bytes for entry in &entries { out.extend_from_slice(&entry.normal.to_le_bytes()); out.extend_from_slice(&entry.shift.to_le_bytes()); @@ -169,10 +206,7 @@ fn main() { out.extend_from_slice(&entry._pad); } - // Compose count: 4 bytes out.extend_from_slice(&(compose.len() as u32).to_le_bytes()); - - // Compose entries: each 6 bytes for entry in &compose { out.extend_from_slice(&entry.dead.to_le_bytes()); out.extend_from_slice(&entry.base.to_le_bytes()); @@ -180,5 +214,9 @@ fn main() { } fs::write(output_path, &out).expect("Failed to write output file"); - eprintln!("Wrote {} bytes to {}", out.len(), output_path); + let mapped = entries.iter().filter(|e| e.normal != 0xFFFF).count(); + eprintln!( + "Wrote {} bytes to {} ({} scancodes, {} compose entries)", + out.len(), output_path, mapped, compose.len() + ); } From ad4323405e0ec05793f9f1d4019c8c24df0fb045 Mon Sep 17 00:00:00 2001 From: Alejandro Martin Perez Date: Wed, 14 Oct 2026 00:44:41 +0200 Subject: [PATCH 2/5] fix(kbd): resolve and persist the configured layout dynamically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Registry value was only normalised in memory at load, so a legacy `Layout = "Spanish"`/`"US"` stayed on disk until the next write. Resolve the persisted name against the layouts actually loaded (by name, language tag, then legacy `US`/`Spanish` aliases) in `kbd_init()`, and persist the canonical name when it differs — the Registry now converges to `en-US`/`es-ES` instead of keeping a stale value. Drops the static `normalize_layout_name` table from `kbd_load_config`. --- neodos-kernel/src/input/kbd/config.rs | 17 +-------- neodos-kernel/src/input/kbd/mod.rs | 51 ++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/neodos-kernel/src/input/kbd/config.rs b/neodos-kernel/src/input/kbd/config.rs index 7c47b8cc..39e1687c 100644 --- a/neodos-kernel/src/input/kbd/config.rs +++ b/neodos-kernel/src/input/kbd/config.rs @@ -3,21 +3,6 @@ use crate::kbd::KbdConfig; const REG_PATH: &str = "\\Registry\\Machine\\System\\Keyboard"; -/// Map legacy layout names (`US`, `Spanish`, `SP`) to their locale-tag form. -fn normalize_layout_name(name: &str) -> &str { - if name.eq_ignore_ascii_case("us") || name.eq_ignore_ascii_case("en-us") { - "en-US" - } else if name.eq_ignore_ascii_case("spanish") - || name.eq_ignore_ascii_case("sp") - || name.eq_ignore_ascii_case("es") - || name.eq_ignore_ascii_case("es-es") - { - "es-ES" - } else { - name - } -} - pub fn kbd_load_config() -> KbdConfig { let mut config = KbdConfig::default(); @@ -28,7 +13,7 @@ pub fn kbd_load_config() -> KbdConfig { if let Some(vc) = crate::cm::cm_query_value(native_id, "Layout").ok() { if let Some(s) = vc.as_str() { if !s.is_empty() { - config.layout_name = normalize_layout_name(s).to_string(); + config.layout_name = s.to_string(); } } } diff --git a/neodos-kernel/src/input/kbd/mod.rs b/neodos-kernel/src/input/kbd/mod.rs index c04b05ac..bbdf04b1 100644 --- a/neodos-kernel/src/input/kbd/mod.rs +++ b/neodos-kernel/src/input/kbd/mod.rs @@ -118,6 +118,23 @@ impl NeoKbd { }).map(|i| i as u32) } + /// Resolve a configured layout name against the loaded layouts: by name, + /// then by language tag, then mapping pre-locale-tag names (`US`, + /// `Spanish`). Kept dynamic so it tracks whatever is actually loaded. + pub fn resolve_layout_index(&self, name: &str) -> Option { + if let Some(i) = self.find_layout_index(name) { + return Some(i); + } + if let Some(i) = self + .layouts + .iter() + .position(|l| l.lang_tag_str().eq_ignore_ascii_case(name)) + { + return Some(i as u32); + } + legacy_layout_alias(name).and_then(|alias| self.find_layout_index(alias)) + } + pub fn set_layout_by_name(&mut self, name: &str) -> Result<(), ()> { let idx = self.find_layout_index(name).ok_or(())?; self.state.active_layout_index = idx; @@ -319,6 +336,17 @@ lazy_static! { pub static ref KBD: Mutex = Mutex::new(NeoKbd::new()); } +/// Map a pre-locale-tag layout name to its current tag (legacy Registry values). +fn legacy_layout_alias(name: &str) -> Option<&'static str> { + if name.eq_ignore_ascii_case("us") { + Some("en-US") + } else if name.eq_ignore_ascii_case("spanish") || name.eq_ignore_ascii_case("sp") { + Some("es-ES") + } else { + None + } +} + pub fn kbd_init() { kinfo!(LogSubsys::Kbd, "Initializing Keyboard Manager (NeoKBD)..."); @@ -335,11 +363,21 @@ pub fn kbd_init() { let config = config::kbd_load_config(); let mut kbd = KBD.lock(); kbd.config = config; - if let Some(idx) = kbd.find_layout_index(&kbd.config.layout_name) { + + // Resolve the persisted layout against the layouts actually loaded, then + // persist the canonical name when it changed. This migrates legacy values + // (`US`/`Spanish`) to `en-US`/`es-ES` and normalises unknown values to the + // default, so the Registry converges instead of keeping a stale name. + if !kbd.layouts.is_empty() { + let requested = kbd.config.layout_name.clone(); + let idx = kbd.resolve_layout_index(&requested).unwrap_or(0); kbd.state.active_layout_index = idx; - } else if !kbd.layouts.is_empty() { - kbd.state.active_layout_index = 0; - kbd.config.layout_name = kbd.layouts[0].name_str().to_string(); + let canonical = kbd.layouts[idx as usize].name_str().to_string(); + let changed = canonical != requested; + kbd.config.layout_name = canonical; + if changed { + let _ = config::kbd_save_config(&kbd.config); + } } if kbd.config.numlock_on_boot { kbd.state.modifiers |= KBD_NUMLOCK; @@ -385,6 +423,11 @@ pub fn register_tests() { test_eq!(kbd.find_layout_index("EN-us"), Some(0)); test_eq!(kbd.find_layout_index("es-ES"), Some(1)); test_eq!(kbd.find_layout_index("de-DE"), None); + // Dynamic resolution: legacy aliases map onto the loaded layouts. + test_eq!(kbd.resolve_layout_index("Spanish"), Some(1)); + test_eq!(kbd.resolve_layout_index("SP"), Some(1)); + test_eq!(kbd.resolve_layout_index("US"), Some(0)); + test_eq!(kbd.resolve_layout_index("de-DE"), None); test_eq!(kbd.active_layout().map(|l| l.name_str()), Some("en-US")); test_true!(kbd.set_layout_by_name("es-ES").is_ok()); From 55cc26a6df0e1693fca6cf5f09fbc915957df7ec Mon Sep 17 00:00:00 2001 From: Alejandro Martin Perez Date: Wed, 14 Oct 2026 00:54:31 +0200 Subject: [PATCH 3/5] test(kbd): missing .kbd, repeat bounds, LEDs and shift (#394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the NeoKBD tests toward the §7 plan: a missing `.kbd` is rejected, repeat delay/rate bounds are enforced, the LED mask is clamped to 0x07 and Shift make/break toggles the modifier bit. --- neodos-kernel/src/input/kbd/mod.rs | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/neodos-kernel/src/input/kbd/mod.rs b/neodos-kernel/src/input/kbd/mod.rs index bbdf04b1..8ea1ba98 100644 --- a/neodos-kernel/src/input/kbd/mod.rs +++ b/neodos-kernel/src/input/kbd/mod.rs @@ -474,4 +474,43 @@ pub fn register_tests() { test_eq!(layout::lookup_codepoint(&en, 0x27, 0), Some(';' as u16)); test_eq!(layout::lookup_codepoint(&en, 0x27, KBD_SHIFT), Some(':' as u16)); }); + + // A missing / malformed `.kbd` is rejected rather than panicking. + test_case!("kbd_missing_layout_is_rejected", { + test_true!(layout::load_kbd("C:\\System\\Keyboard\\does-not-exist.kbd").is_err()); + }); + + // Repeat delay/rate bounds are enforced by the manager. + test_case!("kbd_repeat_bounds", { + let mut kbd = NeoKbd::new(); + test_true!(kbd.set_repeat_delay(99).is_err()); + test_true!(kbd.set_repeat_delay(100).is_ok()); + test_true!(kbd.set_repeat_delay(2000).is_ok()); + test_true!(kbd.set_repeat_delay(2001).is_err()); + test_true!(kbd.set_repeat_rate(1).is_err()); + test_true!(kbd.set_repeat_rate(2).is_ok()); + test_true!(kbd.set_repeat_rate(60).is_ok()); + test_true!(kbd.set_repeat_rate(61).is_err()); + test_eq!(kbd.config.repeat_delay, 2000); + test_eq!(kbd.config.repeat_rate, 60); + }); + + // LED state is masked to the three defined bits. + test_case!("kbd_leds_masked", { + let mut kbd = NeoKbd::new(); + kbd.set_leds(0xFF); + test_eq!(kbd.state.leds, 0x07); + kbd.set_leds(0x02); + test_eq!(kbd.state.leds, 0x02); + }); + + // Shift make/break updates the modifier state. + test_case!("kbd_shift_toggles_modifier", { + let mut kbd = NeoKbd::new(); + test_eq!(kbd.state.modifiers & KBD_SHIFT, 0); + kbd.process_scancode(0x2A, true); + test_true!(kbd.state.modifiers & KBD_SHIFT != 0); + kbd.process_scancode(0x2A, false); + test_eq!(kbd.state.modifiers & KBD_SHIFT, 0); + }); } From 661abd0e9dbc354143f8448b066d5226f8c9beef Mon Sep 17 00:00:00 2001 From: Alejandro Martin Perez Date: Wed, 14 Oct 2026 00:51:54 +0200 Subject: [PATCH 4/5] feat(tools): xkbcompile converts XKB layouts to .kbd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sourcing layouts from Microsoft `.klc` is awkward (MSKLC GUI) and the files are Microsoft-licensed. Add a host tool that reads the open, offline XKB data (`/usr/share/X11/xkb/symbols/*`) and emits the NeoDOS `.kbd` format directly. - resolves `include "file(section)"` chains, maps EVDEV -> set-1 scancodes, maps levels 1-3 to normal/shift/altgr, translates Latin-1 / `U` / common keysym names, marks dead keys and builds the compose table from the X11 `Compose` file. - verified `us`, `es`, `fr`, `de`: the `es` output matches the `.klc`-derived `es-ES` (ñ/Ñ, ´/¨, ç/Ç, dead keys, ¡/¿). Related: #390 (dynamic .kbd discovery) — new layouts still need wiring into the image allowlist and the kernel load list. --- docs/architecture/repository.md | 10 +- docs/design/neokbd-design.md | 12 +- tools/xkbcompile/Cargo.lock | 7 + tools/xkbcompile/Cargo.toml | 10 + tools/xkbcompile/README.md | 46 +++ tools/xkbcompile/src/main.rs | 691 ++++++++++++++++++++++++++++++++ 6 files changed, 771 insertions(+), 5 deletions(-) create mode 100644 tools/xkbcompile/Cargo.lock create mode 100644 tools/xkbcompile/Cargo.toml create mode 100644 tools/xkbcompile/README.md create mode 100644 tools/xkbcompile/src/main.rs diff --git a/docs/architecture/repository.md b/docs/architecture/repository.md index 68aea252..a5b67480 100644 --- a/docs/architecture/repository.md +++ b/docs/architecture/repository.md @@ -28,10 +28,11 @@ neodos/ ├── libnet/ # User-mode networking library ├── userbin/ # 52 user binaries (PID 1, shell, utilities) ├── drivers/ # 10 standalone NEM drivers -├── tools/ # 4 Rust host tools + 1 Python script +├── tools/ # 5 Rust host tools + 1 Python script │ ├── crashdump/ # Crash dump decoder │ ├── gen-hiv/ # Registry hive generator -│ ├── kbdcompile/ # Keyboard layout compiler +│ ├── kbdcompile/ # Keyboard layout compiler (.klc → .kbd) +│ ├── xkbcompile/ # Keyboard layout compiler (XKB → .kbd) │ └── nltc/ # NLT compiler ├── scripts/ # bash utilities (MCP server migrated → neodos-dev-server) ├── data/ # Locale TOML (141 files) + keyboard layouts @@ -109,7 +110,8 @@ kbdcompile (host) ───→ data/keyboard (layout sources) | `nxdump` | Host tool, reads ELF/NXE/NEM files. | | `nxeinfo` | Host tool, reads NXE headers. | | `nxpkg` | Host tool, creates/extracts NXP packages. | -| `kbdcompile` | Host tool, compiles keyboard layouts. | +| `kbdcompile` | Host tool, compiles keyboard layouts (`.klc` → `.kbd`). | +| `xkbcompile` | Host tool, converts XKB layouts → `.kbd`. | | `scripts/mcp_server` | Python, completely independent. | | `scripts/check_deps.py` | Python, standalone validation. | | `scripts/gen_*` | Python, host-side data generation. | @@ -142,7 +144,7 @@ kbdcompile (host) ───→ data/keyboard (layout sources) |-----------|-----------|-----------| | `neodev` | `NeoDev` | Fully standalone host tool. Standard Rust deps (clap, anyhow, serde). Changes at own pace. Can version independently. Build/test/image creation for any repo. | | `neodos-lsp` + `scripts/mcp_server` | `NeoDOS Dev Server` (merged) | **Migrated.** LSP + MCP merged into single Rust workspace at [neodos-dev-server](https://github.com/NeoDOS-Project/neodos-dev-server). Shared toolkit (neodos-toolkit) + LSP binary + MCP binary. | -| `nltc, kbdcompile` | `NeoToolchain` or separate | Both are host-side compilers for OS data formats. Can evolve independently of kernel version. | +| `nltc, kbdcompile, xkbcompile` | `NeoToolchain` or separate | Host-side compilers for OS data formats. Can evolve independently of kernel version. | | `nxdump, nxeinfo, nxpkg` | `NeoTools` | UX-focused analysis tools. Independent release cycle. Useful for OS analysis even without building kernel. | | `scripts/mcp_server` | `NeoMCP` | Python MCP server for AI-assisted development. Completely independent of kernel. | | `data/locale/*` | `NeoTranslations` | Translation data. Community contributions don't need kernel access. Can be translated entirely by non-developers. | diff --git a/docs/design/neokbd-design.md b/docs/design/neokbd-design.md index 3b074efb..f02c5b0d 100644 --- a/docs/design/neokbd-design.md +++ b/docs/design/neokbd-design.md @@ -909,7 +909,17 @@ Los layouts se generan desde los mismos archivos `.klc` existentes, pero convert | `US.kbd` | US | en-US | 256 | 0 | | `Spanish.kbd` | Spanish | es-ES | 256 | 54 | -La herramienta `kbdcompile` toma un `.klc` y produce un `.kbd` binario. Se invoca en el build de la imagen, no en runtime. +La herramienta `kbdcompile` toma un `.klc` y produce un `.kbd` binario. + +Alternativa abierta y offline: `tools/xkbcompile` convierte un layout XKB +(`/usr/share/X11/xkb/symbols/*`) a `.kbd` directamente, sin pasar por `.klc`: + +```text +xkbcompile es basic --name es-ES --lang es-ES --out data/keyboard/es-ES.kbd +``` + +XKB cubre muchos idiomas con licencia abierta; sus límites (un solo grupo, +niveles 1-3, keysyms exóticos) están en `tools/xkbcompile/README.md`. --- diff --git a/tools/xkbcompile/Cargo.lock b/tools/xkbcompile/Cargo.lock new file mode 100644 index 00000000..65de7452 --- /dev/null +++ b/tools/xkbcompile/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "xkbcompile" +version = "0.1.0" diff --git a/tools/xkbcompile/Cargo.toml b/tools/xkbcompile/Cargo.toml new file mode 100644 index 00000000..fe78b64d --- /dev/null +++ b/tools/xkbcompile/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xkbcompile" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "xkbcompile" +path = "src/main.rs" + +[dependencies] diff --git a/tools/xkbcompile/README.md b/tools/xkbcompile/README.md new file mode 100644 index 00000000..a4804c6e --- /dev/null +++ b/tools/xkbcompile/README.md @@ -0,0 +1,46 @@ +# xkbcompile + +Host tool: convert a **Linux XKB** layout (`/usr/share/X11/xkb/symbols/*`) into +the NeoDOS `.kbd` keyboard format. Complements `kbdcompile` (which reads the +Microsoft `.klc` format); XKB is offline and openly licensed, so it is the +preferred source for shipping many languages. + +```bash +cargo build +./target/debug/xkbcompile [variant] \ + [--name TAG] [--lang TAG] [--out FILE] \ + [--symbols-dir DIR] [--keycodes FILE] [--compose FILE] + +# Spanish (es) base variant -> data/keyboard/es-ES.kbd +./target/debug/xkbcompile es basic --name es-ES --lang es-ES \ + --out ../../data/keyboard/es-ES.kbd +``` + +Defaults: `--symbols-dir /usr/share/X11/xkb/symbols`, +`--keycodes /usr/share/X11/xkb/keycodes/evdev`, +`--compose /usr/share/X11/locale/en_US.UTF-8/Compose`, `--out +data/keyboard/.kbd`. + +## What it does + +- Resolves `include "file(section)"` chains (e.g. `es(basic)` → + `latin(type4)` → `latin(basic)`), later `key` definitions overriding earlier. +- Maps EVDEV keycodes to PC set-1 scancodes (`set1 = evdev - 8`). +- Maps levels 1-3 to the kernel's `normal`/`shift`/`altgr` columns. +- Translates keysyms: Latin-1 names, `U` Unicode keysyms and the common + punctuation/accent names. +- Marks `dead_*` keysyms (per level) and builds the compose table from the + X11 `Compose` file for the dead keys used. + +## Limits + +- Only the first keyboard group; levels 4+ (Shift+AltGr) are dropped (the + kernel format has no such column). +- Exotic keysyms (Greek, Cyrillic, ligatures, `Greek_*`, `XF86*`) are not + translated and leave the key unmapped. +- The `Ctrl` column is left unset (XKB does not define ctrl levels); the kernel + falls back to the normal/shift value. + +Adding a layout to the OS also needs wiring it into `neodev/src/image.rs` +(`kbd_layouts`) and the kernel load list (`load_layouts()`), until dynamic +`.kbd` discovery lands (#390). diff --git a/tools/xkbcompile/src/main.rs b/tools/xkbcompile/src/main.rs new file mode 100644 index 00000000..4fd99033 --- /dev/null +++ b/tools/xkbcompile/src/main.rs @@ -0,0 +1,691 @@ +//! xkbcompile — convert an XKB layout (`/usr/share/X11/xkb/symbols/*`) into +//! the NeoDOS `.kbd` format. +//! +//! Usage: +//! xkbcompile [variant] [--name TAG] [--lang TAG] [--out FILE] +//! [--symbols-dir DIR] [--keycodes FILE] [--compose FILE] +//! +//! Scope: single group, levels 1-3 (normal / shift / AltGr). It resolves +//! `include "file(section)"` chains, maps EVDEV keycodes to PC set-1 +//! scancodes, translates X11 keysyms (Latin-1 / `U` / the common names), +//! marks dead keys and, when a Compose file is given, builds the composition +//! table for the dead keysyms used. +//! +//! Known limits: only the first keyboard group; levels 4+ (Shift+AltGr) are +//! dropped; exotic keysyms (Greek, Cyrillic, ligatures) are not translated. + +use std::collections::{BTreeMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +const KBD_MAGIC: &[u8; 4] = b"KBD\0"; + +#[repr(C)] +#[derive(Clone, Copy)] +struct KeyEntry { + normal: u16, + shift: u16, + altgr: u16, + ctrl: u16, + flags: u8, + _pad: [u8; 7], +} + +impl KeyEntry { + fn empty() -> Self { + KeyEntry { normal: 0xFFFF, shift: 0xFFFF, altgr: 0xFFFF, ctrl: 0xFFFF, flags: 0, _pad: [0u8; 7] } + } +} + +struct Compose { + dead: u16, + base: u16, + result: u16, +} + +struct Args { + layout: String, + variant: Option, + name: String, + lang: String, + out: PathBuf, + symbols_dir: PathBuf, + keycodes: PathBuf, + compose: PathBuf, +} + +fn main() { + let args = match parse_args() { + Ok(a) => a, + Err(msg) => { + eprintln!("xkbcompile: {msg}"); + eprintln!( + "usage: xkbcompile [variant] [--name TAG] [--lang TAG] [--out FILE] \ + [--symbols-dir DIR] [--keycodes FILE] [--compose FILE]" + ); + std::process::exit(2); + } + }; + + let mut keymap: BTreeMap> = BTreeMap::new(); + let mut visited: HashSet<(String, String)> = HashSet::new(); + if let Err(e) = resolve(&args.symbols_dir, &args.layout, args.variant.as_deref(), &mut keymap, &mut visited) { + eprintln!("xkbcompile: {e}"); + std::process::exit(1); + } + + let keycodes = match parse_keycodes(&args.keycodes) { + Ok(m) => m, + Err(e) => { + eprintln!("xkbcompile: {}", e); + std::process::exit(1); + } + }; + + let mut entries = vec![KeyEntry::empty(); 256]; + let mut dead_labels: HashSet = HashSet::new(); + + for (kname, syms) in &keymap { + let ev = match keycodes.get(kname) { + Some(v) => *v, + None => continue, + }; + // PC set-1 scancode = EVDEV keycode - 8 for the alphanumeric block. + if ev < 9 { + continue; + } + let set1 = ev - 8; + if set1 == 0 || set1 > 0x58 { + continue; + } + let mut e = KeyEntry::empty(); + for (level, sym) in syms.iter().enumerate() { + if sym.is_empty() || sym == "NoSymbol" || sym == "VoidSymbol" { + continue; + } + let (cp, dead) = match symbol_to_char(sym) { + Some(v) => v, + None => continue, + }; + if dead { + dead_labels.insert(cp); + } + match level { + 0 => { e.normal = cp; if dead { e.flags |= 0x01; } } + 1 => { e.shift = cp; if dead { e.flags |= 0x02; } } + 2 => { e.altgr = cp; if dead { e.flags |= 0x04; } } + _ => {} // level 4+ (Shift+AltGr): no kernel column + } + } + entries[set1 as usize] = e; + } + + let compose = if args.compose.exists() { + parse_compose(&args.compose, &dead_labels) + } else { + Vec::new() + }; + + let bytes = write_kbd(&args.name, &args.lang, &entries, &compose); + if let Err(e) = fs::write(&args.out, &bytes) { + eprintln!("xkbcompile: cannot write {}: {e}", args.out.display()); + std::process::exit(1); + } + let mapped = entries.iter().filter(|e| e.normal != 0xFFFF).count(); + eprintln!( + "xkbcompile: wrote {} ({} keys, {} compose) [{}]", + args.out.display(), mapped, compose.len(), args.name + ); +} + +fn parse_args() -> Result { + let mut pos: Vec = Vec::new(); + let mut name = None; + let mut lang = None; + let mut out = None; + let mut symbols_dir = PathBuf::from("/usr/share/X11/xkb/symbols"); + let mut keycodes = PathBuf::from("/usr/share/X11/xkb/keycodes/evdev"); + let mut compose = PathBuf::from("/usr/share/X11/locale/en_US.UTF-8/Compose"); + + let mut it = std::env::args().skip(1); + while let Some(a) = it.next() { + match a.as_str() { + "--name" => name = it.next(), + "--lang" => lang = it.next(), + "--out" => out = it.next().map(PathBuf::from), + "--symbols-dir" => symbols_dir = it.next().map(PathBuf::from).ok_or("--symbols-dir needs a value")?, + "--keycodes" => keycodes = it.next().map(PathBuf::from).ok_or("--keycodes needs a value")?, + "--compose" => compose = it.next().map(PathBuf::from).ok_or("--compose needs a value")?, + "-h" | "--help" => return Err("help".into()), + other if other.starts_with("--") => return Err(format!("unknown option {other}")), + other => pos.push(other.to_string()), + } + } + + if pos.is_empty() { + return Err("missing ".into()); + } + let layout = pos[0].clone(); + let variant = pos.get(1).cloned(); + let tag = variant.as_ref().map(|v| format!("{layout}-{v}")).unwrap_or_else(|| layout.clone()); + let name = name.unwrap_or_else(|| tag.clone()); + let lang = lang.unwrap_or(tag); + let out = out.unwrap_or_else(|| PathBuf::from(format!("data/keyboard/{name}.kbd"))); + + Ok(Args { layout, variant, name, lang, out, symbols_dir, keycodes, compose }) +} + +// ── XKB symbols parsing ───────────────────────────────────────────── + +/// Resolve a `xkb_symbols` section, expanding `include` chains in order so +/// that later `key` definitions override earlier (included) ones. +fn resolve( + dir: &Path, + file: &str, + section: Option<&str>, + keymap: &mut BTreeMap>, + visited: &mut HashSet<(String, String)>, +) -> Result<(), String> { + let key = (file.to_string(), section.unwrap_or_default().to_string()); + if !visited.insert(key) { + return Ok(()); // cycle guard + } + + let path = dir.join(file); + let text = fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?; + let body = find_section(&text, section).ok_or_else(|| { + format!("{}: section '{}' not found", path.display(), section.unwrap_or("")) + })?; + + for stmt in split_statements(&body) { + let s = stmt.trim(); + if s.is_empty() { + continue; + } + if let Some(rest) = s.strip_prefix("include") { + if let Some(spec) = quoted(rest) { + let (f, sec) = parse_include(&spec); + resolve(dir, &f, sec.as_deref(), keymap, visited)?; + } + } else if s.starts_with("key") { + if let Some((kname, syms)) = parse_key(s) { + keymap.insert(kname, syms); + } + } + } + Ok(()) +} + +/// Return the body of `xkb_symbols "name" { ... }`. With `None`, the default +/// (or first) section is used. +fn find_section(text: &str, name: Option<&str>) -> Option { + let mut idx = 0usize; + let mut first: Option = None; + let mut default: Option = None; + + while let Some(rel) = text[idx..].find("xkb_symbols") { + let start = idx + rel; + let after = &text[start + "xkb_symbols".len()..]; + let sname = quoted(after).unwrap_or_default(); + // `default` attribute lives just before `xkb_symbols`. + let prefix_start = text[..start].rfind('}').map(|p| p + 1).unwrap_or(0); + let is_default = text[prefix_start..start].contains("default"); + + let brace = match after.find('{') { + Some(b) => b, + None => break, + }; + let body_start = start + "xkb_symbols".len() + brace + 1; + let body = match balanced_braces(&text[body_start..]) { + Some(b) => b, + None => break, + }; + + if let Some(n) = name { + if sname == n { + return Some(body); + } + } else { + if first.is_none() { + first = Some(body.clone()); + } + if is_default && default.is_none() { + default = Some(body.clone()); + } + } + idx = body_start + body.len() + 1; + } + + if name.is_none() { + default.or(first) + } else { + None + } +} + +/// Split a section body into `;`-terminated statements (respecting quotes and +/// brackets/braces). +fn split_statements(body: &str) -> Vec { + let mut out = Vec::new(); + let mut cur = String::new(); + let mut in_quote = false; + let mut depth = 0i32; + for c in body.chars() { + match c { + '"' => { in_quote = !in_quote; cur.push(c); } + '[' | '{' => { depth += 1; cur.push(c); } + ']' | '}' => { depth -= 1; cur.push(c); } + ';' if !in_quote && depth <= 0 => { out.push(std::mem::take(&mut cur)); } + _ => cur.push(c), + } + } + if !cur.trim().is_empty() { + out.push(cur); + } + out +} + +fn parse_key(stmt: &str) -> Option<(String, Vec)> { + let lt = stmt.find('<')?; + let gt = stmt[lt..].find('>')? + lt; + let name = stmt[lt + 1..gt].trim().to_string(); + + // Find the keysym list: the first `[` group not preceded by `type`/`symbols`. + let bytes = stmt.as_bytes(); + let mut i = 0; + while let Some(rel) = stmt[i..].find('[') { + let p = i + rel; + // preceding word + let mut w = p; + while w > 0 && (bytes[w - 1].is_ascii_alphanumeric() || bytes[w - 1] == b'_') { + w -= 1; + } + let word = &stmt[w..p]; + if word == "type" || word == "symbols" { + i = p + 1; + continue; + } + let close = stmt[p..].find(']')? + p; + let inner = &stmt[p + 1..close]; + let syms: Vec = inner + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + return Some((name, syms)); + } + Some((name, Vec::new())) +} + +/// Parse the first quoted string in `s`. +fn quoted(s: &str) -> Option { + let start = s.find('"')? + 1; + let end = s[start..].find('"')? + start; + Some(s[start..end].to_string()) +} + +/// Parse `file` or `file(section)` into its parts. +fn parse_include(spec: &str) -> (String, Option) { + match spec.find('(') { + Some(open) => { + let file = spec[..open].trim().to_string(); + let section = spec[open + 1..].trim_end_matches(')').trim().to_string(); + (file, Some(section)) + } + None => (spec.trim().to_string(), None), + } +} + +/// Body of a `{ ... }` block (the input must start right after the `{`). +fn balanced_braces(s: &str) -> Option { + let mut depth = 1i32; + for (i, c) in s.char_indices() { + match c { + '{' => depth += 1, + '}' => { + depth -= 1; + if depth == 0 { + return Some(s[..i].to_string()); + } + } + _ => {} + } + } + None +} + +// ── keycodes ──────────────────────────────────────────────────────── + +fn parse_keycodes(path: &Path) -> Result, String> { + let text = fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?; + let mut map = BTreeMap::new(); + for line in text.lines() { + let line = line.trim(); + if line.starts_with("//") || line.starts_with("alias") { + continue; + } + // = 24; + if let Some(lt) = line.find('<') { + if let Some(gt) = line[lt..].find('>') { + let name = line[lt + 1..lt + gt].trim().to_string(); + if let Some(eq) = line.find('=') { + let num: String = line[eq + 1..] + .trim_start() + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect(); + if let Ok(v) = num.parse::() { + map.insert(name, v); + } + } + } + } + } + Ok(map) +} + +// ── keysyms ───────────────────────────────────────────────────────── + +/// Map an XKB keysym name to `(codepoint, is_dead)`. +fn symbol_to_char(name: &str) -> Option<(u16, bool)> { + let name = name.trim(); + if name.is_empty() || name == "NoSymbol" || name == "VoidSymbol" { + return None; + } + if let Some(rest) = name.strip_prefix("dead_") { + return dead_label(rest).map(|cp| (cp, true)); + } + // Unicode keysym `U` (4-6 hex digits). + if let Some(hex) = name.strip_prefix('U') { + if hex.len() >= 4 && hex.len() <= 6 && hex.chars().all(|c| c.is_ascii_hexdigit()) { + if let Ok(v) = u32::from_str_radix(hex, 16) { + if let Ok(cp) = u16::try_from(v) { + return Some((cp, false)); + } + } + } + } + if name.chars().count() == 1 { + let c = name.chars().next().unwrap(); + if (c as u32) < 0x80 { + return Some((c as u16, false)); + } + } + named_keysym(name).map(|cp| (cp, false)) +} + +/// Spacing codepoint used as the compose key for each dead keysym (matches the +/// convention of the `.klc`-derived layouts). +fn dead_label(rest: &str) -> Option { + Some(match rest { + "grave" => 0x0060, + "acute" => 0x00B4, + "circumflex" => 0x005E, + "tilde" => 0x007E, + "diaeresis" => 0x00A8, + "macron" => 0x00AF, + "breve" => 0x02D8, + "abovedot" => 0x02D9, + "abovering" => 0x02DA, + "doubleacute" => 0x02DD, + "caron" => 0x02C7, + "cedilla" => 0x00B8, + "ogonek" => 0x02DB, + "belowdot" => 0x0323, + "hook" => 0x0309, + "horn" => 0x031B, + "stroke" => 0x0338, + "belowcomma" => 0x0326, + "iota" => 0x0345, + "voiced_sound" => 0x3099, + "semivoiced_sound" => 0x309A, + _ => return None, + }) +} + +fn named_keysym(name: &str) -> Option { + Some(match name { + "space" => 0x20, + "exclam" => 0x21, + "quotedbl" => 0x22, + "numbersign" => 0x23, + "dollar" => 0x24, + "percent" => 0x25, + "ampersand" => 0x26, + "apostrophe" | "quoteright" => 0x27, + "parenleft" => 0x28, + "parenright" => 0x29, + "asterisk" => 0x2A, + "plus" => 0x2B, + "comma" => 0x2C, + "minus" => 0x2D, + "period" => 0x2E, + "slash" => 0x2F, + "0" => 0x30, + "1" => 0x31, + "2" => 0x32, + "3" => 0x33, + "4" => 0x34, + "5" => 0x35, + "6" => 0x36, + "7" => 0x37, + "8" => 0x38, + "9" => 0x39, + "colon" => 0x3A, + "semicolon" => 0x3B, + "less" => 0x3C, + "equal" => 0x3D, + "greater" => 0x3E, + "question" => 0x3F, + "at" => 0x40, + "bracketleft" => 0x5B, + "backslash" => 0x5C, + "bracketright" => 0x5D, + "asciicircum" => 0x5E, + "underscore" => 0x5F, + "grave" | "quoteleft" => 0x60, + "braceleft" => 0x7B, + "bar" => 0x7C, + "braceright" => 0x7D, + "asciitilde" => 0x7E, + // Latin-1 supplement + "nobreakspace" => 0xA0, + "exclamdown" => 0xA1, + "cent" => 0xA2, + "sterling" => 0xA3, + "currency" => 0xA4, + "yen" => 0xA5, + "brokenbar" => 0xA6, + "section" => 0xA7, + "diaeresis" => 0xA8, + "copyright" => 0xA9, + "ordfeminine" => 0xAA, + "guillemotleft" => 0xAB, + "notsign" => 0xAC, + "hyphen" => 0xAD, + "registered" => 0xAE, + "macron" => 0xAF, + "degree" => 0xB0, + "plusminus" => 0xB1, + "twosuperior" => 0xB2, + "threesuperior" => 0xB3, + "acute" => 0xB4, + "mu" => 0xB5, + "paragraph" => 0xB6, + "periodcentered" => 0xB7, + "cedilla" => 0xB8, + "onesuperior" => 0xB9, + "masculine" => 0xBA, + "guillemotright" => 0xBB, + "onequarter" => 0xBC, + "onehalf" => 0xBD, + "threequarters" => 0xBE, + "questiondown" => 0xBF, + "Agrave" => 0xC0, + "Aacute" => 0xC1, + "Acircumflex" => 0xC2, + "Atilde" => 0xC3, + "Adiaeresis" => 0xC4, + "Aring" => 0xC5, + "AE" => 0xC6, + "Ccedilla" => 0xC7, + "Egrave" => 0xC8, + "Eacute" => 0xC9, + "Ecircumflex" => 0xCA, + "Ediaeresis" => 0xCB, + "Igrave" => 0xCC, + "Iacute" => 0xCD, + "Icircumflex" => 0xCE, + "Idiaeresis" => 0xCF, + "ETH" => 0xD0, + "Ntilde" => 0xD1, + "Ograve" => 0xD2, + "Oacute" => 0xD3, + "Ocircumflex" => 0xD4, + "Otilde" => 0xD5, + "Odiaeresis" => 0xD6, + "multiply" => 0xD7, + "Oslash" => 0xD8, + "Ugrave" => 0xD9, + "Uacute" => 0xDA, + "Ucircumflex" => 0xDB, + "Udiaeresis" => 0xDC, + "Yacute" => 0xDD, + "THORN" => 0xDE, + "ssharp" => 0xDF, + "agrave" => 0xE0, + "aacute" => 0xE1, + "acircumflex" => 0xE2, + "atilde" => 0xE3, + "adiaeresis" => 0xE4, + "aring" => 0xE5, + "ae" => 0xE6, + "ccedilla" => 0xE7, + "egrave" => 0xE8, + "eacute" => 0xE9, + "ecircumflex" => 0xEA, + "ediaeresis" => 0xEB, + "igrave" => 0xEC, + "iacute" => 0xED, + "icircumflex" => 0xEE, + "idiaeresis" => 0xEF, + "eth" => 0xF0, + "ntilde" => 0xF1, + "ograve" => 0xF2, + "oacute" => 0xF3, + "ocircumflex" => 0xF4, + "otilde" => 0xF5, + "odiaeresis" => 0xF6, + "division" => 0xF7, + "oslash" => 0xF8, + "ugrave" => 0xF9, + "uacute" => 0xFA, + "ucircumflex" => 0xFB, + "udiaeresis" => 0xFC, + "yacute" => 0xFD, + "thorn" => 0xFE, + "ydiaeresis" => 0xFF, + // Common extras + "EuroSign" => 0x20AC, + "oe" => 0x0153, + "OE" => 0x0152, + "leftsinglequotemark" => 0x2018, + "rightsinglequotemark" => 0x2019, + "leftdoublequotemark" => 0x201C, + "rightdoublequotemark" => 0x201D, + "endash" => 0x2013, + "emdash" => 0x2014, + "ellipsis" => 0x2026, + "trademark" => 0x2122, + "bullet" => 0x2022, + _ => return None, + }) +} + +// ── Compose ───────────────────────────────────────────────────────── + +fn parse_compose(path: &Path, dead_labels: &HashSet) -> Vec { + let text = match fs::read_to_string(path) { + Ok(t) => t, + Err(_) => return Vec::new(), + }; + let mut out = Vec::new(); + for line in text.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with("include") { + continue; + } + let colon = match line.find(':') { + Some(c) => c, + None => continue, + }; + let (keys_part, rest) = line.split_at(colon); + let keys: Vec<&str> = keys_part + .split('<') + .skip(1) + .filter_map(|s| s.split('>').next()) + .collect(); + if keys.len() != 2 { + continue; // only two-key sequences + } + let (d_cp, d_dead) = match symbol_to_char(keys[0]) { + Some(v) => v, + None => continue, + }; + if !d_dead || !dead_labels.contains(&d_cp) { + continue; + } + let base = match symbol_to_char(keys[1]) { + Some((cp, false)) => cp, + _ => continue, + }; + let result = match compose_result(rest) { + Some(cp) => cp, + None => continue, + }; + out.push(Compose { dead: d_cp, base, result }); + } + out +} + +/// First Unicode codepoint of the `"..."` string after the `:`. +fn compose_result(rest: &str) -> Option { + let q = rest.find('"')? + 1; + let end = rest[q..].find('"')? + q; + rest[q..end].chars().next().map(|c| c as u16) +} + +// ── .kbd writer ───────────────────────────────────────────────────── + +fn write_kbd(name: &str, lang: &str, entries: &[KeyEntry], compose: &[Compose]) -> Vec { + let mut out = Vec::with_capacity(60 + 256 * 16 + 4 + compose.len() * 6); + out.extend_from_slice(KBD_MAGIC); + out.extend_from_slice(&1u32.to_le_bytes()); + + let mut name_bytes = [0u8; 32]; + let nlen = name.len().min(31); + name_bytes[..nlen].copy_from_slice(&name.as_bytes()[..nlen]); + out.extend_from_slice(&name_bytes); + + let mut tag_bytes = [0u8; 16]; + let tlen = lang.len().min(15); + tag_bytes[..tlen].copy_from_slice(&lang.as_bytes()[..tlen]); + out.extend_from_slice(&tag_bytes); + + out.extend_from_slice(&256u32.to_le_bytes()); + for e in entries { + out.extend_from_slice(&e.normal.to_le_bytes()); + out.extend_from_slice(&e.shift.to_le_bytes()); + out.extend_from_slice(&e.altgr.to_le_bytes()); + out.extend_from_slice(&e.ctrl.to_le_bytes()); + out.push(e.flags); + out.extend_from_slice(&e._pad); + } + out.extend_from_slice(&(compose.len() as u32).to_le_bytes()); + for c in compose { + out.extend_from_slice(&c.dead.to_le_bytes()); + out.extend_from_slice(&c.base.to_le_bytes()); + out.extend_from_slice(&c.result.to_le_bytes()); + } + out +} From ae0912e0863db5f8e93c6532cf517ef8a177a5dc Mon Sep 17 00:00:00 2001 From: Alejandro Martin Perez Date: Wed, 14 Oct 2026 01:15:45 +0200 Subject: [PATCH 5/5] feat(kbd): generate layouts from XKB, drop the .klc sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the open, offline XKB data the source of truth for keyboard layouts and remove the intermediate `.klc` files. - xkbcompile: strip `//` comments before splitting statements (an `include` preceded by a comment was being dropped — broke no/pl/br), add Latin-2 keysyms, and use it to generate 15 locales (en-US … cs-CZ; es-ES via `es(deadtilde)` so dead tilde is AltGr+4). - data/keyboard: remove en-US.klc/es-ES.klc; add the generated .kbd. - scripts/gen + check for all locales, wired into a CI job. - kernel: load_kbd sizes the buffer from the inode instead of a fixed 8 KiB cap; only en-US/es-ES are enabled at boot for now — loading all fifteen stresses the page cache and triggers #709. --- .github/workflows/ci.yml | 15 ++ data/keyboard/ca-ES.kbd | Bin 0 -> 8060 bytes data/keyboard/cs-CZ.kbd | Bin 0 -> 7358 bytes data/keyboard/da-DK.kbd | Bin 0 -> 7616 bytes data/keyboard/de-DE.kbd | Bin 0 -> 7094 bytes data/keyboard/en-US.kbd | Bin 4160 -> 4160 bytes data/keyboard/en-US.klc | 145 ----------------- data/keyboard/es-ES.kbd | Bin 4484 -> 8060 bytes data/keyboard/es-ES.klc | 219 -------------------------- data/keyboard/fi-FI.kbd | Bin 0 -> 8360 bytes data/keyboard/fr-FR.kbd | Bin 0 -> 7646 bytes data/keyboard/it-IT.kbd | Bin 0 -> 7040 bytes data/keyboard/nb-NO.kbd | Bin 0 -> 6998 bytes data/keyboard/nl-NL.kbd | Bin 0 -> 7292 bytes data/keyboard/pl-PL.kbd | Bin 0 -> 7928 bytes data/keyboard/pt-BR.kbd | Bin 0 -> 7832 bytes data/keyboard/pt-PT.kbd | Bin 0 -> 8060 bytes data/keyboard/sv-SE.kbd | Bin 0 -> 7736 bytes docs/design/neokbd-design.md | 22 ++- neodos-kernel/src/input/kbd/layout.rs | 25 ++- neodos-kernel/src/input/kbd/mod.rs | 2 +- scripts/check-keyboard-layouts.sh | 68 ++++++++ scripts/gen-keyboard-layouts.sh | 37 +++++ tools/xkbcompile/src/main.rs | 56 +++++++ 24 files changed, 203 insertions(+), 386 deletions(-) create mode 100644 data/keyboard/ca-ES.kbd create mode 100644 data/keyboard/cs-CZ.kbd create mode 100644 data/keyboard/da-DK.kbd create mode 100644 data/keyboard/de-DE.kbd delete mode 100644 data/keyboard/en-US.klc delete mode 100644 data/keyboard/es-ES.klc create mode 100644 data/keyboard/fi-FI.kbd create mode 100644 data/keyboard/fr-FR.kbd create mode 100644 data/keyboard/it-IT.kbd create mode 100644 data/keyboard/nb-NO.kbd create mode 100644 data/keyboard/nl-NL.kbd create mode 100644 data/keyboard/pl-PL.kbd create mode 100644 data/keyboard/pt-BR.kbd create mode 100644 data/keyboard/pt-PT.kbd create mode 100644 data/keyboard/sv-SE.kbd create mode 100755 scripts/check-keyboard-layouts.sh create mode 100755 scripts/gen-keyboard-layouts.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6475b9b..e36f2aee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,3 +95,18 @@ jobs: run: ./scripts/check-skills.sh - name: scripts/check-irq-locks.sh (INV-3 / Rule 3.5.1) run: ./scripts/check-irq-locks.sh + + keyboard-layouts: + name: Keyboard layouts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # xkbcompile is a host-side std tool; the layouts come from xkb-data. + - uses: dtolnay/rust-toolchain@stable + + - name: Install xkb-data + run: sudo apt-get update && sudo apt-get install -y xkb-data + + - name: scripts/check-keyboard-layouts.sh (#702) + run: ./scripts/check-keyboard-layouts.sh diff --git a/data/keyboard/ca-ES.kbd b/data/keyboard/ca-ES.kbd new file mode 100644 index 0000000000000000000000000000000000000000..bccf5fef7027751ddf39358bfbae2ef411151894 GIT binary patch literal 8060 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 I_bv1I3n2L|3;+NC literal 0 HcmV?d00001 diff --git a/data/keyboard/cs-CZ.kbd b/data/keyboard/cs-CZ.kbd new file mode 100644 index 0000000000000000000000000000000000000000..3148a1847cada2b1fcfcea205d894e9b8fcaa6ac GIT binary patch literal 7358 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 GmHz^@mpIk{ literal 0 HcmV?d00001 diff --git a/data/keyboard/en-US.kbd b/data/keyboard/en-US.kbd index c420d9083e951422ec3db304fa0aaf5444bc45c8..28be6352054fda19d3538821ed12103d99bfd788 100644 GIT binary patch delta 51 zcmX@0a6n}2;a2>$_ diff --git a/data/keyboard/en-US.klc b/data/keyboard/en-US.klc deleted file mode 100644 index c1e2ea20..00000000 --- a/data/keyboard/en-US.klc +++ /dev/null @@ -1,145 +0,0 @@ -LOCALEID "00000409" - -SHIFTSTATE - -0 // normal -1 // shift -2 // ctrl -3 // shift+ctrl - -LAYOUT - -//SC VK_ Cap 0 1 2 3 -//-- --- ---- ---- ---- ---- ---- - -02 1 0 1 0021 -03 2 0 2 0040 -1 0000 -04 3 0 3 0023 -05 4 0 4 0024 -06 5 0 5 0025 -07 6 0 6 005e -1 001e -08 7 0 7 0026 -09 8 0 8 002a -0a 9 0 9 0028 -0b 0 0 0 0029 -0c OEM_MINUS 0 002d 005f -1 001f -0d OEM_PLUS 0 003d 002b -10 Q 1 q Q -11 W 1 w W -12 E 1 e E -13 R 1 r R -14 T 1 t T -15 Y 1 y Y -16 U 1 u U -17 I 1 i I -18 O 1 o O -19 P 1 p P -1a OEM_4 0 005b 007b 001b -1b OEM_6 0 005d 007d 001d -1e A 1 a A -1f S 1 s S -20 D 1 d D -21 F 1 f F -22 G 1 g G -23 H 1 h H -24 J 1 j J -25 K 1 k K -26 L 1 l L -27 OEM_1 0 003b 003a -28 OEM_7 0 0027 0022 -29 OEM_3 0 0060 007e -2b OEM_5 0 005c 007c 001c -2c Z 1 z Z -2d X 1 x X -2e C 1 c C -2f V 1 v V -30 B 1 b B -31 N 1 n N -32 M 1 m M -33 OEM_COMMA 0 002c 003c -34 OEM_PERIOD 0 002e 003e -35 OEM_2 0 002f 003f -39 SPACE 0 0020 0020 0020 -53 DECIMAL 0 002e 002e -56 OEM_102 0 005c 007c 001c - -KEYNAME - -01 Esc -0e Backspace -0f Tab -1c Enter -1d Ctrl -2a Shift -36 "Right Shift" -37 "Num *" -38 Alt -39 Space -3a "Caps Lock" -3b F1 -3c F2 -3d F3 -3e F4 -3f F5 -40 F6 -41 F7 -42 F8 -43 F9 -44 F10 -45 Pause -46 "Scroll Lock" -47 "Num 7" -48 "Num 8" -49 "Num 9" -4a "Num -" -4b "Num 4" -4c "Num 5" -4d "Num 6" -4e "Num +" -4f "Num 1" -50 "Num 2" -51 "Num 3" -52 "Num 0" -53 "Num Del" -54 "Sys Req" -57 F11 -58 F12 -7c F13 -7d F14 -7e F15 -7f F16 -80 F17 -81 F18 -82 F19 -83 F20 -84 F21 -85 F22 -86 F23 -87 F24 - -KEYNAME_EXT - -1c "Num Enter" -1d "Right Ctrl" -35 "Num /" -37 "Prnt Scrn" -38 "Right Alt" -45 "Num Lock" -46 Break -47 Home -48 Up -49 "Page Up" -4b Left -4d Right -4f End -50 Down -51 "Page Down" -52 Insert -53 Delete -54 <00> -56 Help -5b "Left Windows" -5c "Right Windows" -5d Application - -ENDKBD \ No newline at end of file diff --git a/data/keyboard/es-ES.kbd b/data/keyboard/es-ES.kbd index 62db261a2d194a1a268d9ab61b1afe22adfeae42..0ee2033299245187ebc2909d175212c83ade72ff 100644 GIT binary patch literal 8060 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 Kl`8iw3-}8joGW<% literal 4484 zcmeH~*J~6}6o)_2|3Zba*Q`DEGBKmB(X1L*6H83gQP+}DW{AqUFJtGoClBXHC-}3D__i*o-yBD^itehTz-&o>R>GN;TA8}1;&p+Xu zsE?r^2b2}Y$I_o!MLv!r3@GyP45UfkD?ecZgJ^LI^Gg{_OHuwrM#&2CNeneUnPJ99 zGu`+QW*R@lG2_wwM12Q~P5vI18~5?d`ggL##CNgOc#az5S<=QcB#j58jBjU=@j7Z* zNpE`Q`#FN)tfp`HI)3FA_bImS0K*Z3yp8Q;u&<6EdOzLf>W z+qi1Hm1g$qn{av!*Y zi2EJ)`)3Eb>;8$)TYTQ)^A`W!;`^qf1Mz+MfA?Xj+E_0xF9CEBBr<{rnx6N8cnM6SZtt84EQLzoDwVbKekEvH$_L|2jl0MqW}N^ diff --git a/data/keyboard/es-ES.klc b/data/keyboard/es-ES.klc deleted file mode 100644 index 2547b86f..00000000 --- a/data/keyboard/es-ES.klc +++ /dev/null @@ -1,219 +0,0 @@ -KBD KBDSP "Spanish" - -LOCALENAME "es-ES" - -LOCALEID "0000040a" - -VERSION 1.0 - -ATTRIBUTES -ALTGR - -SHIFTSTATE - -0 // normal -1 // shift -2 // ctrl -6 // altgr -3 // shift+ctrl - -LAYOUT - -//SC VK_ Cap 0 1 2 6 3 -//-- --- ---- ---- ---- ---- ---- ---- - -02 1 0 1 0021 -1 007c -03 2 0 2 0022 -1 0040 -04 3 0 3 00b7 -1 0023 -05 4 0 4 0024 -1 007e@ -06 5 0 5 0025 -1 20ac -07 6 0 6 0026 -1 00ac 001e -08 7 0 7 002f -09 8 0 8 0028 -0a 9 0 9 0029 -0b 0 0 0 003d -0c OEM_4 0 0027 003f -0d OEM_6 0 00a1 00bf -10 Q 1 q Q -11 W 1 w W -12 E 1 e E -1 20ac -13 R 1 r R -14 T 1 t T -15 Y 1 y Y -16 U 1 u U -17 I 1 i I -18 O 1 o O -19 P 1 p P -1a OEM_1 0 0060@ 005e@ 001b 005b -1b OEM_PLUS 0 002b 002a 001d 005d -1e A 1 a A -1f S 1 s S -20 D 1 d D -21 F 1 f F -22 G 1 g G -23 H 1 h H -24 J 1 j J -25 K 1 k K -26 L 1 l L -27 OEM_3 1 00f1 00d1 -28 OEM_7 0 00b4@ 00a8@ -1 007b -29 OEM_5 0 00ba 00aa -1 005c -2b OEM_2 1 00e7 00c7 001c 007d -2c Z 1 z Z -2d X 1 x X -2e C 1 c C -2f V 1 v V -30 B 1 b B -31 N 1 n N -32 M 1 m M -33 OEM_COMMA 0 002c 003b -34 OEM_PERIOD 0 002e 003a -35 OEM_MINUS 0 002d 005f -39 SPACE 0 0020 0020 0020 -53 DECIMAL 0 002e 002e -56 OEM_102 0 003c 003e 001c - -DEADKEY 0060 // grave - -0061 00e0 // a -> à -0065 00e8 // e -> è -0069 00ec // i -> ì -006f 00f2 // o -> ò -0075 00f9 // u -> ù -0041 00c0 // A -> À -0045 00c8 // E -> È -0049 00cc // I -> Ì -004f 00d2 // O -> Ò -0055 00d9 // U -> Ù -0020 0060 // space -> ` - -DEADKEY 005e // circumflex - -0061 00e2 // a -> â -0065 00ea // e -> ê -0069 00ee // i -> î -006f 00f4 // o -> ô -0075 00fb // u -> û -0041 00c2 // A -> Â -0045 00ca // E -> Ê -0049 00ce // I -> Î -004f 00d4 // O -> Ô -0055 00db // U -> Û -0020 005e // space -> ^ - -DEADKEY 00b4 // acute - -0061 00e1 // a -> á -0065 00e9 // e -> é -0069 00ed // i -> í -006f 00f3 // o -> ó -0075 00fa // u -> ú -0079 00fd // y -> ý -0041 00c1 // A -> Á -0045 00c9 // E -> É -0049 00cd // I -> Í -004f 00d3 // O -> Ó -0055 00da // U -> Ú -0059 00dd // Y -> Ý -0020 00b4 // space -> ´ - -DEADKEY 00a8 // diaeresis - -0061 00e4 // a -> ä -0065 00eb // e -> ë -0069 00ef // i -> ï -006f 00f6 // o -> ö -0075 00fc // u -> ü -0079 00ff // y -> ÿ -0041 00c4 // A -> Ä -0045 00cb // E -> Ë -0049 00cf // I -> Ï -004f 00d6 // O -> Ö -0055 00dc // U -> Ü -0020 00a8 // space -> ¨ - -DEADKEY 007e // tilde - -0061 00e3 // a -> ã -006e 00f1 // n -> ñ -006f 00f5 // o -> õ -0041 00c3 // A -> Ã -004e 00d1 // N -> Ñ -004f 00d5 // O -> Õ -0020 007e // space -> ~ - -KEYNAME - -01 ESC -0e RETROCESO -0f TABULACION -1c ENTRAR -1d CTRL -2a MAYUSCULAS -36 "MAYUSCULAS DERECHA" -37 "TECLA DE MULTIPLICACION" -38 ALT -39 "BARRA ESPACIADORA" -3a "BLOQ MAYUS" -3b F1 -3c F2 -3d F3 -3e F4 -3f F5 -40 F6 -41 F7 -42 F8 -43 F9 -44 F10 -45 PAUSA -46 "BLOQ DESPL" -47 "NUMERO 7" -48 "NUMERO 8" -49 "NUMERO 9" -4a "TECLA DE SUSTRACCION" -4b "NUMERO 4" -4c "NUMERO 5" -4d "NUMERO 6" -4e "TECLA DE ADICION" -4f "NUMERO 1" -50 "NUMERO 2" -51 "NUMERO 3" -52 "NUMERO 0" -53 "TECLA DECIMAL" -57 F11 -58 F12 - -KEYNAME_EXT - -1c INTRO -1d "CTRL DERECHA" -35 "TECLA DE DIVISION" -37 "IMP PANT" -38 "ALT DERECHA" -45 "BLOQ NUM" -46 INTERRUMPIR -47 INICIO -48 "FLECHA ARRIBA" -49 "RE PAG" -4b "FLECHA IZQUIERDA" -4d "FLECHA DERECHA" -4f FIN -50 "FLECHA ABAJO" -51 "AV PAG" -52 INSERT -53 SUPR -54 <00> -56 AYUDA -5b "WINDOWS IZQUIERDA" -5c "WINDOWS DERECHA" -5d APLICACION - -KEYNAME_DEAD - -00b4 AGUDO -0060 GRAVE -005e CIRCUNFLEJO -00a8 DIERERIS -007e TILDE - -ENDKBD \ No newline at end of file diff --git a/data/keyboard/fi-FI.kbd b/data/keyboard/fi-FI.kbd new file mode 100644 index 0000000000000000000000000000000000000000..e3edbe05f2d287d589d4e6c7a432f2a018e8bc31 GIT binary patch literal 8360 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 jEi;!I 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 literal 0 HcmV?d00001 diff --git a/data/keyboard/pt-BR.kbd b/data/keyboard/pt-BR.kbd new file mode 100644 index 0000000000000000000000000000000000000000..2e2e0e952153410b1a6722f373265d2ffd969b90 GIT binary patch literal 7832 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 literal 0 HcmV?d00001 diff --git a/docs/design/neokbd-design.md b/docs/design/neokbd-design.md index f02c5b0d..56e6e42b 100644 --- a/docs/design/neokbd-design.md +++ b/docs/design/neokbd-design.md @@ -902,21 +902,17 @@ object::namespace::ob_insert_object("\\Device\\Keyboard", kbd_id); ## Appendix A: Archivos .kbd soportados inicialmente -Los layouts se generan desde los mismos archivos `.klc` existentes, pero convertidos a formato `.kbd` con una herramienta (`tools/kbdcompile/`): +Los layouts se generan con `tools/xkbcompile` desde los XKB del sistema +(`/usr/share/X11/xkb/symbols/*`), sin ficheros `.klc` intermedios: -| Archivo | Layout | LangTag | Scancodes | Compose | -| --- | --- | --- | --- | --- | -| `US.kbd` | US | en-US | 256 | 0 | -| `Spanish.kbd` | Spanish | es-ES | 256 | 54 | - -La herramienta `kbdcompile` toma un `.klc` y produce un `.kbd` binario. - -Alternativa abierta y offline: `tools/xkbcompile` convierte un layout XKB -(`/usr/share/X11/xkb/symbols/*`) a `.kbd` directamente, sin pasar por `.klc`: +| Archivo | Layout XKB | Nombre | LangTag | Teclas | Compose | +| --- | --- | --- | --- | --- | --- | +| `en-US.kbd` | `us(basic)` | en-US | en-US | 47 | 0 | +| `es-ES.kbd` | `es(deadtilde)` | es-ES | es-ES | 46 | 634 | -```text -xkbcompile es basic --name es-ES --lang es-ES --out data/keyboard/es-ES.kbd -``` +`scripts/gen-keyboard-layouts.sh` los regenera y +`scripts/check-keyboard-layouts.sh` verifica en CI que no estén vacíos ni +desincronizados. XKB cubre muchos idiomas con licencia abierta; sus límites (un solo grupo, niveles 1-3, keysyms exóticos) están en `tools/xkbcompile/README.md`. diff --git a/neodos-kernel/src/input/kbd/layout.rs b/neodos-kernel/src/input/kbd/layout.rs index 660b40f6..ee40f141 100644 --- a/neodos-kernel/src/input/kbd/layout.rs +++ b/neodos-kernel/src/input/kbd/layout.rs @@ -73,17 +73,21 @@ impl KbdLayout { const KBD_MAGIC: [u8; 4] = [b'K', b'B', b'D', 0]; pub fn load_kbd(path: &str) -> Result { - let mut buf = [0u8; 8192]; - let size = crate::globals::with_vfs(|vfs| -> Result { + let data: Vec = crate::globals::with_vfs(|vfs| -> Result, ()> { let (drive_idx, node) = vfs.resolve_path(path).map_err(|_| ())?; if node.mode & crate::fs::vfs::MODE_FILE == 0 { return Err(()); } - let mut tmp = [0u8; 8192]; - let n = vfs.read(drive_idx, node.inode, 0, &mut tmp).map_err(|_| ())?; - buf[..n].copy_from_slice(&tmp[..n]); - Ok(n) + // Size the buffer from the inode: layout files grow with their compose + // table, so a fixed cap would silently truncate them. + let mut buf = Vec::new(); + buf.resize((node.size as usize).max(4096), 0u8); + let n = vfs.read(drive_idx, node.inode, 0, &mut buf).map_err(|_| ())?; + buf.truncate(n); + Ok(buf) }).map_err(|_| ())?; + let buf: &[u8] = &data; + let size = data.len(); if size < 16 { return Err(()); @@ -171,8 +175,13 @@ pub fn load_layouts() -> usize { layouts.push(fallback); // Disk layouts (locale-tagged) override the built-in fallback when present. - for path in ["C:\\System\\Keyboard\\en-US.kbd", "C:\\System\\Keyboard\\es-ES.kbd"] { - if let Ok(layout) = load_kbd(path) { + // Generate/verify all locales with scripts/gen-keyboard-layouts.sh; only + // these are enabled because loading many layouts at boot stresses the page + // cache and triggers #709, so more are enabled once that is fixed. + const LAYOUT_TAGS: &[&str] = &["en-US", "es-ES"]; + for tag in LAYOUT_TAGS { + let path = alloc::format!("C:\\System\\Keyboard\\{tag}.kbd"); + if let Ok(layout) = load_kbd(&path) { match layouts.iter().position(|l| l.name_str() == layout.name_str()) { Some(i) => layouts[i] = layout, None => layouts.push(layout), diff --git a/neodos-kernel/src/input/kbd/mod.rs b/neodos-kernel/src/input/kbd/mod.rs index 8ea1ba98..7cc769ad 100644 --- a/neodos-kernel/src/input/kbd/mod.rs +++ b/neodos-kernel/src/input/kbd/mod.rs @@ -455,7 +455,7 @@ pub fn register_tests() { test_eq!(layout::lookup_codepoint(&es, 0x2b, 0), Some('ç' as u16)); test_eq!(layout::lookup_codepoint(&es, 0x33, KBD_SHIFT), Some(';' as u16)); - // Dead keys: ` (grave), ´ (acute) and ~ (AltGr+4). + // Dead keys: ` (grave), ´ (acute) and ~ (AltGr+4, `es(deadtilde)`). test_true!(layout::is_dead_key(&es, 0x1a, 0)); test_true!(layout::is_dead_key(&es, 0x28, 0)); test_true!(layout::is_dead_key(&es, 0x05, KBD_ALTGR)); diff --git a/scripts/check-keyboard-layouts.sh b/scripts/check-keyboard-layouts.sh new file mode 100755 index 00000000..79b0f981 --- /dev/null +++ b/scripts/check-keyboard-layouts.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Verify data/keyboard/.kbd are non-empty and in sync with the XKB +# layouts (issue #702; conversion now via tools/xkbcompile). +# +# Fails if a .kbd is missing, maps no keys, or differs from a fresh compile. +# Run from anywhere; needs the XKB data (package `xkb-data`). +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$root" + +(cd tools/xkbcompile && cargo build --quiet) +bin=tools/xkbcompile/target/debug/xkbcompile + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +layouts=( + "us:basic:en-US" + "es:deadtilde:es-ES" + "es:cat:ca-ES" + "fr:basic:fr-FR" + "de:basic:de-DE" + "it:basic:it-IT" + "pt:basic:pt-PT" + "br:abnt2:pt-BR" + "nl:basic:nl-NL" + "se:basic:sv-SE" + "dk:basic:da-DK" + "no:basic:nb-NO" + "fi:kotoistus:fi-FI" + "pl:basic:pl-PL" + "cz:basic:cs-CZ" +) + +fail=0 +for spec in "${layouts[@]}"; do + IFS=: read -r layout variant tag <<<"$spec" + kbd="data/keyboard/$tag.kbd" + + if [ ! -f "$kbd" ]; then + echo "check-keyboard-layouts: missing $kbd" >&2 + fail=1 + continue + fi + + if ! "$bin" "$layout" "$variant" --name "$tag" --lang "$tag" --out "$tmp/$tag.kbd" 2>"$tmp/$tag.err"; then + echo "check-keyboard-layouts: xkbcompile failed for $layout($variant)" >&2 + fail=1 + continue + fi + + if ! grep -qE '\([1-9][0-9]* keys' "$tmp/$tag.err"; then + echo "check-keyboard-layouts: $layout($variant) produced no keys" >&2 + fail=1 + fi + + if ! cmp -s "$tmp/$tag.kbd" "$kbd"; then + echo "check-keyboard-layouts: $kbd is stale (run scripts/gen-keyboard-layouts.sh)" >&2 + fail=1 + fi +done + +if [ "$fail" -ne 0 ]; then + echo "check-keyboard-layouts: FAILED" >&2 + exit 1 +fi +echo "check-keyboard-layouts: OK" diff --git a/scripts/gen-keyboard-layouts.sh b/scripts/gen-keyboard-layouts.sh new file mode 100755 index 00000000..55e86e46 --- /dev/null +++ b/scripts/gen-keyboard-layouts.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Regenerate data/keyboard/.kbd from the XKB layouts with xkbcompile. +# +# `layout:variant:tag` — the tag is the NeoDOS layout name/lang (locale tag). +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$root" + +(cd tools/xkbcompile && cargo build --quiet) +bin=tools/xkbcompile/target/debug/xkbcompile + +# Keep in sync with check-keyboard-layouts.sh. +layouts=( + "us:basic:en-US" + "es:deadtilde:es-ES" + "es:cat:ca-ES" + "fr:basic:fr-FR" + "de:basic:de-DE" + "it:basic:it-IT" + "pt:basic:pt-PT" + "br:abnt2:pt-BR" + "nl:basic:nl-NL" + "se:basic:sv-SE" + "dk:basic:da-DK" + "no:basic:nb-NO" + "fi:kotoistus:fi-FI" + "pl:basic:pl-PL" + "cz:basic:cs-CZ" +) + +for spec in "${layouts[@]}"; do + IFS=: read -r layout variant tag <<<"$spec" + "$bin" "$layout" "$variant" --name "$tag" --lang "$tag" --out "data/keyboard/$tag.kbd" +done + +echo "gen-keyboard-layouts: done" diff --git a/tools/xkbcompile/src/main.rs b/tools/xkbcompile/src/main.rs index 4fd99033..49a755f4 100644 --- a/tools/xkbcompile/src/main.rs +++ b/tools/xkbcompile/src/main.rs @@ -196,6 +196,7 @@ fn resolve( let body = find_section(&text, section).ok_or_else(|| { format!("{}: section '{}' not found", path.display(), section.unwrap_or("")) })?; + let body = strip_comments(&body); for stmt in split_statements(&body) { let s = stmt.trim(); @@ -263,6 +264,17 @@ fn find_section(text: &str, name: Option<&str>) -> Option { } } +/// Drop `//` line comments (XKB names never contain `//`). +fn strip_comments(text: &str) -> String { + let mut out = String::with_capacity(text.len()); + for line in text.lines() { + let cut = line.find("//").unwrap_or(line.len()); + out.push_str(&line[..cut]); + out.push('\n'); + } + out +} + /// Split a section body into `;`-terminated statements (respecting quotes and /// brackets/braces). fn split_statements(body: &str) -> Vec { @@ -598,6 +610,50 @@ fn named_keysym(name: &str) -> Option { "ellipsis" => 0x2026, "trademark" => 0x2122, "bullet" => 0x2022, + // Latin Extended-A (Polish, Czech, ...) + "aogonek" => 0x0105, + "Aogonek" => 0x0104, + "cacute" => 0x0107, + "Cacute" => 0x0106, + "eogonek" => 0x0119, + "Eogonek" => 0x0118, + "lstroke" => 0x0142, + "Lstroke" => 0x0141, + "nacute" => 0x0144, + "Nacute" => 0x0143, + "sacute" => 0x015B, + "Sacute" => 0x015A, + "zacute" => 0x017A, + "Zacute" => 0x0179, + "zdot" | "zabovedot" => 0x017C, + "Zdot" | "Zabovedot" => 0x017B, + "ccaron" => 0x010D, + "Ccaron" => 0x010C, + "dcaron" => 0x010F, + "Dcaron" => 0x010E, + "ecaron" => 0x011B, + "Ecaron" => 0x011A, + "ncaron" => 0x0148, + "Ncaron" => 0x0147, + "rcaron" => 0x0159, + "Rcaron" => 0x0158, + "scaron" => 0x0161, + "Scaron" => 0x0160, + "tcaron" => 0x0165, + "Tcaron" => 0x0164, + "uring" => 0x016F, + "Uring" => 0x016E, + "zcaron" => 0x017E, + "Zcaron" => 0x017D, + "abreve" => 0x0103, + "Abreve" => 0x0102, + "dstroke" => 0x0111, + "Dstroke" => 0x0110, + "scedilla" => 0x015F, + "Scedilla" => 0x015E, + "gbreve" => 0x011F, + "Gbreve" => 0x011E, + "idotless" => 0x0131, _ => return None, }) }