diff --git a/15-pty/scribble.c b/15-pty/scribble.c
index 6e30b87..8972a5c 100644
--- a/15-pty/scribble.c
+++ b/15-pty/scribble.c
@@ -95,8 +95,38 @@ void configure_terminal() {
     tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
 }
 
-// FIXME: Create a thread handler body that copies data from src_fd to two
-//        destination file descriptors.
+struct copy_thread_arg {
+    int src_fd;
+    int dst_fd;
+    int dump_fd;
+};
+
+void* copy_thread(void* data) {
+    struct copy_thread_arg *arg = data;
+
+    char buf[1024];
+    while (true) {
+        int len = read(arg->src_fd, buf, sizeof(buf));
+        if (len < 0)
+            die("read");
+        if (len == 0) break;
+
+        for (int bufpos = 0; bufpos < len; ) {
+            int wlen = write(arg->dst_fd, buf+bufpos, len - bufpos);
+            if (wlen < 0)
+                die("write");
+            bufpos += wlen;
+        }
+
+        for (int bufpos = 0; bufpos < len; ) {
+            int wlen = write(arg->dump_fd, buf+bufpos, len - bufpos);
+            if (wlen < 0)
+                die("write");
+            bufpos += wlen;
+        }
+    }
+    return NULL;
+}
 
 
 int main(int argc, char *argv[]) {
@@ -108,13 +138,89 @@ int main(int argc, char *argv[]) {
     char *IN     = argv[2];
     char **CMD   = &argv[3];
 
-    (void) IN; (void) OUT; (void) CMD; (void) exec_in_pty;
-    // FIXME: Open OUT and IN file
-    // FIXME: Create a new primary PTY device (see pty(7))
-    // FIXME: Get the PTN with ioctl(fd, TIOCGPTN, &pts)
-    // FIXME: Open the child pty end (/dev/pts/{PTN})
+    // We open the dump file for the terminal output OUT and
+    // for the terminal input IN. We open it read-writable (O_RDWR),
+    // create it in case (O_CREAT), truncate it to zero bytes
+    // (O_TRUNC), and instruct the kernel to close the file descriptor
+    // on exec (O_CLOEXEC). With O_CLOEXEC, the descriptor is not available in our child.
+    int out_fd = open(OUT, O_RDWR|O_CREAT|O_TRUNC|O_CLOEXEC, 0600);
+    if (out_fd < 0)
+        die("open/dump");
+
+    int in_fd = open(IN, O_RDWR|O_CREAT|O_TRUNC|O_CLOEXEC, 0600);
+    if (in_fd < 0)
+        die("open/dump");
+
+    // We allocate a new pseudo terminal by opening the special device
+    // file /dev/ptmx. Please see pty(7) for more details on this.
+    // Special is the O_NOCTTY flag which disables the magic of
+    // setting a control terminal.
+    int primary_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY | O_CLOEXEC);
+    if (primary_fd < 0) die("open/ptmux");
+
+    // And now it goes wild! Like a pipe, a pty has two ends. The
+    // primary and the child end (also referred to as secondary). But
+    // unlike the pipe2(2) system call, we have to deduce the child
+    // ptn (pseudo-terminal number) from the primary fd.
+
+    // For the Unix-98 interface, this can simply be done with an
+    // ioctl. With BSD pseudo-terminals it is not that easy.
+    int ptn;
+    if (ioctl(primary_fd, TIOCGPTN, &ptn) < 0)
+        die("ioctl/TIOCGPTN");
+
+    // This unlocks the secondary pseudo-terminal. In the libc, this is
+    // implemented by unlockpt(3).
+    int unlock = 0;
+    if (ioctl(primary_fd, TIOCSPTLCK, &unlock) < 0)
+        die("ioctl/TIOCSPTLOCK");
+
+
+    // Having the pts, we can create a ptsname(3) filename and open
+    // the child end of our pty. Somehow, with pipe2(2), this was much
+    // easier up to this point.
+    char ptsname[128];
+    sprintf(ptsname, "/dev/pts/%d", ptn);
+    int secondary_fd = open(ptsname, O_RDWR|O_NOCTTY|O_CLOEXEC);
+    if (secondary_fd < 0)
+        die("open/pts");
+
+    printf("primary=%d, pts=%s, child=%d\n", primary_fd, ptsname, secondary_fd);
+
+    // We configure our terminal to pass through some keys and to not
+    // echo everything twice.
+    configure_terminal();
+
+    // Spawn the process into our newly created pty.
+    pid_t pid = exec_in_pty(CMD, secondary_fd);
+    printf("child pid=%d\n", pid);
+
+    // We create two threads that copy data from
+    // 1. From STDIN      to the primary fd _and_ the IN file descriptor
+    // 2. From primary_fd  to our STDOUT    _and_ the OUT file descriptor
+    struct copy_thread_arg args[] = {
+        { .src_fd = STDIN_FILENO, .dst_fd = primary_fd,     .dump_fd = in_fd},
+        { .src_fd = primary_fd,    .dst_fd = STDOUT_FILENO, .dump_fd = out_fd},
+    };
+
+    // We use two threads to perform this task to avoid all problems
+    // with blocking. Thereby, we avoid the need to coordinate
+    // everything with epoll(2), which you are probably tired of anyway.
+    pthread_t threads[2];
+    for (unsigned i = 0; i < 2; i++) {
+        int rc = pthread_create(&threads[i], NULL, copy_thread, &args[i]);
+        if (rc < 0) die("pthread_create");
+    }
 
-    // FIXME: Spawn CMD into secondary_fd pty
-    // FIXME: Create two threads to copy data around
-    // FIXME: Use the main thread to waitpid(2) for the child to exit.
+    // In the main thread, we simply wait with waitpid(2) for the
+    // child process to exit.
+    while (1) {
+        int wstatus;
+        if (waitpid(pid, &wstatus, 0) < 0)
+            die("waitpid");
+        if (WIFEXITED(wstatus)) {
+            fprintf(stderr,"child process exited with: %d\n", WEXITSTATUS(wstatus));
+            exit(WEXITSTATUS(wstatus));
+        }
+    }
 }