From ab84ad75629b0a4124221023ca91411d2cd62a32 Mon Sep 17 00:00:00 2001
From: Ben Bridle <ben@derelict.engineering>
Date: Tue, 25 Mar 2025 12:46:49 +1300
Subject: Restructure program

This commit also includes changes to devices according to the latest
devices specification, in particular the math and system devices.
---
 Cargo.lock                                     |  55 +--
 Cargo.toml                                     |   9 +-
 src/bin/br.rs                                  | 461 -------------------------
 src/bin/br/asm.rs                              | 152 ++++++++
 src/bin/br/formats/clang.rs                    |  10 +
 src/bin/br/formats/mod.rs                      |  24 ++
 src/bin/br/main.rs                             | 111 ++++++
 src/bin/br/run.rs                              | 186 ++++++++++
 src/debug.rs                                   | 121 -------
 src/devices.rs                                 |  20 --
 src/devices/clock_device.rs                    | 225 ++++++------
 src/devices/file_device.rs                     | 155 +++++----
 src/devices/file_device/bedrock_file_path.rs   | 287 ---------------
 src/devices/file_device/bedrock_path_buffer.rs |  60 ----
 src/devices/file_device/buffered_file.rs       | 144 --------
 src/devices/file_device/directory_listing.rs   | 120 -------
 src/devices/file_device/entry.rs               |  36 --
 src/devices/file_device/operations.rs          |  47 ---
 src/devices/input_device.rs                    | 202 +++++------
 src/devices/local_device.rs                    | 227 ------------
 src/devices/math_device.rs                     | 211 ++++++-----
 src/devices/memory_device.rs                   | 210 ++++++-----
 src/devices/mod.rs                             |  17 +
 src/devices/remote_device.rs                   |  35 --
 src/devices/screen_device.rs                   | 249 +++++--------
 src/devices/stream_device.rs                   | 235 +++++++++++++
 src/devices/system_device.rs                   | 125 +++----
 src/emulators.rs                               |  32 --
 src/emulators/dynamic_emulator.rs              | 170 +++++++++
 src/emulators/graphical_emulator.rs            | 363 +++++++++----------
 src/emulators/headless_emulator.rs             | 157 ++++-----
 src/emulators/mod.rs                           |  81 +++++
 src/lib.rs                                     |  19 +-
 src/load_program.rs                            |  93 +++++
 src/metadata.rs                                | 127 -------
 src/types/buffered_file.rs                     | 143 ++++++++
 src/types/debug_symbols.rs                     |  59 ++++
 src/types/directory_listing.rs                 | 120 +++++++
 src/types/entry_type.rs                        |  37 ++
 src/types/file_path.rs                         | 288 +++++++++++++++
 src/types/metadata.rs                          | 125 +++++++
 src/types/mod.rs                               |  19 +
 src/types/path_buffer.rs                       |  60 ++++
 src/types/read_buffer.rs                       |  34 ++
 src/types/sprite_buffer.rs                     |  85 +++++
 45 files changed, 3057 insertions(+), 2689 deletions(-)
 delete mode 100644 src/bin/br.rs
 create mode 100644 src/bin/br/asm.rs
 create mode 100644 src/bin/br/formats/clang.rs
 create mode 100644 src/bin/br/formats/mod.rs
 create mode 100644 src/bin/br/main.rs
 create mode 100644 src/bin/br/run.rs
 delete mode 100644 src/debug.rs
 delete mode 100644 src/devices.rs
 delete mode 100644 src/devices/file_device/bedrock_file_path.rs
 delete mode 100644 src/devices/file_device/bedrock_path_buffer.rs
 delete mode 100644 src/devices/file_device/buffered_file.rs
 delete mode 100644 src/devices/file_device/directory_listing.rs
 delete mode 100644 src/devices/file_device/entry.rs
 delete mode 100644 src/devices/file_device/operations.rs
 delete mode 100644 src/devices/local_device.rs
 create mode 100644 src/devices/mod.rs
 delete mode 100644 src/devices/remote_device.rs
 create mode 100644 src/devices/stream_device.rs
 delete mode 100644 src/emulators.rs
 create mode 100644 src/emulators/dynamic_emulator.rs
 create mode 100644 src/emulators/mod.rs
 create mode 100644 src/load_program.rs
 delete mode 100644 src/metadata.rs
 create mode 100644 src/types/buffered_file.rs
 create mode 100644 src/types/debug_symbols.rs
 create mode 100644 src/types/directory_listing.rs
 create mode 100644 src/types/entry_type.rs
 create mode 100644 src/types/file_path.rs
 create mode 100644 src/types/metadata.rs
 create mode 100644 src/types/mod.rs
 create mode 100644 src/types/path_buffer.rs
 create mode 100644 src/types/read_buffer.rs
 create mode 100644 src/types/sprite_buffer.rs

diff --git a/Cargo.lock b/Cargo.lock
index 81b9a4b..ae44e90 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -57,12 +57,27 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "ansi"
+version = "1.0.0"
+source = "git+git://benbridle.com/ansi?tag=v1.0.0#81d47867c2c97a9ae1d1c8fdfcd42c582410ad2a"
+
 [[package]]
 name = "as-raw-xcb-connection"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
 
+[[package]]
+name = "assembler"
+version = "2.2.1"
+source = "git+git://benbridle.com/assembler?tag=v2.2.1#648151b7684214a86e894d0ca813d7a89317722c"
+dependencies = [
+ "ansi",
+ "log 1.1.2",
+ "vagabond",
+]
+
 [[package]]
 name = "atomic-waker"
 version = "1.1.2"
@@ -77,17 +92,16 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "bedrock-asm"
-version = "4.0.6"
-source = "git+git://benbridle.com/bedrock-asm?tag=v4.0.6#c211280e077703903d968f14e8c6699d9f728aa2"
+version = "5.0.0"
+source = "git+git://benbridle.com/bedrock-asm?tag=v5.0.0#e99c683d66b5e20a610094cbe0fad75729383dd1"
 dependencies = [
- "vagabond",
- "xflags",
+ "assembler",
 ]
 
 [[package]]
 name = "bedrock-core"
-version = "5.0.0"
-source = "git+git://benbridle.com/bedrock-core?tag=v5.0.0#179bd6a13d91f0a1137ee8ed6aebb7e226e99b5d"
+version = "5.1.0"
+source = "git+git://benbridle.com/bedrock-core?tag=v5.1.0#341df6405e3097b3c58aa57e1b08a663d34b4e89"
 
 [[package]]
 name = "bedrock-pc"
