41 Commits

Author SHA1 Message Date
alice pellerin 8f38d07385 only run build.rs when environment variables are non-empty 2026-05-02 00:12:18 -05:00
alice pellerin e437cecb48 fix windows packaging 2026-05-02 00:05:56 -05:00
alice pellerin 2198c9f787 enable publishing on build success 2026-05-02 00:03:28 -05:00
alice pellerin bd67084b87 fix windows CI 2026-05-02 00:01:17 -05:00
alice pellerin ebdf1061cf fix tar.gz packaging 2026-05-01 23:57:13 -05:00
alice pellerin b755f4a603 fix completion/manpage generation 2026-05-01 23:54:47 -05:00
alice pellerin 5bdb44ffba fix CI syntax 2026-05-01 23:47:40 -05:00
alice pellerin d320e286c1 fix version number 2026-05-01 23:44:27 -05:00
alice pellerin f8f6f7e3ef fix packaging, add completions/manpage 2026-05-01 23:43:26 -05:00
alice pellerin 150ff05048 fix version 2026-05-01 16:49:21 -05:00
alice pellerin 4f283ca983 fix cargo binstall pkg urls, add more platforms to CI 2026-05-01 16:46:58 -05:00
alice pellerin d7bdccfddb only let publish run on tags 2026-05-01 16:16:20 -05:00
alice pellerin 22542b6d29 split publish and release actions 2026-05-01 16:09:08 -05:00
alice pellerin 7740461805 add other OSes to actions 2026-05-01 16:03:20 -05:00
alice pellerin 9930adb7b1 fix github actions write permissions 2026-05-01 15:38:40 -05:00
alice pellerin cc56ece472 allow running ci on main branch 2026-05-01 15:35:43 -05:00
alice pellerin 4a911da95a edit release action 2026-05-01 15:28:26 -05:00
alice pellerin 80c8c2ed81 re-enable cast_possible_truncation lint 2026-05-01 01:17:02 -05:00
alice pellerin 35a01b9128 stop using any nightly features (ill miss u const traits) 2026-05-01 00:44:24 -05:00
alice pellerin 7eb8c84c6e drag with mouse to select bytes 2026-04-30 13:23:15 -05:00
alice pellerin e60e63cb16 fix cursor padding for mouse click 2026-04-30 11:54:49 -05:00
alice pellerin 0284279cd5 update showcase 2026-04-29 23:37:21 -05:00
alice pellerin f363dbb919 nu completions and manpage generation 2026-04-29 22:54:40 -05:00
alice pellerin b9db974c17 generate completions 2026-04-29 22:36:04 -05:00
alice pellerin 15132d44af fix scrolling and clamping 2026-04-29 19:51:47 -05:00
alice pellerin 162046d13e make utilities module 2026-04-29 17:44:30 -05:00
alice pellerin 3ad74cc3de fix repeated actions error, add yank message 2026-04-26 01:02:23 -05:00
alice pellerin fd63c6bd57 merge custom config with default instead of replacing 2026-04-26 00:52:02 -05:00
alice pellerin adfb780521 add export config script 2026-04-25 22:17:44 -05:00
alice pellerin 7741bb112e gj/gl/gg/G in select mode 2026-04-25 18:16:17 -05:00
alice pellerin cc0ae065c6 fix till in select mode 2026-04-25 18:09:08 -05:00
alice pellerin cd3ea4d1a6 add binary to inspector for single bytes 2026-04-25 17:51:24 -05:00
alice pellerin e0ce3f3aea add icon to readme 2026-04-13 12:00:01 -05:00
alice pellerin 00bf25b5bd add --show-config-path argument 2026-04-13 11:52:41 -05:00
alice pellerin 3e24d00af4 fix splitting and combining cursors 2026-04-13 02:11:37 -05:00
alice pellerin 901350508d fix extend-to commands not combining cursors 2026-04-13 01:10:00 -05:00
alice pellerin a689949044 clean up files, parse arguments with clap 2026-04-13 00:54:26 -05:00
alice pellerin 44553a5328 add large icon 2026-04-12 20:10:07 -05:00
alice pellerin c6bc3777ad add icon 2026-04-12 18:49:37 -05:00
alice pellerin c65e2bf134 update secondary selection head color 2026-04-11 19:04:26 -05:00
alice pellerin 383cc4ea8e add publish action 2026-04-10 23:05:05 -05:00
41 changed files with 3299 additions and 2445 deletions
+84
View File
@@ -0,0 +1,84 @@
name: release
on:
workflow_dispatch:
push:
tags:
- "v*"
jobs:
release:
runs-on: ${{ matrix.info.runs-on }}
permissions:
contents: write
strategy:
matrix:
info:
- os: "macOS-arm"
runs-on: "macos-latest"
package-extension: "tar.gz"
executable-extension: ""
- os: "macOS-intel"
runs-on: "macos-26-intel"
package-extension: "tar.gz"
executable-extension: ""
- os: "linux-x86_64"
runs-on: "ubuntu-latest"
package-extension: "tar.gz"
executable-extension: ""
- os: "linux-arm"
runs-on: "ubuntu-24.04-arm"
package-extension: "tar.gz"
executable-extension: ""
- os: "Windows-x86_64"
runs-on: "windows-latest"
package-extension: "zip"
executable-extension: ".exe"
- os: "Windows-arm"
runs-on: "windows-11-arm"
package-extension: "zip"
executable-extension: ".exe"
steps:
- name: checkout
uses: actions/checkout@v6
- name: check version
# cut off the v part of the tag to only search for the number
run: grep --quiet "$(echo "${{ github.ref_name }}" | cut -c2-)" Cargo.toml
- name: test
run: cargo test --release
- name: make completion/manpage folders
run: mkdir completions; mkdir manpage
- name: build
run: cargo build --release --locked
env:
HEXAPODA_COMPLETIONS: completions
HEXAPODA_MANPAGE: manpage
- name: package-tar-gz
if: ${{ matrix.info.package-extension == 'tar.gz' }}
run: tar --create --gzip --file "hexapoda-${{ matrix.info.os }}-${{ github.ref_name }}.tar.gz" "completions" "manpage" -C "target/release/" "hexapoda${{ matrix.info.executable-extension }}"
- name: package-zip
if: ${{ matrix.info.package-extension == 'zip' }}
run: tar --create --auto-compress --file "hexapoda-${{ matrix.info.os }}-${{ github.ref_name }}.zip" "completions" "manpage" -C "target/release/" "hexapoda${{ matrix.info.executable-extension }}"
- name: release
uses: softprops/action-gh-release@v2
with:
draft: true
name: "${{ github.ref_name }}"
files: "hexapoda-${{ matrix.info.os }}-${{ github.ref_name }}.${{ matrix.info.package-extension }}"
publish:
runs-on: ubuntu-latest
needs: release
permissions:
id-token: write # Required for OIDC token exchange
steps:
- uses: actions/checkout@v6
- name: check version
# cut off the v part of the tag to only search for the number
# include the " = " to not match on main, only v* tags
run: grep -q " = \"$(echo "${{ github.ref_name }}" | cut -c2-)\"" Cargo.toml
- uses: rust-lang/crates-io-auth-action@v1
id: auth
- run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
Generated
+254 -91
View File
@@ -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,81 @@ 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_complete"
version = "4.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3"
dependencies = [
"clap",
]
[[package]]
name = "clap_complete_nushell"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb9e9715d29a754b468591be588f6b926f5b0a1eb6a8b62acabeb66ff84d897"
dependencies = [
"clap",
"clap_complete",
]
[[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 = "clap_mangen"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07"
dependencies = [
"clap",
"roff",
]
[[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 +257,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 +279,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 +323,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 +333,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 +372,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 +548,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"
@@ -437,8 +568,12 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hexapoda"
version = "0.1.0"
version = "0.2.2"
dependencies = [
"clap",
"clap_complete",
"clap_complete_nushell",
"clap_mangen",
"crossterm",
"itertools",
"ratatui",
@@ -460,12 +595,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 +616,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 +644,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 +689,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 +725,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 +777,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 +793,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 +812,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 +824,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.117",
]
[[package]]
@@ -710,6 +851,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 +919,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.117",
]
[[package]]
@@ -825,7 +972,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.117",
]
[[package]]
@@ -856,23 +1003,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 +1071,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 +1123,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 +1142,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 +1175,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustix"
version = "1.1.2"
name = "roff"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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 +1210,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 +1222,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 +1253,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.117",
]
[[package]]
@@ -1150,10 +1312,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 +1362,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.117",
]
[[package]]
@@ -1215,9 +1378,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 +1416,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
dependencies = [
"anyhow",
"base64",
"bitflags 2.10.0",
"bitflags 2.11.0",
"fancy-regex",
"filedescriptor",
"finl_unicode",
@@ -1313,7 +1476,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.117",
]
[[package]]
@@ -1324,7 +1487,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.117",
]
[[package]]
@@ -1401,15 +1564,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 +1605,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 +1656,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 +1669,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 +1679,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 +1727,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 +1878,7 @@ dependencies = [
"heck",
"indexmap",
"prettyplease",
"syn 2.0.110",
"syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -1731,7 +1894,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -1743,7 +1906,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",
+34 -1
View File
@@ -1,16 +1,49 @@
[package]
name = "hexapoda"
version = "0.1.0"
# if run manually, CI will check for the string "ain" (lol), so here you go :)
version = "0.2.2"
description = "a colorful modal hex editor"
repository = "https://github.com/simonomi/hexapoda"
keywords = ["cli", "tui", "hex", "tool", "editor"]
categories = ["command-line-utilities"]
license = "GPL-3.0-only"
edition = "2024"
build = "build.rs"
[dependencies]
clap = { version = "4.6.0", features = ["derive"] }
crossterm = { version = "0.29.0", features = ["serde"] }
itertools = "0.14.0"
ratatui = "0.30.0"
serde = { version = "1.0.228", features = ["derive"] }
toml = "1.1.2"
[build-dependencies]
clap = { version = "4.6.0", features = ["derive"] }
clap_complete = "4.6.3"
clap_complete_nushell = "4.6.0"
clap_mangen = "0.3.0"
[package.metadata.binstall.overrides.'cfg(all(target_os = "macos", target_arch = "aarch64" ))']
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-macOS-arm-v{ version }{ archive-suffix }"
pkg-fmt = "tgz"
[package.metadata.binstall.overrides.'cfg(all(target_os = "macos", target_arch = "x86_64" ))']
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-macOS-intel-v{ version }{ archive-suffix }"
pkg-fmt = "tgz"
[package.metadata.binstall.overrides.'cfg(all(target_os = "linux", target_arch = "x86_64" ))']
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-x86_64-v{ version }{ archive-suffix }"
pkg-fmt = "tgz"
[package.metadata.binstall.overrides.'cfg(all(target_os = "linux", target_arch = "aarch64" ))']
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-arm-v{ version }{ archive-suffix }"
pkg-fmt = "tgz"
[package.metadata.binstall.overrides.'cfg(all(target_os = "windows", target_arch = "x86_64" ))']
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-Windows-x86_64-v{ version }{ archive-suffix }"
pkg-fmt = "zip"
[package.metadata.binstall.overrides.'cfg(all(target_os = "windows", target_arch = "aarch64" ))']
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-Windows-arm-v{ version }{ archive-suffix }"
pkg-fmt = "zip"
+3 -3
View File
@@ -1,10 +1,10 @@
# hexapoda
# <img height=40 align=top src="https://github.com/simonomi/hexapoda/blob/main/icon/bug%20colored%20large.png?raw=true"> hexapoda
a colorful modal hex editor
(the name comes from [the subphylum](https://en.wikipedia.org/wiki/Hexapoda))
[![asciicast](https://asciinema.org/a/fsVwqdn846Ar5CQZ.svg)](https://asciinema.org/a/fsVwqdn846Ar5CQZ)
[![asciicast](https://asciinema.org/a/P2tCRvr4cwvmsmPl.svg)](https://asciinema.org/a/P2tCRvr4cwvmsmPl)
## status
@@ -19,7 +19,7 @@ currently, hexapoda is very unpolished, and missing some major features. if you'
- split selection(s) into #-byte chunks
- undo/redo
- inspect the current selection(s)
- signed, unsigned, fixed-point, UTF-8, color
- signed, unsigned, binary, fixed-point, UTF-8, color
- mark notable offsets
- jump to selected offset
+33
View File
@@ -0,0 +1,33 @@
use clap::{CommandFactory, ValueEnum};
use clap_complete::{generate_to, Shell};
use clap_complete_nushell::Nushell;
use std::env;
use std::io::Error;
include!("src/arguments.rs");
fn main() -> Result<(), Error> {
let completions_folder = match env::var_os("HEXAPODA_COMPLETIONS") {
Some(folder) if !folder.is_empty() => folder,
_ => return Ok(()),
};
let manpage_folder = match env::var_os("HEXAPODA_MANPAGE") {
Some(folder) if !folder.is_empty() => folder,
_ => return Ok(()),
};
let mut command = Arguments::command();
for &shell in Shell::value_variants() {
generate_to(shell, &mut command, "hexapoda", &completions_folder)?;
}
generate_to(Nushell, &mut command, "hexapoda", &completions_folder)?;
clap_mangen::generate_to(command, &manpage_folder)?;
println!("cargo:warning=completions generated in {completions_folder:?}");
println!("cargo:warning=manpage generated in {manpage_folder:?}");
println!("cargo:rerun-if-changed=src/arguments.rs");
Ok(())
}
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/swift
import Foundation
func camelCaseToSnakeCase(_ string: Substring) -> String {
var output = string.first?.lowercased() ?? ""
var previousCharacterWasUppercase = false
for character in string.dropFirst() {
if character.isUppercase {
if previousCharacterWasUppercase {
output.append(character.lowercased())
} else {
output += "_\(character.lowercased())"
previousCharacterWasUppercase = true
}
} else if character.isNumber {
output += "_\(character)"
previousCharacterWasUppercase = false
} else {
output.append(character)
previousCharacterWasUppercase = false
}
}
return output
}
let defaultConfigPath = URL(filePath: "src/config/default.rs")
let lines = try String(contentsOf: defaultConfigPath, encoding: .utf8)
.split(separator: "\n", omittingEmptySubsequences: false)
.dropFirst(14)
.dropLast(6)
precondition(lines.first!.contains("Mode::Normal"))
var mode: String?
for line in lines {
if let match = line.wholeMatch(of: #/.*Mode::(?'mode'\w*),.*/#) {
mode = match.output.mode.lowercased()
} else if line.contains("None") {
print("[\(mode!)]")
} else if let match = line.wholeMatch(of: #/.*PartialAction::(?'partialAction'\w*).*/#) {
let partialAction = match.output.partialAction.lowercased()
print("[\(mode!).\(partialAction)]")
} else if let match = line.wholeMatch(of: #/.*\(keypress\("(?'keypress'.*?)"\), (?'action'.*?)\.into\(\)\).*/#) {
if match.output.keypress.contains(where: { !($0.isLetter || $0.isNumber) }) {
print(
"\"\(match.output.keypress)\"",
"=",
"\"\(camelCaseToSnakeCase(match.output.action))\""
)
} else {
print(
match.output.keypress,
"=",
"\"\(camelCaseToSnakeCase(match.output.action))\""
)
}
} else {
print()
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.3 MiB

+84
View File
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="800"
height="800"
viewBox="0 0 800 800"
version="1.1"
id="svg9"
sodipodi:docname="bug shape.svg"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs9" />
<sodipodi:namedview
id="namedview9"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="0.28233547"
inkscape:cx="313.4569"
inkscape:cy="525.97005"
inkscape:window-width="1558"
inkscape:window-height="1186"
inkscape:window-x="51"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg9" />
<path
d="m 556.65091,660.71168 52.5,90.93266 -43.30127,25 -52.5,-90.93266 z"
style="fill:#808080;fill-opacity:1;stroke:none;stroke-width:5.04947;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path1"
inkscape:label="bottom right leg" />
<path
d="m 645,443.25116 h 105 v 50 H 645 Z"
style="fill:#808080;fill-opacity:1;stroke:none;stroke-width:5.04947;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path2"
inkscape:label="middle right leg" />
<path
style="fill:#808080;fill-opacity:1;stroke:none;stroke-width:5.04947;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 570.84895,281.10195 52.5,-90.93267 43.30127,25 -52.5,90.93267 z"
id="path3"
inkscape:label="top right leg" />
<path
style="fill:#808080;fill-opacity:1;stroke:none;stroke-width:5.04947;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 190.84968,751.64399 52.5,-90.93266 43.30127,25 -52.5,90.93266 z"
id="path10"
inkscape:label="bottom left leg" />
<path
d="m 50,443.25116 h 105 v 50 H 50 Z"
style="fill:#808080;fill-opacity:1;stroke:none;stroke-width:5.04947;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path4"
inkscape:label="middle left leg" />
<path
d="m 185.84939,306.10183 -52.5,-90.93267 43.30127,-25 52.5,90.93267 z"
style="fill:#808080;fill-opacity:1;stroke:none;stroke-width:5.04947;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path5"
inkscape:label="top left leg" />
<path
d="M 261.51951,259.5272 149.99998,323.91436 V 612.59109 L 375.00037,742.49598 V 344.48116 H 310.5664 Z m 276.96147,0 -49.04616,84.95396 H 425.00086 V 742.49598 L 650.00197,612.59109 V 323.91436 Z"
style="display:inline;fill:#808080;fill-opacity:1;stroke:none;stroke-width:18.2822;stroke-dasharray:none;stroke-opacity:1"
id="path6"
inkscape:label="body" />
<path
d="M 475.00017,59.672943 550.0009,189.57692 475.00017,319.48089 H 324.99973 L 250.00102,189.57692 325.00074,59.672943 Z"
style="display:inline;fill:#808080;fill-opacity:1;stroke:none;stroke-width:50.0001;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path7"
inkscape:label="head" />
<path
d="m 439.59927,70.987052 27.5,-47.631397 43.30127,25 -27.5,47.631397 z"
style="fill:#808080;fill-opacity:1;stroke:none;stroke-width:3.65454;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path8"
inkscape:label="right antenna" />
<path
d="m 332.90131,23.356007 27.5,47.631397 -43.30127,25 -27.5,-47.631397 z"
style="fill:#808080;fill-opacity:1;stroke:none;stroke-width:3.65454;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path9"
inkscape:label="left antenna" />
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.
+66 -953
View File
File diff suppressed because it is too large Load Diff
+109 -110
View File
@@ -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,
@@ -20,19 +19,18 @@ pub struct App {
pub should_quit: bool,
pub is_dragging_mouse: bool,
pub logs: Vec<String>,
}
#[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<PathBuf>,
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 +54,9 @@ impl App {
let mut error_alert: Option<Span> = None;
let mut buffers: Vec<Buffer> = env::args()
.skip(1)
.map(Into::into)
.filter_map(|path: PathBuf| {
let mut buffers: Vec<Buffer> = files
.iter()
.filter_map(|path| {
Buffer::from_file_at(path.clone())
.inspect_err(|error| {
error_alert = Some(
@@ -70,9 +67,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);
}
@@ -116,19 +113,23 @@ impl App {
should_quit: false,
is_dragging_mouse: false,
logs: Vec::new(),
}
}
pub fn handle_events(&mut self, terminal: &mut DefaultTerminal) {
self.handle_event(terminal);
pub fn handle_events(&mut self, terminal: &mut DefaultTerminal) -> bool {
let mut should_redraw = self.handle_event(terminal);
while event::poll(Duration::ZERO).unwrap() {
self.handle_event(terminal);
should_redraw |= self.handle_event(terminal);
}
should_redraw
}
pub fn handle_event(&mut self, terminal: &mut DefaultTerminal) {
pub fn handle_event(&mut self, terminal: &mut DefaultTerminal) -> bool {
let event = event::read()
.inspect_err(|error| {
#[cfg(target_os = "macos")] {
@@ -137,7 +138,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);
}
}
@@ -145,20 +146,24 @@ impl App {
})
.unwrap();
// self.logs.push(format!("{event:?}"));
match event {
Event::Resize(_, height) => {
self.window_size.rows = height as usize;
self.buffers[self.current_buffer_index]
.clamp_screen_to_primary_cursor(self.window_size);
true
}
Event::Key(key_event) => self.handle_key(key_event, terminal),
Event::Mouse(mouse_event) => self.handle_mouse(mouse_event),
_ => {}
_ => false
}
}
fn handle_key(&mut self, key_event: KeyEvent, terminal: &mut DefaultTerminal) {
fn handle_key(&mut self, key_event: KeyEvent, terminal: &mut DefaultTerminal) -> bool {
if key_event.modifiers == KeyModifiers::CONTROL &&
key_event.code == KeyCode::Char('c')
{
@@ -168,7 +173,7 @@ impl App {
exit(130);
}
let maybe_app_action = self.buffers[self.current_buffer_index].handle_key(
let (maybe_app_action, should_redraw) = self.buffers[self.current_buffer_index].handle_key(
key_event,
&self.config,
&self.primary_cursor_register,
@@ -187,116 +192,110 @@ impl App {
AppAction::Yank => self.yank(),
}
}
should_redraw || maybe_app_action.is_some()
}
fn handle_mouse(&mut self, mouse_event: MouseEvent) {
let tab_bar_rows = usize::from(self.buffers.len() > 1);
fn handle_mouse(&mut self, mouse_event: MouseEvent) -> bool {
let position = self.mouse_event_position(mouse_event);
let current_buffer = &mut self.buffers[self.current_buffer_index];
match mouse_event.kind {
MouseEventKind::Down(_) => {
let byte_column = match mouse_event.column {
10..=11 => Some(0),
13..=14 => Some(1),
16..=17 => Some(2),
19..=20 => Some(3),
23..=24 => Some(4),
26..=27 => Some(5),
29..=30 => Some(6),
32..=33 => Some(7),
36..=37 => Some(8),
39..=40 => Some(9),
42..=43 => Some(10),
45..=46 => Some(11),
49..=50 => Some(12),
52..=53 => Some(13),
55..=56 => Some(14),
58..=59 => Some(15),
_ => None,
};
if let Some(byte_column) = byte_column &&
mouse_event.row as usize - tab_bar_rows < self.window_size.hex_rows()
{
current_buffer.primary_cursor = Cursor::at(
current_buffer.scroll_position +
(mouse_event.row as usize - tab_bar_rows) * BYTES_PER_LINE +
byte_column
);
if let Some(position) = position {
current_buffer.primary_cursor = Cursor::at(position);
current_buffer.cursors.clear();
current_buffer.clamp_screen_to_primary_cursor(self.window_size);
self.is_dragging_mouse = true;
}
true
},
MouseEventKind::Drag(_) if self.is_dragging_mouse => {
if let Some(position) = position {
current_buffer.primary_cursor.head = position;
current_buffer.clamp_screen_to_primary_cursor(self.window_size);
}
true
},
MouseEventKind::Up(_) if self.is_dragging_mouse => {
if let Some(position) = position {
current_buffer.primary_cursor.head = position;
current_buffer.clamp_screen_to_primary_cursor(self.window_size);
}
self.is_dragging_mouse = false;
true
},
MouseEventKind::ScrollDown => {
for _ in 0..3 {
current_buffer.scroll_down(self.window_size);
}
true
},
MouseEventKind::ScrollUp => {
for _ in 0..3 {
current_buffer.scroll_up(self.window_size);
}
true
},
_ => (),
_ => false,
}
}
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();
fn mouse_event_position(&self, mouse_event: MouseEvent) -> Option<usize> {
let tab_bar_rows = usize::from(self.buffers.len() > 1);
if usize::from(mouse_event.row) - tab_bar_rows >= self.window_size.hex_rows() {
return None;
}
}
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();
let byte_column = match mouse_event.column {
10..=11 => Some(0),
13..=14 => Some(1),
16..=17 => Some(2),
19..=20 => Some(3),
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
23..=24 => Some(4),
26..=27 => Some(5),
29..=30 => Some(6),
32..=33 => Some(7),
36..=37 => Some(8),
39..=40 => Some(9),
42..=43 => Some(10),
45..=46 => Some(11),
49..=50 => Some(12),
52..=53 => Some(13),
55..=56 => Some(14),
58..=59 => Some(15),
_ => None,
};
byte_column.map(|byte_column| {
current_buffer.scroll_position +
(mouse_event.row as usize - tab_bar_rows) * BYTES_PER_LINE +
byte_column
})
// if let Some(byte_column) = byte_column &&
// mouse_event.row as usize - tab_bar_rows < self.window_size.hex_rows()
// {
// Some(
// current_buffer.scroll_position +
// (mouse_event.row as usize - tab_bar_rows) * BYTES_PER_LINE +
// byte_column
// )
// } else {
// None
// }
}
}
+56
View File
@@ -0,0 +1,56 @@
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 = &mut 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();
current_buffer.alert_message = if current_buffer.cursors.is_empty() {
"yanked 1 selection".into()
} else {
format!("yanked {} selections", current_buffer.cursors.len() + 1).into()
};
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
use ratatui::{layout::Rect, style::{Color, Stylize}, text::{Line, Span}, widgets::Widget};
use crate::{app::App, buffer::Buffer, custom_greys::CustomGreys};
use crate::{app::App, buffer::Buffer, utilities::CustomGreys};
impl Widget for &App {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
@@ -29,7 +29,7 @@ impl App {
fn tab_for(buffer: &Buffer, is_active: bool) -> Span<'static> {
let background = if is_active {
Color::select_grey()
Color::selection_tail_grey()
} else {
Color::ui_grey()
};
+18
View File
@@ -0,0 +1,18 @@
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<PathBuf>,
/// the input files to edit
#[arg(value_name = "files")]
pub files: Vec<PathBuf>,
/// print the path to the config file
#[arg(short, long)]
pub show_config_path: bool,
}
+47 -115
View File
@@ -1,11 +1,11 @@
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, utilities::IsOverlapping, window_size::WindowSize};
mod widget;
mod actions;
pub struct Buffer {
pub file_name: String,
@@ -41,22 +41,14 @@ pub struct Buffer {
#[derive(Debug)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
Normal, Select, Insert
Normal, Select, // Insert
}
#[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug)]
#[serde(rename_all = "snake_case")]
pub enum PartialAction {
Goto, View, Replace, Space, Repeat, To
}
#[derive(Clone)]
pub struct Popup {
pub at: usize,
width: u16,
primary: bool,
lines: Vec<Span<'static>>
Goto, View, Replace, Space, Repeat, Till
}
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -64,39 +56,6 @@ 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 = ();
@@ -109,67 +68,12 @@ impl TryFrom<&str> for PartialAction {
"replace" => Ok(Replace),
"space" => Ok(Space),
"repeat" => Ok(Repeat),
"to" => Ok(To),
"till" => Ok(Till),
_ => Err(()),
}
}
}
impl Popup {
pub fn new(at: usize, lines: Vec<Span<'static>>) -> 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<Self> {
let mut file = File::open(&file_path)?;
@@ -216,13 +120,15 @@ impl Buffer {
primary_cursor_register: &[u8],
other_cursor_registers: &[Vec<u8>],
window_size: WindowSize
) -> Option<AppAction> {
) -> (Option<AppAction>, bool) {
let mut should_redraw = !self.alert_message.content.is_empty();
self.alert_message = "".into();
// self.logs.push(format!("{event:?}"));
let app_action = match self.partial_action {
Some(PartialAction::Replace) => {
self.handle_replace(event, window_size);
should_redraw = true;
None
},
Some(PartialAction::Repeat) => {
@@ -233,19 +139,28 @@ impl Buffer {
other_cursor_registers,
window_size
);
should_redraw = true;
None
},
_ => self.handle_other_modes(event, config, window_size),
_ => {
let (app_action, redraw) = self.handle_other_modes(event, config, window_size);
should_redraw |= redraw;
app_action
},
};
assert!(self.scroll_position.is_multiple_of(BYTES_PER_LINE));
assert!(self.scroll_position < self.contents.len());
assert!(self.primary_cursor.head < self.contents.len());
assert!(self.primary_cursor.tail < self.contents.len());
if !self.contents.is_empty() {
assert!(self.scroll_position < self.contents.len());
assert!(self.primary_cursor.head < self.contents.len());
assert!(self.primary_cursor.tail < self.contents.len());
}
assert!(self.scroll_position <= self.primary_cursor.head);
assert!(self.primary_cursor.head < self.scroll_position + window_size.visible_byte_count());
app_action
debug_assert!(self.cursors.is_sorted_by_key(|cursor| cursor.head));
(app_action, should_redraw)
}
fn handle_replace(&mut self, event: KeyEvent, window_size: WindowSize) {
@@ -281,17 +196,20 @@ impl Buffer {
event: KeyEvent,
config: &Config,
window_size: WindowSize
) -> Option<AppAction> {
) -> (Option<AppAction>, bool) {
use Action::*;
let mut result = None;
let should_reset_partial = self.partial_action.is_some();
let mut should_redraw = should_reset_partial;
if let Some(mode_config) = config.0.get(&self.mode) &&
let Some(keybinds) = mode_config.0.get(&self.partial_action) &&
let Some(action) = keybinds.0.get(&event.into())
{
should_redraw = true;
if action.clears_popups() {
self.popups.clear();
}
@@ -323,7 +241,7 @@ impl Buffer {
self.partial_action = None;
}
result
(result, should_redraw)
}
fn handle_repeat(
@@ -375,7 +293,11 @@ impl Buffer {
self.combine_cursors_if_overlapping();
self.clamp_screen_to_primary_cursor(window_size);
},
_ => panic!("repeated actions may only be cursor actions"),
_ => {
self.alert_message = Span::from(
"only cursor actions may be repeated"
).red();
}
}
}
}
@@ -404,10 +326,20 @@ impl Buffer {
pub fn combine_cursors_if_overlapping(&mut self) {
let mut index = 0;
// TODO: this can miss some in the WEIRD case that
// [ *]
// [ *]
// [ *]
// where * is the head.
// the first one wont merge with the 2nd, but the 2nd will
// merge with the 3rd, which would then overlap with the 1st,
// but won't be checked
while !self.cursors.is_empty() && index < self.cursors.len() {
while index < self.cursors.len() - 1 &&
self.cursors[index].range().is_overlapping(
&self.cursors[index + 1].range())
&self.cursors[index + 1].range()
)
{
let next_cursor = self.cursors[index + 1];
self.cursors[index].combine_with(next_cursor);
@@ -419,9 +351,9 @@ impl Buffer {
{
self.primary_cursor.combine_with(self.cursors[index]);
self.cursors.remove(index);
} else {
index += 1;
}
index += 1;
}
}
}
@@ -451,7 +383,7 @@ mod tests {
#[test]
fn nybble_from_hex_digits_are_correct() {
for (index, character) in ('0'..='9').enumerate() {
assert_eq!(nybble_from_hex(character), Some(index as u8));
assert_eq!(nybble_from_hex(character), Some(u8::try_from(index).unwrap()));
}
}
}
+959
View File
@@ -0,0 +1,959 @@
use std::{cmp::min, 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, utilities::{Floorable, SaturatingSubtract}, 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(window_size),
BufferAction::FindTillMark => self.till_mark(false, window_size), // extend: false
BufferAction::FindTillNull => self.till_null(false, window_size), // extend: false
BufferAction::FindTillFF => self.till_FF(false, window_size), // extend: false
BufferAction::ExtendTillMark => self.till_mark(true, window_size), // extend: true
BufferAction::ExtendTillNull => self.till_null(true, window_size), // extend: true
BufferAction::ExtendTillFF => self.till_FF(true, window_size), // extend: true
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::Till);
}
pub fn scroll_down(&mut self, window_size: WindowSize) {
self.scroll_position += BYTES_PER_LINE;
self.clamp_screen_to_contents(window_size);
self.clamp_primary_cursor_to_screen(window_size);
self.combine_cursors_if_overlapping();
}
pub fn scroll_up(&mut self, window_size: WindowSize) {
self.scroll_position.saturating_subtract(BYTES_PER_LINE);
self.clamp_primary_cursor_to_screen(window_size);
self.combine_cursors_if_overlapping();
}
fn page_cursor_half_down(&mut self, window_size: WindowSize) {
let scroll_amount = (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE);
self.scroll_position += scroll_amount;
self.clamp_screen_to_contents(window_size);
self.primary_cursor.head += scroll_amount;
if self.mode != Mode::Select {
self.primary_cursor.tail += scroll_amount;
}
self.primary_cursor.clamp(0, self.max_contents_index());
self.clamp_screen_to_primary_cursor(window_size);
let max_contents_index = self.max_contents_index();
for cursor in &mut self.cursors {
cursor.head += scroll_amount;
if self.mode != Mode::Select {
cursor.tail += scroll_amount;
}
cursor.clamp(0, max_contents_index);
}
self.combine_cursors_if_overlapping();
}
fn page_cursor_half_up(&mut self, window_size: WindowSize) {
let scroll_amount = (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE);
self.scroll_position.saturating_subtract(scroll_amount);
self.primary_cursor.head.saturating_subtract(scroll_amount);
if self.mode != Mode::Select {
self.primary_cursor.tail.saturating_subtract(scroll_amount);
}
for cursor in &mut self.cursors {
cursor.head.saturating_subtract(scroll_amount);
if self.mode != Mode::Select {
cursor.tail.saturating_subtract(scroll_amount);
}
}
self.combine_cursors_if_overlapping();
}
fn page_down(&mut self, window_size: WindowSize) {
self.scroll_position += window_size.visible_byte_count();
self.clamp_screen_to_contents(window_size);
self.clamp_primary_cursor_to_screen(window_size);
self.combine_cursors_if_overlapping();
}
fn page_up(&mut self, window_size: WindowSize) {
self.scroll_position.saturating_subtract(window_size.visible_byte_count());
self.clamp_screen_to_contents(window_size);
self.clamp_primary_cursor_to_screen(window_size);
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<Cursor> = 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]);
// TODO: is a full sort necessary ?
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
.sorted_by_key(|cursor| cursor.head)
.collect();
self.clamp_screen_to_primary_cursor(window_size);
}
fn jump_to_selected_offset(&mut self, window_size: WindowSize) {
// check all cursors before modifying any
if !iter::once(&self.primary_cursor)
.chain(&self.cursors)
.all(|cursor| {
bytes_to_nat(&self.contents[cursor.range()])
.and_then(|nat| usize::try_from(nat).ok())
.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()
.try_into().unwrap()
);
for cursor in &mut self.cursors {
*cursor = Cursor::at(
bytes_to_nat(&self.contents[cursor.range()])
.unwrap()
.try_into().unwrap()
);
}
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();
// check all cursors before modifying any
if !iter::once(&self.primary_cursor)
.chain(&self.cursors)
.all(|cursor| {
bytes_to_nat(&self.contents[cursor.range()])
.and_then(|offset| usize::try_from(offset).ok())
.map(|offset| mark_before(cursor.lower_bound(), &sorted_marks) + offset)
.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(
mark_before(self.primary_cursor.lower_bound(), &sorted_marks) +
usize::try_from(
bytes_to_nat(&self.contents[self.primary_cursor.range()]).unwrap()
).unwrap()
);
for cursor in &mut self.cursors {
*cursor = Cursor::at(
mark_before(cursor.lower_bound(), &sorted_marks) +
usize::try_from(
bytes_to_nat(&self.contents[cursor.range()]).unwrap()
).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) {
if !self.marks.insert(self.primary_cursor.lower_bound()) {
self.marks.remove(&self.primary_cursor.lower_bound());
}
for cursor in &self.cursors {
if !self.marks.insert(cursor.lower_bound()) {
self.marks.remove(&cursor.lower_bound());
}
}
}
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
.floored_to_the_nearest(BYTES_PER_LINE)
.saturating_sub(half_a_screen.floored_to_the_nearest(BYTES_PER_LINE));
}
fn align_view_bottom(&mut self, window_size: WindowSize) {
self.scroll_position = self.primary_cursor.head
.floored_to_the_nearest(BYTES_PER_LINE)
.saturating_sub(
window_size
.visible_byte_count()
.saturating_sub(BYTES_PER_LINE + Self::bottom_padding(window_size))
)
.min(self.max_contents_index().floored_to_the_nearest(BYTES_PER_LINE));
}
fn align_view_top(&mut self, window_size: WindowSize) {
self.scroll_position = self.primary_cursor.head
.floored_to_the_nearest(BYTES_PER_LINE)
.saturating_sub(self.top_padding(window_size));
}
fn till_mark(&mut self, extend: bool, 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
);
if !extend {
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
);
if !extend {
cursor.tail = cursor.head;
}
cursor.head = mark_after_cursor - 1;
}
self.combine_cursors_if_overlapping();
self.clamp_screen_to_primary_cursor(window_size);
}
fn till_null(&mut self, extend: bool, window_size: WindowSize) {
if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..]
.iter()
.skip(1)
.position(|&byte| byte == 0)
{
if !extend {
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)
{
if !extend {
cursor.tail = cursor.head;
}
cursor.head += null_offset_after_primary;
}
}
self.combine_cursors_if_overlapping();
self.clamp_screen_to_primary_cursor(window_size);
}
#[allow(non_snake_case)]
fn till_FF(&mut self, extend: bool, window_size: WindowSize) {
if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..]
.iter()
.skip(1)
.position(|&byte| byte == 0xFF)
{
if !extend {
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)
{
if !extend {
cursor.tail = cursor.head;
}
cursor.head += null_offset_after_primary;
}
}
self.combine_cursors_if_overlapping();
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;
}
}
}
#[allow(clippy::too_many_lines)]
fn inspect(selection: &[u8]) -> Vec<Span<'static>> {
let nat = bytes_to_nat(selection);
let int = nat.and_then(|nat| nat_to_int_if_different(nat, selection.len()));
let binary = nat
.filter(|_| selection.len() == 1)
.map(|nat| {
let lower_bits = nat & 0b1111;
let upper_bits = nat >> 4;
format!("{upper_bits:04b}_{lower_bits:04b}").into()
});
let utf8 = str::from_utf8(selection).ok()
.filter(|_| selection.len() != 1)
.map(|utf8| utf8.trim_end_matches('\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| u32::try_from(nat).unwrap())
.map(|nat| f64::from(nat) / 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| i32::try_from(int).unwrap())
.map(|int| f64::from(int) / 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| u32::try_from(nat).unwrap())
.map(|nat| f64::from(nat) / 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| i32::try_from(int).unwrap())
.map(|int| f64::from(int) / 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| u16::try_from(nat).unwrap())
.map(|nat| f64::from(nat) / 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| u16::try_from(nat).unwrap())
.map(|nat| f64::from(nat) / 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| u16::try_from(nat).unwrap())
.map(|nat| f64::from(nat) / 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| u16::try_from(nat).unwrap())
.map(color555_to_color888)
.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(binary)
.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<Span<'static>> {
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| u16::try_from(nat).unwrap())
.map(color555_to_color888)
.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 {
const fn bottom_padding(window_size: WindowSize) -> usize {
if window_size.hex_rows() <= LINES_OF_PADDING * 2 {
0
} else {
BYTES_OF_PADDING
}
}
const fn top_padding(&self, window_size: WindowSize) -> usize {
if window_size.hex_rows() <= LINES_OF_PADDING * 2 || self.scroll_position == 0 {
0
} else {
BYTES_OF_PADDING
}
}
pub fn clamp_screen_to_contents(&mut self, window_size: WindowSize) {
let max_scroll_position = self.max_contents_index()
.floored_to_the_nearest(BYTES_PER_LINE)
.saturating_sub(Self::bottom_padding(window_size));
if self.scroll_position > max_scroll_position {
self.scroll_position = max_scroll_position;
}
}
pub fn clamp_screen_to_primary_cursor(&mut self, window_size: WindowSize) {
if self.primary_cursor.head < self.scroll_position + self.top_padding(window_size) {
self.align_view_top(window_size);
} else if self.primary_cursor.head > self.scroll_position + (window_size.visible_byte_count() - 1).saturating_sub(Self::bottom_padding(window_size)) {
self.align_view_bottom(window_size);
}
}
fn clamp_primary_cursor_to_screen(&mut self, window_size: WindowSize) {
let min = self.scroll_position + self.top_padding(window_size);
let max = self.scroll_position + window_size.visible_byte_count()
.saturating_sub(Self::bottom_padding(window_size))
.saturating_sub(BYTES_PER_LINE);
if self.mode == Mode::Select {
self.primary_cursor.head = self.primary_cursor.head.clamp(min, max);
} else {
self.primary_cursor.clamp(min, max);
}
}
}
pub fn bytes_to_nat(bytes: &[u8]) -> Option<u64> {
bytes
.iter()
.rev() // little-endian
.skip_while(|&&byte| byte == 0)
.try_fold(u64::default(), |result, &byte| {
if result.leading_zeros() < 8 {
None
} else {
Some((result << 8) | u64::from(byte))
}
})
}
fn nat_to_int_if_different(nat: u64, bytes: usize) -> Option<i64> {
match bytes {
1 if nat > i8::MAX as u64 => Some(i64::from(u8::try_from(nat).unwrap().cast_signed())),
2 if nat > i16::MAX as u64 => Some(i64::from(u16::try_from(nat).unwrap().cast_signed())),
4 if nat > i32::MAX as u64 => Some(i64::from(u32::try_from(nat).unwrap().cast_signed())),
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
]
}
+9 -402
View File
@@ -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);
@@ -55,8 +61,8 @@ impl Widget for &Buffer {
let popup_area = popup
.area_at(
area.x + byte_column_to_screen_column(hex_column) as u16,
area.y + (position_on_screen / BYTES_PER_LINE) as u16 + 1
area.x + byte_column_to_screen_column(hex_column),
area.y + u16::try_from(position_on_screen / BYTES_PER_LINE).unwrap() + 1
)
.clamp(hex_area);
@@ -102,406 +108,7 @@ 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<Item=Span<'static>> {
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
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::<Vec<_>>()
})
}
pub fn render_partial_chunks(
&self,
address: usize,
bytes: &[u8]
) -> impl Iterator<Item=Span<'static>> {
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
let remainder_address = address + chunks.len() * BYTES_PER_CHUNK;
#[allow(clippy::if_not_else)]
let remainder_chunks: Option<Vec<_>> = 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<Item=Span<'static>> {
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<Item=Span<'static>> {
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::select_grey()),
}
} else {
match self.cursors
.iter()
.find_map(|cursor| cursor.contains(address))
{
Some(InCursor::Head) => span.on_gray(),
Some(InCursor::Rest) => span.bg(Color::select_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::select_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::select_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<Item=Span<'static>> {
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::select_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 {
fn byte_column_to_screen_column(byte_column: usize) -> u16 {
match byte_column {
0 => 10,
1 => 13,
+16
View File
@@ -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))
}
}
+80
View File
@@ -0,0 +1,80 @@
use std::{borrow::Cow, iter, mem};
use ratatui::{style::{Color, Style, Stylize}, text::Span};
use crate::{buffer::Buffer, utilities::{CustomGreys, empty_span, HasCardinality}, cursor::InCursor};
impl Buffer {
pub fn render_character_panel(
&self,
address: usize,
bytes: &[u8]
) -> impl Iterator<Item=Span<'static>> {
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 {
#[allow(clippy::cast_possible_truncation)]
let byte = index as u8;
result[index].style = style_for_character(byte);
mem::forget(mem::replace(
&mut result[index].content,
content_for_character(byte)
));
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]
}
+34
View File
@@ -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 => "×",
Till => "t",
}
}
}
+241
View File
@@ -0,0 +1,241 @@
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}, utilities::{CustomGreys, empty_span, HasCardinality}, cursor::InCursor};
impl Buffer {
pub fn render_chunks(
&self,
address: usize,
bytes: &[u8; BYTES_PER_LINE]
) -> impl Iterator<Item=Span<'static>> {
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
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::<Vec<_>>()
})
}
pub fn render_partial_chunks(
&self,
address: usize,
bytes: &[u8]
) -> impl Iterator<Item=Span<'static>> {
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
let remainder_address = address + chunks.len() * BYTES_PER_CHUNK;
#[allow(clippy::if_not_else)]
let remainder_chunks: Option<Vec<_>> = 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<Item=Span<'static>> {
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<Item=Span<'static>> {
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> {
// TODO: checking this with lots of selections is slow
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,
Mode::Normal => 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 {
#[allow(clippy::cast_possible_truncation)]
let byte = index as u8;
result[index].style = style_for_byte(byte);
mem::forget(mem::replace(&mut result[index].content, content_for_byte(byte)));
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]
}
+54
View File
@@ -0,0 +1,54 @@
use crate::{buffer::{Buffer, Mode}, utilities::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,
}
}
}
+50 -272
View File
@@ -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(
@@ -32,7 +34,7 @@ pub struct Keypress {
impl Config {
#[cfg(unix)]
fn path() -> Option<PathBuf> {
fn default_path() -> Option<PathBuf> {
env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.take_if(|xdg_config_home| xdg_config_home.is_absolute())
@@ -41,16 +43,36 @@ impl Config {
}
#[cfg(windows)]
fn path() -> Option<PathBuf> {
fn default_path() -> Option<PathBuf> {
// this isn't technically the right way but it should be good enough
home_dir().map(|home| home.join("AppData").join("Roaming"))
home_dir().map(|home| home.join("AppData").join("Roaming").join("hexapoda.toml"))
}
pub fn init() -> Result<Self, ConfigInitError> {
let path = Self::path().ok_or(ConfigInitError::NoConfigPath)?;
pub fn path(override_path: Option<PathBuf>) -> Option<PathBuf> {
override_path.or_else(Self::default_path)
}
pub fn init(override_path: Option<PathBuf>) -> Result<Self, ConfigInitError> {
let path = Self::path(override_path)
.ok_or(ConfigInitError::NoConfigPath)?;
let raw_config = read_to_string(path)?;
Ok(toml::from_str(&raw_config)?)
Ok(Self::default().combined_with(toml::from_str(&raw_config)?))
}
fn combined_with(mut self, other: Self) -> Self {
for (mode, mode_config) in other.0 {
match self.0.entry(mode) {
Entry::Occupied(mut occupied_entry) => {
occupied_entry.get_mut().combine_with(mode_config);
}
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(mode_config);
}
}
}
self
}
}
@@ -70,6 +92,27 @@ impl From<toml::de::Error> for ConfigInitError {
}
}
impl ModeConfig {
fn combine_with(&mut self, other: Self) {
for (partial_action, keybinds) in other.0 {
match self.0.entry(partial_action) {
Entry::Occupied(mut occupied_entry) => {
occupied_entry.get_mut().combine_with(keybinds);
}
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(keybinds);
}
}
}
}
}
impl Keybinds {
fn combine_with(&mut self, other: Self) {
self.0.extend(other.0);
}
}
impl Serialize for ModeConfig {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
@@ -291,268 +334,3 @@ impl From<KeyEvent> 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()
}
}
+284
View File
@@ -0,0 +1,284 @@
use crate::{action::{AppAction, BufferAction, CursorAction}, buffer::{Mode, PartialAction}, config::{Config, Keypress}};
fn keypress(string: &str) -> Keypress {
string.try_into().unwrap()
}
impl Default for Config {
#[allow(clippy::too_many_lines)]
fn default() -> Self {
use AppAction::*;
use BufferAction::*;
use CursorAction::*;
[
(Mode::Normal, [
(None, [
(keypress("q"), QuitIfSaved.into()),
(keypress("Q"), Quit.into()),
(keypress("v"), SelectMode.into()),
(keypress("g"), Goto.into()),
(keypress("z"), View.into()),
(keypress("r"), Replace.into()),
(keypress(" "), Space.into()),
(keypress("*"), Repeat.into()),
(keypress("t"), To.into()),
(keypress("i"), MoveByteUp.into()),
(keypress("k"), MoveByteDown.into()),
(keypress("j"), MoveByteLeft.into()),
(keypress("l"), MoveByteRight.into()),
(keypress("up"), MoveByteUp.into()),
(keypress("down"), MoveByteDown.into()),
(keypress("left"), MoveByteLeft.into()),
(keypress("right"), MoveByteRight.into()),
(keypress("G"), GotoFileEnd.into()),
(keypress("C-e"), ScrollDown.into()),
(keypress("C-y"), ScrollUp.into()),
(keypress("C-d"), PageCursorHalfDown.into()),
(keypress("C-u"), PageCursorHalfUp.into()),
(keypress("C-f"), PageDown.into()),
(keypress("C-b"), PageUp.into()),
(keypress("w"), MoveNextWordStart.into()),
(keypress("e"), MoveNextWordEnd.into()),
(keypress("b"), MovePreviousWordStart.into()),
(keypress(";"), CollapseSelection.into()),
(keypress("A-;"), FlipSelections.into()),
(keypress("x"), ExtendLineBelow.into()),
(keypress("X"), ExtendLineAbove.into()),
(keypress("d"), Delete.into()),
(keypress("u"), Undo.into()),
(keypress("U"), Redo.into()),
(keypress("C-j"), PreviousBuffer.into()),
(keypress("C-l"), NextBuffer.into()),
(keypress("C"), CopySelectionOnNextLine.into()),
(keypress("("), RotateSelectionsBackward.into()),
(keypress(")"), RotateSelectionsForward.into()),
(keypress(","), KeepPrimarySelection.into()),
(keypress("A-,"), RemovePrimarySelection.into()),
(keypress("1"), SplitSelectionsInto1s.into()),
(keypress("2"), SplitSelectionsInto2s.into()),
(keypress("3"), SplitSelectionsInto3s.into()),
(keypress("4"), SplitSelectionsInto4s.into()),
(keypress("5"), SplitSelectionsInto5s.into()),
(keypress("6"), SplitSelectionsInto6s.into()),
(keypress("7"), SplitSelectionsInto7s.into()),
(keypress("8"), SplitSelectionsInto8s.into()),
(keypress("9"), SplitSelectionsInto9s.into()),
(keypress("J"), JumpToSelectedOffsetRelativeToMark.into()),
(keypress("A-J"), JumpToSelectedOffset.into()),
(keypress("m"), ToggleMark.into()),
(keypress("y"), Yank.into()),
(keypress("C- "), InspectSelection.into()),
(keypress("A- "), InspectSelectionColor.into()),
].into()),
(Some(PartialAction::Goto), [
(keypress("j"), GotoLineStart.into()),
(keypress("l"), GotoLineEnd.into()),
(keypress("g"), GotoFileStart.into()),
].into()),
(Some(PartialAction::View), [
(keypress("z"), AlignViewCenter.into()),
(keypress("b"), AlignViewBottom.into()),
(keypress("t"), AlignViewTop.into()),
].into()),
(Some(PartialAction::Space), [
(keypress("w"), Save.into()),
(keypress("q"), QuitIfSaved.into()),
(keypress("Q"), Quit.into()),
].into()),
(Some(PartialAction::Repeat), [
(keypress("i"), MoveByteUp.into()),
(keypress("k"), MoveByteDown.into()),
(keypress("j"), MoveByteLeft.into()),
(keypress("l"), MoveByteRight.into()),
(keypress("up"), MoveByteUp.into()),
(keypress("down"), MoveByteDown.into()),
(keypress("left"), MoveByteLeft.into()),
(keypress("right"), MoveByteRight.into()),
(keypress("C-e"), ScrollDown.into()),
(keypress("C-y"), ScrollUp.into()),
(keypress("C-d"), PageCursorHalfDown.into()),
(keypress("C-u"), PageCursorHalfUp.into()),
(keypress("C-f"), PageDown.into()),
(keypress("C-b"), PageUp.into()),
(keypress("w"), MoveNextWordStart.into()),
(keypress("e"), MoveNextWordEnd.into()),
(keypress("b"), MovePreviousWordStart.into()),
(keypress("x"), ExtendLineBelow.into()),
(keypress("X"), ExtendLineAbove.into()),
(keypress("d"), Delete.into()),
(keypress("C"), CopySelectionOnNextLine.into()),
].into()),
(Some(PartialAction::Till), [
(keypress("m"), FindTillMark.into()),
(keypress("0"), FindTillNull.into()),
(keypress("f"), FindTillFF.into()),
].into()),
].into()),
(Mode::Select, [
(None, [
(keypress("q"), QuitIfSaved.into()),
(keypress("Q"), Quit.into()),
(keypress("v"), NormalMode.into()),
(keypress("g"), Goto.into()),
(keypress("z"), View.into()),
(keypress("r"), Replace.into()),
(keypress(" "), Space.into()),
(keypress("*"), Repeat.into()),
(keypress("t"), To.into()),
(keypress("i"), ExtendByteUp.into()),
(keypress("k"), ExtendByteDown.into()),
(keypress("j"), ExtendByteLeft.into()),
(keypress("l"), ExtendByteRight.into()),
(keypress("G"), ExtendFileEnd.into()),
(keypress("up"), ExtendByteUp.into()),
(keypress("down"), ExtendByteDown.into()),
(keypress("left"), ExtendByteLeft.into()),
(keypress("right"), ExtendByteRight.into()),
(keypress("C-e"), ScrollDown.into()),
(keypress("C-y"), ScrollUp.into()),
(keypress("C-d"), PageCursorHalfDown.into()),
(keypress("C-u"), PageCursorHalfUp.into()),
(keypress("C-f"), PageDown.into()),
(keypress("C-b"), PageUp.into()),
(keypress("w"), ExtendNextWordStart.into()),
(keypress("e"), ExtendNextWordEnd.into()),
(keypress("b"), ExtendPreviousWordStart.into()),
(keypress(";"), CollapseSelection.into()),
(keypress("A-;"), FlipSelections.into()),
(keypress("x"), ExtendLineBelow.into()),
(keypress("X"), ExtendLineAbove.into()),
(keypress("d"), Delete.into()),
(keypress("u"), Undo.into()),
(keypress("U"), Redo.into()),
(keypress("C"), CopySelectionOnNextLine.into()),
(keypress("("), RotateSelectionsBackward.into()),
(keypress(")"), RotateSelectionsForward.into()),
(keypress(","), KeepPrimarySelection.into()),
(keypress("A-,"), RemovePrimarySelection.into()),
(keypress("1"), SplitSelectionsInto1s.into()),
(keypress("2"), SplitSelectionsInto2s.into()),
(keypress("3"), SplitSelectionsInto3s.into()),
(keypress("4"), SplitSelectionsInto4s.into()),
(keypress("5"), SplitSelectionsInto5s.into()),
(keypress("6"), SplitSelectionsInto6s.into()),
(keypress("7"), SplitSelectionsInto7s.into()),
(keypress("8"), SplitSelectionsInto8s.into()),
(keypress("9"), SplitSelectionsInto9s.into()),
(keypress("J"), JumpToSelectedOffsetRelativeToMark.into()),
(keypress("A-J"), JumpToSelectedOffset.into()),
(keypress("m"), ToggleMark.into()),
(keypress("y"), Yank.into()),
(keypress("C- "), InspectSelection.into()),
(keypress("A- "), InspectSelectionColor.into()),
].into()),
(Some(PartialAction::Goto), [
(keypress("j"), ExtendLineStart.into()),
(keypress("l"), ExtendLineEnd.into()),
(keypress("g"), ExtendFileStart.into()),
].into()),
(Some(PartialAction::View), [
(keypress("z"), AlignViewCenter.into()),
(keypress("b"), AlignViewBottom.into()),
(keypress("t"), AlignViewTop.into()),
].into()),
(Some(PartialAction::Space), [
(keypress("w"), Save.into()),
(keypress("q"), QuitIfSaved.into()),
(keypress("Q"), Quit.into()),
].into()),
(Some(PartialAction::Repeat), [
(keypress("i"), ExtendByteUp.into()),
(keypress("k"), ExtendByteDown.into()),
(keypress("j"), ExtendByteLeft.into()),
(keypress("l"), ExtendByteRight.into()),
(keypress("up"), ExtendByteUp.into()),
(keypress("down"), ExtendByteDown.into()),
(keypress("left"), ExtendByteLeft.into()),
(keypress("right"), ExtendByteRight.into()),
(keypress("C-e"), ScrollDown.into()),
(keypress("C-y"), ScrollUp.into()),
(keypress("C-d"), PageCursorHalfDown.into()),
(keypress("C-u"), PageCursorHalfUp.into()),
(keypress("C-f"), PageDown.into()),
(keypress("C-b"), PageUp.into()),
(keypress("w"), ExtendNextWordStart.into()),
(keypress("e"), ExtendNextWordEnd.into()),
(keypress("b"), ExtendPreviousWordStart.into()),
(keypress("x"), ExtendLineBelow.into()),
(keypress("X"), ExtendLineAbove.into()),
(keypress("d"), Delete.into()),
(keypress("C"), CopySelectionOnNextLine.into()),
].into()),
(Some(PartialAction::Till), [
(keypress("m"), ExtendTillMark.into()),
(keypress("0"), ExtendTillNull.into()),
(keypress("f"), ExtendTillFF.into()),
].into()),
].into())
].into()
}
}
+5 -446
View File
@@ -1,5 +1,6 @@
use std::{cmp::{max, min}, mem::swap, ops::RangeInclusive};
use crate::{BYTES_PER_LINE, action::CursorAction};
mod actions;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Cursor {
@@ -58,12 +59,9 @@ impl Cursor {
swap(&mut self.head, &mut self.tail);
}
// TODO: in visual mode, should only clamp head
pub fn clamp(&mut self, scroll_position: usize, screen_size: usize) {
let max_row = scroll_position + screen_size - 1;
self.head = self.head.clamp(scroll_position, max_row);
self.tail = self.tail.clamp(scroll_position, max_row);
pub fn clamp(&mut self, min: usize, max: usize) {
self.head = self.head.clamp(min, max);
self.tail = self.tail.clamp(min, max);
}
pub fn combine_with(&mut self, other: Self) {
@@ -75,443 +73,4 @@ impl Cursor {
self.tail = min(self.tail, other.tail);
}
}
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 });
}
}
+460
View File
@@ -0,0 +1,460 @@
use std::{cmp::min, mem::swap};
use crate::{BYTES_PER_LINE, action::CursorAction, cursor::Cursor, utilities::{Floorable, SaturatingSubtract}};
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::ExtendLineStart => self.extend_line_start(),
CursorAction::ExtendLineEnd => self.extend_line_end(max_contents_index),
CursorAction::ExtendFileStart => self.extend_file_start(),
CursorAction::ExtendFileEnd => self.extend_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 fn goto_line_start(&mut self) {
self.extend_line_start();
self.collapse();
}
pub fn goto_line_end(&mut self, max: usize) {
self.extend_line_end(max);
self.collapse();
}
pub const fn goto_file_start(&mut self) {
self.extend_file_start();
self.collapse();
}
pub fn goto_file_end(&mut self, max: usize) {
self.extend_file_end(max);
self.collapse();
}
pub fn extend_line_start(&mut self) {
self.head.floor_to_the_nearest(BYTES_PER_LINE);
}
pub fn extend_line_end(&mut self, max: usize) {
self.head = min(
self.head.floored_to_the_nearest(BYTES_PER_LINE) + BYTES_PER_LINE - 1,
max
);
}
pub const fn extend_file_start(&mut self) {
self.head %= BYTES_PER_LINE;
}
pub fn extend_file_end(&mut self, max: usize) {
self.head += previous_multiple_of(BYTES_PER_LINE, max + 1 - self.head);
}
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 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.floor_to_the_nearest(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 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.floor_to_the_nearest(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.floor_to_the_nearest(BYTES_PER_LINE);
self.head = min(
self.head.floored_to_the_nearest(BYTES_PER_LINE) + BYTES_PER_LINE - 1,
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.saturating_subtract(BYTES_PER_LINE);
} else {
self.head.floor_to_the_nearest(BYTES_PER_LINE);
self.tail = min(
self.tail.floored_to_the_nearest(BYTES_PER_LINE) + BYTES_PER_LINE - 1,
max
);
}
}
}
fn previous_multiple_of(multiple: usize, number: usize) -> usize {
number.saturating_sub(1).floored_to_the_nearest(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 });
}
}
-16
View File
@@ -1,16 +0,0 @@
use ratatui::style::Color;
pub trait CustomGreys {
fn select_grey() -> Self;
fn ui_grey() -> Self;
}
impl CustomGreys for Color {
fn select_grey() -> Self {
Self::Indexed(242)
}
fn ui_grey() -> Self {
Self::Indexed(238)
}
}
+1 -1
View File
@@ -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 {
+47 -33
View File
@@ -1,24 +1,22 @@
#![warn(clippy::pedantic, clippy::nursery)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::enum_glob_use)]
#![feature(get_disjoint_mut_helpers)]
#![feature(exact_bitshifts)]
#![feature(hash_set_entry)]
#![feature(trim_prefix_suffix)]
use arguments::Arguments;
use clap::Parser;
use app::App;
use crossterm::{QueueableCommand, event::{DisableMouseCapture, EnableMouseCapture}};
use crate::config::Config;
mod app;
mod buffer;
mod popup;
mod config;
mod cursor;
mod action;
mod edit_action;
mod cardinality;
mod empty_span;
mod custom_greys;
mod arguments;
mod window_size;
mod utilities;
const BYTES_PER_LINE: usize = 0x10;
const BYTES_PER_CHUNK: usize = 4;
@@ -28,21 +26,21 @@ 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
// - uhhhhh?
// - inspector translations for varint
// - `go` goto entered offset
// - search
// - ascii and bytes (`/` and `A-/`?)
// - `/` hex, `A-/` ascii
// - if non-hex-digit typed, search ascii
// - inspector translations for varint
// - M mark at selected offset? (like Jm)
// - diffing
// - doesn't have to be anything fancy, just compare each byte 1:1
// - sync scroll ? sync selections ??
// - s/A-k/A-K
// - sm select marks
// - C-a/C-x
// - +/- to edit selected bytes by amount ?
// - operate on entire selection (u16/u32/etc)
// - hex or decimal ?
// - modifications
// - insert/append
// - mode
@@ -50,31 +48,47 @@ const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE;
// - replace-and-keep-going
// - mode
// - change
// - edit character panel
// - modifier on existing keys like teehee? or jump to panel?
// - if jump to panel, space?
// - visual gg/G
// - A-r replaces with ASCII
// - jumplist
// - p
// - [/] to cycle view offset?
// - gj jump to entered offset
// future directions
// - 'views' for bytes (i8/16/etc u8/16/etc 20.12/8.4/etc)
// - how to fit??! `-128` longer than `80`
fn main() {
let mut app = App::new();
let arguments = Arguments::parse();
if arguments.show_config_path {
if let Some(path) = Config::path(arguments.config) {
println!("{}", path.display());
} else {
#[cfg(unix)] {
println!("currently, no config file will be used. define the environment variable XDG_CONFIG_HOME or use the -c/--config option to provide one");
}
#[cfg(windows)] {
println!("currently, no config file will be used. use the -c/--config option to provide one");
}
}
return;
}
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();
while !app.should_quit {
terminal.draw(|frame| {
frame.render_widget(&app, frame.area());
}).unwrap();
let mut should_redraw = true;
app.handle_events(&mut terminal);
while !app.should_quit {
if should_redraw {
terminal.draw(|frame| {
frame.render_widget(&app, frame.area());
}).unwrap();
}
should_redraw = app.handle_events(&mut terminal);
}
terminal.backend_mut().queue(DisableMouseCapture).unwrap();
+66
View File
@@ -0,0 +1,66 @@
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<Span<'static>>
}
impl Popup {
pub fn new(at: usize, lines: Vec<Span<'static>>) -> Self {
Self {
at,
width: lines
.iter()
.map(|line| u16::try_from(line.width()).unwrap())
.max()
.unwrap_or(0),
primary: false,
lines
}
}
pub fn area_at(&self, x: u16, y: u16) -> Rect {
Rect {
x,
y,
width: self.width + 2,
height: u16::try_from(self.lines.len()).unwrap()
}
}
#[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(u16::try_from(line.width()).unwrap())
),
buf
);
}
}
}
+13
View File
@@ -0,0 +1,13 @@
mod cardinality;
mod empty_span;
mod custom_greys;
mod floor_to_the_nearest;
mod saturating_subtract;
mod is_overlapping;
pub use cardinality::HasCardinality;
pub use empty_span::empty_span;
pub use custom_greys::CustomGreys;
pub use floor_to_the_nearest::Floorable;
pub use saturating_subtract::SaturatingSubtract;
pub use is_overlapping::IsOverlapping;
+21
View File
@@ -0,0 +1,21 @@
use ratatui::style::Color;
pub trait CustomGreys {
fn selection_tail_grey() -> Self;
fn secondary_selection_head_grey() -> Self;
fn ui_grey() -> Self;
}
impl CustomGreys for Color {
fn selection_tail_grey() -> Self {
Self::Indexed(242)
}
fn secondary_selection_head_grey() -> Self {
Self::Indexed(246)
}
fn ui_grey() -> Self {
Self::Indexed(238)
}
}
@@ -1,6 +1,7 @@
use std::borrow::Cow;
use ratatui::{style::Style, text::Span};
// this can't just use Span::default() because it needs to be const
pub const fn empty_span() -> Span<'static> {
Span {
style: Style::new(),
+16
View File
@@ -0,0 +1,16 @@
pub trait Floorable {
fn floor_to_the_nearest(&mut self, step: Self);
fn floored_to_the_nearest(self, step: Self) -> Self;
}
impl Floorable for usize {
fn floor_to_the_nearest(&mut self, step: Self) {
*self -= *self % step;
}
fn floored_to_the_nearest(self, step: Self) -> Self {
self - (self % step)
}
}
+11
View File
@@ -0,0 +1,11 @@
use std::ops::RangeInclusive;
pub trait IsOverlapping {
fn is_overlapping(&self, other: &Self) -> bool;
}
impl IsOverlapping for RangeInclusive<usize> {
fn is_overlapping(&self, other: &Self) -> bool {
self.contains(other.start()) || self.contains(other.end())
}
}
+9
View File
@@ -0,0 +1,9 @@
pub trait SaturatingSubtract {
fn saturating_subtract(&mut self, other: Self);
}
impl SaturatingSubtract for usize {
fn saturating_subtract(&mut self, other: Self) {
*self = self.saturating_sub(other);
}
}
+17
View File
@@ -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
}
}