RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / tty.rs
6598 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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! The console "tty": keyboard input, and the `File` behind file
//! descriptors 0, 1 and 2.
//!
//! Two input modes, like a Linux terminal:
//!
//! * **cooked** (canonical, the default): typed characters are echoed and
//!   collected in a line buffer (Backspace edits it); only when Enter is
//!   pressed does the whole line become readable from fd 0. So `read(0)`
//!   gets a complete, already-edited line. Programs like `cat` use this.
//! * **raw**: every key goes to the reader immediately and nothing is echoed;
//!   arrow keys arrive as ANSI escape sequences (`ESC [ A` = up). The shell
//!   switches to raw while it edits its command line, so it can do its own
//!   echo, Backspace and up-arrow history. (Linux: `tcsetattr` / `ioctl`.)
//!
//! Output (`Console::write`) understands two control characters besides
//! '\n': '\x08' erases the character before the cursor, and '\x0c' (form
//! feed) clears the screen.

use crate::errno::Errno;
use crate::fs::file::{File, KIND_CONSOLE, Stat};
use crate::thread::WaitQueue;
use crate::{print, serial_print, vga_buffer};
use alloc::string::String;
use conquer_once::spin::OnceCell;
use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use crossbeam_queue::ArrayQueue;
use pc_keyboard::KeyCode;
use spin::Mutex;

/// The line being typed in cooked mode (not yet readable).
static LINE: Mutex<String> = Mutex::new(String::new());
/// Bytes waiting for `read(0, ...)`: finished lines (cooked) or keys (raw).
static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();
/// Raw mode on?
static RAW: AtomicBool = AtomicBool::new(false);
/// pid of the process that switched raw mode on (0 = none). If it exits
/// without switching back, the console goes back to cooked mode.
static RAW_OWNER: AtomicU64 = AtomicU64::new(0);

/// Threads blocked in `Console::read`, waiting for input.
static INPUT_WAIT: WaitQueue = WaitQueue::new();

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

/// Switch between raw (`true`) and cooked mode for process `pid`. Returns
/// the old mode.
pub fn set_raw(raw: bool, pid: u64) -> bool {
    LINE.lock().clear(); // a half-typed cooked line doesn't survive a switch
    RAW_OWNER.store(if raw { pid } else { 0 }, Ordering::SeqCst);
    RAW.swap(raw, Ordering::SeqCst)
}

/// A process ended: if it left the console in raw mode, undo that (so the
/// next program, or a respawned shell, gets normal line input).
pub fn process_exited(pid: u64) {
    if RAW_OWNER.compare_exchange(pid, 0, Ordering::SeqCst, Ordering::SeqCst).is_ok() {
        RAW.store(false, Ordering::SeqCst);
    }
}

pub fn is_raw() -> bool {
    RAW.load(Ordering::SeqCst)
}

/// Make `bytes` readable and wake readers. (Queue full: drop -- nobody reads.)
fn deliver(bytes: &[u8]) {
    for &b in bytes {
        let _ = input().push(b);
    }
    INPUT_WAIT.wake_all();
}

/// Called by the keyboard task for every typed character.
pub fn input_char(c: char) {
    if is_raw() {
        let mut utf8 = [0; 4];
        deliver(c.encode_utf8(&mut utf8).as_bytes());
        return;
    }
    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");
            line.push('\n');
            deliver(line.as_bytes());
            line.clear();
        }
        c => {
            print!("{}", c);
            serial_print!("{}", c);
            line.push(c);
        }
    }
}

/// Called by the keyboard task for keys that aren't characters (arrows...).
pub fn input_key(key: KeyCode) {
    if is_raw() {
        // The ANSI/VT100 sequences a Linux terminal would send.
        let seq: &[u8] = match key {
            KeyCode::ArrowUp => b"\x1b[A",
            KeyCode::ArrowDown => b"\x1b[B",
            KeyCode::ArrowRight => b"\x1b[C",
            KeyCode::ArrowLeft => b"\x1b[D",
            KeyCode::Home => b"\x1b[H",
            KeyCode::End => b"\x1b[F",
            KeyCode::Delete => b"\x1b[3~",
            _ => return,
        };
        deliver(seq);
    } else {
        print!("<{:?}>", key);
        serial_print!("<{:?}>", key);
    }
}

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

impl File for Console {
    /// Blocks until input is available. Cooked: returns up to `buf.len()`
    /// bytes of a line, stopping after its '\n'. Raw: whatever keys are there.
    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {
        if buf.is_empty() {
            return Ok(0);
        }
        // Block until input arrives: the thread is parked on INPUT_WAIT
        // (using no CPU) and `deliver` wakes it. The closure runs with
        // interrupts off; the ArrayQueue is lock-free, so that's fine.
        Ok(INPUT_WAIT.wait_until(|| {
            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;
                }
            }
            if n > 0 { Some(n) } else { None }
        }))
    }

    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {
        // Print valid UTF-8 as is, anything else as '?'. Control characters
        // are handled one by one.
        for chunk in buf.utf8_chunks() {
            for piece in chunk.valid().split_inclusive(['\x08', '\x0c']) {
                let (text, control) = match piece.strip_suffix(['\x08', '\x0c']) {
                    Some(text) => (text, piece.chars().last()),
                    None => (piece, None),
                };
                print!("{}", text);
                serial_print!("{}", text);
                match control {
                    Some('\x08') => {
                        vga_buffer::backspace();
                        serial_print!("\x08");
                    }
                    Some(_) => vga_buffer::clear_screen(), // serial: nothing to clear
                    None => {}
                }
            }
            if !chunk.invalid().is_empty() {
                print!("?");
                serial_print!("?");
            }
        }
        Ok(buf.len())
    }

    fn stat(&self) -> Result<Stat, Errno> {
        Ok(Stat { kind: KIND_CONSOLE, size: 0, mode: 0 })
    }
}