diff --git a/.cargo/config.toml b/.cargo/config.toml
index b2570e105ae3efc2a2616570deb567a155b69425..c04d9e59909d4e93da02fb84aa9c70d46c85d0c3 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,5 +1,8 @@
-#[build]
-#target = "x86_64-unknown-none"
+[alias]
+debug = "run -- --debug"
+
+[build]
+target = "x86_64-unknown-none"
 
 [target.x86_64-unknown-none]
 runner = "./.cargo/runner.sh"
diff --git a/.cargo/runner.sh b/.cargo/runner.sh
index 5914a40ca832eeb293638ee09e78f146e92fb401..b7c557b4cd615ee6aa9947d866208d16374fefd4 100755
--- a/.cargo/runner.sh
+++ b/.cargo/runner.sh
@@ -4,6 +4,8 @@
 
 set -xe
 
+echo $@
+
 LIMINE_GIT_URL="https://github.com/limine-bootloader/limine.git"
 ISO_DIR=target/hyperion/x86_64/iso
 KERNEL=$1
@@ -21,7 +23,8 @@ cd -
 
 # Copy the needed files into an ISO image.
 mkdir -p $ISO_DIR
-cp $KERNEL cfg/limine.cfg target/limine/limine{.sys,-cd.bin,-cd-efi.bin} $ISO_DIR
+cp cfg/limine.cfg target/limine/limine{.sys,-cd.bin,-cd-efi.bin} $ISO_DIR
+cp $KERNEL $ISO_DIR/hyperion
 
 xorriso -as mkisofs \
     -b limine-cd.bin \
@@ -33,9 +36,37 @@ xorriso -as mkisofs \
 # For the image to be bootable on BIOS systems, we must run `limine-deploy` on it.
 target/limine/limine-deploy $KERNEL.iso
 
-# Run the created image with QEMU.
-qemu-system-x86_64 \
-    -machine q35 -cpu qemu64 -M smm=off \
-    -D target/log.txt -d int,guest_errors -no-reboot -no-shutdown \
-    -serial stdio \
-    $KERNEL.iso
+# A hack to detect if the kernel is a testing kernel
+# Cargo test binary generates a 'random id' for testing binaries
+if [ "$(basename $KERNEL)" = "hyperion" ]; then
+    # Run the created image with QEMU.
+    qemu-system-x86_64 \
+        -machine q35 \
+        -cpu qemu64 \
+        -M smm=off \
+        -d int,guest_errors,cpu_reset \
+        -no-reboot \
+        -serial stdio \
+        $KERNEL.iso
+    #-s -S \
+    #-no-shutdown \
+    #-D target/log.txt \
+else
+    set +e
+    # Run the created image with QEMU.
+    qemu-system-x86_64 \
+        -machine q35 \
+        -cpu qemu64 \
+        -M smm=off \
+        -d int,guest_errors,cpu_reset \
+        -device isa-debug-exit,iobase=0xf4,iosize=0x04 \
+        -no-reboot \
+        -serial stdio \
+        -display none \
+        $KERNEL.iso
+    #-no-shutdown \
+    #-D target/log.txt \
+
+    [ $? -ne 33 ] && exit 1
+    exit 0
+fi
diff --git a/.gdbinit b/.gdbinit
new file mode 100644
index 0000000000000000000000000000000000000000..fa861110fe456dcf5b884388197d5ee8e4632e0c
--- /dev/null
+++ b/.gdbinit
@@ -0,0 +1,5 @@
+file target/hyperion/x86_64/iso/hyperion
+
+target remote localhost:1234
+
+symbol-file target/hyperion/x86_64/iso/hyperion
diff --git a/cfg/limine.cfg b/cfg/limine.cfg
index 1777d21e9ef695df6a3a46ba4f16d936389bd62f..fd84e951bfc26b9fe605807bffd8b7678fd12090 100644
--- a/cfg/limine.cfg
+++ b/cfg/limine.cfg
@@ -4,3 +4,4 @@ TIMEOUT=0
 :Hyperion
     PROTOCOL=limine
     KERNEL_PATH=boot:///hyperion
