diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 53d366d..9ec1fec 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -3,13 +3,15 @@ name: Format on: [push, pull_request] jobs: - check: - + run_cargo_fmt: + name: Run cargo fmt runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Add Tool - run: rustup component add rustfmt - - name: Check Format - run: cargo fmt -- --check + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Add Tool + run: rustup component add rustfmt + - name: Check Format + run: cargo fmt -- --check diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 72f3418..f17285f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,6 +14,9 @@ jobs: - name: Check Syntax run: | cargo check + - name: Test + run: | + cargo test --lib - name: Install Targets and Tools run: | rustup target add thumbv7em-none-eabi @@ -22,7 +25,8 @@ jobs: rustup component add llvm-tools-preview cargo install cargo-binutils - name: Build - run: ./build.sh --verbose + run: | + ./build.sh --verbose - name: Upload files to Release if: github.event_name == 'push' && startswith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d1c301..2c56f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased changes +* None + +## v0.5.0 + +* Switch to neotron-common-bios 0.11 +* Added "Shutdown" command +* Added ANSI decoder for colour changes (SGI) and cursor position support +* Added 'standard input' support for applications +* Use new compare-and-swap BIOS API to implement mutexes, instead of `static mut` +* OS now requires 256K Flash space + ## v0.4.0 * The `load` command now takes ELF binaries, not raw binaries. diff --git a/Cargo.lock b/Cargo.lock index d5a352f..40162fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "atomic-polyfill" version = "0.1.11" @@ -19,9 +31,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" -version = "2.3.1" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "byteorder" @@ -31,11 +43,11 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ - "num-integer", + "android-tzdata", "num-traits", ] @@ -96,9 +108,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -137,11 +149,12 @@ dependencies = [ [[package]] name = "neotron-common-bios" -version = "0.8.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4b43ef66a881b66110e2bafd11a8cd06613d0e99c52c9e8cdbc1195c61b94b" +checksum = "9adad98cd2c761f72321c516359f53b8e0cd56637ee6332e7d8628d8144cad3a" dependencies = [ "chrono", + "neotron-ffi", "pc-keyboard", ] @@ -159,10 +172,11 @@ checksum = "b9b8634a088b9d5b338a96b3f6ef45a3bc0b9c0f0d562c7d00e498265fd96e8f" [[package]] name = "neotron-os" -version = "0.4.0" +version = "0.5.0" dependencies = [ "chrono", "embedded-sdmmc", + "heapless", "menu", "neotron-api", "neotron-common-bios", @@ -171,23 +185,14 @@ dependencies = [ "postcard", "r0", "serde", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", + "vte", ] [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] @@ -200,9 +205,9 @@ checksum = "ed089a1fbffe3337a1a345501c981f1eb1e47e69de5a40e852433e12953c3174" [[package]] name = "postcard" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa512cd0d087cc9f99ad30a1bf64795b67871edbead083ffc3a4dfafa59aa00" +checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb" dependencies = [ "cobs", "heapless", @@ -211,18 +216,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" dependencies = [ "proc-macro2", ] @@ -244,30 +249,30 @@ dependencies = [ [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "3b88756493a5bd5e5395d53baa70b194b05764ab85b59e43e4b8f4e1192fa9b1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "6e5c3a298c7f978e53536f95a63bdc4c4a64550582f31a0359a9afda6aede62e" dependencies = [ "proc-macro2", "quote", @@ -291,9 +296,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "syn" -version = "2.0.16" +version = "2.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" dependencies = [ "proc-macro2", "quote", @@ -302,12 +307,36 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "git+https://github.com/alacritty/vte#94e74f3a64f42d5dad4e3d42dbe8c23291038214" [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "vte" +version = "0.11.1" +source = "git+https://github.com/alacritty/vte#94e74f3a64f42d5dad4e3d42dbe8c23291038214" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "git+https://github.com/alacritty/vte#94e74f3a64f42d5dad4e3d42dbe8c23291038214" +dependencies = [ + "proc-macro2", + "quote", +] diff --git a/Cargo.toml b/Cargo.toml index 1bbb4af..ad3c4e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "neotron-os" -version = "0.4.0" +version = "0.5.0" authors = [ "Jonathan 'theJPster' Pallant ", "The Neotron Developers" @@ -34,16 +34,17 @@ required-features = ["native-log"] lto = true debug = true codegen-units = 1 -opt-level = "s" +opt-level = "z" panic = "abort" [profile.dev] panic = "abort" [dependencies] -neotron-common-bios = "0.8" +neotron-common-bios = "0.11" pc-keyboard = "0.7" r0 = "1.0" +heapless = "0.7" postcard = "1.0" serde = { version = "1.0", default-features = false } menu = "0.3" @@ -51,6 +52,7 @@ chrono = { version = "0.4", default-features = false } embedded-sdmmc = { version = "0.5", default-features = false } neotron-api = "0.1" neotron-loader = "0.1" +vte = { git = "https://github.com/alacritty/vte", commit="94e74f3a64f42d5dad4e3d42dbe8c23291038214" } [features] lib-mode = [] diff --git a/README.md b/README.md index 5e61c56..9a1496d 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,24 @@ This OS is a work in progress. We intend to support: * [x] Calling BIOS APIs * [x] Text mode VGA console * [x] Serial console -* [ ] Starting a command-line shell application -* [ ] Executing applications from RAM -* [ ] MBR/FAT32 formatted block devices with standard open/close/read/write file semantics +* [x] Running built-in commands from a shell +* [x] Executing applications from RAM + * [x] Applications can print to stdout + * [ ] Applications can read from stdin + * [ ] Applications can open/close/read/write files +* [x] MBR/FAT32 formatted block devices + * [x] Read blocks + * [x] Directory listing of / + * [ ] Write to files + * [ ] Delete files + * [ ] Change directory +* [x] Load ELF binaries from disk +* [ ] Changing text modes * [ ] Basic networking * [ ] Music playback * [ ] Various keyboard layouts * [ ] Ethernet / WiFi networking +* [ ] Built-in scripting language ## Build instructions @@ -55,20 +66,43 @@ See [`CHANGELOG.md`](./CHANGELOG.md) ## Licence - Neotron-OS Copyright (c) The Neotron Developers, 2022 +```text +Neotron-OS Copyright (c) Jonathan 'theJPster' Pallant and The Neotron Developers, 2023 - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program. If not, see . +You should have received a copy of the GNU General Public License +along with this program. If not, see . +``` + +See the full text in [LICENSE.txt](./LICENSE.txt). Broadly, we (the developers) +interpret this to mean (and note that we are not lawyers and this is not +legal advice) that if you give someone a Neotron computer, you must also give them +one of: + +* Complete and corresponding source code (e.g. on disk, or as a link to your + **own** on-line Git repo) for any GPL components (e.g. the BIOS and the OS), + as supplied on the Neotron computer. +* A written offer to provide complete and corresponding source code on + request. + +If you are not offering a Neotron computer commercially (i.e. you are not +selling a board for commercial gain), and you are using an unmodified upstream +version of the source code, then the third option is to give them: + +* A link to the tag/commit-hash on the relevant official Neotron Github + repository - . + +This is to ensure everyone always has the freedom to access the source code in +their Neotron based computer. ## Contribution diff --git a/neotron-flash-0002.ld b/neotron-flash-0002.ld index a7f56ea..8858b4f 100644 --- a/neotron-flash-0002.ld +++ b/neotron-flash-0002.ld @@ -22,7 +22,7 @@ MEMORY { /* The first 128 KiB is for the BIOS. We get the rest. */ - FLASH (rx) : ORIGIN = 0x00020000, LENGTH = 128K + FLASH (rx) : ORIGIN = 0x00020000, LENGTH = 256K /* * We get the bottom 4KB of RAM. Anything above that is for applications * (up to wherever the BIOS tells us we can use.) diff --git a/neotron-flash-0802.ld b/neotron-flash-0802.ld index cd36166..3162052 100644 --- a/neotron-flash-0802.ld +++ b/neotron-flash-0802.ld @@ -22,7 +22,7 @@ MEMORY { /* The first 128 KiB is for the BIOS. We get the rest. */ - FLASH (rx) : ORIGIN = 0x08020000, LENGTH = 128K + FLASH (rx) : ORIGIN = 0x08020000, LENGTH = 256K /* * We get the bottom 4KB of RAM. Anything above that is for applications * (up to wherever the BIOS tells us we can use.) diff --git a/neotron-flash-1002.ld b/neotron-flash-1002.ld index d2e0f2a..6eabbe1 100644 --- a/neotron-flash-1002.ld +++ b/neotron-flash-1002.ld @@ -22,7 +22,7 @@ MEMORY { /* The first 128 KiB is for the BIOS. We get the rest. */ - FLASH (rx) : ORIGIN = 0x10020000, LENGTH = 128K + FLASH (rx) : ORIGIN = 0x10020000, LENGTH = 256K /* * The RAM reserved for the OS. Above this is the Transient Program Area. diff --git a/src/commands/block.rs b/src/commands/block.rs index df7585d..afe9d13 100644 --- a/src/commands/block.rs +++ b/src/commands/block.rs @@ -1,6 +1,6 @@ //! Block Device related commands for Neotron OS -use crate::{bios, print, println, Ctx, API}; +use crate::{bios, osprint, osprintln, Ctx, API}; pub static LSBLK_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -34,9 +34,9 @@ fn lsblk(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: let api = API.get(); let mut found = false; - println!("Block Devices:"); + osprintln!("Block Devices:"); for dev_idx in 0..=255u8 { - if let bios::Option::Some(device_info) = (api.block_dev_get_info)(dev_idx) { + if let bios::FfiOption::Some(device_info) = (api.block_dev_get_info)(dev_idx) { let (bsize, bunits, dsize, dunits) = match device_info.num_blocks * u64::from(device_info.block_size) { x if x < (1024 * 1024 * 1024) => { @@ -48,12 +48,12 @@ fn lsblk(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: (10 * x / (1024 * 1024 * 1024), "GiB", x / 100_000_000, "GB") } }; - println!("Device {}:", dev_idx); - println!(" Name: {}", device_info.name); - println!(" Type: {:?}", device_info.device_type); - println!(" Block size: {}", device_info.block_size); - println!(" Num Blocks: {}", device_info.num_blocks); - println!( + osprintln!("Device {}:", dev_idx); + osprintln!(" Name: {}", device_info.name); + osprintln!(" Type: {:?}", device_info.device_type); + osprintln!(" Block size: {}", device_info.block_size); + osprintln!(" Num Blocks: {}", device_info.num_blocks); + osprintln!( " Card Size: {}.{} {} ({}.{} {})", bsize / 10, bsize % 10, @@ -62,15 +62,15 @@ fn lsblk(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: dsize % 10, dunits ); - println!(" Ejectable: {}", device_info.ejectable); - println!(" Removable: {}", device_info.removable); - println!(" Media Present: {}", device_info.media_present); - println!(" Read Only: {}", device_info.read_only); + osprintln!(" Ejectable: {}", device_info.ejectable); + osprintln!(" Removable: {}", device_info.removable); + osprintln!(" Media Present: {}", device_info.media_present); + osprintln!(" Read Only: {}", device_info.read_only); found = true; } } if !found { - println!(" None"); + osprintln!(" None"); } } @@ -78,35 +78,35 @@ fn lsblk(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: fn read_block(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { let api = API.get(); let Ok(dev_idx) = args[0].parse::() else { - println!("Couldn't parse {:?}", args[0]); + osprintln!("Couldn't parse {:?}", args[0]); return; }; let Ok(block_idx) = args[1].parse::() else { - println!("Couldn't parse {:?}", args[1]); + osprintln!("Couldn't parse {:?}", args[1]); return; }; - println!("Reading block {}:", block_idx); + osprintln!("Reading block {}:", block_idx); let mut buffer = [0u8; 512]; match (api.block_read)( dev_idx, bios::block_dev::BlockIdx(block_idx), 1, - bios::ApiBuffer::new(&mut buffer), + bios::FfiBuffer::new(&mut buffer), ) { - bios::Result::Ok(_) => { + bios::ApiResult::Ok(_) => { // Carry on let mut count = 0; for chunk in buffer.chunks(32) { - print!("{:03x}: ", count); + osprint!("{:03x}: ", count); for b in chunk { - print!("{:02x}", *b); + osprint!("{:02x}", *b); } count += chunk.len(); - println!(); + osprintln!(); } } - bios::Result::Err(e) => { - println!("Failed to read: {:?}", e); + bios::ApiResult::Err(e) => { + osprintln!("Failed to read: {:?}", e); } } } diff --git a/src/commands/config.rs b/src/commands/config.rs index fedf676..54e37fe 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -1,6 +1,6 @@ //! Configuration related commands for Neotron OS -use crate::{config, println, Ctx}; +use crate::{config, osprintln, Ctx}; pub static COMMAND_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -27,66 +27,66 @@ fn command(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: "reset" => match config::Config::load() { Ok(new_config) => { ctx.config = new_config; - println!("Loaded OK."); + osprintln!("Loaded OK."); } Err(e) => { - println!("Error loading; {}", e); + osprintln!("Error loading; {}", e); } }, "save" => match ctx.config.save() { Ok(_) => { - println!("Saved OK."); + osprintln!("Saved OK."); } Err(e) => { - println!("Error saving: {}", e); + osprintln!("Error saving: {}", e); } }, "vga" => match args.get(1).cloned() { Some("on") => { ctx.config.set_vga_console(true); - println!("VGA now on"); + osprintln!("VGA now on"); } Some("off") => { ctx.config.set_vga_console(false); - println!("VGA now off"); + osprintln!("VGA now off"); } _ => { - println!("Give on or off as argument"); + osprintln!("Give on or off as argument"); } }, "serial" => match (args.get(1).cloned(), args.get(1).map(|s| s.parse::())) { (_, Some(Ok(baud))) => { - println!("Turning serial console on at {} bps", baud); + osprintln!("Turning serial console on at {} bps", baud); ctx.config.set_serial_console_on(baud); } (Some("off"), _) => { - println!("Turning serial console off"); + osprintln!("Turning serial console off"); ctx.config.set_serial_console_off(); } _ => { - println!("Give off or an integer as argument"); + osprintln!("Give off or an integer as argument"); } }, "print" => { - println!("VGA : {}", ctx.config.get_vga_console()); + osprintln!("VGA : {}", ctx.config.get_vga_console()); match ctx.config.get_serial_console() { None => { - println!("Serial: off"); + osprintln!("Serial: off"); } Some((_port, config)) => { - println!("Serial: {} bps", config.data_rate_bps); + osprintln!("Serial: {} bps", config.data_rate_bps); } } } _ => { - println!("config print - print the config"); - println!("config help - print this help text"); - println!("config reset - load config from BIOS store"); - println!("config save - save config to BIOS store"); - println!("config vga on - turn VGA on"); - println!("config vga off - turn VGA off"); - println!("config serial off - turn serial console off"); - println!("config serial - turn serial console on with given baud rate"); + osprintln!("config print - print the config"); + osprintln!("config help - print this help text"); + osprintln!("config reset - load config from BIOS store"); + osprintln!("config save - save config to BIOS store"); + osprintln!("config vga on - turn VGA on"); + osprintln!("config vga off - turn VGA off"); + osprintln!("config serial off - turn serial console off"); + osprintln!("config serial - turn serial console on with given baud rate"); } } } diff --git a/src/commands/fs.rs b/src/commands/fs.rs index e750d20..4d8fcf9 100644 --- a/src/commands/fs.rs +++ b/src/commands/fs.rs @@ -2,7 +2,7 @@ use embedded_sdmmc::VolumeIdx; -use crate::{bios, print, println, Ctx}; +use crate::{bios, osprint, osprintln, Ctx}; pub static DIR_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -28,7 +28,7 @@ pub static LOAD_ITEM: menu::Item = menu::Item { /// Called when the "dir" command is executed. fn dir(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { fn work() -> Result<(), embedded_sdmmc::Error> { - println!("Listing files on Block Device 0, /"); + osprintln!("Listing files on Block Device 0, /"); let bios_block = crate::fs::BiosBlock(); let time = crate::fs::BiosTime(); let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); @@ -41,46 +41,47 @@ fn dir(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: & let padding = 8 - dir_entry.name.base_name().len(); for b in dir_entry.name.base_name() { let ch = *b as char; - print!("{}", if ch.is_ascii_graphic() { ch } else { '?' }); + osprint!("{}", if ch.is_ascii_graphic() { ch } else { '?' }); } for _ in 0..padding { - print!(" "); + osprint!(" "); } - print!(" "); + osprint!(" "); let padding = 3 - dir_entry.name.extension().len(); for b in dir_entry.name.extension() { let ch = *b as char; - print!("{}", if ch.is_ascii_graphic() { ch } else { '?' }); + osprint!("{}", if ch.is_ascii_graphic() { ch } else { '?' }); } for _ in 0..padding { - print!(" "); + osprint!(" "); } if dir_entry.attributes.is_directory() { - print!(" "); + osprint!(" "); } else { - print!(" {:-13}", dir_entry.size,); + osprint!(" {:-13}", dir_entry.size,); } - print!( + osprint!( " {:02}/{:02}/{:04}", dir_entry.mtime.zero_indexed_day + 1, dir_entry.mtime.zero_indexed_month + 1, u32::from(dir_entry.mtime.year_since_1970) + 1970 ); - println!( + osprintln!( " {:02}:{:02}", - dir_entry.mtime.hours, dir_entry.mtime.minutes + dir_entry.mtime.hours, + dir_entry.mtime.minutes ); total_bytes += dir_entry.size as u64; num_files += 1; })?; - println!("{:-9} file(s) {:-13} bytes", num_files, total_bytes); + osprintln!("{:-9} file(s) {:-13} bytes", num_files, total_bytes); Ok(()) } match work() { Ok(_) => {} Err(e) => { - println!("Error: {:?}", e); + osprintln!("Error: {:?}", e); } } } @@ -88,13 +89,13 @@ fn dir(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: & /// Called when the "load" command is executed. fn load(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { let Some(filename) = args.first() else { - println!("Need a filename"); + osprintln!("Need a filename"); return; }; match ctx.tpa.load_program(filename) { Ok(_) => {} Err(e) => { - println!("Error: {:?}", e); + osprintln!("Error: {:?}", e); } } } diff --git a/src/commands/hardware.rs b/src/commands/hardware.rs index 92f4c79..3ff608f 100644 --- a/src/commands/hardware.rs +++ b/src/commands/hardware.rs @@ -1,6 +1,6 @@ //! Hardware related commands for Neotron OS -use crate::{bios, println, Ctx, API}; +use crate::{bios, osprintln, Ctx, API}; pub static LSHW_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -11,44 +11,64 @@ pub static LSHW_ITEM: menu::Item = menu::Item { help: Some("List all the BIOS hardware"), }; +pub static SHUTDOWN_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: shutdown, + parameters: &[ + menu::Parameter::Named { + parameter_name: "reboot", + help: Some("Reboot after shutting down"), + }, + menu::Parameter::Named { + parameter_name: "bootloader", + help: Some("Reboot into the bootloader after shutting down"), + }, + ], + }, + command: "shutdown", + help: Some("Shutdown the system"), +}; + /// Called when the "lshw" command is executed. fn lshw(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { let api = API.get(); let mut found = false; - println!("Memory regions:"); + osprintln!("Memory regions:"); for region_idx in 0..=255u8 { - if let bios::Option::Some(region) = (api.memory_get_region)(region_idx) { - println!(" {}: {}", region_idx, region); + if let bios::FfiOption::Some(region) = (api.memory_get_region)(region_idx) { + osprintln!(" {}: {}", region_idx, region); found = true; } } if !found { - println!(" None"); + osprintln!(" None"); } found = false; - println!("Serial Devices:"); + osprintln!("Serial Devices:"); for dev_idx in 0..=255u8 { - if let bios::Option::Some(device_info) = (api.serial_get_info)(dev_idx) { - println!( + if let bios::FfiOption::Some(device_info) = (api.serial_get_info)(dev_idx) { + osprintln!( " {}: {} {:?}", - dev_idx, device_info.name, device_info.device_type + dev_idx, + device_info.name, + device_info.device_type ); found = true; } } if !found { - println!(" None"); + osprintln!(" None"); } found = false; - println!("Block Devices:"); + osprintln!("Block Devices:"); for dev_idx in 0..=255u8 { - if let bios::Option::Some(device_info) = (api.block_dev_get_info)(dev_idx) { - println!( + if let bios::FfiOption::Some(device_info) = (api.block_dev_get_info)(dev_idx) { + osprintln!( " {}: {} {:?} bs={} size={} MiB", dev_idx, device_info.name, @@ -60,45 +80,61 @@ fn lshw(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: } } if !found { - println!(" None"); + osprintln!(" None"); } found = false; - println!("I2C Buses:"); + osprintln!("I2C Buses:"); for dev_idx in 0..=255u8 { - if let bios::Option::Some(device_info) = (api.i2c_bus_get_info)(dev_idx) { - println!(" {}: {:?}", dev_idx, device_info); + if let bios::FfiOption::Some(device_info) = (api.i2c_bus_get_info)(dev_idx) { + osprintln!(" {}: {:?}", dev_idx, device_info); found = true; } } if !found { - println!(" None"); + osprintln!(" None"); } found = false; - println!("Neotron Bus Devices:"); + osprintln!("Neotron Bus Devices:"); for dev_idx in 0..=255u8 { - if let bios::Option::Some(device_info) = (api.bus_get_info)(dev_idx) { - println!(" {}: {:?}", dev_idx, device_info); + if let bios::FfiOption::Some(device_info) = (api.bus_get_info)(dev_idx) { + osprintln!(" {}: {:?}", dev_idx, device_info); found = true; } } if !found { - println!(" None"); + osprintln!(" None"); } found = false; - println!("Audio Mixers:"); + osprintln!("Audio Mixers:"); for dev_idx in 0..=255u8 { - if let bios::Option::Some(device_info) = (api.audio_mixer_channel_get_info)(dev_idx) { - println!(" {}: {:?}", dev_idx, device_info); + if let bios::FfiOption::Some(device_info) = (api.audio_mixer_channel_get_info)(dev_idx) { + osprintln!(" {}: {:?}", dev_idx, device_info); found = true; } } if !found { - println!(" None"); + osprintln!(" None"); + } +} + +/// Called when the "shutdown" command is executed. +fn shutdown(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { + let api = API.get(); + + if let Ok(Some(_)) = menu::argument_finder(item, args, "reboot") { + osprintln!("Rebooting..."); + (api.power_control)(bios::PowerMode::Reset); + } else if let Ok(Some(_)) = menu::argument_finder(item, args, "bootloader") { + osprintln!("Rebooting into bootloader..."); + (api.power_control)(bios::PowerMode::Bootloader); + } else { + osprintln!("Shutting down..."); + (api.power_control)(bios::PowerMode::Off); } } diff --git a/src/commands/input.rs b/src/commands/input.rs index 36c29d5..eccd3c7 100644 --- a/src/commands/input.rs +++ b/src/commands/input.rs @@ -1,6 +1,6 @@ //! Input related commands for Neotron OS -use crate::{bios, println, Ctx, API}; +use crate::{osprintln, Ctx}; pub static KBTEST_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -12,42 +12,33 @@ pub static KBTEST_ITEM: menu::Item = menu::Item { }; /// Called when the "kbtest" command is executed. -fn kbtest(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], ctx: &mut Ctx) { - let api = API.get(); - loop { - match (api.hid_get_event)() { - bios::Result::Ok(bios::Option::Some(bios::hid::HidEvent::KeyPress(code))) => { - let pckb_ev = pc_keyboard::KeyEvent { - code, - state: pc_keyboard::KeyState::Down, - }; - if let Some(ev) = ctx.keyboard.process_keyevent(pckb_ev) { - println!("Code={code:?} State=Down Decoded={ev:?}"); - } else { - println!("Code={code:?} State=Down Decoded=None"); - } - if code == pc_keyboard::KeyCode::Escape { - break; - } +fn kbtest(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { + osprintln!("Press Ctrl-X to quit"); + const CTRL_X: u8 = 0x18; + 'outer: loop { + if let Some(ev) = crate::STD_INPUT.lock().get_raw() { + osprintln!("Event: {ev:?}"); + if ev == pc_keyboard::DecodedKey::Unicode(CTRL_X as char) { + break 'outer; } - bios::Result::Ok(bios::Option::Some(bios::hid::HidEvent::KeyRelease(code))) => { - let pckb_ev = pc_keyboard::KeyEvent { - code, - state: pc_keyboard::KeyState::Up, - }; - if let Some(ev) = ctx.keyboard.process_keyevent(pckb_ev) { - println!("Code={code:?} State=Up Decoded={ev:?}"); - } else { - println!("Code={code:?} State=Up Decoded=None"); + } + let mut buffer = [0u8; 8]; + let count = if let Some(serial) = crate::SERIAL_CONSOLE.lock().as_mut() { + serial + .read_data(&mut buffer) + .ok() + .and_then(|n| if n == 0 { None } else { Some(n) }) + } else { + None + }; + if let Some(count) = count { + osprintln!("Serial RX: {:x?}", &buffer[0..count]); + for b in &buffer[0..count] { + if *b == CTRL_X { + break 'outer; } } - bios::Result::Ok(bios::Option::Some(bios::hid::HidEvent::MouseInput(_ignore))) => {} - bios::Result::Ok(bios::Option::None) => { - // Do nothing - } - bios::Result::Err(e) => { - println!("Failed to get HID events: {:?}", e); - } } } + osprintln!("Finished."); } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 2b83f3a..8dd58cc 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -31,6 +31,7 @@ pub static OS_MENU: menu::Menu = menu::Menu { &screen::FILL_ITEM, &screen::MANDEL_ITEM, &input::KBTEST_ITEM, + &hardware::SHUTDOWN_ITEM, ], entry: None, exit: None, diff --git a/src/commands/ram.rs b/src/commands/ram.rs index 99d191c..5d0c45c 100644 --- a/src/commands/ram.rs +++ b/src/commands/ram.rs @@ -1,6 +1,6 @@ //! Raw RAM read/write related commands for Neotron OS -use crate::{print, println, Ctx}; +use crate::{osprint, osprintln, Ctx}; pub static HEXDUMP_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -65,50 +65,50 @@ fn hexdump(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ctx const BYTES_PER_LINE: usize = 16; let Some(address_str) = args.first() else { - println!("No address"); + osprintln!("No address"); return; }; let Ok(address) = parse_usize(address_str) else { - println!("Bad address"); + osprintln!("Bad address"); return; }; let len_str = args.get(1).unwrap_or(&"16"); let Ok(len) = parse_usize(len_str) else { - println!("Bad length"); + osprintln!("Bad length"); return; }; let mut ptr = address as *const u8; let mut this_line = 0; - print!("{:08x}: ", address); + osprint!("{:08x}: ", address); for count in 0..len { if this_line == BYTES_PER_LINE { - println!(); - print!("{:08x}: ", address + count); + osprintln!(); + osprint!("{:08x}: ", address + count); this_line = 1; } else { this_line += 1; } let b = unsafe { ptr.read_volatile() }; - print!("{:02x} ", b); + osprint!("{:02x} ", b); ptr = unsafe { ptr.offset(1) }; } - println!(); + osprintln!(); } /// Called when the "run" command is executed. fn run(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], ctx: &mut Ctx) { match ctx.tpa.execute() { Ok(0) => { - println!(); + osprintln!(); } Ok(n) => { - println!("\nError Code: {}", n); + osprintln!("\nError Code: {}", n); } Err(e) => { - println!("\nFailed to execute: {:?}", e); + osprintln!("\nFailed to execute: {:?}", e); } } } @@ -119,19 +119,19 @@ fn run(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], ctx: &m /// don't. fn loadf(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { let Some(address_str) = args.first() else { - println!("No address"); + osprintln!("No address"); return; }; let Ok(address) = parse_usize(address_str) else { - println!("Bad address"); + osprintln!("Bad address"); return; }; let Some(len_str) = args.get(1) else { - println!("No length"); + osprintln!("No length"); return; }; let Ok(len) = parse_usize(len_str) else { - println!("Bad length"); + osprintln!("Bad length"); return; }; @@ -139,10 +139,10 @@ fn loadf(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: & match ctx.tpa.copy_program(src_slice) { Ok(_) => { - println!("Ok"); + osprintln!("Ok"); } Err(e) => { - println!("Error: {:?}", e); + osprintln!("Error: {:?}", e); } } } diff --git a/src/commands/screen.rs b/src/commands/screen.rs index 3a75ee9..a27c45a 100644 --- a/src/commands/screen.rs +++ b/src/commands/screen.rs @@ -2,7 +2,7 @@ use neotron_common_bios::video::{Attr, TextBackgroundColour, TextForegroundColour}; -use crate::{print, println, Ctx, API, VGA_CONSOLE}; +use crate::{osprint, osprintln, Ctx, API, VGA_CONSOLE}; pub static CLEAR_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -42,19 +42,21 @@ pub static MANDEL_ITEM: menu::Item = menu::Item { /// Called when the "clear" command is executed. fn clear(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { - if let Some(ref mut console) = unsafe { &mut VGA_CONSOLE } { - console.clear(); + let mut guard = VGA_CONSOLE.try_lock().unwrap(); + if let Some(vga_console) = guard.as_mut() { + vga_console.clear(); } } /// Called when the "fill" command is executed. fn fill(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { - if let Some(ref mut console) = unsafe { &mut VGA_CONSOLE } { + let mut guard = VGA_CONSOLE.try_lock().unwrap(); + if let Some(console) = guard.as_mut() { console.clear(); let api = API.get(); let mode = (api.video_get_mode)(); let (Some(width), Some(height)) = (mode.text_width(), mode.text_height()) else { - println!("Unable to get console size"); + osprintln!("Unable to get console size"); return; }; // A range of printable ASCII compatible characters @@ -91,7 +93,8 @@ fn fill(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: /// Called when the "bench" command is executed. fn bench(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { const NUM_CHARS: u64 = 1_000_000; - if let Some(ref mut console) = unsafe { &mut VGA_CONSOLE } { + let mut guard = VGA_CONSOLE.try_lock().unwrap(); + if let Some(console) = guard.as_mut() { let api = API.get(); let start = (api.time_ticks_get)(); console.clear(); @@ -102,9 +105,11 @@ fn bench(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: let end = (api.time_ticks_get)(); let delta = end.0 - start.0; let chars_per_second = (NUM_CHARS * (api.time_ticks_per_second)().0) / delta; - println!( + osprintln!( "{} chars in {} ticks, or {} chars per second", - NUM_CHARS, delta, chars_per_second + NUM_CHARS, + delta, + chars_per_second ); } } @@ -131,7 +136,7 @@ fn mandel(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx let api = API.get(); let mode = (api.video_get_mode)(); let (Some(width), Some(height)) = (mode.text_width(), mode.text_height()) else { - println!("Unable to get screen size"); + osprintln!("Unable to get screen size"); return; }; @@ -141,7 +146,7 @@ fn mandel(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx for x_pos in 0..width { let x = (f64::from(x_pos) * 4.0 / f64::from(width)) - 2.0; let result = mandelbrot(x, y, 20); - print!("{}", glyphs[result as usize] as char); + osprint!("{}", glyphs[result as usize] as char); } } } diff --git a/src/commands/timedate.rs b/src/commands/timedate.rs index f575d9d..a73fd54 100644 --- a/src/commands/timedate.rs +++ b/src/commands/timedate.rs @@ -2,7 +2,7 @@ use chrono::{Datelike, Timelike}; -use crate::{println, Ctx, API}; +use crate::{osprintln, Ctx, API}; pub static DATE_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -19,10 +19,10 @@ pub static DATE_ITEM: menu::Item = menu::Item { /// Called when the "date" command is executed. fn date(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { if let Ok(Some(timestamp)) = menu::argument_finder(item, args, "timestamp") { - println!("Setting date/time to {:?}", timestamp); + osprintln!("Setting date/time to {:?}", timestamp); static DATE_FMT: &str = "%Y-%m-%dT%H:%M:%S"; let Ok(timestamp) = chrono::NaiveDateTime::parse_from_str(timestamp, DATE_FMT) else { - println!("Unable to parse date/time"); + osprintln!("Unable to parse date/time"); return; }; API.set_time(timestamp); @@ -30,7 +30,7 @@ fn date(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &m let time = API.get_time(); // Ensure this matches `DATE_FMT`, for consistency - println!( + osprintln!( "The time is {:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}", time.year(), time.month(), diff --git a/src/config.rs b/src/config.rs index 4aa3119..1d77e69 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,11 +17,11 @@ impl Config { pub fn load() -> Result { let api = API.get(); let mut buffer = [0u8; 64]; - match (api.configuration_get)(bios::ApiBuffer::new(&mut buffer)) { - bios::Result::Ok(n) => { + match (api.configuration_get)(bios::FfiBuffer::new(&mut buffer)) { + bios::ApiResult::Ok(n) => { postcard::from_bytes(&buffer[0..n]).map_err(|_e| "Failed to parse config") } - bios::Result::Err(_e) => Err("Failed to load config"), + bios::ApiResult::Err(_e) => Err("Failed to load config"), } } @@ -29,10 +29,12 @@ impl Config { let api = API.get(); let mut buffer = [0u8; 64]; let slice = postcard::to_slice(self, &mut buffer).map_err(|_e| "Failed to parse config")?; - match (api.configuration_set)(bios::ApiByteSlice::new(slice)) { - bios::Result::Ok(_) => Ok(()), - bios::Result::Err(bios::Error::Unimplemented) => Err("BIOS doesn't support this (yet)"), - bios::Result::Err(_) => Err("BIOS reported an error"), + match (api.configuration_set)(bios::FfiByteSlice::new(slice)) { + bios::ApiResult::Ok(_) => Ok(()), + bios::ApiResult::Err(bios::Error::Unimplemented) => { + Err("BIOS doesn't support this (yet)") + } + bios::ApiResult::Err(_) => Err("BIOS reported an error"), } } diff --git a/src/fs.rs b/src/fs.rs index 129a65f..12c7afd 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -27,10 +27,10 @@ impl embedded_sdmmc::BlockDevice for BiosBlock { 0, bios::block_dev::BlockIdx(u64::from(start_block_idx.0)), blocks.len() as u8, - bios::ApiBuffer::new(byte_slice), + bios::FfiBuffer::new(byte_slice), ) { - bios::Result::Ok(_) => Ok(()), - bios::Result::Err(e) => Err(e), + bios::ApiResult::Ok(_) => Ok(()), + bios::ApiResult::Err(e) => Err(e), } } @@ -50,18 +50,18 @@ impl embedded_sdmmc::BlockDevice for BiosBlock { 0, bios::block_dev::BlockIdx(u64::from(start_block_idx.0)), blocks.len() as u8, - bios::ApiByteSlice::new(byte_slice), + bios::FfiByteSlice::new(byte_slice), ) { - bios::Result::Ok(_) => Ok(()), - bios::Result::Err(e) => Err(e), + bios::ApiResult::Ok(_) => Ok(()), + bios::ApiResult::Err(e) => Err(e), } } fn num_blocks(&self) -> Result { let api = API.get(); match (api.block_dev_get_info)(0) { - bios::Option::Some(info) => Ok(embedded_sdmmc::BlockCount(info.num_blocks as u32)), - bios::Option::None => Err(bios::Error::InvalidDevice), + bios::FfiOption::Some(info) => Ok(embedded_sdmmc::BlockCount(info.num_blocks as u32)), + bios::FfiOption::None => Err(bios::Error::InvalidDevice), } } } diff --git a/src/lib.rs b/src/lib.rs index 2f75c17..e68ed78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,26 +6,32 @@ //! //! Licence: GPL v3 or higher (see ../LICENCE.md) -#![no_std] +#![cfg_attr(not(test), no_std)] + +// =========================================================================== +// Modules and Imports +// =========================================================================== -// Imports use core::sync::atomic::{AtomicBool, Ordering}; + use neotron_common_bios as bios; mod commands; mod config; mod fs; mod program; +mod refcell; mod vgaconsole; pub use config::Config as OsConfig; +use refcell::CsRefCell; // =========================================================================== // Global Variables // =========================================================================== /// The OS version string -const OS_VERSION: &str = concat!("Neotron OS, version ", env!("OS_VERSION")); +const OS_VERSION: &str = concat!("Neotron OS, v", env!("OS_VERSION")); /// Used to convert between POSIX epoch (for `chrono`) and Neotron epoch (for BIOS APIs). const SECONDS_BETWEEN_UNIX_AND_NEOTRON_EPOCH: i64 = 946684800; @@ -34,44 +40,159 @@ const SECONDS_BETWEEN_UNIX_AND_NEOTRON_EPOCH: i64 = 946684800; static API: Api = Api::new(); /// We store our VGA console here. -static mut VGA_CONSOLE: Option = None; +static VGA_CONSOLE: CsRefCell> = CsRefCell::new(None); -/// We store our VGA console here. -static mut SERIAL_CONSOLE: Option = None; +/// We store our serial console here. +static SERIAL_CONSOLE: CsRefCell> = CsRefCell::new(None); /// Note if we are panicking right now. /// /// If so, don't panic if a serial write fails. static IS_PANIC: AtomicBool = AtomicBool::new(false); +/// Our keyboard controller +static STD_INPUT: CsRefCell = CsRefCell::new(StdInput::new()); + +struct StdInput { + keyboard: pc_keyboard::EventDecoder, + buffer: heapless::spsc::Queue, +} + +impl StdInput { + const fn new() -> StdInput { + StdInput { + keyboard: pc_keyboard::EventDecoder::new( + pc_keyboard::layouts::AnyLayout::Uk105Key(pc_keyboard::layouts::Uk105Key), + pc_keyboard::HandleControl::MapLettersToUnicode, + ), + buffer: heapless::spsc::Queue::new(), + } + } + + fn get_buffered_data(&mut self, buffer: &mut [u8]) -> usize { + // If there is some data, get it. + let mut count = 0; + for slot in buffer.iter_mut() { + if let Some(n) = self.buffer.dequeue() { + *slot = n; + count += 1; + } + } + count + } + + /// Gets a raw event from the keyboard + fn get_raw(&mut self) -> Option { + let api = API.get(); + match (api.hid_get_event)() { + bios::ApiResult::Ok(bios::FfiOption::Some(bios::hid::HidEvent::KeyPress(code))) => { + let pckb_ev = pc_keyboard::KeyEvent { + code, + state: pc_keyboard::KeyState::Down, + }; + self.keyboard.process_keyevent(pckb_ev) + } + bios::ApiResult::Ok(bios::FfiOption::Some(bios::hid::HidEvent::KeyRelease(code))) => { + let pckb_ev = pc_keyboard::KeyEvent { + code, + state: pc_keyboard::KeyState::Up, + }; + self.keyboard.process_keyevent(pckb_ev) + } + bios::ApiResult::Ok(bios::FfiOption::Some(bios::hid::HidEvent::MouseInput( + _ignore, + ))) => None, + bios::ApiResult::Ok(bios::FfiOption::None) => { + // Do nothing + None + } + bios::ApiResult::Err(_e) => None, + } + } + + /// Gets some input bytes, as UTF-8. + /// + /// The data you get might be cut in the middle of a UTF-8 character. + fn get_data(&mut self, buffer: &mut [u8]) -> usize { + let count = self.get_buffered_data(buffer); + if buffer.is_empty() || count > 0 { + return count; + } + + // Nothing buffered - ask the keyboard for something + let decoded_key = self.get_raw(); + + match decoded_key { + Some(pc_keyboard::DecodedKey::Unicode(mut ch)) => { + if ch == '\n' { + ch = '\r'; + } + let mut buffer = [0u8; 6]; + let s = ch.encode_utf8(&mut buffer); + for b in s.as_bytes() { + // This will always fit + self.buffer.enqueue(*b).unwrap(); + } + } + Some(pc_keyboard::DecodedKey::RawKey(pc_keyboard::KeyCode::ArrowRight)) => { + // Load the ANSI sequence for a right arrow + for b in b"\x1b[0;77b" { + // This will always fit + self.buffer.enqueue(*b).unwrap(); + } + } + _ => { + // Drop anything else + } + } + + if let Some(console) = SERIAL_CONSOLE.lock().as_mut() { + while !self.buffer.is_full() { + let mut buffer = [0u8]; + if let Ok(1) = console.read_data(&mut buffer) { + self.buffer.enqueue(buffer[0]).unwrap(); + } else { + break; + } + } + } + + self.get_buffered_data(buffer) + } +} + // =========================================================================== // Macros // =========================================================================== /// Prints to the screen #[macro_export] -macro_rules! print { +macro_rules! osprint { ($($arg:tt)*) => { - if let Some(ref mut console) = unsafe { &mut $crate::VGA_CONSOLE } { - #[allow(unused)] - use core::fmt::Write as _; - write!(console, $($arg)*).unwrap(); - } - if let Some(ref mut console) = unsafe { &mut $crate::SERIAL_CONSOLE } { - #[allow(unused)] - use core::fmt::Write as _; - write!(console, $($arg)*).unwrap(); - } + $crate::VGA_CONSOLE.with(|guard| { + if let Some(console) = guard.as_mut() { + #[allow(unused)] + use core::fmt::Write as _; + write!(console, $($arg)*).unwrap(); + } + }).unwrap(); + $crate::SERIAL_CONSOLE.with(|guard| { + if let Some(console) = guard.as_mut() { + #[allow(unused)] + use core::fmt::Write as _; + write!(console, $($arg)*).unwrap(); + } + }).unwrap(); }; } /// Prints to the screen and puts a new-line on the end #[macro_export] -macro_rules! println { - () => ($crate::print!("\n")); +macro_rules! osprintln { + () => ($crate::osprint!("\n")); ($($arg:tt)*) => { - $crate::print!($($arg)*); - $crate::print!("\n"); + $crate::osprint!($($arg)*); + $crate::osprint!("\n"); }; } @@ -134,35 +255,47 @@ impl Api { struct SerialConsole(u8); impl SerialConsole { - fn write_bstr(&mut self, data: &[u8]) -> core::fmt::Result { + /// Write some bytes to the serial console + fn write_bstr(&mut self, data: &[u8]) { let api = API.get(); - let is_panic = IS_PANIC.load(Ordering::SeqCst); + let is_panic = IS_PANIC.load(Ordering::Relaxed); let res = (api.serial_write)( // Which port self.0, // Data - bios::ApiByteSlice::new(data), + bios::FfiByteSlice::new(data), // No timeout - bios::Option::None, + bios::FfiOption::None, ); if !is_panic { res.unwrap(); } - Ok(()) + } + + /// Try and get as many bytes as we can from the serial console. + fn read_data(&mut self, buffer: &mut [u8]) -> Result { + let api = API.get(); + let ffi_buffer = bios::FfiBuffer::new(buffer); + let res = (api.serial_read)( + self.0, + ffi_buffer, + bios::FfiOption::Some(bios::Timeout::new_ms(0)), + ); + res.into() } } impl core::fmt::Write for SerialConsole { fn write_str(&mut self, data: &str) -> core::fmt::Result { let api = API.get(); - let is_panic = IS_PANIC.load(Ordering::SeqCst); + let is_panic = IS_PANIC.load(Ordering::Relaxed); let res = (api.serial_write)( // Which port self.0, // Data - bios::ApiByteSlice::new(data.as_bytes()), + bios::FfiByteSlice::new(data.as_bytes()), // No timeout - bios::Option::None, + bios::FfiOption::None, ); if !is_panic { res.unwrap(); @@ -173,13 +306,12 @@ impl core::fmt::Write for SerialConsole { pub struct Ctx { config: config::Config, - keyboard: pc_keyboard::EventDecoder, tpa: program::TransientProgramArea, } impl core::fmt::Write for Ctx { fn write_str(&mut self, data: &str) -> core::fmt::Result { - print!("{}", data); + osprint!("{}", data); Ok(()) } } @@ -249,28 +381,32 @@ pub extern "C" fn os_main(api: &bios::Api) -> ! { height as isize, ); vga.clear(); - unsafe { - VGA_CONSOLE = Some(vga); - } - println!("Configured VGA console {}x{}", width, height); + let mut guard = VGA_CONSOLE.lock(); + *guard = Some(vga); + // Drop the lock before trying to grab it again to print something! + drop(guard); + osprintln!("\u{001b}[0mConfigured VGA console {}x{}", width, height); } } if let Some((idx, serial_config)) = config.get_serial_console() { let _ignored = (api.serial_configure)(idx, serial_config); - unsafe { SERIAL_CONSOLE = Some(SerialConsole(idx)) }; - println!("Configured Serial console on Serial {}", idx); + let mut guard = SERIAL_CONSOLE.lock(); + *guard = Some(SerialConsole(idx)); + // Drop the lock before trying to grab it again to print something! + drop(guard); + osprintln!("Configured Serial console on Serial {}", idx); } - // Now we can call println! - println!("Welcome to {}!", OS_VERSION); - println!("Copyright © Jonathan 'theJPster' Pallant and the Neotron Developers, 2022"); + // Now we can call osprintln! + osprintln!("\u{001b}[44;33;1m{}\u{001b}[0m", OS_VERSION); + osprintln!("\u{001b}[41;37;1mCopyright © Jonathan 'theJPster' Pallant and the Neotron Developers, 2022\u{001b}[0m"); let (tpa_start, tpa_size) = match (api.memory_get_region)(0) { - bios::Option::None => { + bios::FfiOption::None => { panic!("No TPA offered by BIOS!"); } - bios::Option::Some(tpa) => { + bios::FfiOption::Some(tpa) => { if tpa.length < 256 { panic!("TPA not large enough"); } @@ -284,86 +420,29 @@ pub extern "C" fn os_main(api: &bios::Api) -> ! { let mut ctx = Ctx { config, - keyboard: pc_keyboard::EventDecoder::new( - pc_keyboard::layouts::AnyLayout::Uk105Key(pc_keyboard::layouts::Uk105Key), - pc_keyboard::HandleControl::MapLettersToUnicode, - ), tpa: unsafe { // We have to trust the values given to us by the BIOS. If it lies, we will crash. program::TransientProgramArea::new(tpa_start, tpa_size) }, }; - println!( - "TPA: {} bytes @ {:p}", + osprintln!( + "\u{001b}[7mTPA: {} bytes @ {:p}\u{001b}[0m", ctx.tpa.as_slice_u8().len(), ctx.tpa.as_slice_u8().as_ptr() ); + // Show the cursor + osprint!("\u{001b}[?25h"); + let mut buffer = [0u8; 256]; let mut menu = menu::Runner::new(&commands::OS_MENU, &mut buffer, ctx); loop { - match (api.hid_get_event)() { - bios::Result::Ok(bios::Option::Some(bios::hid::HidEvent::KeyPress(code))) => { - let pckb_ev = pc_keyboard::KeyEvent { - code, - state: pc_keyboard::KeyState::Down, - }; - if let Some(pc_keyboard::DecodedKey::Unicode(mut ch)) = - menu.context.keyboard.process_keyevent(pckb_ev) - { - if ch == '\n' { - ch = '\r'; - } - let mut buffer = [0u8; 6]; - let s = ch.encode_utf8(&mut buffer); - for b in s.as_bytes() { - menu.input_byte(*b); - } - } - } - bios::Result::Ok(bios::Option::Some(bios::hid::HidEvent::KeyRelease(code))) => { - let pckb_ev = pc_keyboard::KeyEvent { - code, - state: pc_keyboard::KeyState::Up, - }; - if let Some(pc_keyboard::DecodedKey::Unicode(ch)) = - menu.context.keyboard.process_keyevent(pckb_ev) - { - let mut buffer = [0u8; 6]; - let s = ch.encode_utf8(&mut buffer); - for b in s.as_bytes() { - menu.input_byte(*b); - } - } - } - bios::Result::Ok(bios::Option::Some(bios::hid::HidEvent::MouseInput(_ignore))) => {} - bios::Result::Ok(bios::Option::None) => { - // Do nothing - } - bios::Result::Err(e) => { - println!("Failed to get HID events: {:?}", e); - } - } - if let Some((uart_dev, _serial_conf)) = menu.context.config.get_serial_console() { - loop { - let mut buffer = [0u8; 8]; - let wrapper = neotron_common_bios::ApiBuffer::new(&mut buffer); - match (api.serial_read)(uart_dev, wrapper, neotron_common_bios::Option::None) { - neotron_common_bios::Result::Ok(n) if n == 0 => { - break; - } - neotron_common_bios::Result::Ok(n) => { - for b in &buffer[0..n] { - menu.input_byte(*b); - } - } - _ => { - break; - } - } - } + let mut buffer = [0u8; 16]; + let count = { STD_INPUT.lock().get_data(&mut buffer) }; + for b in &buffer[0..count] { + menu.input_byte(*b); } (api.power_idle)(); } @@ -372,10 +451,10 @@ pub extern "C" fn os_main(api: &bios::Api) -> ! { /// Called when we have a panic. #[inline(never)] #[panic_handler] -#[cfg(not(feature = "lib-mode"))] +#[cfg(not(any(feature = "lib-mode", test)))] fn panic(info: &core::panic::PanicInfo) -> ! { - IS_PANIC.store(true, Ordering::SeqCst); - println!("PANIC!\n{:#?}", info); + IS_PANIC.store(true, Ordering::Relaxed); + osprintln!("PANIC!\n{:#?}", info); let api = API.get(); loop { (api.power_idle)(); diff --git a/src/program.rs b/src/program.rs index c629091..5540438 100644 --- a/src/program.rs +++ b/src/program.rs @@ -4,7 +4,7 @@ use embedded_sdmmc::File; use crate::{ fs::{BiosBlock, BiosTime}, - print, println, + osprint, osprintln, }; #[allow(unused)] @@ -90,7 +90,7 @@ impl FileSource { offset: u32, out_buffer: &mut [u8], ) -> Result<(), embedded_sdmmc::Error> { - println!("Reading from {}", offset); + osprintln!("Reading from {}", offset); self.file.borrow_mut().seek_from_start(offset).unwrap(); self.mgr .borrow_mut() @@ -117,7 +117,7 @@ impl neotron_loader::traits::Source for &FileSource { } } - println!("Reading from {}", offset); + osprintln!("Reading from {}", offset); self.file.borrow_mut().seek_from_start(offset).unwrap(); self.mgr.borrow_mut().read( &self.volume, @@ -205,7 +205,7 @@ impl TransientProgramArea { /// /// The program must be in the Neotron Executable format. pub fn load_program(&mut self, file_name: &str) -> Result<(), Error> { - println!("Loading /{} from Block Device 0", file_name); + osprintln!("Loading /{} from Block Device 0", file_name); let bios_block = crate::fs::BiosBlock(); let time = crate::fs::BiosTime(); let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); @@ -227,7 +227,7 @@ impl TransientProgramArea { if ph.p_vaddr() as *mut u32 >= self.memory_bottom && ph.p_type() == neotron_loader::ProgramHeader::PT_LOAD { - println!("Loading {} bytes to 0x{:08x}", ph.p_memsz(), ph.p_vaddr()); + osprintln!("Loading {} bytes to 0x{:08x}", ph.p_memsz(), ph.p_vaddr()); let ram = unsafe { core::slice::from_raw_parts_mut(ph.p_vaddr() as *mut u8, ph.p_memsz() as usize) }; @@ -287,7 +287,7 @@ impl TransientProgramArea { extern "C" fn print_fn(data: *const u8, len: usize) { let slice = unsafe { core::slice::from_raw_parts(data, len) }; if let Ok(s) = core::str::from_utf8(slice) { - print!("{}", s); + osprint!("{}", s); } else { // Ignore App output - not UTF-8 } @@ -319,13 +319,13 @@ extern "C" fn api_write( buffer: neotron_api::FfiByteSlice, ) -> neotron_api::Result<()> { if fd == neotron_api::file::Handle::new_stdout() { - if let Some(ref mut console) = unsafe { &mut crate::VGA_CONSOLE } { + let mut guard = crate::VGA_CONSOLE.lock(); + if let Some(console) = guard.as_mut() { console.write_bstr(buffer.as_slice()); } - if let Some(ref mut console) = unsafe { &mut crate::SERIAL_CONSOLE } { - if let Err(_e) = console.write_bstr(buffer.as_slice()) { - return neotron_api::Result::Err(neotron_api::Error::DeviceSpecific); - } + let mut guard = crate::SERIAL_CONSOLE.lock(); + if let Some(console) = guard.as_mut() { + console.write_bstr(buffer.as_slice()); } neotron_api::Result::Ok(()) } else { @@ -337,10 +337,19 @@ extern "C" fn api_write( /// /// If you hit the end of the file, you might get less data than you asked for. extern "C" fn api_read( - _fd: neotron_api::file::Handle, - _buffer: neotron_api::FfiBuffer, + fd: neotron_api::file::Handle, + mut buffer: neotron_api::FfiBuffer, ) -> neotron_api::Result { - neotron_api::Result::Err(neotron_api::Error::Unimplemented) + if fd == neotron_api::file::Handle::new_stdin() { + if let Some(buffer) = buffer.as_mut_slice() { + let count = { crate::STD_INPUT.lock().get_data(buffer) }; + Ok(count).into() + } else { + neotron_api::Result::Err(neotron_api::Error::DeviceSpecific) + } + } else { + neotron_api::Result::Err(neotron_api::Error::BadHandle) + } } /// Move the file offset (for the given file handle) to the given position. diff --git a/src/refcell.rs b/src/refcell.rs new file mode 100644 index 0000000..b9d1969 --- /dev/null +++ b/src/refcell.rs @@ -0,0 +1,155 @@ +//! # RefCells for Neotron OS. +//! +//! Like the `RefCell` in the standard library, except that it's thread-safe +//! and uses the BIOS critical section to make it so. + +// =========================================================================== +// Modules and Imports +// =========================================================================== + +use core::{ + cell::UnsafeCell, + ops::{Deref, DerefMut}, + sync::atomic::{AtomicBool, Ordering}, +}; + +// =========================================================================== +// Global Variables +// =========================================================================== + +// None + +// =========================================================================== +// Macros +// =========================================================================== + +// None + +// =========================================================================== +// Public types +// =========================================================================== + +/// Indicates a failure to lock the refcell because it was already locked. +#[derive(Debug)] +pub struct LockError; + +/// A cell that gives you references, and is thread-safe. +/// +/// Uses the BIOS to ensure thread-safety whilst checking if the lock is taken +/// or not. +pub struct CsRefCell { + inner: UnsafeCell, + locked: AtomicBool, +} + +impl CsRefCell { + /// Create a new cell. + pub const fn new(value: T) -> CsRefCell { + CsRefCell { + inner: UnsafeCell::new(value), + locked: AtomicBool::new(false), + } + } + + /// Try and do something with the lock. + pub fn with(&self, f: F) -> Result + where + F: FnOnce(&mut CsRefCellGuard) -> U, + { + let mut guard = self.try_lock()?; + let result = f(&mut guard); + drop(guard); + Ok(result) + } + + /// Lock the cell. + /// + /// If you can't lock it (because it is already locked), this function will panic. + pub fn lock(&self) -> CsRefCellGuard { + self.try_lock().unwrap() + } + + /// Try and grab the lock. + /// + /// It'll fail if it's already been taken. + /// + /// It'll panic if the global lock is in a bad state, or you try and + /// re-enter this function from an interrupt whilst the global lock is held. + /// Don't do that. + pub fn try_lock(&self) -> Result, LockError> { + let api = crate::API.get(); + + if (api.compare_and_swap_bool)(&self.locked, false, true) { + // succesfully swapped `false` for `true` + core::sync::atomic::fence(Ordering::Acquire); + Ok(CsRefCellGuard { parent: self }) + } else { + // cell is already locked + Err(LockError) + } + } +} + +/// Mark our type as thread-safe. +/// +/// # Safety +/// +/// We use the BIOS critical sections to control access. Thus it is now +/// thread-safe. +unsafe impl Sync for CsRefCell {} + +/// Represents an active borrow of a [`CsRefCell`]. +pub struct CsRefCellGuard<'a, T> { + parent: &'a CsRefCell, +} + +impl<'a, T> Deref for CsRefCellGuard<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + let ptr = self.parent.inner.get(); + unsafe { &*ptr } + } +} + +impl<'a, T> DerefMut for CsRefCellGuard<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + let ptr = self.parent.inner.get(); + unsafe { &mut *ptr } + } +} + +impl<'a, T> Drop for CsRefCellGuard<'a, T> { + fn drop(&mut self) { + // We hold this refcell guard exclusively, so this can't race + self.parent.locked.store(false, Ordering::Release); + } +} + +// =========================================================================== +// Private types +// =========================================================================== + +// None + +// =========================================================================== +// Private functions +// =========================================================================== + +// None + +// =========================================================================== +// Public functions +// =========================================================================== + +// None + +// =========================================================================== +// Tests +// =========================================================================== + +// None + +// =========================================================================== +// End of file +// =========================================================================== diff --git a/src/vgaconsole.rs b/src/vgaconsole.rs index c21ca97..7bb7354 100644 --- a/src/vgaconsole.rs +++ b/src/vgaconsole.rs @@ -2,50 +2,196 @@ //! //! Code for dealing with a VGA-style console, where there's a buffer of 16-bit //! values, each corresponding to a glyph and some attributes. +//! +//! The input to this sub-system is a stream of bytes, which should be valid +//! UTF-8 and may contain ANSI escape sequences. +//! +//! The ANSI sequences are parsed using Alacritty's [vte +//! crate](https://crates.io/crates/vte). We then interpret the 'action' codes +//! as best we can. We also interpret commands like `\n` as a New Line (move the +//! cursor down one line), and `\r` as Carriage Return (move the cursor back to +//! Column 0). Note that we use 0-based row and column numbers internally, but +//! we understand ANSI sequences that use 1-based indidices. +//! +//! You can write to the VGA console because `core::fmt::Write` is implemented +//! for `VgaConsole`. Any text sent this way will be sent through the ANSI +//! decoder. Anything that's not an ANSI sequence will be converted to Code Page +//! 850 and then added to the 2D array of glyphs and attributes that is our text +//! buffer. We then assume that some other code somewhere else will take these +//! values and put them on a video screen somehow. + +// =========================================================================== +// Modules and Imports +// =========================================================================== use neotron_common_bios::video::{Attr, TextBackgroundColour, TextForegroundColour}; +// =========================================================================== +// Global Variables +// =========================================================================== + +// =========================================================================== +// Macros +// =========================================================================== + +// =========================================================================== +// Public types +// =========================================================================== + +/// Represents our simulation of a DEC-like ANSI video terminal. pub struct VgaConsole { + inner: ConsoleInner, + parser: vte::Parser<16>, +} + +impl VgaConsole { + /// White on Black + const DEFAULT_ATTR: Attr = Attr::new( + TextForegroundColour::LIGHT_GRAY, + TextBackgroundColour::BLACK, + false, + ); + + pub fn new(addr: *mut u8, width: isize, height: isize) -> VgaConsole { + VgaConsole { + inner: ConsoleInner { + addr, + width, + height, + row: 0, + col: 0, + attr: Self::DEFAULT_ATTR, + bright: false, + reverse: false, + cursor_wanted: false, + cursor_holder: None, + cursor_depth: 0, + }, + parser: vte::Parser::new_with_size(), + } + } + + /// Clear the screen. + /// + /// Every character on the screen is replaced with an space (U+0020). + pub fn clear(&mut self) { + self.inner.cursor_disable(); + self.inner.clear(); + self.inner.cursor_enable(); + } + + /// Set the default attribute for any future text. + pub fn set_attr(&mut self, attr: Attr) { + self.inner.set_attr(attr); + } + + /// Write a UTF-8 byte string to the console. + /// + /// Is parsed for ANSI codes, and Unicode is converted to Code Page 850 for + /// display on the VGA screen. + pub fn write_bstr(&mut self, bstr: &[u8]) { + self.inner.cursor_disable(); + for b in bstr { + self.parser.advance(&mut self.inner, *b); + } + self.inner.cursor_enable(); + } +} + +// =========================================================================== +// Private types +// =========================================================================== + +/// Handles the inner details of where we are on screen. +/// +/// Separate from the parser, so it can be passed to the `advance` method. +struct ConsoleInner { addr: *mut u8, width: isize, height: isize, row: isize, col: isize, attr: Attr, + bright: bool, + reverse: bool, + cursor_wanted: bool, + cursor_depth: u8, + cursor_holder: Option, } -impl VgaConsole { - /// White on Black +impl ConsoleInner { const DEFAULT_ATTR: Attr = Attr::new( - TextForegroundColour::WHITE, + TextForegroundColour::LIGHT_GRAY, TextBackgroundColour::BLACK, false, ); - pub fn new(addr: *mut u8, width: isize, height: isize) -> VgaConsole { - VgaConsole { - addr, - width, - height, - row: 0, - col: 0, - attr: Self::DEFAULT_ATTR, + /// Replace the glyph at the current location with a cursor. + fn cursor_enable(&mut self) { + self.cursor_depth -= 1; + if self.cursor_depth == 0 && self.cursor_wanted && self.cursor_holder.is_none() { + // Remember what was where our cursor is (unless the cursor is off-screen, when we make something up) + if self.row >= 0 && self.row < self.height && self.col >= 0 && self.col < self.width { + let value = self.read(); + self.write_at(self.row, self.col, b'_', true); + self.cursor_holder = Some(value); + } else { + self.cursor_holder = Some(b' '); + } } } - fn move_char_right(&mut self) { - self.col += 1; + /// Replace the cursor at the current location with its previous contents. + fn cursor_disable(&mut self) { + if let Some(glyph) = self.cursor_holder.take() { + if self.row >= 0 && self.row < self.height && self.col >= 0 && self.col < self.width { + // cursor was on-screen, so restore it + self.write(glyph); + } + } + self.cursor_depth += 1; } - fn move_char_down(&mut self) { - self.row += 1; + /// Move the cursor relative to the current location. + /// + /// Clamps to the visible screen. + fn move_cursor_relative(&mut self, rows: isize, cols: isize) { + self.row += rows; + self.col += cols; + if self.row < 0 { + self.row = 0; + } + if self.col < 0 { + self.col = 0; + } + if self.row >= self.height { + self.row = self.height - 1; + } + if self.col >= self.width { + self.col = self.width - 1; + } + } + + /// Move the cursor to the given location. + /// + /// Clamps to the visible screen. + fn move_cursor_absolute(&mut self, rows: isize, cols: isize) { + // move it + self.row = rows; + self.col = cols; + // clamp it + self.move_cursor_relative(0, 0); } - fn reset_cursor(&mut self) { - self.row = 0; - self.col = 0; + /// Move the cursor to 0,0 + fn home(&mut self) { + self.move_cursor_absolute(0, 0); } + /// If we are currently positioned off-screen, scroll and fix that. + /// + /// We defer this so you can write the last char on the last line without + /// causing it to scroll pre-emptively. fn scroll_as_required(&mut self) { assert!(self.row <= self.height); if self.col >= self.width { @@ -58,61 +204,78 @@ impl VgaConsole { } } - pub fn clear(&mut self) { + /// Blank the screen + fn clear(&mut self) { for row in 0..self.height { for col in 0..self.width { - self.write_at(row, col, b' '); - } - } - self.reset_cursor(); - } - - pub fn write_bstr(&mut self, bstr: &[u8]) { - for b in bstr { - self.scroll_as_required(); - match b { - 0x08 => { - // This is a backspace, so we go back one character (if we - // can). We expect the caller to provide "\u{0008} \u{0008}" - // to actually erase the char then move the cursor over it. - if self.col > 0 { - self.col -= 1; - } - } - b'\r' => { - self.col = 0; - } - b'\n' => { - self.col = 0; - self.move_char_down(); - } - _ => { - self.write(*b); - self.move_char_right(); - } + self.write_at(row, col, b' ', false); } } + self.home(); } /// Set the default attribute for any future text. - pub fn set_attr(&mut self, attr: Attr) { + fn set_attr(&mut self, attr: Attr) { self.attr = attr; } - /// Put a glyph at the next position on the screen. + /// Put a glyph at the current position on the screen. + /// + /// Don't do this if the cursor is enabled. fn write(&mut self, glyph: u8) { - self.write_at(self.row, self.col, glyph); + self.write_at(self.row, self.col, glyph, false); } /// Put a glyph at a given position on the screen. - fn write_at(&mut self, row: isize, col: isize, glyph: u8) { + /// + /// Don't do this if the cursor is enabled. + fn write_at(&mut self, row: isize, col: isize, glyph: u8, is_cursor: bool) { assert!(row < self.height, "{} >= {}?", row, self.height); assert!(col < self.width, "{} => {}?", col, self.width); + if !crate::IS_PANIC.load(core::sync::atomic::Ordering::Relaxed) && !is_cursor { + assert!(self.cursor_holder.is_none()); + } + let offset = ((row * self.width) + col) * 2; unsafe { core::ptr::write_volatile(self.addr.offset(offset), glyph) }; - unsafe { core::ptr::write_volatile(self.addr.offset(offset + 1), self.attr.as_u8()) }; + let attr = if self.reverse { + let new_fg = self.attr.bg().as_u8(); + let new_bg = self.attr.fg().as_u8(); + Attr::new( + unsafe { TextForegroundColour::new_unchecked(new_fg) }, + unsafe { TextBackgroundColour::new_unchecked(new_bg & 0x07) }, + false, + ) + } else { + self.attr + }; + + unsafe { core::ptr::write_volatile(self.addr.offset(offset + 1), attr.as_u8()) }; + } + + /// Read a glyph at the current position + /// + /// Don't do this if the cursor is enabled. + fn read(&mut self) -> u8 { + self.read_at(self.row, self.col) } + /// Read a glyph at the given position + /// + /// Don't do this if the cursor is enabled. + fn read_at(&mut self, row: isize, col: isize) -> u8 { + assert!(row < self.height, "{} >= {}?", row, self.height); + assert!(col < self.width, "{} => {}?", col, self.width); + if !crate::IS_PANIC.load(core::sync::atomic::Ordering::Relaxed) { + assert!(self.cursor_holder.is_none()); + } + let offset = ((row * self.width) + col) * 2; + unsafe { core::ptr::read_volatile(self.addr.offset(offset)) } + } + + /// Move everyone on screen up one line, losing the top line. + /// + /// The bottom line will be all space characters. fn scroll_page(&mut self) { let row_len_bytes = self.width * 2; unsafe { @@ -122,10 +285,10 @@ impl VgaConsole { self.addr, (row_len_bytes * (self.height - 1)) as usize, ); - // Blank the bottom line of the screen (rows[height-1]). - for col in 0..self.width { - self.write_at(self.height - 1, col, b' '); - } + } + // Blank the bottom line of the screen (rows[height-1]). + for col in 0..self.width { + self.write_at(self.height - 1, col, b' ', false); } } @@ -138,7 +301,9 @@ impl VgaConsole { // This fixed table only works for the default font. When we support // changing font, we will need to plug-in a different table for each font. match input { - '\u{0000}'..='\u{007F}' => input as u8, + '\u{0020}'..='\u{007E}' => input as u8, + // 0x80 to 0x9F are the C1 control codes with no visual + // representation '\u{00A0}' => 255, // NBSP '\u{00A1}' => 173, // ¡ '\u{00A2}' => 189, // ¢ @@ -152,7 +317,7 @@ impl VgaConsole { '\u{00AA}' => 166, // ª '\u{00AB}' => 174, // « '\u{00AC}' => 170, // ¬ - '\u{00AD}' => 240, // SHY + '\u{00AD}' => 240, // - (Soft Hyphen) '\u{00AE}' => 169, // ® '\u{00AF}' => 238, // ¯ '\u{00B0}' => 248, // ° @@ -238,6 +403,17 @@ impl VgaConsole { '\u{0131}' => 213, // ı '\u{0192}' => 159, // ƒ '\u{2017}' => 242, // ‗ + '\u{2022}' => 7, // • + '\u{203C}' => 19, // ‼ + '\u{2190}' => 27, // ← + '\u{2191}' => 24, // ↑ + '\u{2192}' => 26, // → + '\u{2193}' => 25, // ↓ + '\u{2194}' => 29, // ↔ + '\u{2195}' => 18, // ↕ + '\u{21A8}' => 23, // ↨ + '\u{221F}' => 28, // ∟ + '\u{2302}' => 127, // ⌂ '\u{2500}' => 196, // ─ '\u{2502}' => 179, // │ '\u{250C}' => 218, // ┌ @@ -267,37 +443,1210 @@ impl VgaConsole { '\u{2592}' => 177, // ▒ '\u{2593}' => 178, // ▓ '\u{25A0}' => 254, // ■ + '\u{25AC}' => 22, // ▬ + '\u{25B2}' => 30, // ▲ + '\u{25BA}' => 16, // ► + '\u{25BC}' => 31, // ▼ + '\u{25C4}' => 17, // ◄ + '\u{25CB}' => 9, // ○ + '\u{25D8}' => 8, // ◘ + '\u{25D9}' => 10, // ◙ + '\u{263A}' => 1, // ☺ + '\u{263B}' => 2, // ☻ + '\u{263C}' => 15, // ☼ + '\u{2640}' => 12, // ♀ + '\u{2642}' => 11, // ♂ + '\u{2660}' => 6, // ♠ + '\u{2663}' => 5, // ♣ + '\u{2665}' => 3, // ♥ + '\u{2666}' => 4, // ♦ + '\u{266A}' => 13, // ♪ + '\u{266B}' => 14, // ♫ _ => b'?', } } } impl core::fmt::Write for VgaConsole { + /// Write a UTF-8 string slice to the console. + /// + /// Is parsed for ANSI codes, and Unicode is converted to Code Page 850 for + /// display on the VGA screen. fn write_str(&mut self, data: &str) -> core::fmt::Result { - for ch in data.chars() { - self.scroll_as_required(); - match ch { - '\u{0008}' => { - // This is a backspace, so we go back one character (if we - // can). We expect the caller to provide "\u{0008} \u{0008}" - // to actually erase the char then move the cursor over it. - if self.col > 0 { - self.col -= 1; + self.inner.cursor_disable(); + assert!(self.inner.cursor_holder.is_none()); + for b in data.bytes() { + self.parser.advance(&mut self.inner, b); + } + self.inner.cursor_enable(); + Ok(()) + } +} + +impl vte::Perform for ConsoleInner { + /// Draw a character to the screen and update states. + fn print(&mut self, ch: char) { + self.scroll_as_required(); + self.write(Self::map_char_to_glyph(ch)); + self.col += 1; + } + + /// Execute a C0 or C1 control function. + fn execute(&mut self, byte: u8) { + self.scroll_as_required(); + match byte { + 0x08 => { + // This is a backspace, so we go back one character (if we + // can). We expect the caller to provide "\u{0008} \u{0008}" + // to actually erase the char then move the cursor over it. + if self.col > 0 { + self.col -= 1; + } + } + b'\r' => { + self.col = 0; + } + b'\n' => { + self.col = 0; + self.row += 1; + } + _ => { + // ignore unknown C0 or C1 control code + } + } + // We may now be off-screen, but that's OK because we will scroll before + // we print the next thing. + } + + /// A final character has arrived for a CSI sequence + /// + /// The `ignore` flag indicates that either more than two intermediates arrived + /// or the number of parameters exceeded the maximum supported length, + /// and subsequent characters were ignored. + fn csi_dispatch( + &mut self, + params: &vte::Params, + intermediates: &[u8], + _ignore: bool, + action: char, + ) { + // Just in case you want a single parameter, here it is + let mut first = *params.iter().next().and_then(|s| s.first()).unwrap_or(&1) as isize; + let mut second = *params.iter().nth(1).and_then(|s| s.first()).unwrap_or(&1) as isize; + + match action { + 'm' => { + // Select Graphic Rendition + for p in params.iter() { + let Some(p) = p.first() else { + // Can't handle sub-params, i.e. params with more than one value + return; + }; + match *p { + 0 => { + // Reset, or normal + self.attr = Self::DEFAULT_ATTR; + self.bright = false; + self.reverse = false; + } + 1 => { + // Bold intensity + self.bright = true; + } + 7 => { + // Reverse video + self.reverse = true; + } + 22 => { + // Normal intensity + self.bright = false; + } + // Foreground + 30 => { + self.attr.set_fg(TextForegroundColour::BLACK); + } + 31 => { + self.attr.set_fg(TextForegroundColour::RED); + } + 32 => { + self.attr.set_fg(TextForegroundColour::GREEN); + } + 33 => { + self.attr.set_fg(TextForegroundColour::BROWN); + } + 34 => { + self.attr.set_fg(TextForegroundColour::BLUE); + } + 35 => { + self.attr.set_fg(TextForegroundColour::MAGENTA); + } + 36 => { + self.attr.set_fg(TextForegroundColour::CYAN); + } + 37 | 39 => { + self.attr.set_fg(TextForegroundColour::LIGHT_GRAY); + } + // Background + 40 => { + self.attr.set_bg(TextBackgroundColour::BLACK); + } + 41 => { + self.attr.set_bg(TextBackgroundColour::RED); + } + 42 => { + self.attr.set_bg(TextBackgroundColour::GREEN); + } + 43 => { + self.attr.set_bg(TextBackgroundColour::BROWN); + } + 44 => { + self.attr.set_bg(TextBackgroundColour::BLUE); + } + 45 => { + self.attr.set_bg(TextBackgroundColour::MAGENTA); + } + 46 => { + self.attr.set_bg(TextBackgroundColour::CYAN); + } + 47 | 49 => { + self.attr.set_bg(TextBackgroundColour::LIGHT_GRAY); + } + _ => { + // Ignore unknown code + } } } - '\r' => { - self.col = 0; + // Now check if we're bright, and make it brighter. We do this + // last, because they might set the colour first and set the + // bright bit afterwards. + if self.bright { + match self.attr.fg() { + TextForegroundColour::BLACK => { + self.attr.set_fg(TextForegroundColour::DARK_GRAY); + } + TextForegroundColour::RED => { + self.attr.set_fg(TextForegroundColour::LIGHT_RED); + } + TextForegroundColour::GREEN => { + self.attr.set_fg(TextForegroundColour::LIGHT_GREEN); + } + TextForegroundColour::BROWN => { + self.attr.set_fg(TextForegroundColour::YELLOW); + } + TextForegroundColour::BLUE => { + self.attr.set_fg(TextForegroundColour::LIGHT_BLUE); + } + TextForegroundColour::MAGENTA => { + self.attr.set_fg(TextForegroundColour::PINK); + } + TextForegroundColour::CYAN => { + self.attr.set_fg(TextForegroundColour::LIGHT_CYAN); + } + TextForegroundColour::LIGHT_GRAY => { + self.attr.set_fg(TextForegroundColour::WHITE); + } + _ => { + // Do nothing + } + } } - '\n' => { - self.col = 0; - self.move_char_down(); + } + 'A' => { + // Cursor Up + if first == 0 { + first = 1; } - _ => { - self.write(Self::map_char_to_glyph(ch)); - self.move_char_right(); + self.move_cursor_relative(-first, 0); + } + 'B' => { + // Cursor Down + if first == 0 { + first = 1; } + self.move_cursor_relative(first, 0); + } + 'C' => { + // Cursor Forward + if first == 0 { + first = 1; + } + self.move_cursor_relative(0, first); + } + 'D' => { + // Cursor Back + if first == 0 { + first = 1; + } + self.move_cursor_relative(0, -first); + } + 'E' => { + // Cursor next line + if first == 0 { + first = 1; + } + self.move_cursor_absolute(self.row + first, 0); + } + 'F' => { + // Cursor previous line + if first == 0 { + first = 1; + } + self.move_cursor_absolute(self.row - first, 0); + } + 'G' => { + // Cursor horizontal absolute + if first == 0 { + first = 1; + } + // We are zero-indexed, ANSI is 1-indexed + self.move_cursor_absolute(self.row, first - 1); + } + 'H' | 'f' => { + // Cursor Position (or Horizontal Vertical Position) + if first == 0 { + first = 1; + } + if second == 0 { + second = 1; + } + // We are zero-indexed, ANSI is 1-indexed + self.move_cursor_absolute(first - 1, second - 1); + } + 'J' => { + // Erase in Display + match first { + 0 => { + // Erase the cursor through the end of the display + for row in 0..self.height { + for col in 0..self.width { + if row > self.row || (row == self.row && col >= self.col) { + self.write_at(row, col, b' ', false); + } + } + } + } + 1 => { + // Erase from the beginning of the display through the cursor + for row in 0..self.height { + for col in 0..self.width { + if row < self.row || (row == self.row && col <= self.col) { + self.write_at(row, col, b' ', false); + } + } + } + } + 2 => { + // Erase the complete display + for row in 0..self.height { + for col in 0..self.width { + self.write_at(row, col, b' ', false); + } + } + } + _ => { + // Ignore it + } + } + } + 'K' => { + // Erase in Line + match first { + 0 => { + // Erase the cursor through the end of the line + for col in self.col..self.width { + self.write_at(self.row, col, b' ', false); + } + } + 1 => { + // Erase from the beginning of the line through the cursor + for col in 0..=self.col { + self.write_at(self.row, col, b' ', false); + } + } + 2 => { + // Erase the complete line + for col in 0..self.width { + self.write_at(self.row, col, b' ', false); + } + } + _ => { + // Ignore it + } + } + } + 'n' if first == 6 => { + // Device Status Report - todo. + // + // We should send "\u{001b}[;R" where and + // are integers for 1-indexed rows and columns + // respectively. But for that we need an input buffer to put bytes into. + } + 'h' if intermediates.first().cloned() == Some(b'?') => { + // DEC special code for Cursor On. It'll be activated whenever + // we finish what we're printing. + self.cursor_wanted = true; + } + 'l' if intermediates.first().cloned() == Some(b'?') => { + // DEC special code for Cursor Off. + self.cursor_wanted = false; + } + _ => { + // Unknown code - ignore it } } - Ok(()) } } + +// =========================================================================== +// Private functions +// =========================================================================== + +// None + +// =========================================================================== +// Public functions +// =========================================================================== + +// None + +// =========================================================================== +// Tests +// =========================================================================== + +#[cfg(test)] +mod tests { + use super::VgaConsole; + const WIDTH: usize = 12; + const HEIGHT: usize = 7; + + /// Convert a text buffer into a string we can compare against. + /// + /// Each glyph and attribute is printed like "xx yy", separated by "|" for + /// each column, and "\n" for each row. + fn print_buffer(buffer: &[u8]) -> String { + use std::fmt::Write; + let mut output = String::new(); + let mut pos = 0; + for _r in 0..HEIGHT { + for _c in 0..WIDTH { + write!(output, "{:02x} {:02x}|", buffer[pos], buffer[pos + 1]).unwrap(); + pos += 2; + } + writeln!(output).unwrap(); + } + output + } + + #[test] + fn basic_print() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"Hello\n"); + assert_eq!( + print_buffer(&buffer), + "\ + 48 07|65 07|6c 07|6c 07|6f 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 0); + } + + #[test] + fn cr_overprint() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"0\r1\n"); + // Second row + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 0); + // The '1' has replaced the 0 + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + // We are on the second row + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 0); + } + + #[test] + fn scroll() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"0\n"); + console.write_bstr(b"1\n"); + for _ in 0..HEIGHT - 1 { + console.write_bstr(b"\n"); + } + // We are now off the bottom of the screen + assert_eq!(console.inner.row, HEIGHT as isize); + assert_eq!(console.inner.col, 0); + // And the '1' is on the top row + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n" + ); + } + + #[test] + fn home1() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Print 0 and replace it with a 1 + console.write_bstr(b"0\n\x1b[0;0H1\n"); + // We are on the second row + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 0); + // And the '1' has replaced the '0' + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + } + + #[test] + fn home2() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Print 0 and replace it with a 1 + console.write_bstr(b"0\n\x1b[1;1H1\n"); + // And the '1' has replaced the '0' + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + } + + #[test] + fn home3() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Print 0 and replace it with a 1 + console.write_bstr(b"0\n\x1b[H1\n"); + // The '1' has replaced the '0' + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + // We are on the second row + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 0); + } + + #[test] + fn movecursor() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Print 0 and replace it with a 1 + console.write_bstr(b"\x1b[2;2H1"); + assert_eq!( + print_buffer(&buffer), + "\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + // We are on the second row + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 2); + } + + #[test] + fn sgr_reset() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"\x1b[0m1"); + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 0); + assert_eq!(console.inner.col, 1); + } + + #[test] + fn sgr_backgrounds() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // +-------+-----+-----+-----+-----+-----+-----+-----+ + // + BLINK | BG2 | BG1 | BG0 | FG3 | FG2 | FG1 | FG0 | + // +-------+-----+-----+-----+-----+-----+-----+-----+ + let colour_map = [ + "40", // Light Gray on Black + "41", // Light Gray on Red + "42", // Light Gray on Green + "43", // Light Gray on Yellow + "44", // Light Gray on Blue + "45", // Light Gray on Magenta + "46", // Light Gray on Cyan + "47", // Light Gray on White + ]; + + for ansi in colour_map.iter() { + console.write_bstr(b"\x1b["); + console.write_bstr(ansi.as_bytes()); + console.write_bstr(b"m1"); + } + + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|31 47|31 27|31 67|31 17|31 57|31 37|31 77|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + } + + #[test] + fn sgr_foregrounds() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // +-------+-----+-----+-----+-----+-----+-----+-----+ + // + BLINK | BG2 | BG1 | BG0 | FG3 | FG2 | FG1 | FG0 | + // +-------+-----+-----+-----+-----+-----+-----+-----+ + let colour_map = [ + "30", // Black on Black + "31", // Red on Black + "32", // Green on Black + "33", // Yellow on Black + "34", // Blue on Black + "35", // Magenta on Black + "36", // Cyan on Black + "37", // White on Black + ]; + + for ansi in colour_map.iter() { + console.write_bstr(b"\x1b["); + console.write_bstr(ansi.as_bytes()); + console.write_bstr(b"m1"); + } + + assert_eq!( + print_buffer(&buffer), + "\ + 31 00|31 04|31 02|31 06|31 01|31 05|31 03|31 07|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + } + + #[test] + fn sgr_bold() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // +-------+-----+-----+-----+-----+-----+-----+-----+ + // + BLINK | BG2 | BG1 | BG0 | FG3 | FG2 | FG1 | FG0 | + // +-------+-----+-----+-----+-----+-----+-----+-----+ + let colour_map = [ + "30", // Bright Black on Black + "31", // Bright Red on Black + "32", // Bright Green on Black + "33", // Bright Yellow on Black + "34", // Bright Blue on Black + "35", // Bright Magenta on Black + "36", // Bright Cyan on Black + "37", // Bright White on Black + ]; + + console.write_bstr(b"\x1b[1m"); + + for ansi in colour_map.iter() { + console.write_bstr(b"\x1b["); + console.write_bstr(ansi.as_bytes()); + console.write_bstr(b"m1"); + } + + assert_eq!( + print_buffer(&buffer), + "\ + 31 08|31 0c|31 0a|31 0e|31 09|31 0d|31 0b|31 0f|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + } + + #[test] + fn sgr_all_three() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // +-------+-----+-----+-----+-----+-----+-----+-----+ + // + BLINK | BG2 | BG1 | BG0 | FG3 | FG2 | FG1 | FG0 | + // +-------+-----+-----+-----+-----+-----+-----+-----+ + let colour_map = [ + "1;40;37", // Bright White on Black + "0", // Default + "33;44", // Brown on Blue + ]; + + for ansi in colour_map.iter() { + console.write_bstr(b"\x1b["); + console.write_bstr(ansi.as_bytes()); + console.write_bstr(b"m1"); + } + + assert_eq!( + print_buffer(&buffer), + "\ + 31 0f|31 07|31 16|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + } + + #[test] + fn cursor_up() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Go home, print 0\n then go up a line and replace the 0 with a 1 + console.write_bstr(b"\x1b[H0\n\x1b[A1"); + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 0); + assert_eq!(console.inner.col, 1); + // Go home, print 0\n then go up a line and replace the 0 with a 2 + console.write_bstr(b"\x1b[H0\n\x1b[0A2"); + assert_eq!( + print_buffer(&buffer), + "\ + 32 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 0); + assert_eq!(console.inner.col, 1); + // Go home, print 0\n then go up a line and replace the 0 with a 3 + console.write_bstr(b"\x1b[H0\n\x1b[1A3"); + assert_eq!( + print_buffer(&buffer), + "\ + 33 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 0); + assert_eq!(console.inner.col, 1); + // Go home then print 40\n50\n60\n7 then go up two lines and replace the 0 of 50 with a 8 + console.write_bstr(b"\x1b[H40\n50\n60\n7\x1b[2A8"); + assert_eq!( + print_buffer(&buffer), + "\ + 34 07|30 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 35 07|38 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 36 07|30 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 37 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 2); + } + + #[test] + fn cursor_down() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Go home, go down 1 line, and print 0 + console.write_bstr(b"\x1b[H\x1b[B0"); + assert_eq!( + print_buffer(&buffer), + "\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 30 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + // go down 1 line, and print 1 + console.write_bstr(b"\x1b[0B1"); + assert_eq!( + print_buffer(&buffer), + "\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 30 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 2); + assert_eq!(console.inner.col, 2); + // go down 1 line, and print 2 + console.write_bstr(b"\x1b[1B2"); + assert_eq!( + print_buffer(&buffer), + "\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 30 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|32 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 3); + assert_eq!(console.inner.col, 3); + // go down 2 lines, and print 3 + console.write_bstr(b"\x1b[2B3"); + assert_eq!( + print_buffer(&buffer), + "\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 30 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|32 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|33 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 5); + assert_eq!(console.inner.col, 4); + } + + #[test] + fn cursor_forward() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Print .0.1.2..3 + console.write_bstr(b"\x1b[C0"); + console.write_bstr(b"\x1b[0C1"); + console.write_bstr(b"\x1b[1C2"); + console.write_bstr(b"\x1b[2C3"); + assert_eq!( + print_buffer(&buffer), + "\ + 00 00|30 07|00 00|31 07|00 00|32 07|00 00|00 00|33 07|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + } + + #[test] + fn cursor_backwards() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Print 123 then replace the 3 with a 4 + console.write_bstr(b"123\x1b[D4"); + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|32 07|34 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + // Replace the 4 with a 5 + console.write_bstr(b"\x1b[0D5"); + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|32 07|35 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + // Replace the 5 with a 6 + console.write_bstr(b"\x1b[1D6"); + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|32 07|36 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + // Replace the 2 with a 7 + console.write_bstr(b"\x1b[2D7"); + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|37 07|36 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + } + + #[test] + fn cursor_next_line() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Go home, print xxx, go down 1 line, and print 0 + console.write_bstr(b"\x1b[Hxxx\x1b[E0"); + // We should have returned to col 0 for the '0' so are in col 1 + assert_eq!( + print_buffer(&buffer), + "\ + 78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 30 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + // go down 1 line, and print 1 + console.write_bstr(b"xxx\x1b[0E1"); + assert_eq!( + print_buffer(&buffer), + "\ + 78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 30 07|78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 2); + assert_eq!(console.inner.col, 1); + // go down 1 line, and print 2 + console.write_bstr(b"xxx\x1b[1E2"); + assert_eq!( + print_buffer(&buffer), + "\ + 78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 30 07|78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 31 07|78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 32 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 3); + assert_eq!(console.inner.col, 1); + // go down 2 lines, and print 3 + console.write_bstr(b"xxx\x1b[2E3"); + assert_eq!( + print_buffer(&buffer), + "\ + 78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 30 07|78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 31 07|78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 32 07|78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 33 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 5); + assert_eq!(console.inner.col, 1); + } + + #[test] + fn cursor_previous_line() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Print xx, xx, 11, 22, 33, 456 on the first five lines + // Then go back and replace 4 with 7, 3 with 8, 2 with 9 and the first x with 0 + console.write_bstr(b"xx\nxx\n11\n22\n33\n456\x1b[F7\x1b[0F8\x1b[1F9\x1b[2F0"); + // We should be back up on the top row + assert_eq!( + print_buffer(&buffer), + "\ + 30 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 39 07|31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 38 07|32 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 37 07|33 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 34 07|35 07|36 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 0); + assert_eq!(console.inner.col, 1); + } + + #[test] + fn cursor_horizontal_absolute() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // Print 12345 the replace the 3 with a 9 + console.write_bstr(b"12345\x1b[3G9"); + assert_eq!( + print_buffer(&buffer), + "\ + 31 07|32 07|39 07|34 07|35 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 0); + assert_eq!(console.inner.col, 3); + } + + #[test] + fn cursor_position() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + // In row;col form. + console.write_bstr(b"xxx\x1b[H0\x1b[;3H1\x1b[2;H2\x1b[3;4H3"); + // the 4 should be in the right-hand column, and the 5 should wrap + // around and start on the next row. + console.write_bstr(format!("\x1b[4;{}H45", WIDTH).as_bytes()); + assert_eq!( + print_buffer(&buffer), + "\ + 30 07|78 07|31 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 32 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|33 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|34 07|\n\ + 35 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 4); + assert_eq!(console.inner.col, 1); + } + + #[test] + fn erase_in_display_cursor_to_end() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"xxx\nxxx\n\x1b[2;2H"); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + console.write_bstr(b"\x1b[0J"); + assert_eq!( + print_buffer(&buffer), + "\ + 78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 78 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + } + + #[test] + fn erase_in_display_start_to_cursor() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"xxx\nxxx\n\x1b[2;2H"); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + console.write_bstr(b"\x1b[1J"); + assert_eq!( + print_buffer(&buffer), + "\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + } + + #[test] + fn erase_in_display_entire_screen() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"xxx\nxxx\n\x1b[2;2H"); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + console.write_bstr(b"\x1b[2J"); + assert_eq!( + print_buffer(&buffer), + "\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + } + + #[test] + fn erase_in_line_cursor_to_end() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"xxx\nxxx\n\x1b[2;2H"); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + console.write_bstr(b"\x1b[0K"); + assert_eq!( + print_buffer(&buffer), + "\ + 78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 78 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + } + + #[test] + fn erase_in_line_start_to_cursor() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"xxx\nxxx\n\x1b[2;2H"); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + console.write_bstr(b"\x1b[1K"); + assert_eq!( + print_buffer(&buffer), + "\ + 78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 20 07|20 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + } + + #[test] + fn erase_in_line_entire_line() { + let mut buffer = [0u8; WIDTH * HEIGHT * 2]; + let mut console = VgaConsole::new(buffer.as_mut_ptr(), WIDTH as isize, HEIGHT as isize); + console.write_bstr(b"xxx\nxxx\n\x1b[2;2H"); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + console.write_bstr(b"\x1b[2K"); + assert_eq!( + print_buffer(&buffer), + "\ + 78 07|78 07|78 07|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|20 07|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n\ + 00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|00 00|\n" + ); + assert_eq!(console.inner.row, 1); + assert_eq!(console.inner.col, 1); + } +} + +// =========================================================================== +// End of file +// ===========================================================================