diff --git a/applications/init/source/main.cpp b/applications/init/source/main.cpp
index ed0ef8e..6aa85a4 100644
--- a/applications/init/source/main.cpp
+++ b/applications/init/source/main.cpp
@@ -1,28 +1,120 @@
 #include <daguerre.hpp>
 
+void overlay_encode(
+  daguerre::hilbert_color &dest, const daguerre::rgb24 &src) {
+  if (src.r != 0xff || src.g != 0x00 || src.b != 0xff)
+    daguerre::encode(dest, src);
+}
+
+void invert(daguerre::rgb24 &dest, const daguerre::rgb24 &src) {
+  dest.r = 255 - src.r;
+  dest.g = 255 - src.g;
+  dest.b = 255 - src.b;
+}
+
 int main(int, char **) {
 
-  auto hfb = daguerre::get_hilbert_framebuffer();
-  daguerre::image<daguerre::rgb24> bim;
+  auto framebuffer = daguerre::get_hilbert_framebuffer();
 
-  std::FILE *burdon = std::fopen("/assets/burden.ppm", "r");
-  daguerre::try_load_ppm(burdon, bim);
-  std::fclose(burdon);
+  daguerre::image<daguerre::rgb24> burden;
+  std::FILE *burden_file = std::fopen("/assets/burden.ppm", "r");
+  daguerre::try_load_ppm(burden_file, burden);
+  std::fclose(burden_file);
 
-  unsigned width = bim.width < hfb.width ? bim.width : hfb.width;
-  unsigned height = bim.height < hfb.height ? bim.height : hfb.height;
-  unsigned x = (hfb.width - width) / 2;
-  unsigned y = (hfb.height - height) / 2;
+  daguerre::image<daguerre::rgb24> pointer;
+  std::FILE *pointer_file = std::fopen("/assets/pointer.ppm", "r");
+  daguerre::try_load_ppm(pointer_file, pointer);
+  std::fclose(pointer_file);
+
+  int32_t width = burden.width < framebuffer.width
+                ? burden.width : framebuffer.width;
+  int32_t height = burden.height < framebuffer.height
+                 ? burden.height : framebuffer.height;
+  unsigned x = (framebuffer.width - width) / 2;
+  unsigned y = (framebuffer.height - height) / 2;
+
+  int32_t new_mouse_x = width / 2;
+  int32_t new_mouse_y = height / 2;
+  int32_t old_mouse_x = new_mouse_x;
+  int32_t old_mouse_y = new_mouse_y;
+  bool was_left_mouse_down = false;
+
+  daguerre::overlay_region<
+    daguerre::hilbert_color, daguerre::rgb24, daguerre::encode>(
+    framebuffer, x, y, burden, 0, 0, width, height);
 
   while (1) {
-    daguerre::copy_region<>(hfb, bim, 0, 0, x, y, width, height);
-    while ((__euler_read_key_packet() & 0x0400ff) != 0x00005a)
-      ;
-    for (unsigned i = 0; i < bim.width * bim.height; ++i) {
-      bim.buffer[i].r = 255 - bim.buffer[i].r;
-      bim.buffer[i].g = 255 - bim.buffer[i].g;
-      bim.buffer[i].b = 255 - bim.buffer[i].b;
+
+    __euler_mouse_buttons mouse_buttons;
+    int16_t mouse_change_x, mouse_change_y;
+    uint32_t key_packet;
+    __euler_input_packet_type packet_type = __euler_get_input_packet(
+      mouse_buttons, mouse_change_x, mouse_change_y, key_packet);
+
+    bool anything_changed = false;
+    bool should_invert = false;
+
+    if (packet_type == __EULER_IPT_MOUSE) {
+
+      if (mouse_change_x != 0 || mouse_change_y != 0) {
+
+        old_mouse_x = new_mouse_x;
+        old_mouse_y = new_mouse_y;
+        new_mouse_x += mouse_change_x;
+        new_mouse_y += mouse_change_y;
+
+        if (new_mouse_x < 0)
+          new_mouse_x = 0;
+        else if ((unsigned)new_mouse_x > width - pointer.width)
+          new_mouse_x = width - pointer.width;
+
+        if (new_mouse_y < 0)
+          new_mouse_y = 0;
+        else if ((unsigned)new_mouse_y > height - pointer.height)
+          new_mouse_y = height - pointer.height;
+
+        anything_changed = true;
+
+      }
+
+      if (!was_left_mouse_down && (mouse_buttons & __EULER_MB_LEFT))
+        should_invert = true;
+
+      was_left_mouse_down = mouse_buttons & __EULER_MB_LEFT;
+
     }
+
+    else if (packet_type == __EULER_IPT_KEYBOARD)
+      if ((key_packet & 0x0400ff) == 0x00005a)
+        should_invert = true;
+
+    if (should_invert) {
+
+      //this works with the current implementation of overlay_region, but
+      //maybe it would be better to have a dedicated function for when the
+      //two regions are exactly the same, given the comment on overlay_region.
+      daguerre::overlay_region<daguerre::rgb24, daguerre::rgb24, invert>(
+        burden, 0, 0, burden, 0, 0, width, height);
+
+      daguerre::overlay_region<
+        daguerre::hilbert_color, daguerre::rgb24, daguerre::encode>(
+        framebuffer, x, y, burden, 0, 0, width, height);
+
+      anything_changed = true;
+
+    }
+
+    if (anything_changed) {
+      daguerre::overlay_region<
+        daguerre::hilbert_color, daguerre::rgb24, daguerre::encode>(
+        framebuffer, old_mouse_x + x, old_mouse_y + y, burden,
+        old_mouse_x, old_mouse_y, pointer.width, pointer.height);
+      daguerre::overlay_region<
+        daguerre::hilbert_color, daguerre::rgb24, overlay_encode>(
+        framebuffer, new_mouse_x + x, new_mouse_y + y,
+        pointer, 0, 0, pointer.width, pointer.height);
+    }
+
   }
 
 }
diff --git a/documentation/keys.txt b/documentation/kernel-interface/keys.txt
similarity index 100%
rename from documentation/keys.txt
rename to documentation/kernel-interface/keys.txt
diff --git a/documentation/sockets.txt b/documentation/kernel-interface/sockets.txt
similarity index 100%
rename from documentation/sockets.txt
rename to documentation/kernel-interface/sockets.txt
diff --git a/documentation/syscalls.txt b/documentation/kernel-interface/syscalls.txt
similarity index 93%
rename from documentation/syscalls.txt
rename to documentation/kernel-interface/syscalls.txt
index 52e909d..792c300 100644
--- a/documentation/syscalls.txt
+++ b/documentation/kernel-interface/syscalls.txt
@@ -63,9 +63,19 @@ get new pages:
   rax out: start of first page
   the allocated pages are next to each other, writable, and not executable.
 
-read key packet:
+get input packet:
   rax in: 5
-  eax out: key packet
+  al out:
+    bit 7: was mouse packet
+    if bit 7:
+      bit 0: left mouse button down
+      bit 1: right mouse button down
+      bit 2: middle mouse button down
+  if bit 7:
+    di out: mouse x change (signed)
+    si out: mouse y change (signed)
+  else:
+    edi out: key packet
 
 create private socket:
   rax in: 6
diff --git a/euler/include/euler/syscall.hpp b/euler/include/euler/syscall.hpp
index 9255642..761dbcc 100644
--- a/euler/include/euler/syscall.hpp
+++ b/euler/include/euler/syscall.hpp
@@ -58,4 +58,17 @@ extern "C" uint32_t *__euler_get_framebuffer(
 
 extern "C" uint32_t __euler_encode_color(uint8_t r, uint8_t g, uint8_t b);
 
-extern "C" uint32_t __euler_read_key_packet();
+enum __euler_mouse_buttons : uint8_t {
+  __EULER_MB_LEFT = 1,
+  __EULER_MB_RIGHT = 2,
+  __EULER_MB_MIDDLE = 4
+};
+
+enum __euler_input_packet_type : uint8_t {
+  __EULER_IPT_MOUSE = 1,
+  __EULER_IPT_KEYBOARD = 2
+};
+
+extern "C" __euler_input_packet_type __euler_get_input_packet(
+  __euler_mouse_buttons &buttons_out, int16_t &x_change_out,
+  int16_t &y_change_out, uint32_t &keyboard_packet_out);
diff --git a/euler/source/euler/syscall.asm b/euler/source/euler/syscall.asm
index 34f2735..41bd05c 100644
--- a/euler/source/euler/syscall.asm
+++ b/euler/source/euler/syscall.asm
@@ -11,7 +11,7 @@ global __euler_end_this_thread
 global __euler_read_from_stream
 global __euler_get_framebuffer
 global __euler_encode_color
-global __euler_read_key_packet
+global __euler_get_input_packet
 
 section .text
 
@@ -96,7 +96,31 @@ __euler_encode_color:
   syscall
   ret
 
-__euler_read_key_packet:
+__euler_get_input_packet:
+  push rdi
+  push rsi
+  push rdx
+  push rcx
   mov rax, 5
   syscall
+
+  test al, 0x80
+  jnz .mouse_packet
+
+  pop rcx
+  mov dword [rcx], edi
+  add rsp, 24
+  mov al, 2
+  ret
+
+.mouse_packet:
+  add rsp, 8
+  pop rdx
+  mov word [rdx], si
+  pop rdx
+  mov word [rdx], di
+  pop rdx
+  and al, 0x7f
+  mov byte [rdx], al
+  mov al, 1
   ret
diff --git a/kernel/include/hilbert/kernel/input.hpp b/kernel/include/hilbert/kernel/input.hpp
index 2209ddc..d1b7ca2 100644
--- a/kernel/include/hilbert/kernel/input.hpp
+++ b/kernel/include/hilbert/kernel/input.hpp
@@ -18,7 +18,26 @@ namespace hilbert::kernel::input {
     BREAK       = 1 << 18,
   };
 
-  extern utility::queue<uint32_t> *key_queue;
+  enum buttons_t : uint8_t {
+    LEFT_BUTTON = 1,
+    RIGHT_BUTTON = 2,
+    MIDDLE_BUTTON = 4
+  };
+
+  struct input_packet {
+    union {
+      struct {
+        int16_t x_change;
+        int16_t y_change;
+        buttons_t buttons;
+      } mouse;
+      uint32_t keyboard;
+    };
+    bool is_mouse;
+  };
+
+  extern utility::queue<input_packet> *input_queue;
+
   //notify a process waiting for input
   void got_input();
 
diff --git a/kernel/include/hilbert/kernel/utility.hpp b/kernel/include/hilbert/kernel/utility.hpp
index 47f78ea..c0b8c19 100644
--- a/kernel/include/hilbert/kernel/utility.hpp
+++ b/kernel/include/hilbert/kernel/utility.hpp
@@ -342,6 +342,11 @@ namespace hilbert::kernel::utility {
       return move(ret);
     }
 
+    //assumes not empty
+    value_t &last_inserted() const {
+      return buffer[(count - 1 + next_read_index) % buffer_len];
+    }
+
   };
 
 }
