diff --git a/l0dable/build.rs b/l0dable/build.rs
index cd855226c38a6d6bc88904842ca82542d0370619..e1c320d6da3d0091fff77c8ca756a9facc245fa3 100644
--- a/l0dable/build.rs
+++ b/l0dable/build.rs
@@ -36,6 +36,11 @@ fn main() {
         .file("../c/l0dables/lib/hardware.c")
         .file("../c/epicardium/api/caller.c")
         .file("src/client.c")
+        .file("../c/lib/gfx/Fonts/font12.c")
+        .file("../c/lib/gfx/Fonts/font16.c")
+        .file("../c/lib/gfx/Fonts/font20.c")
+        .file("../c/lib/gfx/Fonts/font24.c")
+        .file("../c/lib/gfx/Fonts/font8.c")
         .compile("card10");
     println!("cargo:rerun-if-changed=src/client.rs");
 
diff --git a/l0dable/src/framebuffer/font.rs b/l0dable/src/framebuffer/font.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6875046f4bc62ce7dc6851a04470fb83b7e3afbf
--- /dev/null
+++ b/l0dable/src/framebuffer/font.rs
@@ -0,0 +1,68 @@
+use core::slice::from_raw_parts;
+
+extern "C" {
+    static Font8: Font;
+    static Font12: Font;
+    static Font16: Font;
+    static Font20: Font;
+    static Font24: Font;
+}
+
+#[repr(C)]
+pub struct Font {
+    table: *const u8,
+    pub w: u16,
+    pub h: u16,
+}
+
+impl Font {
+    pub fn font8() -> &'static Self {
+        unsafe { &Font8 }
+    }
+    pub fn font12() -> &'static Self {
+        unsafe { &Font12 }
+    }
+    pub fn font16() -> &'static Self {
+        unsafe { &Font16 }
+    }
+    pub fn font20() -> &'static Self {
+        unsafe { &Font20 }
+    }
+    pub fn font24() -> &'static Self {
+        unsafe { &Font24 }
+    }
+    
+    fn bytes_per_row(&self) -> usize {
+        self.w as usize / 8 + 1
+    }
+    
+    pub fn get_glyph(&self, c: char) -> Option<Glyph> {
+        let h = self.h as usize;
+        let bytes_per_row = self.bytes_per_row();
+        let table = unsafe {
+            from_raw_parts(self.table, ('~' as usize - (' ' as usize) - 1) * bytes_per_row * h)
+        };
+        let offset = (c as usize - (' ' as usize)) * bytes_per_row * h;
+        if offset < table.len() {
+            let table = &table[offset..(offset + bytes_per_row * h)];
+            Some(Glyph {
+                table,
+                bytes_per_row,
+            })
+        } else {
+            None
+        }
+    }
+}
+
+pub struct Glyph<'a> {
+    table: &'a [u8],
+    bytes_per_row: usize,
+}
+
+impl<'a> Glyph<'a> {
+    pub fn get_pixel(&self, x: usize, y: usize) -> bool {
+        let offset = x / 8 + y * self.bytes_per_row;
+        self.table[offset] & (1 << (7 - (x & 7))) != 0
+    }
+}
diff --git a/l0dable/src/framebuffer/mod.rs b/l0dable/src/framebuffer/mod.rs
index d57d0f6dd7717f16aefabe5270bfc9606fa828d2..0f8bb9159e7b02e56f2e86c23baeb4d8f87cf7d0 100644
--- a/l0dable/src/framebuffer/mod.rs
+++ b/l0dable/src/framebuffer/mod.rs
@@ -3,6 +3,11 @@ use core::ops::{Deref, DerefMut};
 use crate::bindings::*;
 use crate::{Color, Display};
 
+mod font;
+pub use font::*;
+mod text;
+pub use text::TextRenderer;
+
 pub struct FrameBuffer<'d> {
     _display: &'d Display,
     buffer: disp_framebuffer,
@@ -24,6 +29,13 @@ impl<'d> FrameBuffer<'d> {
             epic_disp_framebuffer(&mut self.buffer);
         }
     }
+
+    pub fn text<'a, 'f>(&'a mut self, x: isize, y: isize, font: &'f Font, color: Color) -> TextRenderer<'a, 'd, 'f> {
+        TextRenderer {
+            framebuffer: self,
+            x, y, font, color,
+        }
+    }
 }
 
 impl<'d> Deref for FrameBuffer<'d> {
diff --git a/l0dable/src/framebuffer/text.rs b/l0dable/src/framebuffer/text.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3814c8d6269474978a3634924e050a8bb8858cab
--- /dev/null
+++ b/l0dable/src/framebuffer/text.rs
@@ -0,0 +1,43 @@
+use core::fmt::Write;
+use super::{FrameBuffer, Font};
+use crate::{Color, Display};
+
+pub struct TextRenderer<'a, 'd, 'f> {
+    pub framebuffer: &'a mut FrameBuffer<'d>,
+    pub x: isize,
+    pub y: isize,
+    pub font: &'f Font,
+    pub color: Color,
+}
+
+impl<'a, 'd, 'f> Write for TextRenderer<'a, 'd, 'f> {
+    fn write_str(&mut self, s: &str) -> core::fmt::Result {
+        for c in s.chars() {
+            self.write_char(c)?;
+        }
+        Ok(())
+    }
+
+    fn write_char(&mut self, c: char) -> core::fmt::Result {
+        match self.font.get_glyph(c) {
+            None => Ok(()),
+            Some(glyph) => {
+                for y in 0..self.font.h {
+                    let y1 = self.y + y as isize;
+                    if y1 >= 0 && y1 < Display::H as isize {
+                        for x in 0..self.font.w {
+                            let x1 = self.x + x as isize;
+                            if x1 >= 0 && x1 < Display::W as isize {
+                                if glyph.get_pixel(x as usize, y as usize) {
+                                    self.framebuffer[y1 as usize][x1 as usize] = self.color;
+                                }
+                            }
+                        }
+                    }
+                }
+                self.x += self.font.w as isize;
+                Ok(())
+            }
+        }
+    }
+}