@@ -97,7 +111,7 @@ dependencies = [
  "bedrock-core",
  "chrono",
  "geometry",
- "log 1.1.1",
+ "log 1.1.2",
  "phosphor",
  "switchboard",
  "windows",
@@ -637,6 +651,14 @@ name = "log"
 version = "1.1.1"
 source = "git+git://benbridle.com/log?tag=v1.1.1#930f3d0e2b82df1243f423c092a38546ea7533c3"
 
+[[package]]
+name = "log"
+version = "1.1.2"
+source = "git+git://benbridle.com/log?tag=v1.1.2#3d5d1f7a19436151ba1dd52a2b50664969d90db6"
+dependencies = [
+ "ansi",
+]
+
 [[package]]
 name = "memchr"
 version = "2.6.4"
@@ -1198,8 +1220,8 @@ dependencies = [
 
 [[package]]
 name = "switchboard"
-version = "1.0.0"
-source = "git+git://benbridle.com/switchboard?tag=v1.0.0#ea70fa89659e5cf1a9d4ca6ea31fb67f7a2cc633"
+version = "2.1.0"
+source = "git+git://benbridle.com/switchboard?tag=v2.1.0#e6435712ba5b3ca36e99fc8cbe7755940f8b1f3f"
 dependencies = [
  "log 1.1.1",
  "paste",
@@ -1909,21 +1931,6 @@ version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d491ee231a51ae64a5b762114c3ac2104b967aadba1de45c86ca42cf051513b7"
 
-[[package]]
-name = "xflags"
-version = "0.4.0-pre.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4697c0db52cfb7277cf997ed334c92c739fafc7c5d44a948a906a5bf4b41a63f"
-dependencies = [
- "xflags-macros",
-]
-
-[[package]]
-name = "xflags-macros"
-version = "0.4.0-pre.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94d18ac1a136311770ed587356f8a828c9b86261f68761f34e6cdc6d5b4c435c"
-
 [[package]]
 name = "xkbcommon-dl"
 version = "0.4.2"
diff --git a/Cargo.toml b/Cargo.toml
index 448a71f..bfad173 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,18 +6,19 @@ edition = "2021"
 description = "Emulator for running Bedrock programs"
 
 [dependencies]
-bedrock-asm = { git = "git://benbridle.com/bedrock-asm", tag = "v4.0.6" }
-bedrock-core = { git = "git://benbridle.com/bedrock-core", tag = "v5.0.0" }
+bedrock-asm = { git = "git://benbridle.com/bedrock-asm", tag = "v5.0.0" }
+bedrock-core = { git = "git://benbridle.com/bedrock-core", tag = "v5.1.0" }
 phosphor = { git = "git://benbridle.com/phosphor", tag = "v3.2.2" }
 geometry = { git = "git://benbridle.com/geometry", tag = "v1.0.0" }
-log = { git = "git://benbridle.com/log", tag = "v1.1.1" }
-switchboard = { git = "git://benbridle.com/switchboard", tag = "v1.0.0" }
+log = { git = "git://benbridle.com/log", tag = "v1.1.2" }
+switchboard = { git = "git://benbridle.com/switchboard", tag = "v2.1.0" }
 
 chrono = { version = "0.4.38" }
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows = { version = "0.58.0", features = ["Win32_Storage_FileSystem"] }
 
+
 [profile.release]
 lto=true
 opt-level="s"
diff --git a/src/bin/br.rs b/src/bin/br.rs
deleted file mode 100644
index d7bde60..0000000
--- a/src/bin/br.rs
+++ /dev/null
@@ -1,461 +0,0 @@
-use bedrock_asm::*;
-use bedrock_pc::*;
-use phosphor::*;
-
-use log::{info, fatal};
-use switchboard::{Switchboard, SwitchQuery};
-
-use std::cmp::max;
-use std::io::{Read, Write};
-use std::path::{Path, PathBuf};
-use std::process::exit;
-
-
-fn print_help() -> ! {
-    println!("\
-Usage: br [source]
-       br asm [source] [destination] [extension]
-
-Integrated Bedrock assembler and emulator.
-
-Usage:
-  To load a Bedrock program from a file, run `br <path>`, where
-  <path> is the path to an assembled Bedrock program.
-
-  To load a Bedrock program from piped input, run `<command> | br`,
-  where <command> is a command that generates Bedrock bytecode.
-
-  To assemble a Bedrock program from a source file and write to an
-  output file, run `br asm <source> <output>`, where <source> is the
-  path of the source file and <output> is the path to write to.
-
-  To assemble and run a Bedrock program without saving to a file,
-  run `br asm <source> | br`, where <source> is the path of the
-  source file.
-
-Environment variables:
-  BEDROCK_PATH
-    A list of colon-separated paths which will be searched to find
-    a Bedrock program when the program doesn't exist in the current
-    directory. This allows the user to run frequently-used programs
-    from any directory.
-  BEDROCK_LIBS
-    A list of colon-separated paths which will be searched to find
-    Bedrock source code files to use as libraries when assembling a
-    Bedrock program. If a library file resolves an unresolved symbol
-    in the program being assembled, the library file will be merged
-    into the program.
-
-Arguments:
-  [program]            Path to a Bedrock program to run
-
-Switches:
-  --verbose,       (-v)  Print additional debug information
-  --version              Print the assembler version and exit
-  --help           (-h)  Prints help
-  --debug,         (-d)  Show debug information while the program is running
-  --fullscreen     (-f)  Start the program in fullscreen mode (toggle with F11)
-  --size=<dim>     (-s)  Set the initial window size in the format <width>x<height>
-  --zoom=<scale>   (-z)  Set the pixel size for the screen (change with F5/F6)
-  --palette=<pal>        Set a debug colour palette in the format <rgb>,... (toggle with F2)
-  --show-cursor    (-c)  Show the operating system cursor over the window
-  --decode-stdin   (-i)  Decode standard input
-  --encode-stdout  (-o)  Encode standard output
-
-Arguments (asm mode):
-  [source]               Bedrock source code file to assemble.
-  [destination]          Destination path for assembler output.
-  [extension]            File extension to identify source files.
-
-Switches (asm mode):
-  --no-libs              Don't include libraries or resolve references.
-  --no-project-libs      Don't include project libraries
-  --no-env-libs          Don't include environment libraries.
-  --tree                 Show the resolved source file heirarchy
-  --check                Assemble the program without saving any output
-  --resolve              Only return resolved source code.
-  --symbols              Generate debug symbols with file extension '.br.sym'
-");
-    std::process::exit(0);
-}
-
-
-fn print_version() -> ! {
-    let name = env!("CARGO_PKG_NAME");
-    let version = env!("CARGO_PKG_VERSION");
-    eprintln!("{name} v{version}");
-    eprintln!("Written by Ben Bridle.");
-    std::process::exit(0);
-}
-
-
-fn main() {
-    let mut args = Switchboard::from_env();
-    if args.named("help").short('h').as_bool() {
-        print_help();
-    }
-    if args.named("version").as_bool() {
-        print_version();
-    }
-    if args.named("verbose").short('v').as_bool() {
-        log::set_log_level(log::LogLevel::Info);
-    }
-    match args.peek() {
-        Some("run") => { args.pop(); main_run(args) },
-        Some("asm") => { args.pop(); main_asm(args) },
-        _ => main_run(args),
-    }
-}
-
-fn main_run(mut args: Switchboard) {
-    let source = args.positional("source").as_path_opt();
-    let debug = args.named("debug").short('d').as_bool();
-    let fullscreen = args.named("fullscreen").short('f').as_bool();
-    let scale = max(1, args.named("zoom").short('z').default("1").quick("3").as_u32());
-    let dimensions = match args.named("size").short('s').as_string_opt() {
-        Some(string) => parse_dimensions(&string).unwrap_or_else(||
-            fatal!("Invalid dimensions string {string:?}")),
-        None => DEFAULT_SCREEN_SIZE / (scale as u16),
-    };
-    let debug_palette = match args.named("palette").as_string_opt() {
-        Some(string) => match parse_palette(&string) {
-            Some(palette) => Some(palette),
-            None => fatal!("Invalid palette string {string:?}"),
-        },
-        None => None,
-    };
-    let show_cursor = args.named("show-cursor").short('c').as_bool();
-    let decode_stdin = args.named("decode-stdin").short('i').as_bool();
-    let encode_stdout = args.named("encode-stdout").short('o').as_bool();
-
-    let Bytecode { bytes: bytecode, path } = load_bytecode(source.as_ref());
-    let symbols_path = path.as_ref().map(|p| {
-        let mut path = p.to_path_buf();
-        path.set_extension("br.sym");
-        path
-    });
-
-    let metadata = parse_metadata(&bytecode);
-    if metadata.is_none() {
-        info!("Could not read program metadata");
-    }
-
-    let config = EmulatorConfig {
-        dimensions,
-        fullscreen,
-        scale,
-        debug_palette,
-        show_cursor,
-        initial_transmission: None,
-        decode_stdin,
-        encode_stdout,
-        symbols_path,
-    };
-    let phosphor = Phosphor::new();
-
-    if phosphor.is_ok() && dimensions.area_usize() != 0 {
-        info!("Starting graphical emulator");
-        let mut phosphor = phosphor.unwrap();
-        let cursor = match config.show_cursor {
-            true => Some(CursorIcon::Default),
-            false => None,
-        };
-
-        let mut graphical = GraphicalEmulator::new(&config, debug);
-        graphical.load_program(&bytecode);
-        if let EmulatorSignal::Promote = graphical.run() {
-            let program_name = match &metadata {
-                Some(metadata) => match &metadata.name {
-                    Some(name) => name.to_string(),
-                    None => String::from("Bedrock"),
-                }
-                None => String::from("Bedrock"),
-            };
-            let window = WindowBuilder {
-                dimensions: Some(graphical.dimensions()),
-                size_bounds: Some(graphical.size_bounds()),
-                fullscreen: graphical.fullscreen,
-                scale: graphical.scale,
-                title: Some(program_name),
-                cursor,
-                icon: None,
-                program: Box::new(graphical),
-            };
-
-            phosphor.create_window(window);
-            phosphor.run().unwrap();
-        }
-    } else {
-        info!("Starting headless emulator");
-        let mut headless = HeadlessEmulator::new(&config, debug);
-        headless.load_program(&bytecode);
-        headless.run(debug);
-    };
-
-    std::process::exit(0);
-}
-
-fn load_bytecode(path: Option<&PathBuf>) -> Bytecode {
-    // TODO: Etch file location into bytecode as per metadata.
-    if let Some(path) = path {
-        if let Ok(bytecode) = load_bytecode_from_file(path) {
-            let length = bytecode.bytes.len();
-            let path = bytecode.path();
-            info!("Loaded program from {path:?} ({length} bytes)");
-            return bytecode;
-        } else if let Some(bytecode) = load_bytecode_from_bedrock_path(path) {
-            let length = bytecode.bytes.len();
-            let path = bytecode.path();
-            info!("Loaded program from {path:?} ({length} bytes)");
-            return bytecode;
-        } else {
-            fatal!("Could not read program from {path:?}");
-        }
-    } else {
-        info!("Reading program from standard input...");
-        if let Ok(bytecode) = load_bytecode_from_stdin() {
-            let length = bytecode.bytes.len();
-            info!("Loaded program from standard input ({length} bytes)");
-            return bytecode;
-        } else {
-            fatal!("Could not read program from standard input");
-        }
-    }
-}
-
-/// Attempt to load bytecode from a directory in the BEDROCK_PATH environment variable.
-fn load_bytecode_from_bedrock_path(path: &Path) -> Option<Bytecode> {
-    if path.is_relative() && path.components().count() == 1 {
-        for base_path in std::env::var("BEDROCK_PATH").ok()?.split(':') {
-            let mut base_path = PathBuf::from(base_path);
-            if !base_path.is_absolute() { continue; }
-            base_path.push(path);
-            info!("Attempting to load program from {base_path:?}");
-            if let Ok(bytecode) = load_bytecode_from_file(&base_path) {
-                return Some(bytecode);
-            }
-            if path.extension().is_some() { continue; }
-            base_path.set_extension("br");
-            info!("Attempting to load program from {base_path:?}");
-            if let Ok(bytecode) = load_bytecode_from_file(&base_path) {
-                return Some(bytecode);
-            }
-        }
-    }
-    return None;
-}
-
-/// Attempt to load bytecode from a file path.
-fn load_bytecode_from_file(path: &Path) -> Result<Bytecode, std::io::Error> {
-    // Canonicalize paths so that symbolic links to program files resolve to
-    // the real program directory, which could contain a symbols file.
-    let path = match path.canonicalize() {
-        Ok(canonical) => canonical,
-        Err(_) => path.to_path_buf(),
-    };
-    load_bytecode_from_readable_source(std::fs::File::open(&path)?, Some(&path))
-}
-
-/// Attempt to load bytecode from standard input.
-fn load_bytecode_from_stdin() -> Result<Bytecode, std::io::Error> {
-    load_bytecode_from_readable_source(std::io::stdin(), None)
-}
-
-/// Attempt to load bytecode from a source that implements std::io::Read.
-fn load_bytecode_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<Bytecode, std::io::Error> {
-    let mut bytes = Vec::<u8>::new();
-    source.take(65536).read_to_end(&mut bytes)?;
-    return Ok(Bytecode { bytes, path: path.map(|p| p.to_path_buf()) });
-}
-
-struct Bytecode {
-    bytes: Vec<u8>,
-    path: Option<PathBuf>,
-}
-
-impl Bytecode {
-    fn path(&self) -> String {
-        match &self.path {
-            Some(path) => path.as_os_str().to_string_lossy().to_string(),
-            None => String::from("<unknown>"),
-        }
-    }
-}
-
-
-fn main_asm(mut args: Switchboard) {
-    let source = args.positional("source").as_path_opt();
-    let destination = args.positional("source").as_path_opt();
-    let extension = args.positional("extension").default("brc").as_string();
-
-    let no_libs = args.named("no-libs").as_bool();
-    let no_project_libs = args.named("no-project-libs").as_bool();
-    let no_environment_libs = args.named("no-env-libs").as_bool();
-
-    let print_tree = args.named("tree").as_bool();
-    let dry_run = args.named("check").as_bool();
-    let only_resolve = args.named("resolve").as_bool();
-    let export_symbols = args.named("symbols").as_bool();
-
-    // -----------------------------------------------------------------------
-    // RESOLVE syntactic symbols
-    let source_path = source.clone().map(|p| {
-        p.canonicalize().unwrap_or(p)
-    });
-
-    let mut resolver = if let Some(path) = &source_path {
-        match SourceUnit::from_path(&path, &extension) {
-            Ok(source_unit) => SymbolResolver::from_source_unit(source_unit),
-            Err(err) => {
-                match err {
-                    ParseError::InvalidExtension => fatal!(
-                        "File {path:?} has invalid extension, must be '.{extension}'"),
-                    ParseError::NotFound => fatal!(
-                        "File {path:?} was not found"),
-                    ParseError::InvalidUtf8 => fatal!(
-                        "File {path:?} does not contain valid UTF-8 text"),
-                    ParseError::NotReadable => fatal!(
-                        "File {path:?} is not readable"),
-                    ParseError::IsADirectory => fatal!(
-                        "File {path:?} is a directory"),
-                    ParseError::Unknown => fatal!(
-                        "Unknown error while attempting to read from {path:?}")
-                };
-            }
-        }
-    } else {
-        let mut source_code = String::new();
-        info!("Reading program source from standard input");
-        if let Err(err) = std::io::stdin().read_to_string(&mut source_code) {
-            fatal!("Could not read from standard input\n{err:?}");
-        }
-        let path = "<standard input>";
-        let source_unit = SourceUnit::from_source_code(source_code, path);
-        SymbolResolver::from_source_unit(source_unit)
-    };
-    // Load project libraries.
-    if let Some(path) = &source_path {
-        if !no_libs && !no_project_libs {
-            let project_library = gather_project_libraries(path, &extension);
-            resolver.add_library_units(project_library);
-        }
-    }
-    // Load environment libraries.
-    if !no_libs && !no_environment_libs {
-        for env_library in gather_environment_libraries(&extension) {
-            resolver.add_library_units(env_library);
-        }
-    }
-    resolver.resolve();
-
-    // -----------------------------------------------------------------------
-    // PRINT information, generate merged source code
-    if print_tree {
-        print_source_tree(&resolver);
-    }
-    if print_resolver_errors(&resolver) {
-        std::process::exit(1);
-    };
-    let merged_source = match resolver.get_merged_source_code() {
-        Ok(merged_source) => merged_source,
-        Err(ids) => {
-            print_cyclic_source_units(&ids, &resolver);
-            std::process::exit(1);
-        },
-    };
-    if only_resolve && !dry_run {
-        write_bytes_and_exit(merged_source.as_bytes(), destination.as_ref());
-    }
-
-    // -----------------------------------------------------------------------
-    // PARSE semantic tokens from merged source code
-    let path = Some("<merged source>");
-    let mut semantic_tokens = generate_semantic_tokens(&merged_source, path);
-    if print_semantic_errors(&semantic_tokens, &merged_source) {
-        std::process::exit(1);
-    };
-
-    // -----------------------------------------------------------------------
-    // GENERATE symbols file and bytecode
-    let bytecode = generate_bytecode(&mut semantic_tokens);
-
-    if export_symbols && !dry_run {
-        if let Some(path) = &destination {
-            let mut symbols_path = path.to_path_buf();
-            symbols_path.set_extension("br.sym");
-            let symbols = generate_symbols_file(&semantic_tokens);
-            if let Err(err) = std::fs::write(&symbols_path, symbols) {
-                info!("Could not write to symbols path {symbols_path:?}");
-                eprintln!("{err:?}");
-            } else {
-                info!("Saved debug symbols to {symbols_path:?}");
-            }
-        }
-    }
-
-    let length = bytecode.len();
-    let percentage = (length as f32 / 65536.0 * 100.0).round() as u16;
-    info!("Assembled program in {length} bytes ({percentage}% of maximum)");
-
-    if !dry_run {
-        write_bytes_and_exit(&bytecode, destination.as_ref());
-    }
-}
-
-fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! {
-    if let Some(path) = path {
-        if let Err(err) = std::fs::write(path, bytes) {
-            fatal!("Could not write to path {:?}\n{err:?}", path.as_ref());
-        }
-    } else {
-        if let Err(err) = std::io::stdout().write_all(bytes) {
-            fatal!("Could not write to standard output\n{err:?}");
-        }
-    }
-    exit(0);
-}
-
-
-fn parse_dimensions(string: &str) -> Option<ScreenDimensions> {
-    if string.trim().to_lowercase() == "none" {
-        return Some(ScreenDimensions::ZERO);
-    }
-    let (w_str, h_str) = string.trim().split_once('x')?;
-    Some( ScreenDimensions {
-        width:  w_str.parse().ok()?,
-        height: h_str.parse().ok()?,
-    } )
-}
-
-fn parse_palette(string: &str) -> Option<[Colour; 16]> {
-    fn decode_ascii_hex_digit(ascii: u8) -> Option<u8> {
-        match ascii {
-            b'0'..=b'9' => Some(ascii - b'0'),
-            b'a'..=b'f' => Some(ascii - b'a' + 10),
-            b'A'..=b'F' => Some(ascii - b'A' + 10),
-            _ => { None }
-        }
-    }
-    let mut c = Vec::new();
-    for token in string.split(',') {
-        let mut bytes = token.bytes();
-        if bytes.len() != 3 { return None; }
-        let r = decode_ascii_hex_digit(bytes.next().unwrap())?;
-        let g = decode_ascii_hex_digit(bytes.next().unwrap())?;
-        let b = decode_ascii_hex_digit(bytes.next().unwrap())?;
-        c.push(Colour::from_rgb(r*17, g*17, b*17));
-    }
-    Some(match c.len() {
-        2 => [ c[0],  c[1],  c[0],  c[1],  c[0],  c[1],  c[0],  c[1],
-               c[0],  c[1],  c[0],  c[1],  c[0],  c[1],  c[0],  c[1] ],
-        3 => [ c[0],  c[1],  c[0],  c[2],  c[0],  c[1],  c[0],  c[2],
-               c[0],  c[1],  c[0],  c[2],  c[0],  c[1],  c[0],  c[2] ],
-        4 => [ c[0],  c[1],  c[2],  c[3],  c[0],  c[1],  c[2],  c[3],
-               c[0],  c[1],  c[2],  c[3],  c[0],  c[1],  c[2],  c[3] ],
-        8 => [ c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7],
-               c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7] ],
-       16 => [ c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7],
-               c[8],  c[9], c[10], c[11], c[12], c[13], c[14], c[15] ],
-        _ => return None,
-    })
-}
diff --git a/src/bin/br/asm.rs b/src/bin/br/asm.rs
new file mode 100644
index 0000000..f4620cf
--- /dev/null
+++ b/src/bin/br/asm.rs
@@ -0,0 +1,152 @@
+use crate::formats::*;
+
+use bedrock_asm::*;
+use log::{info, fatal};
+use switchboard::*;
+
+use std::io::{Read, Write};
+use std::path::Path;
+
+
+pub fn main(mut args: Switchboard) {
+    args.positional("source");
+    args.positional("destination");
+    args.named("extension").default("brc");
+
+    args.named("no-libs");
+    args.named("no-project-libs");
+    args.named("no-env-libs");
+
+    args.named("format").default("binary");
+    args.named("dry-run").short('n');
+    args.named("tree");
+    args.named("with-symbols");
+    args.raise_errors();
+
+    let source_path        = args.get("source").as_path_opt().map(
+        |p| p.canonicalize().unwrap_or_else(|e| fatal!("{p:?}: {e:?}")));
+    let destination_path   = args.get("destination").as_path_opt();
+    let extension          = args.get("extension").as_string();
+    let opt_extension      = Some(extension.as_str());
+
+    let no_libs            = args.get("no-libs").as_bool();
+    let no_project_libs    = args.get("no-project-libs").as_bool();
+    let no_env_libs        = args.get("no-env-libs").as_bool();
+
+    let format             = Format::from_str(args.get("format").as_str());
+    let dry_run            = args.get("dry-run").as_bool();
+    let print_tree         = args.get("tree").as_bool();
+    let export_symbols     = args.get("with-symbols").as_bool();
+
+    // -----------------------------------------------------------------------
+
+    let mut compiler = new_compiler();
+
+    if let Some(path) = &source_path {
+        info!("Reading program source from {path:?}");
+        compiler.root_from_path(path).unwrap_or_else(|err| fatal!("{err:?}: {path:?}"));
+    } else {
+        let mut source_code = String::new();
+        info!("Reading program source from standard input");
+        if let Err(err) = std::io::stdin().read_to_string(&mut source_code) {
+            fatal!("Could not read from standard input\n{err:?}");
+        }
+        compiler.root_from_string(source_code, "<standard input>")
+    };
+    if compiler.error().is_some() && !no_libs && !no_project_libs {
+        compiler.include_libs_from_parent(opt_extension);
+    }
+    if compiler.error().is_some() && !no_libs && !no_env_libs {
+        compiler.include_libs_from_path_variable("BEDROCK_LIBS", opt_extension);
+    }
+
+    if print_tree {
+        compiler.hierarchy().report()
+    }
+    if let Some(error) = compiler.error() {
+        error.report();
+        std::process::exit(1);
+    }
+
+    let merged_source = compiler.get_compiled_source().unwrap_or_else(|error| {
+        error.report();
+        std::process::exit(1);
+    });
+
+    if !dry_run && format == Format::Source {
+        write_bytes_and_exit(merged_source.as_bytes(), destination_path.as_ref());
+    }
+
+    // -----------------------------------------------------------------------
+
+    let path = Some("<merged source>");
+    let syntactic = match parse_syntactic(&merged_source, path) {
+        Ok(tokens) => tokens,
+        Err(errors) => {
+            report_syntactic_errors(&errors, &merged_source);
+            std::process::exit(1);
+        }
+    };
+
+    let semantic = match parse_semantic(syntactic) {
+        Ok(tokens) => tokens,
+        Err(errors) => {
+            report_semantic_errors(&errors, &merged_source);
+            std::process::exit(1);
+        }
+    };
+
+    let AssembledProgram { mut bytecode, symbols } = generate_bytecode(&semantic);
+    // Remove null bytes from end of bytecode.
+    while let Some(0) = bytecode.last() {
+        bytecode.pop();
+    }
+
+    let length = bytecode.len();
+    let percentage = (length as f32 / 65536.0 * 100.0).round() as u16;
+    info!("Assembled program in {length} bytes ({percentage}% of maximum)");
+
+    if !dry_run {
+        if export_symbols {
+            if let Some(path) = &destination_path {
+                let mut symbols_path = path.to_path_buf();
+                symbols_path.add_extension("sym");
+                let mut symbols_string = String::new();
+                for symbol in &symbols {
+                    let address = &symbol.address;
+                    let name = &symbol.name;
+                    let location = &symbol.source.location();
+                    symbols_string.push_str(&format!(
+                        "{address:04x} {name} {location}\n"
+                    ));
+                }
+                match std::fs::write(&symbols_path, symbols_string) {
+                    Ok(_) => info!("Saved symbols to {symbols_path:?}"),
+                    Err(err) => info!("Could not write symbols to {symbols_path:?}\n{err:?}"),
+                }
+            }
+        }
+
+        let bytes = match format {
+            Format::Binary => bytecode,
+            Format::Clang => format_clang(&bytecode),
+            Format::Source => unreachable!("Source output is handled before full assembly"),
+        };
+        write_bytes_and_exit(&bytes, destination_path.as_ref());
+    }
+}
+
+
+fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! {
+    match path {
+        Some(path) => match std::fs::write(path, bytes) {
+            Ok(_) => info!("Wrote output to {:?}", path.as_ref()),
+            Err(err) => fatal!("Could not write to {:?}\n{err:?}", path.as_ref()),
+        }
+        None => match std::io::stdout().write_all(bytes) {
+            Ok(_) => info!("Wrote output to standard output"),
+            Err(err) => fatal!("Could not write to standard output\n{err:?}"),
+        }
+    }
+    std::process::exit(0);
+}
diff --git a/src/bin/br/formats/clang.rs b/src/bin/br/formats/clang.rs
new file mode 100644
index 0000000..30d80de
--- /dev/null
+++ b/src/bin/br/formats/clang.rs
@@ -0,0 +1,10 @@
+pub fn format_clang(bytecode: &[u8]) -> Vec<u8> {
+    let mut output = String::new();
+    for chunk in bytecode.chunks(16) {
+        for byte in chunk {
+            output.push_str(&format!("0x{byte:02x}, "));
+        }
+        output.push('\n');
+    }
+    return output.into_bytes();
+}
diff --git a/src/bin/br/formats/mod.rs b/src/bin/br/formats/mod.rs
new file mode 100644
index 0000000..b159b16
--- /dev/null
+++ b/src/bin/br/formats/mod.rs
@@ -0,0 +1,24 @@
+mod clang;
+
+pub use clang::*;
+
+use log::fatal;
+
+
+#[derive(Clone, Copy, PartialEq)]
+pub enum Format {
+    Binary,
+    Source,
+    Clang,
+}
+
+impl Format {
+    pub fn from_str(string: &str) -> Self {
+        match string {
+            "binary" => Self::Binary,
+            "source" => Self::Source,
+            "c" => Self::Clang,
+            _ => fatal!("Unknown format '{string}', expected 'binary', 'c', or 'source'"),
+        }
+    }
+}
diff --git a/src/bin/br/main.rs b/src/bin/br/main.rs
new file mode 100644
index 0000000..52c53b0
--- /dev/null
+++ b/src/bin/br/main.rs
@@ -0,0 +1,111 @@
+#![feature(path_add_extension)]
+
+mod asm;
+mod run;
+mod formats;
+
+use switchboard::*;
+
+
+fn main() {
+    let mut args = Switchboard::from_env();
+    args.named("help").short('h');
+    args.named("version");
+    args.named("verbose").short('v');
+
+    if args.get("help").as_bool() {
+        print_help();
+    }
+    if args.get("version").as_bool() {
+        print_version();
+    }
+    if args.get("verbose").as_bool() {
+        log::set_log_level(log::LogLevel::Info);
+    }
+    match args.peek() {
+        Some("run") => { args.pop(); run::main(args) },
+        Some("asm") => { args.pop(); asm::main(args) },
+        _ => run::main(args),
+    }
+}
+
+
+fn print_help() -> ! {
+    eprintln!("\
+Usage: br [source]
+       br asm [source] [destination]
+
+Integrated Bedrock assembler and emulator.
+
+Usage:
+  To load a Bedrock program from a file, run `br <path>`, where
+  <path> is the path to an assembled Bedrock program.
+
+  To load a Bedrock program from piped input, run `<command> | br`,
+  where <command> is a command that generates Bedrock bytecode.
+
+  To assemble a Bedrock program from a source file and write to an
+  output file, run `br asm <source> <output>`, where <source> is the
+  path of the source file and <output> is the path to write to.
+
+  To assemble and run a Bedrock program without saving to a file,
+  run `br asm <source> | br`, where <source> is the path of the
+  source file.
+
+Environment variables:
+  BEDROCK_PATH
+    A list of colon-separated paths which will be searched to find
+    a Bedrock program when the program doesn't exist in the current
+    directory. This allows the user to run frequently-used programs
+    from any directory.
+  BEDROCK_LIBS
+    A list of colon-separated paths which will be searched to find
+    Bedrock source code files to use as libraries when assembling a
+    Bedrock program. If a library file resolves an unresolved symbol
+    in the program being assembled, the library file will be merged
+    into the program.
+
+Arguments:
+  [program]            Path to a Bedrock program to run
+
+Switches:
+  --fullscreen     (-f)  Start the program in fullscreen mode (toggle with F11)
+  --size=<dim>     (-s)  Set the initial window size in the format <width>x<height>
+  --zoom=<scale>   (-z)  Set the pixel size for the screen (change with F5/F6)
+  --show-cursor    (-c)  Show the operating system cursor over the window
+  --palette=<pal>        Set a debug colour palette in the format <rgb>,... (toggle with F2)
+  --debug,         (-d)  Show debug information while the program is running
+  --decode-stdin   (-i)  Decode transmissions on standard input from text lines.
+  --encode-stdout  (-o)  Encode transmissions on standard output as text lines.
+  --help           (-h)  Print this help information
+  --verbose,       (-v)  Print additional information
+  --version              Print the program version and exit
+
+Arguments (asm mode):
+  [source]               Bedrock source code file to assemble.
+  [destination]          Destination path for assembler output.
+
+Switches (asm mode):
+  --dry-run        (-n)  Assemble and show errors only, don't write any output
+  --extension            File extension to identify source files (default is 'brc')
+  --format=<fmt>         Output format to use for assembled program (default is 'binary')
+  --no-project-libs      Don't search for libraries in the source parent folder
+  --no-env-libs          Don't search for libraries in the BEDROCK_LIBS path variable
+  --no-libs              Combination of --no-project-libs and --no-env-libs
+  --tree                 Display a tree visualisation of all included library files
+  --with-symbols         Also generate debug symbols file with extension '.sym'
+  --help           (-h)  Print this help information
+  --verbose,       (-v)  Print additional information
+  --version              Print the program version and exit
+");
+    std::process::exit(0);
+}
+
+
+fn print_version() -> ! {
+    let name = env!("CARGO_PKG_NAME");
+    let version = env!("CARGO_PKG_VERSION");
+    eprintln!("{name} v{version}");
+    eprintln!("Written by Ben Bridle.");
+    std::process::exit(0);
+}
diff --git a/src/bin/br/run.rs b/src/bin/br/run.rs
new file mode 100644
index 0000000..86fdd43
--- /dev/null
+++ b/src/bin/br/run.rs
@@ -0,0 +1,186 @@
+use log::*;
+use crate::*;
+
+use bedrock_pc::*;
+use phosphor::*;
+
+use std::cmp::{min, max};
+use std::num::NonZeroU32;
+
+
+pub fn main(mut args: Switchboard) {
+    args.positional("source");
+
+    args.named("debug").short('d');
+    args.named("mode").default("dynamic");
+
+    args.named("palette");
+    args.named("fullscreen").short('f');
+    args.named("show-cursor").short('c');
+    args.named("zoom").short('z').quick("3").default("1");
+    args.named("size").short('s');
+
+    args.named("decode-stdin").short('i');
+    args.named("encode-stdout").short('o');
+    args.raise_errors();
+
+    let source             = args.get("source").as_path_opt();
+    let debug              = args.get("debug").as_bool();
+    let mode               = Mode::from_str(args.get("mode").as_str());
+    let override_palette   = args.get("palette").as_str_opt().map(parse_palette);
+    let fullscreen         = args.get("fullscreen").as_bool();
+    let show_cursor        = args.get("show-cursor").as_bool();
+    let zoom_raw           = min(10, max(1, args.get("zoom").as_u32()));
+    let zoom               = unsafe { NonZeroU32::new_unchecked(zoom_raw) };
+    let initial_dimensions = match args.get("size").as_str_opt() {
+        Some(string) => parse_dimensions(string),
+        None => DEFAULT_SCREEN_SIZE / (zoom_raw as u16),
+    };
+    let decode_stdin       = args.get("decode-stdin").as_bool();
+    let encode_stdout      = args.get("encode-stdout").as_bool();
+
+    // -----------------------------------------------------------------------
+
+    let Program { bytecode, path } = load_program(source.as_ref());
+
+    let symbols_path = path.as_ref().map(|p| {
+        let mut path = p.to_path_buf();
+        path.add_extension("sym");
+        path
+    });
+
+    let metadata = parse_metadata(&bytecode);
+    if metadata.is_none() {
+        info!("Could not read program metadata");
+    }
+
+    let config = EmulatorConfig {
+        initial_dimensions,
+        fullscreen,
+        zoom,
+        override_palette,
+        show_cursor,
+
+        decode_stdin,
+        encode_stdout,
+
+        symbols_path,
+        metadata,
+    };
+
+    if let Ok(phosphor) = Phosphor::new() {
+        match mode {
+            Mode::Dynamic => {
+                info!("Starting dynamic emulator");
+                let mut emulator = DynamicEmulator::new(&config, debug);
+                emulator.load_program(&bytecode);
+                emulator.run();
+                info!("Upgrading dynamic emulator to graphical emulator");
+                let emulator = emulator.to_graphical(config);
+                emulator.run(phosphor);
+            }
+            Mode::Graphical => {
+                info!("Starting graphical emulator");
+                let emulator = GraphicalEmulator::new(config, debug);
+                emulator.run(phosphor);
+            }
+            Mode::Headless => {
+                info!("Starting headless emulator");
+                let mut emulator = HeadlessEmulator::new(&config, debug);
+                emulator.run();
+            }
+        }
+    } else {
+        match mode {
+            Mode::Dynamic => {
+                info!("Could not start graphical event loop");
+                info!("Starting headless emulator");
+                let mut emulator = HeadlessEmulator::new(&config, debug);
+                emulator.run();
+            }
+            Mode::Graphical => {
+                fatal!("Could not start graphical event loop");
+            }
+            Mode::Headless => {
+                info!("Starting headless emulator");
+                let mut emulator = HeadlessEmulator::new(&config, debug);
+                emulator.run();
+            }
+        }
+    }
+}
+
+
+fn parse_dimensions(string: &str) -> ScreenDimensions {
+    fn parse_inner(string: &str) -> Option<ScreenDimensions> {
+        let (w_str, h_str) = string.trim().split_once('x')?;
+        Some( ScreenDimensions {
+            width:  w_str.parse().ok()?,
+            height: h_str.parse().ok()?,
+        } )
+    }
+    let dimensions = parse_inner(string).unwrap_or_else(|| {
+        fatal!("Invalid dimensions string '{string}'");
+    });
+    if dimensions.is_zero() {
+        fatal!("Screen dimensions must be greater than zero");
+    }
+    return dimensions;
+}
+
+fn parse_palette(string: &str) -> [Colour; 16] {
+    fn decode_ascii_hex_digit(ascii: u8) -> Option<u8> {
+        match ascii {
+            b'0'..=b'9' => Some(ascii - b'0'),
+            b'a'..=b'f' => Some(ascii - b'a' + 10),
+            b'A'..=b'F' => Some(ascii - b'A' + 10),
+            _ => { None }
+        }
+    }
+    fn parse_inner(string: &str) -> Option<[Colour; 16]> {
+        let mut c = Vec::new();
+        for token in string.split(',') {
+            let mut bytes = token.bytes();
+            if bytes.len() != 3 { return None; }
+            let r = decode_ascii_hex_digit(bytes.next().unwrap())?;
+            let g = decode_ascii_hex_digit(bytes.next().unwrap())?;
+            let b = decode_ascii_hex_digit(bytes.next().unwrap())?;
+            c.push(Colour::from_rgb(r*17, g*17, b*17));
+        }
+        Some(match c.len() {
+            2 => [ c[0],  c[1],  c[0],  c[1],  c[0],  c[1],  c[0],  c[1],
+                   c[0],  c[1],  c[0],  c[1],  c[0],  c[1],  c[0],  c[1] ],
+            3 => [ c[0],  c[1],  c[0],  c[2],  c[0],  c[1],  c[0],  c[2],
+                   c[0],  c[1],  c[0],  c[2],  c[0],  c[1],  c[0],  c[2] ],
+            4 => [ c[0],  c[1],  c[2],  c[3],  c[0],  c[1],  c[2],  c[3],
+                   c[0],  c[1],  c[2],  c[3],  c[0],  c[1],  c[2],  c[3] ],
+            8 => [ c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7],
+                   c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7] ],
+           16 => [ c[0],  c[1],  c[2],  c[3],  c[4],  c[5],  c[6],  c[7],
+                   c[8],  c[9], c[10], c[11], c[12], c[13], c[14], c[15] ],
+            _ => return None,
+        })
+    }
+    parse_inner(string).unwrap_or_else(|| {
+        fatal!("Invalid palette string '{string}'");
+    })
+}
+
+
+#[derive(Copy, Clone, PartialEq)]
+enum Mode {
+    Graphical,
+    Headless,
+    Dynamic,
+}
+
+impl Mode {
+    pub fn from_str(string: &str) -> Self {
+        match string {
+            "g" | "graphical" => Self::Graphical,
+            "h" | "headless" => Self::Headless,
+            "d" | "dynamic" => Self::Dynamic,
+            _ => fatal!("Invalid mode string '{string}'"),
+        }
+    }
+}
diff --git a/src/debug.rs b/src/debug.rs
deleted file mode 100644
index 7fd4ea5..0000000
--- a/src/debug.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use bedrock_core::*;
-
-use std::path::Path;
-use std::time::Instant;
-
-
-const NORMAL: &str = "\x1b[0m";
-const DIM:    &str = "\x1b[2m";
-const YELLOW: &str = "\x1b[33m";
-const BLUE:   &str = "\x1b[34m";
-
-
-pub struct DebugState {
-    pub enabled: bool,
-    last_cycle: usize,
-    last_mark: Instant,
-    symbols: DebugSymbols,
-}
-
-impl DebugState {
-    pub fn new<P: AsRef<Path>>(enabled: bool, symbols_path: Option<P>) -> Self {
-        Self {
-            enabled,
-            last_cycle: 0,
-            last_mark: Instant::now(),
-            symbols: DebugSymbols::from_path_opt(symbols_path),
-        }
-    }
-
-    pub fn debug_summary(&mut self, core: &BedrockCore) {
-        if self.enabled {
-            let prev_pc = core.mem.pc.wrapping_sub(1);
-            eprintln!("\n PC: 0x{:04x}   Cycles: {} (+{} in {:.2?})",
-                prev_pc, core.cycle,
-                core.cycle.saturating_sub(self.last_cycle),
-                self.last_mark.elapsed(),
-            );
-            eprint!("WST: ");
-            debug_stack(&core.wst, 0x10);
-            eprint!("RST: ");
-            debug_stack(&core.rst, 0x10);
-            // Print information about the closest symbol.
-            if let Some(symbol) = self.symbols.for_address(prev_pc) {
-                let name = &symbol.name;
-                let address = &symbol.address;
-                eprint!("SYM: {BLUE}@{name}{NORMAL} 0x{address:04x}");
-                if let Some(location) = &symbol.location {
-                    eprint!(" {DIM}{location}{NORMAL}");
-                }
-                eprintln!();
-            }
-        }
-        self.last_cycle = core.cycle;
-        self.last_mark = Instant::now();
-    }
-}
-
-
-fn debug_stack(stack: &Stack, len: usize) {
-    for i in 0..len {
-        if i == stack.sp as usize { eprint!("{YELLOW}"); }
-        eprint!("{:02x} ", stack.mem[i]);
-    }
-    eprintln!("{NORMAL}");
-}
-
-
-struct DebugSymbols {
-    symbols: Vec<DebugSymbol>
-}
-
-impl DebugSymbols {
-    /// Load debug symbols from a symbols file.
-    pub fn from_path_opt<P: AsRef<Path>>(path: Option<P>) -> Self {
-        let mut symbols = Vec::new();
-        if let Some(path) = path {
-            if let Ok(string) = std::fs::read_to_string(path) {
-                for line in string.lines() {
-                    if let Some(symbol) = DebugSymbol::from_line(line) {
-                        symbols.push(symbol);
-                    }
-                }
-            }
-        }
-        symbols.sort_by_key(|s| s.address);
-        Self { symbols }
-    }
-
-    pub fn for_address(&self, address: u16) -> Option<&DebugSymbol> {
-        if self.symbols.is_empty() { return None; }
-        let symbol = match self.symbols.binary_search_by_key(&address, |s| s.address) {
-            Ok(index) => self.symbols.get(index)?,
-            Err(index) => self.symbols.get(index.checked_sub(1)?)?,
-        };
-        Some(&symbol)
-    }
-}
-
-struct DebugSymbol {
-    address: u16,
-    name: String,
-    location: Option<String>,
-}
-
-impl DebugSymbol {
-    pub fn from_line(line: &str) -> Option<Self> {
-        if let Some((address, line)) = line.split_once(' ') {
-            let address = u16::from_str_radix(address, 16).ok()?;
-            if let Some((name, location)) = line.split_once(' ') {
-                let name = name.to_string();
-                let location = Some(location.to_string());
-                Some( DebugSymbol { address, name, location } )
-            } else {
-                let name = line.to_string();
-                Some( DebugSymbol { address, name, location: None } )
-            }
-        } else {
-            None
-        }
-    }
-}
diff --git a/src/devices.rs b/src/devices.rs
deleted file mode 100644
index 2221152..0000000
--- a/src/devices.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-mod system_device;
-mod memory_device;
-mod math_device;
-mod clock_device;
-mod input_device;
-mod screen_device;
-mod local_device;
-mod remote_device;
-mod file_device;
-
-pub use system_device::SystemDevice;
-pub use memory_device::MemoryDevice;
-pub use math_device::MathDevice;
-pub use clock_device::ClockDevice;
-pub use input_device::InputDevice;
-pub use screen_device::ScreenDevice;
-pub use local_device::LocalDevice;
-pub use remote_device::RemoteDevice;
-pub use file_device::FileDevice;
-
diff --git a/src/devices/clock_device.rs b/src/devices/clock_device.rs
index 7cc877e..b7c5414 100644
--- a/src/devices/clock_device.rs
+++ b/src/devices/clock_device.rs
@@ -1,114 +1,32 @@
-use bedrock_core::*;
+use crate::*;
 
 use chrono::prelude::*;
-use std::time::{Duration, Instant};
-
-
-macro_rules! fn_read_timer {
-    ($fn_name:ident($read:ident, $end:ident)) => {
-        pub fn $fn_name(&mut self) {
-            let uptime = self.uptime();
-            if self.$end > uptime {
-                self.$read = (self.$end.saturating_sub(uptime)) as u16;
-            } else {
-                if self.$end > 0 {
-                    self.$end = 0;
-                    self.wake = true;
-                }
-                self.$read = 0;
-            }
-        }
-    };
-}
-
-macro_rules! fn_set_timer {
-    ($fn_name:ident($write:ident, $end:ident)) => {
-        pub fn $fn_name(&mut self) {
-            let uptime = self.uptime();
-            if self.$write > 0 {
-                self.$end = uptime.saturating_add(self.$write as u32);
-            } else {
-                self.$end = 0;
-            }
-        }
-    };
-}
 
 
 pub struct ClockDevice {
-    pub wake: bool,
-
-    pub start: Instant,
+    pub epoch: Instant,
     pub uptime_read: u16,
 
-    pub t1_end: u32,
-    pub t2_end: u32,
-    pub t3_end: u32,
-    pub t4_end: u32,
+    // End time for each timer as ticks since epoch, zero if not set.
+    pub t1_end: u64,
+    pub t2_end: u64,
+    pub t3_end: u64,
+    pub t4_end: u64,
+    // Cached read value for each timer.
     pub t1_read: u16,
     pub t2_read: u16,
     pub t3_read: u16,
     pub t4_read: u16,
+    // Cached write value for each timer.
     pub t1_write: u16,
     pub t2_write: u16,
     pub t3_write: u16,
     pub t4_write: u16,
-}
-
-impl ClockDevice {
-    pub fn new() -> Self {
-        Self {
-            start: Instant::now(),
-            uptime_read: 0,
-            wake: false,
-
-            t1_end: 0,
-            t2_end: 0,
-            t3_end: 0,
-            t4_end: 0,
-            t1_read: 0,
-            t2_read: 0,
-            t3_read: 0,
-            t4_read: 0,
-            t1_write: 0,
-            t2_write: 0,
-            t3_write: 0,
-            t4_write: 0,
-        }
-    }
 
-    pub fn uptime(&self) -> u32 {
-        (self.start.elapsed().as_millis() / 4) as u32
-    }
-
-    pub fn read_uptime(&mut self) {
-        self.uptime_read = self.uptime() as u16;
-    }
-
-    fn_read_timer!{ read_t1(t1_read, t1_end) }
-    fn_read_timer!{ read_t2(t2_read, t2_end) }
-    fn_read_timer!{ read_t3(t3_read, t3_end) }
-    fn_read_timer!{ read_t4(t4_read, t4_end) }
-
-    fn_set_timer!{ set_t1(t1_write, t1_end) }
-    fn_set_timer!{ set_t2(t2_write, t2_end) }
-    fn_set_timer!{ set_t3(t3_write, t3_end) }
-    fn_set_timer!{ set_t4(t4_write, t4_end) }
-
-
-    pub fn time_remaining(&mut self) -> Option<Duration> {
-        use std::cmp::max;
-        let uptime = self.uptime();
-
-        let end = max(self.t1_end, max(self.t2_end, max(self.t3_end, self.t4_end)));
-        let remaining = end.saturating_sub(uptime);
-        match remaining > 0 {
-            true => Some(Duration::from_millis(remaining as u64) * 4),
-            false => None,
-        }
-    }
+    pub wake: bool,
 }
 