diff --git a/kernel/source/input.cpp b/kernel/source/input.cpp
index 696cb13..921ae7b 100644
--- a/kernel/source/input.cpp
+++ b/kernel/source/input.cpp
@@ -5,10 +5,10 @@
 
 namespace hilbert::kernel::input {
 
-  utility::queue<uint32_t> *key_queue;
+  utility::queue<input_packet> *input_queue;
 
   void init_input() {
-    key_queue = new utility::queue<uint32_t>();
+    input_queue = new utility::queue<input_packet>();
   }
 
   void got_input() {
diff --git a/kernel/source/interrupts.asm b/kernel/source/interrupts.asm
index babc020..e0c3cdb 100644
--- a/kernel/source/interrupts.asm
+++ b/kernel/source/interrupts.asm
@@ -227,14 +227,6 @@ section .bss
 
 section .text
 
-write_keyboard_byte:
-  in al, 0x64
-  test al, 0x02
-  jnz write_keyboard_byte
-  mov al, dil
-  out 0x60, al
-  ret
-
 extern on_keyboard_interrupt
 
 keyboard_isr:
@@ -249,6 +241,7 @@ keyboard_isr:
   push rcx
   push rax
 
+  call wait_read_ps2
   in al, 0x60
   mov dil, al
 
@@ -269,6 +262,54 @@ keyboard_isr:
 
   iretq
 
+extern on_mouse_interrupt
+
+mouse_isr:
+
+  push r11
+  push r10
+  push r9
+  push r8
+  push rsi
+  push rdi
+  push rdx
+  push rcx
+  push rax
+
+  call wait_read_ps2
+  in al, 0x60
+  mov dil, al
+
+  call on_mouse_interrupt
+
+  mov al, 0x20
+  out 0x20, al
+  out 0xa0, al
+
+  pop rax
+  pop rcx
+  pop rdx
+  pop rdi
+  pop rsi
+  pop r8
+  pop r9
+  pop r10
+  pop r11
+
+  iretq
+
+wait_send_ps2:
+  in al, 0x64
+  test al, 0x02
+  jnz wait_send_ps2
+  ret
+
+wait_read_ps2:
+  in al, 0x64
+  test al, 0x01
+  jz wait_send_ps2
+  ret
+
 load_gdt_and_idt:
 
   ;fill exception entries in idt
@@ -294,7 +335,7 @@ load_gdt_and_idt:
   out 0x21, al
   mov al, 0x01
   out 0x21, al
-  mov al, 0xfd ;mask all but irq 1
+  mov al, 0xf9 ;mask all but irq 1 and 2
   out 0x21, al
 
   mov al, 0x11
@@ -305,19 +346,54 @@ load_gdt_and_idt:
   out 0xa1, al
   mov al, 0x01
   out 0xa1, al
-  mov al, 0xff ;mask all
+  mov al, 0xef ;mask all but irq 12
   out 0xa1, al
 
+  ;register keyboard and mouse interrupts
+
   mov rdi, 0x21
   mov rsi, keyboard_isr
   call set_isr
 
-  ;set keyboard config
+  mov rdi, 0x2c
+  mov rsi, mouse_isr
+  call set_isr
 
+  ;set ps2 config
+
+  call wait_send_ps2
   mov al, 0x60
   out 0x64, al
-  mov dil, 0x01
-  call write_keyboard_byte
+
+  call wait_send_ps2
+  mov al, 0x03
+  out 0x60, al
+
+  ;set mouse defaults
+
+  call wait_send_ps2
+  mov al, 0xd4
+  out 0x64, al
+
+  call wait_send_ps2
+  mov al, 0xf6
+  out 0x60, al
+
+  call wait_read_ps2
+  in al, 0x60
+
+  ;enable mouse reporting
+
+  call wait_send_ps2
+  mov al, 0xd4
+  out 0x64, al
+
+  call wait_send_ps2
+  mov al, 0xf4
+  out 0x60, al
+
+  call wait_read_ps2
+  in al, 0x60
 
   ;make tss entry in gdt
 
diff --git a/kernel/source/interrupts.cpp b/kernel/source/interrupts.cpp
index 6e22121..9b6495e 100644
--- a/kernel/source/interrupts.cpp
+++ b/kernel/source/interrupts.cpp
@@ -1,3 +1,4 @@
+#include <hilbert/kernel/framebuffer.hpp>
 #include <hilbert/kernel/input.hpp>
 #include <hilbert/kernel/panic.hpp>
 
@@ -54,7 +55,9 @@ static uint32_t current_flags = 0;
 
 static void got_key(uint32_t key) {
 
-  input::key_queue->insert(current_flags | key);
+  input::input_queue->insert({
+    .keyboard = current_flags | key, .is_mouse = false});
+
   input::got_input();
 
   if (key == (input::BREAK | 0x77))
@@ -90,7 +93,7 @@ static void got_key(uint32_t key) {
 }
 
 static uint8_t key_so_far[8];
-uint8_t key_so_far_len = 0;
+static uint8_t key_so_far_len = 0;
 
 extern "C" void on_keyboard_interrupt(uint8_t byte) {
 
@@ -150,3 +153,45 @@ extern "C" void on_keyboard_interrupt(uint8_t byte) {
   }
 
 }
+
+static uint8_t mouse_packet_so_far[3];
+static uint8_t mouse_packet_so_far_len = 0;
+
+extern "C" void on_mouse_interrupt(uint8_t byte) {
+
+  if (mouse_packet_so_far_len == 0 && byte == 0xfa)
+    //dirty hack
+    return;
+
+  mouse_packet_so_far[mouse_packet_so_far_len++] = byte;
+  if (mouse_packet_so_far_len < 3)
+    return;
+
+  mouse_packet_so_far_len = 0;
+
+  int16_t x_change = mouse_packet_so_far[1];
+  int16_t y_change = -(int16_t)mouse_packet_so_far[2];
+  if (mouse_packet_so_far[0] & 0x10)
+    x_change -= 0x100;
+  if (mouse_packet_so_far[0] & 0x20)
+    y_change += 0x100;
+
+  input::input_packet packet = {
+    .mouse = {
+      .x_change = x_change, .y_change = y_change,
+      .buttons = (input::buttons_t)(mouse_packet_so_far[0] & 7) },
+    .is_mouse = true };
+
+  if (input::input_queue->count > 0 &&
+      input::input_queue->last_inserted().is_mouse &&
+      input::input_queue->last_inserted().mouse.buttons ==
+      packet.mouse.buttons) {
+    input::input_queue->last_inserted().mouse.x_change = packet.mouse.x_change;
+    input::input_queue->last_inserted().mouse.y_change = packet.mouse.y_change;
+  }
+  else
+    input::input_queue->insert(packet);
+
+  input::got_input();
+
+}
diff --git a/kernel/source/syscall.cpp b/kernel/source/syscall.cpp
index 768ff0d..5d9714c 100644
--- a/kernel/source/syscall.cpp
+++ b/kernel/source/syscall.cpp
@@ -157,7 +157,7 @@ namespace hilbert::kernel::syscall {
 
   }
 
-  void read_key_packet_syscall(
+  void get_input_packet_syscall(
     uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
   ) {
 
@@ -165,8 +165,17 @@ namespace hilbert::kernel::syscall {
     auto *t = application::running_thread;
 
     do
-      if (input::key_queue->count > 0) {
-        rax = (uint64_t)input::key_queue->take();
+      if (input::input_queue->count > 0) {
+        input::input_packet packet = input::input_queue->take();
+        if (packet.is_mouse) {
+          rax = packet.mouse.buttons | 0x80;
+          rdi = (uint16_t)packet.mouse.x_change;
+          rsi = (uint16_t)packet.mouse.y_change;
+        }
+        else {
+          rax = 0;
+          rdi = packet.keyboard;
+        }
         return;
       }
     while (application::save_thread_state(t->cpu));
@@ -504,7 +513,7 @@ namespace hilbert::kernel::syscall {
     &open_file_syscall,
     &end_this_thread_syscall,
     &get_new_pages_syscall,
-    &read_key_packet_syscall,
+    &get_input_packet_syscall,
     &create_private_socket_syscall,
     &create_socket_listener_syscall,
     &stop_socket_listener_syscall,
@@ -520,7 +529,7 @@ namespace hilbert::kernel::syscall {
     &set_stream_length_syscall
   };
 
-  static constexpr int max_syscall_number = 18;
+  static constexpr int max_syscall_number = 19;
 
 }
 
diff --git a/libraries/daguerre/include/daguerre.hpp b/libraries/daguerre/include/daguerre.hpp
index 274e257..62d10f0 100644
--- a/libraries/daguerre/include/daguerre.hpp
+++ b/libraries/daguerre/include/daguerre.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <stdint.h>
+#include <cstring>
 #include <cstdio>
 
 namespace daguerre {
@@ -13,14 +14,6 @@ namespace daguerre {
     uint8_t b;
   };
 
-  template <class to_type, class from_type>
-  to_type convert_color(const from_type &from);
-
-  template <>
-  inline hilbert_color convert_color<hilbert_color, rgb24>(const rgb24 &from) {
-    return __euler_encode_color(from.r, from.g, from.b);
-  }
-
   template <class color_t>
   class image {
 
@@ -88,15 +81,15 @@ namespace daguerre {
   //from [from_x, from_x + width) x [from_y, from_y + height).
   template <class color_t>
   void copy_region(
-    image<color_t> &to, const image<color_t> &from, unsigned from_x,
-    unsigned from_y, unsigned to_x, unsigned to_y, unsigned width,
-    unsigned height) {
+    image<color_t> &to, unsigned to_x, unsigned to_y,
+    const image<color_t> &from, unsigned from_x, unsigned from_y,
+    unsigned width, unsigned height) {
 
     color_t *to_start = to.buffer + to.pitch * to_y + to_x;
     const color_t *from_start = from.buffer + from.pitch * from_y + from_x;
 
     for (unsigned y = 0; y < height; ++y)
-      memcpy(
+      std::memcpy(
         to_start + to.pitch * y, from_start + from.pitch * y,
         width * sizeof(color_t));
 
@@ -107,12 +100,11 @@ namespace daguerre {
   //from [from_x, from_x + width) x [from_y, from_y + height).
   template <
     class to_color_t, class from_color_t,
-    to_color_t converter(const from_color_t &) =
-      convert_color<to_color_t, from_color_t>>
-  void copy_region(
-    image<to_color_t> &to, const image<from_color_t> &from, unsigned from_x,
-    unsigned from_y, unsigned to_x, unsigned to_y, unsigned width,
-    unsigned height) {
+    void overlay(to_color_t &dest, const from_color_t &src)>
+  void overlay_region(
+    image<to_color_t> &to, unsigned to_x, unsigned to_y,
+    const image<from_color_t> &from, unsigned from_x, unsigned from_y,
+    unsigned width, unsigned height) {
 
     to_color_t *to_start = to.buffer + to.pitch * to_y + to_x;
     const from_color_t *from_start =
@@ -120,10 +112,14 @@ namespace daguerre {
 
     for (unsigned y = 0; y < height; ++y)
       for (unsigned x = 0; x < width; ++x)
-        to_start[to.pitch * y + x] = converter(from_start[from.pitch * y + x]);
+        overlay(to_start[to.pitch * y + x], from_start[from.pitch * y + x]);
 
   }
 
+  static inline void encode(hilbert_color &dest, const rgb24 &src) {
+    dest = __euler_encode_color(src.r, src.g, src.b);
+  }
+
   image<hilbert_color> get_hilbert_framebuffer();
 
   bool try_load_ppm(std::FILE *input, image<rgb24> &into);
diff --git a/libraries/daguerre/source/daguerre.cpp b/libraries/daguerre/source/daguerre.cpp
index fb3ddc7..7c13a88 100644
--- a/libraries/daguerre/source/daguerre.cpp
+++ b/libraries/daguerre/source/daguerre.cpp
@@ -8,15 +8,22 @@ namespace daguerre {
     return image<hilbert_color>(ptr, width, height, pitch, false);
   }
 
+  //TODO: make this more robust
   unsigned read_text_int(std::FILE *input) {
     unsigned n = 0;
     char ch;
-    while (true) {
+    std::fread(&ch, 1, 1, input);
+    if (ch == '#') {
+      do
+        std::fread(&ch, 1, 1, input);
+      while (ch != '\n');
       std::fread(&ch, 1, 1, input);
-      if (ch < '0' || ch > '9')
-        return n;
-      n = n * 10 + ch - '0';
     }
+    do {
+      n = n * 10 + ch - '0';
+      std::fread(&ch, 1, 1, input);
+    } while (ch >= '0' && ch <= '9');
+    return n;
   }
 
   bool try_load_ppm(std::FILE *input, image<rgb24> &into) {
diff --git a/skeleton/assets/pointer.ppm b/skeleton/assets/pointer.ppm
new file mode 100644
index 0000000..7169033
Binary files /dev/null and b/skeleton/assets/pointer.ppm differ
diff --git a/skeleton/assets/readme.txt b/skeleton/assets/readme.txt
index 56b1a9b..72476c5 100644
--- a/skeleton/assets/readme.txt
+++ b/skeleton/assets/readme.txt
@@ -2,3 +2,5 @@ the photo in burden.ppm is "selective focus photography snowflakes" by aaron
 burden. it can be found online at
 https://unsplash.com/photos/selective-focus-photography-snowflakes-9yhy1FXlKwI.
 its license can be found online at https://unsplash.com/license.
+
+the icon in pointer.ppm is by me :)