From a689949044a2652067b13736b78a26670b887633 Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Sun, 12 Apr 2026 22:59:54 -0500 Subject: [PATCH] clean up files, parse arguments with clap --- Cargo.lock | 305 ++++++--- Cargo.toml | 1 + src/action.rs | 923 --------------------------- src/{app/mod.rs => app.rs} | 88 +-- src/app/actions.rs | 50 ++ src/arguments.rs | 14 + src/{buffer/mod.rs => buffer.rs} | 101 +-- src/buffer/actions.rs | 923 +++++++++++++++++++++++++++ src/buffer/widget.rs | 405 +----------- src/buffer/widget/address.rs | 16 + src/buffer/widget/character_panel.rs | 76 +++ src/buffer/widget/extra_statuses.rs | 34 + src/buffer/widget/hex.rs | 237 +++++++ src/buffer/widget/status_line.rs | 54 ++ src/config.rs | 276 +------- src/config/default.rs | 266 ++++++++ src/cursor/actions.rs | 440 +++++++++++++ src/edit_action.rs | 2 +- src/main.rs | 16 +- src/popup.rs | 64 ++ src/window_size.rs | 17 + 21 files changed, 2452 insertions(+), 1856 deletions(-) rename src/{app/mod.rs => app.rs} (74%) create mode 100644 src/app/actions.rs create mode 100644 src/arguments.rs rename src/{buffer/mod.rs => buffer.rs} (82%) create mode 100644 src/buffer/actions.rs create mode 100644 src/buffer/widget/address.rs create mode 100644 src/buffer/widget/character_panel.rs create mode 100644 src/buffer/widget/extra_statuses.rs create mode 100644 src/buffer/widget/hex.rs create mode 100644 src/buffer/widget/status_line.rs create mode 100644 src/config/default.rs create mode 100644 src/cursor/actions.rs create mode 100644 src/popup.rs create mode 100644 src/window_size.rs diff --git a/Cargo.lock b/Cargo.lock index 718d378..eac7cc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,56 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -67,9 +117,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] @@ -116,6 +166,52 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "compact_str" version = "0.9.0" @@ -132,9 +228,9 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -154,7 +250,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm_winapi", "derive_more", "document-features", @@ -198,9 +294,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -208,27 +304,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -248,23 +343,24 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.110", + "rustc_version", + "syn 2.0.117", ] [[package]] @@ -423,6 +519,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heck" version = "0.5.0" @@ -439,6 +541,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" name = "hexapoda" version = "0.1.0" dependencies = [ + "clap", "crossterm", "itertools", "ratatui", @@ -460,12 +563,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -481,17 +584,23 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.9" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" dependencies = [ "darling", "indoc", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -503,15 +612,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -548,24 +657,24 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "line-clipping" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" @@ -584,9 +693,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" @@ -636,9 +745,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", @@ -652,7 +761,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -671,9 +780,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-derive" @@ -683,7 +792,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -710,6 +819,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "ordered-float" version = "4.6.0" @@ -772,7 +887,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -825,7 +940,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -856,23 +971,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -924,7 +1039,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "compact_str", "hashbrown 0.16.1", "indoc", @@ -976,7 +1091,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.16.1", "indoc", "instability", @@ -995,7 +1110,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -1028,12 +1143,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "rustix" -version = "1.1.2" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags 2.10.0", + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -1048,9 +1172,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "scopeguard" @@ -1060,9 +1184,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1091,7 +1215,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1150,10 +1274,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -1199,7 +1324,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1215,9 +1340,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1253,7 +1378,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64", - "bitflags 2.10.0", + "bitflags 2.11.0", "fancy-regex", "filedescriptor", "finl_unicode", @@ -1313,7 +1438,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1324,7 +1449,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1401,15 +1526,15 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-truncate" @@ -1442,9 +1567,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "atomic", "getrandom 0.4.2", @@ -1493,9 +1618,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -1506,9 +1631,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1516,22 +1641,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -1564,7 +1689,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -1715,7 +1840,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.110", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -1731,7 +1856,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -1743,7 +1868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 42bbfc0..8c4495f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ license = "GPL-3.0-only" edition = "2024" [dependencies] +clap = { version = "4.6.0", features = ["derive"] } crossterm = { version = "0.29.0", features = ["serde"] } itertools = "0.14.0" ratatui = "0.30.0" diff --git a/src/action.rs b/src/action.rs index 6652375..cf707f5 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,8 +1,4 @@ -use std::{cmp::min, collections::hash_set::Entry, convert::identity, fs::File, io::Write, iter, mem::{replace, swap}}; -use itertools::Itertools; -use ratatui::{style::{Color, Stylize}, text::Span}; use serde::{Deserialize, Serialize}; -use crate::{BYTES_OF_PADDING, BYTES_PER_LINE, LINES_OF_PADDING, app::WindowSize, buffer::{Buffer, InspectionStatus, Mode, PartialAction, Popup}, cursor::Cursor, edit_action::EditAction}; #[derive(Clone, Copy, Serialize, Deserialize)] #[derive(Debug)] @@ -559,922 +555,3 @@ impl TryFrom<&str> for CursorAction { } } } - -impl Buffer { - pub fn execute(&mut self, action: BufferAction, window_size: WindowSize) { - match action { - BufferAction::NormalMode => self.normal_mode(), - BufferAction::SelectMode => self.select_mode(), - - BufferAction::Goto => self.goto(), - BufferAction::View => self.view(), - BufferAction::Replace => self.replace(), - BufferAction::Space => self.space(), - BufferAction::Repeat => self.repeat(), - BufferAction::To => self.to(), - - BufferAction::ScrollDown => self.scroll_down(window_size), - BufferAction::ScrollUp => self.scroll_up(window_size), - - BufferAction::PageCursorHalfDown => self.page_cursor_half_down(window_size), - BufferAction::PageCursorHalfUp => self.page_cursor_half_up(window_size), - - BufferAction::PageDown => self.page_down(window_size), - BufferAction::PageUp => self.page_up(window_size), - - BufferAction::CollapseSelection => self.collapse_selection(), - BufferAction::FlipSelections => self.flip_selection(window_size), - - BufferAction::Delete => self.delete(window_size), - - BufferAction::Undo => self.undo(window_size), - BufferAction::Redo => self.redo(window_size), - - BufferAction::Save => self.save(), - - BufferAction::CopySelectionOnNextLine => self.copy_selection_on_next_line(window_size), - - BufferAction::RotateSelectionsBackward => self.rotate_selections_backward(window_size), - BufferAction::RotateSelectionsForward => self.rotate_selections_forward(window_size), - - BufferAction::KeepPrimarySelection => self.keep_primary_selection(), - BufferAction::RemovePrimarySelection => self.remove_primary_selection(), - - BufferAction::SplitSelectionsInto1s => self.split_selections_into_size(1, window_size), - BufferAction::SplitSelectionsInto2s => self.split_selections_into_size(2, window_size), - BufferAction::SplitSelectionsInto3s => self.split_selections_into_size(3, window_size), - BufferAction::SplitSelectionsInto4s => self.split_selections_into_size(4, window_size), - BufferAction::SplitSelectionsInto5s => self.split_selections_into_size(5, window_size), - BufferAction::SplitSelectionsInto6s => self.split_selections_into_size(6, window_size), - BufferAction::SplitSelectionsInto7s => self.split_selections_into_size(7, window_size), - BufferAction::SplitSelectionsInto8s => self.split_selections_into_size(8, window_size), - BufferAction::SplitSelectionsInto9s => self.split_selections_into_size(9, window_size), - - BufferAction::JumpToSelectedOffset => self.jump_to_selected_offset(window_size), - BufferAction::JumpToSelectedOffsetRelativeToMark => self.jump_to_selected_offset_relative_to_mark(window_size), - - BufferAction::ToggleMark => self.toggle_mark(), - - BufferAction::AlignViewCenter => self.align_view_center(window_size), - BufferAction::AlignViewBottom => self.align_view_bottom(window_size), - BufferAction::AlignViewTop => self.align_view_top(), - - BufferAction::ExtendToMark => self.extend_to_mark(window_size), - BufferAction::ExtendToNull => self.extend_to_null(window_size), - BufferAction::ExtendToFF => self.extend_to_FF(window_size), - - BufferAction::InspectSelection => self.inspect_selection(), - BufferAction::InspectSelectionColor => self.inspect_selection_color(), - } - } - - const fn normal_mode(&mut self) { - self.mode = Mode::Normal; - } - - const fn select_mode(&mut self) { - self.mode = Mode::Select; - } - - const fn goto(&mut self) { - self.partial_action = Some(PartialAction::Goto); - } - - const fn view(&mut self) { - self.partial_action = Some(PartialAction::View); - } - - const fn replace(&mut self) { - if !self.contents.is_empty() { - self.partial_action = Some(PartialAction::Replace); - } - } - - const fn space(&mut self) { - self.partial_action = Some(PartialAction::Space); - } - - const fn repeat(&mut self) { - self.partial_action = Some(PartialAction::Repeat); - } - - const fn to(&mut self) { - self.partial_action = Some(PartialAction::To); - } - - pub fn scroll_down(&mut self, window_size: WindowSize) { - if self.contents.len() <= BYTES_OF_PADDING { return; } - - self.scroll_position = min( - self.scroll_position + BYTES_PER_LINE, - self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE - ); - - if window_size.hex_rows() > LINES_OF_PADDING * 2 { - self.primary_cursor.clamp( - self.scroll_position + BYTES_OF_PADDING, - window_size.visible_byte_count() - BYTES_OF_PADDING * 2 - ); - } else { - self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); - } - self.combine_cursors_if_overlapping(); - } - - pub fn scroll_up(&mut self, window_size: WindowSize) { - self.scroll_position = self.scroll_position.saturating_sub(BYTES_PER_LINE); - if window_size.hex_rows() > LINES_OF_PADDING * 2 { - self.primary_cursor.clamp( - self.scroll_position + BYTES_OF_PADDING, - window_size.visible_byte_count() - BYTES_OF_PADDING * 2 - ); - } else { - self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); - } - self.combine_cursors_if_overlapping(); - } - - fn page_cursor_half_down(&mut self, window_size: WindowSize) { - if self.contents.len() <= BYTES_OF_PADDING { return; } - - let old_scroll_position = self.scroll_position; - - self.scroll_position = min( - self.scroll_position + (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE), - self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE - ); - - let scroll_position_change = self.scroll_position - old_scroll_position; - let max_contents_index = self.max_contents_index(); - - self.primary_cursor.head = min( - self.primary_cursor.head + scroll_position_change, - max_contents_index - ); - self.primary_cursor.tail = min( - self.primary_cursor.tail + scroll_position_change, - max_contents_index - ); - - for cursor in &mut self.cursors { - cursor.head = (cursor.head + scroll_position_change).min(max_contents_index); - cursor.tail = (cursor.tail + scroll_position_change).min(max_contents_index); - } - - self.combine_cursors_if_overlapping(); - } - - fn page_cursor_half_up(&mut self, window_size: WindowSize) { - let old_scroll_position = self.scroll_position; - - self.scroll_position = self.scroll_position.saturating_sub( - (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE) - ); - - let scroll_position_change = old_scroll_position - self.scroll_position; - let max_contents_index = self.max_contents_index(); - - self.primary_cursor.head = min( - self.primary_cursor.head - scroll_position_change, - max_contents_index - ); - self.primary_cursor.tail = min( - self.primary_cursor.tail - scroll_position_change, - max_contents_index - ); - - for cursor in &mut self.cursors { - cursor.head = (cursor.head - scroll_position_change).min(max_contents_index); - cursor.tail = (cursor.tail - scroll_position_change).min(max_contents_index); - } - - self.combine_cursors_if_overlapping(); - } - - fn page_down(&mut self, window_size: WindowSize) { - if self.contents.len() <= BYTES_OF_PADDING { return; } - - self.scroll_position = min( - self.scroll_position + window_size.visible_byte_count(), - self.max_contents_index() - BYTES_OF_PADDING - self.max_contents_index() % BYTES_PER_LINE - ); - - if window_size.hex_rows() > LINES_OF_PADDING * 2 { - self.primary_cursor.clamp( - self.scroll_position + BYTES_OF_PADDING, - window_size.visible_byte_count() - BYTES_OF_PADDING * 2 - ); - } else { - self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); - } - self.combine_cursors_if_overlapping(); - } - - fn page_up(&mut self, window_size: WindowSize) { - self.scroll_position = self.scroll_position.saturating_sub( - window_size.visible_byte_count() - ); - - if window_size.hex_rows() > LINES_OF_PADDING * 2 { - self.primary_cursor.clamp( - self.scroll_position + BYTES_OF_PADDING, - window_size.visible_byte_count() - BYTES_OF_PADDING * 2 - ); - } else { - self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); - } - self.combine_cursors_if_overlapping(); - } - - fn collapse_selection(&mut self) { - self.primary_cursor.collapse(); - - for cursor in &mut self.cursors { - cursor.collapse(); - } - } - - fn flip_selection(&mut self, window_size: WindowSize) { - self.primary_cursor.flip(); - - for cursor in &mut self.cursors { - cursor.flip(); - } - - self.clamp_screen_to_primary_cursor(window_size); - } - - fn delete(&mut self, window_size: WindowSize) { - if !self.contents.is_empty() { - self.execute_and_add( - EditAction::Delete { - primary_cursor: self.primary_cursor, - cursors: self.cursors.clone(), - primary_old_data: self.contents[self.primary_cursor.range()].into(), - old_data: self.cursors - .iter() - .map(|cursor| self.contents[cursor.range()].to_vec()) - .collect(), - }, - window_size - ); - } - - if self.mode == Mode::Select { - self.mode = Mode::Normal; - } - } - - fn undo(&mut self, window_size: WindowSize) { - if self.time_traveling == Some(0) || self.edit_history.is_empty() { return; } - - let current_date = self.time_traveling - .map_or(self.edit_history.len() - 1, |date| date - 1); - - self.time_traveling = Some(current_date); - - let edit_action = replace( - &mut self.edit_history[current_date], - EditAction::Placeholder - ); - - self.undo_edit(&edit_action, window_size); - - self.edit_history[current_date] = edit_action; - } - - fn redo(&mut self, window_size: WindowSize) { - let Some(previous_date) = self.time_traveling else { return; }; - - let current_date = previous_date + 1; - - self.time_traveling = if current_date == self.edit_history.len() { - None - } else { - Some(current_date) - }; - - let edit_action = replace( - &mut self.edit_history[previous_date], - EditAction::Placeholder - ); - - self.execute_edit(&edit_action, window_size); - - self.edit_history[previous_date] = edit_action; - } - - fn save(&mut self) { - let mut file = File::create(&self.file_path).unwrap(); - file.write_all(&self.contents).unwrap(); - - self.last_saved_at = Some( - self.time_traveling.unwrap_or(self.edit_history.len()) - ); - } - - fn copy_selection_on_next_line(&mut self, window_size: WindowSize) { - let new_cursors: Vec = iter::once(&self.primary_cursor) - .chain(&self.cursors) - .filter_map(|cursor| { - let number_of_lines_tall = (cursor.upper_bound() - cursor.lower_bound()) / BYTES_PER_LINE; - let offset_to_add = (number_of_lines_tall + 1) * BYTES_PER_LINE; - - if cursor.lower_bound() + offset_to_add < self.contents.len() { - Some( - Cursor { - head: min(cursor.head + offset_to_add, self.max_contents_index()), - tail: min(cursor.tail + offset_to_add, self.max_contents_index()) - } - ) - } else { - None - } - }) - .collect(); - - self.cursors.extend(new_cursors); - self.cursors.sort_by_key(|cursor| cursor.head); - - self.combine_cursors_if_overlapping(); - - self.rotate_selections_forward(window_size); - } - - fn rotate_selections_backward(&mut self, window_size: WindowSize) { - if self.cursors.is_empty() { return; } - - let next_cursor_index = self.cursors - .binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head) - .unwrap_or_else(identity); - - - if next_cursor_index == 0 { - let cursor_count = self.cursors.len(); - swap(&mut self.primary_cursor, &mut self.cursors[cursor_count - 1]); - - self.cursors.sort_by_key(|cursor| cursor.head); - } else { - swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index - 1]); - } - - self.clamp_screen_to_primary_cursor(window_size); - } - - fn rotate_selections_forward(&mut self, window_size: WindowSize) { - if self.cursors.is_empty() { return; } - - let next_cursor_index = self.cursors - .binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head) - .unwrap_or_else(identity); - - if next_cursor_index == self.cursors.len() { - swap(&mut self.primary_cursor, &mut self.cursors[0]); - - self.cursors.sort_by_key(|cursor| cursor.head); - } else { - swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index]); - } - - self.clamp_screen_to_primary_cursor(window_size); - } - - fn keep_primary_selection(&mut self) { - self.cursors.clear(); - } - - fn remove_primary_selection(&mut self) { - if self.cursors.is_empty() { return; } - - let next_cursor_index = self.cursors - .binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head) - .unwrap_or_else(identity); - - if next_cursor_index == self.cursors.len() { - self.primary_cursor = self.cursors.remove(0); - } else { - self.primary_cursor = self.cursors.remove(next_cursor_index); - } - } - - fn split_selections_into_size(&mut self, size: usize, window_size: WindowSize) { - if !iter::once(&self.primary_cursor) - .chain(&self.cursors) - .all(|cursor| cursor.len().is_multiple_of(size)) - { - self.alert_message = Span::from( - format!("not all selections are a multiple of {size} long") - ).red(); - return; - } - - let mut new_cursors = iter::once(self.primary_cursor) - .chain(self.cursors.iter().copied()) - .flat_map(|cursor| { - cursor - .range() - .step_by(size) - .map(|tail| Cursor { head: tail + size - 1, tail }) - }); - - self.primary_cursor = new_cursors.next().unwrap(); - self.cursors = new_cursors.collect(); - - self.clamp_screen_to_primary_cursor(window_size); - } - - fn jump_to_selected_offset(&mut self, window_size: WindowSize) { - if !iter::once(&self.primary_cursor) - .chain(&self.cursors) - .all(|cursor| { - bytes_to_nat(&self.contents[cursor.range()]) - .map(|nat| nat as usize) - .is_some_and(|offset| offset < self.contents.len()) - }) - { - if self.cursors.is_empty() { - self.alert_message = Span::from( - "selection is not a valid offset" - ).red(); - } else { - self.alert_message = Span::from( - "not all selections are valid offsets" - ).red(); - } - return; - } - - self.primary_cursor = Cursor::at( - bytes_to_nat(&self.contents[self.primary_cursor.range()]).unwrap() as usize - ); - - for cursor in &mut self.cursors { - *cursor = Cursor::at( - bytes_to_nat(&self.contents[cursor.range()]).unwrap() as usize - ); - } - - self.cursors.sort_by_key(|cursor| cursor.head); - - self.combine_cursors_if_overlapping(); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn jump_to_selected_offset_relative_to_mark(&mut self, window_size: WindowSize) { - let mut sorted_marks: Vec<_> = self.marks.iter().copied().collect(); - sorted_marks.sort_unstable(); - - if !iter::once(&self.primary_cursor) - .chain(&self.cursors) - .all(|cursor| { - bytes_to_nat(&self.contents[cursor.range()]) - .map(|offset| mark_before(cursor.lower_bound(), &sorted_marks) + offset as usize) - .is_some_and(|offset| offset < self.contents.len()) - }) - { - if self.cursors.is_empty() { - self.alert_message = Span::from( - "selection is not a valid offset" - ).red(); - } else { - self.alert_message = Span::from( - "not all selections are valid offsets" - ).red(); - } - return; - } - - self.primary_cursor = Cursor::at( - bytes_to_nat(&self.contents[self.primary_cursor.range()]) - .map(|offset| { - mark_before(self.primary_cursor.lower_bound(), &sorted_marks) + offset as usize - }) - .unwrap() - ); - - for cursor in &mut self.cursors { - *cursor = Cursor::at( - bytes_to_nat(&self.contents[cursor.range()]) - .map(|offset| { - mark_before(cursor.lower_bound(), &sorted_marks) + offset as usize - }) - .unwrap() - ); - } - - self.cursors.sort_by_key(|cursor| cursor.head); - - self.combine_cursors_if_overlapping(); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn toggle_mark(&mut self) { - match self.marks.entry(self.primary_cursor.lower_bound()) { - Entry::Occupied(occupied_entry) => { occupied_entry.remove(); }, - Entry::Vacant(vacant_entry) => vacant_entry.insert(), - } - - for cursor in &self.cursors { - match self.marks.entry(cursor.lower_bound()) { - Entry::Occupied(occupied_entry) => { occupied_entry.remove(); }, - Entry::Vacant(vacant_entry) => vacant_entry.insert(), - } - } - } - - const fn align_view_center(&mut self, window_size: WindowSize) { - let half_a_screen = window_size.visible_byte_count() / 2; - - self.scroll_position = self.primary_cursor.head - .saturating_sub(self.primary_cursor.head % BYTES_PER_LINE) - .saturating_sub(half_a_screen - (half_a_screen % BYTES_PER_LINE)); - } - - fn align_view_bottom(&mut self, window_size: WindowSize) { - self.scroll_position = self.primary_cursor.head - .saturating_sub(self.primary_cursor.head % BYTES_PER_LINE) - .saturating_sub( - window_size - .visible_byte_count() - .saturating_sub(BYTES_PER_LINE + BYTES_OF_PADDING) - ) - .min(self.max_contents_index() - self.max_contents_index() % BYTES_PER_LINE); - } - - const fn align_view_top(&mut self) { - self.scroll_position = self.primary_cursor.head - .saturating_sub(self.primary_cursor.head % BYTES_PER_LINE) - .saturating_sub(BYTES_OF_PADDING); - } - - fn extend_to_mark(&mut self, window_size: WindowSize) { - let mut sorted_marks: Vec<_> = self.marks.iter().copied().collect(); - sorted_marks.sort_unstable(); - - let max_contents_index = self.max_contents_index(); - - let mark_after_primary = mark_after( - self.primary_cursor.head, - &sorted_marks, - max_contents_index - ); - - self.primary_cursor.tail = self.primary_cursor.head; - self.primary_cursor.head = mark_after_primary - 1; - - for cursor in &mut self.cursors { - let mark_after_cursor = mark_after( - cursor.head, - &sorted_marks, - max_contents_index - ); - - cursor.tail = cursor.head; - cursor.head = mark_after_cursor - 1; - } - - self.clamp_screen_to_primary_cursor(window_size); - } - - fn extend_to_null(&mut self, window_size: WindowSize) { - if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..] - .iter() - .skip(1) - .position(|&byte| byte == 0) - { - self.primary_cursor.tail = self.primary_cursor.head; - self.primary_cursor.head += null_offset_after_primary; - } - - for cursor in &mut self.cursors { - if let Some(null_offset_after_primary) = self.contents[cursor.head..] - .iter() - .skip(1) - .position(|&byte| byte == 0) - { - cursor.tail = cursor.head; - cursor.head += null_offset_after_primary; - } - } - - self.clamp_screen_to_primary_cursor(window_size); - } - - #[allow(non_snake_case)] - fn extend_to_FF(&mut self, window_size: WindowSize) { - if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..] - .iter() - .skip(1) - .position(|&byte| byte == 0xFF) - { - self.primary_cursor.tail = self.primary_cursor.head; - self.primary_cursor.head += null_offset_after_primary; - } - - for cursor in &mut self.cursors { - if let Some(null_offset_after_primary) = self.contents[cursor.head..] - .iter() - .skip(1) - .position(|&byte| byte == 0xFF) - { - cursor.tail = cursor.head; - cursor.head += null_offset_after_primary; - } - } - - self.clamp_screen_to_primary_cursor(window_size); - } - - #[allow(clippy::too_many_lines)] - fn inspect_selection(&mut self) { - if self.inspection_status == Some(InspectionStatus::Normal) { - self.inspection_status = None; - return; - } - - self.inspection_status = Some(InspectionStatus::Normal); - - self.popups.extend( - iter::once(&self.primary_cursor) - .chain(&self.cursors) - .filter_map(|cursor| { - let selection = &self.contents[cursor.range()]; - - let popup_lines = inspect(selection); - - if popup_lines.is_empty() { - None - } else { - Some(Popup::new(cursor.lower_bound(), popup_lines)) - } - }) - .sorted_unstable_by_key(|popup| popup.at) - ); - - if self.popups.is_empty() { - self.inspection_status = None; - } - } - - fn inspect_selection_color(&mut self) { - if self.inspection_status == Some(InspectionStatus::ColorsOnly) { - self.inspection_status = None; - return; - } - - self.inspection_status = Some(InspectionStatus::ColorsOnly); - - self.popups.extend( - iter::once(&self.primary_cursor) - .chain(&self.cursors) - .filter_map(|cursor| { - let selection = &self.contents[cursor.range()]; - - let popup_lines = inspect_color(selection); - - if popup_lines.is_empty() { - None - } else { - Some(Popup::new(cursor.lower_bound(), popup_lines)) - } - }) - .sorted_unstable_by_key(|popup| popup.at) - ); - - if self.popups.is_empty() { - self.inspection_status = None; - } - } -} - -fn inspect(selection: &[u8]) -> Vec> { - let nat = bytes_to_nat(selection); - - let int = nat.and_then(|nat| nat_to_int_if_different(nat, selection.len())); - - let utf8 = str::from_utf8(selection).ok() - .filter(|_| selection.len() == 1) - .map(|utf8| utf8.trim_suffix('\0')) - .filter(|utf8| !utf8.contains(is_illegal_control_character)) - .map(|utf8| Span::from(format!("\"{utf8}\"")).red()); - - let fixedpoint2012 = nat - .filter(|_| selection.len() == 4) - .map(|nat| f64::from(nat as u32) / f64::from(1 << 12)) - .map(|fixedpoint2012| { - let two_decimals_is_enough = (fixedpoint2012 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("20.12: {approximate_symbol}{fixedpoint2012:.2}").into() - }); - - let fixedpoint2012_signed = int - .filter(|_| selection.len() == 4) - .map(|int| f64::from(int as i32) / f64::from(1 << 12)) - .map(|fixedpoint2012_signed| { - let two_decimals_is_enough = (fixedpoint2012_signed * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("i20.12: {approximate_symbol}{fixedpoint2012_signed:.2}").into() - }); - - let fixedpoint1616 = nat - .filter(|_| selection.len() == 4) - .map(|nat| f64::from(nat as u32) / f64::from(1 << 16)) - .map(|fixedpoint1616| { - let two_decimals_is_enough = (fixedpoint1616 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("16.16: {approximate_symbol}{fixedpoint1616:.2}").into() - }); - - let fixedpoint1616_signed = int - .filter(|_| selection.len() == 4) - .map(|int| f64::from(int as i32) / f64::from(1 << 16)) - .map(|fixedpoint1616_signed| { - let two_decimals_is_enough = (fixedpoint1616_signed * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("i16.16: {approximate_symbol}{fixedpoint1616_signed:.2}").into() - }); - - let fixedpoint124 = nat - .filter(|_| selection.len() == 2) - .map(|nat| f64::from(nat as u16) / f64::from(1 << 4)) - .map(|fixedpoint124| { - let two_decimals_is_enough = (fixedpoint124 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("12.4: {approximate_symbol}{fixedpoint124:.2}").into() - }); - - let fixedpoint88 = nat - .filter(|_| selection.len() == 2) - .map(|nat| f64::from(nat as u16) / f64::from(1 << 8)) - .map(|fixedpoint88| { - let two_decimals_is_enough = (fixedpoint88 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("8.8: {approximate_symbol}{fixedpoint88:.2}").into() - }); - - let fixedpoint412 = nat - .filter(|_| selection.len() == 2) - .map(|nat| f64::from(nat as u16) / f64::from(1 << 12)) - .map(|fixedpoint412| { - let two_decimals_is_enough = (fixedpoint412 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("4.12: {approximate_symbol}{fixedpoint412:.2}").into() - }); - - let color888 = (selection.len() == 3) - .then(|| [selection[0], selection[1], selection[2]]) - .map(|[red, green, blue]| { - Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) - .fg(Color::Rgb(red, green, blue)) - - }); - - let color555 = nat - .filter(|_| selection.len() == 2) - .filter(|&nat| nat >> 15 == 0) - .map(|nat| color555_to_color888(nat as u16)) - .map(|[red, green, blue]| { - Span::from(format!("555: #{red:02X}{green:02X}{blue:02X}")) - .fg(Color::Rgb(red, green, blue)) - - }); - - int.map(|int| format!("{int}").into()) - .into_iter() - .chain(nat.map(|nat| format!("{nat}").into())) - .chain(utf8) - .chain(fixedpoint2012_signed) - .chain(fixedpoint2012) - .chain(fixedpoint1616_signed) - .chain(fixedpoint1616) - .chain(fixedpoint124) - .chain(fixedpoint88) - .chain(fixedpoint412) - .chain(color888) - .chain(color555) - .collect() -} - -fn inspect_color(selection: &[u8]) -> Vec> { - let nat = bytes_to_nat(selection); - - let color888 = (selection.len() == 3) - .then(|| [selection[0], selection[1], selection[2]]) - .map(|[red, green, blue]| { - Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) - .fg(Color::Rgb(red, green, blue)) - - }); - - let color555 = nat - .filter(|_| selection.len() == 2) - .filter(|&nat| nat >> 15 == 0) - .map(|nat| color555_to_color888(nat as u16)) - .map(|[red, green, blue]| { - Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) - .fg(Color::Rgb(red, green, blue)) - - }); - - color888 - .into_iter() - .chain(color555) - .collect() -} - -// helpers -impl Buffer { - pub fn clamp_screen_to_primary_cursor(&mut self, window_size: WindowSize) { - if self.primary_cursor.head < self.scroll_position + BYTES_OF_PADDING { - self.align_view_top(); - } else if self.primary_cursor.head > self.scroll_position + (window_size.visible_byte_count() - 1).saturating_sub(BYTES_OF_PADDING) { - self.align_view_bottom(window_size); - } - } -} - -pub fn bytes_to_nat(bytes: &[u8]) -> Option { - bytes - .iter() - .rev() // little-endian - .skip_while(|&&byte| byte == 0) - .try_fold(u64::default(), |result, &byte| { - Some(result.shl_exact(8)? | u64::from(byte)) - }) -} - -const fn nat_to_int_if_different(nat: u64, bytes: usize) -> Option { - match bytes { - 1 if nat > i8::MAX as u64 => Some((nat as u8).cast_signed() as i64), - 2 if nat > i16::MAX as u64 => Some((nat as u16).cast_signed() as i64), - 4 if nat > i32::MAX as u64 => Some((nat as u32).cast_signed() as i64), - 8 if nat > i64::MAX as u64 => Some(nat.cast_signed()), - _ => None, - } -} - -#[test] -fn nat_to_int_tests() { - assert_eq!(nat_to_int_if_different(0, 1), None); - assert_eq!(nat_to_int_if_different(i8::MAX as u64, 1), None); - assert_eq!(nat_to_int_if_different(i8::MAX as u64 + 1, 1), Some(i8::MIN.into())); - assert_eq!(nat_to_int_if_different(u8::MAX.into(), 1), Some(-1)); - - assert_eq!(nat_to_int_if_different(0, 2), None); - assert_eq!(nat_to_int_if_different(i16::MAX as u64, 2), None); - assert_eq!(nat_to_int_if_different(i16::MAX as u64 + 1, 2), Some(i16::MIN.into())); - assert_eq!(nat_to_int_if_different(u16::MAX.into(), 2), Some(-1)); -} - -// or 0 if no mark is before -fn mark_before(offset: usize, sorted_marks: &[usize]) -> usize { - match sorted_marks.binary_search(&offset) { - Ok(_) => offset, - Err(0) => 0, - Err(mark_after_index) => sorted_marks[mark_after_index - 1], - } -} - -// or end index if no mark is after -fn mark_after(offset: usize, sorted_marks: &[usize], max: usize) -> usize { - if sorted_marks.is_empty() { return max + 1; } - - match sorted_marks.binary_search(&offset) { - Ok(mark_before_index) => if mark_before_index == sorted_marks.len() - 1 { - max + 1 - } else { - sorted_marks[mark_before_index + 1] - }, - Err(mark_after_index) => { - if mark_after_index == sorted_marks.len() { - max + 1 - } else { - sorted_marks[mark_after_index] - } - }, - } -} - -const fn is_illegal_control_character(character: char) -> bool { - match character { - '\t' | '\n' | '\r' => false, - _ if character.is_ascii_control() => true, - _ => false, - } -} - -const fn color555_to_color888(color555: u16) -> [u8; 3] { - [ - // 8 is the ratio between the number of colors in 555 vs 888 (32:256) - (color555 & 0b11111) as u8 * 8, - (color555 >> 5 & 0b11111) as u8 * 8, - (color555 >> 10 & 0b11111) as u8 * 8 - ] -} diff --git a/src/app/mod.rs b/src/app.rs similarity index 74% rename from src/app/mod.rs rename to src/app.rs index 32b52a6..2d10350 100644 --- a/src/app/mod.rs +++ b/src/app.rs @@ -1,11 +1,10 @@ -use std::{env, io, path::PathBuf, process::exit, time::Duration}; +use std::{io, path::PathBuf, process::exit, time::Duration}; use crossterm::{ExecutableCommand, event::{self, DisableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}, terminal::window_size}; use ratatui::{DefaultTerminal, style::Stylize, text::Span}; -use crate::{BYTES_PER_LINE, action::AppAction, buffer::Buffer, config::{Config, ConfigInitError}, cursor::Cursor}; +use crate::{BYTES_PER_LINE, action::AppAction, buffer::Buffer, config::{Config, ConfigInitError}, cursor::Cursor, window_size::WindowSize}; mod widget; - -const MACOS_STDIN_BROKEN_MESSAGE: &str = "reading from stdin on macOS does not work due to a limitation in crossterm. see https://github.com/crossterm-rs/crossterm/issues/396"; +mod actions; pub struct App { pub config: Config, @@ -23,16 +22,13 @@ pub struct App { pub logs: Vec, } -#[derive(Clone, Copy)] -pub struct WindowSize { - pub rows: usize, - pub covered_rows: usize, -} - impl App { - pub fn new() -> Self { + pub fn new( + config_path: Option, + files: &[PathBuf], + ) -> Self { let config = { - let config = Config::init(); + let config = Config::init(config_path); match &config { Err(ConfigInitError::IO(io_error)) if io_error.kind() != io::ErrorKind::NotFound => { @@ -56,10 +52,9 @@ impl App { let mut error_alert: Option = None; - let mut buffers: Vec = env::args() - .skip(1) - .map(Into::into) - .filter_map(|path: PathBuf| { + let mut buffers: Vec = files + .iter() + .filter_map(|path| { Buffer::from_file_at(path.clone()) .inspect_err(|error| { error_alert = Some( @@ -70,9 +65,9 @@ impl App { }) .collect(); - if env::args().len() <= 1 { + if files.is_empty() { #[cfg(target_os = "macos")] { - eprintln!("{MACOS_STDIN_BROKEN_MESSAGE}"); + eprintln!("please provide at least one file as input. use --help for options"); exit(1); } @@ -137,7 +132,7 @@ impl App { if error.kind() == ErrorKind::Other { let error_message = error.to_string(); if error_message == "Failed to initialize input reader" { - eprintln!("{MACOS_STDIN_BROKEN_MESSAGE}"); + eprintln!("reading from stdin on macOS does not work due to a limitation in crossterm. see https://github.com/crossterm-rs/crossterm/issues/396"); exit(1); } } @@ -244,59 +239,4 @@ impl App { _ => (), } } - - fn quit_if_saved(&mut self) { - if self.buffers.iter().all(Buffer::all_changes_saved) { - self.quit(); - } else { - self.buffers[self.current_buffer_index].alert_message = Span::from( - "there are unsaved changes, use Q to override" - ).red(); - } - } - - const fn quit(&mut self) { - self.should_quit = true; - } - - const fn previous_buffer(&mut self) { - if self.current_buffer_index == 0 { - self.current_buffer_index = self.buffers.len() - 1; - } else { - self.current_buffer_index -= 1; - } - } - - const fn next_buffer(&mut self) { - if self.current_buffer_index == self.buffers.len() - 1 { - self.current_buffer_index = 0; - } else { - self.current_buffer_index += 1; - } - } - - fn yank(&mut self) { - let current_buffer = &self.buffers[self.current_buffer_index]; - - self.primary_cursor_register = current_buffer - .contents[current_buffer.primary_cursor.range()] - .to_vec(); - - self.other_cursor_registers = current_buffer.cursors - .iter() - .map(|cursor| { - current_buffer.contents[cursor.range()].to_vec() - }) - .collect(); - } -} - -impl WindowSize { - pub const fn visible_byte_count(&self) -> usize { - self.hex_rows() * BYTES_PER_LINE - } - - pub const fn hex_rows(&self) -> usize { - self.rows - self.covered_rows - } } diff --git a/src/app/actions.rs b/src/app/actions.rs new file mode 100644 index 0000000..1336c04 --- /dev/null +++ b/src/app/actions.rs @@ -0,0 +1,50 @@ +use ratatui::{style::Stylize, text::Span}; + +use crate::{app::App, buffer::Buffer}; + +impl App { + pub fn quit_if_saved(&mut self) { + if self.buffers.iter().all(Buffer::all_changes_saved) { + self.quit(); + } else { + self.buffers[self.current_buffer_index].alert_message = Span::from( + "there are unsaved changes, use Q to override" + ).red(); + } + } + + pub const fn quit(&mut self) { + self.should_quit = true; + } + + pub const fn previous_buffer(&mut self) { + if self.current_buffer_index == 0 { + self.current_buffer_index = self.buffers.len() - 1; + } else { + self.current_buffer_index -= 1; + } + } + + pub const fn next_buffer(&mut self) { + if self.current_buffer_index == self.buffers.len() - 1 { + self.current_buffer_index = 0; + } else { + self.current_buffer_index += 1; + } + } + + pub fn yank(&mut self) { + let current_buffer = &self.buffers[self.current_buffer_index]; + + self.primary_cursor_register = current_buffer + .contents[current_buffer.primary_cursor.range()] + .to_vec(); + + self.other_cursor_registers = current_buffer.cursors + .iter() + .map(|cursor| { + current_buffer.contents[cursor.range()].to_vec() + }) + .collect(); + } +} diff --git a/src/arguments.rs b/src/arguments.rs new file mode 100644 index 0000000..774c2ce --- /dev/null +++ b/src/arguments.rs @@ -0,0 +1,14 @@ +use std::path::PathBuf; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about)] +pub struct Arguments { + /// specify a file to use for configuration + #[arg(short, long, value_name = "file")] + pub config: Option, + + /// the input files to edit + #[arg(value_name = "files")] + pub files: Vec, +} diff --git a/src/buffer/mod.rs b/src/buffer.rs similarity index 82% rename from src/buffer/mod.rs rename to src/buffer.rs index 8484e1f..881d7eb 100644 --- a/src/buffer/mod.rs +++ b/src/buffer.rs @@ -1,11 +1,12 @@ use core::slice::GetDisjointMutIndex; use std::{collections::HashSet, fs::File, io::{self, Read}, path::PathBuf}; use crossterm::event::KeyEvent; -use ratatui::{layout::{Constraint, Rect}, style::{Color, Style, Stylize}, text::Span, widgets::{Block, Borders, Clear, Widget}}; +use ratatui::{style::Stylize, text::Span}; use serde::{Deserialize, Serialize}; -use crate::{BYTES_PER_LINE, action::{Action, AppAction, bytes_to_nat}, app::WindowSize, config::Config, cursor::Cursor, edit_action::EditAction}; +use crate::{BYTES_PER_LINE, action::{Action, AppAction}, buffer::actions::bytes_to_nat, config::Config, cursor::Cursor, edit_action::EditAction, popup::Popup, window_size::WindowSize}; mod widget; +mod actions; pub struct Buffer { pub file_name: String, @@ -51,52 +52,11 @@ pub enum PartialAction { Goto, View, Replace, Space, Repeat, To } -#[derive(Clone)] -pub struct Popup { - pub at: usize, - width: u16, - primary: bool, - lines: Vec> -} - #[derive(Clone, Copy, PartialEq, Eq)] pub enum InspectionStatus { Normal, ColorsOnly } -impl Mode { - pub const fn label(self) -> &'static str { - match self { - Self::Normal => " NORMAL ", - Self::Select => " SELECT ", - Self::Insert => " INSERT ", - } - } - - pub const fn color(self) -> Color { - match self { - Self::Normal => Color::Blue, - Self::Select => Color::Yellow, - Self::Insert => Color::Green, - } - } -} - -impl PartialAction { - pub const fn label(self) -> &'static str { - use PartialAction::*; - - match self { - Goto => "g", - View => "z", - Replace => "r", - Space => "␠", - Repeat => "×", - To => "t", - } - } -} - impl TryFrom<&str> for PartialAction { type Error = (); @@ -115,61 +75,6 @@ impl TryFrom<&str> for PartialAction { } } -impl Popup { - pub fn new(at: usize, lines: Vec>) -> Self { - Self { - at, - width: lines - .iter() - .map(|line| line.width() as u16) - .max() - .unwrap_or(0), - primary: false, - lines - } - } - - const fn area_at(&self, x: u16, y: u16) -> Rect { - Rect { - x, - y, - width: self.width + 2, - height: self.lines.len() as u16 - } - } - - #[allow(clippy::wrong_self_convention)] - const fn as_primary(mut self) -> Self { - self.primary = true; - self - } -} - -impl Widget for Popup { - fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) { - Clear.render(area, buf); - - let border_color = if self.primary { - Style::new().white() - } else { - Style::new().gray() - }; - - Block::new() - .on_dark_gray() - .borders(Borders::LEFT | Borders::RIGHT) - .border_style(border_color) - .render(area, buf); - - for (line, area) in self.lines.iter().zip(area.rows()) { - line.render( - area.centered_horizontally(Constraint::Length(line.width() as u16)), - buf - ); - } - } -} - impl Buffer { pub fn from_file_at(file_path: PathBuf) -> io::Result { let mut file = File::open(&file_path)?; diff --git a/src/buffer/actions.rs b/src/buffer/actions.rs new file mode 100644 index 0000000..fa41ed8 --- /dev/null +++ b/src/buffer/actions.rs @@ -0,0 +1,923 @@ +use std::{cmp::min, collections::hash_set::Entry, convert::identity, fs::File, io::Write, iter, mem::{replace, swap}}; +use itertools::Itertools; +use ratatui::{style::{Color, Stylize}, text::Span}; +use crate::{BYTES_OF_PADDING, BYTES_PER_LINE, LINES_OF_PADDING, action::BufferAction, buffer::{Buffer, InspectionStatus, Mode, PartialAction}, cursor::Cursor, edit_action::EditAction, popup::Popup, window_size::WindowSize}; + +impl Buffer { + pub fn execute(&mut self, action: BufferAction, window_size: WindowSize) { + match action { + BufferAction::NormalMode => self.normal_mode(), + BufferAction::SelectMode => self.select_mode(), + + BufferAction::Goto => self.goto(), + BufferAction::View => self.view(), + BufferAction::Replace => self.replace(), + BufferAction::Space => self.space(), + BufferAction::Repeat => self.repeat(), + BufferAction::To => self.to(), + + BufferAction::ScrollDown => self.scroll_down(window_size), + BufferAction::ScrollUp => self.scroll_up(window_size), + + BufferAction::PageCursorHalfDown => self.page_cursor_half_down(window_size), + BufferAction::PageCursorHalfUp => self.page_cursor_half_up(window_size), + + BufferAction::PageDown => self.page_down(window_size), + BufferAction::PageUp => self.page_up(window_size), + + BufferAction::CollapseSelection => self.collapse_selection(), + BufferAction::FlipSelections => self.flip_selection(window_size), + + BufferAction::Delete => self.delete(window_size), + + BufferAction::Undo => self.undo(window_size), + BufferAction::Redo => self.redo(window_size), + + BufferAction::Save => self.save(), + + BufferAction::CopySelectionOnNextLine => self.copy_selection_on_next_line(window_size), + + BufferAction::RotateSelectionsBackward => self.rotate_selections_backward(window_size), + BufferAction::RotateSelectionsForward => self.rotate_selections_forward(window_size), + + BufferAction::KeepPrimarySelection => self.keep_primary_selection(), + BufferAction::RemovePrimarySelection => self.remove_primary_selection(), + + BufferAction::SplitSelectionsInto1s => self.split_selections_into_size(1, window_size), + BufferAction::SplitSelectionsInto2s => self.split_selections_into_size(2, window_size), + BufferAction::SplitSelectionsInto3s => self.split_selections_into_size(3, window_size), + BufferAction::SplitSelectionsInto4s => self.split_selections_into_size(4, window_size), + BufferAction::SplitSelectionsInto5s => self.split_selections_into_size(5, window_size), + BufferAction::SplitSelectionsInto6s => self.split_selections_into_size(6, window_size), + BufferAction::SplitSelectionsInto7s => self.split_selections_into_size(7, window_size), + BufferAction::SplitSelectionsInto8s => self.split_selections_into_size(8, window_size), + BufferAction::SplitSelectionsInto9s => self.split_selections_into_size(9, window_size), + + BufferAction::JumpToSelectedOffset => self.jump_to_selected_offset(window_size), + BufferAction::JumpToSelectedOffsetRelativeToMark => self.jump_to_selected_offset_relative_to_mark(window_size), + + BufferAction::ToggleMark => self.toggle_mark(), + + BufferAction::AlignViewCenter => self.align_view_center(window_size), + BufferAction::AlignViewBottom => self.align_view_bottom(window_size), + BufferAction::AlignViewTop => self.align_view_top(), + + BufferAction::ExtendToMark => self.extend_to_mark(window_size), + BufferAction::ExtendToNull => self.extend_to_null(window_size), + BufferAction::ExtendToFF => self.extend_to_FF(window_size), + + BufferAction::InspectSelection => self.inspect_selection(), + BufferAction::InspectSelectionColor => self.inspect_selection_color(), + } + } + + const fn normal_mode(&mut self) { + self.mode = Mode::Normal; + } + + const fn select_mode(&mut self) { + self.mode = Mode::Select; + } + + const fn goto(&mut self) { + self.partial_action = Some(PartialAction::Goto); + } + + const fn view(&mut self) { + self.partial_action = Some(PartialAction::View); + } + + const fn replace(&mut self) { + if !self.contents.is_empty() { + self.partial_action = Some(PartialAction::Replace); + } + } + + const fn space(&mut self) { + self.partial_action = Some(PartialAction::Space); + } + + const fn repeat(&mut self) { + self.partial_action = Some(PartialAction::Repeat); + } + + const fn to(&mut self) { + self.partial_action = Some(PartialAction::To); + } + + pub fn scroll_down(&mut self, window_size: WindowSize) { + if self.contents.len() <= BYTES_OF_PADDING { return; } + + self.scroll_position = min( + self.scroll_position + BYTES_PER_LINE, + self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE + ); + + if window_size.hex_rows() > LINES_OF_PADDING * 2 { + self.primary_cursor.clamp( + self.scroll_position + BYTES_OF_PADDING, + window_size.visible_byte_count() - BYTES_OF_PADDING * 2 + ); + } else { + self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); + } + self.combine_cursors_if_overlapping(); + } + + pub fn scroll_up(&mut self, window_size: WindowSize) { + self.scroll_position = self.scroll_position.saturating_sub(BYTES_PER_LINE); + if window_size.hex_rows() > LINES_OF_PADDING * 2 { + self.primary_cursor.clamp( + self.scroll_position + BYTES_OF_PADDING, + window_size.visible_byte_count() - BYTES_OF_PADDING * 2 + ); + } else { + self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); + } + self.combine_cursors_if_overlapping(); + } + + fn page_cursor_half_down(&mut self, window_size: WindowSize) { + if self.contents.len() <= BYTES_OF_PADDING { return; } + + let old_scroll_position = self.scroll_position; + + self.scroll_position = min( + self.scroll_position + (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE), + self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE + ); + + let scroll_position_change = self.scroll_position - old_scroll_position; + let max_contents_index = self.max_contents_index(); + + self.primary_cursor.head = min( + self.primary_cursor.head + scroll_position_change, + max_contents_index + ); + self.primary_cursor.tail = min( + self.primary_cursor.tail + scroll_position_change, + max_contents_index + ); + + for cursor in &mut self.cursors { + cursor.head = (cursor.head + scroll_position_change).min(max_contents_index); + cursor.tail = (cursor.tail + scroll_position_change).min(max_contents_index); + } + + self.combine_cursors_if_overlapping(); + } + + fn page_cursor_half_up(&mut self, window_size: WindowSize) { + let old_scroll_position = self.scroll_position; + + self.scroll_position = self.scroll_position.saturating_sub( + (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE) + ); + + let scroll_position_change = old_scroll_position - self.scroll_position; + let max_contents_index = self.max_contents_index(); + + self.primary_cursor.head = min( + self.primary_cursor.head - scroll_position_change, + max_contents_index + ); + self.primary_cursor.tail = min( + self.primary_cursor.tail - scroll_position_change, + max_contents_index + ); + + for cursor in &mut self.cursors { + cursor.head = (cursor.head - scroll_position_change).min(max_contents_index); + cursor.tail = (cursor.tail - scroll_position_change).min(max_contents_index); + } + + self.combine_cursors_if_overlapping(); + } + + fn page_down(&mut self, window_size: WindowSize) { + if self.contents.len() <= BYTES_OF_PADDING { return; } + + self.scroll_position = min( + self.scroll_position + window_size.visible_byte_count(), + self.max_contents_index() - BYTES_OF_PADDING - self.max_contents_index() % BYTES_PER_LINE + ); + + if window_size.hex_rows() > LINES_OF_PADDING * 2 { + self.primary_cursor.clamp( + self.scroll_position + BYTES_OF_PADDING, + window_size.visible_byte_count() - BYTES_OF_PADDING * 2 + ); + } else { + self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); + } + self.combine_cursors_if_overlapping(); + } + + fn page_up(&mut self, window_size: WindowSize) { + self.scroll_position = self.scroll_position.saturating_sub( + window_size.visible_byte_count() + ); + + if window_size.hex_rows() > LINES_OF_PADDING * 2 { + self.primary_cursor.clamp( + self.scroll_position + BYTES_OF_PADDING, + window_size.visible_byte_count() - BYTES_OF_PADDING * 2 + ); + } else { + self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); + } + self.combine_cursors_if_overlapping(); + } + + fn collapse_selection(&mut self) { + self.primary_cursor.collapse(); + + for cursor in &mut self.cursors { + cursor.collapse(); + } + } + + fn flip_selection(&mut self, window_size: WindowSize) { + self.primary_cursor.flip(); + + for cursor in &mut self.cursors { + cursor.flip(); + } + + self.clamp_screen_to_primary_cursor(window_size); + } + + fn delete(&mut self, window_size: WindowSize) { + if !self.contents.is_empty() { + self.execute_and_add( + EditAction::Delete { + primary_cursor: self.primary_cursor, + cursors: self.cursors.clone(), + primary_old_data: self.contents[self.primary_cursor.range()].into(), + old_data: self.cursors + .iter() + .map(|cursor| self.contents[cursor.range()].to_vec()) + .collect(), + }, + window_size + ); + } + + if self.mode == Mode::Select { + self.mode = Mode::Normal; + } + } + + fn undo(&mut self, window_size: WindowSize) { + if self.time_traveling == Some(0) || self.edit_history.is_empty() { return; } + + let current_date = self.time_traveling + .map_or(self.edit_history.len() - 1, |date| date - 1); + + self.time_traveling = Some(current_date); + + let edit_action = replace( + &mut self.edit_history[current_date], + EditAction::Placeholder + ); + + self.undo_edit(&edit_action, window_size); + + self.edit_history[current_date] = edit_action; + } + + fn redo(&mut self, window_size: WindowSize) { + let Some(previous_date) = self.time_traveling else { return; }; + + let current_date = previous_date + 1; + + self.time_traveling = if current_date == self.edit_history.len() { + None + } else { + Some(current_date) + }; + + let edit_action = replace( + &mut self.edit_history[previous_date], + EditAction::Placeholder + ); + + self.execute_edit(&edit_action, window_size); + + self.edit_history[previous_date] = edit_action; + } + + fn save(&mut self) { + let mut file = File::create(&self.file_path).unwrap(); + file.write_all(&self.contents).unwrap(); + + self.last_saved_at = Some( + self.time_traveling.unwrap_or(self.edit_history.len()) + ); + } + + fn copy_selection_on_next_line(&mut self, window_size: WindowSize) { + let new_cursors: Vec = iter::once(&self.primary_cursor) + .chain(&self.cursors) + .filter_map(|cursor| { + let number_of_lines_tall = (cursor.upper_bound() - cursor.lower_bound()) / BYTES_PER_LINE; + let offset_to_add = (number_of_lines_tall + 1) * BYTES_PER_LINE; + + if cursor.lower_bound() + offset_to_add < self.contents.len() { + Some( + Cursor { + head: min(cursor.head + offset_to_add, self.max_contents_index()), + tail: min(cursor.tail + offset_to_add, self.max_contents_index()) + } + ) + } else { + None + } + }) + .collect(); + + self.cursors.extend(new_cursors); + self.cursors.sort_by_key(|cursor| cursor.head); + + self.combine_cursors_if_overlapping(); + + self.rotate_selections_forward(window_size); + } + + fn rotate_selections_backward(&mut self, window_size: WindowSize) { + if self.cursors.is_empty() { return; } + + let next_cursor_index = self.cursors + .binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head) + .unwrap_or_else(identity); + + + if next_cursor_index == 0 { + let cursor_count = self.cursors.len(); + swap(&mut self.primary_cursor, &mut self.cursors[cursor_count - 1]); + + self.cursors.sort_by_key(|cursor| cursor.head); + } else { + swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index - 1]); + } + + self.clamp_screen_to_primary_cursor(window_size); + } + + fn rotate_selections_forward(&mut self, window_size: WindowSize) { + if self.cursors.is_empty() { return; } + + let next_cursor_index = self.cursors + .binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head) + .unwrap_or_else(identity); + + if next_cursor_index == self.cursors.len() { + swap(&mut self.primary_cursor, &mut self.cursors[0]); + + self.cursors.sort_by_key(|cursor| cursor.head); + } else { + swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index]); + } + + self.clamp_screen_to_primary_cursor(window_size); + } + + fn keep_primary_selection(&mut self) { + self.cursors.clear(); + } + + fn remove_primary_selection(&mut self) { + if self.cursors.is_empty() { return; } + + let next_cursor_index = self.cursors + .binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head) + .unwrap_or_else(identity); + + if next_cursor_index == self.cursors.len() { + self.primary_cursor = self.cursors.remove(0); + } else { + self.primary_cursor = self.cursors.remove(next_cursor_index); + } + } + + fn split_selections_into_size(&mut self, size: usize, window_size: WindowSize) { + if !iter::once(&self.primary_cursor) + .chain(&self.cursors) + .all(|cursor| cursor.len().is_multiple_of(size)) + { + self.alert_message = Span::from( + format!("not all selections are a multiple of {size} long") + ).red(); + return; + } + + let mut new_cursors = iter::once(self.primary_cursor) + .chain(self.cursors.iter().copied()) + .flat_map(|cursor| { + cursor + .range() + .step_by(size) + .map(|tail| Cursor { head: tail + size - 1, tail }) + }); + + self.primary_cursor = new_cursors.next().unwrap(); + self.cursors = new_cursors.collect(); + + self.clamp_screen_to_primary_cursor(window_size); + } + + fn jump_to_selected_offset(&mut self, window_size: WindowSize) { + if !iter::once(&self.primary_cursor) + .chain(&self.cursors) + .all(|cursor| { + bytes_to_nat(&self.contents[cursor.range()]) + .map(|nat| nat as usize) + .is_some_and(|offset| offset < self.contents.len()) + }) + { + if self.cursors.is_empty() { + self.alert_message = Span::from( + "selection is not a valid offset" + ).red(); + } else { + self.alert_message = Span::from( + "not all selections are valid offsets" + ).red(); + } + return; + } + + self.primary_cursor = Cursor::at( + bytes_to_nat(&self.contents[self.primary_cursor.range()]).unwrap() as usize + ); + + for cursor in &mut self.cursors { + *cursor = Cursor::at( + bytes_to_nat(&self.contents[cursor.range()]).unwrap() as usize + ); + } + + self.cursors.sort_by_key(|cursor| cursor.head); + + self.combine_cursors_if_overlapping(); + self.clamp_screen_to_primary_cursor(window_size); + } + + fn jump_to_selected_offset_relative_to_mark(&mut self, window_size: WindowSize) { + let mut sorted_marks: Vec<_> = self.marks.iter().copied().collect(); + sorted_marks.sort_unstable(); + + if !iter::once(&self.primary_cursor) + .chain(&self.cursors) + .all(|cursor| { + bytes_to_nat(&self.contents[cursor.range()]) + .map(|offset| mark_before(cursor.lower_bound(), &sorted_marks) + offset as usize) + .is_some_and(|offset| offset < self.contents.len()) + }) + { + if self.cursors.is_empty() { + self.alert_message = Span::from( + "selection is not a valid offset" + ).red(); + } else { + self.alert_message = Span::from( + "not all selections are valid offsets" + ).red(); + } + return; + } + + self.primary_cursor = Cursor::at( + bytes_to_nat(&self.contents[self.primary_cursor.range()]) + .map(|offset| { + mark_before(self.primary_cursor.lower_bound(), &sorted_marks) + offset as usize + }) + .unwrap() + ); + + for cursor in &mut self.cursors { + *cursor = Cursor::at( + bytes_to_nat(&self.contents[cursor.range()]) + .map(|offset| { + mark_before(cursor.lower_bound(), &sorted_marks) + offset as usize + }) + .unwrap() + ); + } + + self.cursors.sort_by_key(|cursor| cursor.head); + + self.combine_cursors_if_overlapping(); + self.clamp_screen_to_primary_cursor(window_size); + } + + fn toggle_mark(&mut self) { + match self.marks.entry(self.primary_cursor.lower_bound()) { + Entry::Occupied(occupied_entry) => { occupied_entry.remove(); }, + Entry::Vacant(vacant_entry) => vacant_entry.insert(), + } + + for cursor in &self.cursors { + match self.marks.entry(cursor.lower_bound()) { + Entry::Occupied(occupied_entry) => { occupied_entry.remove(); }, + Entry::Vacant(vacant_entry) => vacant_entry.insert(), + } + } + } + + const fn align_view_center(&mut self, window_size: WindowSize) { + let half_a_screen = window_size.visible_byte_count() / 2; + + self.scroll_position = self.primary_cursor.head + .saturating_sub(self.primary_cursor.head % BYTES_PER_LINE) + .saturating_sub(half_a_screen - (half_a_screen % BYTES_PER_LINE)); + } + + fn align_view_bottom(&mut self, window_size: WindowSize) { + self.scroll_position = self.primary_cursor.head + .saturating_sub(self.primary_cursor.head % BYTES_PER_LINE) + .saturating_sub( + window_size + .visible_byte_count() + .saturating_sub(BYTES_PER_LINE + BYTES_OF_PADDING) + ) + .min(self.max_contents_index() - self.max_contents_index() % BYTES_PER_LINE); + } + + const fn align_view_top(&mut self) { + self.scroll_position = self.primary_cursor.head + .saturating_sub(self.primary_cursor.head % BYTES_PER_LINE) + .saturating_sub(BYTES_OF_PADDING); + } + + fn extend_to_mark(&mut self, window_size: WindowSize) { + let mut sorted_marks: Vec<_> = self.marks.iter().copied().collect(); + sorted_marks.sort_unstable(); + + let max_contents_index = self.max_contents_index(); + + let mark_after_primary = mark_after( + self.primary_cursor.head, + &sorted_marks, + max_contents_index + ); + + self.primary_cursor.tail = self.primary_cursor.head; + self.primary_cursor.head = mark_after_primary - 1; + + for cursor in &mut self.cursors { + let mark_after_cursor = mark_after( + cursor.head, + &sorted_marks, + max_contents_index + ); + + cursor.tail = cursor.head; + cursor.head = mark_after_cursor - 1; + } + + self.clamp_screen_to_primary_cursor(window_size); + } + + fn extend_to_null(&mut self, window_size: WindowSize) { + if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..] + .iter() + .skip(1) + .position(|&byte| byte == 0) + { + self.primary_cursor.tail = self.primary_cursor.head; + self.primary_cursor.head += null_offset_after_primary; + } + + for cursor in &mut self.cursors { + if let Some(null_offset_after_primary) = self.contents[cursor.head..] + .iter() + .skip(1) + .position(|&byte| byte == 0) + { + cursor.tail = cursor.head; + cursor.head += null_offset_after_primary; + } + } + + self.clamp_screen_to_primary_cursor(window_size); + } + + #[allow(non_snake_case)] + fn extend_to_FF(&mut self, window_size: WindowSize) { + if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..] + .iter() + .skip(1) + .position(|&byte| byte == 0xFF) + { + self.primary_cursor.tail = self.primary_cursor.head; + self.primary_cursor.head += null_offset_after_primary; + } + + for cursor in &mut self.cursors { + if let Some(null_offset_after_primary) = self.contents[cursor.head..] + .iter() + .skip(1) + .position(|&byte| byte == 0xFF) + { + cursor.tail = cursor.head; + cursor.head += null_offset_after_primary; + } + } + + self.clamp_screen_to_primary_cursor(window_size); + } + + #[allow(clippy::too_many_lines)] + fn inspect_selection(&mut self) { + if self.inspection_status == Some(InspectionStatus::Normal) { + self.inspection_status = None; + return; + } + + self.inspection_status = Some(InspectionStatus::Normal); + + self.popups.extend( + iter::once(&self.primary_cursor) + .chain(&self.cursors) + .filter_map(|cursor| { + let selection = &self.contents[cursor.range()]; + + let popup_lines = inspect(selection); + + if popup_lines.is_empty() { + None + } else { + Some(Popup::new(cursor.lower_bound(), popup_lines)) + } + }) + .sorted_unstable_by_key(|popup| popup.at) + ); + + if self.popups.is_empty() { + self.inspection_status = None; + } + } + + fn inspect_selection_color(&mut self) { + if self.inspection_status == Some(InspectionStatus::ColorsOnly) { + self.inspection_status = None; + return; + } + + self.inspection_status = Some(InspectionStatus::ColorsOnly); + + self.popups.extend( + iter::once(&self.primary_cursor) + .chain(&self.cursors) + .filter_map(|cursor| { + let selection = &self.contents[cursor.range()]; + + let popup_lines = inspect_color(selection); + + if popup_lines.is_empty() { + None + } else { + Some(Popup::new(cursor.lower_bound(), popup_lines)) + } + }) + .sorted_unstable_by_key(|popup| popup.at) + ); + + if self.popups.is_empty() { + self.inspection_status = None; + } + } +} + +fn inspect(selection: &[u8]) -> Vec> { + let nat = bytes_to_nat(selection); + + let int = nat.and_then(|nat| nat_to_int_if_different(nat, selection.len())); + + let utf8 = str::from_utf8(selection).ok() + .filter(|_| selection.len() == 1) + .map(|utf8| utf8.trim_suffix('\0')) + .filter(|utf8| !utf8.contains(is_illegal_control_character)) + .map(|utf8| Span::from(format!("\"{utf8}\"")).red()); + + let fixedpoint2012 = nat + .filter(|_| selection.len() == 4) + .map(|nat| f64::from(nat as u32) / f64::from(1 << 12)) + .map(|fixedpoint2012| { + let two_decimals_is_enough = (fixedpoint2012 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("20.12: {approximate_symbol}{fixedpoint2012:.2}").into() + }); + + let fixedpoint2012_signed = int + .filter(|_| selection.len() == 4) + .map(|int| f64::from(int as i32) / f64::from(1 << 12)) + .map(|fixedpoint2012_signed| { + let two_decimals_is_enough = (fixedpoint2012_signed * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("i20.12: {approximate_symbol}{fixedpoint2012_signed:.2}").into() + }); + + let fixedpoint1616 = nat + .filter(|_| selection.len() == 4) + .map(|nat| f64::from(nat as u32) / f64::from(1 << 16)) + .map(|fixedpoint1616| { + let two_decimals_is_enough = (fixedpoint1616 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("16.16: {approximate_symbol}{fixedpoint1616:.2}").into() + }); + + let fixedpoint1616_signed = int + .filter(|_| selection.len() == 4) + .map(|int| f64::from(int as i32) / f64::from(1 << 16)) + .map(|fixedpoint1616_signed| { + let two_decimals_is_enough = (fixedpoint1616_signed * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("i16.16: {approximate_symbol}{fixedpoint1616_signed:.2}").into() + }); + + let fixedpoint124 = nat + .filter(|_| selection.len() == 2) + .map(|nat| f64::from(nat as u16) / f64::from(1 << 4)) + .map(|fixedpoint124| { + let two_decimals_is_enough = (fixedpoint124 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("12.4: {approximate_symbol}{fixedpoint124:.2}").into() + }); + + let fixedpoint88 = nat + .filter(|_| selection.len() == 2) + .map(|nat| f64::from(nat as u16) / f64::from(1 << 8)) + .map(|fixedpoint88| { + let two_decimals_is_enough = (fixedpoint88 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("8.8: {approximate_symbol}{fixedpoint88:.2}").into() + }); + + let fixedpoint412 = nat + .filter(|_| selection.len() == 2) + .map(|nat| f64::from(nat as u16) / f64::from(1 << 12)) + .map(|fixedpoint412| { + let two_decimals_is_enough = (fixedpoint412 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("4.12: {approximate_symbol}{fixedpoint412:.2}").into() + }); + + let color888 = (selection.len() == 3) + .then(|| [selection[0], selection[1], selection[2]]) + .map(|[red, green, blue]| { + Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) + .fg(Color::Rgb(red, green, blue)) + + }); + + let color555 = nat + .filter(|_| selection.len() == 2) + .filter(|&nat| nat >> 15 == 0) + .map(|nat| color555_to_color888(nat as u16)) + .map(|[red, green, blue]| { + Span::from(format!("555: #{red:02X}{green:02X}{blue:02X}")) + .fg(Color::Rgb(red, green, blue)) + + }); + + int.map(|int| format!("{int}").into()) + .into_iter() + .chain(nat.map(|nat| format!("{nat}").into())) + .chain(utf8) + .chain(fixedpoint2012_signed) + .chain(fixedpoint2012) + .chain(fixedpoint1616_signed) + .chain(fixedpoint1616) + .chain(fixedpoint124) + .chain(fixedpoint88) + .chain(fixedpoint412) + .chain(color888) + .chain(color555) + .collect() +} + +fn inspect_color(selection: &[u8]) -> Vec> { + let nat = bytes_to_nat(selection); + + let color888 = (selection.len() == 3) + .then(|| [selection[0], selection[1], selection[2]]) + .map(|[red, green, blue]| { + Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) + .fg(Color::Rgb(red, green, blue)) + + }); + + let color555 = nat + .filter(|_| selection.len() == 2) + .filter(|&nat| nat >> 15 == 0) + .map(|nat| color555_to_color888(nat as u16)) + .map(|[red, green, blue]| { + Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) + .fg(Color::Rgb(red, green, blue)) + + }); + + color888 + .into_iter() + .chain(color555) + .collect() +} + +// MARK: helpers +impl Buffer { + pub fn clamp_screen_to_primary_cursor(&mut self, window_size: WindowSize) { + if self.primary_cursor.head < self.scroll_position + BYTES_OF_PADDING { + self.align_view_top(); + } else if self.primary_cursor.head > self.scroll_position + (window_size.visible_byte_count() - 1).saturating_sub(BYTES_OF_PADDING) { + self.align_view_bottom(window_size); + } + } +} + +pub fn bytes_to_nat(bytes: &[u8]) -> Option { + bytes + .iter() + .rev() // little-endian + .skip_while(|&&byte| byte == 0) + .try_fold(u64::default(), |result, &byte| { + Some(result.shl_exact(8)? | u64::from(byte)) + }) +} + +const fn nat_to_int_if_different(nat: u64, bytes: usize) -> Option { + match bytes { + 1 if nat > i8::MAX as u64 => Some((nat as u8).cast_signed() as i64), + 2 if nat > i16::MAX as u64 => Some((nat as u16).cast_signed() as i64), + 4 if nat > i32::MAX as u64 => Some((nat as u32).cast_signed() as i64), + 8 if nat > i64::MAX as u64 => Some(nat.cast_signed()), + _ => None, + } +} + +#[test] +fn nat_to_int_tests() { + assert_eq!(nat_to_int_if_different(0, 1), None); + assert_eq!(nat_to_int_if_different(i8::MAX as u64, 1), None); + assert_eq!(nat_to_int_if_different(i8::MAX as u64 + 1, 1), Some(i8::MIN.into())); + assert_eq!(nat_to_int_if_different(u8::MAX.into(), 1), Some(-1)); + + assert_eq!(nat_to_int_if_different(0, 2), None); + assert_eq!(nat_to_int_if_different(i16::MAX as u64, 2), None); + assert_eq!(nat_to_int_if_different(i16::MAX as u64 + 1, 2), Some(i16::MIN.into())); + assert_eq!(nat_to_int_if_different(u16::MAX.into(), 2), Some(-1)); +} + +// or 0 if no mark is before +fn mark_before(offset: usize, sorted_marks: &[usize]) -> usize { + match sorted_marks.binary_search(&offset) { + Ok(_) => offset, + Err(0) => 0, + Err(mark_after_index) => sorted_marks[mark_after_index - 1], + } +} + +// or end index if no mark is after +fn mark_after(offset: usize, sorted_marks: &[usize], max: usize) -> usize { + if sorted_marks.is_empty() { return max + 1; } + + match sorted_marks.binary_search(&offset) { + Ok(mark_before_index) => if mark_before_index == sorted_marks.len() - 1 { + max + 1 + } else { + sorted_marks[mark_before_index + 1] + }, + Err(mark_after_index) => { + if mark_after_index == sorted_marks.len() { + max + 1 + } else { + sorted_marks[mark_after_index] + } + }, + } +} + +const fn is_illegal_control_character(character: char) -> bool { + match character { + '\t' | '\n' | '\r' => false, + _ if character.is_ascii_control() => true, + _ => false, + } +} + +const fn color555_to_color888(color555: u16) -> [u8; 3] { + [ + // 8 is the ratio between the number of colors in 555 vs 888 (32:256) + (color555 & 0b11111) as u8 * 8, + (color555 >> 5 & 0b11111) as u8 * 8, + (color555 >> 10 & 0b11111) as u8 * 8 + ] +} diff --git a/src/buffer/widget.rs b/src/buffer/widget.rs index fd900b9..d094f84 100644 --- a/src/buffer/widget.rs +++ b/src/buffer/widget.rs @@ -2,6 +2,12 @@ use std::{cmp::min, iter}; use ratatui::{layout::Rect, text::{Line, Text}, widgets::Widget}; use crate::{BYTES_PER_LINE, buffer::Buffer}; +mod address; +mod hex; +mod character_panel; +mod status_line; +mod extra_statuses; + impl Widget for &Buffer { fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) { let screen_end = self.scroll_position + BYTES_PER_LINE * (area.height as usize - 1); @@ -102,405 +108,6 @@ impl Buffer { } } -mod address { - use ratatui::{style::{Color, Style}, text::Span}; - - pub fn render_address(address: usize) -> Span<'static> { - Span { - style: style_for_address(address), - content: format!("{address:08x}").into() - } - } - - pub const fn style_for_address(address: usize) -> Style { - if address.is_multiple_of(0x100) { - Style::new().fg(Color::Rgb(0x68, 0x99, 0xA0)) - } else { - Style::new().fg(Color::Rgb(0x8A, 0xBB, 0xC3)) - } - } -} - -mod hex { - use std::{borrow::Cow, iter::{self, repeat_n}, mem}; - use itertools::Itertools; -use ratatui::{style::{Color, Style, Stylize}, text::Span}; - - use crate::{BYTES_PER_CHUNK, BYTES_PER_LINE, CHUNKS_PER_LINE, buffer::{Buffer, Mode, PartialAction}, cardinality::HasCardinality, cursor::InCursor, custom_greys::CustomGreys, empty_span::empty_span}; - - impl Buffer { - pub fn render_chunks( - &self, - address: usize, - bytes: &[u8; BYTES_PER_LINE] - ) -> impl Iterator> { - let (chunks, remainder) = bytes.as_chunks::(); - - assert!(remainder.is_empty(), "BYTES_PER_LINE should be a multiple of BYTES_PER_CHUNK"); - - #[allow(unstable_name_collisions)] - chunks - .iter() - .copied() - .zip((address..).step_by(BYTES_PER_CHUNK)) - .flat_map(|(chunk, address)| { - self.render_chunk(address, &chunk).collect::>() - }) - } - - pub fn render_partial_chunks( - &self, - address: usize, - bytes: &[u8] - ) -> impl Iterator> { - let (chunks, remainder) = bytes.as_chunks::(); - - let remainder_address = address + chunks.len() * BYTES_PER_CHUNK; - #[allow(clippy::if_not_else)] - let remainder_chunks: Option> = if !remainder.is_empty() { - Some(self.render_partial_chunk(remainder_address, remainder).collect()) - } else { - None - }; - - let chunks_rendered = chunks.len() + remainder_chunks.iter().len(); - let chunks_not_rendered = CHUNKS_PER_LINE - chunks_rendered; - let spaces_per_chunk = BYTES_PER_CHUNK - 1 + 2; - let bytes_not_rendered = BYTES_PER_LINE - bytes.len(); - - let padding_width = 2 * bytes_not_rendered + - spaces_per_chunk * chunks_not_rendered; - - #[allow(unstable_name_collisions)] - chunks - .iter() - .copied() - .zip((address..).step_by(BYTES_PER_CHUNK)) - .map(|(chunk, address)| self.render_chunk(address, &chunk).collect()) - .chain(remainder_chunks) - .flatten() - .chain(repeat_n(" ".into(), padding_width)) - } - - fn render_chunk( - &self, - address: usize, - bytes: &[u8; BYTES_PER_CHUNK] - ) -> impl Iterator> { - iter::once(self.render_large_space_before(address)) - .chain( - bytes - .iter() - .copied() - .zip(address..) - .map(|(byte, address)| self.render_byte_at(address, byte)) - .interleave( - (address..) - .take(BYTES_PER_CHUNK) - .skip(1) - .map(|address| self.render_space_before(address)) - ) - ) - } - - fn render_partial_chunk( - &self, - address: usize, - bytes: &[u8] - ) -> impl Iterator> { - iter::once(self.render_large_space_before(address)) - .chain( - bytes - .iter() - .copied() - .zip(address..) - .map(|(byte, address)| self.render_byte_at(address, byte)) - .interleave( - (address..) - .take(BYTES_PER_CHUNK) - .skip(1) - .map(|address| self.render_space_before(address)) - ) - ) - } - - fn render_byte_at( - &self, - address: usize, - byte: u8 - ) -> Span<'static> { - if self.partial_action == Some(PartialAction::Replace) && - iter::once(&self.primary_cursor) - .chain(&self.cursors) - .any(|cursor| cursor.contains(address).is_some()) - { - let replaced_byte = self.partial_replace.unwrap_or(0) << 4; - - self.render_byte_without_replace_preview(address, replaced_byte) - .black() - } else { - self.render_byte_without_replace_preview(address, byte) - } - } - - fn render_byte_without_replace_preview( - &self, - address: usize, - byte: u8 - ) -> Span<'static> { - const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_byte_lookup_table(); - - let span = SPAN_FOR_BYTE[byte as usize].clone(); - - if let Some(place_in_cursor) = self.primary_cursor.contains(address) { - let head_color = match self.mode { - Mode::Select => Color::Yellow, - _ => Color::Gray - }; - - match place_in_cursor { - InCursor::Head => span.bg(head_color), - InCursor::Rest => span.bg(Color::selection_tail_grey()), - } - } else { - match self.cursors - .iter() - .find_map(|cursor| cursor.contains(address)) - { - Some(InCursor::Head) => span.bg(Color::secondary_selection_head_grey()), - Some(InCursor::Rest) => span.bg(Color::selection_tail_grey()), - None => span, - } - } - } - - fn render_large_space_before(&self, address: usize) -> Span<'static> { - let span: Span = if self.marks.contains(&address) { - " →".into() - } else { - " ".into() - }; - - if !address.is_multiple_of(BYTES_PER_LINE) && - iter::once(&self.primary_cursor) - .chain(&self.cursors) - .any(|cursor| cursor.contains_space_before(address)) - { - span.bg(Color::selection_tail_grey()) - } else { - span - } - } - - fn render_space_before(&self, address: usize) -> Span<'static> { - let span: Span = if self.marks.contains(&address) { - "→".into() - } else { - " ".into() - }; - - if iter::once(&self.primary_cursor) - .chain(&self.cursors) - .any(|cursor| cursor.contains_space_before(address)) - { - span.bg(Color::selection_tail_grey()) - } else { - span - } - } - } - - const fn create_byte_lookup_table() -> [Span<'static>; u8::CARDINALITY] { - let mut result = [const { empty_span() }; u8::CARDINALITY]; - - let mut index = 0; - while index < u8::CARDINALITY { - result[index].style = style_for_byte(index as u8); - mem::forget(mem::replace(&mut result[index].content, content_for_byte(index as u8))); - index += 1; - } - - result - } - - const fn style_for_byte(byte: u8) -> Style { - Style::new().fg(fg_for_byte(byte)) - } - - const fn fg_for_byte(byte: u8) -> Color { - match byte { - 0x00 => Color::Rgb(0x80, 0x80, 0x80), // grey - 0x01..0x10 => Color::Rgb(0xFF, 0x71, 0xA9), // red - 0x10..0x20 => Color::Rgb(0xFF, 0x7A, 0x78), // salmon - 0x20..0x30 => Color::Rgb(0xFF, 0x81, 0x23), // red-orange - 0x30..0x40 => Color::Rgb(0xF7, 0x93, 0x00), // yellow-orange - 0x40..0x50 => Color::Rgb(0xE6, 0x9F, 0x00), // yellow - 0x50..0x60 => Color::Rgb(0xC1, 0xB2, 0x00), // green-yellow - 0x60..0x70 => Color::Rgb(0x82, 0xC6, 0x00), // lime - 0x70..0x80 => Color::Rgb(0x00, 0xD5, 0x00), // green - 0x80..0x90 => Color::Rgb(0x00, 0xD4, 0x59), // clover - 0x90..0xA0 => Color::Rgb(0x00, 0xD0, 0x91), // teal - 0xA0..0xB0 => Color::Rgb(0x00, 0xCC, 0xBB), // cyan - 0xB0..0xC0 => Color::Rgb(0x00, 0xC7, 0xDE), // light blue - 0xC0..0xD0 => Color::Rgb(0x00, 0xBE, 0xFF), // blue - 0xD0..0xE0 => Color::Rgb(0x6C, 0xAF, 0xFF), // blurple - 0xE0..0xF0 => Color::Rgb(0xB2, 0x98, 0xFF), // purple - 0xF0..0xFF => Color::Rgb(0xFF, 0x4D, 0xFF), // pink - 0xFF => Color::White - } - } - - const fn content_for_byte(byte: u8) -> Cow<'static, str> { - Cow::Borrowed(hex_for_byte(byte)) - } - - const fn hex_for_byte(byte: u8) -> &'static str { - const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff"]; - - LOOK_UP_TABLE[byte as usize] - } -} - -mod character_panel { - use std::{borrow::Cow, iter, mem}; - use ratatui::{style::{Color, Style, Stylize}, text::Span}; - use crate::{buffer::Buffer, cardinality::HasCardinality, cursor::InCursor, custom_greys::CustomGreys, empty_span::empty_span}; - - impl Buffer { - pub fn render_character_panel( - &self, - address: usize, - bytes: &[u8] - ) -> impl Iterator> { - bytes - .iter() - .copied() - .zip(address..) - .map(|(byte, address)| self.render_character_at(address, byte)) - } - - fn render_character_at( - &self, - address: usize, - byte: u8 - ) -> Span<'static> { - const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_character_lookup_table(); - - let span = SPAN_FOR_BYTE[byte as usize].clone(); - - match iter::once(&self.primary_cursor) - .chain(&self.cursors) - .find_map(|cursor| cursor.contains(address)) - { - Some(InCursor::Head) => span.bg(Color::selection_tail_grey()), - Some(InCursor::Rest) => span.on_dark_gray(), - None => span, - } - } - } - const fn create_character_lookup_table() -> [Span<'static>; u8::CARDINALITY] { - let mut result = [const { empty_span() }; u8::CARDINALITY]; - - let mut index = 0; - while index < u8::CARDINALITY { - result[index].style = style_for_character(index as u8); - mem::forget(mem::replace( - &mut result[index].content, - content_for_character(index as u8) - )); - index += 1; - } - - result - } - - const fn style_for_character(byte: u8) -> Style { - Style::new().fg(fg_for_character(byte)) - } - - const fn fg_for_character(byte: u8) -> Color { - match byte { - b'\0' => Color::Rgb(0xA0, 0xA0, 0xA0), // grey - b'\t' | b'\n' | b'\r' | b' ' => Color::Red, - _ if byte.is_ascii_graphic() => Color::Red, - _ if byte.is_ascii() => Color::Green, - 0xFF => Color::White, - _ => Color::Yellow, - } - } - - const fn content_for_character(byte: u8) -> Cow<'static, str> { - Cow::Borrowed(character_for_byte(byte)) - } - - const fn character_for_byte(byte: u8) -> &'static str { - const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["⋄", "•", "•", "•", "•", "•", "•", "•", "•", "→", "⏎", "•", "•", "␍", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "•", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "╳"]; - - LOOK_UP_TABLE[byte as usize] - } -} - -mod status_line { - use crate::{buffer::Buffer, custom_greys::CustomGreys}; - use ratatui::{style::{Color, Stylize}, text::{Line, Span, Text}}; - - impl Buffer { - pub fn render_status_line(&self) -> Text<'_> { - Text::from( - Line::from_iter([ - self.render_mode(), - " ".into(), - self.render_file_name(), - self.modified_indicator(), - " ".into(), - self.alert_message.clone() - ]) - ) - .bg(Color::ui_grey()) - } - - fn render_mode(&self) -> Span<'static> { - Span::from(self.mode.label()) - .black() - .bg(self.mode.color()) - } - - fn render_file_name(&self) -> Span<'_> { - Span::from(&self.file_name) - } - - fn modified_indicator(&self) -> Span<'static> { - if self.has_unsaved_changes() { - " [+]".into() - } else { - "".into() - } - } - } -} - -mod extra_statuses { - use crate::buffer::Buffer; - use ratatui::text::Line; - - impl Buffer { - pub fn render_extra_statuses(&self) -> Line<'_> { - let partial_action = self.partial_action - .as_ref() - .map_or("", |partial_action| partial_action.label()); - - if self.contents.is_empty() { - format!("{partial_action} ").into() - } else { - #[allow(clippy::cast_precision_loss)] - let percentage = self.primary_cursor.head as f64 / self.max_contents_index() as f64 * 100.0; - - format!("{partial_action} {percentage:.0}% ").into() - } - } - } -} - fn byte_column_to_screen_column(byte_column: usize) -> usize { match byte_column { 0 => 10, diff --git a/src/buffer/widget/address.rs b/src/buffer/widget/address.rs new file mode 100644 index 0000000..a79853f --- /dev/null +++ b/src/buffer/widget/address.rs @@ -0,0 +1,16 @@ +use ratatui::{style::{Color, Style}, text::Span}; + +pub fn render_address(address: usize) -> Span<'static> { + Span { + style: style_for_address(address), + content: format!("{address:08x}").into() + } +} + +pub const fn style_for_address(address: usize) -> Style { + if address.is_multiple_of(0x100) { + Style::new().fg(Color::Rgb(0x68, 0x99, 0xA0)) + } else { + Style::new().fg(Color::Rgb(0x8A, 0xBB, 0xC3)) + } +} diff --git a/src/buffer/widget/character_panel.rs b/src/buffer/widget/character_panel.rs new file mode 100644 index 0000000..a29cc0b --- /dev/null +++ b/src/buffer/widget/character_panel.rs @@ -0,0 +1,76 @@ +use std::{borrow::Cow, iter, mem}; +use ratatui::{style::{Color, Style, Stylize}, text::Span}; +use crate::{buffer::Buffer, cardinality::HasCardinality, cursor::InCursor, custom_greys::CustomGreys, empty_span::empty_span}; + +impl Buffer { + pub fn render_character_panel( + &self, + address: usize, + bytes: &[u8] + ) -> impl Iterator> { + bytes + .iter() + .copied() + .zip(address..) + .map(|(byte, address)| self.render_character_at(address, byte)) + } + + fn render_character_at( + &self, + address: usize, + byte: u8 + ) -> Span<'static> { + const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_character_lookup_table(); + + let span = SPAN_FOR_BYTE[byte as usize].clone(); + + match iter::once(&self.primary_cursor) + .chain(&self.cursors) + .find_map(|cursor| cursor.contains(address)) + { + Some(InCursor::Head) => span.bg(Color::selection_tail_grey()), + Some(InCursor::Rest) => span.on_dark_gray(), + None => span, + } + } +} +const fn create_character_lookup_table() -> [Span<'static>; u8::CARDINALITY] { + let mut result = [const { empty_span() }; u8::CARDINALITY]; + + let mut index = 0; + while index < u8::CARDINALITY { + result[index].style = style_for_character(index as u8); + mem::forget(mem::replace( + &mut result[index].content, + content_for_character(index as u8) + )); + index += 1; + } + + result +} + +const fn style_for_character(byte: u8) -> Style { + Style::new().fg(fg_for_character(byte)) +} + +const fn fg_for_character(byte: u8) -> Color { + match byte { + b'\0' => Color::Rgb(0xA0, 0xA0, 0xA0), // grey + b'\t' | b'\n' | b'\r' | b' ' => Color::Red, + _ if byte.is_ascii_graphic() => Color::Red, + _ if byte.is_ascii() => Color::Green, + 0xFF => Color::White, + _ => Color::Yellow, + } +} + +const fn content_for_character(byte: u8) -> Cow<'static, str> { + Cow::Borrowed(character_for_byte(byte)) +} + +const fn character_for_byte(byte: u8) -> &'static str { + const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["⋄", "•", "•", "•", "•", "•", "•", "•", "•", "→", "⏎", "•", "•", "␍", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "•", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "╳"]; + + LOOK_UP_TABLE[byte as usize] +} diff --git a/src/buffer/widget/extra_statuses.rs b/src/buffer/widget/extra_statuses.rs new file mode 100644 index 0000000..1057953 --- /dev/null +++ b/src/buffer/widget/extra_statuses.rs @@ -0,0 +1,34 @@ +use crate::buffer::{Buffer, PartialAction}; +use ratatui::text::Line; + +impl Buffer { + pub fn render_extra_statuses(&self) -> Line<'_> { + let partial_action = self.partial_action + .as_ref() + .map_or("", |partial_action| partial_action.label()); + + if self.contents.is_empty() { + format!("{partial_action} ").into() + } else { + #[allow(clippy::cast_precision_loss)] + let percentage = self.primary_cursor.head as f64 / self.max_contents_index() as f64 * 100.0; + + format!("{partial_action} {percentage:.0}% ").into() + } + } +} + +impl PartialAction { + pub const fn label(self) -> &'static str { + use PartialAction::*; + + match self { + Goto => "g", + View => "z", + Replace => "r", + Space => "␠", + Repeat => "×", + To => "t", + } + } +} diff --git a/src/buffer/widget/hex.rs b/src/buffer/widget/hex.rs new file mode 100644 index 0000000..3591547 --- /dev/null +++ b/src/buffer/widget/hex.rs @@ -0,0 +1,237 @@ +use std::{borrow::Cow, iter::{self, repeat_n}, mem}; +use itertools::Itertools; +use ratatui::{style::{Color, Style, Stylize}, text::Span}; + +use crate::{BYTES_PER_CHUNK, BYTES_PER_LINE, CHUNKS_PER_LINE, buffer::{Buffer, Mode, PartialAction}, cardinality::HasCardinality, cursor::InCursor, custom_greys::CustomGreys, empty_span::empty_span}; + +impl Buffer { + pub fn render_chunks( + &self, + address: usize, + bytes: &[u8; BYTES_PER_LINE] + ) -> impl Iterator> { + let (chunks, remainder) = bytes.as_chunks::(); + + assert!(remainder.is_empty(), "BYTES_PER_LINE should be a multiple of BYTES_PER_CHUNK"); + + #[allow(unstable_name_collisions)] + chunks + .iter() + .copied() + .zip((address..).step_by(BYTES_PER_CHUNK)) + .flat_map(|(chunk, address)| { + self.render_chunk(address, &chunk).collect::>() + }) + } + + pub fn render_partial_chunks( + &self, + address: usize, + bytes: &[u8] + ) -> impl Iterator> { + let (chunks, remainder) = bytes.as_chunks::(); + + let remainder_address = address + chunks.len() * BYTES_PER_CHUNK; + #[allow(clippy::if_not_else)] + let remainder_chunks: Option> = if !remainder.is_empty() { + Some(self.render_partial_chunk(remainder_address, remainder).collect()) + } else { + None + }; + + let chunks_rendered = chunks.len() + remainder_chunks.iter().len(); + let chunks_not_rendered = CHUNKS_PER_LINE - chunks_rendered; + let spaces_per_chunk = BYTES_PER_CHUNK - 1 + 2; + let bytes_not_rendered = BYTES_PER_LINE - bytes.len(); + + let padding_width = 2 * bytes_not_rendered + + spaces_per_chunk * chunks_not_rendered; + + #[allow(unstable_name_collisions)] + chunks + .iter() + .copied() + .zip((address..).step_by(BYTES_PER_CHUNK)) + .map(|(chunk, address)| self.render_chunk(address, &chunk).collect()) + .chain(remainder_chunks) + .flatten() + .chain(repeat_n(" ".into(), padding_width)) + } + + fn render_chunk( + &self, + address: usize, + bytes: &[u8; BYTES_PER_CHUNK] + ) -> impl Iterator> { + iter::once(self.render_large_space_before(address)) + .chain( + bytes + .iter() + .copied() + .zip(address..) + .map(|(byte, address)| self.render_byte_at(address, byte)) + .interleave( + (address..) + .take(BYTES_PER_CHUNK) + .skip(1) + .map(|address| self.render_space_before(address)) + ) + ) + } + + fn render_partial_chunk( + &self, + address: usize, + bytes: &[u8] + ) -> impl Iterator> { + iter::once(self.render_large_space_before(address)) + .chain( + bytes + .iter() + .copied() + .zip(address..) + .map(|(byte, address)| self.render_byte_at(address, byte)) + .interleave( + (address..) + .take(BYTES_PER_CHUNK) + .skip(1) + .map(|address| self.render_space_before(address)) + ) + ) + } + + fn render_byte_at( + &self, + address: usize, + byte: u8 + ) -> Span<'static> { + if self.partial_action == Some(PartialAction::Replace) && + iter::once(&self.primary_cursor) + .chain(&self.cursors) + .any(|cursor| cursor.contains(address).is_some()) + { + let replaced_byte = self.partial_replace.unwrap_or(0) << 4; + + self.render_byte_without_replace_preview(address, replaced_byte) + .black() + } else { + self.render_byte_without_replace_preview(address, byte) + } + } + + fn render_byte_without_replace_preview( + &self, + address: usize, + byte: u8 + ) -> Span<'static> { + const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_byte_lookup_table(); + + let span = SPAN_FOR_BYTE[byte as usize].clone(); + + if let Some(place_in_cursor) = self.primary_cursor.contains(address) { + let head_color = match self.mode { + Mode::Select => Color::Yellow, + _ => Color::Gray + }; + + match place_in_cursor { + InCursor::Head => span.bg(head_color), + InCursor::Rest => span.bg(Color::selection_tail_grey()), + } + } else { + match self.cursors + .iter() + .find_map(|cursor| cursor.contains(address)) + { + Some(InCursor::Head) => span.bg(Color::secondary_selection_head_grey()), + Some(InCursor::Rest) => span.bg(Color::selection_tail_grey()), + None => span, + } + } + } + + fn render_large_space_before(&self, address: usize) -> Span<'static> { + let span: Span = if self.marks.contains(&address) { + " →".into() + } else { + " ".into() + }; + + if !address.is_multiple_of(BYTES_PER_LINE) && + iter::once(&self.primary_cursor) + .chain(&self.cursors) + .any(|cursor| cursor.contains_space_before(address)) + { + span.bg(Color::selection_tail_grey()) + } else { + span + } + } + + fn render_space_before(&self, address: usize) -> Span<'static> { + let span: Span = if self.marks.contains(&address) { + "→".into() + } else { + " ".into() + }; + + if iter::once(&self.primary_cursor) + .chain(&self.cursors) + .any(|cursor| cursor.contains_space_before(address)) + { + span.bg(Color::selection_tail_grey()) + } else { + span + } + } +} + +const fn create_byte_lookup_table() -> [Span<'static>; u8::CARDINALITY] { + let mut result = [const { empty_span() }; u8::CARDINALITY]; + + let mut index = 0; + while index < u8::CARDINALITY { + result[index].style = style_for_byte(index as u8); + mem::forget(mem::replace(&mut result[index].content, content_for_byte(index as u8))); + index += 1; + } + + result +} + +const fn style_for_byte(byte: u8) -> Style { + Style::new().fg(fg_for_byte(byte)) +} + +const fn fg_for_byte(byte: u8) -> Color { + match byte { + 0x00 => Color::Rgb(0x80, 0x80, 0x80), // grey + 0x01..0x10 => Color::Rgb(0xFF, 0x71, 0xA9), // red + 0x10..0x20 => Color::Rgb(0xFF, 0x7A, 0x78), // salmon + 0x20..0x30 => Color::Rgb(0xFF, 0x81, 0x23), // red-orange + 0x30..0x40 => Color::Rgb(0xF7, 0x93, 0x00), // yellow-orange + 0x40..0x50 => Color::Rgb(0xE6, 0x9F, 0x00), // yellow + 0x50..0x60 => Color::Rgb(0xC1, 0xB2, 0x00), // green-yellow + 0x60..0x70 => Color::Rgb(0x82, 0xC6, 0x00), // lime + 0x70..0x80 => Color::Rgb(0x00, 0xD5, 0x00), // green + 0x80..0x90 => Color::Rgb(0x00, 0xD4, 0x59), // clover + 0x90..0xA0 => Color::Rgb(0x00, 0xD0, 0x91), // teal + 0xA0..0xB0 => Color::Rgb(0x00, 0xCC, 0xBB), // cyan + 0xB0..0xC0 => Color::Rgb(0x00, 0xC7, 0xDE), // light blue + 0xC0..0xD0 => Color::Rgb(0x00, 0xBE, 0xFF), // blue + 0xD0..0xE0 => Color::Rgb(0x6C, 0xAF, 0xFF), // blurple + 0xE0..0xF0 => Color::Rgb(0xB2, 0x98, 0xFF), // purple + 0xF0..0xFF => Color::Rgb(0xFF, 0x4D, 0xFF), // pink + 0xFF => Color::White + } +} + +const fn content_for_byte(byte: u8) -> Cow<'static, str> { + Cow::Borrowed(hex_for_byte(byte)) +} + +const fn hex_for_byte(byte: u8) -> &'static str { + const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff"]; + + LOOK_UP_TABLE[byte as usize] +} diff --git a/src/buffer/widget/status_line.rs b/src/buffer/widget/status_line.rs new file mode 100644 index 0000000..5c9d1ce --- /dev/null +++ b/src/buffer/widget/status_line.rs @@ -0,0 +1,54 @@ +use crate::{buffer::{Buffer, Mode}, custom_greys::CustomGreys}; +use ratatui::{style::{Color, Stylize}, text::{Line, Span, Text}}; + +impl Buffer { + pub fn render_status_line(&self) -> Text<'_> { + Text::from( + Line::from_iter([ + self.render_mode(), + " ".into(), + self.render_file_name(), + self.modified_indicator(), + " ".into(), + self.alert_message.clone() + ]) + ) + .bg(Color::ui_grey()) + } + + fn render_mode(&self) -> Span<'static> { + Span::from(self.mode.label()) + .black() + .bg(self.mode.color()) + } + + fn render_file_name(&self) -> Span<'_> { + Span::from(&self.file_name) + } + + fn modified_indicator(&self) -> Span<'static> { + if self.has_unsaved_changes() { + " [+]".into() + } else { + "".into() + } + } +} + +impl Mode { + pub const fn label(self) -> &'static str { + match self { + Self::Normal => " NORMAL ", + Self::Select => " SELECT ", + Self::Insert => " INSERT ", + } + } + + pub const fn color(self) -> Color { + match self { + Self::Normal => Color::Blue, + Self::Select => Color::Yellow, + Self::Insert => Color::Green, + } + } +} diff --git a/src/config.rs b/src/config.rs index 4924cf2..0889150 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,10 @@ use std::{collections::{HashMap, hash_map::Entry}, env::{self, home_dir}, fmt::{self, Formatter}, fs::read_to_string, io, path::PathBuf}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use crate::{action::{Action, AppAction, BufferAction, CursorAction}, buffer::{Mode, PartialAction}}; +use crate::{action::Action, buffer::{Mode, PartialAction}}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de::{Error, MapAccess, Unexpected, Visitor}, ser::SerializeMap}; +mod default; + #[derive(Serialize, Deserialize)] #[serde(transparent)] pub struct Config( @@ -46,8 +48,11 @@ impl Config { home_dir().map(|home| home.join("AppData").join("Roaming")) } - pub fn init() -> Result { - let path = Self::path().ok_or(ConfigInitError::NoConfigPath)?; + pub fn init(override_path: Option) -> Result { + let path = override_path + .or_else(Self::path) + .ok_or(ConfigInitError::NoConfigPath)?; + let raw_config = read_to_string(path)?; Ok(toml::from_str(&raw_config)?) @@ -291,268 +296,3 @@ impl From for Keypress { } } } - -impl Default for Config { - #[allow(clippy::too_many_lines)] - fn default() -> Self { - use AppAction::*; - use BufferAction::*; - use CursorAction::*; - - [ - (Mode::Normal, [ - (None, [ - ("q".try_into().unwrap(), QuitIfSaved.into()), - ("Q".try_into().unwrap(), Quit.into()), - - ("v".try_into().unwrap(), SelectMode.into()), - - ("g".try_into().unwrap(), Goto.into()), - ("z".try_into().unwrap(), View.into()), - ("r".try_into().unwrap(), Replace.into()), - (" ".try_into().unwrap(), Space.into()), - ("*".try_into().unwrap(), Repeat.into()), - ("t".try_into().unwrap(), To.into()), - - ("i".try_into().unwrap(), MoveByteUp.into()), - ("k".try_into().unwrap(), MoveByteDown.into()), - ("j".try_into().unwrap(), MoveByteLeft.into()), - ("l".try_into().unwrap(), MoveByteRight.into()), - - ("up".try_into().unwrap(), MoveByteUp.into()), - ("down".try_into().unwrap(), MoveByteDown.into()), - ("left".try_into().unwrap(), MoveByteLeft.into()), - ("right".try_into().unwrap(), MoveByteRight.into()), - - ("G".try_into().unwrap(), GotoFileEnd.into()), - - ("C-e".try_into().unwrap(), ScrollDown.into()), - ("C-y".try_into().unwrap(), ScrollUp.into()), - - ("C-d".try_into().unwrap(), PageCursorHalfDown.into()), - ("C-u".try_into().unwrap(), PageCursorHalfUp.into()), - - ("C-f".try_into().unwrap(), PageDown.into()), - ("C-b".try_into().unwrap(), PageUp.into()), - - ("w".try_into().unwrap(), MoveNextWordStart.into()), - ("e".try_into().unwrap(), MoveNextWordEnd.into()), - ("b".try_into().unwrap(), MovePreviousWordStart.into()), - - (";".try_into().unwrap(), CollapseSelection.into()), - ("A-;".try_into().unwrap(), FlipSelections.into()), - - ("x".try_into().unwrap(), ExtendLineBelow.into()), - ("X".try_into().unwrap(), ExtendLineAbove.into()), - - ("d".try_into().unwrap(), Delete.into()), - - ("u".try_into().unwrap(), Undo.into()), - ("U".try_into().unwrap(), Redo.into()), - - ("C-j".try_into().unwrap(), PreviousBuffer.into()), - ("C-l".try_into().unwrap(), NextBuffer.into()), - - ("C".try_into().unwrap(), CopySelectionOnNextLine.into()), - - ("(".try_into().unwrap(), RotateSelectionsBackward.into()), - (")".try_into().unwrap(), RotateSelectionsForward.into()), - - (",".try_into().unwrap(), KeepPrimarySelection.into()), - ("A-,".try_into().unwrap(), RemovePrimarySelection.into()), - - ("1".try_into().unwrap(), SplitSelectionsInto1s.into()), - ("2".try_into().unwrap(), SplitSelectionsInto2s.into()), - ("3".try_into().unwrap(), SplitSelectionsInto3s.into()), - ("4".try_into().unwrap(), SplitSelectionsInto4s.into()), - ("5".try_into().unwrap(), SplitSelectionsInto5s.into()), - ("6".try_into().unwrap(), SplitSelectionsInto6s.into()), - ("7".try_into().unwrap(), SplitSelectionsInto7s.into()), - ("8".try_into().unwrap(), SplitSelectionsInto8s.into()), - ("9".try_into().unwrap(), SplitSelectionsInto9s.into()), - - ("J".try_into().unwrap(), JumpToSelectedOffsetRelativeToMark.into()), - ("A-J".try_into().unwrap(), JumpToSelectedOffset.into()), - - ("m".try_into().unwrap(), ToggleMark.into()), - - ("y".try_into().unwrap(), Yank.into()), - - ("C- ".try_into().unwrap(), InspectSelection.into()), - ("A- ".try_into().unwrap(), InspectSelectionColor.into()), - ].into()), - (Some(PartialAction::Goto), [ - ("j".try_into().unwrap(), GotoLineStart.into()), - ("l".try_into().unwrap(), GotoLineEnd.into()), - - ("g".try_into().unwrap(), GotoFileStart.into()), - ].into()), - (Some(PartialAction::View), [ - ("z".try_into().unwrap(), AlignViewCenter.into()), - ("b".try_into().unwrap(), AlignViewBottom.into()), - ("t".try_into().unwrap(), AlignViewTop.into()), - ].into()), - (Some(PartialAction::Space), [ - ("w".try_into().unwrap(), Save.into()), - ].into()), - (Some(PartialAction::Repeat), [ - ("i".try_into().unwrap(), MoveByteUp.into()), - ("k".try_into().unwrap(), MoveByteDown.into()), - ("j".try_into().unwrap(), MoveByteLeft.into()), - ("l".try_into().unwrap(), MoveByteRight.into()), - - ("up".try_into().unwrap(), MoveByteUp.into()), - ("down".try_into().unwrap(), MoveByteDown.into()), - ("left".try_into().unwrap(), MoveByteLeft.into()), - ("right".try_into().unwrap(), MoveByteRight.into()), - - ("C-e".try_into().unwrap(), ScrollDown.into()), - ("C-y".try_into().unwrap(), ScrollUp.into()), - - ("C-d".try_into().unwrap(), PageCursorHalfDown.into()), - ("C-u".try_into().unwrap(), PageCursorHalfUp.into()), - - ("C-f".try_into().unwrap(), PageDown.into()), - ("C-b".try_into().unwrap(), PageUp.into()), - - ("w".try_into().unwrap(), MoveNextWordStart.into()), - ("e".try_into().unwrap(), MoveNextWordEnd.into()), - ("b".try_into().unwrap(), MovePreviousWordStart.into()), - - ("x".try_into().unwrap(), ExtendLineBelow.into()), - ("X".try_into().unwrap(), ExtendLineAbove.into()), - - ("d".try_into().unwrap(), Delete.into()), - - ("C".try_into().unwrap(), CopySelectionOnNextLine.into()), - ].into()), - (Some(PartialAction::To), [ - ("m".try_into().unwrap(), ExtendToMark.into()), - ("0".try_into().unwrap(), ExtendToNull.into()), - ("f".try_into().unwrap(), ExtendToFF.into()), - ].into()), - ].into()), - (Mode::Select, [ - (None, [ - ("q".try_into().unwrap(), QuitIfSaved.into()), - ("Q".try_into().unwrap(), Quit.into()), - - ("v".try_into().unwrap(), NormalMode.into()), - - ("g".try_into().unwrap(), Goto.into()), - ("z".try_into().unwrap(), View.into()), - ("r".try_into().unwrap(), Replace.into()), - (" ".try_into().unwrap(), Space.into()), - ("*".try_into().unwrap(), Repeat.into()), - ("t".try_into().unwrap(), To.into()), - - ("i".try_into().unwrap(), ExtendByteUp.into()), - ("k".try_into().unwrap(), ExtendByteDown.into()), - ("j".try_into().unwrap(), ExtendByteLeft.into()), - ("l".try_into().unwrap(), ExtendByteRight.into()), - - ("up".try_into().unwrap(), ExtendByteUp.into()), - ("down".try_into().unwrap(), ExtendByteDown.into()), - ("left".try_into().unwrap(), ExtendByteLeft.into()), - ("right".try_into().unwrap(), ExtendByteRight.into()), - - ("C-e".try_into().unwrap(), ScrollDown.into()), - ("C-y".try_into().unwrap(), ScrollUp.into()), - - ("C-d".try_into().unwrap(), PageCursorHalfDown.into()), - ("C-u".try_into().unwrap(), PageCursorHalfUp.into()), - - ("C-f".try_into().unwrap(), PageDown.into()), - ("C-b".try_into().unwrap(), PageUp.into()), - - ("w".try_into().unwrap(), ExtendNextWordStart.into()), - ("e".try_into().unwrap(), ExtendNextWordEnd.into()), - ("b".try_into().unwrap(), ExtendPreviousWordStart.into()), - - (";".try_into().unwrap(), CollapseSelection.into()), - ("A-;".try_into().unwrap(), FlipSelections.into()), - - ("x".try_into().unwrap(), ExtendLineBelow.into()), - ("X".try_into().unwrap(), ExtendLineAbove.into()), - - ("d".try_into().unwrap(), Delete.into()), - - ("u".try_into().unwrap(), Undo.into()), - ("U".try_into().unwrap(), Redo.into()), - - ("C".try_into().unwrap(), CopySelectionOnNextLine.into()), - - ("(".try_into().unwrap(), RotateSelectionsBackward.into()), - (")".try_into().unwrap(), RotateSelectionsForward.into()), - - (",".try_into().unwrap(), KeepPrimarySelection.into()), - ("A-,".try_into().unwrap(), RemovePrimarySelection.into()), - - ("1".try_into().unwrap(), SplitSelectionsInto1s.into()), - ("2".try_into().unwrap(), SplitSelectionsInto2s.into()), - ("3".try_into().unwrap(), SplitSelectionsInto3s.into()), - ("4".try_into().unwrap(), SplitSelectionsInto4s.into()), - ("5".try_into().unwrap(), SplitSelectionsInto5s.into()), - ("6".try_into().unwrap(), SplitSelectionsInto6s.into()), - ("7".try_into().unwrap(), SplitSelectionsInto7s.into()), - ("8".try_into().unwrap(), SplitSelectionsInto8s.into()), - ("9".try_into().unwrap(), SplitSelectionsInto9s.into()), - - ("J".try_into().unwrap(), JumpToSelectedOffsetRelativeToMark.into()), - ("A-J".try_into().unwrap(), JumpToSelectedOffset.into()), - - ("m".try_into().unwrap(), ToggleMark.into()), - - ("y".try_into().unwrap(), Yank.into()), - - ("C- ".try_into().unwrap(), InspectSelection.into()), - ("A- ".try_into().unwrap(), InspectSelectionColor.into()), - ].into()), - (Some(PartialAction::View), [ - ("z".try_into().unwrap(), AlignViewCenter.into()), - ("b".try_into().unwrap(), AlignViewBottom.into()), - ("t".try_into().unwrap(), AlignViewTop.into()), - ].into()), - (Some(PartialAction::Space), [ - ("w".try_into().unwrap(), Save.into()), - ].into()), - (Some(PartialAction::Repeat), [ - ("i".try_into().unwrap(), ExtendByteUp.into()), - ("k".try_into().unwrap(), ExtendByteDown.into()), - ("j".try_into().unwrap(), ExtendByteLeft.into()), - ("l".try_into().unwrap(), ExtendByteRight.into()), - - ("up".try_into().unwrap(), ExtendByteUp.into()), - ("down".try_into().unwrap(), ExtendByteDown.into()), - ("left".try_into().unwrap(), ExtendByteLeft.into()), - ("right".try_into().unwrap(), ExtendByteRight.into()), - - ("C-e".try_into().unwrap(), ScrollDown.into()), - ("C-y".try_into().unwrap(), ScrollUp.into()), - - ("C-d".try_into().unwrap(), PageCursorHalfDown.into()), - ("C-u".try_into().unwrap(), PageCursorHalfUp.into()), - - ("C-f".try_into().unwrap(), PageDown.into()), - ("C-b".try_into().unwrap(), PageUp.into()), - - ("w".try_into().unwrap(), ExtendNextWordStart.into()), - ("e".try_into().unwrap(), ExtendNextWordEnd.into()), - ("b".try_into().unwrap(), ExtendPreviousWordStart.into()), - - ("x".try_into().unwrap(), ExtendLineBelow.into()), - ("X".try_into().unwrap(), ExtendLineAbove.into()), - - ("d".try_into().unwrap(), Delete.into()), - - ("C".try_into().unwrap(), CopySelectionOnNextLine.into()), - ].into()), - (Some(PartialAction::To), [ - ("m".try_into().unwrap(), ExtendToMark.into()), - ("0".try_into().unwrap(), ExtendToNull.into()), - ("f".try_into().unwrap(), ExtendToFF.into()), - ].into()), - ].into()) - ].into() - } -} diff --git a/src/config/default.rs b/src/config/default.rs new file mode 100644 index 0000000..7e7f7df --- /dev/null +++ b/src/config/default.rs @@ -0,0 +1,266 @@ +use crate::{action::{AppAction, BufferAction, CursorAction}, buffer::{Mode, PartialAction}, config::Config}; + +impl Default for Config { + #[allow(clippy::too_many_lines)] + fn default() -> Self { + use AppAction::*; + use BufferAction::*; + use CursorAction::*; + + [ + (Mode::Normal, [ + (None, [ + ("q".try_into().unwrap(), QuitIfSaved.into()), + ("Q".try_into().unwrap(), Quit.into()), + + ("v".try_into().unwrap(), SelectMode.into()), + + ("g".try_into().unwrap(), Goto.into()), + ("z".try_into().unwrap(), View.into()), + ("r".try_into().unwrap(), Replace.into()), + (" ".try_into().unwrap(), Space.into()), + ("*".try_into().unwrap(), Repeat.into()), + ("t".try_into().unwrap(), To.into()), + + ("i".try_into().unwrap(), MoveByteUp.into()), + ("k".try_into().unwrap(), MoveByteDown.into()), + ("j".try_into().unwrap(), MoveByteLeft.into()), + ("l".try_into().unwrap(), MoveByteRight.into()), + + ("up".try_into().unwrap(), MoveByteUp.into()), + ("down".try_into().unwrap(), MoveByteDown.into()), + ("left".try_into().unwrap(), MoveByteLeft.into()), + ("right".try_into().unwrap(), MoveByteRight.into()), + + ("G".try_into().unwrap(), GotoFileEnd.into()), + + ("C-e".try_into().unwrap(), ScrollDown.into()), + ("C-y".try_into().unwrap(), ScrollUp.into()), + + ("C-d".try_into().unwrap(), PageCursorHalfDown.into()), + ("C-u".try_into().unwrap(), PageCursorHalfUp.into()), + + ("C-f".try_into().unwrap(), PageDown.into()), + ("C-b".try_into().unwrap(), PageUp.into()), + + ("w".try_into().unwrap(), MoveNextWordStart.into()), + ("e".try_into().unwrap(), MoveNextWordEnd.into()), + ("b".try_into().unwrap(), MovePreviousWordStart.into()), + + (";".try_into().unwrap(), CollapseSelection.into()), + ("A-;".try_into().unwrap(), FlipSelections.into()), + + ("x".try_into().unwrap(), ExtendLineBelow.into()), + ("X".try_into().unwrap(), ExtendLineAbove.into()), + + ("d".try_into().unwrap(), Delete.into()), + + ("u".try_into().unwrap(), Undo.into()), + ("U".try_into().unwrap(), Redo.into()), + + ("C-j".try_into().unwrap(), PreviousBuffer.into()), + ("C-l".try_into().unwrap(), NextBuffer.into()), + + ("C".try_into().unwrap(), CopySelectionOnNextLine.into()), + + ("(".try_into().unwrap(), RotateSelectionsBackward.into()), + (")".try_into().unwrap(), RotateSelectionsForward.into()), + + (",".try_into().unwrap(), KeepPrimarySelection.into()), + ("A-,".try_into().unwrap(), RemovePrimarySelection.into()), + + ("1".try_into().unwrap(), SplitSelectionsInto1s.into()), + ("2".try_into().unwrap(), SplitSelectionsInto2s.into()), + ("3".try_into().unwrap(), SplitSelectionsInto3s.into()), + ("4".try_into().unwrap(), SplitSelectionsInto4s.into()), + ("5".try_into().unwrap(), SplitSelectionsInto5s.into()), + ("6".try_into().unwrap(), SplitSelectionsInto6s.into()), + ("7".try_into().unwrap(), SplitSelectionsInto7s.into()), + ("8".try_into().unwrap(), SplitSelectionsInto8s.into()), + ("9".try_into().unwrap(), SplitSelectionsInto9s.into()), + + ("J".try_into().unwrap(), JumpToSelectedOffsetRelativeToMark.into()), + ("A-J".try_into().unwrap(), JumpToSelectedOffset.into()), + + ("m".try_into().unwrap(), ToggleMark.into()), + + ("y".try_into().unwrap(), Yank.into()), + + ("C- ".try_into().unwrap(), InspectSelection.into()), + ("A- ".try_into().unwrap(), InspectSelectionColor.into()), + ].into()), + (Some(PartialAction::Goto), [ + ("j".try_into().unwrap(), GotoLineStart.into()), + ("l".try_into().unwrap(), GotoLineEnd.into()), + + ("g".try_into().unwrap(), GotoFileStart.into()), + ].into()), + (Some(PartialAction::View), [ + ("z".try_into().unwrap(), AlignViewCenter.into()), + ("b".try_into().unwrap(), AlignViewBottom.into()), + ("t".try_into().unwrap(), AlignViewTop.into()), + ].into()), + (Some(PartialAction::Space), [ + ("w".try_into().unwrap(), Save.into()), + ].into()), + (Some(PartialAction::Repeat), [ + ("i".try_into().unwrap(), MoveByteUp.into()), + ("k".try_into().unwrap(), MoveByteDown.into()), + ("j".try_into().unwrap(), MoveByteLeft.into()), + ("l".try_into().unwrap(), MoveByteRight.into()), + + ("up".try_into().unwrap(), MoveByteUp.into()), + ("down".try_into().unwrap(), MoveByteDown.into()), + ("left".try_into().unwrap(), MoveByteLeft.into()), + ("right".try_into().unwrap(), MoveByteRight.into()), + + ("C-e".try_into().unwrap(), ScrollDown.into()), + ("C-y".try_into().unwrap(), ScrollUp.into()), + + ("C-d".try_into().unwrap(), PageCursorHalfDown.into()), + ("C-u".try_into().unwrap(), PageCursorHalfUp.into()), + + ("C-f".try_into().unwrap(), PageDown.into()), + ("C-b".try_into().unwrap(), PageUp.into()), + + ("w".try_into().unwrap(), MoveNextWordStart.into()), + ("e".try_into().unwrap(), MoveNextWordEnd.into()), + ("b".try_into().unwrap(), MovePreviousWordStart.into()), + + ("x".try_into().unwrap(), ExtendLineBelow.into()), + ("X".try_into().unwrap(), ExtendLineAbove.into()), + + ("d".try_into().unwrap(), Delete.into()), + + ("C".try_into().unwrap(), CopySelectionOnNextLine.into()), + ].into()), + (Some(PartialAction::To), [ + ("m".try_into().unwrap(), ExtendToMark.into()), + ("0".try_into().unwrap(), ExtendToNull.into()), + ("f".try_into().unwrap(), ExtendToFF.into()), + ].into()), + ].into()), + (Mode::Select, [ + (None, [ + ("q".try_into().unwrap(), QuitIfSaved.into()), + ("Q".try_into().unwrap(), Quit.into()), + + ("v".try_into().unwrap(), NormalMode.into()), + + ("g".try_into().unwrap(), Goto.into()), + ("z".try_into().unwrap(), View.into()), + ("r".try_into().unwrap(), Replace.into()), + (" ".try_into().unwrap(), Space.into()), + ("*".try_into().unwrap(), Repeat.into()), + ("t".try_into().unwrap(), To.into()), + + ("i".try_into().unwrap(), ExtendByteUp.into()), + ("k".try_into().unwrap(), ExtendByteDown.into()), + ("j".try_into().unwrap(), ExtendByteLeft.into()), + ("l".try_into().unwrap(), ExtendByteRight.into()), + + ("up".try_into().unwrap(), ExtendByteUp.into()), + ("down".try_into().unwrap(), ExtendByteDown.into()), + ("left".try_into().unwrap(), ExtendByteLeft.into()), + ("right".try_into().unwrap(), ExtendByteRight.into()), + + ("C-e".try_into().unwrap(), ScrollDown.into()), + ("C-y".try_into().unwrap(), ScrollUp.into()), + + ("C-d".try_into().unwrap(), PageCursorHalfDown.into()), + ("C-u".try_into().unwrap(), PageCursorHalfUp.into()), + + ("C-f".try_into().unwrap(), PageDown.into()), + ("C-b".try_into().unwrap(), PageUp.into()), + + ("w".try_into().unwrap(), ExtendNextWordStart.into()), + ("e".try_into().unwrap(), ExtendNextWordEnd.into()), + ("b".try_into().unwrap(), ExtendPreviousWordStart.into()), + + (";".try_into().unwrap(), CollapseSelection.into()), + ("A-;".try_into().unwrap(), FlipSelections.into()), + + ("x".try_into().unwrap(), ExtendLineBelow.into()), + ("X".try_into().unwrap(), ExtendLineAbove.into()), + + ("d".try_into().unwrap(), Delete.into()), + + ("u".try_into().unwrap(), Undo.into()), + ("U".try_into().unwrap(), Redo.into()), + + ("C".try_into().unwrap(), CopySelectionOnNextLine.into()), + + ("(".try_into().unwrap(), RotateSelectionsBackward.into()), + (")".try_into().unwrap(), RotateSelectionsForward.into()), + + (",".try_into().unwrap(), KeepPrimarySelection.into()), + ("A-,".try_into().unwrap(), RemovePrimarySelection.into()), + + ("1".try_into().unwrap(), SplitSelectionsInto1s.into()), + ("2".try_into().unwrap(), SplitSelectionsInto2s.into()), + ("3".try_into().unwrap(), SplitSelectionsInto3s.into()), + ("4".try_into().unwrap(), SplitSelectionsInto4s.into()), + ("5".try_into().unwrap(), SplitSelectionsInto5s.into()), + ("6".try_into().unwrap(), SplitSelectionsInto6s.into()), + ("7".try_into().unwrap(), SplitSelectionsInto7s.into()), + ("8".try_into().unwrap(), SplitSelectionsInto8s.into()), + ("9".try_into().unwrap(), SplitSelectionsInto9s.into()), + + ("J".try_into().unwrap(), JumpToSelectedOffsetRelativeToMark.into()), + ("A-J".try_into().unwrap(), JumpToSelectedOffset.into()), + + ("m".try_into().unwrap(), ToggleMark.into()), + + ("y".try_into().unwrap(), Yank.into()), + + ("C- ".try_into().unwrap(), InspectSelection.into()), + ("A- ".try_into().unwrap(), InspectSelectionColor.into()), + ].into()), + (Some(PartialAction::View), [ + ("z".try_into().unwrap(), AlignViewCenter.into()), + ("b".try_into().unwrap(), AlignViewBottom.into()), + ("t".try_into().unwrap(), AlignViewTop.into()), + ].into()), + (Some(PartialAction::Space), [ + ("w".try_into().unwrap(), Save.into()), + ].into()), + (Some(PartialAction::Repeat), [ + ("i".try_into().unwrap(), ExtendByteUp.into()), + ("k".try_into().unwrap(), ExtendByteDown.into()), + ("j".try_into().unwrap(), ExtendByteLeft.into()), + ("l".try_into().unwrap(), ExtendByteRight.into()), + + ("up".try_into().unwrap(), ExtendByteUp.into()), + ("down".try_into().unwrap(), ExtendByteDown.into()), + ("left".try_into().unwrap(), ExtendByteLeft.into()), + ("right".try_into().unwrap(), ExtendByteRight.into()), + + ("C-e".try_into().unwrap(), ScrollDown.into()), + ("C-y".try_into().unwrap(), ScrollUp.into()), + + ("C-d".try_into().unwrap(), PageCursorHalfDown.into()), + ("C-u".try_into().unwrap(), PageCursorHalfUp.into()), + + ("C-f".try_into().unwrap(), PageDown.into()), + ("C-b".try_into().unwrap(), PageUp.into()), + + ("w".try_into().unwrap(), ExtendNextWordStart.into()), + ("e".try_into().unwrap(), ExtendNextWordEnd.into()), + ("b".try_into().unwrap(), ExtendPreviousWordStart.into()), + + ("x".try_into().unwrap(), ExtendLineBelow.into()), + ("X".try_into().unwrap(), ExtendLineAbove.into()), + + ("d".try_into().unwrap(), Delete.into()), + + ("C".try_into().unwrap(), CopySelectionOnNextLine.into()), + ].into()), + (Some(PartialAction::To), [ + ("m".try_into().unwrap(), ExtendToMark.into()), + ("0".try_into().unwrap(), ExtendToNull.into()), + ("f".try_into().unwrap(), ExtendToFF.into()), + ].into()), + ].into()) + ].into() + } +} diff --git a/src/cursor/actions.rs b/src/cursor/actions.rs new file mode 100644 index 0000000..bca5e47 --- /dev/null +++ b/src/cursor/actions.rs @@ -0,0 +1,440 @@ +impl Cursor { + pub fn execute( + &mut self, + action: CursorAction, + max_contents_index: usize + ) { + match action { + CursorAction::MoveByteUp => self.move_byte_up(), + CursorAction::MoveByteDown => self.move_byte_down(max_contents_index), + CursorAction::MoveByteLeft => self.move_byte_left(), + CursorAction::MoveByteRight => self.move_byte_right(max_contents_index), + + CursorAction::ExtendByteUp => self.extend_byte_up(), + CursorAction::ExtendByteDown => self.extend_byte_down(max_contents_index), + CursorAction::ExtendByteLeft => self.extend_byte_left(), + CursorAction::ExtendByteRight => self.extend_byte_right(max_contents_index), + + CursorAction::GotoLineStart => self.goto_line_start(), + CursorAction::GotoLineEnd => self.goto_line_end(max_contents_index), + CursorAction::GotoFileStart => self.goto_file_start(), + CursorAction::GotoFileEnd => self.goto_file_end(max_contents_index), + + CursorAction::MoveNextWordStart => self.move_next_word_start(max_contents_index), + CursorAction::MoveNextWordEnd => self.move_next_word_end(max_contents_index), + CursorAction::MovePreviousWordStart => self.move_previous_word_start(), + + CursorAction::ExtendNextWordStart => self.extend_next_word_start(max_contents_index), + CursorAction::ExtendNextWordEnd => self.extend_next_word_end(max_contents_index), + CursorAction::ExtendPreviousWordStart => self.extend_previous_word_start(), + + CursorAction::ExtendLineBelow => self.extend_line_below(max_contents_index), + CursorAction::ExtendLineAbove => self.extend_line_above(max_contents_index), + } + } + + pub const fn move_byte_up(&mut self) { + if self.head >= BYTES_PER_LINE { + self.head -= BYTES_PER_LINE; + self.collapse(); + } + } + + pub const fn move_byte_down(&mut self, max: usize) { + if max - self.head >= BYTES_PER_LINE { + self.head += BYTES_PER_LINE; + self.collapse(); + } + } + + pub const fn move_byte_left(&mut self) { + if self.head >= 1 { + self.head -= 1; + self.collapse(); + } + } + + pub const fn move_byte_right(&mut self, max: usize) { + if max - self.head >= 1 { + self.head += 1; + self.collapse(); + } + } + + pub const fn extend_byte_up(&mut self) { + if self.head >= BYTES_PER_LINE { + self.head -= BYTES_PER_LINE; + } + } + + pub const fn extend_byte_down(&mut self, max: usize) { + if max - self.head >= BYTES_PER_LINE { + self.head += BYTES_PER_LINE; + } + } + + pub const fn extend_byte_left(&mut self) { + if self.head >= 1 { + self.head -= 1; + } + } + + pub const fn extend_byte_right(&mut self, max: usize) { + if max - self.head >= 1 { + self.head += 1; + } + } + + pub const fn goto_line_start(&mut self) { + self.head -= self.head % BYTES_PER_LINE; + self.collapse(); + } + + pub fn goto_line_end(&mut self, max: usize) { + self.head = min( + self.head + BYTES_PER_LINE - 1 - (self.head % BYTES_PER_LINE), + max + ); + self.collapse(); + } + + pub const fn goto_file_start(&mut self) { + self.head %= BYTES_PER_LINE; + self.collapse(); + } + + pub const fn goto_file_end(&mut self, max: usize) { + self.head += previous_multiple_of(BYTES_PER_LINE, max + 1 - self.head); + + self.collapse(); + } + + pub fn move_next_word_start(&mut self, max: usize) { + if self.head == max { return; } + + if self.head.is_multiple_of(4) { // at the beginning of a word + self.head = (self.head + 4).min(max); + } else { + self.head = self.head.next_multiple_of(4).min(max); + } + self.collapse(); + } + + pub fn move_next_word_end(&mut self, max: usize) { + if self.head == max { return; } + + self.collapse(); + if self.head % 4 == 3 { // at the end of a word + self.tail = self.head + 1; + self.head = (self.head + 4).min(max); + } else { + self.head = ((self.head + 1).next_multiple_of(4) - 1).min(max); + } + } + + pub const fn move_previous_word_start(&mut self) { + if self.head == 0 { return; } + + self.collapse(); + if self.head.is_multiple_of(4) { // at the beginning of a word + self.tail = self.head - 1; + self.head -= 4; + } else { + self.head -= self.head % 4; + } + } + + pub fn extend_next_word_start(&mut self, max: usize) { + if self.head == max { return; } + + if self.head.is_multiple_of(4) { // at the beginning of a word + self.head = (self.head + 4).min(max); + } else { + self.head = self.head.next_multiple_of(4).min(max); + } + } + + pub fn extend_next_word_end(&mut self, max: usize) { + if self.head == max { return; } + + if self.head % 4 == 3 { // at the end of a word + self.head = (self.head + 4).min(max); + } else { + self.head = ((self.head + 1).next_multiple_of(4) - 1).min(max); + } + } + + pub const fn extend_previous_word_start(&mut self) { + if self.head == 0 { return; } + + if self.head.is_multiple_of(4) { // at the beginning of a word + self.head -= 4; + } else { + self.head -= self.head % 4; + } + } + + pub fn extend_line_below(&mut self, max: usize) { + if self.tail > self.head { + swap(&mut self.head, &mut self.tail); + } + + if self.tail.is_multiple_of(BYTES_PER_LINE) && + self.head % BYTES_PER_LINE == BYTES_PER_LINE - 1 + { + self.head = min(self.head + BYTES_PER_LINE, max); + } else { + self.tail -= self.tail % BYTES_PER_LINE; + self.head = min( + self.head + BYTES_PER_LINE - 1 - (self.head % BYTES_PER_LINE), + max + ); + } + } + + pub fn extend_line_above(&mut self, max: usize) { + if self.head > self.tail { + swap(&mut self.head, &mut self.tail); + } + + if self.head.is_multiple_of(BYTES_PER_LINE) && + (self.tail % BYTES_PER_LINE == BYTES_PER_LINE - 1 || + self.tail == max) + { + self.head = self.head.saturating_sub(BYTES_PER_LINE); + } else { + self.head -= self.head % BYTES_PER_LINE; + self.tail = min( + self.tail + BYTES_PER_LINE - 1 - (self.tail % BYTES_PER_LINE), + max + ); + } + } +} + +const fn previous_multiple_of(multiple: usize, number: usize) -> usize { + if number == 0 { + 0 + } else { + (number - 1) - ((number - 1) % multiple) + } +} + +mod tests { + #[allow(unused_imports)] + use crate::cursor::Cursor; + + #[test] + fn next_word() { + // [a]bcd efgh -> abcd [e]fgh + let mut cursor = Cursor::at(0); + cursor.move_next_word_start(99); + assert_eq!(cursor, Cursor::at(4)); + + // a[b]cd efgh -> abcd [e]fgh + let mut cursor = Cursor::at(1); + cursor.move_next_word_start(99); + assert_eq!(cursor, Cursor::at(4)); + + // ab[c]d efgh -> abcd [e]fgh + let mut cursor = Cursor::at(2); + cursor.move_next_word_start(99); + assert_eq!(cursor, Cursor::at(4)); + + // abc[d] efgh -> abcd [e]fgh + let mut cursor = Cursor::at(3); + cursor.move_next_word_start(99); + assert_eq!(cursor, Cursor::at(4)); + + // [a]bcd -> abc[d] + let mut cursor = Cursor::at(0); + cursor.move_next_word_start(3); + assert_eq!(cursor, Cursor::at(3)); + + // [a]bc -> ab[c] + let mut cursor = Cursor::at(0); + cursor.move_next_word_start(2); + assert_eq!(cursor, Cursor::at(2)); + + // [a]b -> a[b] + let mut cursor = Cursor::at(0); + cursor.move_next_word_start(1); + assert_eq!(cursor, Cursor::at(1)); + + // [a] -> [a] + let mut cursor = Cursor::at(0); + cursor.move_next_word_start(0); + assert_eq!(cursor, Cursor::at(0)); + + // ab[c]d -> abc[d] + let mut cursor = Cursor::at(2); + cursor.move_next_word_start(3); + assert_eq!(cursor, Cursor::at(3)); + + // abc[d] -> abc[d] + let mut cursor = Cursor::at(3); + cursor.move_next_word_start(3); + assert_eq!(cursor, Cursor::at(3)); + + // ab[c[d] -> ab[c[d] + let mut cursor = Cursor { tail: 2, head: 3 }; + cursor.move_next_word_start(3); + assert_eq!(cursor, Cursor { tail: 2, head: 3 }); + } + + #[test] + fn next_end() { + // [a]bcd -> [abcd] + let mut cursor = Cursor::at(0); + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 0, head: 3 }); + + // a[b]cd -> [abcd] + let mut cursor = Cursor::at(1); + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 1, head: 3 }); + + // ab[c]d -> [abcd] + let mut cursor = Cursor::at(2); + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 2, head: 3 }); + + // abc[d] efgh -> abcd [efgh] + let mut cursor = Cursor::at(3); + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 4, head: 7 }); + + // abcd [e]fgh -> abcd [efgh] + let mut cursor = Cursor::at(4); + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 4, head: 7 }); + + // abcd e[f]gh -> abcd e[fgh] + let mut cursor = Cursor::at(5); + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 5, head: 7 }); + + // abcd ef[g]h -> abcd ef[gh] + let mut cursor = Cursor::at(6); + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 6, head: 7 }); + + // abcd efg[h] ijkl -> abcd efgh [ijkl] + let mut cursor = Cursor::at(7); + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 8, head: 11 }); + + // abcd efg[h] -> abcd efg[h] + let mut cursor = Cursor::at(7); + cursor.move_next_word_end(7); + assert_eq!(cursor, Cursor { tail: 7, head: 7 }); + + // abcd e[fgh] -> abcd e[fgh] + let mut cursor = Cursor { tail: 5, head: 7 }; + cursor.move_next_word_end(7); + assert_eq!(cursor, Cursor { tail: 5, head: 7 }); + + // a[b]c -> a[bc] + let mut cursor = Cursor::at(1); + cursor.move_next_word_end(2); + assert_eq!(cursor, Cursor { tail: 1, head: 2 }); + + // a[bc] -> a[bc] + let mut cursor = Cursor { tail: 1, head: 2}; + cursor.move_next_word_end(2); + assert_eq!(cursor, Cursor { tail: 1, head: 2 }); + + // a[b] -> a[b] + let mut cursor = Cursor::at(1); + cursor.move_next_word_end(1); + assert_eq!(cursor, Cursor::at(1)); + + // [a]b -> [ab] + let mut cursor = Cursor::at(0); + cursor.move_next_word_end(1); + assert_eq!(cursor, Cursor { tail: 0, head: 1 }); + + // [ab] -> [ab] + let mut cursor = Cursor { tail: 0, head: 1}; + cursor.move_next_word_end(1); + assert_eq!(cursor, Cursor { tail: 0, head: 1 }); + + // [a] -> [a] + let mut cursor = Cursor::at(0); + cursor.move_next_word_end(0); + assert_eq!(cursor, Cursor::at(0)); + + // [a]bcd] -> [abc[d] + let mut cursor = Cursor { head: 0, tail: 3 }; + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 0, head: 3 }); + + // [a[b]cd -> a[bc[d] + let mut cursor = Cursor { tail: 0, head: 1 }; + cursor.move_next_word_end(99); + assert_eq!(cursor, Cursor { tail: 1, head: 3 }); + + // abc[d] ef -> abcd [ef] + let mut cursor = Cursor::at(3); + cursor.move_next_word_end(5); + assert_eq!(cursor, Cursor { tail: 4, head: 5 }); + } + + #[test] + fn previous_beginning() { + // abcd efgh [i]jkl -> abcd [efgh] ijkl + let mut cursor = Cursor::at(8); + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 4, tail: 7 }); + + // abcd efg[h] -> abcd [efgh] + let mut cursor = Cursor::at(7); + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 4, tail: 7 }); + + // abcd ef[g]h -> abcd [efg]h + let mut cursor = Cursor::at(6); + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 4, tail: 6 }); + + // abcd e[f]gh -> abcd [ef]gh + let mut cursor = Cursor::at(5); + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 4, tail: 5 }); + + // abcd [e]fgh -> [abcd] efgh + let mut cursor = Cursor::at(4); + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 0, tail: 3 }); + + // abc[d] -> [abcd] + let mut cursor = Cursor::at(3); + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 0, tail: 3 }); + + // ab[c]d -> [abc]d + let mut cursor = Cursor::at(2); + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 0, tail: 2 }); + + // a[b]cd -> [ab]cd + let mut cursor = Cursor::at(1); + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 0, tail: 1 }); + + // [a]bcd -> [a]bcd + let mut cursor = Cursor::at(0); + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 0, tail: 0 }); + + // [abc[d] -> [a]bcd] + let mut cursor = Cursor { tail: 0, head: 3 }; + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 0, tail: 3 }); + + // ab[c]d] -> [a]bc]d + let mut cursor = Cursor { head: 2, tail: 3 }; + cursor.move_previous_word_start(); + assert_eq!(cursor, Cursor { head: 0, tail: 2 }); + } +} diff --git a/src/edit_action.rs b/src/edit_action.rs index 4645780..18ed838 100644 --- a/src/edit_action.rs +++ b/src/edit_action.rs @@ -1,5 +1,5 @@ use std::{cmp::min, convert::identity, iter}; -use crate::{app::WindowSize, buffer::Buffer, cursor::Cursor}; +use crate::{window_size::WindowSize, buffer::Buffer, cursor::Cursor}; #[derive(Debug)] pub enum EditAction { diff --git a/src/main.rs b/src/main.rs index 4f0f2c6..b88811d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,15 +6,20 @@ #![feature(hash_set_entry)] #![feature(trim_prefix_suffix)] +use arguments::Arguments; +use clap::Parser; use app::App; use crossterm::{QueueableCommand, event::{DisableMouseCapture, EnableMouseCapture}}; mod app; mod buffer; +mod popup; mod config; mod cursor; mod action; mod edit_action; +mod arguments; +mod window_size; mod cardinality; mod empty_span; @@ -28,14 +33,13 @@ const LINES_OF_PADDING: usize = 5; const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE; // TODO: -// - help (use clap?) -// - clean up files // - update showcase // - write docs // - simonomi.dev/hexapoda? // - config // - schema!! // - uhhhhh? +// - fix scroll clamping // - inspector translations for varint // - search // - ascii and bytes (`/` and `A-/`?) @@ -65,7 +69,13 @@ const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE; // - how to fit??! `-128` longer than `80` fn main() { - let mut app = App::new(); + let arguments = Arguments::parse(); + + let mut app = App::new( + arguments.config, + &arguments.files + ); + let mut terminal = ratatui::init(); crossterm::terminal::enable_raw_mode().unwrap(); terminal.backend_mut().queue(EnableMouseCapture).unwrap(); diff --git a/src/popup.rs b/src/popup.rs new file mode 100644 index 0000000..b19db15 --- /dev/null +++ b/src/popup.rs @@ -0,0 +1,64 @@ +use ratatui::{layout::{Constraint, Rect}, style::{Style, Stylize}, text::Span, widgets::{Block, Borders, Clear, Widget}}; + +#[derive(Clone)] +pub struct Popup { + pub at: usize, + width: u16, + primary: bool, + lines: Vec> +} + +impl Popup { + pub fn new(at: usize, lines: Vec>) -> Self { + Self { + at, + width: lines + .iter() + .map(|line| line.width() as u16) + .max() + .unwrap_or(0), + primary: false, + lines + } + } + + pub const fn area_at(&self, x: u16, y: u16) -> Rect { + Rect { + x, + y, + width: self.width + 2, + height: self.lines.len() as u16 + } + } + + #[allow(clippy::wrong_self_convention)] + pub const fn as_primary(mut self) -> Self { + self.primary = true; + self + } +} + +impl Widget for Popup { + fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) { + Clear.render(area, buf); + + let border_color = if self.primary { + Style::new().white() + } else { + Style::new().gray() + }; + + Block::new() + .on_dark_gray() + .borders(Borders::LEFT | Borders::RIGHT) + .border_style(border_color) + .render(area, buf); + + for (line, area) in self.lines.iter().zip(area.rows()) { + line.render( + area.centered_horizontally(Constraint::Length(line.width() as u16)), + buf + ); + } + } +} diff --git a/src/window_size.rs b/src/window_size.rs new file mode 100644 index 0000000..6f99b97 --- /dev/null +++ b/src/window_size.rs @@ -0,0 +1,17 @@ +use crate::BYTES_PER_LINE; + +#[derive(Clone, Copy)] +pub struct WindowSize { + pub rows: usize, + pub covered_rows: usize, +} + +impl WindowSize { + pub const fn visible_byte_count(&self) -> usize { + self.hex_rows() * BYTES_PER_LINE + } + + pub const fn hex_rows(&self) -> usize { + self.rows - self.covered_rows + } +}