+
 impl Device for ClockDevice {
     fn read(&mut self, port: u8) -> u8 {
         match port {
@@ -118,16 +36,17 @@ impl Device for ClockDevice {
             0x3 => Local::now().hour() as u8,
             0x4 => Local::now().minute() as u8,
             0x5 => Local::now().second() as u8,
-            0x6 => { self.read_uptime(); read_h!(self.uptime_read) },
-            0x7 => read_l!(self.uptime_read),
+            0x6 => { self.uptime_read = self.uptime() as u16;
+                     read_h!(self.uptime_read) },
+            0x7 =>   read_l!(self.uptime_read),
             0x8 => { self.read_t1(); read_h!(self.t1_read) },
-            0x9 => read_l!(self.t1_read),
+            0x9 =>                   read_l!(self.t1_read),
             0xa => { self.read_t2(); read_h!(self.t2_read) },
-            0xb => read_l!(self.t2_read),
+            0xb =>                   read_l!(self.t2_read),
             0xc => { self.read_t3(); read_h!(self.t3_read) },
-            0xd => read_l!(self.t3_read),
+            0xd =>                   read_l!(self.t3_read),
             0xe => { self.read_t4(); read_h!(self.t4_read) },
-            0xf => read_l!(self.t4_read),
+            0xf =>                   read_l!(self.t4_read),
             _ => unreachable!(),
         }
     }
@@ -142,13 +61,13 @@ impl Device for ClockDevice {
             0x5 => (),
             0x6 => (),
             0x7 => (),
-            0x8 => write_h!(self.t1_write, value),
+            0x8 =>   write_h!(self.t1_write, value),
             0x9 => { write_l!(self.t1_write, value); self.set_t1() },
-            0xa => write_h!(self.t2_write, value),
+            0xa =>   write_h!(self.t2_write, value),
             0xb => { write_l!(self.t2_write, value); self.set_t2() },
-            0xc => write_h!(self.t3_write, value),
+            0xc =>   write_h!(self.t3_write, value),
             0xd => { write_l!(self.t3_write, value); self.set_t3() },
-            0xe => write_h!(self.t4_write, value),
+            0xe =>   write_h!(self.t4_write, value),
             0xf => { write_l!(self.t4_write, value); self.set_t4() },
             _ => unreachable!(),
         };
@@ -157,18 +76,100 @@ impl Device for ClockDevice {
 
     fn wake(&mut self) -> bool {
         let uptime = self.uptime();
-        macro_rules! check_timer {
-            ($end:ident) => {
-                if self.$end > 0 && self.$end <= uptime {
+
+        if self.t1_end > 0 && self.t1_end <= uptime {
+            self.t1_end = 0; self.wake = true; }
+        if self.t2_end > 0 && self.t2_end <= uptime {
+            self.t2_end = 0; self.wake = true; }
+        if self.t3_end > 0 && self.t3_end <= uptime {
+            self.t3_end = 0; self.wake = true; }
+        if self.t4_end > 0 && self.t4_end <= uptime {
+            self.t4_end = 0; self.wake = true; }
+
+        return std::mem::take(&mut self.wake);
+    }
+}
+
+
+impl ClockDevice {
+    pub fn new() -> Self {
+        Self {
+            epoch: Instant::now(),
+            uptime_read: 0,
+            wake: false,
+
+            t1_end: 0,
+            t2_end: 0,
+            t3_end: 0,
+            t4_end: 0,
+            t1_read: 0,
+            t2_read: 0,
+            t3_read: 0,
+            t4_read: 0,
+            t1_write: 0,
+            t2_write: 0,
+            t3_write: 0,
+            t4_write: 0,
+        }
+    }
+
+    pub fn uptime(&self) -> u64 {
+        (self.epoch.elapsed().as_nanos() * 256 / 1_000_000_000) as u64
+    }
+
+    /// Duration remaining until the next timer expires, if any timer is set.
+    pub fn duration_remaining(&mut self) -> Option<Duration> {
+        let mut end = u64::MAX;
+        if self.t1_end > 0 { end = std::cmp::min(end, self.t1_end); }
+        if self.t2_end > 0 { end = std::cmp::min(end, self.t2_end); }
+        if self.t3_end > 0 { end = std::cmp::min(end, self.t3_end); }
+        if self.t4_end > 0 { end = std::cmp::min(end, self.t4_end); }
+
+        if end != u64::MAX {
+            let remaining = end.saturating_sub(self.uptime());
+            Some(Duration::from_nanos(remaining * 1_000_000_000 / 256))
+        } else {
+            None
+        }
+    }
+}
+
+macro_rules! fn_read_timer {
+    ($fn_name:ident($read:ident, $end:ident)) => {
+        pub fn $fn_name(&mut self) {
+            let uptime = self.uptime();
+            if self.$end > uptime {
+                self.$read = (self.$end.saturating_sub(uptime)) as u16;
+            } else {
+                if self.$end > 0 {
                     self.$end = 0;
                     self.wake = true;
                 }
+                self.$read = 0;
+            }
+        }
+    };
+}
+
+macro_rules! fn_set_timer {
+    ($fn_name:ident($write:ident, $end:ident)) => {
+        pub fn $fn_name(&mut self) {
+            match self.$write > 0 {
+                true => self.$end = self.uptime().saturating_add(self.$write as u64),
+                false => self.$end = 0,
             };
         }
-        check_timer!(t1_end);
-        check_timer!(t2_end);
-        check_timer!(t3_end);
-        check_timer!(t4_end);
-        return std::mem::take(&mut self.wake);
-    }
+    };
+}
+
+impl ClockDevice {
+    fn_read_timer!{ read_t1(t1_read, t1_end) }
+    fn_read_timer!{ read_t2(t2_read, t2_end) }
+    fn_read_timer!{ read_t3(t3_read, t3_end) }
+    fn_read_timer!{ read_t4(t4_read, t4_end) }
+
+    fn_set_timer!{ set_t1(t1_write, t1_end) }
+    fn_set_timer!{ set_t2(t2_write, t2_end) }
+    fn_set_timer!{ set_t3(t3_write, t3_end) }
+    fn_set_timer!{ set_t4(t4_write, t4_end) }
 }
diff --git a/src/devices/file_device.rs b/src/devices/file_device.rs
index 61966b1..f8d8fa0 100644
--- a/src/devices/file_device.rs
+++ b/src/devices/file_device.rs
@@ -1,20 +1,4 @@
-mod bedrock_file_path;
-mod bedrock_path_buffer;
-mod buffered_file;
-mod directory_listing;
-mod entry;
-mod operations;
-
-use buffered_file::BufferedFile;
-use bedrock_file_path::BedrockFilePath;
-use bedrock_path_buffer::BedrockPathBuffer;
-use directory_listing::DirectoryListing;
-use entry::{Entry, EntryType};
-use operations::{create_file, move_entry, delete_entry};
-
-use bedrock_core::*;
-
-use std::path::{Component, Path, PathBuf};
+use crate::*;
 
 
 pub struct FileDevice {
@@ -39,6 +23,59 @@ pub struct FileDevice {
     pub enable_delete: bool,
 }
 
+
+impl Device for FileDevice {
+    fn read(&mut self, port: u8) -> u8 {
+        match port {
+            0x0 => read_b!(self.entry.is_some()),
+            0x1 => read_b!(self.success),
+            0x2 => self.path_buffer.read(),
+            0x3 => read_b!(self.entry_type()),
+            0x4 => self.read_byte(),
+            0x5 => self.read_byte(),
+            0x6 => self.read_child_path(),
+            0x7 => read_b!(self.child_type()),
+            0x8 => read_hh!(self.pointer()),
+            0x9 => read_hl!(self.pointer()),
+            0xa => read_lh!(self.pointer()),
+            0xb => read_ll!(self.pointer()),
+            0xc => read_hh!(self.length()),
+            0xd => read_hl!(self.length()),
+            0xe => read_lh!(self.length()),
+            0xf => read_ll!(self.length()),
+            _ => unreachable!(),
+        }
+    }
+
+    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
+        match port {
+            0x0 => self.write_to_entry_port(value),
+            0x1 => self.write_to_action_port(value),
+            0x2 => self.path_buffer.set_pointer(value),
+            0x3 => self.ascend_to_parent(),
+            0x4 => self.write_byte(value),
+            0x5 => self.write_byte(value),
+            0x6 => self.set_child_path(value),
+            0x7 => self.descend_to_child(),
+            0x8 => write_hh!(self.pointer_write, value),
+            0x9 => write_hl!(self.pointer_write, value),
+            0xa => write_lh!(self.pointer_write, value),
+            0xb => {write_ll!(self.pointer_write, value); self.commit_pointer()},
+            0xc => write_hh!(self.length_write, value),
+            0xd => write_hl!(self.length_write, value),
+            0xe => write_lh!(self.length_write, value),
+            0xf => {write_ll!(self.length_write, value); self.commit_length()},
+            _ => unreachable!(),
+        };
+        return None;
+    }
+
+    fn wake(&mut self) -> bool {
+        false
+    }
+}
+
+
 impl FileDevice {
     pub fn new() -> Self {
         #[cfg(target_family = "unix")]
@@ -279,59 +316,57 @@ impl FileDevice {
     }
 }
 
+
 impl Drop for FileDevice {
     fn drop(&mut self) {
         self.flush();
     }
 }
 
-impl Device for FileDevice {
-    fn read(&mut self, port: u8) -> u8 {
-        match port {
-            0x0 => read_b!(self.entry.is_some()),
-            0x1 => read_b!(self.success),
-            0x2 => self.path_buffer.read(),
-            0x3 => read_b!(self.entry_type()),
-            0x4 => self.read_byte(),
-            0x5 => self.read_byte(),
-            0x6 => self.read_child_path(),
-            0x7 => read_b!(self.child_type()),
-            0x8 => read_hh!(self.pointer()),
-            0x9 => read_hl!(self.pointer()),
-            0xa => read_lh!(self.pointer()),
-            0xb => read_ll!(self.pointer()),
-            0xc => read_hh!(self.length()),
-            0xd => read_hl!(self.length()),
-            0xe => read_lh!(self.length()),
-            0xf => read_ll!(self.length()),
-            _ => unreachable!(),
+
+/// Create a new file if it doesn't already exist, returning true if successful.
+pub fn create_file(destination: &Path) -> bool {
+    if entry_exists(destination) {
+        false
+    } else {
+        if let Some(parent_path) = destination.parent() {
+            let _ = std::fs::create_dir_all(parent_path);
         }
+        std::fs::OpenOptions::new().write(true).create_new(true)
+            .open(destination).is_ok()
     }
+}
 
-    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
-        match port {
-            0x0 => self.write_to_entry_port(value),
-            0x1 => self.write_to_action_port(value),
-            0x2 => self.path_buffer.set_pointer(value),
-            0x3 => self.ascend_to_parent(),
-            0x4 => self.write_byte(value),
-            0x5 => self.write_byte(value),
-            0x6 => self.set_child_path(value),
-            0x7 => self.descend_to_child(),
-            0x8 => write_hh!(self.pointer_write, value),
-            0x9 => write_hl!(self.pointer_write, value),
-            0xa => write_lh!(self.pointer_write, value),
-            0xb => {write_ll!(self.pointer_write, value); self.commit_pointer()},
-            0xc => write_hh!(self.length_write, value),
-            0xd => write_hl!(self.length_write, value),
-            0xe => write_lh!(self.length_write, value),
-            0xf => {write_ll!(self.length_write, value); self.commit_length()},
-            _ => unreachable!(),
-        };
-        return None;
+/// Move an entry from one location to another, returning true if successful.
+pub fn move_entry(source: &Path, destination: &Path) -> bool {
+    if !entry_exists(source) || entry_exists(destination) {
+        return false;
     }
+    std::fs::rename(source, destination).is_ok()
+}
 
-    fn wake(&mut self) -> bool {
-        false
+/// Delete an entry, returning true if successful.
+pub fn delete_entry(source: &Path) -> bool {
+    use std::fs::{remove_file, remove_dir_all};
+    use std::io::ErrorKind;
+
+    match remove_file(source) {
+        Ok(_) => true,
+        Err(error) => match error.kind() {
+            ErrorKind::NotFound => true,
+            ErrorKind::IsADirectory => match remove_dir_all(source) {
+                Ok(_) => true,
+                Err(error) => match error.kind() {
+                    ErrorKind::NotFound => true,
+                    _ => false,
+                }
+            }
+            _ => false,
+        }
     }
 }
+
+/// Returns true if an entry already exists at the given path.
+fn entry_exists(source: &Path) -> bool {
+    std::fs::metadata(source).is_ok()
+}
diff --git a/src/devices/file_device/bedrock_file_path.rs b/src/devices/file_device/bedrock_file_path.rs
deleted file mode 100644
index fdd8f79..0000000
--- a/src/devices/file_device/bedrock_file_path.rs
+++ /dev/null
@@ -1,287 +0,0 @@
-use super::*;
-
-use std::cmp::Ordering;
-use std::ffi::OsString;
-
-
-#[derive(Clone)]
-pub struct BedrockFilePath {
-    /// Sandbox directory
-    base: PathBuf,
-    /// Path relative to sandbox directory
-    relative: PathBuf,
-    bytes: Vec<u8>,
-    entry_type: Option<EntryType>,
-}
-
-impl BedrockFilePath {
-    pub fn from_buffer(buffer: [u8; 256], base: &Path) -> Option<Self> {
-        let base = base.to_path_buf();
-        let relative = buffer_to_path(buffer)?;
-        let bytes = path_to_bytes(&relative)?;
-        let entry_type = get_entry_type(base.join(&relative));
-        assert_path_is_safe(&relative, &base)?;
-        Some(Self { base, relative, bytes, entry_type })
-    }
-
-    /// Construct an instance from an absolute path and a prefix of that path.
-    pub fn from_path(path: &Path, base: &Path) -> Option<Self> {
-        let base = base.to_path_buf();
-        let relative = path.strip_prefix(&base).ok()?.to_path_buf();
-        let bytes = path_to_bytes(&relative)?;
-        let entry_type = get_entry_type(base.join(&relative));
-        assert_path_is_safe(&relative, &base)?;
-        Some( Self { base, relative, bytes, entry_type } )
-    }
-
-    /// Get the base path used by this path.
-    pub fn base(&self) -> &Path {
-        &self.base
-    }
-
-    /// Get this path as a Bedrock-style path, which can be passed to
-    /// a Bedrock program.
-    pub fn as_bytes(&self) -> &[u8] {
-        &self.bytes
-    }
-
-    /// Get this path as a byte buffer, from which a CircularPathBuffer
-    /// can be populated.
-    pub fn as_buffer(&self) -> [u8; 256] {
-        let mut buffer: [u8; 256] = [0; 256];
-        buffer[..self.bytes.len()].copy_from_slice(&self.bytes);
-        return buffer;
-    }
-
-    /// Get this path as an absolute operating-system path, which can
-    /// be used to open a file or directory.
-    pub fn as_path(&self) -> PathBuf {
-        self.base.join(&self.relative)
-    }
-
-    /// Get the entry type of this path.
-    pub fn entry_type(&self) -> Option<EntryType> {
-        self.entry_type
-    }
-
-    /// Get a path which represents the parent of this path.
-    pub fn parent(&self) -> Option<Self> {
-        #[cfg(target_family = "unix")] {
-            Self::from_path(self.as_path().parent()?, &self.base)
-        }
-        #[cfg(target_family = "windows")] {
-            if self.base.components().count() != 0 {
-                // Sandboxed path, cannot ascend to a virtual root directory.
-                Self::from_path(self.as_path().parent()?, &self.base)
-            } else {
-                // Unsandboxed path, we can ascend to a virtual root directory.
-                match self.as_path().parent() {
-                    // Ascend to concrete parent directory.
-                    Some(parent) => Self::from_path(parent, &self.base),
-                    // Ascend into a virtual root directory.
-                    None => {
-                        if self.relative.components().count() != 0 {
-                            // Ascend from concrete path to virtual root.
-                            let blank = PathBuf::from("");
-                            BedrockFilePath::from_path(&blank, &blank)
-                        } else {
-                            // Cannot ascend above the virtual root.
-                            None
-                        }
-                    },
-                }
-            }
-        }
-    }
-
-    /// Returns true if the file would be hidden by the default file browser.
-    pub fn is_hidden(&self) -> bool {
-        #[cfg(target_family = "unix")] {
-            if let Some(stem) = self.relative.file_stem() {
-                if let Some(string) = stem.to_str() {
-                    return string.starts_with('.');
-                }
-            }
-        }
-        #[cfg(target_family = "windows")] {
-            use std::os::windows::fs::MetadataExt;
-            // See https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
-            // const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002;
-            // const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: u32 = 0x00002000;
-            if let Ok(metadata) = std::fs::metadata(self.as_path()) {
-                return metadata.file_attributes() & 0x2002 != 0;
-            }
-        }
-        return false;
-    }
-}
-
-
-/// Converts the contents of a CircularPathBuffer to a relative path.
-fn buffer_to_path(bytes: [u8; 256]) -> Option<PathBuf> {
-    // The buffer must be non-empty and slash-prefixed.
-    if bytes[0] != ('/' as u8) {
-        return None;
-    }
-
-    // Find the index of the first null byte.
-    let mut null_i = None;
-    for (i, b) in bytes.iter().enumerate() {
-        if *b == 0x00 {
-            null_i = Some(i);
-            break;
-        }
-    }
-    // Take a slice, excluding the leading slash, up to the trailing null.
-    let slice = &bytes[1..null_i?];
-
-    #[cfg(target_family = "unix")] {
-        use std::os::unix::ffi::OsStringExt;
-        let vec = Vec::from(slice);
-        return Some(OsString::from_vec(vec).into())
-    }
-    #[cfg(target_family = "windows")] {
-        use std::os::windows::ffi::OsStringExt;
-        let mut string = String::from_utf8_lossy(slice).to_string();
-        // Convert drive-current-directory paths to drive-root paths. This is
-        // needed because the paths C: and C:/ point to separate directories,
-        // but trailing forward-slashes are optional in Bedrock.
-        if string.ends_with(':') {
-            string.push('/');
-        }
-        let utf16: Vec<u16> = string.replace(r"/", r"\").encode_utf16().collect();
-        return Some(OsString::from_wide(&utf16).into())
-    }
-}
-
-/// Convert an operating system path to a Bedrock-style byte path.
-///
-/// A byte path contains at most 255 bytes, and is not null-terminated.
-fn path_to_bytes(path: &Path) -> Option<Vec<u8>> {
-    #[cfg(target_family = "unix")]
-    let string = path.as_os_str().to_str()?.to_string();
-    #[cfg(target_family = "windows")]
-    let string = path.as_os_str().to_str()?.replace(r"\", r"/");
-
-    // Remove any trailing forward-slash and add a leading forward-slash.
-    let mut prefixed_string = String::from("/");
-    prefixed_string.push_str(string.trim_end_matches('/'));
-    let slice = prefixed_string.as_bytes();
-
-    // Error if bytes does not fit into a CircularPathBuffer.
-    if slice.len() > 255 { return None; }
-
-    Some(Vec::from(slice))
-}
-
-/// Returns true if a relative path can be safely attached to a base without
-/// breaking out of the sandbox.
-fn assert_path_is_safe(relative: &Path, _base: &Path) -> Option<()> {
-    #[cfg(target_family = "unix")] {
-        // Error if path contains special components.
-        for component in relative.components() {
-            match component {
-                Component::Normal(_) => continue,
-                _ => return None,
-            }
-        }
-    }
-    #[cfg(target_family = "windows")] {
-        // If the base path is empty, the relative path needs to be able to
-        // contain the prefix and root element. If the base path is not
-        // empty, the relative path must not contain these elements else
-        // they will override the base path when joined.
-        if _base.components().count() != 0 {
-            for component in relative.components() {
-                match component {
-                    Component::Normal(_) => continue,
-                    _ => return None,
-                }
-            }
-        }
-    }
-    return Some(());
-}
-
-fn get_entry_type(absolute: PathBuf) -> Option<EntryType> {
-    #[cfg(target_family = "windows")] {
-        // If path is empty, this is a virtual root directory.
-        if absolute.components().count() == 0 {
-            return Some(EntryType::Directory)
-        }
-    }
-    let metadata = std::fs::metadata(absolute).ok()?;
-    if metadata.is_file() {
-        Some(EntryType::File)
-    } else if metadata.is_dir() {
-        Some(EntryType::Directory)
-    } else {
-        None
-    }
-}
-
-impl std::fmt::Debug for BedrockFilePath {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
-        self.as_path().fmt(f)
-    }
-}
-
-// ---------------------------------------------------------------------------
-
-impl PartialEq for BedrockFilePath {
-    fn eq(&self, other: &Self) -> bool {
-        self.bytes == other.bytes && self.entry_type == other.entry_type
-    }
-}
-
-impl Eq for BedrockFilePath {}
-
-impl PartialOrd for BedrockFilePath {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
-impl Ord for BedrockFilePath {
-    fn cmp(&self, other: &Self) -> Ordering {
-        match self.entry_type.cmp(&other.entry_type) {
-            Ordering::Equal => compare_ascii_slices(&self.bytes, &other.bytes),
-            ordering => ordering,
-        }
-    }
-}
-
-/// Compare two ASCII byte-slices in case-agnostic alphabetic order.
-fn compare_ascii_slices(left: &[u8], right: &[u8]) -> Ordering {
-    let l = std::cmp::min(left.len(), right.len());
-    let lhs = &left[..l];
-    let rhs = &right[..l];
-
-    for i in 0..l {
-        let a = remap_ascii(lhs[i]);
-        let b = remap_ascii(rhs[i]);
-        match a.cmp(&b) {
-            Ordering::Equal => (),
-            non_eq => return non_eq,
-        }
-    }
-
-    left.len().cmp(&right.len())
-}
-
-/// Remap ASCII values so that they sort in case-agnostic alphabetic order:
-///
-/// ```text
-///    !"#$%&'()*+,-./0123456789:;<=>?
-///   @`AaBbCcDdEeFfGgHhIiJjKkLlMmNnOo
-///   PpQqRrSsTtUuVvWwXxYyZz[{\|]}^~_
-/// ```
-fn remap_ascii(c: u8) -> u8 {
-    if 0x40 <= c && c <= 0x5F {
-        (c - 0x40) * 2 + 0x40
-    } else if 0x60 <= c && c <= 0x7F {
-        (c - 0x60) * 2 + 0x41
-    } else {
-        c
-    }
-}
diff --git a/src/devices/file_device/bedrock_path_buffer.rs b/src/devices/file_device/bedrock_path_buffer.rs
deleted file mode 100644
index d6a0861..0000000
--- a/src/devices/file_device/bedrock_path_buffer.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-pub struct BedrockPathBuffer {
-    buffer: [u8; 256],
-    pointer: u8,
-}
-
-impl BedrockPathBuffer {
-    pub fn new() -> Self {
-        Self { buffer: [0; 256] , pointer: 0 }
-    }
-
-    /// Clear the buffer, returning the previous buffer contents.
-    pub fn clear(&mut self) -> [u8; 256] {
-        self.pointer = 0;
-        std::mem::replace(&mut self.buffer, [0; 256])
-    }
-
-    /// Reset the pointer and hot-swap the byte buffer.
-    pub fn populate(&mut self, buffer: [u8; 256]) {
-        self.pointer = 0;
-        self.buffer = buffer;
-    }
-
-    /// Move internal pointer to the start of the path or file name.
-    ///
-    /// If value is non-zero, the pointer will be moved to the byte
-    /// directly following the final forward-slash.
-    pub fn set_pointer(&mut self, value: u8) {
-        self.pointer = 0;
-        // Set the pointer to the start of the filename if value is truthy.
-        if value != 0x00 {
-            for (i, c) in self.buffer.iter().enumerate() {
-                match c {
-                    b'/' => self.pointer = (i as u8).saturating_add(1),
-                    0x00 => break,
-                    _ => continue,
-                }
-            }
-        }
-    }
-
-    /// Read a single byte from the buffer.
-    pub fn read(&mut self) -> u8 {
-        let pointer = self.pointer as usize;
-        self.pointer = self.pointer.wrapping_add(1);
-        self.buffer[pointer]
-    }
-
-    /// Write a single byte to the buffer.
-    ///
-    /// If a null-byte is written, the buffer will be cleared and returned.
-    pub fn write(&mut self, byte: u8) -> Option<[u8; 256]> {
-        if byte == 0x00 {
-            Some(self.clear())
-        } else {
-            self.buffer[self.pointer as usize] = byte;
-            self.pointer = self.pointer.saturating_add(1);
-            None
-        }
-    }
-}
diff --git a/src/devices/file_device/buffered_file.rs b/src/devices/file_device/buffered_file.rs
deleted file mode 100644
index 29e1fa3..0000000
--- a/src/devices/file_device/buffered_file.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-use std::fs::File;
-use std::io::{BufReader, BufWriter};
-use std::io::{Read, Write};
-use std::io::{ErrorKind, Seek, SeekFrom};
-
-
-pub struct BufferedFile {
-    file: AccessMode,
-}
-
-impl BufferedFile {
-    pub fn new(file: File) -> Self {
-        Self {
-            file: AccessMode::Read(BufReader::new(file)),
-        }
-    }
-
-    pub fn close(&mut self) {
-        self.file = AccessMode::None;
-    }
-
-    pub fn read(&mut self) -> u8 {
-        let mut buffer = [0u8; 1];
-
-        let read_result = match &mut self.file {
-            AccessMode::Read(reader) => reader.read_exact(&mut buffer),
-            AccessMode::Write(writer) => {
-                let address = writer.stream_position().unwrap();
-                let file = std::mem::take(&mut self.file).unwrap();
-                let mut reader = BufReader::new(file);
-                reader.seek(SeekFrom::Start(address)).unwrap();
-                let read_result = reader.read_exact(&mut buffer);
-                self.file = AccessMode::Read(reader);
-                read_result
-            }
-            AccessMode::None => unreachable!(),
-        };
-
-        match read_result {
-            Ok(_) => buffer[0],
-            Err(error) => match error.kind() {
-                ErrorKind::UnexpectedEof => 0,
-                _ => { log::error!("BufferedFile::read: {error:?}"); 0 },
-            }
-        }
-    }
-
-    pub fn write(&mut self, byte: u8) {
-        let mut buffer = [byte; 1];
-
-        let write_result = match &mut self.file {
-            AccessMode::Write(writer) => writer.write_all(&mut buffer),
-            AccessMode::Read(reader) => {
-                let address = reader.stream_position().unwrap();
-                let file = std::mem::take(&mut self.file).unwrap();
-                let mut writer = BufWriter::new(file);
-                writer.seek(SeekFrom::Start(address)).unwrap();
-                let write_result = writer.write_all(&mut buffer);
-                self.file = AccessMode::Write(writer);
-                write_result
-            }
-            AccessMode::None => unreachable!(),
-        };
-
-        write_result.unwrap();
-    }
-
-    pub fn pointer(&mut self) -> u32 {
-        let position = match &mut self.file {
-            AccessMode::Read(reader) => reader.stream_position(),
-            AccessMode::Write(writer) => writer.stream_position(),
-            AccessMode::None => unreachable!(),
-        };
-        u32::try_from(position.unwrap()).unwrap_or(u32::MAX)
-    }
-
-    pub fn set_pointer(&mut self, pointer: u32) {
-        let position = SeekFrom::Start(pointer as u64);
-        match &mut self.file {
-            AccessMode::Read(reader) => reader.seek(position).unwrap(),
-            AccessMode::Write(writer) => writer.seek(position).unwrap(),
-            AccessMode::None => unreachable!(),
-        };
-    }
-
-    pub fn length(&mut self) -> u32 {
-        let length = match &mut self.file {
-            AccessMode::Read(reader) => reader.stream_len(),
-            AccessMode::Write(writer) => writer.stream_len(),
-            AccessMode::None => unreachable!(),
-        };
-        u32::try_from(length.unwrap()).unwrap_or(u32::MAX)
-    }
-
-    pub fn set_length(&mut self, length: u32) {
-        match &mut self.file {
-            AccessMode::Read(_) => {
-                let file = std::mem::take(&mut self.file).unwrap();
-                file.set_len(length as u64).unwrap();
-                self.file = AccessMode::Read(BufReader::new(file));
-            }
-            AccessMode::Write(_) => {
-                let file = std::mem::take(&mut self.file).unwrap();
-                file.set_len(length as u64).unwrap();
-                self.file = AccessMode::Read(BufReader::new(file));
-            }
-            AccessMode::None => unreachable!(),
-        };
-    }
-
-    pub fn flush(&mut self) {
-        if let AccessMode::Write(writer) = &mut self.file {
-            let _ = writer.flush();
-        }
-    }
-}
-
-impl Drop for BufferedFile {
-    fn drop(&mut self) {
-        self.flush()
-    }
-}
-
-enum AccessMode {
-    Read(BufReader<File>),
-    Write(BufWriter<File>),
-    None,
-}
-
-impl AccessMode {
-    pub fn unwrap(self) -> File {
-        match self {
-            Self::Read(reader) => reader.into_inner(),
-            Self::Write(writer) => writer.into_inner().unwrap(),
-            Self::None => unreachable!(),
-        }
-    }
-}
-
-impl Default for AccessMode {
-    fn default() -> Self {
-        Self::None
-    }
-}
diff --git a/src/devices/file_device/directory_listing.rs b/src/devices/file_device/directory_listing.rs
deleted file mode 100644
index 465efc7..0000000
--- a/src/devices/file_device/directory_listing.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-use super::*;
-
-
-pub struct DirectoryListing {
-    children: Vec<BedrockFilePath>,
-    length: u32,
-    selected: Option<u32>,
-    child_path_buffer: BedrockPathBuffer,
-}
-
-
-impl DirectoryListing {
-    pub fn from_path(path: &BedrockFilePath) -> Option<Self> {
-        macro_rules! unres {
-            ($result:expr) => { match $result { Ok(v) => v, Err(_) => continue} };
-        }
-        macro_rules! unopt {
-            ($option:expr) => { match $option { Some(v) => v, None => continue} };
-        }
-
-        #[cfg(target_family = "windows")] {
-            if path.as_path().components().count() == 0 {
-                return Some(Self::construct_virtual_root())
-            }
-        }
-
-        let mut children = Vec::new();
-        if let Ok(dir_listing) = std::fs::read_dir(path.as_path()) {
-            for (i, entry_result) in dir_listing.enumerate() {
-                // Firebreak to prevent emulator from consuming an absurd amount
-                // of memory when opening too large of a directory.
-                if i == (u16::MAX as usize) {
-                    break;
-                }
-
-                let entry = unres!(entry_result);
-                let entry_path = unopt!(BedrockFilePath::from_path(&entry.path(), path.base()));
-                if entry_path.is_hidden() {
-                    continue;
-                }
-
-                children.push(entry_path);
-            }
-        }
-
-        children.sort();
-        let length = u32::try_from(children.len()).ok()?;
-        let selected = None;
-        let child_path_buffer = BedrockPathBuffer::new();
-        Some( Self { children, length, selected, child_path_buffer } )
-    }
-
-    /// Generate entries for a virtual root directory.
-    #[cfg(target_family = "windows")]
-    fn construct_virtual_root() -> Self {
-        let mut children = Vec::new();
-        let base = PathBuf::from("");
-        let drive_bits = unsafe {
-            windows::Win32::Storage::FileSystem::GetLogicalDrives()
-        };
-        for i in 0..26 {
-            if drive_bits & (0x1 << i) != 0 {
-                let letter: char = (b'A' + i).into();
-                let path = PathBuf::from(format!("{letter}:/"));
-                if let Some(drive) = BedrockFilePath::from_path(&path, &base) {
-                    children.push(drive);
-                }
-            }
-        }
-
-        let length = children.len() as u32;
-        let selected = None;
-        let child_path_buffer = BedrockPathBuffer::new();
-        Self { children, length, selected, child_path_buffer }
-    }
-
-    /// Attempts to return a directory child by index.
-    pub fn get(&self, index: u32) -> Option<&BedrockFilePath> {
-        self.children.get(index as usize)
-    }
-
-    pub fn length(&self) -> u32 {
-        self.length
-    }
-
-    /// Returns the index of the selected child, or zero if no child is selected.
-    pub fn selected(&self) -> u32 {
-        self.selected.unwrap_or(0)
-    }
-
-    /// Attempts to select a child by index.
-    pub fn set_selected(&mut self, index: u32) {
-        if let Some(child) = self.get(index) {
-            let buffer = child.as_buffer();
-            self.child_path_buffer.populate(buffer);
-            self.selected = Some(index);
-        } else {
-            self.child_path_buffer.clear();
-            self.selected = None;
-        }
-    }
-
-    pub fn deselect_child(&mut self) {
-        self.child_path_buffer.clear();
-        self.selected = None;
-    }
-
-    pub fn child_path_buffer(&mut self) -> &mut BedrockPathBuffer {
-        &mut self.child_path_buffer
-    }
-
-    pub fn child_type(&self) -> Option<EntryType> {
-        self.selected.and_then(|s| self.get(s).and_then(|i| i.entry_type()))
-    }
-
-    pub fn child_path(&self) -> Option<BedrockFilePath> {
-        self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.clone())))
-    }
-}
-
diff --git a/src/devices/file_device/entry.rs b/src/devices/file_device/entry.rs
deleted file mode 100644
index d604bb7..0000000
--- a/src/devices/file_device/entry.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-use super::*;
-
-use std::cmp::Ordering;
-
-pub enum Entry {
-    File(BufferedFile),
-    Directory(DirectoryListing),
-}
-
-#[derive(Copy, Clone, PartialEq, Eq)]
-pub enum EntryType {
-    File,
-    Directory,
-}
-
-impl PartialOrd for EntryType {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        match (self, other) {
-            (EntryType::Directory, EntryType::Directory) => Some(Ordering::Equal  ),
-            (EntryType::Directory, EntryType::File     ) => Some(Ordering::Less   ),
-            (EntryType::File,      EntryType::Directory) => Some(Ordering::Greater),
-            (EntryType::File,      EntryType::File     ) => Some(Ordering::Equal  ),
-        }
-    }
-}
-
-impl Ord for EntryType {
-    fn cmp(&self, other: &Self) -> Ordering {
-        match (self, other) {
-            (EntryType::Directory, EntryType::Directory) => Ordering::Equal  ,
-            (EntryType::Directory, EntryType::File     ) => Ordering::Less   ,
-            (EntryType::File,      EntryType::Directory) => Ordering::Greater,
-            (EntryType::File,      EntryType::File     ) => Ordering::Equal  ,
-        }
-    }
-}
diff --git a/src/devices/file_device/operations.rs b/src/devices/file_device/operations.rs
deleted file mode 100644
index 3a3f81b..0000000
--- a/src/devices/file_device/operations.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use std::io::ErrorKind;
-use std::path::Path;
-
-
-/// Create a new file if it doesn't already exist, returning true if successful.
-pub fn create_file(destination: &Path) -> bool {
-    if entry_exists(destination) {
-        false
-    } else {
-        if let Some(parent_path) = destination.parent() {
-            let _ = std::fs::create_dir_all(parent_path);
-        }
-        std::fs::OpenOptions::new().write(true).create_new(true)
-            .open(destination).is_ok()
-    }
-}
-
-/// Move an entry from one location to another, returning true if successful.
-pub fn move_entry(source: &Path, destination: &Path) -> bool {
-    if !entry_exists(source) || entry_exists(destination) {
-        return false;
-    }
-    std::fs::rename(source, destination).is_ok()
-}
-
-/// Delete an entry, returning true if successful.
-pub fn delete_entry(source: &Path) -> bool {
-    match std::fs::remove_file(source) {
-        Ok(_) => true,
-        Err(e) => match e.kind() {
-            ErrorKind::NotFound => true,
-            ErrorKind::IsADirectory => match std::fs::remove_dir_all(source) {
-                Ok(_) => true,
-                Err(e) => match e.kind() {
-                    ErrorKind::NotFound => true,
-                    _ => false,
-                }
-            }
-            _ => false,
-        }
-    }
-}
-
-/// Returns true if an entry already exists at the given path.
-fn entry_exists(source: &Path) -> bool {
-    std::fs::metadata(source).is_ok()
-}
diff --git a/src/devices/input_device.rs b/src/devices/input_device.rs
index 9b7038c..d2dd682 100644
--- a/src/devices/input_device.rs
+++ b/src/devices/input_device.rs
@@ -1,35 +1,10 @@
 use crate::*;
