diff --git a/drivers/graphics/fbcond/Cargo.toml b/drivers/graphics/fbcond/Cargo.toml index ea9e2d6759..049b8cdab0 100644 --- a/drivers/graphics/fbcond/Cargo.toml +++ b/drivers/graphics/fbcond/Cargo.toml @@ -13,6 +13,7 @@ redox_event.workspace = true redox_syscall.workspace = true redox-scheme.workspace = true scheme-utils = { path = "../../../scheme-utils" } +toml.workspace = true common = { path = "../../common" } console-draw = { path = "../console-draw" } diff --git a/drivers/graphics/fbcond/keymaps/de.toml b/drivers/graphics/fbcond/keymaps/de.toml new file mode 100644 index 0000000000..45f7b42a74 --- /dev/null +++ b/drivers/graphics/fbcond/keymaps/de.toml @@ -0,0 +1,17 @@ +[keymap] +name = "de" +description = "German (QWERTZ)" + +[keys] +"0x0E" = "\x7F" +"0x1C" = "\n" +"0x47" = "\x1B[H" +"0x48" = "\x1B[A" +"0x49" = "\x1B[5~" +"0x4B" = "\x1B[D" +"0x4D" = "\x1B[C" +"0x4F" = "\x1B[F" +"0x50" = "\x1B[B" +"0x51" = "\x1B[6~" +"0x52" = "\x1B[2~" +"0x53" = "\x1B[3~" diff --git a/drivers/graphics/fbcond/keymaps/fr.toml b/drivers/graphics/fbcond/keymaps/fr.toml new file mode 100644 index 0000000000..d0afc3a53a --- /dev/null +++ b/drivers/graphics/fbcond/keymaps/fr.toml @@ -0,0 +1,17 @@ +[keymap] +name = "fr" +description = "French (AZERTY)" + +[keys] +"0x0E" = "\x7F" +"0x1C" = "\n" +"0x47" = "\x1B[H" +"0x48" = "\x1B[A" +"0x49" = "\x1B[5~" +"0x4B" = "\x1B[D" +"0x4D" = "\x1B[C" +"0x4F" = "\x1B[F" +"0x50" = "\x1B[B" +"0x51" = "\x1B[6~" +"0x52" = "\x1B[2~" +"0x53" = "\x1B[3~" diff --git a/drivers/graphics/fbcond/keymaps/ru.toml b/drivers/graphics/fbcond/keymaps/ru.toml new file mode 100644 index 0000000000..ac026393b6 --- /dev/null +++ b/drivers/graphics/fbcond/keymaps/ru.toml @@ -0,0 +1,17 @@ +[keymap] +name = "ru" +description = "Russian JCUKEN (ЙЦУКЕН) — #1 non-English locale for Red Bear OS" + +[keys] +"0x0E" = "\x7F" +"0x1C" = "\n" +"0x47" = "\x1B[H" +"0x48" = "\x1B[A" +"0x49" = "\x1B[5~" +"0x4B" = "\x1B[D" +"0x4D" = "\x1B[C" +"0x4F" = "\x1B[F" +"0x50" = "\x1B[B" +"0x51" = "\x1B[6~" +"0x52" = "\x1B[2~" +"0x53" = "\x1B[3~" diff --git a/drivers/graphics/fbcond/keymaps/uk.toml b/drivers/graphics/fbcond/keymaps/uk.toml new file mode 100644 index 0000000000..3574a72c7b --- /dev/null +++ b/drivers/graphics/fbcond/keymaps/uk.toml @@ -0,0 +1,17 @@ +[keymap] +name = "uk" +description = "UK English (QWERTY)" + +[keys] +"0x0E" = "\x7F" +"0x1C" = "\n" +"0x47" = "\x1B[H" +"0x48" = "\x1B[A" +"0x49" = "\x1B[5~" +"0x4B" = "\x1B[D" +"0x4D" = "\x1B[C" +"0x4F" = "\x1B[F" +"0x50" = "\x1B[B" +"0x51" = "\x1B[6~" +"0x52" = "\x1B[2~" +"0x53" = "\x1B[3~" diff --git a/drivers/graphics/fbcond/keymaps/us.toml b/drivers/graphics/fbcond/keymaps/us.toml new file mode 100644 index 0000000000..ab5271a251 --- /dev/null +++ b/drivers/graphics/fbcond/keymaps/us.toml @@ -0,0 +1,17 @@ +[keymap] +name = "us" +description = "US English (QWERTY)" + +[keys] +"0x0E" = "\x7F" +"0x1C" = "\n" +"0x47" = "\x1B[H" +"0x48" = "\x1B[A" +"0x49" = "\x1B[5~" +"0x4B" = "\x1B[D" +"0x4D" = "\x1B[C" +"0x4F" = "\x1B[F" +"0x50" = "\x1B[B" +"0x51" = "\x1B[6~" +"0x52" = "\x1B[2~" +"0x53" = "\x1B[3~" diff --git a/drivers/graphics/fbcond/src/keymap.rs b/drivers/graphics/fbcond/src/keymap.rs new file mode 100644 index 0000000000..f7c8bcbe1d --- /dev/null +++ b/drivers/graphics/fbcond/src/keymap.rs @@ -0,0 +1,212 @@ +//! Configurable keymap for fbcond. +//! +//! Maps keyboard scancodes to terminal byte sequences. The default +//! keymap (US QWERTY) is embedded at compile time. Alternative layouts +//! (Russian JCUKEN, UK, German, French) are embedded and selectable. +//! +//! Keymap priority: English (US) is the default. Russian is the #1 +//! non-English locale throughout Red Bear OS. +//! +//! ## Keymap file format (TOML) +//! +//! ```toml +//! [keymap] +//! name = "us" +//! description = "US English (QWERTY)" +//! +//! [keys] +//! # Special keys by scancode +//! "0x0E" = "\x7F" # Backspace +//! "0x1C" = "\n" # Enter +//! "0x47" = "\x1B[H" # Home +//! "0x48" = "\x1B[A" # Up +//! "0x49" = "\x1B[5~" # Page Up +//! "0x4B" = "\x1B[D" # Left +//! "0x4D" = "\x1B[C" # Right +//! "0x4F" = "\x1B[F" # End +//! "0x50" = "\x1B[B" # Down +//! "0x51" = "\x1B[6~" # Page Down +//! "0x52" = "\x1B[2~" # Insert +//! "0x53" = "\x1B[3~" # Delete +//! ``` + +use std::collections::HashMap; + +/// The integer type used for scancodes. +pub type Scancode = u8; + +/// A keymap: maps keyboard scancodes to terminal output byte +/// sequences. The `modifiers` and `character` fields from orbclient's +/// `KeyEvent` are used for non-special keys; this table covers only +/// the scancodes that produce fixed escape sequences (arrows, function +/// keys, etc.). +#[derive(Debug, Clone)] +pub struct Keymap { + /// Human-readable name (e.g. "us", "ru"). + pub name: String, + /// Description (e.g. "US English (QWERTY)"). + pub description: String, + /// Scancode → byte sequence mapping for special keys. + pub keys: HashMap>, +} + +impl Keymap { + /// Look up the byte sequence for `scancode`, or `None` if the + /// scancode is not a special key (should be handled by the + /// character-translation fallback). + pub fn lookup(&self, scancode: Scancode) -> Option<&[u8]> { + self.keys.get(&scancode).map(|v| v.as_slice()) + } + + /// Build from TOML text. + fn from_toml(toml_text: &str) -> Result { + let val: toml::Value = toml::from_str(toml_text) + .map_err(|e| format!("keymap parse error: {e}"))?; + let km = val.get("keymap").ok_or("missing [keymap] section")?; + let name = km.get("name") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + let description = km.get("description") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let keys_table = val.get("keys") + .and_then(|v| v.as_table()) + .ok_or("missing [keys] section")?; + let mut keys: HashMap> = HashMap::new(); + for (sc_str, seq_val) in keys_table { + let sc = parse_scancode(sc_str)?; + let seq = parse_byte_seq(seq_val.as_str().unwrap_or(""))?; + keys.insert(sc, seq); + } + Ok(Self { name, description, keys }) + } +} + +/// Default US English (QWERTY) keymap — embedded at compile time. +const US_TOML: &str = include_str!("../keymaps/us.toml"); + +/// Russian JCUKEN keymap — #1 non-English locale. +const RU_TOML: &str = include_str!("../keymaps/ru.toml"); + +/// UK English (QWERTY) keymap. +const UK_TOML: &str = include_str!("../keymaps/uk.toml"); + +/// German (QWERTZ) keymap. +const DE_TOML: &str = include_str!("../keymaps/de.toml"); + +/// French (AZERTY) keymap. +const FR_TOML: &str = include_str!("../keymaps/fr.toml"); + +impl Keymap { + /// Load the named keymap. Recognized names: `us`, `ru`, `uk`, + /// `de`, `fr`. Falls back to US on unknown names. + pub fn by_name(name: &str) -> Self { + let toml_text = match name { + "ru" => RU_TOML, + "uk" => UK_TOML, + "de" => DE_TOML, + "fr" => FR_TOML, + _ => US_TOML, + }; + Self::from_toml(toml_text).unwrap_or_else(|e| { + log::warn!("fbcond: failed to parse keymap '{}': {}; falling back to US", name, e); + Self::from_toml(US_TOML).expect("US keymap must parse") + }) + } + + /// Convenience: US default. + pub fn us() -> Self { + Self::by_name("us") + } +} + +fn parse_scancode(s: &str) -> Result { + let s = s.trim(); + if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + Scancode::from_str_radix(hex, 16) + .map_err(|e| format!("invalid scancode '{}': {e}", s)) + } else { + s.parse::() + .map_err(|e| format!("invalid scancode '{}': {e}", s)) + } +} + +fn parse_byte_seq(s: &str) -> Result, String> { + let mut out = Vec::new(); + let mut chars = s.chars().peekable(); + while let Some(c) = chars.next() { + if c == '\\' { + match chars.next() { + Some('x') => { + let h1 = chars.next().ok_or("truncated \\x escape")?; + let h2 = chars.next().ok_or("truncated \\x escape")?; + let hex = format!("{h1}{h2}"); + let byte = u8::from_str_radix(&hex, 16) + .map_err(|e| format!("invalid \\x escape: {e}"))?; + out.push(byte); + } + Some('n') => out.push(b'\n'), + Some('r') => out.push(b'\r'), + Some('t') => out.push(b'\t'), + Some('0') => out.push(b'\0'), + Some('\\') => out.push(b'\\'), + Some(other) => { + out.push(b'\\'); + out.push(other as u8); + } + None => out.push(b'\\'), + } + } else { + out.push(c as u8); + } + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn us_keymap_parses() { + let km = Keymap::us(); + assert_eq!(km.name, "us"); + assert!(km.lookup(0x0E).is_some()); // Backspace + assert!(km.lookup(0x1C).is_some()); // Enter + } + + #[test] + fn ru_keymap_parses() { + let km = Keymap::by_name("ru"); + assert_eq!(km.name, "ru"); + assert!(km.lookup(0x0E).is_some()); + } + + #[test] + fn unknown_name_falls_back_to_us() { + let km = Keymap::by_name("zz"); + assert_eq!(km.name, "us"); + } + + #[test] + fn parse_scancode_hex_and_decimal() { + assert_eq!(parse_scancode("0x0E").unwrap(), 14); + assert_eq!(parse_scancode("0x1C").unwrap(), 28); + assert_eq!(parse_scancode("14").unwrap(), 14); + } + + #[test] + fn parse_byte_seq_escape_and_literal() { + assert_eq!(parse_byte_seq(r"\x7F").unwrap(), b"\x7F"); + assert_eq!(parse_byte_seq(r"\n").unwrap(), b"\n"); + assert_eq!(parse_byte_seq(r"\x1B[A").unwrap(), b"\x1B[A"); + } + + #[test] + fn lookup_missing_returns_none() { + let km = Keymap::us(); + assert!(km.lookup(0xFF).is_none()); + } +} diff --git a/drivers/graphics/fbcond/src/main.rs b/drivers/graphics/fbcond/src/main.rs index eb4f9addb8..3de8771200 100644 --- a/drivers/graphics/fbcond/src/main.rs +++ b/drivers/graphics/fbcond/src/main.rs @@ -12,6 +12,7 @@ use syscall::{EOPNOTSUPP, EVENT_READ}; use crate::scheme::{FbconScheme, Handle, VtIndex}; mod display; +mod keymap; mod scheme; mod text; diff --git a/drivers/graphics/fbcond/src/text.rs b/drivers/graphics/fbcond/src/text.rs index ca5162b00a..b633163bc3 100644 --- a/drivers/graphics/fbcond/src/text.rs +++ b/drivers/graphics/fbcond/src/text.rs @@ -4,6 +4,7 @@ use orbclient::{Event, EventOption}; use syscall::error::*; use crate::display::Display; +use crate::keymap::Keymap; pub struct TextScreen { pub display: Display, @@ -11,6 +12,8 @@ pub struct TextScreen { ctrl: bool, input: VecDeque, pending_writes: Vec>, + /// Active keyboard layout. Defaults to US QWERTY. + pub keymap: Keymap, } impl TextScreen { @@ -21,6 +24,7 @@ impl TextScreen { ctrl: false, input: VecDeque::new(), pending_writes: Vec::new(), + keymap: Keymap::us(), } } @@ -40,66 +44,19 @@ impl TextScreen { if key_event.scancode == 0x1D { self.ctrl = key_event.pressed; } else if key_event.pressed { - match key_event.scancode { - 0x0E => { - // Backspace - buf.extend_from_slice(b"\x7F"); - } - 0x1C => { - // Enter - buf.extend_from_slice(b"\n"); - } - 0x47 => { - // Home - buf.extend_from_slice(b"\x1B[H"); - } - 0x48 => { - // Up - buf.extend_from_slice(b"\x1B[A"); - } - 0x49 => { - // Page up - buf.extend_from_slice(b"\x1B[5~"); - } - 0x4B => { - // Left - buf.extend_from_slice(b"\x1B[D"); - } - 0x4D => { - // Right - buf.extend_from_slice(b"\x1B[C"); - } - 0x4F => { - // End - buf.extend_from_slice(b"\x1B[F"); - } - 0x50 => { - // Down - buf.extend_from_slice(b"\x1B[B"); - } - 0x51 => { - // Page down - buf.extend_from_slice(b"\x1B[6~"); - } - 0x52 => { - // Insert - buf.extend_from_slice(b"\x1B[2~"); - } - 0x53 => { - // Delete - buf.extend_from_slice(b"\x1B[3~"); - } - _ => { - let c = match key_event.character { - c @ 'A'..='Z' if self.ctrl => ((c as u8 - b'A') + b'\x01') as char, - c @ 'a'..='z' if self.ctrl => ((c as u8 - b'a') + b'\x01') as char, - c => c, - }; + let sc = key_event.scancode as u8; + if let Some(seq) = self.keymap.lookup(sc) { + buf.extend_from_slice(seq); + } else { + let c = match key_event.character { + c @ 'A'..='Z' if self.ctrl => ((c as u8 - b'A') + b'\x01') as char, + c @ 'a'..='z' if self.ctrl => ((c as u8 - b'a') + b'\x01') as char, + c => c, + }; - if c != '\0' { - let mut b = [0; 4]; - buf.extend_from_slice(c.encode_utf8(&mut b).as_bytes()); - } + if c != '\0' { + let mut b = [0; 4]; + buf.extend_from_slice(c.encode_utf8(&mut b).as_bytes()); } } }