575 lines
16 KiB
Rust

//! Console device interfaces
use core::time::Duration;
use abi::{error::Error, io::TerminalSize, primitive_enum};
use alloc::{vec, vec::Vec};
use bitflags::bitflags;
use libk::{task::runtime, vfs::TerminalOutput};
use libk_util::{sync::IrqSafeSpinlock, StaticVector};
use crate::debug::DebugSink;
const CONSOLE_ROW_LEN: usize = 80;
const MAX_CSI_ARGS: usize = 8;
const DEFAULT_FG_COLOR: ColorAttribute = ColorAttribute::White;
const DEFAULT_BG_COLOR: ColorAttribute = ColorAttribute::Blue;
primitive_enum! {
#[allow(missing_docs)]
#[doc = "Color attribute of a console character"]
pub enum ColorAttribute: u8 {
Black = 0,
Red = 1,
Green = 2,
Yellow = 3,
Blue = 4,
Magenta = 5,
Cyan = 6,
White = 7,
}
}
bitflags! {
#[doc = "Extra attributes of a console character"]
#[derive(Clone, Copy)]
pub struct Attributes: u8 {
#[allow(missing_docs)]
const BOLD = 1 << 0;
}
}
impl ColorAttribute {
fn from_vt100(val: u8) -> Self {
match val {
0..=7 => Self::try_from(val).unwrap(),
_ => ColorAttribute::Red,
}
}
/// Converts the attribute to RGBA representation
pub fn as_rgba(&self, bold: bool) -> u32 {
let color = match self {
Self::Black => 0x000000,
Self::Red => 0x7F0000,
Self::Green => 0x007F00,
Self::Yellow => 0x7F7F00,
Self::Blue => 0x00007F,
Self::Magenta => 0x7F007F,
Self::Cyan => 0x007F7F,
Self::White => 0x7F7F7F,
};
if bold {
color * 2
} else {
color
}
}
}
/// Represents a single character with its attributes
#[derive(Clone, Copy)]
#[repr(C)]
pub struct ConsoleChar {
attributes: u16,
char: u8,
_pad: u8,
}
/// Represents a single line in the console buffer
#[derive(Clone, Copy)]
pub struct ConsoleRow {
dirty: u8,
chars: [ConsoleChar; CONSOLE_ROW_LEN],
}
/// Buffer that contains text rows of the console with their attributes + tracks dirty rows which
/// need to be flushed to the display
pub struct ConsoleBuffer {
rows: Vec<ConsoleRow>,
height: u32,
}
/// Console wrapper for adding it into devfs as a char device
pub struct ConsoleWrapper<'a>(pub &'a dyn DisplayConsole);
enum EscapeState {
Normal,
Escape,
Csi,
}
/// Common state for console output devices
pub struct ConsoleState {
/// Current cursor row
pub cursor_row: u32,
/// Current cursor column
pub cursor_col: u32,
/// Current foreground color
pub fg_color: ColorAttribute,
/// Current background color
pub bg_color: ColorAttribute,
/// Current set of attributes
pub attributes: Attributes,
esc_args: StaticVector<u32, MAX_CSI_ARGS>,
esc_state: EscapeState,
/// Row buffer
pub buffer: ConsoleBuffer,
}
/// Helper type to iterate over dirty rows in the buffer
pub struct RowIter<'a> {
buffer: &'a mut ConsoleBuffer,
index: u32,
}
/// Interface to implement buffered console semantics on an abstract console output device
pub trait DisplayConsole: Sync {
/// Returns the state lock
fn state(&self) -> &IrqSafeSpinlock<ConsoleState>;
/// Flushes the data from console buffer to the display
fn flush(&self, state: &mut ConsoleState);
/// Writes characters to the backing buffer + handles special control sequences
fn write_char(&self, c: u8) {
let mut state = self.state().lock();
if state.putc(c) {
self.flush(&mut state);
}
}
/// Returns the dimensions of the console in chars: (rows, columns)
fn text_dimensions(&self) -> (usize, usize) {
let state = self.state().lock();
(state.buffer.height as _, CONSOLE_ROW_LEN as _)
}
}
impl ConsoleChar {
/// Empty character
pub const BLANK: Self = Self {
attributes: 0,
char: 0,
_pad: 0,
};
/// Constructs a console character from a char and its attributes
#[inline(always)]
pub fn from_parts(
char: u8,
fg: ColorAttribute,
bg: ColorAttribute,
attributes: Attributes,
) -> Self {
let attributes = ((attributes.bits() as u16) << 8)
| ((u8::from(bg) as u16) << 4)
| (u8::from(fg) as u16);
Self {
attributes,
char,
_pad: 0,
}
}
/// Returns the attributes of the character
#[inline(always)]
pub fn attributes(self) -> (ColorAttribute, ColorAttribute, Attributes) {
let fg =
ColorAttribute::try_from((self.attributes & 0xF) as u8).unwrap_or(DEFAULT_FG_COLOR);
let bg = ColorAttribute::try_from(((self.attributes >> 4) & 0xF) as u8)
.unwrap_or(DEFAULT_BG_COLOR);
let attributes =
Attributes::from_bits((self.attributes >> 8) as u8).unwrap_or(Attributes::empty());
(fg, bg, attributes)
}
/// Returns the character data of this [ConsoleChar]
#[inline(always)]
pub const fn character(self) -> u8 {
self.char
}
}
impl<'a> RowIter<'a> {
/// Returns the next dirty row
pub fn next_dirty(&mut self) -> Option<(u32, &[ConsoleChar])> {
loop {
if self.index == self.buffer.height {
return None;
}
if !self.buffer.rows[self.index as usize].clear_dirty() {
self.index += 1;
continue;
}
let row_index = self.index;
let row = &self.buffer.rows[self.index as usize];
self.index += 1;
return Some((row_index, &row.chars));
}
}
}
impl ConsoleRow {
/// Constructs a row filled with blank characters
pub const fn zeroed() -> Self {
Self {
dirty: 1,
chars: [ConsoleChar {
attributes: ((DEFAULT_BG_COLOR as u8) as u16) << 4,
char: b' ',
_pad: 0,
}; CONSOLE_ROW_LEN],
}
}
/// Returns `true` if the row's dirty flag is set
#[inline]
pub const fn is_dirty(&self) -> bool {
self.dirty != 0
}
/// Clears "dirty" flag for the row
#[inline]
pub fn clear_dirty(&mut self) -> bool {
let old = self.dirty;
self.dirty = 0;
old == 1
}
/// Clears the console row with blank characters
pub fn clear(&mut self, bg: ColorAttribute) {
self.dirty = 1;
self.chars
.fill(ConsoleChar::from_parts(b' ', bg, bg, Attributes::empty()));
}
}
impl ConsoleBuffer {
/// Constructs a fixed-size console buffer
pub fn new(height: u32) -> Result<Self, Error> {
// let size = size_of::<ConsoleRow>() * (height as usize);
let mut rows = vec![ConsoleRow::zeroed(); height as usize];
for row in rows.iter_mut() {
row.clear(DEFAULT_BG_COLOR);
}
Ok(Self { rows, height })
// let size = size_of::<ConsoleRow>() * (height as usize);
// let page_count = (size + 0xFFF) / 0x1000;
// let pages = phys::alloc_pages_contiguous(page_count, PageUsage::Used)?;
// let rows = unsafe {
// core::slice::from_raw_parts_mut(pages.virtualize() as *mut ConsoleRow, height as usize)
// };
// for row in rows.iter_mut() {
// row.clear(DEFAULT_BG_COLOR);
// }
// Ok(Self { rows, height })
}
#[inline(never)]
fn set_char(&mut self, row: u32, col: u32, c: ConsoleChar) {
self.rows[row as usize].dirty = 1;
self.rows[row as usize].chars[col as usize] = c;
}
#[inline(never)]
fn set_dirty(&mut self, row: u32) {
self.rows[row as usize].dirty = 1;
}
/// Returns an iterator over dirty rows, while clearing dirty flag for them
pub fn flush_rows(&mut self) -> RowIter {
RowIter {
buffer: self,
index: 0,
}
}
fn clear(&mut self, bg: ColorAttribute) {
for row in self.rows.iter_mut() {
row.clear(bg);
}
}
fn clear_row(&mut self, row: u32, bg: ColorAttribute) {
self.rows[row as usize].dirty = 1;
self.rows[row as usize].clear(bg);
}
fn erase_in_row(&mut self, row: u32, start: usize, bg: ColorAttribute) {
self.rows[row as usize].dirty = 1;
self.rows[row as usize].chars[start..].fill(ConsoleChar::from_parts(
b' ',
DEFAULT_FG_COLOR,
bg,
Attributes::empty(),
));
}
fn scroll_once(&mut self, bg: ColorAttribute) {
self.rows.copy_within(1.., 0);
self.rows[(self.height - 1) as usize].clear(bg);
// Mark everything dirty
self.rows.iter_mut().for_each(|row| {
row.dirty = 1;
});
}
}
impl ConsoleState {
/// Constructs a new console state with given buffer
pub fn new(buffer: ConsoleBuffer) -> Self {
Self {
cursor_row: 0,
cursor_col: 0,
fg_color: DEFAULT_FG_COLOR,
bg_color: DEFAULT_BG_COLOR,
attributes: Attributes::empty(),
esc_args: StaticVector::new(),
esc_state: EscapeState::Normal,
buffer,
}
}
fn putc_normal(&mut self, c: u8) -> bool {
let mut flush = false;
match c {
c if c >= 127 => {
self.buffer.set_char(
self.cursor_row,
self.cursor_col,
ConsoleChar::from_parts(
b'?',
self.fg_color,
ColorAttribute::Red,
self.attributes,
),
);
self.cursor_col += 1;
}
b'\x1b' => {
self.esc_state = EscapeState::Escape;
return false;
}
b'\r' => {
self.cursor_col = 0;
}
b'\n' => {
self.cursor_row += 1;
self.cursor_col = 0;
flush = true;
}
_ => {
self.buffer.set_char(
self.cursor_row,
self.cursor_col,
ConsoleChar::from_parts(c, self.fg_color, self.bg_color, self.attributes),
);
self.cursor_col += 1;
}
}
if self.cursor_col == CONSOLE_ROW_LEN as u32 {
self.cursor_col = 0;
self.cursor_row += 1;
}
if self.cursor_row == self.buffer.height {
self.buffer.scroll_once(self.bg_color);
self.cursor_row = self.buffer.height - 1;
flush = true;
}
flush
}
fn handle_csi(&mut self, c: u8) -> bool {
match c {
// Move back one character
b'D' => {
if self.cursor_col > 0 {
self.cursor_col -= 1;
}
}
// Manipulate display attributes
b'm' => {
if let Some(arg) = self.esc_args.first() {
match arg {
// Reset
0 => {
self.fg_color = DEFAULT_FG_COLOR;
self.bg_color = DEFAULT_BG_COLOR;
self.attributes = Attributes::empty();
}
// Bold
1 => {
self.attributes |= Attributes::BOLD;
}
// Foreground colors
30..=39 => {
let vt_color = self.esc_args[0] % 10;
if vt_color == 9 {
self.fg_color = DEFAULT_FG_COLOR;
} else {
self.fg_color = ColorAttribute::from_vt100(vt_color as u8);
}
}
// Background colors
40..=49 => {
let vt_color = self.esc_args[0] % 10;
if vt_color == 9 {
self.bg_color = DEFAULT_BG_COLOR;
} else {
self.bg_color = ColorAttribute::from_vt100(vt_color as u8);
}
}
_ => (),
}
}
}
// Move cursor to position
b'f' => {
let row = self.esc_args[0].clamp(1, self.buffer.height) - 1;
let col = self.esc_args[1].clamp(1, CONSOLE_ROW_LEN as u32) - 1;
self.buffer.set_dirty(row);
self.cursor_row = row;
self.cursor_col = col;
}
// Clear rows/columns/screen
b'J' => match self.esc_args[0] {
// Erase lines down
0 => (),
// Erase lines up
1 => (),
// Erase all
2 => {
self.buffer.clear(self.bg_color);
}
_ => (),
},
// Erase in Line
b'K' => match self.esc_args[0] {
// Erase to Right
0 => {
self.buffer
.erase_in_row(self.cursor_row, self.cursor_col as _, self.bg_color);
}
// Erase All
2 => {
self.buffer.clear_row(self.cursor_row, self.bg_color);
}
_ => (),
},
_ => (),
}
self.esc_state = EscapeState::Normal;
false
}
fn handle_csi_byte(&mut self, c: u8) -> bool {
match c {
b'0'..=b'9' => {
let arg = self.esc_args.last_mut().unwrap();
*arg *= 10;
*arg += (c - b'0') as u32;
false
}
b';' => {
self.esc_args.push(0);
false
}
_ => self.handle_csi(c),
}
}
fn putc(&mut self, c: u8) -> bool {
match self.esc_state {
EscapeState::Normal => self.putc_normal(c),
EscapeState::Escape => match c {
b'[' => {
self.esc_state = EscapeState::Csi;
self.esc_args.clear();
self.esc_args.push(0);
false
}
_ => {
self.esc_state = EscapeState::Normal;
self.esc_args.clear();
false
}
},
EscapeState::Csi => self.handle_csi_byte(c),
}
}
}
impl DebugSink for dyn DisplayConsole {
fn putc(&self, c: u8) -> Result<(), Error> {
self.write_char(c);
Ok(())
}
fn supports_control_sequences(&self) -> bool {
true
}
}
impl TerminalOutput for ConsoleWrapper<'_> {
fn size(&self) -> TerminalSize {
let (rows, columns) = self.0.text_dimensions();
TerminalSize { rows, columns }
}
fn write(&self, byte: u8) -> Result<(), Error> {
self.0.write_char(byte);
Ok(())
}
}
static CONSOLES: IrqSafeSpinlock<Vec<&'static dyn DisplayConsole>> =
IrqSafeSpinlock::new(Vec::new());
/// Adds a console device to a auto-flush list
pub fn add_console_autoflush(console: &'static dyn DisplayConsole) {
CONSOLES.lock().push(console);
}
/// Flushes console buffers to their displays
pub fn flush_consoles() {
for console in CONSOLES.lock().iter() {
let mut state = console.state().lock();
console.flush(&mut state);
}
}
/// Periodically flushes data from console buffers onto their displays
pub async fn update_consoles_task() {
loop {
flush_consoles();
runtime::sleep(Duration::from_millis(20)).await;
}
}