-use bedrock_core::*;
-use phosphor::*;
 
 use std::collections::VecDeque;
 
-macro_rules! fn_on_scroll {
-    ($fn_name:ident($value:ident, $delta:ident)) => {
-        pub fn $fn_name(&mut self, delta: f32) {
-            self.$delta += delta;
-            while self.$delta >= 1.0 {
-                self.$value = self.$value.saturating_add(1);
-                self.$delta -= 1.0;
-                self.wake = true;
-            }
-            while self.$delta <= -1.0 {
-                self.$value = self.$value.saturating_sub(1);
-                self.$delta += 1.0;
-                self.wake = true;
-            }
-        }
-    };
-}
-
 
 pub struct InputDevice {
-    pub wake: bool,
-    pub accessed: bool,
-
-    pub pointer_active: bool,
-    pub pointer_buttons: u8,
-    pub position: ScreenPosition,
+    pub cursor: ScreenPosition,
     pub x_read: u16,
     pub y_read: u16,
 
@@ -38,27 +13,83 @@ pub struct InputDevice {
     pub h_scroll_delta: f32,
     pub v_scroll_delta: f32,
 
-    pub keyboard_active: bool,
-    pub characters: VecDeque<u8>,
+    pub pointer_buttons: u8,
+    pub pointer_active: bool,
+
     pub navigation: u8,
     pub modifiers: u8,
+    pub characters: VecDeque<u8>,
+    pub keyboard_active: bool,
 
     pub gamepad_1: u8,
     pub gamepad_2: u8,
     pub gamepad_3: u8,
     pub gamepad_4: u8,
+
+    pub accessed: bool,
+    pub wake: bool,
 }
 
+
+impl Device for InputDevice {
+    fn read(&mut self, port: u8) -> u8 {
+        self.accessed = true;
+        match port {
+            0x0 => { self.x_read = self.cursor.x; read_h!(self.x_read) },
+            0x1 =>                                read_l!(self.cursor.x),
+            0x2 => { self.y_read = self.cursor.y; read_h!(self.y_read) },
+            0x3 =>                                read_l!(self.cursor.y),
+            0x4 => self.read_horizontal_scroll(),
+            0x5 => self.read_vertical_scroll(),
+            0x6 => self.pointer_buttons,
+            0x7 => read_b!(self.pointer_active),
+            0x8 => self.navigation,
+            0x9 => self.modifiers,
+            0xa => self.characters.pop_front().unwrap_or(0),
+            0xb => read_b!(self.keyboard_active),
+            0xc => self.gamepad_1,
+            0xd => self.gamepad_2,
+            0xe => self.gamepad_3,
+            0xf => self.gamepad_4,
+            _ => unreachable!(),
+        }
+    }
+
+    fn write(&mut self, port: u8, _value: u8) -> Option<Signal> {
+        self.accessed = true;
+        match port {
+            0x0 => (),
+            0x1 => (),
+            0x2 => (),
+            0x3 => (),
+            0x4 => (),
+            0x5 => (),
+            0x6 => (),
+            0x7 => (),
+            0x8 => (),
+            0x9 => (),
+            0xa => self.characters.clear(),
+            0xb => (),
+            0xc => (),
+            0xd => (),
+            0xe => (),
+            0xf => (),
+            _ => unreachable!(),
+        };
+        return None;
+    }
+
+    fn wake(&mut self) -> bool {
+        self.accessed = true;
+        std::mem::take(&mut self.wake)
+    }
+}
+
+
 impl InputDevice {
     pub fn new() -> Self {
         Self {
-            wake: false,
-            accessed: false,
-
-            pointer_active: false,
-            pointer_buttons: 0,
-
-            position: ScreenPosition::ZERO,
+            cursor: ScreenPosition::ZERO,
             x_read: 0,
             y_read: 0,
 
@@ -67,15 +98,21 @@ impl InputDevice {
             h_scroll_delta: 0.0,
             v_scroll_delta: 0.0,
 
-            keyboard_active: true,
-            characters: VecDeque::new(),
-            modifiers: 0,
+            pointer_active: false,
+            pointer_buttons: 0,
+
             navigation: 0,
+            modifiers: 0,
+            characters: VecDeque::new(),
+            keyboard_active: true,
 
             gamepad_1: 0,
             gamepad_2: 0,
             gamepad_3: 0,
             gamepad_4: 0,
+
+            accessed: false,
+            wake: false,
         }
     }
 
@@ -90,12 +127,12 @@ impl InputDevice {
     }
 
     pub fn on_cursor_move(&mut self, position: Position) {
-        let screen_position = ScreenPosition {
+        let cursor_position = ScreenPosition {
             x: position.x as i16 as u16,
             y: position.y as i16 as u16,
         };
-        if self.position != screen_position {
-            self.position = screen_position;
+        if self.cursor != cursor_position {
+            self.cursor = cursor_position;
             self.wake = true;
         }
     }
@@ -117,8 +154,33 @@ impl InputDevice {
         }
     }
 
-    fn_on_scroll!(on_horizontal_scroll(h_scroll, h_scroll_delta));
-    fn_on_scroll!(on_vertical_scroll(v_scroll, v_scroll_delta));
+    pub fn on_horizontal_scroll(&mut self, delta: f32) {
+        self.h_scroll_delta += delta;
+        while self.h_scroll_delta >= 1.0 {
+            self.h_scroll = self.h_scroll.saturating_add(1);
+            self.h_scroll_delta -= 1.0;
+            self.wake = true;
+        }
+        while self.h_scroll_delta <= -1.0 {
+            self.h_scroll = self.h_scroll.saturating_sub(1);
+            self.h_scroll_delta += 1.0;
+            self.wake = true;
+        }
+    }
+
+    pub fn on_vertical_scroll(&mut self, delta: f32) {
+        self.v_scroll_delta += delta;
+        while self.v_scroll_delta >= 1.0 {
+            self.v_scroll = self.v_scroll.saturating_add(1);
+            self.v_scroll_delta -= 1.0;
+            self.wake = true;
+        }
+        while self.v_scroll_delta <= -1.0 {
+            self.v_scroll = self.v_scroll.saturating_sub(1);
+            self.v_scroll_delta += 1.0;
+            self.wake = true;
+        }
+    }
 
     pub fn read_horizontal_scroll(&mut self) -> u8 {
         std::mem::take(&mut self.h_scroll) as u8
@@ -178,57 +240,3 @@ impl InputDevice {
         }
     }
 }
-
-impl Device for InputDevice {
-    fn read(&mut self, port: u8) -> u8 {
-        self.accessed = true;
-        match port {
-            0x0 => read_b!(self.pointer_active),
-            0x1 => self.pointer_buttons,
-            0x2 => self.read_horizontal_scroll(),
-            0x3 => self.read_vertical_scroll(),
-            0x4 => { self.x_read = self.position.x as u16; read_h!(self.x_read) },
-            0x5 => read_l!(self.position.x),
-            0x6 => { self.y_read = self.position.y as u16; read_h!(self.y_read) },
-            0x7 => read_l!(self.position.y),
-            0x8 => read_b!(self.keyboard_active),
-            0x9 => self.characters.pop_front().unwrap_or(0),
-            0xa => self.navigation,
-            0xb => self.modifiers,
-            0xc => self.gamepad_1,
-            0xd => self.gamepad_2,
-            0xe => self.gamepad_3,
-            0xf => self.gamepad_4,
-            _ => unreachable!(),
-        }
-    }
-
-    fn write(&mut self, port: u8, _value: u8) -> Option<Signal> {
-        self.accessed = true;
-        match port {
-            0x0 => (),
-            0x1 => (),
-            0x2 => (),
-            0x3 => (),
-            0x4 => (),
-            0x5 => (),
-            0x6 => (),
-            0x7 => (),
-            0x8 => (),
-            0x9 => self.characters.clear(),
-            0xa => (),
-            0xb => (),
-            0xc => (),
-            0xd => (),
-            0xe => (),
-            0xf => (),
-            _ => unreachable!(),
-        };
-        return None;
-    }
-
-    fn wake(&mut self) -> bool {
-        self.accessed = true;
-        std::mem::take(&mut self.wake)
-    }
-}
diff --git a/src/devices/local_device.rs b/src/devices/local_device.rs
deleted file mode 100644
index c6456de..0000000
--- a/src/devices/local_device.rs
+++ /dev/null
@@ -1,227 +0,0 @@
-use crate::*;
-
-use bedrock_core::*;
-
-use std::collections::VecDeque;
-use std::io::{BufRead, Stdout, Write};
-use std::sync::mpsc::{self, TryRecvError};
-
-
-pub struct LocalDevice {
-    wake: bool,
-
-    stdin_connected: bool,
-    stdin_control: bool,
-    stdin_rx: mpsc::Receiver<Vec<u8>>,
-    stdin_queue: VecDeque<u8>,
-    stdin_excess: VecDeque<u8>,
-    stdout: Stdout,
-
-    stdout_connected: bool,
-
-    decode_stdin: bool,
-    encode_stdout: bool,
-    decode_buffer: Option<u8>,
-}
-
-impl LocalDevice {
-    pub fn new(config: &EmulatorConfig) -> Self {
-        // Fill input queue with initial transmission.
-        let mut stdin_queue = VecDeque::new();
-        if let Some(bytes) = &config.initial_transmission {
-            for byte in bytes { stdin_queue.push_front(*byte) }
-        }
-        // Spawn a thread to enable non-blocking reads of stdin.
-        let (stdin_tx, stdin_rx) = std::sync::mpsc::channel();
-        std::thread::spawn(move || loop {
-            let mut stdin = std::io::stdin().lock();
-            match stdin.fill_buf() {
-                Ok(buf) if !buf.is_empty() => {
-                    let length = buf.len();
-                    stdin_tx.send(buf.to_vec()).unwrap();
-                    stdin.consume(length);
-                }
-                _ => break,
-            };
-        });
-
-        Self {
-            wake: true,
-
-            stdin_connected: true,
-            stdin_control: false,
-            stdin_rx,
-            stdin_queue,
-            stdin_excess: VecDeque::new(),
-            stdout: std::io::stdout(),
-
-            stdout_connected: true,
-
-            decode_stdin: config.decode_stdin,
-            encode_stdout: config.encode_stdout,
-            decode_buffer: None,
-        }
-    }
-
-    pub fn stdin_length(&mut self) -> u8 {
-        self.fetch_stdin_data();
-        self.stdin_queue.len().try_into().unwrap_or(u8::MAX)
-    }
-
-    pub fn stdin_enable(&mut self) {
-        self.stdin_control = true;
-    }
-
-    pub fn stdin_read(&mut self) -> u8 {
-        self.fetch_stdin_data();
-        self.stdin_queue.pop_front().unwrap_or(0)
-    }
-
-    pub fn stdout_write(&mut self, value: u8) {
-        macro_rules! hex {
-            ($value:expr) => { match $value {
-            0x0..=0x9 => $value + b'0',
-            0xa..=0xf => $value - 0x0a + b'a',
-            _ => unreachable!("Cannot encode value as hex digit: 0x{:02x}", $value),
-            } };
-        }
-        if self.encode_stdout {
-            let encoded = [hex!(value >> 4), hex!(value & 0xf), b' '];
-            self.stdout_write_raw(&encoded);
-        } else {
-            self.stdout_write_raw(&[value]);
-        };
-    }
-
-    fn stdout_write_raw(&mut self, bytes: &[u8]) {
-        if let Err(_) = self.stdout.write_all(bytes) {
-            if self.stdout_connected {
-                self.stdout_connected = false;
-                self.wake = true;  // wake because stdout was disconnected.
-            }
-        }
-    }
-
-    pub fn stdout_disable(&mut self) {
-        if self.encode_stdout {
-            self.stdout_write_raw(&[b'\n']);
-        }
-    }
-
-    pub fn fetch_stdin_data(&mut self) {
-        while self.stdin_control {
-            match self.stdin_excess.pop_front() {
-                Some(byte) => self.fetch_byte(byte),
-                None => break,
-            }
-        }
-        match self.stdin_rx.try_recv() {
-            Ok(tx) => {
-                for byte in tx {
-                    match self.stdin_control {
-                        true => self.fetch_byte(byte),
-                        false => self.stdin_excess.push_back(byte),
-                    }
-                }
-            }
-            Err(TryRecvError::Empty) => (),
-            Err(TryRecvError::Disconnected) => {
-                self.stdin_control = false;
-                if self.stdin_connected {
-                    self.stdin_connected = false;
-                    self.wake = true; // wake because stdin was disconnected.
-                }
-            }
-        }
-    }
-
-    fn fetch_byte(&mut self, byte: u8) {
-        if self.decode_stdin {
-            let decoded = match byte {
-                b'0'..=b'9' => byte - b'0',
-                b'a'..=b'f' => byte - b'a' + 0x0a,
-                b'A'..=b'F' => byte - b'A' + 0x0a,
-                b'\n' => {
-                    self.decode_buffer = None;
-                    self.stdin_control = false;
-                    self.wake = true;  // wake because a transmission ended.
-                    return;
-                },
-                _ => return,
-            };
-            if let Some(high) = std::mem::take(&mut self.decode_buffer) {
-                self.stdin_queue.push_back((high << 4) | decoded);
-                self.wake = true; // wake because a byte was received.
-            } else {
-                self.decode_buffer = Some(decoded);
-            }
-        } else {
-            self.stdin_queue.push_back(byte);
-            self.wake = true; // wake because a byte was received.
-        }
-    }
-
-    pub fn flush(&mut self) {
-       let _ = self.stdout.flush();
-    }
-}
-
-
-impl Drop for LocalDevice {
-    fn drop(&mut self) {
-        self.flush();
-    }
-}
-
-
-impl Device for LocalDevice {
-    fn read(&mut self, port: u8) -> u8 {
-        match port {
-            0x0 => read_b!(self.stdin_connected),
-            0x1 => 0xff,
-            0x2 => read_b!(self.stdin_control),
-            0x3 => 0xff,
-            0x4 => self.stdin_length(),
-            0x5 => 0xff,
-            0x6 => self.stdin_read(),
-            0x7 => self.stdin_read(),
-            0x8 => todo!(),
-            0x9 => todo!(),
-            0xa => todo!(),
-            0xb => todo!(),
-            0xc => todo!(),
-            0xd => todo!(),
-            0xe => todo!(),
-            0xf => todo!(),
-            _ => unreachable!(),
-        }
-    }
-
-    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
-        match port {
-            0x0 => (),
-            0x1 => (),
-            0x2 => self.stdin_enable(),
-            0x3 => self.stdout_disable(),
-            0x4 => self.stdin_queue.clear(),
-            0x5 => (),
-            0x6 => self.stdout_write(value),
-            0x7 => self.stdout_write(value),
-            0x8 => todo!(),
-            0x9 => todo!(),
-            0xa => todo!(),
-            0xb => todo!(),
-            0xc => todo!(),
-            0xd => todo!(),
-            0xe => todo!(),
-            0xf => todo!(),
-            _ => unreachable!(),
-        };
-        return None;
-    }
-
-    fn wake(&mut self) -> bool {
-        self.fetch_stdin_data();
-        std::mem::take(&mut self.wake)
-    }
-}
diff --git a/src/devices/math_device.rs b/src/devices/math_device.rs
index 015545e..7944b48 100644
--- a/src/devices/math_device.rs
+++ b/src/devices/math_device.rs
@@ -1,59 +1,155 @@
-use bedrock_core::*;
+use crate::*;
 
+const ANGLE_SCALE: f64 = 10430.378350470453; // 65536 / 2Ï€
 
-pub struct MathDevice {
-    pub op1: u16,
-    pub op2: u16,
 
-    pub sqrt: Option<u16>,
-    pub atan: Option<u16>,
-    pub prod: Option<(u16, u16)>,  // (low, high)
+pub struct MathDevice {
+    pub x: u16,
+    pub y: u16,
+    pub r: u16,
+    pub t: u16,
+    pub x_read: Option<u16>,
+    pub y_read: Option<u16>,
+    pub r_read: Option<u16>,
+    pub t_read: Option<u16>,
+    /// (low, high)
+    pub prod: Option<(u16, u16)>,
     pub quot: Option<u16>,
     pub rem:  Option<u16>,
 }
 
+
+impl Device for MathDevice {
+    fn read(&mut self, port: u8) -> u8 {
+        match port {
+            0x0 => read_h!(self.x()),
+            0x1 => read_l!(self.x()),
+            0x2 => read_h!(self.y()),
+            0x3 => read_l!(self.y()),
+            0x4 => read_h!(self.r()),
+            0x5 => read_l!(self.r()),
+            0x6 => read_h!(self.t()),
+            0x7 => read_l!(self.t()),
+            0x8 => read_h!(self.prod().1),
+            0x9 => read_l!(self.prod().1),
+            0xa => read_h!(self.prod().0),
+            0xb => read_l!(self.prod().0),
+            0xc => read_h!(self.quot()),
+            0xd => read_l!(self.quot()),
+            0xe => read_h!(self.rem()),
+            0xf => read_l!(self.rem()),
+            _ => unreachable!(),
+        }
+    }
+
+    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
+        match port {
+            0x0 => { write_h!(self.x, value); self.clear_polar(); },
+            0x1 => { write_l!(self.x, value); self.clear_polar(); },
+            0x2 => { write_h!(self.y, value); self.clear_polar(); },
+            0x3 => { write_l!(self.y, value); self.clear_polar(); },
+            0x4 => { write_h!(self.r, value); self.clear_cartesian(); },
+            0x5 => { write_l!(self.r, value); self.clear_cartesian(); },
+            0x6 => { write_h!(self.t, value); self.clear_cartesian(); },
+            0x7 => { write_l!(self.t, value); self.clear_cartesian(); },
+            0x8 => (),
+            0x9 => (),
+            0xa => (),
+            0xb => (),
+            0xc => (),
+            0xd => (),
+            0xe => (),
+            0xf => (),
+            _ => unreachable!(),
+        };
+        return None;
+    }
+
+    fn wake(&mut self) -> bool {
+        false
+    }
+}
+
+
 impl MathDevice {
     pub fn new() -> Self {
         Self {
-            op1: 0,
-            op2: 0,
+            x: 0,
+            y: 0,
+            r: 0,
+            t: 0,
+            x_read: None,
+            y_read: None,
+            r_read: None,
+            t_read: None,
 
-            sqrt: None,
-            atan: None,
             prod: None,
             quot: None,
             rem:  None,
         }
     }
 
-    pub fn clear(&mut self) {
-        self.sqrt = None;
-        self.atan = None;
+    pub fn clear_cartesian(&mut self) {
+        self.x_read = None;
+        self.y_read = None;
+    }
+
+    pub fn clear_polar(&mut self) {
+        self.r_read = None;
+        self.t_read = None;
         self.prod = None;
         self.quot = None;
-        self.rem  = None;
+        self.rem = None;
+    }
+
+    pub fn x(&mut self) -> u16 {
+        match self.x_read {
+            Some(x) => x,
+            None => {
+                let r = self.r as f64;
+                let t = self.t as f64;
+                let angle = t / ANGLE_SCALE;
+                let x = angle.cos() * r;
+                self.x_read = Some(x as i16 as u16);
+                self.x_read.unwrap()
+            }
+        }
     }
 
-    pub fn atan(&mut self) -> u16 {
-        match self.atan {
-            Some(atan) => atan,
+    pub fn y(&mut self) -> u16 {
+        match self.y_read {
+            Some(y) => y,
             None => {
-                let x = self.op1 as i16 as f64;
-                let y = self.op2 as i16 as f64;
-                const SCALE: f64 = 10430.378350470453; // PI * 32768
-                self.atan = Some((f64::atan2(x, y) * SCALE) as i16 as u16);
-                self.atan.unwrap()
+                let r = self.r as f64;
+                let t = self.t as f64;
+                let angle = t / ANGLE_SCALE;
+                let y = angle.sin() * r;
+                self.y_read = Some(y as i16 as u16);
+                self.y_read.unwrap()
             }
         }
     }
 
-    pub fn sqrt(&mut self) -> u16 {
-        match self.sqrt {
-            Some(sqrt) => sqrt,
+    pub fn r(&mut self) -> u16 {
+        match self.r_read {
+            Some(r) => r,
             None => {
-                let input = ((self.op1 as u32) << 16) | (self.op2 as u32);
-                self.sqrt = Some((input as f64).sqrt() as u16);
-                self.sqrt.unwrap()
+                let sum = (self.x as f64).powi(2) + (self.y as f64).powi(2);
+                self.r_read = Some(sum.sqrt() as u16);
+                self.r_read.unwrap()
+            }
+        }
+    }
+
+    pub fn t(&mut self) -> u16 {
+        match self.t_read {
+            Some(t) => t,
+            None => {
+                let x = self.x as i16 as f64;
+                let y = self.x as i16 as f64;
+                let angle = f64::atan2(y, x) * ANGLE_SCALE;
+                self.t_read = Some(angle as i16 as u16);
+                self.t_read.unwrap()
             }
         }
     }
@@ -62,7 +158,7 @@ impl MathDevice {
         match self.prod {
             Some(prod) => prod,
             None => {
-                self.prod = Some(self.op1.widening_mul(self.op2));
+                self.prod = Some(self.x.widening_mul(self.y));
                 self.prod.unwrap()
             }
         }
@@ -72,7 +168,7 @@ impl MathDevice {
         match self.quot {
             Some(quot) => quot,
             None => {
-                self.quot = Some(self.op1.checked_div(self.op2).unwrap_or(0));
+                self.quot = Some(self.x.checked_div(self.y).unwrap_or(0));
                 self.quot.unwrap()
             }
         }
@@ -82,60 +178,9 @@ impl MathDevice {
         match self.rem {
             Some(rem) => rem,
             None => {
-                self.rem = Some(self.op1.checked_rem(self.op2).unwrap_or(0));
+                self.rem = Some(self.x.checked_rem(self.y).unwrap_or(0));
                 self.rem.unwrap()
             }
         }
     }
 }
-
-impl Device for MathDevice {
-    fn read(&mut self, port: u8) -> u8 {
-        match port {
-            0x0 => read_h!(self.op1),
-            0x1 => read_l!(self.op1),
-            0x2 => read_h!(self.op2),
-            0x3 => read_l!(self.op2),
-            0x4 => read_h!(self.sqrt()),
-            0x5 => read_l!(self.sqrt()),
-            0x6 => read_h!(self.atan()),
-            0x7 => read_l!(self.atan()),
-            0x8 => read_h!(self.prod().1),
-            0x9 => read_l!(self.prod().1),
-            0xa => read_h!(self.prod().0),
-            0xb => read_l!(self.prod().0),
-            0xc => read_h!(self.quot()),
-            0xd => read_l!(self.quot()),
-            0xe => read_h!(self.rem()),
-            0xf => read_l!(self.rem()),
-            _ => unreachable!(),
-        }
-    }
-
-    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
-        match port {
-            0x0 => { write_h!(self.op1, value); self.clear(); },
-            0x1 => { write_l!(self.op1, value); self.clear(); },
-            0x2 => { write_h!(self.op2, value); self.clear(); },
-            0x3 => { write_l!(self.op2, value); self.clear(); },
-            0x4 => (),
-            0x5 => (),
-            0x6 => (),
-            0x7 => (),
-            0x8 => (),
-            0x9 => (),
-            0xa => (),
-            0xb => (),
-            0xc => (),
-            0xd => (),
-            0xe => (),
-            0xf => (),
-            _ => unreachable!(),
-        };
-        return None;
-    }
-
-    fn wake(&mut self) -> bool {
-        false
-    }
-}
diff --git a/src/devices/memory_device.rs b/src/devices/memory_device.rs
index 0128d55..8efb12a 100644
--- a/src/devices/memory_device.rs
+++ b/src/devices/memory_device.rs
@@ -1,53 +1,73 @@
-use bedrock_core::*;
+use crate::*;
 
-type Page = [u8; 256];
-
-macro_rules! fn_read_head {
-    ($fn_name:ident($offset:ident, $address:ident)) => {
-        pub fn $fn_name(&mut self) -> u8 {
-            let page_i = (self.$offset + (self.$address / 256)) as usize;
-            let byte_i = (self.$address % 256) as usize;
-            self.$address = self.$address.wrapping_add(1);
-            match self.pages.get(page_i) {
-                Some(page) => page[byte_i],
-                None => 0,
-            }
-        }
-    };
-}
+use std::cmp::min;
 
-macro_rules! fn_write_head {
-    ($fn_name:ident($offset:ident, $address:ident)) => {
-        pub fn $fn_name(&mut self, byte: u8) {
-            let page_i = (self.$offset + (self.$address / 256)) as usize;
-            let byte_i = (self.$address % 256) as usize;
-            self.$address = self.$address.wrapping_add(1);
-            match self.pages.get_mut(page_i) {
-                Some(page) => page[byte_i] = byte,
-                None => if page_i < self.provisioned {
-                    self.pages.resize(page_i + 1, [0; 256]);
-                    self.pages[page_i][byte_i] = byte;
-                }
-            }
-        }
-    };
-}
 
+type Page = [u8; 256];
 
 pub struct MemoryDevice {
     pub limit: u16,          // maximum provisionable number of pages
     pub requested: u16,      // number of pages requested by program
     pub provisioned: usize,  // number of pages provisioned for use
     pub pages: Vec<Page>,    // all allocated pages
+    pub head_1: HeadAddress,
+    pub head_2: HeadAddress,
+    pub copy_length: u16,
+}
 
-    pub offset_1: u16,
-    pub address_1: u16,
-    pub offset_2: u16,
-    pub address_2: u16,
 
-    pub copy_length: u16,
+impl Device for MemoryDevice {
+    fn read(&mut self, port: u8) -> u8 {
+        match port {
+            0x0 => self.read_head_1(),
+            0x1 => self.read_head_1(),
+            0x2 => read_h!(self.head_1.offset),
+            0x3 => read_l!(self.head_1.offset),
+            0x4 => read_h!(self.head_1.address),
+            0x5 => read_l!(self.head_1.address),
+            0x6 => read_h!(self.provisioned),
+            0x7 => read_l!(self.provisioned),
+            0x8 => self.read_head_2(),
+            0x9 => self.read_head_2(),
+            0xa => read_h!(self.head_2.offset),
+            0xb => read_l!(self.head_2.offset),
+            0xc => read_h!(self.head_2.address),
+            0xd => read_l!(self.head_2.address),
+            0xe => 0x00,
+            0xf => 0x00,
+            _ => unreachable!(),
+        }
+    }
+
+    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
+        match port {
+            0x0 => self.write_head_1(value),
+            0x1 => self.write_head_1(value),
+            0x2 => write_h!(self.head_1.offset, value),
+            0x3 => write_l!(self.head_1.offset, value),
+            0x4 => write_h!(self.head_1.address, value),
+            0x5 => write_l!(self.head_1.address, value),
+            0x6 => write_h!(self.requested, value),
+            0x7 => { write_l!(self.requested, value); self.provision(); },
+            0x8 => self.write_head_2(value),
+            0x9 => self.write_head_2(value),
+            0xa => write_h!(self.head_2.offset, value),
+            0xb => write_l!(self.head_2.offset, value),
+            0xc => write_h!(self.head_2.address, value),
+            0xd => write_l!(self.head_2.address, value),
+            0xe => write_h!(self.copy_length, value),
+            0xf => { write_l!(self.copy_length, value); self.copy(); },
+            _ => unreachable!(),
+        };
+        return None;
+    }
+
+    fn wake(&mut self) -> bool {
+        false
+    }
 }
 
+
 impl MemoryDevice {
     pub fn new() -> Self {
         Self {
@@ -55,34 +75,63 @@ impl MemoryDevice {
             requested: 0,
             provisioned: 0,
             pages: Vec::new(),
+            head_1: HeadAddress::new(),
+            head_2: HeadAddress::new(),
+            copy_length: 0,
+        }
+    }
+
+    pub fn read_head_1(&mut self) -> u8 {
+        let (page_i, byte_i) = self.head_1.get_page_address();
+        self.read_byte(page_i, byte_i)
+    }
 
-            offset_1: 0,
-            address_1: 0,
-            offset_2: 0,
-            address_2: 0,
+    pub fn read_head_2(&mut self) -> u8 {
+        let (page_i, byte_i) = self.head_2.get_page_address();
+        self.read_byte(page_i, byte_i)
+    }
 
-            copy_length: 0,
+    fn read_byte(&self, page_i: usize, byte_i: usize) -> u8 {
+        match self.pages.get(page_i) {
+            Some(page) => page[byte_i],
+            None => 0,
         }
     }
 
-    fn_read_head! { read_head_1( offset_1, address_1) }
-    fn_read_head! { read_head_2( offset_2, address_2) }
-    fn_write_head!{ write_head_1(offset_1, address_1) }
-    fn_write_head!{ write_head_2(offset_2, address_2) }
+    pub fn write_head_1(&mut self, value: u8) {
+        let (page_i, byte_i) = self.head_1.get_page_address();
+        self.write_byte(page_i, byte_i, value);
+    }
+
+    pub fn write_head_2(&mut self, value: u8) {
+        let (page_i, byte_i) = self.head_2.get_page_address();
+        self.write_byte(page_i, byte_i, value);
+    }
+
+    // Write a byte to a page of memory.
+    fn write_byte(&mut self, page_i: usize, byte_i: usize, value: u8) {
+        match self.pages.get_mut(page_i) {
+            Some(page) => page[byte_i] = value,
+            None => if page_i < self.provisioned {
+                self.pages.resize(page_i + 1, [0; 256]);
+                self.pages[page_i][byte_i] = value;
+            }
+        }
+    }
 
     pub fn provision(&mut self) {
-        self.provisioned = std::cmp::min(self.requested, self.limit) as usize;
+        self.provisioned = min(self.requested, self.limit) as usize;
         // Defer allocation of new pages.
         self.pages.truncate(self.provisioned as usize);
     }
 
     pub fn copy(&mut self) {
-        let src  = self.offset_2 as usize;
-        let dest = self.offset_1 as usize;
+        let src  = self.head_2.offset as usize;
+        let dest = self.head_1.offset as usize;
         let count = self.copy_length as usize;
 
         // Pre-allocate destination pages as needed.
-        let pages_needed = std::cmp::min(dest + count, self.provisioned);
+        let pages_needed = min(dest + count, self.provisioned);
         if pages_needed > self.pages.len() {
             self.pages.resize(pages_needed, [0; 256]);
         }
@@ -100,53 +149,24 @@ impl MemoryDevice {
     }
 }
 
-impl Device for MemoryDevice {
-    fn read(&mut self, port: u8) -> u8 {
-        match port {
-            0x0 => self.read_head_1(),
-            0x1 => self.read_head_1(),
-            0x2 => read_h!(self.offset_1),
-            0x3 => read_l!(self.offset_1),
-            0x4 => read_h!(self.address_1),
-            0x5 => read_l!(self.address_1),
-            0x6 => read_h!(self.provisioned),
-            0x7 => read_l!(self.provisioned),
-            0x8 => self.read_head_2(),
-            0x9 => self.read_head_2(),
-            0xa => read_h!(self.offset_2),
-            0xb => read_l!(self.offset_2),
-            0xc => read_h!(self.address_2),
-            0xd => read_l!(self.address_2),
-            0xe => 0x00,
-            0xf => 0x00,
-            _ => unreachable!(),
-        }
-    }
 
-    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
-        match port {
-            0x0 => self.write_head_1(value),
-            0x1 => self.write_head_1(value),
-            0x2 => write_h!(self.offset_1, value),
-            0x3 => write_l!(self.offset_1, value),
-            0x4 => write_h!(self.address_1, value),
-            0x5 => write_l!(self.address_1, value),
-            0x6 => write_h!(self.requested, value),
-            0x7 => { write_l!(self.requested, value); self.provision(); },
-            0x8 => self.write_head_2(value),
-            0x9 => self.write_head_2(value),
-            0xa => write_h!(self.offset_2, value),
-            0xb => write_l!(self.offset_2, value),
-            0xc => write_h!(self.address_2, value),
-            0xd => write_l!(self.address_2, value),
-            0xe => write_h!(self.copy_length, value),
-            0xf => { write_l!(self.copy_length, value); self.copy(); },
-            _ => unreachable!(),
-        };
-        return None;
+pub struct HeadAddress {
+    pub offset: u16,
+    pub address: u16,
+}
+
+impl HeadAddress {
+    pub fn new() -> Self {
+        Self {
+            offset: 0,
+            address: 0,
+        }
     }
 
-    fn wake(&mut self) -> bool {
-        false
+    fn get_page_address(&mut self) -> (usize, usize) {
+        let page_i = (self.offset + (self.address / 256)) as usize;
+        let byte_i = (self.address % 256) as usize;
+        self.address = self.address.wrapping_add(1);
+        (page_i, byte_i)
     }
 }
diff --git a/src/devices/mod.rs b/src/devices/mod.rs
new file mode 100644
index 0000000..aa98a49
--- /dev/null
+++ b/src/devices/mod.rs
@@ -0,0 +1,17 @@
+mod system_device;
+mod memory_device;
+mod math_device;
+mod clock_device;
+mod input_device;
+mod screen_device;
+mod stream_device;
+mod file_device;
+
+pub use system_device::*;
+pub use memory_device::*;
+pub use math_device::*;
+pub use clock_device::*;
+pub use input_device::*;
+pub use screen_device::*;
+pub use stream_device::*;
+pub use file_device::*;
diff --git a/src/devices/remote_device.rs b/src/devices/remote_device.rs
deleted file mode 100644
index f50ac7a..0000000
--- a/src/devices/remote_device.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-use bedrock_core::*;
-
-
-pub struct RemoteDevice {
-
-}
-
-impl RemoteDevice {
-    pub fn new() -> Self {
-        Self {
-
-        }
-    }
-}
-
-impl Device for RemoteDevice {
-    fn read(&mut self, _port: u8) -> u8 {
-        todo!()
-        // match port {
-        //     _ => unreachable!(),
-        // }
-    }
-
-    fn write(&mut self, _port: u8, _value: u8) -> Option<Signal> {
-        todo!()
-        // match port {
-        //     _ => unreachable!(),
-        // };
-        // return None;
-    }
-
-    fn wake(&mut self) -> bool {
-        false
-    }
-}
diff --git a/src/devices/screen_device.rs b/src/devices/screen_device.rs
index a10ab20..4c3f5ab 100644
--- a/src/devices/screen_device.rs
+++ b/src/devices/screen_device.rs
@@ -1,21 +1,18 @@
 use crate::*;
 
-use bedrock_core::*;
 use geometry::*;
 use phosphor::*;
 
-type Sprite = [[u8; 8]; 8];
+
+pub type Sprite = [[u8; 8]; 8];
+
 #[derive(Clone, Copy)]
 pub enum Layer { Fg, Bg }
 
 
 pub struct ScreenDevice {
-    pub wake: bool,
-    pub accessed: bool,
-
     /// Each byte represents a screen pixel, left-to-right and top-to-bottom.
     // Only the bottom four bits of each byte are used.
-    // TODO: Consider using the high bit of each pixel byte as a dirty bit.
     pub fg: Vec<u8>,
     pub bg: Vec<u8>,
     pub dirty: bool,
@@ -32,18 +29,81 @@ pub struct ScreenDevice {
 
     pub palette_write: u16,
     pub palette: [Colour; 16],
-    pub colours: u16,
+    pub sprite_colours: u16,
     pub sprite: SpriteBuffer,
+
+    pub accessed: bool,
+    pub wake: bool,
+}
+
+
+impl HasDimensions<u16> for ScreenDevice {
+    fn dimensions(&self) -> ScreenDimensions {
+        self.dimensions
+    }
 }
 
+
+impl Device for ScreenDevice {
+    fn read(&mut self, port: u8) -> u8 {
+        self.accessed = true;
+        match port {
+            0x0 => read_h!(self.cursor.x),
+            0x1 => read_l!(self.cursor.x),
+            0x2 => read_h!(self.cursor.y),
+            0x3 => read_l!(self.cursor.y),
+            0x4 => read_h!(self.dimensions.width),
+            0x5 => read_l!(self.dimensions.width),
+            0x6 => read_h!(self.dimensions.height),
+            0x7 => read_l!(self.dimensions.height),
+            0x8 => 0,
+            0x9 => 0,
+            0xa => 0,
+            0xb => 0,
+            0xc => 0,
+            0xd => 0,
+            0xe => 0,
+            0xf => 0,
+            _ => unreachable!(),
+        }
+    }
+
+    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
+        self.accessed = true;
+        match port {
+            0x0 =>   write_h!(self.cursor.x, value),
+            0x1 =>   write_l!(self.cursor.x, value),
+            0x2 =>   write_h!(self.cursor.y, value),
+            0x3 =>   write_l!(self.cursor.y, value),
+            0x4 =>   write_h!(self.width_write, value),
+            0x5 => { write_l!(self.width_write, value); self.resize_width() }
+            0x6 =>   write_h!(self.height_write, value),
+            0x7 => { write_l!(self.height_write, value); self.resize_height() }
+            0x8 =>   write_h!(self.palette_write, value),
+            0x9 => { write_l!(self.palette_write, value); self.set_palette() }
+            0xa =>   write_h!(self.sprite_colours, value),
+            0xb =>   write_l!(self.sprite_colours, value),
+            0xc =>   self.sprite.push_byte(value),
+            0xd =>   self.sprite.push_byte(value),
+            0xe =>   self.draw_dispatch(value),
+            0xf =>   self.move_cursor(value),
+            _ => unreachable!(),
+        };
+        return None;
+    }
+
+    fn wake(&mut self) -> bool {
+        self.accessed = true;
+        std::mem::take(&mut self.wake)
+    }
+}
+
+
 impl ScreenDevice {
     pub fn new(config: &EmulatorConfig) -> Self {
-        let area = config.dimensions.area_usize();
+        let area = config.initial_dimensions.area_usize();
 
         Self {
-            wake: false,
-            accessed: false,
-
             fg: vec![0; area],
             bg: vec![0; area],
             dirty: false,
@@ -51,7 +111,7 @@ impl ScreenDevice {
             cursor: ScreenPosition::ZERO,
             vector: ScreenPosition::ZERO,
 
-            dimensions: config.dimensions,
+            dimensions: config.initial_dimensions,
             dirty_dimensions: true,
             width_write: 0,
             height_write: 0,
@@ -60,12 +120,15 @@ impl ScreenDevice {
 
             palette_write: 0,
             palette: [Colour::BLACK; 16],
-            colours: 0,
+            sprite_colours: 0,
             sprite: SpriteBuffer::new(),
+
+            accessed: false,
+            wake: false,
         }
     }
 
-    /// External resize.
+    /// Resize screen to match window dimensions.
     pub fn resize(&mut self, dimensions: phosphor::Dimensions) {
         // Replace dimensions with fixed dimensions.
         let screen_dimensions = ScreenDimensions {
@@ -231,10 +294,10 @@ impl ScreenDevice {
             false => self.sprite.read_1bit_sprite(draw),
         };
         let colours = [
-            (self.colours >> 12 & 0x000f) as u8,
-            (self.colours >>  8 & 0x000f) as u8,
-            (self.colours >>  4 & 0x000f) as u8,
-            (self.colours       & 0x000f) as u8,
+            (self.sprite_colours >> 12 & 0x000f) as u8,
+            (self.sprite_colours >>  8 & 0x000f) as u8,
+            (self.sprite_colours >>  4 & 0x000f) as u8,
+            (self.sprite_colours       & 0x000f) as u8,
         ];
         let cx = self.cursor.x;
         let cy = self.cursor.y;
@@ -279,8 +342,8 @@ impl ScreenDevice {
         if draw & 0x10 != 0 {
         // Draw 1-bit textured line.
             let sprite = self.sprite.read_1bit_sprite(draw);
-            let c1 = (self.colours >>  8 & 0xf) as u8;
-            let c0 = (self.colours >> 12 & 0xf) as u8;
+            let c1 = (self.sprite_colours >>  8 & 0xf) as u8;
+            let c0 = (self.sprite_colours >> 12 & 0xf) as u8;
             let opaque = draw & 0x08 == 0;
             loop {
                 let sprite_pixel = sprite[(y as usize) % 8][(x as usize) % 8];
@@ -333,8 +396,8 @@ impl ScreenDevice {
         if draw & 0x10 != 0 {
         // Draw 1-bit textured rectangle.
             let sprite = self.sprite.read_1bit_sprite(draw);
-            let c1 = (self.colours >>  8 & 0xf) as u8;
-            let c0 = (self.colours >> 12 & 0xf) as u8;
+            let c1 = (self.sprite_colours >>  8 & 0xf) as u8;
+            let c0 = (self.sprite_colours >> 12 & 0xf) as u8;
             let opaque = draw & 0x08 == 0;
             for y in t..=b {
                 for x in l..=r {
@@ -355,146 +418,4 @@ impl ScreenDevice {
     }
 }
 
-impl Device for ScreenDevice {
-    fn read(&mut self, port: u8) -> u8 {
-        self.accessed = true;
-        match port {
-            0x0 => read_h!(self.dimensions.width),
-            0x1 => read_l!(self.dimensions.width),
-            0x2 => read_h!(self.dimensions.height),
-            0x3 => read_l!(self.dimensions.height),
-            0x4 => read_h!(self.cursor.x),
-            0x5 => read_l!(self.cursor.x),
-            0x6 => read_h!(self.cursor.y),
-            0x7 => read_l!(self.cursor.y),
-            0x8 => 0,
-            0x9 => 0,
-            0xa => 0,
-            0xb => 0,
-            0xc => 0,
-            0xd => 0,
-            0xe => 0,
-            0xf => 0,
-            _ => unreachable!(),
-        }
-    }
-
-    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
-        self.accessed = true;
-        match port {
-            0x0 => write_h!(self.width_write, value),
-            0x1 => { write_l!(self.width_write, value); self.resize_width(); },
-            0x2 => write_h!(self.height_write, value),
-            0x3 => { write_l!(self.height_write, value); self.resize_height(); },
-            0x4 => write_h!(self.cursor.x, value),
-            0x5 => write_l!(self.cursor.x, value),
-            0x6 => write_h!(self.cursor.y, value),
-            0x7 => write_l!(self.cursor.y, value),
-            0x8 => write_h!(self.palette_write, value),
-            0x9 => { write_l!(self.palette_write, value); self.set_palette(); },
-            0xa => write_h!(self.colours, value),
-            0xb => write_l!(self.colours, value),
-            0xc => self.sprite.push_byte(value),
-            0xd => self.sprite.push_byte(value),
-            0xe => self.draw_dispatch(value),
-            0xf => self.move_cursor(value),
-            _ => unreachable!(),
-        };
-        return None;
-    }
-
-    fn wake(&mut self) -> bool {
-        self.accessed = true;
-        std::mem::take(&mut self.wake)
-    }
-}
-
-impl HasDimensions<u16> for ScreenDevice {
-    fn dimensions(&self) -> ScreenDimensions {
-        self.dimensions
-    }
-}
-
-
-pub struct SpriteBuffer {
-    pub mem: [u8; 16],
-    pub pointer: usize,
-    pub cached: Option<(Sprite, u8)>,
-}
-
-impl SpriteBuffer {
-    pub fn new() -> Self {
-        Self {
-            mem: [0; 16],
-            pointer: 0,
-            cached: None,
-        }
-    }
-
-    pub fn push_byte(&mut self, byte: u8) {
-        self.mem[self.pointer] = byte;
-        self.pointer = (self.pointer + 1) % 16;
-        self.cached = None;
-    }
-
-    pub fn read_1bit_sprite(&mut self, draw: u8) -> Sprite {
-        if let Some((sprite, transform)) = self.cached {
-            if transform == (draw & 0x77) {
-                return sprite;
-            }
-        }
-        macro_rules! c {
-            ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); };
-            ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; };
-        }
-        let mut sprite = [[0; 8]; 8];
-        let mut p = match draw & 0x02 != 0 {
-            true  => self.pointer,
-            false => self.pointer + 8,
-        };
-        match draw & 0x07 {
-            0x0 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } },
-            0x1 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>(  x) & 1; } } },
-            0x2 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } },
-            0x3 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>(  x) & 1; } } },
-            0x4 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } },
-            0x5 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>(  x) & 1; } } },
-            0x6 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } },
-            0x7 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>(  x) & 1; } } },
-            _ => unreachable!(),
-        }
-        self.cached = Some((sprite, draw & 0x77));
-        return sprite;
-    }
 