+    # KERNEL_CMDLINE=
diff --git a/src/arch/x86_64/gdt.rs b/src/arch/x86_64/gdt.rs
new file mode 100644
index 0000000000000000000000000000000000000000..052643f04205884ab17a6269c6fc41ae041f9bd2
--- /dev/null
+++ b/src/arch/x86_64/gdt.rs
@@ -0,0 +1,60 @@
+use super::idt::DOUBLE_FAULT_IST;
+use spin::Lazy;
+use x86_64::{
+    instructions::tables::load_tss,
+    registers::segmentation::{Segment, CS, SS},
+    structures::{
+        gdt::{Descriptor, GlobalDescriptorTable, SegmentSelector},
+        tss::TaskStateSegment,
+    },
+    VirtAddr,
+};
+
+//
+
+pub fn init() {
+    GDT.0.load();
+
+    unsafe {
+        CS::set_reg(GDT.1.kc);
+        SS::set_reg(GDT.1.kd);
+        load_tss(GDT.1.tss);
+    }
+}
+
+//
+
+struct SegmentSelectors {
+    kc: SegmentSelector,
+    kd: SegmentSelector,
+    tss: SegmentSelector,
+}
+
+static GDT: Lazy<(GlobalDescriptorTable, SegmentSelectors)> = Lazy::new(|| {
+    let mut gdt = GlobalDescriptorTable::new();
+    let sel = SegmentSelectors {
+        kc: gdt.add_entry(Descriptor::kernel_code_segment()),
+        kd: gdt.add_entry(Descriptor::kernel_data_segment()),
+        tss: gdt.add_entry(Descriptor::tss_segment(&TSS)),
+    };
+    // gdt.add_entry(Descriptor::user_code_segment());
+    // gdt.add_entry(Descriptor::user_data_segment());
+    (gdt, sel)
+});
+
+static TSS: Lazy<TaskStateSegment> = Lazy::new(|| {
+    let mut tss = TaskStateSegment::new();
+    tss.interrupt_stack_table[DOUBLE_FAULT_IST as usize] = {
+        static mut STACK: [u8; 4096 * 5] = [0; 4096 * 5];
+
+        let stack_range = unsafe { STACK }.as_ptr_range();
+        VirtAddr::from_ptr(stack_range.end)
+    };
+    tss.privilege_stack_table[0] = {
+        static mut STACK: [u8; 4096 * 5] = [0; 4096 * 5];
+
+        let stack_range = unsafe { STACK }.as_ptr_range();
+        VirtAddr::from_ptr(stack_range.end)
+    };
+    tss
+});
diff --git a/src/arch/x86_64/idt.rs b/src/arch/x86_64/idt.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6866baa600893a2b20aaee0fede23cdcab84031b
--- /dev/null
+++ b/src/arch/x86_64/idt.rs
@@ -0,0 +1,55 @@
+use crate::println;
+use spin::Lazy;
+use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
+
+//
+
+pub static DOUBLE_FAULT_IST: u16 = 1;
+
+//
+
+pub fn init() {
+    IDT.load();
+}
+
+//
+
+extern "x86-interrupt" fn breakpoint(stack: InterruptStackFrame) {
+    println!("INT: Breakpoint\n{stack:#?}")
+}
+
+extern "x86-interrupt" fn double_fault(stack: InterruptStackFrame, ec: u64) -> ! {
+    // SAFETY: Unlocking the Mutex is safe if this is the only CPU running
+    //
+    // This CPU might have locked the COM1 writer and then stack-overflowed before unlocking it but
+    // we won't return anyways, so lets just unlock it
+    unsafe {
+        // TODO: This won't be safe when multiple CPUs are running
+        crate::qemu::unlock();
+    }
+    panic!("INT: Double fault ({ec})\n{stack:#?}")
+}
+
+//
+
+static IDT: Lazy<InterruptDescriptorTable> = Lazy::new(|| {
+    let mut idt = InterruptDescriptorTable::new();
+    idt.breakpoint.set_handler_fn(breakpoint);
+    unsafe {
+        idt.double_fault
+            .set_handler_fn(double_fault)
+            .set_stack_index(DOUBLE_FAULT_IST);
+    }
+    idt
+});
+
+//
+
+#[cfg(test)]
+mod tests {
+    #[test_case]
+    fn breakpoint() {
+        // breakpoint instruction
+        x86_64::instructions::interrupts::int3();
+    }
+}
diff --git a/src/arch/x86_64/limine/framebuffer.rs b/src/arch/x86_64/limine/framebuffer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..70df236452e161ef9752b0b8ec75fda6f3ec0799
--- /dev/null
+++ b/src/arch/x86_64/limine/framebuffer.rs
@@ -0,0 +1,37 @@
+use crate::{
+    println,
+    video::framebuffer::{Framebuffer, FBO},
+};
+use core::{ops::Deref, slice};
+use limine::{LimineFramebuffer, LimineFramebufferRequest, LimineFramebufferResponse};
+use spin::{Lazy, Mutex, MutexGuard};
+
+//
+
+pub fn init() {
+    static FB_REQ: LimineFramebufferRequest = LimineFramebufferRequest::new(0);
+
+    let fbo = FB_REQ
+        .get_response()
+        .get()
+        .into_iter()
+        .flat_map(|resp| resp.framebuffers().into_iter())
+        .find_map(|fb| {
+            if fb.bpp != 32 {
+                return None;
+            }
+
+            let buf = unsafe { slice::from_raw_parts_mut(fb.address.as_ptr()?, fb.size()) };
+            Some(Framebuffer {
+                buf,
+                width: fb.width as _,
+                height: fb.height as _,
+                pitch: fb.pitch as _,
+            })
+        });
+
+    if let Some(fbo) = fbo {
+        FBO.call_once(|| Mutex::new(fbo));
+    }
+    println!("Global framebuffer {:#?}", FBO.get())
+}
diff --git a/src/arch/x86_64/limine/mod.rs b/src/arch/x86_64/limine/mod.rs
index ef9274aceb65802760e45470b8bafa801a6a9c00..032aa5ea4b50beb1940861d6828d2ee82bfdba1a 100644
--- a/src/arch/x86_64/limine/mod.rs
+++ b/src/arch/x86_64/limine/mod.rs
@@ -1,52 +1,36 @@
-use core::fmt::{self, Arguments, Write};
-use limine::{LimineTerminalRequest, LimineTerminalResponse};
-use spin::{Lazy, Mutex, MutexGuard, Once};
+use super::{gdt, idt};
 
 //
 
