diff --git a/src/buffered_display.rs b/src/buffered_display.rs
new file mode 100644
index 0000000000000000000000000000000000000000..51a629fe51f0ee0ae0306d9c64ab823c33fc5da3
--- /dev/null
+++ b/src/buffered_display.rs
@@ -0,0 +1,142 @@
+// Uses too much RAM to be useful ):
+// 320 * 240 * 2 * 2 bytes/pixel = 307KB of RAM
+
+use core::convert::Infallible;
+
+use embedded_graphics::{
+    draw_target::DrawTarget,
+    geometry::{Dimensions, Point},
+    pixelcolor::PixelColor,
+    Pixel,
+};
+
+pub struct BufferedDisplay<const L: usize, C: 'static, E, DT: DrawTarget<Color = C, Error = E>> {
+    target: DT,
+    width: usize,
+    height: usize,
+    last: &'static mut [C; L],
+    cur: &'static mut [C; L],
+}
+
+impl<const L: usize, C: PixelColor, E, DT: DrawTarget<Color = C, Error = E>>
+    BufferedDisplay<L, C, E, DT>
+{
+    pub fn new(
+        mut target: DT,
+        color: C,
+        last: &'static mut [C; L],
+        cur: &'static mut [C; L],
+    ) -> Result<Self, E> {
+        let size = target.bounding_box().size;
+        let width = size.width.try_into().unwrap();
+        let height = size.height.try_into().unwrap();
+        assert_eq!(L, width * height);
+        target.fill_solid(&target.bounding_box(), color)?;
+
+        Ok(Self {
+            target,
+            width,
+            height,
+            last,
+            cur,
+        })
+    }
+
+    pub fn flush(&mut self) -> Result<(), E> {
+        let iter = BufferedDisplayIter::new(self.width, self.height, self.last, self.cur);
+        self.target.draw_iter(iter)?;
+
+        Ok(())
+    }
+
+    #[inline(always)]
+    fn index<X: TryInto<usize>, Y: TryInto<usize>>(&self, x: X, y: Y) -> Option<usize> {
+        let x: usize = x.try_into().ok()?;
+        let y: usize = y.try_into().ok()?;
+
+        if y >= self.height || x >= self.width {
+            return None;
+        }
+
+        Some(x + y * self.width)
+    }
+}
+
+impl<const L: usize, C: PixelColor, E, DT: DrawTarget<Color = C, Error = E>> Dimensions
+    for BufferedDisplay<L, C, E, DT>
+{
+    fn bounding_box(&self) -> embedded_graphics::primitives::Rectangle {
+        self.target.bounding_box()
+    }
+}
+
+impl<const L: usize, C: PixelColor, E, DT: DrawTarget<Color = C, Error = E>> DrawTarget
+    for BufferedDisplay<L, C, E, DT>
+{
+    type Color = C;
+    type Error = Infallible;
+
+    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
+    where
+        I: IntoIterator<Item = embedded_graphics::prelude::Pixel<Self::Color>>,
+    {
+        for Pixel(p, c) in pixels {
+            if let Some(idx) = self.index(p.x, p.y) {
+                self.cur[idx] = c;
+            }
+        }
+
+        Ok(())
+    }
+}
+
+struct BufferedDisplayIter<'a, C: PixelColor> {
+    width: usize,
+    height: usize,
+    last: &'a mut [C],
+    cur: &'a mut [C],
+    i: usize,
+}
+
+impl<'a, C: PixelColor> BufferedDisplayIter<'a, C> {
+    fn new(width: usize, height: usize, last: &'a mut [C], cur: &'a mut [C]) -> Self {
+        Self {
+            width,
+            height,
+            last,
+            cur,
+            i: 0,
+        }
+    }
+
+    #[inline(always)]
+    fn get_xy(&self, idx: usize) -> (i32, i32) {
+        let y = (idx / self.width).try_into().unwrap();
+        let x = (idx % self.width).try_into().unwrap();
+
+        (x, y)
+    }
+}
+
+impl<'a, C: PixelColor> Iterator for BufferedDisplayIter<'a, C> {
+    type Item = Pixel<C>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        loop {
+            if self.i >= self.cur.len() {
+                return None;
+            }
+
+            let c = self.cur[self.i];
+            if self.last[self.i] != c {
+                self.last[self.i] = c;
+
+                let (x, y) = self.get_xy(self.i);
+
+                return Some(Pixel(Point::new(x, y), c));
+            }
+
+            self.i += 1;
+        }
+    }
+}
diff --git a/src/keyboard.rs b/src/keyboard.rs
index 3da7f1aa752d90a555b4a002d2953e74c753295b..4070ae17b43bed4409f89b3123b3243654a43305 100644
--- a/src/keyboard.rs
+++ b/src/keyboard.rs
@@ -37,7 +37,7 @@ const KEYMAP: [[char; 8]; 6] = [
     ],
 ];
 