-    pub fn read_2bit_sprite(&mut self, draw: u8) -> Sprite {
-        if let Some((sprite, transform)) = self.cached {
-            if transform == (draw & 0x77) {
-                return sprite;
-            }
-        }
-        macro_rules! c {
-            ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); };
-            ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; };
-        }
-        let mut sprite = [[0; 8]; 8];
-        let mut p = match draw & 0x02 != 0 {
-            true  => self.pointer,
-            false => self.pointer + 8,
-        };
-        let mut s = p + 8;
-        match draw & 0x07 {
-            0x0 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
-            0x1 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=  x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
-            0x2 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
-            0x3 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=  x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
-            0x4 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
-            0x5 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=  x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
-            0x6 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
-            0x7 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=  x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
-            _ => unreachable!(),
-        }
-        self.cached = Some((sprite, draw & 0x77));
-        return sprite;
-    }
-}
diff --git a/src/devices/stream_device.rs b/src/devices/stream_device.rs
new file mode 100644
index 0000000..e44ffb8
--- /dev/null
+++ b/src/devices/stream_device.rs
@@ -0,0 +1,235 @@
+use crate::*;
+
+use std::collections::VecDeque;
+use std::io::{BufRead, Stdout, Write};
+use std::sync::mpsc::{self, TryRecvError};
+
+
+pub struct StreamDevice {
+    /// True if a source is connected to stdin.
+    stdin_connected: bool,
+    /// True if a transmission is in progress.
+    stdin_control: bool,
+    stdin_rx: mpsc::Receiver<Vec<u8>>,
+    /// Bytes received in the current transmission.
+    stdin_queue: VecDeque<u8>,
+    /// Bytes received since stdin end-of-transmission.
+    stdin_excess: VecDeque<u8>,
+
+    stdout: Stdout,
+    /// True if a sink is connected to stdout.
+    stdout_connected: bool,
+
+    /// True if stdin is transmission-encoded.
+    decode_stdin: bool,
+    /// True if stdout should be transmission-encoded.
+    encode_stdout: bool,
+    /// Half-byte buffer for decoding stdin.
+    decode_buffer: Option<u8>,
+
+    wake: bool,
+}
+
+
+impl Device for StreamDevice {
+    fn read(&mut self, port: u8) -> u8 {
+        match port {
+            0x0 => read_b!(self.stdin_connected),
+            0x1 => read_b!(self.stdout_connected),
+            0x2 => read_b!(self.stdin_control),
+            0x3 => 0xff,
+            0x4 => self.stdin_length(),
+            0x5 => 0xff,
+            0x6 => self.stdin_read(),
+            0x7 => self.stdin_read(),
+            0x8 => todo!(),
+            0x9 => todo!(),
+            0xa => todo!(),
+            0xb => todo!(),
+            0xc => todo!(),
+            0xd => todo!(),
+            0xe => todo!(),
+            0xf => todo!(),
+            _ => unreachable!(),
+        }
+    }
+
+    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
+        match port {
+            0x0 => (),
+            0x1 => (),
+            0x2 => self.stdin_enable(),
+            0x3 => self.stdout_disable(),
+            0x4 => (),
+            0x5 => (),
+            0x6 => self.stdout_write(value),
+            0x7 => self.stdout_write(value),
+            0x8 => todo!(),
+            0x9 => todo!(),
+            0xa => todo!(),
+            0xb => todo!(),
+            0xc => todo!(),
+            0xd => todo!(),
+            0xe => todo!(),
+            0xf => todo!(),
+            _ => unreachable!(),
+        };
+        return None;
+    }
+
+    fn wake(&mut self) -> bool {
+        self.fetch_stdin_data();
+        std::mem::take(&mut self.wake)
+    }
+}
+
+
+impl StreamDevice {
+    pub fn new(config: &EmulatorConfig) -> Self {
+        // Spawn a thread to enable non-blocking reads of stdin.
+        let (stdin_tx, stdin_rx) = std::sync::mpsc::channel();
+        std::thread::spawn(move || loop {
+            let mut stdin = std::io::stdin().lock();
+            match stdin.fill_buf() {
+                Ok(buf) if !buf.is_empty() => {
+                    let length = buf.len();
+                    stdin_tx.send(buf.to_vec()).unwrap();
+                    stdin.consume(length);
+                }
+                _ => break,
+            };
+        });
+
+        Self {
+            stdin_connected: true,
+            stdin_control: false,
+            stdin_rx,
+            stdin_queue: VecDeque::new(),
+            stdin_excess: VecDeque::new(),
+
+            stdout: std::io::stdout(),
+            stdout_connected: true,
+
+            decode_stdin: config.decode_stdin,
+            encode_stdout: config.encode_stdout,
+            decode_buffer: None,
+
+            wake: true,
+        }
+    }
+
+    pub fn stdin_length(&mut self) -> u8 {
+        self.fetch_stdin_data();
+        self.stdin_queue.len().try_into().unwrap_or(u8::MAX)
+    }
+
+    pub fn stdin_enable(&mut self) {
+        self.stdin_control = true;
+    }
+
+    pub fn stdin_read(&mut self) -> u8 {
+        self.fetch_stdin_data();
+        self.stdin_queue.pop_front().unwrap_or(0)
+    }
+
+    pub fn stdout_write(&mut self, value: u8) {
+        macro_rules! hex {
+            ($value:expr) => { match $value {
+            0x0..=0x9 => $value + b'0',
+            0xa..=0xf => $value - 0x0a + b'a',
+            _ => unreachable!("Cannot encode value as hex digit: 0x{:02x}", $value),
+            } };
+        }
+        if self.encode_stdout {
+            let encoded = [hex!(value >> 4), hex!(value & 0xf), b' '];
+            self.stdout_write_raw(&encoded);
+        } else {
+            self.stdout_write_raw(&[value]);
+        };
+    }
+
+    fn stdout_write_raw(&mut self, bytes: &[u8]) {
+        if let Err(_) = self.stdout.write_all(bytes) {
+            if self.stdout_connected {
+                self.stdout_connected = false;
+                self.wake = true;  // wake because stdout was disconnected.
+            }
+        }
+    }
+
+    pub fn stdout_disable(&mut self) {
+        if self.encode_stdout {
+            self.stdout_write_raw(&[b'\n']);
+        }
+    }
+
+    /// Fetch all pending data from stdin.
+    pub fn fetch_stdin_data(&mut self) {
+        while self.stdin_control {
+            match self.stdin_excess.pop_front() {
+                Some(byte) => self.fetch_byte(byte),
+                None => break,
+            }
+        }
+        loop {
+            match self.stdin_rx.try_recv() {
+                Ok(tx) => {
+                    for byte in tx {
+                        match self.stdin_control {
+                            true => self.fetch_byte(byte),
+                            false => self.stdin_excess.push_back(byte),
+                        }
+                    }
+                }
+                Err(TryRecvError::Empty) => {
+                    break;
+                }
+                Err(TryRecvError::Disconnected) => {
+                    self.stdin_control = false;
+                    if self.stdin_connected {
+                        self.stdin_connected = false;
+                        self.wake = true; // wake because stdin was disconnected.
+                    }
+                    break;
+                }
+            }
+        }
+    }
+
+    fn fetch_byte(&mut self, byte: u8) {
+        if self.decode_stdin {
+            let decoded = match byte {
+                b'0'..=b'9' => byte - b'0',
+                b'a'..=b'f' => byte - b'a' + 0x0a,
+                b'A'..=b'F' => byte - b'A' + 0x0a,
+                b'\n' => {
+                    self.decode_buffer = None;
+                    self.stdin_control = false;
+                    self.wake = true;  // wake because a transmission ended.
+                    return;
+                },
+                _ => return,
+            };
+            if let Some(high) = std::mem::take(&mut self.decode_buffer) {
+                self.stdin_queue.push_back((high << 4) | decoded);
+                self.wake = true; // wake because a byte was received.
+            } else {
+                self.decode_buffer = Some(decoded);
+            }
+        } else {
+            self.stdin_queue.push_back(byte);
+            self.wake = true; // wake because a byte was received.
+        }
+    }
+
+    pub fn flush(&mut self) {
+       let _ = self.stdout.flush();
+    }
+}
+
+
+impl Drop for StreamDevice {
+    fn drop(&mut self) {
+        self.flush();
+    }
+}
diff --git a/src/devices/system_device.rs b/src/devices/system_device.rs
index 383bb08..10ddad1 100644
--- a/src/devices/system_device.rs
+++ b/src/devices/system_device.rs
@@ -1,76 +1,69 @@
-use bedrock_core::*;
+use crate::*;
 
 
 pub struct SystemDevice {
+    /// Name and version of this system.
     pub name: ReadBuffer,
+    /// Authors of this system.
     pub authors: ReadBuffer,
-    pub can_wake: u16,
-    pub wake_id: u8,
+    /// Mask of all devices waiting to wake from sleep.
+    pub wakers: u16,
+    /// Device that most recently woke the system.
+    pub waker: u8,
+    /// True if the system has been put to sleep.
     pub asleep: bool,
+    /// Mask of all available devices.
+    pub devices: u16,
+    /// Name of the first custom devices.
+    pub custom1: ReadBuffer,
+    /// Name of the second custom devices.
+    pub custom2: ReadBuffer,
+    /// Name of the third custom devices.
+    pub custom3: ReadBuffer,
+    /// Name of the fourth custom devices.
+    pub custom4: ReadBuffer,
 }
 
