Files
RedBear-OS/drivers/graphics/fbcond/src/keymap.rs
T
Red Bear OS bd595851e2 base: apply Red Bear patches on latest upstream/main
251 files: init, acpid, ipcd, netcfg, ihdgd, virtio-gpud, scheme-utils,
inputd, block driver, ptyd, ramfs, randd, initfs bootstrap, path deps,
version +rb0.3.1, author attribution
2026-07-11 11:39:24 +03:00

213 lines
6.7 KiB
Rust

//! 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<Scancode, Vec<u8>>,
}
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<Self, String> {
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<Scancode, Vec<u8>> = 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<Scancode, String> {
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::<Scancode>()
.map_err(|e| format!("invalid scancode '{}': {e}", s))
}
}
fn parse_byte_seq(s: &str) -> Result<Vec<u8>, 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());
}
}