From 7f7968af6c7f7eedcfae8c12ee4f9c2596107b2f Mon Sep 17 00:00:00 2001
From: Eemeli <eemeli.o.lehtonen@utu.fi>
Date: Sun, 15 Jan 2023 05:43:49 +0200
Subject: [PATCH] framebuffer logger + some ansi escapes

---
 src/term/escape/decode.rs | 108 ++++++++++++++++++++++++++++++
 src/term/escape/encode.rs |  62 +++++++++++++++++
 src/term/escape/mod.rs    |   2 +
 src/term/mod.rs           |   1 +
 src/video/font.bmp        | Bin 0 -> 196746 bytes
 src/video/logger.rs       | 137 ++++++++++++++++++++++++++++++++++++++
 6 files changed, 310 insertions(+)
 create mode 100644 src/term/escape/decode.rs
 create mode 100644 src/term/escape/encode.rs
 create mode 100644 src/term/escape/mod.rs
 create mode 100644 src/term/mod.rs
 create mode 100644 src/video/font.bmp
 create mode 100644 src/video/logger.rs

diff --git a/src/term/escape/decode.rs b/src/term/escape/decode.rs
new file mode 100644
index 0000000..9ce4e1b
--- /dev/null
+++ b/src/term/escape/decode.rs
@@ -0,0 +1,108 @@
+use crate::video::framebuffer::Color;
+
+/// foreground color can be changed like this: "\x1B[38;2;<r>;<g>;<b>m"
+/// background color can be changed like this: "\x1B[48;2;<r>;<g>;<b>m"
+///
+/// THESE ARE NON STANDARD ESCAPE SEQUENCES
+pub struct EscapeDecoder {
+    buf: [u8; LONGEST_ESCAPE],
+    len: u8,
+}
+
+pub enum DecodedPart {
+    Byte(u8),
+
+    /// Null terminated
+    Bytes([u8; LONGEST_ESCAPE]),
+
+    FgColor(Color),
+    BgColor(Color),
+    Reset,
+
+    None,
+}
+
+//
+
+impl EscapeDecoder {
+    pub const fn new() -> Self {
+        Self {
+            buf: [0; LONGEST_ESCAPE],
+            len: 0,
+        }
+    }
+
+    pub fn next(&mut self, byte: u8) -> DecodedPart {
+        match (self.len, byte) {
+            (0, b'\x1B') => {
+                self.len += 1;
+                self.buf[0 as usize] = byte;
+                DecodedPart::None
+            }
+            (0, _) => DecodedPart::Byte(byte),
+            (1, b'[') => {
+                self.len += 1;
+                self.buf[1 as usize] = byte;
+                DecodedPart::None
+            }
+            (i, b'm') => {
+                self.len += 1;
+                self.buf[i as usize] = byte;
+
+                // crate::qemu::_print(format_args_nl!(
+                //     "seq part: {:?}",
+                //     core::str::from_utf8(&self.buf[..self.len as usize])
+                // ));
+
+                let result = match self.buf[..self.len as usize] {
+                    [b'\x1B', b'[', b'3', b'8', b';', b'2', b';', ref rgb @ .., b'm'] => {
+                        Self::parse_rgb_part(rgb).map(DecodedPart::FgColor)
+                    }
+                    [b'\x1B', b'[', b'4', b'8', b';', b'2', b';', ref rgb @ .., b'm'] => {
+                        Self::parse_rgb_part(rgb).map(DecodedPart::BgColor)
+                    }
+                    [b'\x1B', b'[', b'm'] => Some(DecodedPart::Reset),
+                    _ => None,
+                };
+
+                if let Some(result) = result {
+                    self.clear();
+                    result
+                } else {
+                    self.clear()
+                }
+            }
+            (i @ LONGEST_ESCAPE_PREV_U8.., _) => {
+                self.len += 1;
+                self.buf[i as usize] = byte;
+                self.clear()
+            }
+            (i, _) => {
+                self.len += 1;
+                self.buf[i as usize] = byte;
+                DecodedPart::None
+            }
+        }
+    }
+
+    pub fn clear(&mut self) -> DecodedPart {
+        self.len = 0;
+        DecodedPart::Bytes(core::mem::take(&mut self.buf))
+    }
+
+    fn parse_rgb_part(rgb: &[u8]) -> Option<Color> {
+        let mut iter = rgb.split(|c| *c == b';');
+        let r = core::str::from_utf8(iter.next()?).ok()?.parse().ok()?;
+        let g = core::str::from_utf8(iter.next()?).ok()?.parse().ok()?;
+        let b = core::str::from_utf8(iter.next()?).ok()?.parse().ok()?;
+        Some(Color::new(r, g, b))
+    }
+}
+
+//
+
+// longest supported: "\x1B[48;2;255;255;255m"
+const LONGEST_ESCAPE: usize = "\x1B[48;2;255;255;255m".len();
+const LONGEST_ESCAPE_PREV: usize = LONGEST_ESCAPE - 1;
+const LONGEST_ESCAPE_U8: u8 = LONGEST_ESCAPE as u8;
+const LONGEST_ESCAPE_PREV_U8: u8 = LONGEST_ESCAPE as u8 - 1;
diff --git a/src/term/escape/encode.rs b/src/term/escape/encode.rs
new file mode 100644
index 0000000..23af242
--- /dev/null
+++ b/src/term/escape/encode.rs
@@ -0,0 +1,62 @@
+use core::fmt;
+
+//
+
+pub trait EscapeEncoder {
+    fn with_escape_code<'a>(&'a self, code: &'a str) -> EncodedPart<'a, Self> {
+        EncodedPart { code, data: self }
+    }
+
+    fn red(&self) -> EncodedPart<Self> {
+        self.with_escape_code("\x1B[38;2;255;0;0m")
+    }
+
+    fn green(&self) -> EncodedPart<Self> {
+        self.with_escape_code("\x1B[38;2;0;255;0m")
+    }
+
+    fn blue(&self) -> EncodedPart<Self> {
+        self.with_escape_code("\x1B[38;2;0;0;255m")
+    }
+
+    fn cyan(&self) -> EncodedPart<Self> {
+        self.with_escape_code("\x1B[38;2;0;255;255m")
+    }
+
+    fn magenta(&self) -> EncodedPart<Self> {
+        self.with_escape_code("\x1B[38;2;255;0;255m")
+    }
+
+    fn yellow(&self) -> EncodedPart<Self> {
+        self.with_escape_code("\x1B[38;2;255;255;0m")
+    }
+}
+
+pub struct EncodedPart<'a, T: ?Sized> {
+    code: &'a str,
+    data: &'a T,
+}
+
+//
+
+impl EscapeEncoder for &str {}
+
+impl<'a, T> fmt::Display for EncodedPart<'a, T>
+where
+    T: fmt::Display,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}{}\x1B[m", self.code, self.data)
+    }
+}
+
+impl<'a, T> fmt::Debug for EncodedPart<'a, T>
+where
+    T: fmt::Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.code)?;
+        self.data.fmt(f)?;
+        write!(f, "\x1B[m")
+    }
+}
diff --git a/src/term/escape/mod.rs b/src/term/escape/mod.rs
new file mode 100644
index 0000000..1c80d24
--- /dev/null
+++ b/src/term/escape/mod.rs
@@ -0,0 +1,2 @@
+pub mod decode;
+pub mod encode;
diff --git a/src/term/mod.rs b/src/term/mod.rs
new file mode 100644
index 0000000..46e0349
--- /dev/null
+++ b/src/term/mod.rs
@@ -0,0 +1 @@
+pub mod escape;
diff --git a/src/video/font.bmp b/src/video/font.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..d80f083c72b977d9d74b6382872c77f1c010d486
GIT binary patch
literal 196746
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
CZ0*tj

