bd595851e2
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
213 lines
6.7 KiB
Rust
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());
|
|
}
|
|
}
|