From 546010762f19af6724218781e522f6fe8f7d73d8 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Sat, 18 Nov 2023 22:44:11 +0200 Subject: [PATCH] red: add red, the text editor --- Cargo.lock | 420 ++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- build.sh | 4 + red/Cargo.lock | 424 ++++++++++++++++++++++++++++++++++++ red/Cargo.toml | 15 ++ red/src/buffer/mod.rs | 483 +++++++++++++++++++++++++++++++++++++++++ red/src/command.rs | 124 +++++++++++ red/src/config.rs | 54 +++++ red/src/error.rs | 18 ++ red/src/line.rs | 226 +++++++++++++++++++ red/src/main.rs | 334 ++++++++++++++++++++++++++++ red/src/term/common.rs | 85 ++++++++ red/src/term/mod.rs | 59 +++++ red/src/term/simple.rs | 285 ++++++++++++++++++++++++ sysutils/Cargo.toml | 4 + sysutils/src/colors.rs | 11 + 16 files changed, 2545 insertions(+), 4 deletions(-) create mode 100644 red/Cargo.lock create mode 100644 red/Cargo.toml create mode 100644 red/src/buffer/mod.rs create mode 100644 red/src/command.rs create mode 100644 red/src/config.rs create mode 100644 red/src/error.rs create mode 100644 red/src/line.rs create mode 100644 red/src/main.rs create mode 100644 red/src/term/common.rs create mode 100644 red/src/term/mod.rs create mode 100644 red/src/term/simple.rs create mode 100644 sysutils/src/colors.rs diff --git a/Cargo.lock b/Cargo.lock index 5d21d5a4..0d10269a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,30 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.3.19" @@ -47,12 +71,66 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.1", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "humansize" version = "2.1.3" @@ -69,12 +147,46 @@ dependencies = [ "yggdrasil-rt", ] +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "memchr" version = "2.6.4" @@ -87,6 +199,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + [[package]] name = "nom" version = "7.1.3" @@ -97,12 +221,50 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.66" @@ -121,6 +283,52 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "red" +version = "0.1.0" +dependencies = [ + "crossterm", + "libc", + "syslog", + "thiserror", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "shell" version = "0.1.0" @@ -132,16 +340,65 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.27" +name = "signal-hook" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "syn" +version = "2.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syslog" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7434e95bcccce1215d30f4bf84fe8c00e8de1b9be4fb736d747ca53d36e7f96f" +dependencies = [ + "error-chain", + "hostname", + "libc", + "log", + "time", +] + [[package]] name = "sysutils" version = "0.1.0" @@ -151,12 +408,169 @@ dependencies = [ "yggdrasil-rt", ] +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "unicode-ident" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "yggdrasil-abi" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 572ca10e..0940d052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ resolver = "1" members = [ "init", "shell", - "sysutils" + "sysutils", + "red" ] [patch.'https://git.alnyan.me/yggdrasil/yggdrasil-abi.git'] diff --git a/build.sh b/build.sh index 8c953d24..7d2fb956 100755 --- a/build.sh +++ b/build.sh @@ -54,6 +54,10 @@ pack_initrd() { cp ${build_dir}/login ${root_dir}/sbin/ cp ${build_dir}/ls ${root_dir}/bin/ cp ${build_dir}/hexd ${root_dir}/bin/ + cp ${build_dir}/colors ${root_dir}/bin/ + + # red + cp ${build_dir}/red ${root_dir}/bin/red cp -r ${workspace_dir}/etc ${root_dir}/ diff --git a/red/Cargo.lock b/red/Cargo.lock new file mode 100644 index 00000000..aaff7024 --- /dev/null +++ b/red/Cargo.lock @@ -0,0 +1,424 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.1", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "red" +version = "0.1.0" +dependencies = [ + "crossterm", + "libc", + "syslog", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syslog" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7434e95bcccce1215d30f4bf84fe8c00e8de1b9be4fb736d747ca53d36e7f96f" +dependencies = [ + "error-chain", + "hostname", + "libc", + "log", + "time", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/red/Cargo.toml b/red/Cargo.toml new file mode 100644 index 00000000..6e4d7087 --- /dev/null +++ b/red/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "red" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +thiserror = "1.0.50" +unicode-width = "0.1.11" + +[target.'cfg(not(target_os = "yggdrasil"))'.dependencies] +libc = "0.2.150" +crossterm = "0.27.0" +syslog = "6.1.0" diff --git a/red/src/buffer/mod.rs b/red/src/buffer/mod.rs new file mode 100644 index 00000000..e999e7ee --- /dev/null +++ b/red/src/buffer/mod.rs @@ -0,0 +1,483 @@ +use std::{ + ffi::OsStr, + fs::File, + io::{self, BufRead, BufReader, BufWriter, Write}, + path::{Path, PathBuf}, +}; + +use unicode_width::UnicodeWidthChar; + +use crate::{ + config::Config, + error::Error, + line::{Line, TextLike}, + term::{Color, CursorStyle, Term, Terminal}, +}; + +#[derive(Default)] +pub struct View { + cursor_column: usize, + cursor_row: usize, + column_offset: usize, + row_offset: usize, + width: usize, + height: usize, + offset_x: usize, +} + +#[derive(Clone, Copy, PartialEq)] +pub enum Mode { + Normal, + Insert, +} + +pub enum SetMode { + Normal, + InsertBefore, + InsertAfter, +} + +pub struct Buffer { + lines: Vec, + dirty: bool, + mode_dirty: bool, + mode: Mode, + view: View, + name: Option, + path: Option, + modified: bool, +} + +impl Mode { + pub fn as_str(&self) -> &str { + match self { + Self::Normal => "NORMAL", + Self::Insert => "INSERT", + } + } +} + +impl View { + pub fn set_row(&mut self, row: usize) { + self.cursor_row = row; + + if self.cursor_row < self.row_offset { + self.row_offset = self.cursor_row; + } else if self.cursor_row >= self.row_offset + self.height { + self.row_offset = self.cursor_row - self.height + 1; + } + } + + pub fn set_column(&mut self, config: &Config, col: usize, line: Option<&Line>) { + let Some(line) = line else { + self.column_offset = 0; + self.cursor_column = 0; + return; + }; + + self.cursor_column = col; + + if line.display_width(config.tab_width) + 1 <= self.width { + self.column_offset = 0; + return; + } + + let width_to_cursor = line + .span(..self.cursor_column) + .display_width(config.tab_width); + + if width_to_cursor < self.column_offset { + self.column_offset = width_to_cursor; + } else if width_to_cursor >= self.column_offset + self.width { + self.column_offset = width_to_cursor - self.width + 1; + } + } + + pub fn reset(&mut self) { + self.column_offset = 0; + self.row_offset = 0; + self.cursor_row = 0; + self.cursor_column = 0; + } + + // pub fn resize(&mut self, width: usize, height: usize) { + // self.width = width; + // self.height = height; + + // self.column_offset = 0; + // self.row_offset = 0; + // } +} + +impl Buffer { + pub fn empty() -> Self { + Self { + lines: vec![], + dirty: true, + mode_dirty: true, + view: View::default(), + mode: Mode::Normal, + name: None, + path: None, + modified: false, + } + } + + fn read_lines>(path: P) -> io::Result> { + let input = BufReader::new(File::open(path)?); + let lines = input.lines().collect::, _>>()?; + let lines = lines + .into_iter() + .map(|line| Line::from_str(line.trim_end_matches('\n'))) + .collect(); + Ok(lines) + } + + pub fn open>(path: P) -> io::Result { + let path = path.as_ref(); + let name = path.file_name().and_then(OsStr::to_str).map(String::from); + let lines = if path.exists() { + Self::read_lines(path)? + } else { + vec![] + }; + + Ok(Self { + lines, + name, + path: Some(path.into()), + mode: Mode::Normal, + dirty: true, + mode_dirty: true, + view: View::default(), + modified: false, + }) + } + + pub fn reopen>(&mut self, path: P) -> io::Result<()> { + let path = path.as_ref(); + let name = path.file_name().and_then(OsStr::to_str).map(String::from); + let lines = if path.exists() { + Self::read_lines(path)? + } else { + vec![] + }; + + self.lines = lines; + self.modified = false; + self.mode = Mode::Normal; + self.dirty = true; + self.mode_dirty = true; + self.view.reset(); + + self.path = Some(path.into()); + self.name = name; + + Ok(()) + } + + pub fn save(&mut self) -> Result<(), Error> { + let path = self.path.as_ref().ok_or(Error::NoPath)?; + let mut writer = BufWriter::new(File::create(path).map_err(Error::WriteError)?); + + for line in self.lines.iter() { + writer + .write_all(line.to_string().as_ref()) + .map_err(Error::WriteError)?; + writer.write_all(b"\n").map_err(Error::WriteError)?; + } + + self.modified = false; + + Ok(()) + } + + pub fn set_path>(&mut self, path: P) { + let path = PathBuf::from(path.as_ref()); + let name = path.file_name().and_then(OsStr::to_str).map(String::from); + + self.path = Some(path); + self.name = name; + } + + pub fn set_mode(&mut self, config: &Config, mode: SetMode) { + let dst_mode = match mode { + SetMode::Normal => Mode::Normal, + SetMode::InsertAfter | SetMode::InsertBefore => Mode::Insert, + }; + + if dst_mode == self.mode { + return; + } + + self.mode = dst_mode; + self.mode_dirty = true; + match mode { + SetMode::Normal => self.move_cursor(config, -1, 0), + SetMode::InsertBefore => self.move_cursor(config, 0, 0), + SetMode::InsertAfter => self.move_cursor(config, 1, 0), + } + } + + pub fn mode(&self) -> Mode { + self.mode + } + + pub fn name(&self) -> Option<&String> { + self.name.as_ref() + } + + pub fn path(&self) -> Option<&PathBuf> { + self.path.as_ref() + } + + pub fn row_offset(&self) -> usize { + self.view.row_offset + } + + pub fn len(&self) -> usize { + self.lines.len() + } + + pub fn cursor_row(&self) -> usize { + self.view.cursor_row + } + + pub fn set_position(&mut self, config: &Config, px: usize, py: usize) { + self.dirty = true; + + if self.lines.is_empty() { + self.view.reset(); + return; + } + + // Move to row + self.view.set_row(py.min(self.lines.len() - 1)); + + // Set mode- and line-len-adjusted column + if let Some(line) = self.lines.get(self.view.cursor_row) && !line.is_empty() { + match self.mode { + // Limited by line.len() + Mode::Normal => self.view.set_column(config, px.min(line.len() - 1), Some(line)), + Mode::Insert => self.view.set_column(config, px.min(line.len()), Some(line)), + } + } else { + self.view.set_column(config, 0, None); + } + } + + pub fn to_line_end(&mut self, config: &Config) { + let len = self + .lines + .get(self.view.cursor_row) + .map(Line::len) + .unwrap_or(0); + + self.set_position(config, len, self.view.cursor_row); + } + + pub fn set_column(&mut self, config: &Config, x: usize) { + self.set_position(config, x, self.view.cursor_row); + } + + pub fn move_cursor(&mut self, config: &Config, dx: isize, dy: isize) { + let px = (self.view.cursor_column as isize + dx).max(0) as usize; + let py = (self.view.cursor_row as isize + dy).max(0) as usize; + + self.set_position(config, px, py); + } + + pub fn resize(&mut self, config: &Config, offset_x: usize, width: usize, height: usize) { + self.dirty = true; + self.view.height = height; + self.view.width = width; + self.view.offset_x = offset_x; + + self.view.set_row(self.view.cursor_row); + self.view.set_column( + config, + self.view.cursor_column, + self.lines.get(self.view.cursor_row), + ); + } + + pub fn display_cursor(&self, config: &Config) -> (usize, usize) { + if self.lines.is_empty() { + return (0, 0); + } + + // assert!(self.view.column_offset <= self.view.cursor_column); + assert!(self.view.row_offset <= self.view.cursor_row); + + let line = &self.lines[self.view.cursor_row]; + assert!(self.view.cursor_column <= line.len()); + + let column = line + .span(..self.view.cursor_column) + .display_width(config.tab_width); + assert!(self.view.column_offset <= column); + + ( + column - self.view.column_offset, + self.view.cursor_row - self.view.row_offset, + ) + } + + pub fn height(&self) -> usize { + self.view.height + } + + pub fn is_modified(&self) -> bool { + self.modified + } + + pub fn is_dirty(&self) -> bool { + self.dirty + } + + fn display_line(&self, config: &Config, term: &mut Term, row: usize, line: &Line) { + let mut pos = 0; + term.set_cursor_position(row, self.view.offset_x); + + let span = line.skip_to_width(self.view.column_offset, config.tab_width); + let long_line = span.display_width(config.tab_width) > self.view.width; + + for &ch in span.iter() { + if pos >= self.view.width { + break; + } + + if ch == '\t' { + let old_pos = pos; + let new_pos = (pos + config.tab_width) & !(config.tab_width - 1); + pos = new_pos; + + for i in old_pos..new_pos { + if i >= self.view.width { + break; + } + if i == old_pos { + term.set_foreground(Color::Blue); + term.put_byte(b'>'); + term.set_foreground(Color::Default); + } else { + term.put_byte(b' '); + } + } + } else { + // TODO optimize later + let s = std::iter::once(ch).collect::(); + term.put_bytes(s.as_str()); + pos += ch.width().unwrap_or(1); + } + } + + if long_line { + term.set_cursor_position(row, self.view.width + self.view.offset_x); + term.set_foreground(Color::Black); + term.set_background(Color::White); + term.put_byte(b'>'); + term.set_foreground(Color::Default); + term.set_background(Color::Default); + } + } + + pub fn display(&mut self, config: &Config, term: &mut Term) { + for (row, line) in self + .lines + .iter() + .skip(self.view.row_offset) + .take(self.view.height) + .enumerate() + { + self.display_line(config, term, row, line); + } + } + + pub fn newline_before(&mut self) { + self.lines.insert(self.view.cursor_row, Line::new()); + } + + pub fn newline_after(&mut self, break_line: bool) { + if self.lines.is_empty() { + self.lines.push(Line::new()); + return; + } + + let newline = if break_line { + self.lines[self.view.cursor_row].split_off(self.view.cursor_column) + } else { + Line::new() + }; + self.lines.insert(self.view.cursor_row + 1, newline); + } + + pub fn insert(&mut self, config: &Config, ch: char) { + if self.lines.is_empty() { + assert_eq!(self.view.cursor_row, 0); + self.lines.push(Line::new()); + } + + let line = &mut self.lines[self.view.cursor_row]; + line.insert(self.view.cursor_column, ch); + self.move_cursor(config, 1, 0); + self.modified = true; + } + + pub fn erase_backward(&mut self, config: &Config) { + if self.lines.is_empty() { + return; + } + + if self.view.cursor_column == 0 { + if self.view.cursor_row != 0 { + let line = self.lines.remove(self.view.cursor_row); + let prev_line = &mut self.lines[self.view.cursor_row - 1]; + + let len = prev_line.len(); + prev_line.extend(line); + self.set_position(config, len, self.view.cursor_row - 1); + } + return; + } + + let line = &mut self.lines[self.view.cursor_row]; + line.remove(self.view.cursor_column - 1); + self.move_cursor(config, -1, 0); + self.modified = true; + } + + pub fn erase_forward(&mut self) { + if self.lines.is_empty() { + return; + } + + let line = &mut self.lines[self.view.cursor_row]; + if self.view.cursor_column == line.len() { + return; + } + line.remove(self.view.cursor_column); + self.dirty = true; + self.modified = true; + } + + pub fn set_terminal_cursor(&mut self, config: &Config, term: &mut Term) { + let (x, y) = self.display_cursor(config); + if self.mode_dirty { + match self.mode { + Mode::Normal => term.set_cursor_style(CursorStyle::Default), + Mode::Insert => term.set_cursor_style(CursorStyle::Line), + } + } + term.set_cursor_position(y, x + self.view.offset_x); + self.mode_dirty = false; + } + + pub fn number_width(&mut self) -> usize { + if self.lines.len() == 0 { + 1 + } else { + self.lines.len().ilog10() as usize + 1 + } + } +} diff --git a/red/src/command.rs b/red/src/command.rs new file mode 100644 index 00000000..ea09d90e --- /dev/null +++ b/red/src/command.rs @@ -0,0 +1,124 @@ +use std::ops::RangeInclusive; + +use crate::{error::Error, State, buffer::{Buffer, SetMode}, config::Config}; + +pub type CommandFn = fn(&mut State, &[&str]) -> Result<(), Error>; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Action { + // Editing + EraseBackward, + InsertBefore, + InsertAfter, + NewlineBefore, + NewlineAfter, + BreakLine, + + // Movement + MoveCharPrev, + MoveCharNext, + MoveLinePrev, + MoveLineNext, + MoveLineStart, + MoveLineEnd, +} + +static COMMANDS: &[(&str, RangeInclusive, CommandFn)] = &[ + ("w", 0..=1, cmd_write), + ("w!", 0..=1, cmd_force_write), + ("q", 0..=0, cmd_exit), + ("q!", 0..=0, cmd_force_exit), + ("e", 1..=1, cmd_edit), + ("e!", 0..=1, cmd_force_edit), +]; + +// Commands +fn cmd_write(state: &mut State, args: &[&str]) -> Result<(), Error> { + if args.len() == 1 && state.buffer().is_modified() && state.buffer().path().is_some() { + return Err(Error::UnsavedBuffer("Use :w! FILE to force write to another file")); + } + + cmd_force_write(state, args) +} + +fn cmd_force_write(state: &mut State, args: &[&str]) -> Result<(), Error> { + let buffer = state.buffer_mut(); + if let Some(&path) = args.first() { + buffer.set_path(path); + } + + buffer.save() +} + +fn cmd_edit(state: &mut State, args: &[&str]) -> Result<(), Error> { + if state.buffer().is_modified() { + return Err(Error::UnsavedBuffer("Use :e! [FILE] to open another file")); + } + + cmd_force_edit(state, args) +} + +fn cmd_force_edit(state: &mut State, args: &[&str]) -> Result<(), Error> { + if let Some(&path) = args.first() { + state.buffer_mut().reopen(path).map_err(Error::OpenError) + } else if let Some(path) = state.buffer().path().cloned() { + state.buffer_mut().reopen(path).map_err(Error::OpenError) + } else { + Err(Error::NoPath) + } +} + +fn cmd_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> { + let buffer = state.buffer(); + + if buffer.is_modified() { + return Err(Error::UnsavedBuffer("Use :q! to force exit")); + } + + state.exit(); + Ok(()) +} + +fn cmd_force_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> { + state.exit(); + Ok(()) +} + +pub fn execute(state: &mut State, command: String) -> Result<(), Error> { + let words = command.split(' ').collect::>(); + let Some((&cmd, args)) = words.split_first() else { + return Ok(()); + }; + + for (name, nargs, f) in COMMANDS.iter() { + if *name == cmd { + if !nargs.contains(&args.len()) { + todo!(); + } + + return f(state, args); + } + } + + Err(Error::UnknownCommand(cmd.into())) +} + +pub fn perform(buffer: &mut Buffer, config: &Config, action: Action) -> Result<(), Error> { + match action { + // Editing + Action::EraseBackward => buffer.erase_backward(config), + Action::InsertBefore => buffer.set_mode(config, SetMode::InsertBefore), + Action::InsertAfter => buffer.set_mode(config, SetMode::InsertAfter), + Action::NewlineBefore => buffer.newline_before(), + Action::NewlineAfter => buffer.newline_after(false), + Action::BreakLine => buffer.newline_after(true), + // Movement + Action::MoveCharPrev => buffer.move_cursor(config, -1, 0), + Action::MoveCharNext => buffer.move_cursor(config, 1, 0), + Action::MoveLinePrev => buffer.move_cursor(config, 0, -1), + Action::MoveLineNext => buffer.move_cursor(config, 0, 1), + Action::MoveLineStart => buffer.set_column(config, 0), + Action::MoveLineEnd => buffer.to_line_end(config), + } + Ok(()) +} diff --git a/red/src/config.rs b/red/src/config.rs new file mode 100644 index 00000000..531a6f41 --- /dev/null +++ b/red/src/config.rs @@ -0,0 +1,54 @@ +use std::collections::HashMap; + +use crate::{buffer::Mode, command::Action}; + + +pub struct Config { + // TODO must be a power of 2, lol + pub tab_width: usize, + pub number: bool, + + pub nmap: HashMap>, + pub imap: HashMap>, +} + +fn bind>(key: char, items: I) -> (char, Vec) { + (key, items.into_iter().map(Into::into).collect()) +} + +impl Default for Config { + fn default() -> Self { + use Action::*; + + Self { + tab_width: 4, + number: true, + nmap: HashMap::from_iter([ + bind('i', [InsertBefore]), + bind('a', [InsertAfter]), + bind('h', [MoveCharPrev]), + bind('l', [MoveCharNext]), + bind('j', [MoveLineNext]), + bind('k', [MoveLinePrev]), + bind('I', [MoveLineStart, InsertBefore]), + bind('A', [MoveLineEnd, InsertAfter]), + bind('o', [NewlineAfter, MoveLineNext, InsertBefore]), + bind('O', [NewlineBefore, MoveLinePrev, InsertBefore]), + ]), + imap: HashMap::from_iter([ + bind('\x7F', [EraseBackward]), + bind('\n', [BreakLine, MoveLineNext, MoveLineStart, InsertBefore]), + bind('\x0D', [BreakLine, MoveLineNext, MoveLineStart, InsertBefore]), + ]) + } + } +} + +impl Config { + pub fn key(&self, mode: Mode, key: char) -> Option<&[Action]> { + match mode { + Mode::Normal => self.nmap.get(&key), + Mode::Insert => self.imap.get(&key) + }.map(AsRef::as_ref) + } +} diff --git a/red/src/error.rs b/red/src/error.rs new file mode 100644 index 00000000..69c22454 --- /dev/null +++ b/red/src/error.rs @@ -0,0 +1,18 @@ +use std::io; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + // I/O errors + #[error("Could not open file: {0}")] + OpenError(io::Error), + #[error("Could not write file: {0}")] + WriteError(io::Error), + #[error("Buffer does not have a path")] + NoPath, + #[error("Buffer has unsaved changes: {0}")] + UnsavedBuffer(&'static str), + #[error("Invalid command, usage: {0}")] + InvalidCommand(&'static str), + #[error("Unknown command: {0:?}")] + UnknownCommand(String), +} diff --git a/red/src/line.rs b/red/src/line.rs new file mode 100644 index 00000000..4dab8d7d --- /dev/null +++ b/red/src/line.rs @@ -0,0 +1,226 @@ +use std::{ops::Index, slice::SliceIndex}; + +use unicode_width::UnicodeWidthChar; + +#[derive(Debug, PartialEq, Default)] +pub struct Line { + data: Vec, +} + +#[derive(Debug, PartialEq)] +pub struct Span<'a>(&'a [char]); + +pub trait TextLike: Index + ToString { + type Iter<'a>: Iterator where Self: 'a; + type Span<'a>: TextLike + 'a + where + Self: 'a; + + fn display_width(&self, tab_width: usize) -> usize; + fn span>(&self, range: R) -> Self::Span<'_>; + fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_>; + + fn iter(&self) -> Self::Iter<'_>; +} + +// Line +impl Line { + pub fn new() -> Self { + Self { data: vec![] } + } + + pub fn from_str>(s: S) -> Self { + let chars = s.as_ref().chars(); + Self { + data: Vec::from_iter(chars), + } + } + + pub fn as_span(&self) -> Span { + Span(self.data.as_ref()) + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + pub fn split_off(&mut self, at: usize) -> Line { + let data = self.data.split_off(at); + Line { data } + } + + pub fn insert(&mut self, at: usize, ch: char) { + self.data.insert(at, ch); + } + + pub fn remove(&mut self, at: usize) { + self.data.remove(at); + } + + pub fn extend(&mut self, other: Line) { + self.data.extend(other.data); + } +} + +impl IntoIterator for Line { + type Item = char; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.data.into_iter() + } +} + +impl TextLike for Line { + type Span<'a> = Span<'a>; + type Iter<'a> = std::slice::Iter<'a, char>; + + fn display_width(&self, tab_width: usize) -> usize { + self.as_span().display_width(tab_width) + } + + fn span>(&self, range: R) -> Self::Span<'_> { + self.as_span().span(range) + } + + fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_> { + self.as_span().skip_to_width(offset, tab_width) + } + + fn iter(&self) -> Self::Iter<'_> { + self.data.iter() + } +} + +impl Index for Line { + type Output = char; + + fn index(&self, index: usize) -> &Self::Output { + &self.data[index] + } +} + +impl ToString for Line { + fn to_string(&self) -> String { + self.as_span().to_string() + } +} + +// Span +impl<'s> TextLike for Span<'s> { + type Iter<'a> = std::slice::Iter<'a, char> where 's: 'a; + type Span<'a> = Span<'s> where 's: 'a; + + fn display_width(&self, tab_width: usize) -> usize { + self.0.iter().fold(0, |pos, &ch| match ch { + '\t' => (pos + tab_width) & !(tab_width - 1), + _ => pos + ch.width().unwrap_or(1), + }) + } + + fn span>(&self, range: R) -> Self::Span<'_> { + Span(&self.0[range]) + } + + fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_> { + let mut index = 0; + let mut pos = 0; + for &ch in self.0.iter() { + if pos >= offset { + break; + } + match ch { + '\t' => pos = (pos + tab_width) & !(tab_width - 1), + _ => pos += ch.width().unwrap_or(1), + } + index += 1; + } + + self.span(index..) + } + + fn iter(&self) -> Self::Iter<'_> { + self.0.iter() + } +} + +impl Index for Span<'_> { + type Output = char; + + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +impl ToString for Span<'_> { + fn to_string(&self) -> String { + self.0.iter().collect() + } +} + +#[cfg(test)] +mod tests { + use crate::line::{Span, TextLike}; + + use super::Line; + + #[test] + fn line_from_str() { + // pure ASCII + let text = "abc123\n\t xyz"; + let line = Line::from_str(text); + assert_eq!( + line.data, + vec!['a', 'b', 'c', '1', '2', '3', '\n', '\t', ' ', 'x', 'y', 'z'] + ); + + // cyrillic unicode + let text = "це тест123"; + let line = Line::from_str(text); + assert_eq!( + line.data, + vec!['ц', 'е', ' ', 'т', 'е', 'с', 'т', '1', '2', '3'] + ); + + // japanese unicode + let text = "1日本2"; + let line = Line::from_str(text); + assert_eq!(line.data, vec!['1', '日', '本', '2']); + } + + #[test] + fn line_to_string() { + let line = Line { + data: vec!['a', 'b', 'c', 'т', 'е', 'с', 'т', '1', '2', '3', '\n'], + }; + assert_eq!(line.to_string().as_str(), "abcтест123\n"); + } + + #[test] + fn line_span() { + // All span + let line = Line::from_str("abcdef"); + assert_eq!(line.as_span(), Span(&['a', 'b', 'c', 'd', 'e', 'f'])); + + assert_eq!(line.span(..3), Span(&['a', 'b', 'c'])); + assert_eq!(line.span(..=3), Span(&['a', 'b', 'c', 'd'])); + + assert_eq!(line.span(..=3).span(2..), Span(&['c', 'd'])); + assert_eq!(line.span(2..=3), Span(&['c', 'd'])); + } + + #[test] + fn line_width() { + // No tabs + let line = Line::from_str("abcdef"); + assert_eq!(line.display_width(4), line.len()); + + // Tabs + let line = Line::from_str("\ta\tbcdef"); + assert_eq!(line.display_width(4), 8 + 5); + } +} diff --git a/red/src/main.rs b/red/src/main.rs new file mode 100644 index 00000000..ac2e8c30 --- /dev/null +++ b/red/src/main.rs @@ -0,0 +1,334 @@ +#![feature(let_chains, rustc_private)] +#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_raw_fd, yggdrasil_os))] + +use std::{env, path::Path, fmt::Write}; + +use buffer::{Buffer, Mode, SetMode}; +use config::Config; +use error::Error; +use term::{Clear, Color, Term}; + +use crate::term::Terminal; + +pub mod buffer; +pub mod command; +pub mod config; +pub mod error; +pub mod line; +pub mod term; + +#[derive(Clone, Copy)] +pub enum TopMode { + Normal, + Command, +} + +pub struct State { + term: Term, + buffer: Buffer, + command: String, + message: Option, + top_mode: TopMode, + config: Config, + running: bool, + number_width: usize, +} + +fn display_modeline(term: &mut Term, top_mode: TopMode, buf: &Buffer) { + term.set_cursor_position(buf.height(), 0); + + let bg = match (top_mode, buf.mode()) { + (TopMode::Normal, Mode::Normal) => Color::Yellow, + (TopMode::Normal, Mode::Insert) => Color::Cyan, + (TopMode::Command, _) => Color::Green, + }; + + term.set_background(bg); + term.set_foreground(Color::Black); + + match top_mode { + TopMode::Normal => { + term.put_byte(b' '); + term.put_bytes(buf.mode().as_str()); + term.put_byte(b' '); + + if buf.is_modified() { + term.set_background(Color::Magenta); + term.set_foreground(Color::Default); + } else { + term.set_foreground(Color::Green); + term.set_background(Color::Default); + } + } + TopMode::Command => { + term.put_bytes(b" COMMAND "); + + term.set_foreground(Color::Green); + term.set_background(Color::Default); + } + } + + term.put_byte(b' '); + term.put_bytes(buf.name().map(String::as_str).unwrap_or("")); + term.clear(Clear::LineToEnd); + + term.reset_style(); +} + +impl State { + pub fn open>(path: Option

) -> Result { + let config = Config::default(); + let mut buffer = match path { + Some(path) => Buffer::open(path).unwrap(), + None => Buffer::empty(), + }; + let term = Term::open(); + + let (w, h) = term.size(); + if config.number { + let nw = buffer.number_width() + 2; + buffer.resize(&config, nw, w - nw - 1, h - 2); + } else { + buffer.resize(&config, 0, w - 1, h - 2); + } + + Ok(Self { + number_width: buffer.number_width(), + top_mode: TopMode::Normal, + message: None, + command: String::new(), + running: true, + buffer, + term, + config, + }) + } + + pub fn buffer(&self) -> &Buffer { + &self.buffer + } + + pub fn buffer_mut(&mut self) -> &mut Buffer { + &mut self.buffer + } + + pub fn exit(&mut self) { + self.running = false; + } + + fn display_number(&mut self) -> Result<(), Error> { + let start = self.buffer.row_offset(); + let end = self.buffer.len(); + + for i in 0.. { + self.term.set_cursor_position(i, 0); + + if i + start == self.buffer.cursor_row() { + self.term.set_bright(true); + self.term.set_foreground(Color::Yellow); + } + + if i + start < end { + write!(self.term, " {0:1$} ", i + start + 1, self.number_width).ok(); + } else { + for _ in 0..self.number_width + 2 { + self.term.put_byte(b' '); + } + } + + if i == self.buffer.height() { + break; + } + + if i + start == self.buffer.cursor_row() { + self.term.reset_style(); + } + } + + self.term.reset_style(); + + Ok(()) + } + + fn display(&mut self) -> Result<(), Error> { + if self.buffer.is_dirty() { + self.term.clear(Clear::All); + } + + if self.config.number && self.buffer.is_dirty() { + self.display_number()?; + } + + self.buffer.display(&self.config, &mut self.term); + + if let Some(msg) = &self.message { + self.term.set_cursor_position(self.buffer.height(), 0); + self.term.put_bytes(msg); + self.term.flush(); + return Ok(()); + } + + display_modeline(&mut self.term, self.top_mode, &self.buffer); + match self.top_mode { + TopMode::Normal => self + .buffer + .set_terminal_cursor(&self.config, &mut self.term), + TopMode::Command => { + self.term.set_cursor_position(self.buffer.height() + 1, 0); + self.term.put_byte(b':'); + self.term.put_bytes(self.command.as_bytes()); + } + } + self.term.flush(); + Ok(()) + } + + fn handle_command(&mut self) -> Result<(), Error> { + let cmd = self.command.clone(); + command::execute(self, cmd) + } + + fn handle_command_key(&mut self, key: char) -> Result<(), Error> { + match key { + '\n' | '\x0D' => { + self.top_mode = TopMode::Normal; + self.handle_command()?; + } + '\x7F' => { + if self.command.is_empty() { + self.top_mode = TopMode::Normal; + } else { + self.command.pop(); + } + } + '\x1B' => { + self.top_mode = TopMode::Normal; + } + c if c.is_ascii_graphic() => self.command.push(c), + ' ' => self.command.push(' '), + _ => (), + } + + Ok(()) + } + + fn handle_normal_key(&mut self, key: char) -> Result<(), Error> { + match key { + '\x1B' => { + self.buffer.set_mode(&self.config, SetMode::Normal); + Ok(()) + } + ':' => { + self.command.clear(); + self.top_mode = TopMode::Command; + Ok(()) + } + _ => { + let buffer = &mut self.buffer; + if let Some(actions) = self.config.key(Mode::Normal, key) { + for &action in actions { + command::perform(buffer, &self.config, action)?; + } + } + + Ok(()) + } + } + } + + fn handle_insert_key(&mut self, key: char) -> Result<(), Error> { + match key { + '\x1B' => { + self.buffer.set_mode(&self.config, SetMode::Normal); + Ok(()) + } + key if !key.is_ascii() || key == ' ' || key == '\t' || key.is_ascii_graphic() => { + self.buffer.insert(&self.config, key); + Ok(()) + } + _ => { + let buffer = &mut self.buffer; + if let Some(actions) = self.config.key(Mode::Insert, key) { + for &action in actions { + command::perform(buffer, &self.config, action)?; + } + } + + Ok(()) + } + } + } + + pub fn update(&mut self) -> Result<(), Error> { + if self.config.number { + let nw = self.buffer.number_width(); + if nw != self.number_width { + self.number_width = nw; + let nw = nw + 2; + let (w, h) = self.term.size(); + self.buffer.resize(&self.config, nw, w - nw - 1, h - 2); + } + } + + self.display()?; + + let Some(key) = self.term.read_key() else { + return Ok(()); + }; + + if self.message.is_some() { + self.message = None; + if key != ':' { + return Ok(()); + } + } + + let result = match (self.top_mode, self.buffer.mode()) { + (TopMode::Normal, Mode::Normal) => self.handle_normal_key(key), + (TopMode::Normal, Mode::Insert) => self.handle_insert_key(key), + (TopMode::Command, _) => self.handle_command_key(key), + }; + + match result { + Ok(()) => Ok(()), + Err(e) => { + self.message = Some(format!("Error: {}", e)); + Ok(()) + } + } + } + + pub fn cleanup(&mut self) { + self.term.clear(Clear::All); + } +} + +fn main() { + let args = env::args().collect::>(); + if args.len() > 2 { + eprintln!("Usage: red [FILE]"); + return; + } + + if !Term::is_tty() { + eprintln!("Not a tty"); + return; + } + + let path = args.get(1); + let mut state = State::open(path).unwrap(); + let error = loop { + if !state.running { + break None; + } + + if let Err(error) = state.update() { + break Some(error); + } + }; + state.cleanup(); + + if let Some(error) = error { + eprintln!("Error: {:?}", error); + } +} diff --git a/red/src/term/common.rs b/red/src/term/common.rs new file mode 100644 index 00000000..91db63d0 --- /dev/null +++ b/red/src/term/common.rs @@ -0,0 +1,85 @@ +use std::io::{stdin, stdout, Read, Stdin, Stdout, Write}; + +use crossterm::{ + cursor, execute, queue, style, + terminal::{ + self, disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, + LeaveAlternateScreen, + }, + tty::IsTty, + ExecutableCommand, +}; + +use super::Terminal; + +pub struct Term { + stdin: Stdin, + stdout: Stdout, +} + +impl Terminal for Term { + fn is_tty() -> bool { + stdout().is_tty() + } + + fn open() -> Self { + let stdin = stdin(); + let mut stdout = stdout(); + + execute!( + stdout, + EnterAlternateScreen, + Clear(ClearType::All), + cursor::MoveTo(0, 0) + ) + .unwrap(); + enable_raw_mode().unwrap(); + + Self { stdin, stdout } + } + + fn set_cursor_position(&mut self, row: usize, column: usize) { + queue!(self.stdout, cursor::MoveTo(column as _, row as _)).ok(); + } + fn set_cursor_visible(&mut self, visible: bool) { + if visible { + queue!(self.stdout, cursor::Show).ok(); + } else { + queue!(self.stdout, cursor::Hide).ok(); + } + } + fn size(&self) -> (usize, usize) { + let (w, h) = terminal::size().unwrap(); + (w as _, h as _) + } + + fn put_bytes>(&mut self, s: B) { + self.stdout.write(s.as_ref()).ok(); + } + fn put_byte(&mut self, ch: u8) { + self.put_bytes(&[ch]); + } + fn flush(&mut self) { + self.stdout.flush().ok(); + } + fn clear(&mut self) { + queue!(self.stdout, Clear(ClearType::All)).ok(); + } + + fn read_key(&mut self) -> Option { + let mut buf = [0; 1]; + let len = self.stdin.read(&mut buf).unwrap(); + if len != 0 { + Some(buf[0]) + } else { + None + } + } +} + +impl Drop for Term { + fn drop(&mut self) { + disable_raw_mode().ok(); + execute!(self.stdout, LeaveAlternateScreen).ok(); + } +} diff --git a/red/src/term/mod.rs b/red/src/term/mod.rs new file mode 100644 index 00000000..e72e7d6b --- /dev/null +++ b/red/src/term/mod.rs @@ -0,0 +1,59 @@ +// #[cfg(not(target_os = "yggdrasil"))] +// pub mod common; +// +// #[cfg(not(target_os = "yggdrasil"))] +// pub use common::Term; + +pub mod simple; + +pub use simple::Term; + +#[derive(Debug, Clone, Copy)] +pub enum CursorStyle { + Default, + Line, +} + +#[derive(Debug, Clone, Copy)] +#[repr(u32)] +pub enum Color { + Black = 0, + Red = 1, + Green = 2, + Yellow = 3, + Blue = 4, + Magenta = 5, + Cyan = 6, + White = 7, + Default = 9, +} + +#[derive(Debug, Clone, Copy)] +pub enum Clear { + All, + LineToEnd, +} + +pub trait Terminal { + fn is_tty() -> bool; + fn open() -> Self; + + // Cursor & size + fn set_cursor_position(&mut self, row: usize, column: usize); + fn set_cursor_visible(&mut self, visible: bool); + fn set_cursor_style(&mut self, style: CursorStyle); + fn size(&self) -> (usize, usize); + + // Display + fn put_bytes>(&mut self, s: B); + fn put_byte(&mut self, ch: u8); + fn set_foreground(&mut self, color: Color); + fn set_background(&mut self, color: Color); + fn set_bright(&mut self, bright: bool); + fn reset_style(&mut self); + fn flush(&mut self); + fn clear(&mut self, clear: Clear); + + // Input + fn read_key(&mut self) -> Option; +} diff --git a/red/src/term/simple.rs b/red/src/term/simple.rs new file mode 100644 index 00000000..44fafb9a --- /dev/null +++ b/red/src/term/simple.rs @@ -0,0 +1,285 @@ +use std::{ + io::{stdin, stdout, Read, Stdin, Stdout, Write}, + mem::MaybeUninit, fmt, +}; + +use super::{Clear, Color, CursorStyle, Terminal}; + +struct RawMode { + #[cfg(not(target_os = "yggdrasil"))] + saved_termios: libc::termios, + #[cfg(target_os = "yggdrasil")] + saved_termios: std::os::yggdrasil::io::TerminalOptions, +} + +pub struct Term { + stdin: Stdin, + stdout: Stdout, + raw: RawMode, +} + +#[cfg(target_os = "yggdrasil")] +impl RawMode { + unsafe fn enter(stdin: &Stdin) -> Option { + use std::os::yggdrasil::io::TerminalOptions; + + let saved_termios = std::os::yggdrasil::io::update_terminal_options(stdin, |_| { + TerminalOptions::raw_input() + }) + .ok()?; + + Some(Self { saved_termios }) + } + + unsafe fn leave(&self, stdin: &Stdin) { + std::os::yggdrasil::io::update_terminal_options(stdin, |_| self.saved_termios.clone()).ok(); + } +} + +#[cfg(not(target_os = "yggdrasil"))] +impl RawMode { + unsafe fn enter(stdin: &Stdin) -> Option { + use std::os::fd::AsRawFd; + + let mut old = MaybeUninit::uninit(); + + if libc::tcgetattr(stdin.as_raw_fd(), old.as_mut_ptr()) != 0 { + return None; + } + + let old = old.assume_init(); + let mut new = old; + new.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN); + new.c_iflag &= !(libc::IGNBRK + | libc::BRKINT + | libc::PARMRK + | libc::ISTRIP + | libc::INLCR + | libc::IGNCR + | libc::ICRNL + | libc::IXON); + new.c_oflag &= !libc::OPOST; + new.c_cflag &= !(libc::PARENB | libc::CSIZE); + new.c_cflag |= libc::CS8; + + if libc::tcsetattr(stdin.as_raw_fd(), libc::TCSANOW, &new) != 0 { + return None; + } + + Some(Self { saved_termios: old }) + } + + unsafe fn leave(&self, stdin: &Stdin) { + use std::os::fd::AsRawFd; + libc::tcsetattr(stdin.as_raw_fd(), libc::TCSANOW, &self.saved_termios); + } +} + +#[cfg(not(target_os = "yggdrasil"))] +unsafe fn terminal_size(stdout: &Stdout) -> std::io::Result<(usize, usize)> { + use std::os::fd::AsRawFd; + let mut size: MaybeUninit = MaybeUninit::uninit(); + if libc::ioctl(stdout.as_raw_fd(), libc::TIOCGWINSZ, size.as_mut_ptr()) != 0 { + todo!(); + } + let size = size.assume_init(); + Ok((size.ws_col as _, size.ws_row as _)) +} + +#[cfg(target_os = "yggdrasil")] +unsafe fn terminal_size(stdout: &Stdout) -> std::io::Result<(usize, usize)> { + use std::os::yggdrasil::io::{DeviceRequest, FdDeviceRequest}; + let mut req = DeviceRequest::GetTerminalSize(MaybeUninit::uninit()); + if let Err(_) = stdout.device_request(&mut req) { + // Fallback + return Ok((60, 20)); + } + let DeviceRequest::GetTerminalSize(size) = req else { + unreachable!(); + }; + let size = size.assume_init(); + Ok((size.columns, size.rows)) +} + +pub trait ReadChar { + fn read_char(&mut self) -> Option; +} + +impl ReadChar for Stdin { + fn read_char(&mut self) -> Option { + let mut buf = [0; 4]; + self.read_exact(&mut buf[..1]).ok()?; + + let len = utf8_len_prefix(buf[0])?; + + if len != 0 { + self.read_exact(&mut buf[1..=len]).ok()?; + } + + // TODO optimize + let s = core::str::from_utf8(&buf[..len + 1]).ok()?; + s.chars().next() + } +} + +const fn utf8_len_prefix(l: u8) -> Option { + let mask0 = 0b10000000; + let val0 = 0; + let mask1 = 0b11100000; + let val1 = 0b11000000; + let mask2 = 0b11110000; + let val2 = 0b11100000; + let mask3 = 0b11111000; + let val3 = 0b11110000; + + if l & mask3 == val3 { + Some(3) + } else if l & mask2 == val2 { + Some(2) + } else if l & mask1 == val1 { + Some(1) + } else if l & mask0 == val0 { + Some(0) + } else { + None + } +} + +impl Term { + fn enter_alternate_mode(out: &mut O) { + out.write_all(b"\x1B[?1049h").ok(); + } + + fn leave_alternate_mode(out: &mut O) { + out.write_all(b"\x1B[?1049l").ok(); + } + + fn clear_all(out: &mut O) { + out.write_all(b"\x1B[2J").ok(); + } + + fn clear_line(out: &mut O, what: u32) { + out.write_all(format!("\x1B[{}K", what).as_bytes()).ok(); + } + + fn move_cursor(out: &mut O, row: usize, column: usize) { + out.write_all(format!("\x1B[{};{}f", row + 1, column + 1).as_bytes()) + .ok(); + } + + fn set_cursor_style_raw(out: &mut O, style: CursorStyle) { + // TODO yggdrasil support for cursor styles + #[cfg(not(target_os = "yggdrasil"))] + { + match style { + CursorStyle::Default => out.write_all(b"\x1B[0 q"), + CursorStyle::Line => out.write_all(b"\x1B[6 q"), + } + .ok(); + } + } + + fn set_color(out: &mut O, fgbg: u32, color: Color) { + out.write_all(format!("\x1B[{}{}m", fgbg, color as u32).as_bytes()) + .ok(); + } +} + +impl Terminal for Term { + fn is_tty() -> bool { + // TODO + true + } + + fn open() -> Self { + let stdin = stdin(); + let mut stdout = stdout(); + + // Set stdin to raw mode + let raw = unsafe { RawMode::enter(&stdin).unwrap() }; // unsafe { Self::enable_raw(&stdin).unwrap() }; + Self::enter_alternate_mode(&mut stdout); + Self::clear_all(&mut stdout); + Self::move_cursor(&mut stdout, 0, 0); + + Self { stdin, stdout, raw } + } + + fn set_cursor_position(&mut self, row: usize, column: usize) { + Self::move_cursor(&mut self.stdout, row, column) + } + fn set_cursor_visible(&mut self, _visible: bool) {} + fn set_cursor_style(&mut self, style: CursorStyle) { + Self::set_cursor_style_raw(&mut self.stdout, style); + } + fn size(&self) -> (usize, usize) { + unsafe { terminal_size(&self.stdout).unwrap() } + // #[cfg(target_os = "yggdrasil")] + // { + // (80, 30) + // } + // #[cfg(not(target_os = "yggdrasil"))] + // { + // (80, 25) + // } + } + + fn put_bytes>(&mut self, s: B) { + self.stdout.write_all(s.as_ref()).ok(); + } + fn put_byte(&mut self, ch: u8) { + self.put_bytes([ch]); + } + fn set_foreground(&mut self, color: Color) { + Self::set_color(&mut self.stdout, 3, color) + } + fn set_background(&mut self, color: Color) { + Self::set_color(&mut self.stdout, 4, color) + } + fn set_bright(&mut self, bright: bool) { + if bright { + self.stdout.write_all(b"\x1B[1m").ok(); + } else { + self.stdout.write_all(b"\x1B[22m").ok(); + } + } + fn reset_style(&mut self) { + self.stdout.write_all(b"\x1B[0m").ok(); + } + fn flush(&mut self) { + self.stdout.flush().ok(); + } + fn clear(&mut self, clear: Clear) { + match clear { + Clear::All => Self::clear_all(&mut self.stdout), + Clear::LineToEnd => Self::clear_line(&mut self.stdout, 0), + } + } + + fn read_key(&mut self) -> Option { + self.stdin.read_char() + + // let mut buf = [0; 1]; + // let len = self.stdin.read(&mut buf).unwrap(); + // if len != 0 { + // Some(buf[0]) + // } else { + // None + // } + } +} + +impl fmt::Write for Term { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.put_bytes(s); + Ok(()) + } +} + +impl Drop for Term { + fn drop(&mut self) { + unsafe { + self.raw.leave(&self.stdin); + } + Self::leave_alternate_mode(&mut self.stdout); + } +} diff --git a/sysutils/Cargo.toml b/sysutils/Cargo.toml index 26c25139..a4b24584 100644 --- a/sysutils/Cargo.toml +++ b/sysutils/Cargo.toml @@ -31,3 +31,7 @@ path = "src/ls.rs" [[bin]] name = "hexd" path = "src/hexd.rs" + +[[bin]] +name = "colors" +path = "src/colors.rs" diff --git a/sysutils/src/colors.rs b/sysutils/src/colors.rs new file mode 100644 index 00000000..954a5560 --- /dev/null +++ b/sysutils/src/colors.rs @@ -0,0 +1,11 @@ + +fn main() { + for bg in 40..=49 { + if bg == 48 { continue; } + for fg in 30..=39 { + if fg == 48 { continue; } + print!("\x1B[{}m\x1B[{}m@\x1B[0m", bg, fg); + } + println!(); + } +}