ling-lang
↓ 0 downloadsLing - The Omniglot Systems Language
Keywords systems, polyglot, ai, crypto, game
lingfu install ling-langCode / Files (995)
crates/ling-kernel/src/drivers/keyboard.rs
use crate::arch::cpu;
// F1..F4 (scancode-set-1 make codes) switch the visible terminal (term.rs).
const F1: u8 = 0x3B;
const F4: u8 = 0x3E;
// Left Ctrl + C: reported to callers as ASCII ETX (0x03), the conventional
// "interrupt" byte — `ling_kernel_read_line` uses it to abort the line
// currently being typed. Release codes have the top bit set (0x80).
const LCTRL: u8 = 0x1D;
const C_KEY: u8 = 0x2E;
const ETX: u8 = 0x03;
static mut CTRL_DOWN: bool = false;
// Left/Right Shift: swap in `SCANCODE_ASCII_SHIFTED` (uppercase letters and
// the punctuation each digit/symbol key produces when shifted) for as long
// as either is held. Caps Lock isn't tracked — only Shift toggles this.
const LSHIFT: u8 = 0x2A;
const RSHIFT: u8 = 0x36;
static mut SHIFT_DOWN: bool = false;
// Extended (0xE0-prefixed) keys: the four arrows, reported to callers as
// DC1..DC4 (0x11..0x14) — control codes nothing else uses. Up/Down came
// first (command history recall in `ling_kernel_read_line`); Left/Right
// were added for the desktop's Settings rows (value cycling). The line
// editor still ignores Left/Right (no mid-line cursor movement yet).
const EXT_PREFIX: u8 = 0xE0;
const EXT_UP: u8 = 0x48;
const EXT_DOWN: u8 = 0x50;
const EXT_LEFT: u8 = 0x4B;
const EXT_RIGHT: u8 = 0x4D;
const EXT_HOME: u8 = 0x47;
const EXT_END: u8 = 0x4F;
const EXT_PGUP: u8 = 0x49;
const EXT_PGDN: u8 = 0x51;
/// Left "GUI"/Super key (the Windows/Command key). QEMU `sendkey meta_l`
/// produces its `E0 5B` make code. Surfaced so the desktop can use it as the
/// Applications-menu launcher — the keyboard path to opening apps that the
/// mouse-only dock/menu never had.
const EXT_SUPER: u8 = 0x5B;
pub const UP_ARROW: u8 = 0x11;
pub const DOWN_ARROW: u8 = 0x12;
pub const LEFT_ARROW: u8 = 0x13;
pub const RIGHT_ARROW: u8 = 0x14;
// Paging/navigation keys, reported as DC-range control codes nothing else
// uses (0x15..0x18) -- same rationale as the arrows above.
pub const HOME_KEY: u8 = 0x15;
pub const END_KEY: u8 = 0x16;
pub const PGUP_KEY: u8 = 0x17;
pub const PGDN_KEY: u8 = 0x18;
/// The Super/GUI key, reported as the next free DC-range control code. The
/// desktop treats it as "open the Applications menu".
pub const SUPER_KEY: u8 = 0x19;
// Ctrl-chords, reported in a dedicated 0x80+ range: raw ASCII control
// codes (letter & 0x1F) would collide with the arrows above (Ctrl+Q/R/S/T
// = 0x11..0x14). A Ctrl+letter is `0x80 | (1..26)`; a few Ctrl+symbol
// combos map to zoom. Every app decides what a chord means -- the
// terminal reads CTRL_C as interrupt, the editor as copy. Bytes are >=
// 0x80 so they never collide with printable ASCII or the arrows.
pub const CTRL_A: u8 = 0x80 | 1; // select all
pub const CTRL_C: u8 = 0x80 | 3; // copy (terminal: interrupt)
pub const CTRL_N: u8 = 0x80 | 14; // new window
pub const CTRL_S: u8 = 0x80 | 19; // save
pub const CTRL_T: u8 = 0x80 | 20; // new tab
pub const CTRL_V: u8 = 0x80 | 22; // paste
pub const CTRL_W: u8 = 0x80 | 23; // close
pub const CTRL_X: u8 = 0x80 | 24; // cut
pub const CTRL_Z: u8 = 0x80 | 26; // undo
pub const CTRL_ZOOM_IN: u8 = 0xE0; // Ctrl + '='/'+'
pub const CTRL_ZOOM_OUT: u8 = 0xE1; // Ctrl + '-'
pub const CTRL_ZOOM_RESET: u8 = 0xE2; // Ctrl + '0'
/// Map a scancode pressed while Ctrl is held to its chord byte, or 0 if it
/// isn't a chord we route (a bare modifier, or a key with no Ctrl meaning).
fn ctrl_chord(code: u8) -> u8 {
let base = {
// Always use the UNSHIFTED table for the letter identity so
// Ctrl+Shift+C still resolves to the 'c' chord.
let table = &SCANCODE_ASCII;
table.get(code as usize).copied().unwrap_or(0)
};
match base {
b'a'..=b'z' => 0x80 | (base - b'a' + 1),
b'=' | b'+' => CTRL_ZOOM_IN,
b'-' | b'_' => CTRL_ZOOM_OUT,
b'0' | b')' => CTRL_ZOOM_RESET,
_ => 0,
}
}
static mut PENDING_EXTENDED: bool = false;
/// Idle-font swap threshold, now a real duration off `timer::now_ms` (a
/// calibrated TSC clock, independent of IRQ delivery) rather than a busy-poll
/// spin count guessed at "a few idle seconds at typical hardware/QEMU
/// speeds" — replaced now that IRQ-driven input makes counting poll
/// iterations meaningless (there often aren't any: `read_char` parks in
/// `hlt` between keystrokes instead of spinning).
const IDLE_MS_THRESHOLD: u64 = 300_000; // 5 minutes
static mut LAST_ACTIVITY_MS: u64 = 0;
static mut IDLE_FIRED: bool = false;
/// A gap this much longer than any real inter-keystroke timing most likely
/// means something interrupted input mid-sequence -- a VirtualBox host
/// focus change or Ctrl+Alt+Del was the reported trigger. Without this,
/// e.g. `PENDING_EXTENDED` left `true` by a make code that never got its
/// follow-up byte before focus was lost would silently misroute every
/// subsequent scancode through the extended-key branch forever, with no way
/// to self-recover. Needs real VirtualBox confirmation, not just QEMU --
/// this is the best fix available without reproducing the exact PS/2
/// resync behavior VirtualBox's host-focus handling triggers.
const FOCUS_LOSS_GAP_MS: u64 = 2_000;
/// Reset modifier/sequence state if it's been stale long enough that
/// trusting it would misinterpret the next scancode. Call before processing
/// any newly-read scancode, using the *old* `LAST_ACTIVITY_MS` (i.e. before
/// `note_activity()` updates it).
fn reset_stale_modifier_state() {
unsafe {
if LAST_ACTIVITY_MS == 0 {
return; // first keypress ever -- nothing to have gotten stuck
}
let now = crate::arch::timer::now_ms();
if now.saturating_sub(LAST_ACTIVITY_MS) > FOCUS_LOSS_GAP_MS {
PENDING_EXTENDED = false;
CTRL_DOWN = false;
SHIFT_DOWN = false;
}
}
}
/// Fixed-capacity scancode queue the IRQ1 handler (`arch::x86_64::idt`)
/// pushes into and `read_scancode` drains. Single producer (the ISR, which
/// the "interrupt gate" IDT entry type guarantees can't re-enter itself),
/// single consumer (whichever kernel/task context calls `read_char`/
/// `poll_char`) — safe without a lock: the producer only ever advances
/// `RING_HEAD` and writes the slot it just claimed, the consumer only ever
/// advances `RING_TAIL` after reading the slot it names, and x86's strong
/// memory ordering means a plain `u8` index (which wraps for free at 256,
/// matching `RING`'s length) is enough for both sides to agree on what's
/// been produced vs. consumed.
const RING_CAPACITY: usize = 256;
static mut RING: [u8; RING_CAPACITY] = [0; RING_CAPACITY];
static mut RING_HEAD: u8 = 0;
static mut RING_TAIL: u8 = 0;
/// Called from the IRQ1 handler with the raw scancode byte just read off
/// port 0x60. Drops the byte (rather than overwriting an unread one) if the
/// ring is full — a burst of unread input is a starved consumer, not a
/// reason to corrupt older, still-unread bytes.
pub(crate) fn irq_scancode(code: u8) {
unsafe {
let next = RING_HEAD.wrapping_add(1);
if next != RING_TAIL {
RING[RING_HEAD as usize] = code;
RING_HEAD = next;
}
}
}
/// US QWERTY scancode-set-1 (make codes only) -> ASCII, unshifted.
static SCANCODE_ASCII: [u8; 88] = [
0, 27, b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', // 0x00-0x09
b'9', b'0', b'-', b'=', 8, b'\t', // 0x0A-0x0F
b'q', b'w', b'e', b'r', b't', b'y', b'u', b'i', b'o', b'p', b'[', b']', b'\n',
0, // 0x10-0x1D (0x1D = LCtrl)
b'a', b's', b'd', b'f', b'g', b'h', b'j', b'k', b'l', b';', b'\'', b'`', 0,
b'\\', // 0x1E-0x2B (0x2A = LShift)
b'z', b'x', b'c', b'v', b'b', b'n', b'm', b',', b'.', b'/',
0, // 0x2C-0x36 (0x36 = RShift)
b'*', 0, b' ', // 0x37-0x39 (0x38 = LAlt, 0x39 = space)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x3A-0x43 (function/lock keys, unmapped)
0, 0, 0, 0, 0, 0, 0, b'7', b'8', b'9', b'-', b'4', b'5', b'6', b'+', b'1', b'2', b'3', b'0',
b'.', // keypad
];
/// Same layout, shifted: digit row becomes its symbols, letters go
/// uppercase, and the rest of the punctuation keys produce their shifted
/// character. Modifier/lock/keypad slots are left identical to the unshifted
/// table (keypad ignores Shift here; only NumLock would change it, and that
/// isn't tracked).
static SCANCODE_ASCII_SHIFTED: [u8; 88] = [
0, 27, b'!', b'@', b'#', b'$', b'%', b'^', b'&', b'*', // 0x00-0x09
b'(', b')', b'_', b'+', 8, b'\t', // 0x0A-0x0F
b'Q', b'W', b'E', b'R', b'T', b'Y', b'U', b'I', b'O', b'P', b'{', b'}', b'\n',
0, // 0x10-0x1D
b'A', b'S', b'D', b'F', b'G', b'H', b'J', b'K', b'L', b':', b'"', b'~', 0,
b'|', // 0x1E-0x2B
b'Z', b'X', b'C', b'V', b'B', b'N', b'M', b'<', b'>', b'?', 0, // 0x2C-0x36
b'*', 0, b' ', // 0x37-0x39
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x3A-0x43
0, 0, 0, 0, 0, 0, 0, b'7', b'8', b'9', b'-', b'4', b'5', b'6', b'+', b'1', b'2', b'3', b'0',
b'.', // keypad
];
/// Whether Shift is currently held -- the WM reads this to distinguish a
/// selection-extending Shift+Arrow from a plain caret move in the editor.
pub fn shift_down() -> bool {
unsafe { SHIFT_DOWN }
}
fn lookup_ascii(code: u8) -> u8 {
let shifted = unsafe { SHIFT_DOWN };
if let Some(c) = crate::drivers::kbdlayout::override_for(code, shifted) {
return c;
}
let table = if shifted { &SCANCODE_ASCII_SHIFTED } else { &SCANCODE_ASCII };
table.get(code as usize).copied().unwrap_or(0)
}
/// Non-blocking scancode read from the IRQ-fed ring buffer. Returns `None`
/// if nothing is waiting.
fn read_scancode() -> Option<u8> {
unsafe {
if RING_HEAD == RING_TAIL {
return None;
}
let byte = RING[RING_TAIL as usize];
RING_TAIL = RING_TAIL.wrapping_add(1);
Some(byte)
}
}
/// Any real key event (or, via the IRQ12 mouse handler, mouse movement/
/// click) resets the idle clock, swaps back to the normal font if the idle
/// one had fired, and re-arms the idle-font swap so it can fire again next
/// time activity stops for a while.
pub(crate) fn note_activity() {
unsafe {
if IDLE_FIRED {
crate::drivers::vga::restore_normal_font();
}
LAST_ACTIVITY_MS = crate::arch::timer::now_ms();
IDLE_FIRED = false;
}
}
/// Called whenever `read_char` wakes from `hlt` with nothing waiting;
/// swaps to the idle font the first time `IDLE_MS_THRESHOLD` elapses since
/// the last real activity (and only that once, until activity resets it).
fn note_idle_check() {
unsafe {
if IDLE_FIRED {
return;
}
if crate::arch::timer::now_ms().saturating_sub(LAST_ACTIVITY_MS) >= IDLE_MS_THRESHOLD {
IDLE_FIRED = true;
crate::drivers::vga::use_idle_font();
}
}
}
/// If `code` is an F1..F4 make code, switch terminals and report handled.
fn try_switch_term(code: u8) -> bool {
if (F1..=F4).contains(&code) {
crate::drivers::term::switch_to((code - F1) as usize);
true
} else {
false
}
}
/// Poll for a single ASCII character (blocking). Release codes (top bit set)
/// and unmapped/extended scancodes are skipped over rather than returned;
/// F1..F4 switch terminals instead of producing a character.
pub fn read_char() -> u8 {
loop {
if let Some(code) = read_scancode() {
reset_stale_modifier_state();
note_activity();
if code == EXT_PREFIX {
unsafe {
PENDING_EXTENDED = true;
}
continue;
}
if unsafe { PENDING_EXTENDED } {
unsafe {
PENDING_EXTENDED = false;
}
if code == EXT_UP {
return UP_ARROW;
}
if code == EXT_DOWN {
return DOWN_ARROW;
}
if code == EXT_LEFT {
return LEFT_ARROW;
}
if code == EXT_RIGHT {
return RIGHT_ARROW;
}
if code == EXT_HOME {
return HOME_KEY;
}
if code == EXT_END {
return END_KEY;
}
if code == EXT_PGUP {
return PGUP_KEY;
}
if code == EXT_PGDN {
return PGDN_KEY;
}
continue;
}
if code == LCTRL {
unsafe {
CTRL_DOWN = true;
}
continue;
}
if code == LCTRL | 0x80 {
unsafe {
CTRL_DOWN = false;
}
continue;
}
if code == LSHIFT || code == RSHIFT {
unsafe {
SHIFT_DOWN = true;
}
continue;
}
if code == LSHIFT | 0x80 || code == RSHIFT | 0x80 {
unsafe {
SHIFT_DOWN = false;
}
continue;
}
// Release (key-up) codes have the top bit set; ignore them.
if code & 0x80 != 0 {
continue;
}
if try_switch_term(code) {
continue;
}
if unsafe { CTRL_DOWN } {
// Ctrl+C stays the shell's interrupt (returned as ETX) so
// read_line's cancel keeps working; other Ctrl-chords pass
// through as their 0x80+ codes for line-editor callers that
// care (none today -- they just ignore them).
if code == C_KEY {
return ETX;
}
let chord = ctrl_chord(code);
if chord != 0 {
return chord;
}
}
let ascii = lookup_ascii(code);
if ascii != 0 {
return ascii;
}
} else {
note_idle_check();
// The PIT heartbeat wakes this `hlt` ~100x/sec even with no key
// activity, which is exactly the periodic hook the cursor blink
// needs (see `term::tick_cursor_blink`'s doc comment) -- no new
// interrupt wiring required to get it ticking while idle.
crate::drivers::term::tick_cursor_blink();
// Park until the next interrupt (keyboard, mouse, or the PIT
// heartbeat, whichever comes first) instead of burning CPU in a
// busy-poll — real work now that input is IRQ-delivered rather
// than requiring a spin to notice a new byte.
unsafe { cpu::halt() };
}
}
}
/// Non-blocking poll: returns 0 if no mapped key is currently waiting.
pub fn poll_char() -> u8 {
match read_scancode() {
Some(code) => {
reset_stale_modifier_state();
note_activity();
if code == EXT_PREFIX {
unsafe {
PENDING_EXTENDED = true;
}
return 0;
}
if unsafe { PENDING_EXTENDED } {
unsafe {
PENDING_EXTENDED = false;
}
if code == EXT_UP {
return UP_ARROW;
}
if code == EXT_DOWN {
return DOWN_ARROW;
}
if code == EXT_LEFT {
return LEFT_ARROW;
}
if code == EXT_RIGHT {
return RIGHT_ARROW;
}
if code == EXT_HOME {
return HOME_KEY;
}
if code == EXT_END {
return END_KEY;
}
if code == EXT_PGUP {
return PGUP_KEY;
}
if code == EXT_PGDN {
return PGDN_KEY;
}
if code == EXT_SUPER {
return SUPER_KEY;
}
return 0;
}
if code == LCTRL {
unsafe {
CTRL_DOWN = true;
}
return 0;
}
if code == LCTRL | 0x80 {
unsafe {
CTRL_DOWN = false;
}
return 0;
}
if code == LSHIFT || code == RSHIFT {
unsafe {
SHIFT_DOWN = true;
}
return 0;
}
if code == LSHIFT | 0x80 || code == RSHIFT | 0x80 {
unsafe {
SHIFT_DOWN = false;
}
return 0;
}
if code & 0x80 != 0 || try_switch_term(code) {
return 0;
}
if unsafe { CTRL_DOWN } {
let chord = ctrl_chord(code);
if chord != 0 {
return chord;
}
}
lookup_ascii(code)
},
None => 0,
}
}