-enum KeyCode {
+pub enum KeyCode {
     CallStart,
     Menu,
     Touchpad,
@@ -110,6 +110,7 @@ pub struct Keyboard<
     row_ser: RSER,
     row_ld: RLD,
     last_state: u64,
+    cur_key: Option<KeyCode>,
 }
 
 impl<
@@ -142,6 +143,7 @@ impl<
             row_ld,
 
             last_state: 0,
+            cur_key: None,
         }
     }
 
@@ -175,7 +177,7 @@ impl<
                             is_alt_pressed(state),
                             is_shift_pressed(state),
                         );
-                        rprint!("{}", keycode);
+                        self.cur_key = Some(keycode);
                     }
                 }
             }
@@ -185,6 +187,10 @@ impl<
         state
     }
 
+    pub fn pushed_key(&mut self) -> Option<KeyCode> {
+        self.cur_key.take()
+    }
+
     // 1s in val are low
     // 0s are high
     // indexes from furthest pin (QH on second shift register)
diff --git a/src/main.rs b/src/main.rs
index e4cc6bab5e476620773ce96d195f38168750f54b..1206eef9496cf0f52e7dac6c65887088a463aa09 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
 #![no_std]
 #![no_main]
 
+mod buffered_display;
 mod keyboard;
 mod touchpad;
 
@@ -15,16 +16,18 @@ pub static BOOT_LOADER: [u8; 256] = rp2040_boot2::BOOT_LOADER_GD25Q64CS;
     dispatchers = [TIMER_IRQ_1, TIMER_IRQ_2]
 )]
 mod app {
-    use crate::{keyboard::Keyboard, touchpad::Touchpad};
-    use core::fmt::Write;
-    use cortex_m::delay::Delay;
+    use crate::{
+        buffered_display::BufferedDisplay,
+        keyboard::{KeyCode, Keyboard},
+        touchpad::Touchpad,
+    };
+    use cortex_m::{delay::Delay, singleton};
     use display_interface_spi::SPIInterface;
     use embedded_graphics::{
         draw_target::DrawTarget,
         mono_font::{ascii::FONT_10X20, MonoTextStyleBuilder},
         pixelcolor::{Rgb565, RgbColor},
-        prelude::{Point, Primitive},
-        primitives::{Circle, PrimitiveStyle, Triangle},
+        prelude::Point,
         text::{Baseline, Text},
         Drawable,
     };
@@ -41,16 +44,18 @@ mod app {
         },
         pac::{I2C1, SPI1},
         pwm::{self, Channel, FreeRunning, Pwm4, Pwm5, Slice},
-        reset, spi,
+        spi,
         timer::{monotonic::Monotonic, Timer},
         Clock, I2C,
     };
     use heapless::String;
-    use mipidsi::{models::ST7789, Builder};
+    use mipidsi::{models::ST7789, Builder, Orientation};
     use rp2040_hal::{self as hal, timer::Alarm0};
     use rtic_monotonics::rp2040::*;
 
     const XTAL_FREQ_HZ: u32 = 12_000_000u32;
+    const WIDTH: usize = 320;
+    const HEIGHT: usize = 240;
 
     type KeyboardBl = Channel<Slice<Pwm4, FreeRunning>, pwm::B>;
 