-impl SystemDevice {
-    pub fn new() -> Self {
-        let pkg_version = env!("CARGO_PKG_VERSION");
-        let pkg_name = env!("CARGO_PKG_NAME");
-        let pkg_authors = env!("CARGO_PKG_AUTHORS");
-        let name_str = format!("{pkg_name}/{pkg_version}");
-        let authors_str = pkg_authors.replace(":", "\n");
-        Self {
-            name: ReadBuffer::from_str(&name_str),
-            authors: ReadBuffer::from_str(&authors_str),
-            can_wake: 0,
-            wake_id: 0,
-            asleep: false,
-        }
-    }
-
-    pub fn can_wake(&self, dev_id: u8) -> bool {
-        test_bit!(self.can_wake, 0x8000 >> dev_id)
-    }
-}
 
 impl Device for SystemDevice {
     fn read(&mut self, port: u8) -> u8 {
         match port {
-            0x0 => self.name.read(),
-            0x1 => self.authors.read(),
-            0x2 => 0x00,
+            0x0 => 0x00,
+            0x1 => 0x00,
+            0x2 => self.waker,
             0x3 => 0x00,
             0x4 => 0x00,
             0x5 => 0x00,
-            0x6 => 0b1111_1100,
-            0x7 => 0b0000_0000,  // TODO: Update when fs and stream implemented
-            0x8 => 0x00,
-            0x9 => 0x00,
-            0xa => self.wake_id,
+            0x6 => 0x00,
+            0x7 => 0x00,
+            0x8 => self.name.read(),
+            0x9 => self.authors.read(),
+            0xa => 0x00,
             0xb => 0x00,
             0xc => 0x00,
             0xd => 0x00,
-            0xe => 0x00,
-            0xf => 0x00,
+            0xe => read_h!(self.devices),
+            0xf => read_l!(self.devices),
             _ => unreachable!(),
         }
     }
 
     fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
         match port {
-            0x0 => self.name.pointer = 0,
-            0x1 => self.authors.pointer = 0,
+            0x0 =>   write_h!(self.wakers, value),
+            0x1 => { write_l!(self.wakers, value);
+                     self.asleep = true;
+                     return Some(Signal::Sleep); },
             0x2 => (),
-            0x3 => (),
+            0x3 => return Some(Signal::Fork),
             0x4 => (),
             0x5 => (),
             0x6 => (),
             0x7 => (),
-            0x8 => write_h!(self.can_wake, value),
-            0x9 => {
-                write_l!(self.can_wake, value);
-                self.asleep = true;
-                return Some(Signal::Sleep);
-            },
+            0x8 => self.name.pointer = 0,
+            0x9 => self.authors.pointer = 0,
             0xa => (),
-            0xb => return Some(Signal::Fork),
+            0xb => (),
             0xc => (),
             0xd => (),
             0xe => (),
@@ -81,31 +74,41 @@ impl Device for SystemDevice {
     }
 
     fn wake(&mut self) -> bool {
-        true
+        false
     }
 }
 
 
-pub struct ReadBuffer {
-    pub bytes: Vec<u8>,
-    pub pointer: usize,
-}
-
-impl ReadBuffer {
-    pub fn from_str(text: &str) -> Self {
+impl SystemDevice {
+    pub fn new(devices: u16) -> Self {
         Self {
-            bytes: text.bytes().collect(),
-            pointer: 0,
+            name: get_name(),
+            authors: get_authors(),
+            wakers: 0,
+            waker: 0,
+            asleep: false,
+            devices,
+            custom1: ReadBuffer::new(),
+            custom2: ReadBuffer::new(),
+            custom3: ReadBuffer::new(),
+            custom4: ReadBuffer::new(),
         }
     }
+}
 
-    pub fn read(&mut self) -> u8 {
-        let pointer = self.pointer;
-        self.pointer += 1;
-        match self.bytes.get(pointer) {
-            Some(byte) => *byte,
-            None => 0,
-        }
-    }
+
+fn get_name() -> ReadBuffer {
+    let pkg_version = env!("CARGO_PKG_VERSION");
+    let pkg_name = env!("CARGO_PKG_NAME");
+    ReadBuffer::from_str(&format!("{pkg_name}/{pkg_version}"))
 }
 
+fn get_authors() -> ReadBuffer {
+    let pkg_authors = env!("CARGO_PKG_AUTHORS");
+    let mut authors_string = String::new();
+    for author in pkg_authors.split(':') {
+        authors_string.push_str(author);
+        authors_string.push('\n');
+    }
+    ReadBuffer::from_str(&authors_string)
+}
diff --git a/src/emulators.rs b/src/emulators.rs
deleted file mode 100644
index 56f7181..0000000
--- a/src/emulators.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-mod headless_emulator;
-mod graphical_emulator;
-
-pub use headless_emulator::{HeadlessEmulator, HeadlessDeviceBus};
-pub use graphical_emulator::{GraphicalEmulator, GraphicalDeviceBus};
-
-use crate::*;
-
-use phosphor::Colour;
-
-use std::path::PathBuf;
-
-
-pub enum EmulatorSignal {
-    Promote,
-    Halt,
-}
-
-
-pub struct EmulatorConfig {
-    pub dimensions: ScreenDimensions,
-    pub fullscreen: bool,
-    pub scale: u32,
-    pub debug_palette: Option<[Colour; 16]>,
-    pub show_cursor: bool,
-
-    pub initial_transmission: Option<Vec<u8>>,
-    pub decode_stdin: bool,
-    pub encode_stdout: bool,
-
-    pub symbols_path: Option<PathBuf>,
-}
diff --git a/src/emulators/dynamic_emulator.rs b/src/emulators/dynamic_emulator.rs
new file mode 100644
index 0000000..b5126d0
--- /dev/null
+++ b/src/emulators/dynamic_emulator.rs
@@ -0,0 +1,170 @@
+use crate::*;
+
+
+pub struct DynamicEmulator {
+    pub br: BedrockEmulator<DynamicDeviceBus>,
+    pub debug: DebugState,
+}
+
+
+impl DynamicEmulator {
+    pub fn new(config: &EmulatorConfig, debug: bool) -> Self {
+        Self {
+            br: BedrockEmulator::new(DynamicDeviceBus::new(config)),
+            debug: DebugState::new(debug, config.symbols_path.as_ref()),
+        }
+    }
+
+    pub fn load_program(&mut self, bytecode: &[u8]) {
+        self.br.core.mem.load_program(bytecode);
+    }
+
+    pub fn run(&mut self) {
+        loop {
+            match self.br.evaluate(BATCH_SIZE, self.debug.enabled) {
+                Some(Signal::Fork) => {
+                    self.br.core.mem.pc = 0;
+                    self.br.core.wst.sp = 0;
+                    self.br.core.rst.sp = 0;
+                }
+                Some(Signal::Sleep) => loop {
+                    if self.br.dev.wake() { break; }
+                    std::thread::sleep(MIN_TICK_DURATION);
+                }
+                Some(Signal::Halt) => {
+                    self.br.dev.stream.flush();
+                    info!("Program halted, exiting.");
+                    self.debug.debug_summary(&self.br.core);
+                    std::process::exit(0);
+                }
+                Some(Signal::Debug(Debug::Debug1)) => {
+                    self.debug.debug_summary(&self.br.core);
+                }
+                _ => (),
+            }
+            if self.br.dev.input.accessed || self.br.dev.screen.accessed {
+                return;
+            }
+        }
+    }
+
+    pub fn to_graphical(self, config: EmulatorConfig) -> GraphicalEmulator {
+        GraphicalEmulator {
+            br: BedrockEmulator {
+                core: self.br.core,
+                dev: GraphicalDeviceBus {
+                    system: self.br.dev.system,
+                    memory: self.br.dev.memory,
+                    math:   self.br.dev.math,
+                    clock:  self.br.dev.clock,
+                    input:  self.br.dev.input,
+                    screen: self.br.dev.screen,
+                    stream: self.br.dev.stream,
+                    file:   self.br.dev.file,
+                }
+            },
+            debug: self.debug,
+
+            fullscreen: config.fullscreen,
+            scale: config.zoom.into(),
+            show_override_palette: config.override_palette.is_some(),
+            render_mark: Instant::now(),
+
+            config,
+        }
+    }
+
+    pub fn to_headless(self) -> HeadlessEmulator {
+        HeadlessEmulator {
+            br: BedrockEmulator {
+                core: self.br.core,
+                dev: HeadlessDeviceBus {
+                    system: self.br.dev.system,
+                    memory: self.br.dev.memory,
+                    math:   self.br.dev.math,
+                    clock:  self.br.dev.clock,
+                    stream: self.br.dev.stream,
+                    file:   self.br.dev.file,
+                }
+            },
+            debug: self.debug,
+        }
+    }
+}
+
+
+pub struct DynamicDeviceBus {
+    pub system: SystemDevice,
+    pub memory: MemoryDevice,
+    pub math:   MathDevice,
+    pub clock:  ClockDevice,
+    pub input:  InputDevice,
+    pub screen: ScreenDevice,
+    pub stream: StreamDevice,
+    pub file:   FileDevice,
+}
+
+impl DynamicDeviceBus {
+    pub fn new(config: &EmulatorConfig) -> Self {
+        Self {
+            system: SystemDevice::new(0b1111_1100_1010_0000),
+            memory: MemoryDevice::new(),
+            math:   MathDevice::new(),
+            clock:  ClockDevice::new(),
+            input:  InputDevice::new(),
+            screen: ScreenDevice::new(&config),
+            stream: StreamDevice::new(&config),
+            file:   FileDevice::new(),
+        }
+    }
+}
+
+
+impl DeviceBus for DynamicDeviceBus {
+    fn read(&mut self, port: u8) -> u8 {
+        match port & 0xf0 {
+            0x00 => self.system.read(port & 0x0f),
+            0x10 => self.memory.read(port & 0x0f),
+            0x20 => self.math  .read(port & 0x0f),
+            0x30 => self.clock .read(port & 0x0f),
+            0x40 => self.input .read(port & 0x0f),
+            0x50 => self.screen.read(port & 0x0f),
+            0x80 => self.stream.read(port & 0x0f),
+            0xa0 => self.file  .read(port & 0x0f),
+            _ => 0
+        }
+    }
+
+    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
+        match port & 0xf0 {
+            0x00 =>   self.system.write(port & 0x0f, value),
+            0x10 =>   self.memory.write(port & 0x0f, value),
+            0x20 =>   self.math  .write(port & 0x0f, value),
+            0x30 =>   self.clock .write(port & 0x0f, value),
+            0x40 => { self.input .write(port & 0x0f, value); Some(Signal::Interrupt) }
+            0x50 => { self.screen.write(port & 0x0f, value); Some(Signal::Interrupt) }
+            0x80 =>   self.stream.write(port & 0x0f, value),
+            0xa0 =>   self.file  .write(port & 0x0f, value),
+            _ => None
+        }
+    }
+
+    fn wake(&mut self) -> bool {
+        macro_rules! rouse {
+            ($id:expr, $dev:ident) => {
+                let is_eligible = test_bit!(self.system.wakers, 0x8000 >> $id);
+                if is_eligible && self.$dev.wake() {
+                    self.system.waker = $id;
+                    self.system.asleep = false;
+                    return true;
+                }
+            };
+        }
+        rouse!(0xa, file  );
+        rouse!(0x8, stream);
+        rouse!(0x5, screen);
+        rouse!(0x4, input );
+        rouse!(0x3, clock );
+        return false;
+    }
+}
diff --git a/src/emulators/graphical_emulator.rs b/src/emulators/graphical_emulator.rs
index 1c58a34..c4b06c8 100644
--- a/src/emulators/graphical_emulator.rs
+++ b/src/emulators/graphical_emulator.rs
@@ -1,128 +1,30 @@
 use crate::*;
-use bedrock_core::*;
-
-use phosphor::*;
-
-use std::time::Instant;
-
-
-pub struct GraphicalDeviceBus {
-    pub sys: SystemDevice,
-    pub mem: MemoryDevice,
-    pub mat: MathDevice,
-    pub clk: ClockDevice,
-    pub inp: InputDevice,
-    pub scr: ScreenDevice,
-    pub loc: LocalDevice,
-    pub rem: RemoteDevice,
-    pub fs1: FileDevice,
-    pub fs2: FileDevice,
-}
-
-impl GraphicalDeviceBus {
-    pub fn new(config: &EmulatorConfig) -> Self {
-        Self {
-            sys: SystemDevice::new(),
-            mem: MemoryDevice::new(),
-            mat: MathDevice::new(),
-            clk: ClockDevice::new(),
-            inp: InputDevice::new(),
-            scr: ScreenDevice::new(config),
-            loc: LocalDevice::new(config),
-            rem: RemoteDevice::new(),
-            fs1: FileDevice::new(),
-            fs2: FileDevice::new(),
-        }
-    }
-
-    pub fn graphical(&self) -> bool {
-        self.inp.accessed || self.scr.accessed
-    }
-}
-
-impl DeviceBus for GraphicalDeviceBus {
-    fn read(&mut self, port: u8) -> u8 {
-        match port & 0xf0 {
-            0x00 => self.sys.read(port & 0x0f),
-            0x10 => self.mem.read(port & 0x0f),
-            0x20 => self.mat.read(port & 0x0f),
-            0x30 => self.clk.read(port & 0x0f),
-            0x40 => self.inp.read(port & 0x0f),
-            0x50 => self.scr.read(port & 0x0f),
-            0x80 => self.loc.read(port & 0x0f),
-            0x90 => self.rem.read(port & 0x0f),
-            0xa0 => self.fs1.read(port & 0x0f),
-            0xb0 => self.fs2.read(port & 0x0f),
-            _ => 0
-        }
-    }
-
-    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
-        match port & 0xf0 {
-            0x00 => self.sys.write(port & 0x0f, value),
-            0x10 => self.mem.write(port & 0x0f, value),
-            0x20 => self.mat.write(port & 0x0f, value),
-            0x30 => self.clk.write(port & 0x0f, value),
-            0x40 => self.inp.write(port & 0x0f, value),
-            0x50 => self.scr.write(port & 0x0f, value),
-            0x80 => self.loc.write(port & 0x0f, value),
-            0x90 => self.rem.write(port & 0x0f, value),
-            0xa0 => self.fs1.write(port & 0x0f, value),
-            0xb0 => self.fs2.write(port & 0x0f, value),
-            _ => None
-        }
-    }
-
-    fn wake(&mut self) -> bool {
-        macro_rules! rouse {
-            ($id:expr, $dev:ident) => {
-                if self.sys.can_wake($id) && self.$dev.wake() {
-                    self.sys.wake_id = $id;
-                    self.sys.asleep = false;
-                    return true;
-                }
-            };
-        }
-        rouse!(0xb, fs2);
-        rouse!(0xa, fs1);
-        rouse!(0x9, rem);
-        rouse!(0x8, loc);
-        rouse!(0x5, scr);
-        rouse!(0x4, inp);
-        rouse!(0x3, clk);
-        rouse!(0x2, mat);
-        rouse!(0x1, mem);
-        rouse!(0x0, sys);
-        return false;
-    }
-}
 
 
 pub struct GraphicalEmulator {
     pub br: BedrockEmulator<GraphicalDeviceBus>,
     pub debug: DebugState,
-    pub dimensions: ScreenDimensions,
+
     pub fullscreen: bool,
     pub scale: u32,
     pub render_mark: Instant,
-    pub debug_palette: Option<[Colour; 16]>,
-    pub show_debug_palette: bool,
-    pub show_cursor: bool,
+    pub show_override_palette: bool,
+
+    pub config: EmulatorConfig,
 }
 
 impl GraphicalEmulator {
-    pub fn new(config: &EmulatorConfig, debug: bool) -> Self {
-        let devices = GraphicalDeviceBus::new(config);
+    pub fn new(config: EmulatorConfig, debug: bool) -> Self {
         Self {
-            br: BedrockEmulator::new(devices),
+            br: BedrockEmulator::new(GraphicalDeviceBus::new(&config)),
             debug: DebugState::new(debug, config.symbols_path.as_ref()),
-            dimensions: config.dimensions,
+
             fullscreen: config.fullscreen,
-            scale: config.scale,
+            scale: config.zoom.into(),
+            show_override_palette: config.override_palette.is_some(),
             render_mark: Instant::now(),
-            debug_palette: config.debug_palette,
-            show_debug_palette: config.debug_palette.is_some(),
-            show_cursor: config.show_cursor,
+
+            config,
         }
     }
 
@@ -130,60 +32,47 @@ impl GraphicalEmulator {
         self.br.core.mem.load_program(bytecode);
     }
 
-    pub fn run(&mut self) -> EmulatorSignal {
-        loop {
-            match self.br.evaluate(BATCH_SIZE, self.debug.enabled) {
-                Some(Signal::Fork) => {
-                    self.br.core.mem.pc = 0;
-                    self.br.core.wst.sp = 0;
-                    self.br.core.rst.sp = 0;
-                }
-                Some(Signal::Sleep) => loop {
-                    if self.br.dev.graphical() {
-                        return EmulatorSignal::Promote;
-                    }
-                    if self.br.dev.wake() { break; }
-                    std::thread::sleep(MIN_TICK_DURATION);
-                }
-                Some(Signal::Halt) => {
-                    self.br.dev.loc.flush();
-                    log::info!("Program halted, exiting.");
-                    self.debug.debug_summary(&self.br.core);
-                    return EmulatorSignal::Halt;
-                }
-                Some(Signal::Debug1) => {
-                    self.debug.debug_summary(&self.br.core);
-                }
-                _ => (),
-            }
+    pub fn run(self, mut phosphor: Phosphor) {
+        let cursor = match self.config.show_cursor {
+            true => Some(CursorIcon::Default),
+            false => None,
+        };
 
-            if self.br.dev.graphical() {
-                return EmulatorSignal::Promote;
+        let program_name = match &self.config.metadata {
+            Some(metadata) => match &metadata.name {
+                Some(name) => name.to_string(),
+                None => String::from("Bedrock"),
             }
-        }
+            None => String::from("Bedrock"),
+        };
+        let window = WindowBuilder {
+            dimensions: Some(self.dimensions()),
+            size_bounds: Some(self.size_bounds()),
+            fullscreen: self.fullscreen,
+            scale: self.scale,
+            title: Some(program_name),
+            cursor,
+            icon: parse_icon_from_config(&self.config),
+            program: Box::new(self),
+        };
+
+        phosphor.create_window(window);
+        phosphor.run().unwrap();
     }
 
     pub fn size_bounds(&self) -> SizeBounds {
-        macro_rules! to_u32 {
-            ($opt:expr) => {
-                match $opt {
-                    Some(a) => Some(u32::from(a)),
-                    None => None,
-                }
-            };
-        }
         match self.fullscreen {
             true => SizeBounds {
-                min_width: None,
-                max_width: None,
+                min_width:  None,
+                max_width:  None,
                 min_height: None,
                 max_height: None,
             },
             false => SizeBounds {
-                min_width: to_u32!(self.br.dev.scr.fixed_width),
-                max_width: to_u32!(self.br.dev.scr.fixed_width),
-                min_height: to_u32!(self.br.dev.scr.fixed_height),
-                max_height: to_u32!(self.br.dev.scr.fixed_height),
+                min_width:  self.br.dev.screen.fixed_width.map(u32::from),
+                max_width:  self.br.dev.screen.fixed_width.map(u32::from),
+                min_height: self.br.dev.screen.fixed_height.map(u32::from),
+                max_height: self.br.dev.screen.fixed_height.map(u32::from),
             },
 
         }
@@ -191,44 +80,121 @@ impl GraphicalEmulator {
 
     pub fn dimensions(&self) -> Dimensions {
         Dimensions {
-            width: u32::from(self.br.dev.scr.dimensions.width),
-            height: u32::from(self.br.dev.scr.dimensions.height),
+            width: self.br.dev.screen.dimensions.width.into(),
+            height: self.br.dev.screen.dimensions.height.into(),
+        }
+    }
+}
+
+
+pub struct GraphicalDeviceBus {
+    pub system: SystemDevice,
+    pub memory: MemoryDevice,
+    pub math:   MathDevice,
+    pub clock:  ClockDevice,
+    pub input:  InputDevice,
+    pub screen: ScreenDevice,
+    pub stream: StreamDevice,
+    pub file:   FileDevice,
+}
+
+impl GraphicalDeviceBus {
+    pub fn new(config: &EmulatorConfig) -> Self {
+        Self {
+            system: SystemDevice::new(0b1111_1100_1010_0000),
+            memory: MemoryDevice::new(),
+            math:   MathDevice::new(),
+            clock:  ClockDevice::new(),
+            input:  InputDevice::new(),
+            screen: ScreenDevice::new(&config),
+            stream: StreamDevice::new(&config),
+            file:   FileDevice::new(),
         }
     }
 }
 
 
+impl DeviceBus for GraphicalDeviceBus {
+    fn read(&mut self, port: u8) -> u8 {
+        match port & 0xf0 {
+            0x00 => self.system.read(port & 0x0f),
+            0x10 => self.memory.read(port & 0x0f),
+            0x20 => self.math  .read(port & 0x0f),
+            0x30 => self.clock .read(port & 0x0f),
+            0x40 => self.input .read(port & 0x0f),
+            0x50 => self.screen.read(port & 0x0f),
+            0x80 => self.stream.read(port & 0x0f),
+            0xa0 => self.file  .read(port & 0x0f),
+            _ => 0
+        }
+    }
+
+    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
+        match port & 0xf0 {
+            0x00 => self.system.write(port & 0x0f, value),
+            0x10 => self.memory.write(port & 0x0f, value),
+            0x20 => self.math  .write(port & 0x0f, value),
+            0x30 => self.clock .write(port & 0x0f, value),
+            0x40 => self.input .write(port & 0x0f, value),
+            0x50 => self.screen.write(port & 0x0f, value),
+            0x80 => self.stream.write(port & 0x0f, value),
+            0xa0 => self.file  .write(port & 0x0f, value),
+            _ => None
+        }
+    }
+
+    fn wake(&mut self) -> bool {
+        macro_rules! rouse {
+            ($id:expr, $dev:ident) => {
+                let is_eligible = test_bit!(self.system.wakers, 0x8000 >> $id);
+                if is_eligible && self.$dev.wake() {
+                    self.system.waker = $id;
+                    self.system.asleep = false;
+                    return true;
+                }
+            };
+        }
+        rouse!(0xa, file  );
+        rouse!(0x8, stream);
+        rouse!(0x5, screen);
+        rouse!(0x4, input );
+        rouse!(0x3, clock );
+        return false;
+    }
+}
+
+
 impl WindowProgram for GraphicalEmulator {
     fn handle_event(&mut self, event: Event, r: &mut EventWriter<Request>) {
         match event {
             Event::CloseRequest => r.write(Request::CloseWindow),
-            Event::CursorEnter => self.br.dev.inp.on_cursor_enter(),
-            Event::CursorExit => self.br.dev.inp.on_cursor_exit(),
-            Event::CursorMove(p) => self.br.dev.inp.on_cursor_move(p),
-            Event::Resize(d) => self.br.dev.scr.resize(d),
-            Event::CharacterInput(c) => self.br.dev.inp.on_character(c),
-            Event::ModifierChange(m) => self.br.dev.inp.on_modifier(m),
+            Event::CursorEnter => self.br.dev.input.on_cursor_enter(),
+            Event::CursorExit => self.br.dev.input.on_cursor_exit(),
+            Event::CursorMove(p) => self.br.dev.input.on_cursor_move(p),
+            Event::Resize(d) => self.br.dev.screen.resize(d),
+            Event::CharacterInput(c) => self.br.dev.input.on_character(c),
+            Event::ModifierChange(m) => self.br.dev.input.on_modifier(m),
             Event::MouseButton { button, action } =>
-                self.br.dev.inp.on_mouse_button(button, action),
+                self.br.dev.input.on_mouse_button(button, action),
             Event::FocusChange(_) => (),
             Event::Initialise => (),
             Event::ScrollLines { axis, distance } => match axis {
-                Axis::Horizontal => self.br.dev.inp.on_horizontal_scroll(distance),
-                Axis::Vertical   => self.br.dev.inp.on_vertical_scroll(distance),
+                Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance),
+                Axis::Vertical   => self.br.dev.input.on_vertical_scroll(distance),
             }
             Event::ScrollPixels { axis, distance } => match axis {
-                Axis::Horizontal => self.br.dev.inp.on_horizontal_scroll(distance / 20.0),
-                Axis::Vertical   => self.br.dev.inp.on_vertical_scroll(distance / 20.0),
+                Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance / 20.0),
+                Axis::Vertical   => self.br.dev.input.on_vertical_scroll(distance / 20.0),
             }
             Event::FileDrop(_path) => todo!("FileDrop"),
 
             Event::Close => (),
             Event::KeyboardInput { key, action } => {
-                self.br.dev.inp.on_keypress(key, action);
+                self.br.dev.input.on_keypress(key, action);
                 if action == Action::Pressed {
                     match key {
                         KeyCode::F2 => {
-                            self.show_debug_palette = !self.show_debug_palette;
+                            self.show_override_palette = !self.show_override_palette;
                             r.write(Request::Redraw);
                         },
                         KeyCode::F5 => {
@@ -251,12 +217,12 @@ impl WindowProgram for GraphicalEmulator {
     }
 
     fn process(&mut self, requests: &mut EventWriter<Request>) {
-        self.br.dev.loc.flush();
+        self.br.dev.stream.flush();
 
-        if self.br.dev.sys.asleep {
+        if self.br.dev.system.asleep {
             // Stay asleep if there are no pending wake events.
             if !self.br.dev.wake() {
-                if self.br.dev.scr.dirty {
+                if self.br.dev.screen.dirty {
                     requests.write(Request::Redraw);
                 }
                 std::thread::sleep(MIN_TICK_DURATION);
@@ -264,7 +230,7 @@ impl WindowProgram for GraphicalEmulator {
             }
 
             // Wait for the current frame to be rendered.
-            if self.br.dev.scr.dirty {
+            if self.br.dev.screen.dirty {
                 if self.render_mark.elapsed() > MIN_FRAME_DURATION {
                     requests.write(Request::Redraw);
                 }
@@ -281,30 +247,30 @@ impl WindowProgram for GraphicalEmulator {
                     todo!("Fork")
                 }
                 Some(Signal::Sleep) => {
-                    self.br.dev.sys.asleep = true;
+                    self.br.dev.system.asleep = true;
                     break;
                 }
                 Some(Signal::Halt) => {
-                    self.br.dev.loc.flush();
+                    self.br.dev.stream.flush();
                     log::info!("Program halted, exiting.");
                     self.debug.debug_summary(&self.br.core);
                     requests.write(Request::CloseWindow);
                     break;
                 }
-                Some(Signal::Debug1) => {
+                Some(Signal::Debug(Debug::Debug1)) => {
                     self.debug.debug_summary(&self.br.core);
                 }
                 _ => (),
             }
         }
 
-        if std::mem::take(&mut self.br.dev.scr.dirty_dimensions) {
+        if std::mem::take(&mut self.br.dev.screen.dirty_dimensions) {
             requests.write(Request::SetSizeBounds(self.size_bounds()));
         }
 
-        if self.br.dev.scr.dirty {
+        if self.br.dev.screen.dirty {
             let elapsed = self.render_mark.elapsed();
-            if self.br.dev.sys.asleep && elapsed > MIN_FRAME_DURATION {
+            if self.br.dev.system.asleep && elapsed > MIN_FRAME_DURATION {
                 requests.write(Request::Redraw);
             } else if elapsed > MAX_FRAME_DURATION {
                 requests.write(Request::Redraw);
@@ -315,15 +281,15 @@ impl WindowProgram for GraphicalEmulator {
     }
 
     fn render(&mut self, buffer: &mut Buffer, _full: bool) {
-        let screen = &mut self.br.dev.scr;
+        let screen = &mut self.br.dev.screen;
 
         // Generate table for calculating pixel colours from layer values.
         // A given screen pixel will be rendered as the colour given by
         // table[fg][bg], where fg and bg are the corresponding layer values.
         let mut table = [Colour::BLACK; 256];
-        let palette = match self.debug_palette {
-            Some(debug_palette) => match self.show_debug_palette {
-                true => debug_palette,
+        let palette = match self.config.override_palette {
+            Some(override_palette) => match self.show_override_palette {
+                true => override_palette,
                 false => screen.palette,
             }
             None => screen.palette,
@@ -376,3 +342,38 @@ impl WindowProgram for GraphicalEmulator {
         self.render_mark = Instant::now();
     }
 }
+
+
+fn parse_icon_from_config(config: &EmulatorConfig) -> Option<Icon> {
+    let metadata = config.metadata.as_ref()?;
+    let bytes = metadata.small_icon.as_ref()?;
+    let bg = metadata.bg_colour.unwrap_or(Colour::BLACK);
+    let fg = metadata.bg_colour.unwrap_or(Colour::WHITE);
+    let rgba = sprite_icon_to_rgb(bytes, 3, bg, fg);
+    match Icon::from_rgba(rgba, 24, 24) {
+        Ok(icon) => Some(icon),
+        Err(err) => unreachable!("Error while parsing small icon data: {err}"),
+    }
+}
+
+fn sprite_icon_to_rgb(bytes: &[u8], size: usize, bg: Colour, fg: Colour) -> Vec<u8> {
+    let sprites: Vec<&[u8]> = bytes.chunks_exact(8).collect();
+    let mut rgba = Vec::new();
+    for sprite_row in 0..size {
+        for pixel_row in 0..8 {
+            for sprite_column in 0..size {
+                let sprite = &sprites[sprite_column + (sprite_row * size)];
+                let row = &sprite[pixel_row];
+                for bit in 0..8 {
+                    let state = row & (0x80 >> bit);
+                    let colour = match state != 0 {
+                        true => fg,
+                        false => bg,
+                    };
+                    rgba.extend_from_slice(&colour.as_rgba_array());
+                }
+            }
+        }
+    }
+    return rgba;
+}
diff --git a/src/emulators/headless_emulator.rs b/src/emulators/headless_emulator.rs
index 9207b3d..b07b06e 100644
--- a/src/emulators/headless_emulator.rs
+++ b/src/emulators/headless_emulator.rs
@@ -1,83 +1,4 @@
 use crate::*;
-use bedrock_core::*;
-
-
-pub struct HeadlessDeviceBus {
-    pub sys: SystemDevice,
-    pub mem: MemoryDevice,
-    pub mat: MathDevice,
-    pub clk: ClockDevice,
-    pub loc: LocalDevice,
-    pub rem: RemoteDevice,
-    pub fs1: FileDevice,
-    pub fs2: FileDevice,
-}
-
-impl HeadlessDeviceBus {
-    pub fn new(config: &EmulatorConfig) -> Self {
-        Self {
-            sys: SystemDevice::new(),
-            mem: MemoryDevice::new(),
-            mat: MathDevice::new(),
-            clk: ClockDevice::new(),
-            loc: LocalDevice::new(config),
-            rem: RemoteDevice::new(),
-            fs1: FileDevice::new(),
-            fs2: FileDevice::new(),
-        }
-    }
-}
-
-impl DeviceBus for HeadlessDeviceBus {
-    fn read(&mut self, port: u8) -> u8 {
-        match port & 0xf0 {
-            0x00 => self.sys.read(port & 0x0f),
-            0x10 => self.mem.read(port & 0x0f),
-            0x20 => self.mat.read(port & 0x0f),
-            0x30 => self.clk.read(port & 0x0f),
-            0x80 => self.loc.read(port & 0x0f),
-            0x90 => self.rem.read(port & 0x0f),
-            0xa0 => self.fs1.read(port & 0x0f),
-            0xb0 => self.fs2.read(port & 0x0f),
-            _ => 0
-        }
-    }
-
-    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
-        match port & 0xf0 {
-            0x00 => self.sys.write(port & 0x0f, value),
-            0x10 => self.mem.write(port & 0x0f, value),
-            0x20 => self.mat.write(port & 0x0f, value),
-            0x30 => self.clk.write(port & 0x0f, value),
-            0x80 => self.loc.write(port & 0x0f, value),
-            0x90 => self.rem.write(port & 0x0f, value),
-            0xa0 => self.fs1.write(port & 0x0f, value),
-            0xb0 => self.fs2.write(port & 0x0f, value),
-            _ => None
-        }
-    }
-
-    fn wake(&mut self) -> bool {
-        macro_rules! rouse {
-            ($id:expr, $dev:ident) => {
-                if self.sys.can_wake($id) && self.$dev.wake() {
-                    self.sys.wake_id = $id;
-                    self.sys.asleep = false;
-                    return true;
-                }
-            };
-        }
-        rouse!(0xb, fs2);
-        rouse!(0xa, fs1);
-        rouse!(0x9, rem);
-        rouse!(0x8, loc);
-        rouse!(0x3, clk);
-        rouse!(0x2, mat);
-        rouse!(0x1, mem);
-        rouse!(0x0, sys);
-        return false;
-    }
-}
 
 
 pub struct HeadlessEmulator {
@@ -85,6 +6,7 @@ pub struct HeadlessEmulator {
     pub debug: DebugState,
 }
 
+
 impl HeadlessEmulator {
     pub fn new(config: &EmulatorConfig, debug: bool) -> Self {
         Self {
@@ -97,7 +19,7 @@ impl HeadlessEmulator {
         self.br.core.mem.load_program(bytecode);
     }
 
-    pub fn run(&mut self, debug: bool) -> EmulatorSignal {
+    pub fn run(&mut self) -> ! {
         loop {
             match self.br.evaluate(BATCH_SIZE, self.debug.enabled) {
                 Some(Signal::Fork) => {
@@ -110,12 +32,12 @@ impl HeadlessEmulator {
                     std::thread::sleep(MIN_TICK_DURATION);
                 }
                 Some(Signal::Halt) => {
-                    self.br.dev.loc.flush();
-                    log::info!("Program halted, exiting.");
+                    self.br.dev.stream.flush();
+                    info!("Program halted, exiting.");
                     self.debug.debug_summary(&self.br.core);
-                    return EmulatorSignal::Halt;
+                    std::process::exit(0);
                 }
-                Some(Signal::Debug1) => if debug {
+                Some(Signal::Debug(Debug::Debug1)) => {
                     self.debug.debug_summary(&self.br.core);
                 }
                 _ => (),
@@ -123,3 +45,70 @@ impl HeadlessEmulator {
         }
     }
 }
+
+
+pub struct HeadlessDeviceBus {
+    pub system: SystemDevice,
+    pub memory: MemoryDevice,
+    pub math:   MathDevice,
+    pub clock:  ClockDevice,
+    pub stream: StreamDevice,
+    pub file:   FileDevice,
+}
+
+impl HeadlessDeviceBus {
+    pub fn new(config: &EmulatorConfig) -> Self {
+        Self {
+            system: SystemDevice::new(0b1111_0000_1010_0000),
+            memory: MemoryDevice::new(),
+            math:   MathDevice::new(),
+            clock:  ClockDevice::new(),
+            stream: StreamDevice::new(&config),
+            file:   FileDevice::new(),
+        }
+    }
+}
+
+
+impl DeviceBus for HeadlessDeviceBus {
+    fn read(&mut self, port: u8) -> u8 {
+        match port & 0xf0 {
+            0x00 => self.system.read(port & 0x0f),
+            0x10 => self.memory.read(port & 0x0f),
+            0x20 => self.math  .read(port & 0x0f),
+            0x30 => self.clock .read(port & 0x0f),
+            0x80 => self.stream.read(port & 0x0f),
+            0xa0 => self.file  .read(port & 0x0f),
+            _ => 0
+        }
+    }
+
+    fn write(&mut self, port: u8, value: u8) -> Option<Signal> {
+        match port & 0xf0 {
+            0x00 => self.system.write(port & 0x0f, value),
+            0x10 => self.memory.write(port & 0x0f, value),
+            0x20 => self.math  .write(port & 0x0f, value),
+            0x30 => self.clock .write(port & 0x0f, value),
+            0x80 => self.stream.write(port & 0x0f, value),
+            0xa0 => self.file  .write(port & 0x0f, value),
+            _ => None
+        }
+    }
+
+    fn wake(&mut self) -> bool {
+        macro_rules! rouse {
+            ($id:expr, $dev:ident) => {
+                let is_eligible = test_bit!(self.system.wakers, 0x8000 >> $id);
+                if is_eligible && self.$dev.wake() {
+                    self.system.waker = $id;
+                    self.system.asleep = false;
+                    return true;
+                }
+            };
+        }
+        rouse!(0xa, file  );
+        rouse!(0x8, stream);
+        rouse!(0x3, clock );
+        return false;
+    }
+}
diff --git a/src/emulators/mod.rs b/src/emulators/mod.rs
new file mode 100644
index 0000000..e723862
--- /dev/null
+++ b/src/emulators/mod.rs
@@ -0,0 +1,81 @@
+mod headless_emulator;
+mod graphical_emulator;
+mod dynamic_emulator;
+
+pub use headless_emulator::*;
+pub use graphical_emulator::*;
+pub use dynamic_emulator::*;
+
+use crate::*;
+
+use ansi::*;
+
+
+pub struct EmulatorConfig {
+    pub initial_dimensions: ScreenDimensions,
+    pub fullscreen: bool,
+    pub zoom: NonZeroU32,
+    pub override_palette: Option<[Colour; 16]>,
+    pub show_cursor: bool,
+
+    pub decode_stdin: bool,
+    pub encode_stdout: bool,
+
+    pub symbols_path: Option<PathBuf>,
+    pub metadata: Option<ProgramMetadata>,
+}
+
+
+pub struct DebugState {
+    pub enabled: bool,
+    last_cycle: usize,
+    last_mark: Instant,
+    symbols: DebugSymbols,
+}
+
+impl DebugState {
+    pub fn new<P: AsRef<Path>>(enabled: bool, symbols_path: Option<P>) -> Self {
+        Self {
+            enabled,
+            last_cycle: 0,
+            last_mark: Instant::now(),
+            symbols: DebugSymbols::from_path_opt(symbols_path),
+        }
+    }
+
+    pub fn debug_summary(&mut self, core: &BedrockCore) {
+        if self.enabled {
+            let prev_pc = core.mem.pc.wrapping_sub(1);
+            eprintln!("\n PC: 0x{:04x}   Cycles: {} (+{} in {:.2?})",
+                prev_pc, core.cycle,
+                core.cycle.saturating_sub(self.last_cycle),
+                self.last_mark.elapsed(),
+            );
+            eprint!("WST: ");
+            debug_stack(&core.wst, 0x10);
+            eprint!("RST: ");
+            debug_stack(&core.rst, 0x10);
+            // Print information about the closest symbol.
+            if let Some(symbol) = self.symbols.for_address(prev_pc) {
+                let name = &symbol.name;
+                let address = &symbol.address;
+                eprint!("SYM: {BLUE}@{name}{NORMAL} 0x{address:04x}");
+                if let Some(location) = &symbol.location {
+                    eprint!(" {DIM}{location}{NORMAL}");
+                }
+                eprintln!();
+            }
+        }
+        self.last_cycle = core.cycle;
+        self.last_mark = Instant::now();
+    }
+}
+
+
+fn debug_stack(stack: &Stack, len: usize) {
+    for i in 0..len {
+        if i == stack.sp as usize { eprint!("{YELLOW}"); }
+        eprint!("{:02x} ", stack.mem[i]);
+    }
+    eprintln!("{NORMAL}");
+}
diff --git a/src/lib.rs b/src/lib.rs
index 0df1efb..ac72b98 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,20 +1,25 @@
 #![feature(bigint_helper_methods)]
-#![feature(unchecked_shifts)]
 #![feature(seek_stream_len)]
-#![feature(io_error_more)]
+#![feature(unchecked_shifts)]
 
-mod debug;
 mod devices;
 mod emulators;
-mod metadata;
+mod types;
+mod load_program;
 
-pub use debug::DebugState;
 pub use devices::*;
 pub use emulators::*;
-pub use metadata::*;
+pub use types::*;
+pub use load_program::*;
+
+use bedrock_core::*;
+use log::*;
+use phosphor::*;
 
 use std::num::NonZeroU32;
-use std::time::Duration;
+use std::path::{Path, PathBuf};
+use std::time::{Duration, Instant};
+
 pub const BATCH_SIZE: usize = 1000;
 pub const MIN_TICK_DURATION: Duration = Duration::from_millis( 4 );
 pub const MIN_FRAME_DURATION: Duration = Duration::from_millis( 14 );
diff --git a/src/load_program.rs b/src/load_program.rs
new file mode 100644
index 0000000..0473207
--- /dev/null
+++ b/src/load_program.rs
@@ -0,0 +1,93 @@
+use crate::*;
+
+use std::io::Read;
+
+
+pub struct Program {
+    pub bytecode: Vec<u8>,
+    pub path: Option<PathBuf>,
+}
+
+impl Program {
+    fn path(&self) -> String {
+        match &self.path {
+            Some(path) => path.as_os_str().to_string_lossy().to_string(),
+            None => String::from("<unknown>"),
+        }
+    }
+}
+
+
+/// Load program from path or standard input.
+pub fn load_program(path: Option<&PathBuf>) -> Program {
+    if let Some(path) = path {
+        if let Ok(program) = load_program_from_file(path) {
+            let length = program.bytecode.len();
+            let path = program.path();
+            info!("Loaded program from {path:?} ({length} bytes)");
+            return program;
+        } else if let Some(program) = load_program_from_bedrock_path(path) {
+            let length = program.bytecode.len();
+            let path = program.path();
+            info!("Loaded program from {path:?} ({length} bytes)");
+            return program;
+        } else {
+            fatal!("Could not read program from {path:?}");
+        }
+    } else {
+        info!("Reading program from standard input...");
+        if let Ok(program) = load_program_from_stdin() {
+            let length = program.bytecode.len();
+            info!("Loaded program from standard input ({length} bytes)");
+            return program;
+        } else {
+            fatal!("Could not read program from standard input");
+        }
+    }
+}
+
+/// Attempt to load program from a directory in the BEDROCK_PATH environment variable.
+fn load_program_from_bedrock_path(path: &Path) -> Option<Program> {
+    // Only if the path can be treated as a file name.
+    if path.is_relative() && path.components().count() == 1 {
+        for base_path in std::env::var("BEDROCK_PATH").ok()?.split(':') {
+            let mut base_path = PathBuf::from(base_path);
+            if !base_path.is_absolute() { continue; }
+            base_path.push(path);
+            info!("Attempting to load program from {base_path:?}");
+            if let Ok(program) = load_program_from_file(&base_path) {
+                return Some(program);
+            }
+            if path.extension().is_some() { continue; }
+            base_path.set_extension("br");
+            info!("Attempting to load program from {base_path:?}");
+            if let Ok(program) = load_program_from_file(&base_path) {
+                return Some(program);
+            }
+        }
+    }
+    return None;
+}
+
+/// Attempt to load program from a specific file path.
+fn load_program_from_file(path: &Path) -> Result<Program, std::io::Error> {
+    // Canonicalize paths so that symbolic links to program files resolve to
+    // the real program directory, which could contain a symbols file.
+    let path = match path.canonicalize() {
+        Ok(canonical) => canonical,
+        Err(_) => path.to_path_buf(),
+    };
+    load_program_from_readable_source(std::fs::File::open(&path)?, Some(&path))
+}
+
+/// Attempt to load program from standard input.
+fn load_program_from_stdin() -> Result<Program, std::io::Error> {
+    load_program_from_readable_source(std::io::stdin(), None)
+}
+
+/// Attempt to load program from a source that implements std::io::Read.
+fn load_program_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<Program, std::io::Error> {
+    let mut bytecode = Vec::<u8>::new();
+    source.take(65536).read_to_end(&mut bytecode)?;
+    return Ok(Program { bytecode, path: path.map(|p| p.to_path_buf()) });
+}
diff --git a/src/metadata.rs b/src/metadata.rs
deleted file mode 100644
index 7692434..0000000
--- a/src/metadata.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-use phosphor::Colour;
-
-
-pub fn parse_metadata(bytecode: &[u8]) -> Option<ProgramMetadata> {
-    MetadataParser::from_bytecode(bytecode).parse()
-}
-
-
-struct MetadataParser<'a> {
-    bytecode: &'a [u8],
-}
-
-impl<'a> MetadataParser<'a> {
-    pub fn from_bytecode(bytecode: &'a [u8]) -> Self {
-        Self { bytecode }
-    }
-
-    pub fn parse(self) -> Option<ProgramMetadata> {
-        macro_rules! array {
-            ($len:expr, $slice:expr) => {{
-                let mut array = [0; $len];
-                for i in 0..$len { array[i] = *$slice.get(i).unwrap_or(&0); }
-                array
-            }};
-        }
-
-        if self.range(0x00, 3) != &[0x41, 0x00, 0x20] {
-            return None;
-        }
-
-        let (name, version) = split_name_version(self.string(self.double(0x10)));
-        let authors = self.string(self.double(0x12)).map(|string| {
-            string.lines().map(|line| line.to_string()).collect()
-        });
-
-        Some( ProgramMetadata {
-            bedrock_string: array!(7, self.range(0x03, 7)),
-            program_memory_size: match self.double(0x0a) {
-                0 => 65536, double => double as usize },
-            working_stack_size: match self.byte(0x0c) {
-                0 => 256, byte => byte as usize },
-            return_stack_size: match self.byte(0x0d) {
-                0 => 256, byte => byte as usize },
-            required_devices: self.double(0x0e),
-            name,
-            version,
-            authors,
-            description: self.string(self.double(0x14)),
-            path_buffer: match self.double(0x16) {
-                0 => None, addr => Some(addr as usize) },
-            small_icon: match self.double(0x18) {
-                0 => None, addr => Some(array!(72, self.range(addr, 72))) },
-            large_icon: match self.double(0x1a) {
-                0 => None, addr => Some(array!(512, self.range(addr, 512))) },
-            bg_colour: parse_colour(self.double(0x1c)),
-            fg_colour: parse_colour(self.double(0x1e)),
-        } )
-    }
-
-    fn byte(&self, address: u16) -> u8 {
-        *self.bytecode.get(address as usize).unwrap_or(&0)
-    }
-
-    fn double(&self, address: u16) -> u16 {
-        u16::from_be_bytes([
-            *self.bytecode.get(address as usize).unwrap_or(&0),
-            *self.bytecode.get(address as usize + 1).unwrap_or(&0),
-        ])
-    }
-
-    fn range(&self, address: u16, length: usize) -> &[u8] {
-        let start = address as usize;
-        let end = start + length;
-        self.bytecode.get(start..end).unwrap_or(&[])
-    }
-
-    fn string(&self, address: u16) -> Option<String> {
-        if address == 0 { return None; }
-        let start = address as usize;
-        let mut end = start;
-        while let Some(byte) = self.bytecode.get(end) {
-            match byte { 0 => break, _ => end += 1 }
-        }
-        let range = self.bytecode.get(start..end)?;
-        Some( String::from_utf8_lossy(range).to_string() )
-    }
-}
-
-fn split_name_version(string: Option<String>) -> (Option<String>, Option<String>) {
-    if let Some(string) = string {
-        match string.split_once('/') {
-            Some((left, right)) => (Some(left.to_string()), Some(right.to_string())),
-            None => (Some(string), None),
-        }
-    } else {
-        (None, None)
-    }
-}
-
-fn parse_colour(double: u16) -> Option<Colour> {
-    let i = (double >> 12      ) as usize;
-    let r = (double >>  8 & 0xf) as u8 * 17;
-    let g = (double >>  4 & 0xf) as u8 * 17;
-    let b = (double       & 0xf) as u8 * 17;
-    match i {
-        0 => Some(Colour::from_rgb(r, g, b)),
-        _ => None,
-    }
-}
-
-
-pub struct ProgramMetadata {
-    pub bedrock_string: [u8; 7],
-    pub program_memory_size: usize,
-    pub working_stack_size: usize,
-    pub return_stack_size: usize,
-    pub required_devices: u16,
-    pub name: Option<String>,
-    pub version: Option<String>,
-    pub authors: Option<Vec<String>>,
-    pub description: Option<String>,
-    pub path_buffer: Option<usize>,
-    pub small_icon: Option<[u8; 72]>,
-    pub large_icon: Option<[u8; 512]>,
-    pub bg_colour: Option<Colour>,
-    pub fg_colour: Option<Colour>,
-}
diff --git a/src/types/buffered_file.rs b/src/types/buffered_file.rs
new file mode 100644
index 0000000..5cdf0ea
--- /dev/null
+++ b/src/types/buffered_file.rs
@@ -0,0 +1,143 @@
+use std::fs::File;
+use std::io::{BufReader, BufWriter, Read, Write};
+use std::io::{ErrorKind, Seek, SeekFrom};
+
+
+pub struct BufferedFile {
+    file: AccessMode,
+}
+
+impl BufferedFile {
+    pub fn new(file: File) -> Self {
+        Self {
+            file: AccessMode::Read(BufReader::new(file)),
+        }
+    }
+
+    pub fn close(&mut self) {
+        self.file = AccessMode::None;
+    }
+
+    pub fn read(&mut self) -> u8 {
+        let mut buffer = [0u8; 1];
+
+        let read_result = match &mut self.file {
+            AccessMode::Read(reader) => reader.read_exact(&mut buffer),
+            AccessMode::Write(writer) => {
+                let address = writer.stream_position().unwrap();
+                let file = std::mem::take(&mut self.file).unwrap();
+                let mut reader = BufReader::new(file);
+                reader.seek(SeekFrom::Start(address)).unwrap();
+                let read_result = reader.read_exact(&mut buffer);
+                self.file = AccessMode::Read(reader);
+                read_result
+            }
+            AccessMode::None => unreachable!(),
+        };
+
+        match read_result {
+            Ok(_) => buffer[0],
+            Err(error) => match error.kind() {
+                ErrorKind::UnexpectedEof => 0,
+                _ => { log::error!("BufferedFile::read: {error:?}"); 0 },
+            }
+        }
+    }
+
+    pub fn write(&mut self, byte: u8) {
+        let mut buffer = [byte; 1];
+
+        let write_result = match &mut self.file {
+            AccessMode::Write(writer) => writer.write_all(&mut buffer),
+            AccessMode::Read(reader) => {
+                let address = reader.stream_position().unwrap();
+                let file = std::mem::take(&mut self.file).unwrap();
+                let mut writer = BufWriter::new(file);
+                writer.seek(SeekFrom::Start(address)).unwrap();
+                let write_result = writer.write_all(&mut buffer);
+                self.file = AccessMode::Write(writer);
+                write_result
+            }
+            AccessMode::None => unreachable!(),
+        };
+
+        write_result.unwrap();
+    }
+
+    pub fn pointer(&mut self) -> u32 {
+        let position = match &mut self.file {
+            AccessMode::Read(reader) => reader.stream_position(),
+            AccessMode::Write(writer) => writer.stream_position(),
+            AccessMode::None => unreachable!(),
+        };
+        u32::try_from(position.unwrap()).unwrap_or(u32::MAX)
+    }
+
+    pub fn set_pointer(&mut self, pointer: u32) {
+        let position = SeekFrom::Start(pointer as u64);
+        match &mut self.file {
+            AccessMode::Read(reader) => reader.seek(position).unwrap(),
+            AccessMode::Write(writer) => writer.seek(position).unwrap(),
+            AccessMode::None => unreachable!(),
+        };
+    }
+
+    pub fn length(&mut self) -> u32 {
+        let length = match &mut self.file {
+            AccessMode::Read(reader) => reader.stream_len(),
+            AccessMode::Write(writer) => writer.stream_len(),
+            AccessMode::None => unreachable!(),
+        };
+        u32::try_from(length.unwrap()).unwrap_or(u32::MAX)
+    }
+
+    pub fn set_length(&mut self, length: u32) {
+        match &mut self.file {
+            AccessMode::Read(_) => {
+                let file = std::mem::take(&mut self.file).unwrap();
+                file.set_len(length as u64).unwrap();
+                self.file = AccessMode::Read(BufReader::new(file));
+            }
+            AccessMode::Write(_) => {
+                let file = std::mem::take(&mut self.file).unwrap();
+                file.set_len(length as u64).unwrap();
+                self.file = AccessMode::Read(BufReader::new(file));
+            }
+            AccessMode::None => unreachable!(),
+        };
+    }
+
+    pub fn flush(&mut self) {
+        if let AccessMode::Write(writer) = &mut self.file {
+            let _ = writer.flush();
+        }
+    }
+}
+
+impl Drop for BufferedFile {
+    fn drop(&mut self) {
+        self.flush()
+    }
+}
+
+enum AccessMode {
+    Read(BufReader<File>),
+    Write(BufWriter<File>),
+    None,
+}
+
+impl AccessMode {
+    pub fn unwrap(self) -> File {
+        match self {
+            Self::Read(reader) => reader.into_inner(),
+            Self::Write(writer) => writer.into_inner().unwrap(),
+            Self::None => unreachable!(),
+        }
+    }
+}
+
+impl Default for AccessMode {
+    fn default() -> Self {
+        Self::None
+    }
+}
diff --git a/src/types/debug_symbols.rs b/src/types/debug_symbols.rs
new file mode 100644
index 0000000..f4fc412
--- /dev/null
+++ b/src/types/debug_symbols.rs
@@ -0,0 +1,59 @@
+use crate::*;
+
+
+pub struct DebugSymbols {
+    symbols: Vec<DebugSymbol>
+}
+
+impl DebugSymbols {
+    /// Load debug symbols from a symbols file.
+    pub fn from_path_opt<P: AsRef<Path>>(path: Option<P>) -> Self {
+        let mut symbols = Vec::new();
+        if let Some(path) = path {
+            if let Ok(string) = std::fs::read_to_string(path) {
+                for line in string.lines() {
+                    if let Some(symbol) = DebugSymbol::from_line(line) {
+                        symbols.push(symbol);
+                    }
+                }
+            }
+        }
+        symbols.sort_by_key(|s| s.address);
+        Self { symbols }
+    }
+
+    /// Return the symbol matching a given address.
+    pub fn for_address(&self, address: u16) -> Option<&DebugSymbol> {
+        if self.symbols.is_empty() { return None; }
+        let symbol = match self.symbols.binary_search_by_key(&address, |s| s.address) {
+            Ok(index) => self.symbols.get(index)?,
+            Err(index) => self.symbols.get(index.checked_sub(1)?)?,
+        };
+        Some(&symbol)
+    }
+}
+
+
+pub struct DebugSymbol {
+    pub address: u16,
+    pub name: String,
+    pub location: Option<String>,
+}
+
+impl DebugSymbol {
+    pub fn from_line(line: &str) -> Option<Self> {
+        if let Some((address, line)) = line.split_once(' ') {
+            let address = u16::from_str_radix(address, 16).ok()?;
+            if let Some((name, location)) = line.split_once(' ') {
+                let name = name.to_string();
+                let location = Some(location.to_string());
+                Some( DebugSymbol { address, name, location } )
+            } else {
+                let name = line.to_string();
+                Some( DebugSymbol { address, name, location: None } )
+            }
+        } else {
+            None
+        }
+    }
+}
diff --git a/src/types/directory_listing.rs b/src/types/directory_listing.rs
new file mode 100644
index 0000000..f079217
--- /dev/null
+++ b/src/types/directory_listing.rs
@@ -0,0 +1,120 @@
+use crate::*;
+
+
+pub struct DirectoryListing {
+    children: Vec<BedrockFilePath>,
+    length: u32,
+    selected: Option<u32>,
+    child_path_buffer: BedrockPathBuffer,
+}
+
+
+impl DirectoryListing {
+    pub fn from_path(path: &BedrockFilePath) -> Option<Self> {
+        macro_rules! unres {
+            ($result:expr) => { match $result { Ok(v) => v, Err(_) => continue} };
+        }
+        macro_rules! unopt {
+            ($option:expr) => { match $option { Some(v) => v, None => continue} };
+        }
+
+        #[cfg(target_family = "windows")] {
+            if path.as_path().components().count() == 0 {
+                return Some(Self::construct_virtual_root())
+            }
+        }
+
+        let mut children = Vec::new();
+        if let Ok(dir_listing) = std::fs::read_dir(path.as_path()) {
+            for (i, entry_result) in dir_listing.enumerate() {
+                // Firebreak to prevent emulator from consuming an absurd amount
+                // of memory when opening too large of a directory.
+                if i == (u16::MAX as usize) {
+                    break;
+                }
+
+                let entry = unres!(entry_result);
+                let entry_path = unopt!(BedrockFilePath::from_path(&entry.path(), path.base()));
+                if entry_path.is_hidden() {
+                    continue;
+                }
+
+                children.push(entry_path);
+            }
+        }
+
+        children.sort();
+        let length = u32::try_from(children.len()).ok()?;
+        let selected = None;
+        let child_path_buffer = BedrockPathBuffer::new();
+        Some( Self { children, length, selected, child_path_buffer } )
+    }
+
+    /// Generate entries for a virtual root directory.
+    #[cfg(target_family = "windows")]
+    fn construct_virtual_root() -> Self {
+        let mut children = Vec::new();
+        let base = PathBuf::from("");
+        let drive_bits = unsafe {
+            windows::Win32::Storage::FileSystem::GetLogicalDrives()
+        };
+        for i in 0..26 {
+            if drive_bits & (0x1 << i) != 0 {
+                let letter: char = (b'A' + i).into();
+                let path = PathBuf::from(format!("{letter}:/"));
+                if let Some(drive) = BedrockFilePath::from_path(&path, &base) {
+                    children.push(drive);
+                }
+            }
+        }
+
+        let length = children.len() as u32;
+        let selected = None;
+        let child_path_buffer = BedrockPathBuffer::new();
+        Self { children, length, selected, child_path_buffer }
+    }
+
+    /// Attempts to return a directory child by index.
+    pub fn get(&self, index: u32) -> Option<&BedrockFilePath> {
+        self.children.get(index as usize)
+    }
+
+    pub fn length(&self) -> u32 {
+        self.length
+    }
+
+    /// Returns the index of the selected child, or zero if no child is selected.
+    pub fn selected(&self) -> u32 {
+        self.selected.unwrap_or(0)
+    }
+
+    /// Attempts to select a child by index.
+    pub fn set_selected(&mut self, index: u32) {
+        if let Some(child) = self.get(index) {
+            let buffer = child.as_buffer();
+            self.child_path_buffer.populate(buffer);
+            self.selected = Some(index);
+        } else {
+            self.child_path_buffer.clear();
+            self.selected = None;
+        }
+    }
+
+    pub fn deselect_child(&mut self) {
+        self.child_path_buffer.clear();
+        self.selected = None;
+    }
+
+    pub fn child_path_buffer(&mut self) -> &mut BedrockPathBuffer {
+        &mut self.child_path_buffer
+    }
+
+    pub fn child_type(&self) -> Option<EntryType> {
+        self.selected.and_then(|s| self.get(s).and_then(|i| i.entry_type()))
+    }
+
+    pub fn child_path(&self) -> Option<BedrockFilePath> {
+        self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.clone())))
+    }
+}
+
diff --git a/src/types/entry_type.rs b/src/types/entry_type.rs
new file mode 100644
index 0000000..6a9ac2d
--- /dev/null
+++ b/src/types/entry_type.rs
@@ -0,0 +1,37 @@
+use crate::*;
+
+use std::cmp::Ordering;
+
+
+pub enum Entry {
+    File(BufferedFile),
+    Directory(DirectoryListing),
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum EntryType {
+    File,
+    Directory,
+}
+
+impl PartialOrd for EntryType {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        match (self, other) {
+            (EntryType::Directory, EntryType::Directory) => Some(Ordering::Equal  ),
+            (EntryType::Directory, EntryType::File     ) => Some(Ordering::Less   ),
+            (EntryType::File,      EntryType::Directory) => Some(Ordering::Greater),
+            (EntryType::File,      EntryType::File     ) => Some(Ordering::Equal  ),
+        }
+    }
+}
+
+impl Ord for EntryType {
+    fn cmp(&self, other: &Self) -> Ordering {
+        match (self, other) {
+            (EntryType::Directory, EntryType::Directory) => Ordering::Equal  ,
+            (EntryType::Directory, EntryType::File     ) => Ordering::Less   ,
+            (EntryType::File,      EntryType::Directory) => Ordering::Greater,
+            (EntryType::File,      EntryType::File     ) => Ordering::Equal  ,
+        }
+    }
+}
diff --git a/src/types/file_path.rs b/src/types/file_path.rs
new file mode 100644
index 0000000..7e6dbe8
--- /dev/null
+++ b/src/types/file_path.rs
@@ -0,0 +1,288 @@
+use crate::*;
+
+use std::cmp::Ordering;
+use std::ffi::OsString;
+use std::path::Component;
+
+
+#[derive(Clone)]
+pub struct BedrockFilePath {
+    /// Sandbox directory
+    base: PathBuf,
+    /// Path relative to sandbox directory
+    relative: PathBuf,
+    bytes: Vec<u8>,
+    entry_type: Option<EntryType>,
+}
+
+impl BedrockFilePath {
+    pub fn from_buffer(buffer: [u8; 256], base: &Path) -> Option<Self> {
+        let base = base.to_path_buf();
+        let relative = buffer_to_path(buffer)?;
+        let bytes = path_to_bytes(&relative)?;
+        let entry_type = get_entry_type(base.join(&relative));
+        assert_path_is_safe(&relative, &base)?;
+        Some(Self { base, relative, bytes, entry_type })
+    }
+
+    /// Construct an instance from an absolute path and a prefix of that path.
+    pub fn from_path(path: &Path, base: &Path) -> Option<Self> {
+        let base = base.to_path_buf();
+        let relative = path.strip_prefix(&base).ok()?.to_path_buf();
+        let bytes = path_to_bytes(&relative)?;
+        let entry_type = get_entry_type(base.join(&relative));
+        assert_path_is_safe(&relative, &base)?;
+        Some( Self { base, relative, bytes, entry_type } )
+    }
+
+    /// Get the base path used by this path.
+    pub fn base(&self) -> &Path {
+        &self.base
+    }
+
+    /// Get this path as a Bedrock-style path, which can be passed to
+    /// a Bedrock program.
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.bytes
+    }
+
+    /// Get this path as a byte buffer, from which a CircularPathBuffer
+    /// can be populated.
+    pub fn as_buffer(&self) -> [u8; 256] {
+        let mut buffer: [u8; 256] = [0; 256];
+        buffer[..self.bytes.len()].copy_from_slice(&self.bytes);
+        return buffer;
+    }
+
+    /// Get this path as an absolute operating-system path, which can
+    /// be used to open a file or directory.
+    pub fn as_path(&self) -> PathBuf {
+        self.base.join(&self.relative)
+    }
+
+    /// Get the entry type of this path.
+    pub fn entry_type(&self) -> Option<EntryType> {
+        self.entry_type
+    }
+
+    /// Get a path which represents the parent of this path.
+    pub fn parent(&self) -> Option<Self> {
+        #[cfg(target_family = "unix")] {
+            Self::from_path(self.as_path().parent()?, &self.base)
+        }
+        #[cfg(target_family = "windows")] {
+            if self.base.components().count() != 0 {
+                // Sandboxed path, cannot ascend to a virtual root directory.
+                Self::from_path(self.as_path().parent()?, &self.base)
+            } else {
+                // Unsandboxed path, we can ascend to a virtual root directory.
+                match self.as_path().parent() {
+                    // Ascend to concrete parent directory.
+                    Some(parent) => Self::from_path(parent, &self.base),
+                    // Ascend into a virtual root directory.
+                    None => {
+                        if self.relative.components().count() != 0 {
+                            // Ascend from concrete path to virtual root.
+                            let blank = PathBuf::from("");
+                            BedrockFilePath::from_path(&blank, &blank)
+                        } else {
+                            // Cannot ascend above the virtual root.
+                            None
+                        }
+                    },
+                }
+            }
+        }
+    }
+
+    /// Returns true if the file would be hidden by the default file browser.
+    pub fn is_hidden(&self) -> bool {
+        #[cfg(target_family = "unix")] {
+            if let Some(stem) = self.relative.file_stem() {
+                if let Some(string) = stem.to_str() {
+                    return string.starts_with('.');
+                }
+            }
+        }
+        #[cfg(target_family = "windows")] {
+            use std::os::windows::fs::MetadataExt;
+            // See https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
+            // const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002;
+            // const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: u32 = 0x00002000;
+            if let Ok(metadata) = std::fs::metadata(self.as_path()) {
+                return metadata.file_attributes() & 0x2002 != 0;
+            }
+        }
+        return false;
+    }
+}
+
+
+/// Converts the contents of a CircularPathBuffer to a relative path.
+fn buffer_to_path(bytes: [u8; 256]) -> Option<PathBuf> {
+    // The buffer must be non-empty and slash-prefixed.
+    if bytes[0] != ('/' as u8) {
+        return None;
+    }
+
+    // Find the index of the first null byte.
+    let mut null_i = None;
+    for (i, b) in bytes.iter().enumerate() {
+        if *b == 0x00 {
+            null_i = Some(i);
+            break;
+        }
+    }
+    // Take a slice, excluding the leading slash, up to the trailing null.
+    let slice = &bytes[1..null_i?];
+
+    #[cfg(target_family = "unix")] {
+        use std::os::unix::ffi::OsStringExt;
+        let vec = Vec::from(slice);
+        return Some(OsString::from_vec(vec).into())
+    }
+    #[cfg(target_family = "windows")] {
+        use std::os::windows::ffi::OsStringExt;
+        let mut string = String::from_utf8_lossy(slice).to_string();
+        // Convert drive-current-directory paths to drive-root paths. This is
+        // needed because the paths C: and C:/ point to separate directories,
+        // but trailing forward-slashes are optional in Bedrock.
+        if string.ends_with(':') {
+            string.push('/');
+        }
+        let utf16: Vec<u16> = string.replace(r"/", r"\").encode_utf16().collect();
+        return Some(OsString::from_wide(&utf16).into())
+    }
+}
+
+/// Convert an operating system path to a Bedrock-style byte path.
+///
+/// A byte path contains at most 255 bytes, and is not null-terminated.
+fn path_to_bytes(path: &Path) -> Option<Vec<u8>> {
+    #[cfg(target_family = "unix")]
+    let string = path.as_os_str().to_str()?.to_string();
+    #[cfg(target_family = "windows")]
+    let string = path.as_os_str().to_str()?.replace(r"\", r"/");
+
+    // Remove any trailing forward-slash and add a leading forward-slash.
+    let mut prefixed_string = String::from("/");
+    prefixed_string.push_str(string.trim_end_matches('/'));
+    let slice = prefixed_string.as_bytes();
+
+    // Error if bytes does not fit into a CircularPathBuffer.
+    if slice.len() > 255 { return None; }
+
+    Some(Vec::from(slice))
+}
+
+/// Returns true if a relative path can be safely attached to a base without
+/// breaking out of the sandbox.
+fn assert_path_is_safe(relative: &Path, _base: &Path) -> Option<()> {
+    #[cfg(target_family = "unix")] {
+        // Error if path contains special components.
+        for component in relative.components() {
+            match component {
+                Component::Normal(_) => continue,
+                _ => return None,
+            }
+        }
+    }
+    #[cfg(target_family = "windows")] {
+        // If the base path is empty, the relative path needs to be able to
+        // contain the prefix and root element. If the base path is not
+        // empty, the relative path must not contain these elements else
+        // they will override the base path when joined.
+        if _base.components().count() != 0 {
+            for component in relative.components() {
+                match component {
+                    Component::Normal(_) => continue,
+                    _ => return None,
+                }
+            }
+        }
+    }
+    return Some(());
+}
+
+fn get_entry_type(absolute: PathBuf) -> Option<EntryType> {
+    #[cfg(target_family = "windows")] {
+        // If path is empty, this is a virtual root directory.
+        if absolute.components().count() == 0 {
+            return Some(EntryType::Directory)
+        }
+    }
+    let metadata = std::fs::metadata(absolute).ok()?;
+    if metadata.is_file() {
+        Some(EntryType::File)
+    } else if metadata.is_dir() {
+        Some(EntryType::Directory)
+    } else {
+        None
+    }
+}
+
+impl std::fmt::Debug for BedrockFilePath {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+        self.as_path().fmt(f)
+    }
+}
+
+// ---------------------------------------------------------------------------
+
+impl PartialEq for BedrockFilePath {
+    fn eq(&self, other: &Self) -> bool {
+        self.bytes == other.bytes && self.entry_type == other.entry_type
+    }
+}
+
+impl Eq for BedrockFilePath {}
+
+impl PartialOrd for BedrockFilePath {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for BedrockFilePath {
+    fn cmp(&self, other: &Self) -> Ordering {
+        match self.entry_type.cmp(&other.entry_type) {
+            Ordering::Equal => compare_ascii_slices(&self.bytes, &other.bytes),
+            ordering => ordering,
+        }
+    }
+}
+
+/// Compare two ASCII byte-slices in case-agnostic alphabetic order.
+fn compare_ascii_slices(left: &[u8], right: &[u8]) -> Ordering {
+    let l = std::cmp::min(left.len(), right.len());
+    let lhs = &left[..l];
+    let rhs = &right[..l];
+
+    for i in 0..l {
+        let a = remap_ascii(lhs[i]);
+        let b = remap_ascii(rhs[i]);
+        match a.cmp(&b) {
+            Ordering::Equal => (),
+            non_eq => return non_eq,
+        }
+    }
+
+    left.len().cmp(&right.len())
+}
+
+/// Remap ASCII values so that they sort in case-agnostic alphabetic order:
+///
+/// ```text
+///    !"#$%&'()*+,-./0123456789:;<=>?
+///   @`AaBbCcDdEeFfGgHhIiJjKkLlMmNnOo
+///   PpQqRrSsTtUuVvWwXxYyZz[{\|]}^~_
+/// ```
+fn remap_ascii(c: u8) -> u8 {
+    if 0x40 <= c && c <= 0x5F {
+        (c - 0x40) * 2 + 0x40
+    } else if 0x60 <= c && c <= 0x7F {
+        (c - 0x60) * 2 + 0x41
+    } else {
+        c
+    }
+}
diff --git a/src/types/metadata.rs b/src/types/metadata.rs
new file mode 100644
index 0000000..dde86fa
--- /dev/null
+++ b/src/types/metadata.rs
@@ -0,0 +1,125 @@
+use crate::*;
+
+
+const SMALL_ICON_LEN: usize = 3*3*8;
+const LARGE_ICON_LEN: usize = 8*8*8;
+
+pub fn parse_metadata(bytecode: &[u8]) -> Option<ProgramMetadata> {
+    MetadataParser::from_bytecode(bytecode).parse()
+}
+
+
+pub struct ProgramMetadata {
+    pub name: Option<String>,
+    pub version: Option<String>,
+    pub authors: Option<Vec<String>>,
+    pub description: Option<String>,
+    pub bg_colour: Option<Colour>,
+    pub fg_colour: Option<Colour>,
+    pub small_icon: Option<[u8; SMALL_ICON_LEN]>,
+    pub large_icon: Option<[u8; LARGE_ICON_LEN]>,
+}
+
+
+struct MetadataParser<'a> {
+    bytecode: &'a [u8],
+}
+
+impl<'a> MetadataParser<'a> {
+    pub fn from_bytecode(bytecode: &'a [u8]) -> Self {
+        Self { bytecode }
+    }
+
+    pub fn parse(self) -> Option<ProgramMetadata> {
+        // Verify metadata identifier.
+        let identifier = self.vec(0x00, 10);
+        if identifier != &[0x41,0x00,0x18,0x42,0x45,0x44,0x52,0x4F,0x43,0x4B] {
+            return None;
+        }
+
+        let (name, version) = if let Some(pointer) = self.pointer(0x0a) {
+            let string = self.string(pointer);
+            if let Some((name, version)) = string.split_once('/') {
+                 (Some(name.trim().to_string()), Some(version.trim().to_string()))
+            } else {
+                (Some(string.trim().to_string()), None)
+            }
+        } else {
+            (None, None)
+        };
+
+        let authors = self.pointer(0x0c).map(|p| {
+            self.string(p).lines().map(|s| s.trim().to_string()).collect()
+        });
+
+        let description = self.pointer(0x0e).map(|p| self.string(p));
+        let bg_colour = self.pointer(0x10).map(|p| self.colour(p));
+        let fg_colour = self.pointer(0x12).map(|p| self.colour(p));
+
+        let small_icon = if let Some(pointer) = self.pointer(0x14) {
+            let vec = self.vec(pointer, SMALL_ICON_LEN);
+            Some(vec.try_into().unwrap())
+        } else {
+            None
+        };
+
+        let large_icon = if let Some(pointer) = self.pointer(0x16) {
+            let vec = self.vec(pointer, LARGE_ICON_LEN);
+            Some(vec.try_into().unwrap())
+        } else {
+            None
+        };
+
+        let metadata = ProgramMetadata {
+            name, version, authors, description,
+            bg_colour, fg_colour, small_icon, large_icon,
+        };
+        return Some(metadata);
+    }
+
+    fn byte(&self, address: usize) -> u8 {
+        match self.bytecode.get(address) {
+            Some(byte) => *byte,
+            None => 0,
+        }
+    }
+
+    fn double(&self, address: usize) -> u16 {
+        u16::from_be_bytes([
+            self.byte(address),
+            self.byte(address + 1),
+        ])
+    }
+
+    fn pointer(&self, address: usize) -> Option<usize> {
+        match self.double(address) {
+            0 => None,
+            v => Some(v as usize),
+        }
+    }
+
+    fn vec(&self, address: usize, length: usize) -> Vec<u8> {
+        let mut vec = Vec::new();
+        for i in 0..length {
+            vec.push(self.byte(address + i));
+        }
+        return vec;
+    }
+
+    fn string(&self, address: usize) -> String {
+        let mut i = address;
+        while self.byte(i) != 0 {
+            i += 1;
+        }
+        let slice = &self.bytecode[address..i];
+        String::from_utf8_lossy(slice).to_string()
+    }
+
+    fn colour(&self, address: usize) -> Colour {
+        let double = self.double(address);
+        let r = (double >>  8 & 0xf) as u8 * 17;
+        let g = (double >>  4 & 0xf) as u8 * 17;
+        let b = (double       & 0xf) as u8 * 17;
+        Colour::from_rgb(r, g, b)
+    }
+}
diff --git a/src/types/mod.rs b/src/types/mod.rs
new file mode 100644
index 0000000..730f053
--- /dev/null
+++ b/src/types/mod.rs
@@ -0,0 +1,19 @@
+mod buffered_file;
+mod debug_symbols;
+mod directory_listing;
+mod entry_type;
+mod file_path;
+mod metadata;
+mod path_buffer;
+mod read_buffer;
+mod sprite_buffer;
+
+pub use buffered_file::*;
+pub use debug_symbols::*;
+pub use directory_listing::*;
+pub use entry_type::*;
+pub use file_path::*;
+pub use metadata::*;
+pub use path_buffer::*;
+pub use read_buffer::*;
+pub use sprite_buffer::*;
diff --git a/src/types/path_buffer.rs b/src/types/path_buffer.rs
new file mode 100644
index 0000000..d6a0861
--- /dev/null
+++ b/src/types/path_buffer.rs
@@ -0,0 +1,60 @@
+pub struct BedrockPathBuffer {
+    buffer: [u8; 256],
+    pointer: u8,
+}
+
+impl BedrockPathBuffer {
+    pub fn new() -> Self {
+        Self { buffer: [0; 256] , pointer: 0 }
+    }
+
+    /// Clear the buffer, returning the previous buffer contents.
+    pub fn clear(&mut self) -> [u8; 256] {
+        self.pointer = 0;
+        std::mem::replace(&mut self.buffer, [0; 256])
+    }
+
+    /// Reset the pointer and hot-swap the byte buffer.
+    pub fn populate(&mut self, buffer: [u8; 256]) {
+        self.pointer = 0;
+        self.buffer = buffer;
+    }
+
+    /// Move internal pointer to the start of the path or file name.
+    ///
+    /// If value is non-zero, the pointer will be moved to the byte
+    /// directly following the final forward-slash.
+    pub fn set_pointer(&mut self, value: u8) {
+        self.pointer = 0;
+        // Set the pointer to the start of the filename if value is truthy.
+        if value != 0x00 {
+            for (i, c) in self.buffer.iter().enumerate() {
+                match c {
+                    b'/' => self.pointer = (i as u8).saturating_add(1),
+                    0x00 => break,
+                    _ => continue,
+                }
+            }
+        }
+    }
+
+    /// Read a single byte from the buffer.
+    pub fn read(&mut self) -> u8 {
+        let pointer = self.pointer as usize;
+        self.pointer = self.pointer.wrapping_add(1);
+        self.buffer[pointer]
+    }
+
+    /// Write a single byte to the buffer.
+    ///
+    /// If a null-byte is written, the buffer will be cleared and returned.
+    pub fn write(&mut self, byte: u8) -> Option<[u8; 256]> {
+        if byte == 0x00 {
+            Some(self.clear())
+        } else {
+            self.buffer[self.pointer as usize] = byte;
+            self.pointer = self.pointer.saturating_add(1);
+            None
+        }
+    }
+}
diff --git a/src/types/read_buffer.rs b/src/types/read_buffer.rs
new file mode 100644
index 0000000..7128048
--- /dev/null
+++ b/src/types/read_buffer.rs
@@ -0,0 +1,34 @@
+pub struct ReadBuffer {
+    pub bytes: Vec<u8>,
+    pub pointer: usize,
+}
+
+impl ReadBuffer {
+    pub fn new() -> Self {
+        Self {
+            bytes: Vec::new(),
+            pointer: 0,
+        }
+    }
+
+    pub fn from_str(text: &str) -> Self {
+        Self {
+            bytes: text.bytes().collect(),
+            pointer: 0,
+        }
+    }
+
+    pub fn set_str(&mut self, text: &str) {
+        self.bytes = text.bytes().collect();
+        self.pointer = 0;
+    }
+
+    pub fn read(&mut self) -> u8 {
+        let pointer = self.pointer;
+        self.pointer += 1;
+        match self.bytes.get(pointer) {
+            Some(byte) => *byte,
+            None => 0,
+        }
+    }
+}
diff --git a/src/types/sprite_buffer.rs b/src/types/sprite_buffer.rs
new file mode 100644
index 0000000..74c7b55
--- /dev/null
+++ b/src/types/sprite_buffer.rs
@@ -0,0 +1,85 @@
+use crate::*;
+
+
+pub struct SpriteBuffer {
+    pub mem: [u8; 16],
+    pub pointer: usize,
+    pub cached: Option<(Sprite, u8)>,
+}
+
+impl SpriteBuffer {
+    pub fn new() -> Self {
+        Self {
+            mem: [0; 16],
+            pointer: 0,
+            cached: None,
+        }
+    }
+
+    pub fn push_byte(&mut self, byte: u8) {
+        self.mem[self.pointer] = byte;
+        self.pointer = (self.pointer + 1) % 16;
+        self.cached = None;
+    }
+
+    pub fn read_1bit_sprite(&mut self, draw: u8) -> Sprite {
+        if let Some((sprite, transform)) = self.cached {
+            if transform == (draw & 0x77) {
+                return sprite;
+            }
+        }
+        macro_rules! c {
+            ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); };
+            ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; };
+        }
+        let mut sprite = [[0; 8]; 8];
+        let mut p = match draw & 0x02 != 0 {
+            true  => self.pointer,
+            false => self.pointer + 8,
+        };
+        match draw & 0x07 {
+            0x0 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } },
+            0x1 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>(  x) & 1; } } },
+            0x2 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } },
+            0x3 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>(  x) & 1; } } },
+            0x4 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } },
+            0x5 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>(  x) & 1; } } },
+            0x6 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } },
+            0x7 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>(  x) & 1; } } },
+            _ => unreachable!(),
+        }
+        self.cached = Some((sprite, draw & 0x77));
+        return sprite;
+    }
+
+    pub fn read_2bit_sprite(&mut self, draw: u8) -> Sprite {
+        if let Some((sprite, transform)) = self.cached {
+            if transform == (draw & 0x77) {
+                return sprite;
+            }
+        }
+        macro_rules! c {
+            ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); };
+            ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; };
+        }
+        let mut sprite = [[0; 8]; 8];
+        let mut p = match draw & 0x02 != 0 {
+            true  => self.pointer,
+            false => self.pointer + 8,
+        };
+        let mut s = p + 8;
+        match draw & 0x07 {
+            0x0 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+            0x1 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=  x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+            0x2 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+            0x3 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=  x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+            0x4 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+            0x5 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=  x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+            0x6 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+            0x7 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=  x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+            _ => unreachable!(),
+        }
+        self.cached = Some((sprite, draw & 0x77));
+        return sprite;
+    }
+}
-- 
cgit v1.2.3-70-g09d2