RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / tty.rs
3159 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//! The console "tty": keyboard input with a tiny line discipline, and the
//! `File` behind file descriptors 0, 1 and 2.
//!
//! Like a Linux terminal in canonical mode: typed characters are echoed and
//! collected in a line buffer (Backspace edits it), and only when Enter is
//! pressed does the whole line become readable from fd 0. So a user program's
//! `read(0, ...)` gets a complete, already-edited line.

use crate::errno::Errno;
use crate::fs::file::File;
use crate::{print, serial_print, vga_buffer};
use alloc::string::String;
use conquer_once::spin::OnceCell;
use crossbeam_queue::ArrayQueue;
use spin::Mutex;

/// The line being typed (not yet readable).
static LINE: Mutex<String> = Mutex::new(String::new());
/// Finished lines, as bytes, waiting for `read(0, ...)`.
static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();

fn input() -> &'static ArrayQueue<u8> {
    INPUT.get_or_init(|| ArrayQueue::new(4096))
}

/// Called by the keyboard task for every typed character.
pub fn input_char(c: char) {
    let mut line = LINE.lock();
    match c {
        '\u{8}' => {
            // Backspace: only erase what's in the current line.
            if line.pop().is_some() {
                vga_buffer::backspace();
                serial_print!("\x08"); // uart_16550 turns this into "\x08 \x08"
            }
        }
        '\n' => {
            print!("\n");
            serial_print!("\n");
            for b in line.bytes().chain(Some(b'\n')) {
                let _ = input().push(b); // queue full: drop (nobody is reading)
            }
            line.clear();
        }
        c => {
            print!("{}", c);
            serial_print!("{}", c);
            line.push(c);
        }
    }
}

/// The console as a file: reading gets keyboard lines, writing prints to the
/// screen and serial port.
pub struct Console;

impl File for Console {
    /// Blocks until a line is available, then returns up to `buf.len()`
    /// bytes of it (stopping after the '\n').
    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {
        if buf.is_empty() {
            return Ok(0);
        }
        // Sleep until a line arrives. Syscalls run with interrupts enabled,
        // so `hlt` wakes on the next interrupt (and the timer may switch to
        // other threads meanwhile). Lines are queued whole, so once the
        // first byte is there, the rest of the line is too.
        while input().is_empty() {
            x86_64::instructions::hlt();
        }
        let mut n = 0;
        while n < buf.len() {
            let Some(b) = input().pop() else { break };
            buf[n] = b;
            n += 1;
            if b == b'\n' {
                break;
            }
        }
        Ok(n)
    }

    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {
        // Print valid UTF-8 as is, anything else as '?'.
        for chunk in buf.utf8_chunks() {
            print!("{}", chunk.valid());
            serial_print!("{}", chunk.valid());
            if !chunk.invalid().is_empty() {
                print!("?");
                serial_print!("?");
            }
        }
        Ok(buf.len())
    }
}