literal 0
HcmV?d00001

diff --git a/src/video/logger.rs b/src/video/logger.rs
new file mode 100644
index 0000000..e1ef1a8
--- /dev/null
+++ b/src/video/logger.rs
@@ -0,0 +1,137 @@
+use crate::term::escape::decode::{DecodedPart, EscapeDecoder};
+
+use super::{
+    font::FONT,
+    framebuffer::{get_fbo, Color, Framebuffer, FBO},
+};
+use core::fmt::{self, Arguments, Write};
+use spin::{Lazy, Mutex, MutexGuard, Once};
+
+//
+
+pub fn _print(args: Arguments) {
+    _ = WRITER.lock().write_fmt(args)
+}
+
+//
+
+static WRITER: Mutex<Writer> = Mutex::new(Writer::new());
+
+//
+
+struct Writer {
+    cursor: [u16; 2],
+    fg_color: Color,
+    bg_color: Color,
+
+    escapes: EscapeDecoder,
+}
+
+//
+
+impl Writer {
+    pub fn write_bytes(&mut self, bytes: &[u8]) {
+        for byte in bytes {
+            self.write_byte(*byte)
+        }
+    }
+
+    pub fn write_byte(&mut self, byte: u8) {
+        match self.escapes.next(byte) {
+            DecodedPart::Byte(b'\n') => {
+                if let Some(mut fbo) = get_fbo() {
+                    self.new_line(1, &mut fbo)
+                }
+            }
+            DecodedPart::Byte(b'\t') => {
+                self.cursor[0] = (self.cursor[0] / 4 + 1) * 4;
+            }
+
+            DecodedPart::Byte(byte) => self.write_byte_raw(byte),
+            DecodedPart::Bytes(bytes) => bytes
+                .into_iter()
+                .take_while(|b| *b != 0)
+                .for_each(|byte| self.write_byte_raw(byte)),
+
+            DecodedPart::FgColor(color) => self.fg_color = color,
+            DecodedPart::BgColor(color) => self.bg_color = color,
+            DecodedPart::Reset => {
+                self.fg_color = Self::FG_COLOR;
+                self.bg_color = Self::BG_COLOR;
+            }
+
+            DecodedPart::None => {}
+        }
+    }
+
+    pub fn write_byte_raw(&mut self, byte: u8) {
+        if let Some(mut fbo) = get_fbo() {
+            let size = Self::size(&mut fbo);
+            if size[0] == 0 || size[1] == 0 {
+                return;
+            }
+
+            self._write_byte_raw(byte, &mut fbo);
+        }
+    }
+
+    const FG_COLOR: Color = Color::from_hex("#bbbbbb");
+    const BG_COLOR: Color = Color::from_hex("#000000");
+
+    const fn new() -> Self {
+        Self {
+            cursor: [0; 2],
+            fg_color: Self::FG_COLOR,
+            bg_color: Self::BG_COLOR,
+
+            escapes: EscapeDecoder::new(),
+        }
+    }
+
+    fn _write_byte_raw(&mut self, byte: u8, fbo: &mut MutexGuard<Framebuffer>) {
+        let (map, is_double) = FONT[byte as usize];
+
+        // insert a new line if the next character would be off screen
+        if self.cursor[0] + if is_double { 1 } else { 0 } >= Self::size(fbo)[0] {
+            self.new_line(8, fbo);
+        }
+
+        let (x, y) = (self.cursor[0] as usize * 8, self.cursor[1] as usize * 16);
+        self.cursor[0] += if is_double { 2 } else { 1 };
+
+        for (yd, row) in map.into_iter().enumerate() {
+            for xd in 0..if is_double { 16 } else { 8 } {
+                fbo.set(
+                    x + xd,
+                    y + yd,
+                    if (row & 1 << xd) != 0 {
+                        self.fg_color
+                    } else {
+                        self.bg_color
+                    },
+                );
+            }
+        }
+    }
+
+    fn new_line(&mut self, count: u16, fbo: &mut MutexGuard<Framebuffer>) {
+        self.cursor[0] = 0;
+        self.cursor[1] += 1;
+        if self.cursor[1] >= Self::size(fbo)[1] {
+            let scroll_count = count.min(self.cursor[1]);
+            self.cursor[1] -= scroll_count;
+            fbo.scroll(16 * scroll_count as usize);
+        }
+    }
+
+    fn size(fbo: &mut MutexGuard<Framebuffer>) -> [u16; 2] {
+        [(fbo.width / 16) as _, (fbo.height / 16) as _]
+    }
+}
+
+impl fmt::Write for Writer {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        self.write_bytes(s.as_bytes());
+        Ok(())
+    }
+}
-- 
GitLab