Phase 2.3: fbcond configurable keymap system (TOML-based)
Replaced hardcoded US scancode→escape-sequence table in fbcond's text.rs with a configurable Keymap struct supporting TOML-based keyboard layouts. Five layouts embedded at compile time: - us.toml — US English (QWERTY), default - ru.toml — Russian JCUKEN, #1 non-English locale throughout Red Bear OS - uk.toml — UK English (QWERTY) - de.toml — German (QWERTZ) - fr.toml — French (AZERTY) Implementation: - src/keymap.rs: Keymap struct with TOML deserialization, scancode→byte sequence lookup, embedded defaults via include_str!(). 6 unit tests. - src/text.rs: TextScreen gains field. Hardcoded 12-arm scancode match replaced with call. Same Ctrl+letter translation fallback preserved. - keymaps/*.toml: Five embedded layout files. - Cargo.toml: added toml.workspace dependency. - main.rs: registered mod keymap. Keymap priority policy: English (US) default, Russian #1 non-English, then UK/DE/FR. Unknown names fall back to US.
This commit is contained in:
@@ -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" }
|
||||
|
||||
@@ -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~"
|
||||
@@ -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~"
|
||||
@@ -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~"
|
||||
@@ -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~"
|
||||
@@ -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~"
|
||||
@@ -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<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());
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use syscall::{EOPNOTSUPP, EVENT_READ};
|
||||
use crate::scheme::{FbconScheme, Handle, VtIndex};
|
||||
|
||||
mod display;
|
||||
mod keymap;
|
||||
mod scheme;
|
||||
mod text;
|
||||
|
||||
|
||||
@@ -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<u8>,
|
||||
pending_writes: Vec<Vec<u8>>,
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user