-#[no_mangle]
-pub extern "C" fn _start() -> ! {
-    *crate::BOOTLOADER.lock() = "Limine";
-    crate::kernel_main()
-}
+pub use term::_print;
 
 //
 
-struct Writer(pub &'static LimineTerminalResponse);
+mod framebuffer;
+mod term;
 
-unsafe impl Send for Writer {}
+//
 
-impl Write for Writer {
-    fn write_str(&mut self, s: &str) -> fmt::Result {
-        let mut write = self.0.write().ok_or(fmt::Error)?;
+#[no_mangle]
+pub extern "C" fn _start() -> ! {
+    x86_64::instructions::interrupts::disable();
+    *crate::BOOTLOADER.lock() = "Limine";
 
-        for term in self.0.terminals() {
-            write(term, s);
-        }
+    framebuffer::init();
 
-        Ok(())
-    }
-}
+    // the initial terminal logger crashes if used after initializing GDT and IDT
+    crate::log::disable_term();
 
-static TERMINALS: LimineTerminalRequest = LimineTerminalRequest::new(0);
-static WRITER: Once<Mutex<Writer>> = Once::new();
+    gdt::init();
+    idt::init();
 
-fn get() -> Result<MutexGuard<'static, Writer>, fmt::Error> {
-    WRITER.try_call_once(|| {
-        Ok(Mutex::new(Writer(
-            TERMINALS.get_response().get().ok_or(fmt::Error)?,
-        )))
-    })?;
-    WRITER.get().ok_or(fmt::Error).map(|mutex| mutex.lock())
-}
+    x86_64::instructions::interrupts::enable();
 
-fn print(args: Arguments) -> Option<()> {
-    Some(())
+    crate::kernel_main()
 }
 
-#[doc(hidden)]
-pub fn _print(args: Arguments) {
-    if let Ok(mut writer) = get() {
-        _ = writer.write_fmt(args)
+pub fn done() -> ! {
+    loop {
+        x86_64::instructions::hlt();
     }
 }
diff --git a/src/arch/x86_64/limine/term.rs b/src/arch/x86_64/limine/term.rs
new file mode 100644
index 0000000000000000000000000000000000000000..97446b77877f01611012ee0c0c732d57570d3181
--- /dev/null
+++ b/src/arch/x86_64/limine/term.rs
@@ -0,0 +1,42 @@
+use core::fmt::{self, Arguments, Write};
+use limine::{LimineTerminalRequest, LimineTerminalResponse};
+use spin::{Mutex, MutexGuard, Once};
+
+//
+
+#[doc(hidden)]
+pub fn _print(args: Arguments) {
+    if let Ok(mut writer) = get() {
+        _ = writer.write_fmt(args)
+    }
+}
+
+//
+
+struct Writer(pub &'static LimineTerminalResponse);
+
+unsafe impl Send for Writer {}
+
+impl Write for Writer {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        let mut write = self.0.write().ok_or(fmt::Error)?;
+
+        for term in self.0.terminals() {
+            write(term, s);
+        }
+
+        Ok(())
+    }
+}
+
+static TERMINALS: LimineTerminalRequest = LimineTerminalRequest::new(0);
+static WRITER: Once<Mutex<Writer>> = Once::new();
+
+fn get() -> Result<MutexGuard<'static, Writer>, fmt::Error> {
+    WRITER.try_call_once(|| {
+        Ok(Mutex::new(Writer(
+            TERMINALS.get_response().get().ok_or(fmt::Error)?,
+        )))
+    })?;
+    WRITER.get().ok_or(fmt::Error).map(|mutex| mutex.lock())
+}
diff --git a/src/arch/x86_64/mod.rs b/src/arch/x86_64/mod.rs
index 42f8285e42045437e0c818ec14c0073133b4e7b8..34c7127a46141758b8460ed11e2f1fdd8e05c7c1 100644
--- a/src/arch/x86_64/mod.rs
+++ b/src/arch/x86_64/mod.rs
@@ -1,15 +1,16 @@
 #[cfg(feature = "multiboot1")]
 #[path = "multiboot1/mod.rs"]
 pub mod boot;
-
 #[cfg(feature = "multiboot2")]
 #[path = "multiboot2/mod.rs"]
 pub mod boot;
-
 #[cfg(feature = "bootboot")]
 #[path = "bootboot/mod.rs"]
 pub mod boot;
-
 #[cfg(feature = "limine")]
 #[path = "limine/mod.rs"]
 pub mod boot;
+pub use boot::*;
+
+pub mod gdt;
+pub mod idt;
diff --git a/src/log.rs b/src/log.rs
index b8e024dcbb05b35200b265a395b8583574400978..92d39e76880e313c536abd326346dae62c151fa0 100644
--- a/src/log.rs
+++ b/src/log.rs
@@ -1,4 +1,7 @@
-use core::fmt::Arguments;
+use core::{
+    fmt::Arguments,
+    sync::atomic::{AtomicBool, Ordering},
+};
 use spin::Lazy;
 
 //
@@ -10,8 +13,26 @@ macro_rules! print {
 
 #[macro_export]
 macro_rules! println {
-    ()          => { $crate::log::_print(format_args!("\n")); };
-    ($($t:tt)*) => { $crate::log::_print(format_args_nl!($($t)*)); };
+    ()          => { $crate::log::_print(format_args!("\n")) };
+    ($($t:tt)*) => { $crate::log::_print(format_args_nl!($($t)*)) };
+}
+
+//
+
+pub fn enable_term() {
+    LOGGER.term.store(true, Ordering::SeqCst);
+}
+
+pub fn disable_term() {
+    LOGGER.term.store(false, Ordering::SeqCst);
+}
+
+pub fn enable_qemu() {
+    LOGGER.qemu.store(true, Ordering::SeqCst);
+}
+
+pub fn disable_qemu() {
+    LOGGER.qemu.store(false, Ordering::SeqCst);
 }
 
 //
@@ -19,25 +40,25 @@ macro_rules! println {
 static LOGGER: Lazy<Logger> = Lazy::new(Logger::init);
 
 struct Logger {
-    term: bool,
-    qemu: bool,
+    term: AtomicBool,
+    qemu: AtomicBool,
 }
 
 impl Logger {
     fn init() -> Self {
         Logger {
-            term: true,
-            qemu: true,
+            term: true.into(),
+            qemu: true.into(),
         }
     }
 
     fn print(&self, args: Arguments) {
-        if self.term {
-            crate::arch::boot::_print(args);
-        }
-        if self.qemu {
+        if self.qemu.load(Ordering::SeqCst) {
             crate::qemu::_print(args);
         }
+        if self.term.load(Ordering::SeqCst) {
+            crate::arch::boot::_print(args);
+        }
     }
 }
 
diff --git a/src/main.rs b/src/main.rs
index 77827b89551ee834ce728191804b522f9f3f33f8..4d81da639e8ab8a3e2880d86b5ab32669c9c8087 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,29 +2,72 @@
 #![no_main]
 #![feature(format_args_nl)]
 #![feature(abi_x86_interrupt)]
+#![feature(custom_test_frameworks)]
+#![feature(type_alias_impl_trait)]
+#![test_runner(crate::testfw::test_runner)]
+#![reexport_test_harness_main = "test_main"]
+
+//
 
 use spin::Mutex;
 
+use crate::video::framebuffer::{Color, FBO};
+
+//
+
 #[path = "arch/x86_64/mod.rs"]
 pub mod arch;
 pub mod log;
+pub mod panic;
 pub mod qemu;
-// pub mod vga;
+#[cfg(test)]
+pub mod testfw;
+pub mod video;
 
-static BOOTLOADER: Mutex<&'static str> = Mutex::new("Hyperion");
+//
 
-#[panic_handler]
-fn panic_handler(_: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
+/// Name of the kernel
+pub static KERNEL: &'static str = if cfg!(test) {
+    "Hyperion-Testing"
+} else {
+    "Hyperion"
+};
+
+/// Name of the detected bootloader
+pub static BOOTLOADER: Mutex<&'static str> = Mutex::new(KERNEL);
+
+//
 
 fn kernel_main() -> ! {
-    println!("Hello from Hyperion");
-    println!(" - Hyperion was booted with {}", BOOTLOADER.lock());
+    println!("Hello from {KERNEL}");
+    println!(" - {KERNEL} was booted with {}", BOOTLOADER.lock());
+
+    // error handling test
+    // stack_overflow(79999999);
+    // unsafe {
+    //     *(0xFFFFFFFFDEADC0DE as *mut u8) = 42;
+    // }
 
-    loop {
-        unsafe {
-            core::arch::asm!("hlt");
-        }
+    if let Some(fbo) = FBO.get() {
+        let mut fbo = fbo.lock();
+        fbo.fill(40, 40, 40, 40, Color::RED);
+        fbo.fill(50, 50, 60, 40, Color::GREEN);
+        fbo.fill(5, 15, 80, 20, Color::BLUE);
+    }
+
+    #[cfg(test)]
+    test_main();
+
+    arch::done();
+}
+
+fn stack_overflow(n: usize) {
+    if n == 0 {
+        return;
+    } else {
+        stack_overflow(n - 1);
+    }
+    unsafe {
+        core::ptr::read_volatile(&0 as *const i32);
     }
 }
diff --git a/src/panic.rs b/src/panic.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d69b7998e0c087a58737629edd7786ab0bfd8165
--- /dev/null
+++ b/src/panic.rs
@@ -0,0 +1,18 @@
+use crate::arch::done;
+use core::panic::PanicInfo;
+
+//
+
+#[cfg(not(feature = "tests"))]
+#[panic_handler]
+fn panic_handler(info: &PanicInfo) -> ! {
+    crate::println!("{info}");
+    done();
+}
+
+#[cfg(feature = "tests")]
+#[panic_handler]
+fn panic_handler(info: &PanicInfo) -> ! {
+    crate::testfw::test_panic_handler(info);
+    done();
+}
diff --git a/src/qemu.rs b/src/qemu.rs
index d2c85f9e6540c4431149f7690b99aeaa30505fb6..cb30c3ac3d39da6d8a0db2a7558427464456a34e 100644
--- a/src/qemu.rs
+++ b/src/qemu.rs
@@ -1,19 +1,35 @@
-use core::fmt::{Arguments, Write};
+use core::{
+    fmt::{Arguments, Write},
+    sync::atomic::AtomicUsize,
+};
 use spin::{Lazy, Mutex};
 use uart_16550::SerialPort;
 
 //
 
+#[doc(hidden)]
+pub fn _print(args: Arguments) {
+    if let Some(mut writer) = COM1.try_lock() {
+        // COM1_LOCKER.store(crate::THREAD, Ordering::SeqCst);
+        _ = writer.write_fmt(args);
+    }
+}
+
+/// Unlocks the COM1 writer IF it is locked by this exact thread
+pub unsafe fn unlock() {
+    // TODO: SMP
+    // if COM1_LOCKER.load(Ordering::SeqCst) != crate::THREAD {
+    //     return;
+    // }
+
+    COM1.force_unlock()
+}
+
+//
+
+static COM1_LOCKER: AtomicUsize = AtomicUsize::new(0);
 static COM1: Lazy<Mutex<SerialPort>> = Lazy::new(|| {
     let mut port = unsafe { SerialPort::new(0x3f8) };
     port.init();
     Mutex::new(port)
 });
-
-//
-
-#[doc(hidden)]
-pub fn _print(args: Arguments) {
-    let mut writer = COM1.lock();
-    writer.write_fmt(args).unwrap();
-}
diff --git a/src/testfw.rs b/src/testfw.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a89324ce0f3d82bb6154664e4f3b4571b6230bcd
--- /dev/null
+++ b/src/testfw.rs
@@ -0,0 +1,71 @@
+use crate::{arch::done, print, println};
+use core::{any::type_name, panic::PanicInfo};
+use x86_64::instructions::port::Port;
+
+//
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[repr(u32)]
+pub enum QemuExitCode {
+    Success = 0x10,
+    Failed = 0x11,
+}
+
+pub trait TestCase {
+    fn run(&self);
+}
+
+//
+
+impl<F: Fn()> TestCase for F {
+    fn run(&self) {
+        let name = type_name::<Self>();
+        name.len();
+        print!(" - {name:.<40}");
+        self();
+        println!("[ok]");
+    }
+}
+
+//
+
+pub fn exit_qemu(exit_code: QemuExitCode) {
+    unsafe {
+        let mut port = Port::new(0xf4);
+        port.write(exit_code as u32);
+    }
+}
+
+pub fn test_runner(tests: &[&dyn TestCase]) {
+    println!("Running {} tests", tests.len());
+    for test in tests {
+        // unsafe {
+        //     core::intrinsics::r#try(
+        //         move |_| test(),
+        //         0 as _,
+        //         |_, _| {
+        //             println!("[failed]\n");
+        //         },
+        //     );
+        // }
+
+        // TODO: core::panic::catch_unwind // https://github.com/rust-lang/rfcs/issues/2810
+
+        test.run();
+    }
+
+    exit_qemu(QemuExitCode::Success);
+}
+
+pub fn test_panic_handler(info: &PanicInfo) {
+    println!("[failed]\n{info}\n");
+    exit_qemu(QemuExitCode::Failed);
+}
+
+#[cfg(test)]
+mod tests {
+    #[test_case]
+    fn trivial() {
+        assert_eq!(0, 0);
+    }
+}
diff --git a/src/vga.rs b/src/vga.rs
deleted file mode 100644
index 1c404904a5e17c622c1f5fd2ff643f6e3c476080..0000000000000000000000000000000000000000
--- a/src/vga.rs
+++ /dev/null
@@ -1,225 +0,0 @@
-use core::{
-    fmt::{Arguments, Write},
-    ops::{Deref, DerefMut},
-};
-use spin::{Mutex, MutexGuard};
-use volatile::Volatile;
-
-//
-
-pub struct Writer {
-    cursor: [usize; 2],
-    color: ColorCode,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-#[repr(C)]
-pub enum Color {
-    #[default]
-    Black = 0,
-    Blue = 1,
-    Green = 2,
-    Cyan = 3,
-    Red = 4,
-    Magenta = 5,
-    Brown = 6,
-    LightGrey = 7,
-    DarkGrey = 8,
-    LightBlue = 9,
-    LightGreen = 10,
-    LightCyan = 11,
-    LightRed = 12,
-    Pink = 13,
-    Yellow = 14,
-    White = 15,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-#[repr(transparent)]
-pub struct ColorCode(u8);
-
-//
-
-impl Writer {
-    pub fn lock() -> MutexGuard<'static, Self> {
-        WRITER.lock()
-    }
-
-    pub fn write_str(&mut self, s: &str) {
-        for byte in s.bytes() {
-            self.write_byte(byte);
-        }
-    }
-
-    pub fn write_char(&mut self, c: char) {
-        self.write_str(c.encode_utf8(&mut [0; 4]))
-    }
-
-    pub fn write_byte(&mut self, byte: u8) {
-        match byte {
-            // 'special' ascii chars
-            b'\r' => self.cursor[0] = 0,
-            b'\n' => self.new_line(),
-            b'\0' => self.clear(),
-
-            // 'normal' ascii chars
-            byte => {
-                // line wrapping
-                if self.cursor[0] >= WIDTH {
-                    self.new_line();
-                }
-
-                // insert the byte
-                self.set_char(
-                    self.cursor,
-                    Char {
-                        byte,
-                        color: self.color,
-                    },
-                );
-
-                // move the cursor
-                self.cursor[0] += 1;
-            }
-        }
-    }
-
-    pub fn clear(&mut self) {
-        self.cursor = [0, 0];
-        for row in 0..HEIGHT {
-            self.clear_row(row);
-        }
-    }
-
-    /// SAFETY: Only one [`Writer`] should ever exist
-    const unsafe fn new() -> Self {
-        Self {
-            cursor: [0, 0],
-            color: ColorCode::new(Color::White, Color::Black),
-        }
-    }
-
-    fn buffer(&self) -> &'static [[Volatile<Char>; WIDTH]; HEIGHT] {
-        // SAFETY: Only one [`Writer`] should ever exist
-        // then multiple immutable refs are allowed
-        unsafe { &*(0xB8000 as *const _) }
-    }
-
-    fn buffer_mut(&mut self) -> &'static mut [[Volatile<Char>; WIDTH]; HEIGHT] {
-        // SAFETY: Only one [`Writer`] should ever exist
-        // then one mutable ref is allowed
-        unsafe { &mut *(0xB8000 as *mut _) }
-    }
-
-    fn new_line(&mut self) {
-        if self.cursor[1] + 1 >= HEIGHT {
-            // move all rows upwards
-            for row in 0..HEIGHT - 1 {
-                for col in 0..WIDTH {
-                    self.set_char([col, row], self.get_char([col, row + 1]));
-                }
-            }
-        } else {
-            // next row
-            self.cursor[1] += 1;
-        }
-        self.clear_row(HEIGHT - 1);
-        self.cursor[0] = 0;
-    }
-
-    fn clear_row(&mut self, row: usize) {
-        self.fill_row(
-            row,
-            Char {
-                byte: b' ',
-                color: ColorCode::default(),
-            },
-        )
-    }
-
-    fn fill_row(&mut self, row: usize, fill: Char) {
-        for col in 0..WIDTH {
-            self.set_char([col, row], fill);
-        }
-    }
-
-    fn get_char(&self, cursor: [usize; 2]) -> Char {
-        self.buffer()[cursor[1]][cursor[0]].read()
-    }
-
-    fn set_char(&mut self, cursor: [usize; 2], ch: Char) {
-        self.buffer_mut()[cursor[1]][cursor[0]].write(ch);
-    }
-}
-
-impl ColorCode {
-    pub const fn new(fg: Color, bg: Color) -> ColorCode {
-        ColorCode((bg as u8) << 4 | (fg as u8))
-    }
-}
-
-impl Default for ColorCode {
-    fn default() -> Self {
-        Self::new(Color::White, Color::Black)
-    }
-}
-
-//
-
-const WIDTH: usize = 80;
-const HEIGHT: usize = 25;
-
-//
-
-/// SAFETY: safe, because this is the only Writer
-static WRITER: Mutex<Writer> = Mutex::new(unsafe { Writer::new() });
-
-//
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-#[repr(C)]
-struct Char {
-    // ascii
-    byte: u8,
-
-    // foreground and background
-    color: ColorCode,
-}
-
-//
-
-impl Write for Writer {
-    fn write_str(&mut self, s: &str) -> core::fmt::Result {
-        self.write_str(s);
-        Ok(())
-    }
-}
-
-impl Deref for Char {
-    type Target = Self;
-
-    fn deref(&self) -> &Self::Target {
-        self
-    }
-}
-
-impl DerefMut for Char {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        self
-    }
-}
-
-//
-
-#[doc(hidden)]
-pub fn _print(args: Arguments) {
-    let mut writer = WRITER.lock();
-    writer.write_fmt(args).unwrap();
-}
-
-#[doc(hidden)]
-pub fn _println(args: Arguments) {
-    let mut writer = WRITER.lock();
-    writer.write_fmt(args).unwrap();
-    writer.write_byte(b'\n');
-}
diff --git a/src/video/font.rs b/src/video/font.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9d16b727050a01d25c5bdc96fd11ed8de200441d
--- /dev/null
+++ b/src/video/font.rs
@@ -0,0 +1,29 @@
+// #[derive(Debug, Clone, Copy, Default)]
+// pub struct FontChar {
+//     bitmap: [u8; 16],
+// }
+
+pub static FONT: [[u8; 16]; 256] = {
+    let mut font = [[0u8; 16]; 256];
+
+    font[b'a' as usize] = [
+        0b_11111111,
+        0b_11111111,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011,
+        0b_11000011, //
+        0b_11111111,
+        0b_11111111,
+    ];
+
+    font
+};
diff --git a/src/video/framebuffer.rs b/src/video/framebuffer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1f96c5cadbba64854b544a3b2d419e2c876948dc
--- /dev/null
+++ b/src/video/framebuffer.rs
@@ -0,0 +1,73 @@
+use core::{
+    fmt,
+    ops::{Deref, DerefMut},
+};
+use spin::{Mutex, Once};
+
+//
+
+pub static FBO: Once<Mutex<Framebuffer>> = Once::new();
+
+//
+
+pub struct Framebuffer {
+    pub buf: &'static mut [u8],
+
+    pub width: usize, // not the pixels to the next row
+    pub height: usize,
+    pub pitch: usize, // pixels to the next row
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct Color {
+    r: u8,
+    g: u8,
+    b: u8,
+}
+
+//
+
+impl Framebuffer {
+    pub fn set(&mut self, x: usize, y: usize, color: Color) {
+        let spot = x * 4 + y * self.pitch;
+        self.buf[spot..spot + 4].copy_from_slice(&color.as_arr()[..]);
+    }
+
+    pub fn fill(&mut self, x: usize, y: usize, w: usize, h: usize, color: Color) {
+        for yd in 0..h {
+            for xd in 0..w {
+                self.set(x + xd, y + yd, color);
+            }
+        }
+    }
+}
+
+impl Color {
+    pub const WHITE: Color = Color::new(0xff, 0xff, 0xff);
+
+    pub const RED: Color = Color::new(0xff, 0x00, 0x00);
+    pub const GREEN: Color = Color::new(0x00, 0xff, 0x00);
+    pub const BLUE: Color = Color::new(0x00, 0x00, 0xff);
+
+    pub const fn new(r: u8, g: u8, b: u8) -> Self {
+        Self { r, g, b }
+    }
+
+    pub const fn as_u32(&self) -> u32 {
+        u32::from_ne_bytes([self.r, self.g, self.b, 0])
+    }
+
+    pub const fn as_arr(&self) -> [u8; 4] {
+        [self.r, self.g, self.b, 0]
+    }
+}
+
+impl fmt::Debug for Framebuffer {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("Framebuffer")
+            .field("width", &self.width)
+            .field("height", &self.height)
+            .field("pitch", &self.pitch)
+            .finish_non_exhaustive()
+    }
+}
diff --git a/src/video/mod.rs b/src/video/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..27cbe8d094ff97edf519ebf411d2fa235aac65be
--- /dev/null
+++ b/src/video/mod.rs
@@ -0,0 +1,2 @@
+pub mod font;
+pub mod framebuffer;