@@ -99,7 +104,9 @@ mod app {
     type MyMono = Monotonic<Alarm0>;
 
     #[shared]
-    struct Shared {}
+    struct Shared {
+        text: String<1024>,
+    }
 
     #[local]
     struct Local {
@@ -179,6 +186,7 @@ mod app {
         let mut display_bl = pwm.channel_a;
         display_bl.output_to(pins.gpio10);
         display_bl.set_inverted();
+        display_bl.set_duty(display_bl.get_max_duty());
 
         let tp_reset = pins.gpio3.into_push_pull_output();
         let mut disp_reset = pins.gpio4.into_push_pull_output_in_state(PinState::Low);
@@ -236,7 +244,8 @@ mod app {
         let spi_cs = SPIInterface::new(spi, display_dc, display_cs);
 
         let display = Builder::st7789(spi_cs)
-            .with_display_size(320, 240)
+            .with_display_size(HEIGHT.try_into().unwrap(), WIDTH.try_into().unwrap())
+            .with_orientation(Orientation::LandscapeInverted(true))
             .init(&mut delay, None)
             .unwrap();
 
@@ -246,7 +255,9 @@ mod app {
         let mono = Monotonic::new(timer, timer.alarm_0().unwrap());
 
         (
-            Shared {},
+            Shared {
+                text: String::new(),
+            },
             Local {
                 keyboard_bl,
                 touchpad,
@@ -261,7 +272,7 @@ mod app {
         )
     }
 
-    #[task(priority = 2, local = [display_bl, keyboard_bl, touchpad, keyboard])]
+    #[task(priority = 2, local = [display_bl, keyboard_bl, touchpad, keyboard], shared = [text])]
     fn keyboard_update(ctx: keyboard_update::Context) {
         //while usb_dev.poll(&mut [&mut serial]) {}
 
@@ -275,13 +286,30 @@ mod app {
         let max_duty = keyboard_bl.get_max_duty();
         let mut cur_duty = display_bl.get_duty();
 
-        let mut buf: String<64> = String::new();
-
         let state = keyboard.read_state();
         if state > 0 {
-            write!(&mut buf, "KB: {state:x}\r\n").unwrap();
-            //serial.write(&buf.as_bytes());
             keyboard_bl.set_duty(max_duty);
+
+            if let Some(k) = keyboard.pushed_key() {
+                let mut text = ctx.shared.text;
+
+                match k {
+                    KeyCode::Backspace => {
+                        text.lock(|f| f.pop());
+                    }
+                    KeyCode::Char(c) => {
+                        text.lock(|f| {
+                            let _ = f.push(c);
+                        });
+                    }
+                    KeyCode::Enter => {
+                        text.lock(|f| {
+                            let _ = f.push_str("\r\n");
+                        });
+                    }
+                    _ => {}
+                }
+            }
         } else {
             keyboard_bl.set_duty(0);
         }
@@ -303,37 +331,34 @@ mod app {
         keyboard_update::spawn_after(5.millis()).unwrap();
     }
 
-    #[task(local = [display, r, g, b])]
-    fn display_update(ctx: display_update::Context) {
+    #[task(local = [display, r, g, b], shared = [text])]
+    fn display_update(mut ctx: display_update::Context) {
         let display_update::LocalResources { display, r, g, b } = ctx.local;
-        *r += 1;
-        *g += 3;
-        *b += 5;
-        *r %= 255;
-        *g %= 255;
-        *b %= 255;
-
+        /*
+         *r += 1;
+         *g += 3;
+         *b += 5;
+         *r %= 255;
+         *g %= 255;
+         *b %= 255;
+         */
         display.clear(Rgb565::new(*r, *g, *b)).unwrap();
 
-        // Draw a smiley face with white eyes and a red mouth
-        draw_smiley(display).unwrap();
-
         // text!
         let text_style = MonoTextStyleBuilder::new()
             .font(&FONT_10X20)
             .text_color(Rgb565::YELLOW)
             .build();
 
-        Text::with_baseline(
-            "Hello world!",
-            Point::new(16, 128),
-            text_style,
-            Baseline::Top,
-        )
-        .draw(display)
-        .unwrap();
+        ctx.shared.text.lock(|text| {
+            let _ = text.push('_');
+            Text::with_baseline(text, Point::new(8, 8), text_style, Baseline::Top)
+                .draw(display)
+                .unwrap();
+            text.pop();
+        });
 
-        display_update::spawn_after(100.millis()).unwrap();
+        display_update::spawn_after(10.millis()).unwrap();
     }
 
     #[idle]
@@ -342,36 +367,4 @@ mod app {
             cortex_m::asm::wfi();
         }
     }
-
-    fn draw_smiley<T: DrawTarget<Color = Rgb565>>(display: &mut T) -> Result<(), T::Error> {
-        // Draw the left eye as a circle located at (50, 100), with a diameter of 40, filled with white
-        Circle::new(Point::new(50, 100), 40)
-            .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE))
-            .draw(display)?;
-
-        // Draw the right eye as a circle located at (50, 200), with a diameter of 40, filled with white
-        Circle::new(Point::new(50, 200), 40)
-            .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE))
-            .draw(display)?;
-
-        // Draw an upside down red triangle to represent a smiling mouth
-        Triangle::new(
-            Point::new(130, 140),
-            Point::new(130, 200),
-            Point::new(160, 170),
-        )
-        .into_styled(PrimitiveStyle::with_fill(Rgb565::RED))
-        .draw(display)?;
-
-        // Cover the top part of the mouth with a black triangle so it looks closed instead of open
-        Triangle::new(
-            Point::new(130, 150),
-            Point::new(130, 190),
-            Point::new(150, 170),
-        )
-        .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK))
-        .draw(display)?;
-
-        Ok(())
-    }
 }