Refactor the processing of ANSI escape sequences
This commit is contained in:
parent
1fd4a65f4b
commit
643df09722
|
|
@ -4,13 +4,28 @@ use crate::Pixel;
|
|||
|
||||
/// A finite-state machine (FSM) to handle ANSI escape sequences.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct EscapeFsm {
|
||||
pub struct EscapeFsm {
|
||||
state: WaitFor,
|
||||
params: [u32; MAX_PARAMS],
|
||||
params: [Option<u32>; MAX_PARAMS],
|
||||
}
|
||||
|
||||
impl Default for EscapeFsm {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// The mode for "Erase in Display" (ED) command.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EraseInDisplay {
|
||||
CursorToEnd,
|
||||
CursorToBeginning,
|
||||
EntireScreen,
|
||||
EntireScreenAndScrollback,
|
||||
}
|
||||
|
||||
/// A trait to execute operations from ANSI escape sequences.
|
||||
pub(super) trait EscapeOp {
|
||||
pub trait EscapeOp {
|
||||
/// Sets the cursor position.
|
||||
fn set_cursor(&mut self, x: usize, y: usize);
|
||||
|
||||
|
|
@ -18,15 +33,30 @@ pub(super) trait EscapeOp {
|
|||
fn set_fg_color(&mut self, val: Pixel);
|
||||
/// Sets the background color.
|
||||
fn set_bg_color(&mut self, val: Pixel);
|
||||
|
||||
/// Erases part or all of the display.
|
||||
fn erase_in_display(&mut self, mode: EraseInDisplay);
|
||||
}
|
||||
|
||||
const MAX_PARAMS: usize = 8;
|
||||
|
||||
// FIXME: Currently we only support a few ANSI escape sequences, and we just swallow the unsupported ones.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum WaitFor {
|
||||
Escape,
|
||||
Bracket,
|
||||
Params(u8),
|
||||
/// Just saw ESC (0x1B), expecting the next selector.
|
||||
AfterEscape,
|
||||
/// Currently parsing CSI parameters.
|
||||
Csi {
|
||||
idx: u8,
|
||||
is_private: bool,
|
||||
saw_digit: bool,
|
||||
in_intermediate: bool,
|
||||
},
|
||||
/// OSC payload after ESC] . Swallow until BEL (0x07) or ST (ESC \).
|
||||
Osc,
|
||||
/// Saw ESC inside OSC and it maybe ST.
|
||||
OscEscape,
|
||||
}
|
||||
|
||||
/// Foreground and background colors.
|
||||
|
|
@ -69,92 +99,265 @@ const COLORS: [Pixel; 16] = [
|
|||
];
|
||||
|
||||
impl EscapeFsm {
|
||||
pub(super) fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: WaitFor::Escape,
|
||||
params: [0; MAX_PARAMS],
|
||||
params: [None; MAX_PARAMS],
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to eat a character as part of the ANSI escape sequence.
|
||||
/// Tries to eat a byte as part of the ANSI escape sequence.
|
||||
///
|
||||
/// This method returns a boolean value indicating whether the character is part of an ANSI
|
||||
/// escape sequence. In other words, if the method returns true, then the character has been
|
||||
/// eaten and should not be displayed in the console.
|
||||
pub(super) fn eat<T: EscapeOp>(&mut self, byte: u8, op: &mut T) -> bool {
|
||||
let num_params = match (self.state, byte) {
|
||||
// Handle '\033'.
|
||||
(WaitFor::Escape, 0o33) => {
|
||||
self.state = WaitFor::Bracket;
|
||||
return true;
|
||||
}
|
||||
(WaitFor::Escape, _) => {
|
||||
// This is not an ANSI escape sequence.
|
||||
return false;
|
||||
/// Returns `true` if the byte is consumed by the FSM and must not be rendered as text.
|
||||
/// Returns `false` if the byte is not part of an escape sequence and should be rendered.
|
||||
pub fn eat<T: EscapeOp>(&mut self, byte: u8, op: &mut T) -> bool {
|
||||
match self.state {
|
||||
WaitFor::Escape => {
|
||||
if byte == 0x1b {
|
||||
self.state = WaitFor::AfterEscape;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Handle '['.
|
||||
(WaitFor::Bracket, b'[') => {
|
||||
self.state = WaitFor::Params(0);
|
||||
self.params[0] = 0;
|
||||
return true;
|
||||
}
|
||||
(WaitFor::Bracket, _) => {
|
||||
// The character is invalid. We cannot handle it, so we are aborting the ANSI
|
||||
// escape sequence.
|
||||
self.state = WaitFor::Escape;
|
||||
return true;
|
||||
WaitFor::AfterEscape => {
|
||||
match byte {
|
||||
b'[' => {
|
||||
// CSI begins.
|
||||
self.params.fill(None);
|
||||
self.state = WaitFor::Csi {
|
||||
idx: 0,
|
||||
is_private: false,
|
||||
saw_digit: false,
|
||||
in_intermediate: false,
|
||||
};
|
||||
true
|
||||
}
|
||||
b']' => {
|
||||
// OSC begins.
|
||||
self.state = WaitFor::Osc;
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
// The character is invalid. We cannot handle it, so we are aborting the ANSI
|
||||
// escape sequence.
|
||||
self.state = WaitFor::Escape;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle numeric parameters.
|
||||
(WaitFor::Params(i), b'0'..=b'9') => {
|
||||
let param = &mut self.params[i as usize];
|
||||
*param = param.wrapping_mul(10).wrapping_add((byte - b'0') as u32);
|
||||
return true;
|
||||
}
|
||||
(WaitFor::Params(i), b';') if (i as usize + 1) < MAX_PARAMS => {
|
||||
self.state = WaitFor::Params(i + 1);
|
||||
self.params[i as usize + 1] = 0;
|
||||
return true;
|
||||
}
|
||||
(WaitFor::Params(_), b';') => {
|
||||
// There are too many parameters. We cannot handle that many, so we are aborting
|
||||
// the ANSI escape sequence.
|
||||
self.state = WaitFor::Escape;
|
||||
return true;
|
||||
WaitFor::Osc => {
|
||||
match byte {
|
||||
0x07 => {
|
||||
// BEL terminator
|
||||
self.state = WaitFor::Escape;
|
||||
true
|
||||
}
|
||||
0x1b => {
|
||||
// Might be ST
|
||||
self.state = WaitFor::OscEscape;
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
// Swallow OSC payload.
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Break and handle the final action.
|
||||
(WaitFor::Params(i), _) => {
|
||||
self.state = WaitFor::Escape;
|
||||
(i + 1) as usize
|
||||
WaitFor::OscEscape => {
|
||||
if byte == b'\\' {
|
||||
self.state = WaitFor::Escape;
|
||||
true
|
||||
} else {
|
||||
// Not ST and we go back to OSC and keep swallowing.
|
||||
self.state = WaitFor::Osc;
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match byte {
|
||||
// CUP - Cursor Position
|
||||
b'H' if num_params == 2 => {
|
||||
op.set_cursor(
|
||||
self.params[1].saturating_sub(1) as usize,
|
||||
self.params[0].saturating_sub(1) as usize,
|
||||
);
|
||||
WaitFor::Csi {
|
||||
idx,
|
||||
is_private,
|
||||
saw_digit,
|
||||
in_intermediate,
|
||||
} => {
|
||||
match byte {
|
||||
// Intermediate bytes (0x20..=0x2F).
|
||||
// Once we see any intermediate, we are in the intermediate section;
|
||||
// later bytes must be intermediate or final.
|
||||
0x20..=0x2f => {
|
||||
// If we already entered intermediate section, just keep swallowing them.
|
||||
// If we were still in parameter section, we now transition to intermediate.
|
||||
self.state = WaitFor::Csi {
|
||||
idx,
|
||||
is_private,
|
||||
saw_digit,
|
||||
in_intermediate: true,
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
// Parameter bytes (0x30..=0x3F).
|
||||
0x30..=0x3f if !in_intermediate => {
|
||||
match byte {
|
||||
// digits: contribute to numeric params
|
||||
b'0'..=b'9' => {
|
||||
let i = idx as usize;
|
||||
if i < MAX_PARAMS {
|
||||
let p = &mut self.params[i];
|
||||
*p = Some(
|
||||
p.unwrap_or(0)
|
||||
.saturating_mul(10)
|
||||
.saturating_add((byte - b'0') as u32),
|
||||
);
|
||||
}
|
||||
self.state = WaitFor::Csi {
|
||||
idx,
|
||||
is_private,
|
||||
saw_digit: true,
|
||||
in_intermediate: false,
|
||||
};
|
||||
}
|
||||
|
||||
// ';' separates numeric parameters.
|
||||
b';' => {
|
||||
let next = idx.saturating_add(1);
|
||||
if (next as usize) < MAX_PARAMS {
|
||||
// If there were no digits for this param, it already stays None.
|
||||
self.state = WaitFor::Csi {
|
||||
idx: next,
|
||||
is_private,
|
||||
saw_digit: false,
|
||||
in_intermediate: false,
|
||||
};
|
||||
} else {
|
||||
// There are too many parameters. We cannot handle that many, so we are aborting
|
||||
// the ANSI escape sequence.
|
||||
self.state = WaitFor::Escape;
|
||||
}
|
||||
}
|
||||
|
||||
b':' => {
|
||||
// The behavior of ':' is not defined by the standard.
|
||||
log::warn!("EscapeFsm: unsupported ':' parameter separator in CSI");
|
||||
}
|
||||
|
||||
// Sequences containing <=>? are "private". We swallow them and mark `is_private`.
|
||||
b'<' | b'=' | b'>' | b'?' => {
|
||||
self.state = WaitFor::Csi {
|
||||
idx,
|
||||
is_private: true,
|
||||
saw_digit,
|
||||
in_intermediate: false,
|
||||
};
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// Parameter bytes after intermediate section is illegal by the formal grammar.
|
||||
// We'll abort and swallow to avoid leaking garbage.
|
||||
0x30..=0x3f if in_intermediate => {
|
||||
self.state = WaitFor::Escape;
|
||||
true
|
||||
}
|
||||
|
||||
// Final byte (0x40..=0x7E): ends the CSI.
|
||||
0x40..=0x7e => {
|
||||
self.state = WaitFor::Escape;
|
||||
|
||||
let num_params = (idx as usize).saturating_add(1).min(MAX_PARAMS);
|
||||
|
||||
self.dispatch_csi(byte, num_params, is_private, op);
|
||||
true
|
||||
}
|
||||
|
||||
_ => {
|
||||
// Terminal behavior is undefined if a CSI contains bytes outside 0x20..=0x7E.
|
||||
log::warn!("EscapeFsm: invalid byte {:#x} in CSI sequence", byte);
|
||||
self.state = WaitFor::Escape;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the parameter at the given index, or returns the default value if the parameter is not present.
|
||||
fn param_or(&self, i: usize, default: u32) -> u32 {
|
||||
self.params.get(i).and_then(|p| *p).unwrap_or(default)
|
||||
}
|
||||
|
||||
fn dispatch_csi<T: EscapeOp>(
|
||||
&self,
|
||||
final_byte: u8,
|
||||
num_params: usize,
|
||||
is_private: bool,
|
||||
op: &mut T,
|
||||
) {
|
||||
if is_private {
|
||||
// For now we don't handle any private sequences, so just swallow them.
|
||||
return;
|
||||
}
|
||||
|
||||
match final_byte {
|
||||
// CUP - Cursor Position: CSI n ; m H
|
||||
//
|
||||
// - n=row, m=col
|
||||
// - default to 1 if omitted
|
||||
//
|
||||
// Examples:
|
||||
// - CSI H -> 1;1
|
||||
// - CSI ;5H -> 1;5
|
||||
// - CSI 17H -> 17;1
|
||||
// - CSI 17;H -> 17;1
|
||||
// - CSI 17;1H -> 17;1
|
||||
b'H' => {
|
||||
let row_1b = self.param_or(0, 1);
|
||||
let col_1b = self.param_or(1, 1);
|
||||
|
||||
op.set_cursor((col_1b - 1) as usize, (row_1b - 1) as usize);
|
||||
}
|
||||
|
||||
// ED - Erase in Display: CSI n J
|
||||
//
|
||||
// n:
|
||||
// - 0 (or missing): cursor to end of screen
|
||||
// - 1: cursor to beginning of screen
|
||||
// - 2: entire screen
|
||||
// - 3: entire screen + scrollback
|
||||
b'J' => {
|
||||
let n = self.param_or(0, 0);
|
||||
let mode = match n {
|
||||
0 => EraseInDisplay::CursorToEnd,
|
||||
1 => EraseInDisplay::CursorToBeginning,
|
||||
2 => EraseInDisplay::EntireScreen,
|
||||
3 => EraseInDisplay::EntireScreenAndScrollback,
|
||||
_ => {
|
||||
// Invalid parameter.
|
||||
return;
|
||||
}
|
||||
};
|
||||
op.erase_in_display(mode);
|
||||
}
|
||||
|
||||
// SGR - Select Graphic Rendition
|
||||
b'm' => self.handle_srg(num_params, op),
|
||||
b'm' => self.handle_sgr(num_params, op),
|
||||
|
||||
// Invalid or unsupported
|
||||
// Unknown CSI: swallow silently.
|
||||
_ => {}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Handles the "Select Graphic Rendition" sequence.
|
||||
fn handle_srg<T: EscapeOp>(&self, num_params: usize, op: &mut T) {
|
||||
fn handle_sgr<T: EscapeOp>(&self, num_params: usize, op: &mut T) {
|
||||
let mut cursor = 0;
|
||||
while cursor < num_params {
|
||||
let op_code = self.params[cursor];
|
||||
let op_code = self.param_or(cursor, 0) as u8;
|
||||
cursor += 1;
|
||||
|
||||
match op_code {
|
||||
|
|
@ -167,15 +370,15 @@ impl EscapeFsm {
|
|||
// Set foreground colors
|
||||
// Reference: <https://en.wikipedia.org/wiki/ANSI_escape_code#Colors>
|
||||
30..=37 => op.set_fg_color(COLORS[op_code as usize - 30]),
|
||||
38 if num_params - cursor >= 2 && self.params[cursor] == 5 => {
|
||||
op.set_fg_color(Self::get_256_color(self.params[cursor + 1] as u8));
|
||||
38 if num_params - cursor >= 2 && self.param_or(cursor, 0) == 5 => {
|
||||
op.set_fg_color(Self::get_256_color(self.param_or(cursor + 1, 0) as u8));
|
||||
cursor += 2;
|
||||
}
|
||||
38 if num_params - cursor >= 4 && self.params[cursor] == 2 => {
|
||||
38 if num_params - cursor >= 4 && self.param_or(cursor, 0) == 2 => {
|
||||
op.set_fg_color(Pixel {
|
||||
red: self.params[cursor + 1] as u8,
|
||||
green: self.params[cursor + 2] as u8,
|
||||
blue: self.params[cursor + 3] as u8,
|
||||
red: self.param_or(cursor + 1, 0) as u8,
|
||||
green: self.param_or(cursor + 2, 0) as u8,
|
||||
blue: self.param_or(cursor + 3, 0) as u8,
|
||||
});
|
||||
cursor += 4;
|
||||
}
|
||||
|
|
@ -186,15 +389,15 @@ impl EscapeFsm {
|
|||
// Set background colors
|
||||
// Reference: <https://en.wikipedia.org/wiki/ANSI_escape_code#Colors>
|
||||
40..=47 => op.set_bg_color(COLORS[op_code as usize - 40]),
|
||||
48 if num_params - cursor >= 2 && self.params[cursor] == 5 => {
|
||||
op.set_bg_color(Self::get_256_color(self.params[cursor + 1] as u8));
|
||||
48 if num_params - cursor >= 2 && self.param_or(cursor, 0) == 5 => {
|
||||
op.set_bg_color(Self::get_256_color(self.param_or(cursor + 1, 0) as u8));
|
||||
cursor += 2;
|
||||
}
|
||||
48 if num_params - cursor >= 4 && self.params[cursor] == 2 => {
|
||||
48 if num_params - cursor >= 4 && self.param_or(cursor, 0) == 2 => {
|
||||
op.set_bg_color(Pixel {
|
||||
red: self.params[cursor + 1] as u8,
|
||||
green: self.params[cursor + 2] as u8,
|
||||
blue: self.params[cursor + 3] as u8,
|
||||
red: self.param_or(cursor + 1, 0) as u8,
|
||||
green: self.param_or(cursor + 2, 0) as u8,
|
||||
blue: self.param_or(cursor + 3, 0) as u8,
|
||||
});
|
||||
cursor += 4;
|
||||
}
|
||||
|
|
@ -260,6 +463,7 @@ mod test {
|
|||
y: usize,
|
||||
fg: Pixel,
|
||||
bg: Pixel,
|
||||
last_ed: Option<EraseInDisplay>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
|
|
@ -269,6 +473,7 @@ mod test {
|
|||
y: 0,
|
||||
fg: Pixel::WHITE,
|
||||
bg: Pixel::BLACK,
|
||||
last_ed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -286,6 +491,10 @@ mod test {
|
|||
fn set_bg_color(&mut self, val: Pixel) {
|
||||
self.bg = val;
|
||||
}
|
||||
|
||||
fn erase_in_display(&mut self, mode: EraseInDisplay) {
|
||||
self.last_ed = Some(mode);
|
||||
}
|
||||
}
|
||||
|
||||
fn eat_escape_sequence(esc_fsm: &mut EscapeFsm, state: &mut State, bytes: &[u8]) {
|
||||
|
|
@ -306,15 +515,38 @@ mod test {
|
|||
|
||||
assert!(!esc_fsm.eat(b'a', &mut state));
|
||||
|
||||
// There is invalid as there is no 0-th row or 0-th column. But in this case, let's move
|
||||
// the cursor to the first row and the first column.
|
||||
eat_escape_sequence(&mut esc_fsm, &mut state, b"\x1B[0;0H");
|
||||
// CUP defaults to 1;1 when omitted.
|
||||
eat_escape_sequence(&mut esc_fsm, &mut state, b"\x1B[H");
|
||||
assert_eq!(state.x, 0);
|
||||
assert_eq!(state.y, 0);
|
||||
|
||||
assert!(!esc_fsm.eat(b'a', &mut state));
|
||||
}
|
||||
|
||||
#[ktest]
|
||||
fn erase_in_display() {
|
||||
let mut esc_fsm = EscapeFsm::new();
|
||||
let mut state = State::default();
|
||||
|
||||
// Default (or missing) is 0: cursor to end of screen.
|
||||
eat_escape_sequence(&mut esc_fsm, &mut state, b"\x1B[J");
|
||||
assert_eq!(state.last_ed, Some(EraseInDisplay::CursorToEnd));
|
||||
|
||||
eat_escape_sequence(&mut esc_fsm, &mut state, b"\x1B[1J");
|
||||
assert_eq!(state.last_ed, Some(EraseInDisplay::CursorToBeginning));
|
||||
|
||||
eat_escape_sequence(&mut esc_fsm, &mut state, b"\x1B[2J");
|
||||
assert_eq!(state.last_ed, Some(EraseInDisplay::EntireScreen));
|
||||
|
||||
eat_escape_sequence(&mut esc_fsm, &mut state, b"\x1B[3J");
|
||||
assert_eq!(
|
||||
state.last_ed,
|
||||
Some(EraseInDisplay::EntireScreenAndScrollback)
|
||||
);
|
||||
|
||||
assert!(!esc_fsm.eat(b'a', &mut state));
|
||||
}
|
||||
|
||||
#[ktest]
|
||||
fn set_color() {
|
||||
let mut esc_fsm = EscapeFsm::new();
|
||||
|
|
|
|||
|
|
@ -2,167 +2,63 @@
|
|||
|
||||
use alloc::{sync::Arc, vec::Vec};
|
||||
|
||||
use aster_console::{
|
||||
AnyConsoleDevice, ConsoleCallback, ConsoleSetFontError,
|
||||
font::BitmapFont,
|
||||
mode::{ConsoleMode, KeyboardMode},
|
||||
};
|
||||
use ostd::{
|
||||
mm::{HasSize, VmReader},
|
||||
sync::{LocalIrqDisabled, SpinLock},
|
||||
};
|
||||
use spin::Once;
|
||||
use aster_console::{ConsoleSetFontError, font::BitmapFont, mode::ConsoleMode};
|
||||
use ostd::mm::HasSize;
|
||||
|
||||
use crate::{
|
||||
FRAMEBUFFER, FrameBuffer, Pixel,
|
||||
ansi_escape::{EscapeFsm, EscapeOp},
|
||||
FrameBuffer, Pixel,
|
||||
ansi_escape::{EraseInDisplay, EscapeOp},
|
||||
};
|
||||
|
||||
/// A text console rendered onto the framebuffer.
|
||||
pub struct FramebufferConsole {
|
||||
callbacks: SpinLock<ConsoleCallbacks, LocalIrqDisabled>,
|
||||
inner: SpinLock<(ConsoleState, EscapeFsm), LocalIrqDisabled>,
|
||||
}
|
||||
|
||||
pub const CONSOLE_NAME: &str = "Framebuffer-Console";
|
||||
|
||||
pub static FRAMEBUFFER_CONSOLE: Once<Arc<FramebufferConsole>> = Once::new();
|
||||
|
||||
pub(crate) fn init() {
|
||||
let Some(fb) = FRAMEBUFFER.get() else {
|
||||
log::warn!("Framebuffer not initialized");
|
||||
return;
|
||||
};
|
||||
|
||||
FRAMEBUFFER_CONSOLE.call_once(|| Arc::new(FramebufferConsole::new(fb.clone())));
|
||||
}
|
||||
|
||||
impl AnyConsoleDevice for FramebufferConsole {
|
||||
fn send(&self, buf: &[u8]) {
|
||||
let mut inner = self.inner.lock();
|
||||
let (state, esc_fsm) = &mut *inner;
|
||||
|
||||
for byte in buf {
|
||||
if esc_fsm.eat(*byte, state) {
|
||||
// The character is part of an ANSI escape sequence.
|
||||
continue;
|
||||
}
|
||||
|
||||
if *byte == 0 {
|
||||
// The character is a NUL character.
|
||||
continue;
|
||||
}
|
||||
|
||||
state.send_char(*byte);
|
||||
}
|
||||
}
|
||||
|
||||
fn register_callback(&self, callback: &'static ConsoleCallback) {
|
||||
self.callbacks.lock().callbacks.push(callback);
|
||||
}
|
||||
|
||||
fn set_font(&self, font: BitmapFont) -> Result<(), ConsoleSetFontError> {
|
||||
self.inner.lock().0.set_font(font)
|
||||
}
|
||||
|
||||
fn set_mode(&self, mode: ConsoleMode) -> bool {
|
||||
self.inner.lock().0.set_mode(mode);
|
||||
true
|
||||
}
|
||||
|
||||
fn mode(&self) -> Option<ConsoleMode> {
|
||||
Some(self.inner.lock().0.mode())
|
||||
}
|
||||
|
||||
fn set_keyboard_mode(&self, mode: KeyboardMode) -> bool {
|
||||
match mode {
|
||||
KeyboardMode::Xlate => self.callbacks.lock().is_input_enabled = true,
|
||||
KeyboardMode::Off => self.callbacks.lock().is_input_enabled = false,
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn keyboard_mode(&self) -> Option<KeyboardMode> {
|
||||
if self.callbacks.lock().is_input_enabled {
|
||||
Some(KeyboardMode::Xlate)
|
||||
} else {
|
||||
Some(KeyboardMode::Off)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FramebufferConsole {
|
||||
/// Creates a new framebuffer console.
|
||||
pub(self) fn new(framebuffer: Arc<FrameBuffer>) -> Self {
|
||||
let callbacks = ConsoleCallbacks {
|
||||
callbacks: Vec::new(),
|
||||
is_input_enabled: true,
|
||||
};
|
||||
|
||||
let state = ConsoleState {
|
||||
x_pos: 0,
|
||||
y_pos: 0,
|
||||
fg_color: Pixel::WHITE,
|
||||
bg_color: Pixel::BLACK,
|
||||
font: BitmapFont::new_basic8x8(),
|
||||
is_output_enabled: true,
|
||||
|
||||
bytes: alloc::vec![0u8; framebuffer.io_mem().size()],
|
||||
backend: framebuffer,
|
||||
};
|
||||
|
||||
let esc_fsm = EscapeFsm::new();
|
||||
|
||||
Self {
|
||||
callbacks: SpinLock::new(callbacks),
|
||||
inner: SpinLock::new((state, esc_fsm)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Triggers the registered input callbacks with the given data.
|
||||
pub(crate) fn trigger_input_callbacks(&self, bytes: &[u8]) {
|
||||
let callbacks = self.callbacks.lock();
|
||||
if !callbacks.is_input_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let reader = VmReader::from(bytes);
|
||||
for callback in callbacks.callbacks.iter() {
|
||||
callback(reader.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for FramebufferConsole {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("FramebufferConsole").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleCallbacks {
|
||||
callbacks: Vec<&'static ConsoleCallback>,
|
||||
/// Whether the input characters will be handled by the callbacks.
|
||||
is_input_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ConsoleState {
|
||||
pub struct ConsoleState {
|
||||
x_pos: usize,
|
||||
y_pos: usize,
|
||||
fg_color: Pixel,
|
||||
bg_color: Pixel,
|
||||
font: BitmapFont,
|
||||
/// Whether the output characters will be drawn in the framebuffer.
|
||||
is_output_enabled: bool,
|
||||
|
||||
is_rendering_enabled: bool,
|
||||
mode: ConsoleMode,
|
||||
bytes: Vec<u8>,
|
||||
backend: Arc<FrameBuffer>,
|
||||
}
|
||||
|
||||
impl ConsoleState {
|
||||
pub fn new(backend: Arc<FrameBuffer>) -> Self {
|
||||
let buffer_size = backend.io_mem().size();
|
||||
Self {
|
||||
x_pos: 0,
|
||||
y_pos: 0,
|
||||
fg_color: Pixel::WHITE,
|
||||
bg_color: Pixel::BLACK,
|
||||
font: BitmapFont::new_basic8x8(),
|
||||
is_rendering_enabled: false,
|
||||
mode: ConsoleMode::Text,
|
||||
bytes: alloc::vec![0; buffer_size],
|
||||
backend,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables rendering to the framebuffer.
|
||||
pub fn enable_rendering(&mut self) {
|
||||
self.is_rendering_enabled = true;
|
||||
}
|
||||
|
||||
/// Disables rendering to the framebuffer.
|
||||
pub fn disable_rendering(&mut self) {
|
||||
self.is_rendering_enabled = false;
|
||||
}
|
||||
|
||||
/// Flushes the entire console buffer to the framebuffer.
|
||||
pub fn flush_fullscreen(&self) {
|
||||
if self.is_rendering_enabled && self.mode == ConsoleMode::Text {
|
||||
self.backend.write_bytes_at(0, &self.bytes).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a single character to be drawn on the framebuffer.
|
||||
pub(self) fn send_char(&mut self, ch: u8) {
|
||||
pub fn send_char(&mut self, ch: u8) {
|
||||
if ch == b'\n' {
|
||||
self.newline();
|
||||
return;
|
||||
|
|
@ -197,7 +93,7 @@ impl ConsoleState {
|
|||
self.bytes.copy_within(offset.., 0);
|
||||
self.bytes[self.backend.io_mem().size() - offset..].fill(0);
|
||||
|
||||
if self.is_output_enabled {
|
||||
if self.is_rendering_enabled && self.mode == ConsoleMode::Text {
|
||||
self.backend.write_bytes_at(0, &self.bytes).unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -243,7 +139,7 @@ impl ConsoleState {
|
|||
}
|
||||
|
||||
// Write pixels to the framebuffer.
|
||||
if self.is_output_enabled {
|
||||
if self.is_rendering_enabled && self.mode == ConsoleMode::Text {
|
||||
self.backend.write_bytes_at(off_st, render_buf).unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +148,7 @@ impl ConsoleState {
|
|||
}
|
||||
|
||||
/// Sets the font for the framebuffer console.
|
||||
pub(self) fn set_font(&mut self, font: BitmapFont) -> Result<(), ConsoleSetFontError> {
|
||||
pub fn set_font(&mut self, font: BitmapFont) -> Result<(), ConsoleSetFontError> {
|
||||
// Note that the font height cannot exceed the half the height of the framebuffer.
|
||||
// Otherwise, `shift_lines_up` will underflow `x_pos`.
|
||||
if font.width() > self.backend.width() || font.height() > self.backend.height() / 2 {
|
||||
|
|
@ -269,29 +165,66 @@ impl ConsoleState {
|
|||
}
|
||||
|
||||
/// Sets the console mode (text or graphics).
|
||||
pub(self) fn set_mode(&mut self, mode: ConsoleMode) {
|
||||
if mode == ConsoleMode::Graphics {
|
||||
self.is_output_enabled = false;
|
||||
pub fn set_mode(&mut self, mode: ConsoleMode) {
|
||||
if self.mode == mode {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_output_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're switching from the graphics mode back to the text mode. The characters need to be
|
||||
// redrawn in the framebuffer.
|
||||
self.is_output_enabled = true;
|
||||
self.backend.write_bytes_at(0, &self.bytes).unwrap();
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
/// Gets the current console mode.
|
||||
pub(self) fn mode(&self) -> ConsoleMode {
|
||||
if self.is_output_enabled {
|
||||
ConsoleMode::Text
|
||||
} else {
|
||||
ConsoleMode::Graphics
|
||||
pub fn mode(&self) -> ConsoleMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
/// Fill a rectangular pixel region [x0, x1) × [y0, y1) with the given color.
|
||||
///
|
||||
/// This writes to the shadow buffer, and optionally flushes
|
||||
/// the affected region to the real framebuffer.
|
||||
fn fill_rect_pixels(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, color: Pixel) {
|
||||
if x0 >= x1 || y0 >= y1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let w = self.backend.width();
|
||||
let h = self.backend.height();
|
||||
let x0 = x0.min(w);
|
||||
let x1 = x1.min(w);
|
||||
let y0 = y0.min(h);
|
||||
let y1 = y1.min(h);
|
||||
|
||||
if x0 >= x1 || y0 >= y1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let rendered_pixel = self.backend.render_pixel(color);
|
||||
let rendered_pixel_size = rendered_pixel.nbytes();
|
||||
let row_bytes = (x1 - x0) * rendered_pixel_size;
|
||||
|
||||
// Fill shadow buffer row by row.
|
||||
for y in y0..y1 {
|
||||
let off = self.backend.calc_offset(x0, y).as_usize();
|
||||
let buf = &mut self.bytes[off..off + row_bytes];
|
||||
|
||||
// Write pixels.
|
||||
for chunk in buf.chunks_exact_mut(rendered_pixel_size) {
|
||||
chunk.copy_from_slice(rendered_pixel.as_slice());
|
||||
}
|
||||
|
||||
// Flush to framebuffer if needed.
|
||||
if self.is_rendering_enabled && self.mode == ConsoleMode::Text {
|
||||
self.backend.write_bytes_at(off, buf).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixel coordinates for the cursor cell.
|
||||
fn cursor_cell_rect(&self) -> (usize, usize, usize, usize) {
|
||||
let cx = self.x_pos.min(self.backend.width());
|
||||
let cy = self.y_pos.min(self.backend.height());
|
||||
let x1 = (cx + self.font.width()).min(self.backend.width());
|
||||
let y1 = (cy + self.font.height()).min(self.backend.height());
|
||||
(cx, cy, x1, y1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -313,4 +246,38 @@ impl EscapeOp for ConsoleState {
|
|||
fn set_bg_color(&mut self, val: Pixel) {
|
||||
self.bg_color = val;
|
||||
}
|
||||
|
||||
fn erase_in_display(&mut self, mode: EraseInDisplay) {
|
||||
let bg = self.bg_color;
|
||||
let w = self.backend.width();
|
||||
let h = self.backend.height();
|
||||
|
||||
let (cx0, cy0, cx1, cy1) = self.cursor_cell_rect();
|
||||
|
||||
match mode {
|
||||
EraseInDisplay::CursorToEnd => {
|
||||
// Clear from cursor x to end-of-line, within the cursor row.
|
||||
self.fill_rect_pixels(cx0, cy0, w, cy1, bg);
|
||||
|
||||
// Clear all rows below cursor row.
|
||||
if cy1 < h {
|
||||
self.fill_rect_pixels(0, cy1, w, h, bg);
|
||||
}
|
||||
}
|
||||
|
||||
EraseInDisplay::CursorToBeginning => {
|
||||
// Clear all rows above cursor row.
|
||||
if cy0 > 0 {
|
||||
self.fill_rect_pixels(0, 0, w, cy0, bg);
|
||||
}
|
||||
|
||||
// Clear from start-of-line to cursor cell end, within cursor row.
|
||||
self.fill_rect_pixels(0, cy0, cx1, cy1, bg);
|
||||
}
|
||||
|
||||
EraseInDisplay::EntireScreen | EraseInDisplay::EntireScreenAndScrollback => {
|
||||
self.fill_rect_pixels(0, 0, w, h, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ extern crate alloc;
|
|||
|
||||
mod ansi_escape;
|
||||
mod console;
|
||||
mod console_input;
|
||||
mod dummy_console;
|
||||
mod framebuffer;
|
||||
mod pixel;
|
||||
|
||||
pub use ansi_escape::{EscapeFsm, EscapeOp};
|
||||
use component::{ComponentInitError, init_component};
|
||||
pub use console::{CONSOLE_NAME, FRAMEBUFFER_CONSOLE, FramebufferConsole};
|
||||
pub use console::ConsoleState;
|
||||
pub use dummy_console::DummyFramebufferConsole;
|
||||
pub use framebuffer::{ColorMapEntry, FRAMEBUFFER, FrameBuffer, MAX_CMAP_SIZE};
|
||||
pub use pixel::{Pixel, PixelFormat, RenderedPixel};
|
||||
|
|
@ -22,7 +22,5 @@ pub use pixel::{Pixel, PixelFormat, RenderedPixel};
|
|||
#[init_component]
|
||||
fn init() -> Result<(), ComponentInitError> {
|
||||
framebuffer::init();
|
||||
console::init();
|
||||
console_input::init();
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue