54 Commits

Author SHA1 Message Date
alice pellerin 64f8944e59 set OSC 6 (current document) 2026-05-28 18:28:33 -05:00
alice pellerin 9c6f273703 add brew install to readme 2026-05-23 15:22:24 -05:00
alice pellerin 01a0ab5978 click tab bar to go to buffer 2026-05-16 16:46:09 -05:00
alice pellerin 6517de6b61 allow switching buffers in visual mode 2026-05-16 16:29:57 -05:00
alice pellerin 42a83b1245 normal escape stop inspecting 2026-05-16 16:29:57 -05:00
alice pellerin 906a152a21 fix color555 to 888 conversion 2026-05-16 16:14:52 -05:00
alice pellerin b721091c3c Update README.md 2026-05-05 19:53:06 -05:00
alice pellerin a69409a36f update readme 2026-05-05 18:13:46 -05:00
alice pellerin 1f77123cb0 update Node.js-20-depending github actions 2026-05-05 15:23:14 -05:00
alice pellerin c879cdb271 improve export script, add todo.md 2026-05-05 15:18:43 -05:00
alice pellerin 596f3d5c12 add gn/gp next/previous buffer keybinds 2026-05-02 02:11:58 -05:00
alice pellerin 7b5d56dd92 update unsaved changes alert 2026-05-02 01:42:45 -05:00
alice pellerin e94c70fa9e add caching to CI 2026-05-02 00:16:48 -05:00
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
42 changed files with 3378 additions and 2480 deletions
+92
View File
@@ -0,0 +1,92 @@
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: cache
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- 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@v3
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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
@@ -67,9 +117,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
@@ -116,6 +166,81 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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]] [[package]]
name = "compact_str" name = "compact_str"
version = "0.9.0" version = "0.9.0"
@@ -132,9 +257,9 @@ dependencies = [
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.7.1" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
@@ -154,7 +279,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.11.0",
"crossterm_winapi", "crossterm_winapi",
"derive_more", "derive_more",
"document-features", "document-features",
@@ -198,9 +323,9 @@ dependencies = [
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.11" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"darling_macro", "darling_macro",
@@ -208,27 +333,26 @@ dependencies = [
[[package]] [[package]]
name = "darling_core" name = "darling_core"
version = "0.20.11" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [ dependencies = [
"fnv",
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.20.11" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
@@ -248,23 +372,24 @@ dependencies = [
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.0.1" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [ dependencies = [
"derive_more-impl", "derive_more-impl",
] ]
[[package]] [[package]]
name = "derive_more-impl" name = "derive_more-impl"
version = "2.0.1" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "rustc_version",
"syn 2.0.117",
] ]
[[package]] [[package]]
@@ -423,6 +548,12 @@ dependencies = [
"foldhash 0.2.0", "foldhash 0.2.0",
] ]
[[package]]
name = "hashbrown"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -437,8 +568,12 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hexapoda" name = "hexapoda"
version = "0.1.0" version = "0.2.3"
dependencies = [ dependencies = [
"clap",
"clap_complete",
"clap_complete_nushell",
"clap_mangen",
"crossterm", "crossterm",
"itertools", "itertools",
"ratatui", "ratatui",
@@ -460,12 +595,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.13.0" version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.1", "hashbrown 0.17.0",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -481,17 +616,23 @@ dependencies = [
[[package]] [[package]]
name = "instability" name = "instability"
version = "0.3.9" version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
dependencies = [ dependencies = [
"darling", "darling",
"indoc", "indoc",
"proc-macro2", "proc-macro2",
"quote", "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]] [[package]]
name = "itertools" name = "itertools"
version = "0.14.0" version = "0.14.0"
@@ -503,15 +644,15 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.91" version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -548,24 +689,24 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.177" version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]] [[package]]
name = "line-clipping" name = "line-clipping"
version = "0.3.5" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.11.0",
] ]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.11.0" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litrs" name = "litrs"
@@ -584,9 +725,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.28" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -636,9 +777,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.1.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@@ -652,7 +793,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.11.0",
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"libc", "libc",
@@ -671,9 +812,9 @@ dependencies = [
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]] [[package]]
name = "num-derive" name = "num-derive"
@@ -683,7 +824,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
@@ -710,6 +851,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "ordered-float" name = "ordered-float"
version = "4.6.0" version = "4.6.0"
@@ -772,7 +919,7 @@ dependencies = [
"pest_meta", "pest_meta",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
@@ -825,7 +972,7 @@ dependencies = [
"phf_shared", "phf_shared",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
@@ -856,23 +1003,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.103" version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.42" version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -924,7 +1071,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.11.0",
"compact_str", "compact_str",
"hashbrown 0.16.1", "hashbrown 0.16.1",
"indoc", "indoc",
@@ -976,7 +1123,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.11.0",
"hashbrown 0.16.1", "hashbrown 0.16.1",
"indoc", "indoc",
"instability", "instability",
@@ -995,7 +1142,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.11.0",
] ]
[[package]] [[package]]
@@ -1028,12 +1175,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "rustix" name = "roff"
version = "1.1.2" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ 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", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@@ -1048,9 +1210,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.20" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
@@ -1060,9 +1222,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.27" version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]] [[package]]
name = "serde" name = "serde"
@@ -1091,7 +1253,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
@@ -1150,10 +1312,11 @@ dependencies = [
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.6" version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [ dependencies = [
"errno",
"libc", "libc",
] ]
@@ -1199,7 +1362,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
@@ -1215,9 +1378,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.110" version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1253,7 +1416,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"bitflags 2.10.0", "bitflags 2.11.0",
"fancy-regex", "fancy-regex",
"filedescriptor", "filedescriptor",
"finl_unicode", "finl_unicode",
@@ -1313,7 +1476,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
@@ -1324,7 +1487,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
] ]
[[package]] [[package]]
@@ -1401,15 +1564,15 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.22" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.12.0" version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]] [[package]]
name = "unicode-truncate" name = "unicode-truncate"
@@ -1442,9 +1605,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.22.0" version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [ dependencies = [
"atomic", "atomic",
"getrandom 0.4.2", "getrandom 0.4.2",
@@ -1493,9 +1656,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.114" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -1506,9 +1669,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.114" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1516,22 +1679,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.114" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.114" version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1564,7 +1727,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.11.0",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"indexmap", "indexmap",
"semver", "semver",
@@ -1715,7 +1878,7 @@ dependencies = [
"heck", "heck",
"indexmap", "indexmap",
"prettyplease", "prettyplease",
"syn 2.0.110", "syn 2.0.117",
"wasm-metadata", "wasm-metadata",
"wit-bindgen-core", "wit-bindgen-core",
"wit-component", "wit-component",
@@ -1731,7 +1894,7 @@ dependencies = [
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.117",
"wit-bindgen-core", "wit-bindgen-core",
"wit-bindgen-rust", "wit-bindgen-rust",
] ]
@@ -1743,7 +1906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.10.0", "bitflags 2.11.0",
"indexmap", "indexmap",
"log", "log",
"serde", "serde",
+34 -1
View File
@@ -1,16 +1,49 @@
[package] [package]
name = "hexapoda" name = "hexapoda"
version = "0.1.0" # if run manually, CI will check for the string "ain" (lol), so here you go :)
version = "0.2.3"
description = "a colorful modal hex editor" description = "a colorful modal hex editor"
repository = "https://github.com/simonomi/hexapoda" repository = "https://github.com/simonomi/hexapoda"
keywords = ["cli", "tui", "hex", "tool", "editor"] keywords = ["cli", "tui", "hex", "tool", "editor"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
edition = "2024" edition = "2024"
build = "build.rs"
[dependencies] [dependencies]
clap = { version = "4.6.0", features = ["derive"] }
crossterm = { version = "0.29.0", features = ["serde"] } crossterm = { version = "0.29.0", features = ["serde"] }
itertools = "0.14.0" itertools = "0.14.0"
ratatui = "0.30.0" ratatui = "0.30.0"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
toml = "1.1.2" 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"
+8 -19
View File
@@ -1,31 +1,20 @@
# hexapoda # <img height=40 align=top src="https://github.com/simonomi/hexapoda/blob/main/icon/bug%20colored%20large.png?raw=true"> [hexapoda](https://simonomi.dev/hexapoda)
a colorful modal hex editor a colorful modal hex editor
(the name comes from [the subphylum](https://en.wikipedia.org/wiki/Hexapoda)) (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 ## status
currently, hexapoda is very unpolished, and missing some major features. if you'd be interested in using it, please let me know! if enough people want, i'd be willing to make it more accessible and write some docs still missing some notable features (see [todo.md](https://github.com/simonomi/hexapoda/blob/main/todo.md)), but ready for general use. visit [the website](https://simonomi.dev/hexapoda) for more detailed documentation
## features ## installation
- [color-codes bytes](https://simonomi.dev/blog/color-code-your-bytes) by value - short answer: `brew install hexapoda`, `cargo binstall hexapoda`, or `cargo install hexapoda`
- modal editing - [slightly longer answer](https://simonomi.dev/hexapoda/install)
- selection-first, like [Kakoune](https://kakoune.org) and [Helix](https://helix-editor.com)
- multiple selections
- split selection(s) into #-byte chunks
- undo/redo
- inspect the current selection(s)
- signed, unsigned, fixed-point, UTF-8, color
- mark notable offsets
- jump to selected offset
### notable features that are missing (for now) ## config
- search see [https://simonomi.dev/hexapoda/config](https://simonomi.dev/hexapoda/config)
- diffing
- inserting bytes
- only replacing and deleting right now
+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(())
}
+77
View File
@@ -0,0 +1,77 @@
#!/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 cargoTOMLPath = URL(filePath: "Cargo.toml")
let cargoTOML = try String(contentsOf: cargoTOMLPath, encoding: .utf8)
let versionNumber = cargoTOML.matches(of: #/version = "(?'number'[\d\.]+)"/#).first!.output.number
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 output = """
{%- highlight toml -%}
#:schema https://simonomi.dev/hexapoda/config/schema-v\(versionNumber).json
"""
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") {
output += "[\(mode!)]\n"
} else if let match = line.wholeMatch(of: #/.*PartialAction::(?'partialAction'\w*).*/#) {
let partialAction = match.output.partialAction.lowercased()
output += "[\(mode!).\(partialAction)]\n"
} else if let match = line.wholeMatch(of: #/.*\(keypress\("(?'keypress'.*?)"\), (?'action'.*?)\.into\(\)\).*/#) {
if match.output.keypress.contains(where: { !($0.isLetter || $0.isNumber) }) {
output += "\"\(match.output.keypress)\" = \"\(camelCaseToSnakeCase(match.output.action))\"\n"
} else {
output += "\(match.output.keypress) = \"\(camelCaseToSnakeCase(match.output.action))\"\n"
}
} else {
output += "\n"
}
}
output += "{%- endhighlight -%}\n"
let outputPath = URL(filePath: "~/Documents/programming/websites/simonomi.dev/_includes/hexapoda/hexapoda v\(versionNumber).toml")
try Data(output.utf8).write(to: outputPath)
print("wrote config to \(outputPath.path(percentEncoded: false))")
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.
+74 -953
View File
File diff suppressed because it is too large Load Diff
+117 -111
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 crossterm::{ExecutableCommand, event::{self, DisableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}, terminal::window_size};
use ratatui::{DefaultTerminal, style::Stylize, text::Span}; 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; mod widget;
mod actions;
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";
pub struct App { pub struct App {
pub config: Config, pub config: Config,
@@ -20,19 +19,18 @@ pub struct App {
pub should_quit: bool, pub should_quit: bool,
pub is_dragging_mouse: bool,
pub logs: Vec<String>, pub logs: Vec<String>,
} }
#[derive(Clone, Copy)]
pub struct WindowSize {
pub rows: usize,
pub covered_rows: usize,
}
impl App { impl App {
pub fn new() -> Self { pub fn new(
config_path: Option<PathBuf>,
files: &[PathBuf],
) -> Self {
let config = { let config = {
let config = Config::init(); let config = Config::init(config_path);
match &config { match &config {
Err(ConfigInitError::IO(io_error)) if io_error.kind() != io::ErrorKind::NotFound => { Err(ConfigInitError::IO(io_error)) if io_error.kind() != io::ErrorKind::NotFound => {
@@ -56,11 +54,10 @@ impl App {
let mut error_alert: Option<Span> = None; let mut error_alert: Option<Span> = None;
let mut buffers: Vec<Buffer> = env::args() let mut buffers: Vec<Buffer> = files
.skip(1) .iter()
.map(Into::into) .filter_map(|path| {
.filter_map(|path: PathBuf| { Buffer::from_file_at(path)
Buffer::from_file_at(path.clone())
.inspect_err(|error| { .inspect_err(|error| {
error_alert = Some( error_alert = Some(
Span::raw(format!("error reading '{}': {error}", path.display())).red() Span::raw(format!("error reading '{}': {error}", path.display())).red()
@@ -70,9 +67,9 @@ impl App {
}) })
.collect(); .collect();
if env::args().len() <= 1 { if files.is_empty() {
#[cfg(target_os = "macos")] { #[cfg(target_os = "macos")] {
eprintln!("{MACOS_STDIN_BROKEN_MESSAGE}"); eprintln!("please provide at least one file as input. use --help for options");
exit(1); exit(1);
} }
@@ -116,19 +113,23 @@ impl App {
should_quit: false, should_quit: false,
is_dragging_mouse: false,
logs: Vec::new(), logs: Vec::new(),
} }
} }
pub fn handle_events(&mut self, terminal: &mut DefaultTerminal) { pub fn handle_events(&mut self, terminal: &mut DefaultTerminal) -> bool {
self.handle_event(terminal); let mut should_redraw = self.handle_event(terminal);
while event::poll(Duration::ZERO).unwrap() { 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() let event = event::read()
.inspect_err(|error| { .inspect_err(|error| {
#[cfg(target_os = "macos")] { #[cfg(target_os = "macos")] {
@@ -137,7 +138,7 @@ impl App {
if error.kind() == ErrorKind::Other { if error.kind() == ErrorKind::Other {
let error_message = error.to_string(); let error_message = error.to_string();
if error_message == "Failed to initialize input reader" { 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); exit(1);
} }
} }
@@ -145,20 +146,24 @@ impl App {
}) })
.unwrap(); .unwrap();
// self.logs.push(format!("{event:?}"));
match event { match event {
Event::Resize(_, height) => { Event::Resize(_, height) => {
self.window_size.rows = height as usize; self.window_size.rows = height as usize;
self.buffers[self.current_buffer_index] self.buffers[self.current_buffer_index]
.clamp_screen_to_primary_cursor(self.window_size); .clamp_screen_to_primary_cursor(self.window_size);
true
} }
Event::Key(key_event) => self.handle_key(key_event, terminal), Event::Key(key_event) => self.handle_key(key_event, terminal),
Event::Mouse(mouse_event) => self.handle_mouse(mouse_event), 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 && if key_event.modifiers == KeyModifiers::CONTROL &&
key_event.code == KeyCode::Char('c') key_event.code == KeyCode::Char('c')
{ {
@@ -168,7 +173,7 @@ impl App {
exit(130); 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, key_event,
&self.config, &self.config,
&self.primary_cursor_register, &self.primary_cursor_register,
@@ -187,116 +192,117 @@ impl App {
AppAction::Yank => self.yank(), AppAction::Yank => self.yank(),
} }
} }
should_redraw || maybe_app_action.is_some()
} }
fn handle_mouse(&mut self, mouse_event: MouseEvent) { fn handle_mouse(&mut self, mouse_event: MouseEvent) -> bool {
let tab_bar_rows = usize::from(self.buffers.len() > 1); let position = self.mouse_event_position(mouse_event);
let tab_bar_row_count = u16::from(self.buffers.len() > 1);
let current_buffer = &mut self.buffers[self.current_buffer_index]; let current_buffer = &mut self.buffers[self.current_buffer_index];
match mouse_event.kind { match mouse_event.kind {
MouseEventKind::Down(_) => { MouseEventKind::Down(_) => {
let byte_column = match mouse_event.column { if let Some(position) = position {
10..=11 => Some(0), current_buffer.primary_cursor = Cursor::at(position);
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
);
current_buffer.cursors.clear(); current_buffer.cursors.clear();
current_buffer.clamp_screen_to_primary_cursor(self.window_size);
self.is_dragging_mouse = true;
} else if mouse_event.row < tab_bar_row_count {
self.switch_to_tab_at(mouse_event.column);
} }
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 => { MouseEventKind::ScrollDown => {
for _ in 0..3 { for _ in 0..3 {
current_buffer.scroll_down(self.window_size); current_buffer.scroll_down(self.window_size);
} }
true
}, },
MouseEventKind::ScrollUp => { MouseEventKind::ScrollUp => {
for _ in 0..3 { for _ in 0..3 {
current_buffer.scroll_up(self.window_size); current_buffer.scroll_up(self.window_size);
} }
true
}, },
_ => (), _ => false,
} }
} }
fn quit_if_saved(&mut self) { fn mouse_event_position(&self, mouse_event: MouseEvent) -> Option<usize> {
if self.buffers.iter().all(Buffer::all_changes_saved) { let tab_bar_row_count = usize::from(self.buffers.len() > 1);
self.quit();
} else { if usize::from(mouse_event.row) < tab_bar_row_count ||
self.buffers[self.current_buffer_index].alert_message = Span::from( usize::from(mouse_event.row) - tab_bar_row_count >= self.window_size.hex_rows() {
"there are unsaved changes, use Q to override" return None;
).red();
} }
}
const fn quit(&mut self) {
self.should_quit = true;
}
const fn previous_buffer(&mut self) {
if self.current_buffer_index == 0 {
self.current_buffer_index = self.buffers.len() - 1;
} else {
self.current_buffer_index -= 1;
}
}
const fn next_buffer(&mut self) {
if self.current_buffer_index == self.buffers.len() - 1 {
self.current_buffer_index = 0;
} else {
self.current_buffer_index += 1;
}
}
fn yank(&mut self) {
let current_buffer = &self.buffers[self.current_buffer_index]; let current_buffer = &self.buffers[self.current_buffer_index];
self.primary_cursor_register = current_buffer let byte_column = match mouse_event.column {
.contents[current_buffer.primary_cursor.range()] 10..=11 => Some(0),
.to_vec(); 13..=14 => Some(1),
16..=17 => Some(2),
19..=20 => Some(3),
self.other_cursor_registers = current_buffer.cursors 23..=24 => Some(4),
.iter() 26..=27 => Some(5),
.map(|cursor| { 29..=30 => Some(6),
current_buffer.contents[cursor.range()].to_vec() 32..=33 => Some(7),
})
.collect(); 36..=37 => Some(8),
} 39..=40 => Some(9),
} 42..=43 => Some(10),
45..=46 => Some(11),
impl WindowSize {
pub const fn visible_byte_count(&self) -> usize { 49..=50 => Some(12),
self.hex_rows() * BYTES_PER_LINE 52..=53 => Some(13),
} 55..=56 => Some(14),
58..=59 => Some(15),
pub const fn hex_rows(&self) -> usize {
self.rows - self.covered_rows _ => None,
};
byte_column.map(|byte_column| {
current_buffer.scroll_position +
(mouse_event.row as usize - tab_bar_row_count) * BYTES_PER_LINE +
byte_column
})
}
fn switch_to_tab_at(&mut self, column: u16) {
let mut column: usize = column.into();
for buffer_index in 0..self.buffers.len() {
let tab_width = self.buffers[buffer_index].file_name.len() + 2;
if column < tab_width {
self.current_buffer_index = buffer_index;
return;
}
column -= tab_width;
}
} }
} }
+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(
"unsaved changes, use <space>w to save or 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 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 { impl Widget for &App {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) { 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> { fn tab_for(buffer: &Buffer, is_active: bool) -> Span<'static> {
let background = if is_active { let background = if is_active {
Color::select_grey() Color::selection_tail_grey()
} else { } else {
Color::ui_grey() 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,
}
+51 -117
View File
@@ -1,11 +1,11 @@
use core::slice::GetDisjointMutIndex; use std::{collections::HashSet, fs::File, io::{self, Read}, path::{Path, PathBuf}};
use std::{collections::HashSet, fs::File, io::{self, Read}, path::PathBuf};
use crossterm::event::KeyEvent; 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 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 widget;
mod actions;
pub struct Buffer { pub struct Buffer {
pub file_name: String, pub file_name: String,
@@ -41,22 +41,14 @@ pub struct Buffer {
#[derive(Debug)] #[derive(Debug)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Mode { pub enum Mode {
Normal, Select, Insert Normal, Select, // Insert
} }
#[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug)] #[derive(Debug)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum PartialAction { pub enum PartialAction {
Goto, View, Replace, Space, Repeat, To Goto, View, Replace, Space, Repeat, Till
}
#[derive(Clone)]
pub struct Popup {
pub at: usize,
width: u16,
primary: bool,
lines: Vec<Span<'static>>
} }
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
@@ -64,39 +56,6 @@ pub enum InspectionStatus {
Normal, ColorsOnly 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 { impl TryFrom<&str> for PartialAction {
type Error = (); type Error = ();
@@ -109,69 +68,16 @@ impl TryFrom<&str> for PartialAction {
"replace" => Ok(Replace), "replace" => Ok(Replace),
"space" => Ok(Space), "space" => Ok(Space),
"repeat" => Ok(Repeat), "repeat" => Ok(Repeat),
"to" => Ok(To), "till" => Ok(Till),
_ => Err(()), _ => 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 { impl Buffer {
pub fn from_file_at(file_path: PathBuf) -> io::Result<Self> { pub fn from_file_at(file_path: &Path) -> io::Result<Self> {
let file_path = file_path.canonicalize()?;
let mut file = File::open(&file_path)?; let mut file = File::open(&file_path)?;
let mut contents = Vec::new(); let mut contents = Vec::new();
file.read_to_end(&mut contents)?; file.read_to_end(&mut contents)?;
@@ -216,13 +122,15 @@ impl Buffer {
primary_cursor_register: &[u8], primary_cursor_register: &[u8],
other_cursor_registers: &[Vec<u8>], other_cursor_registers: &[Vec<u8>],
window_size: WindowSize window_size: WindowSize
) -> Option<AppAction> { ) -> (Option<AppAction>, bool) {
let mut should_redraw = !self.alert_message.content.is_empty();
self.alert_message = "".into(); self.alert_message = "".into();
// self.logs.push(format!("{event:?}")); // self.logs.push(format!("{event:?}"));
let app_action = match self.partial_action { let app_action = match self.partial_action {
Some(PartialAction::Replace) => { Some(PartialAction::Replace) => {
self.handle_replace(event, window_size); self.handle_replace(event, window_size);
should_redraw = true;
None None
}, },
Some(PartialAction::Repeat) => { Some(PartialAction::Repeat) => {
@@ -233,19 +141,28 @@ impl Buffer {
other_cursor_registers, other_cursor_registers,
window_size window_size
); );
should_redraw = true;
None 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.is_multiple_of(BYTES_PER_LINE));
assert!(self.scroll_position < self.contents.len()); if !self.contents.is_empty() {
assert!(self.primary_cursor.head < self.contents.len()); assert!(self.scroll_position < self.contents.len());
assert!(self.primary_cursor.tail < 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.scroll_position <= self.primary_cursor.head);
assert!(self.primary_cursor.head < self.scroll_position + window_size.visible_byte_count()); 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) { fn handle_replace(&mut self, event: KeyEvent, window_size: WindowSize) {
@@ -281,17 +198,20 @@ impl Buffer {
event: KeyEvent, event: KeyEvent,
config: &Config, config: &Config,
window_size: WindowSize window_size: WindowSize
) -> Option<AppAction> { ) -> (Option<AppAction>, bool) {
use Action::*; use Action::*;
let mut result = None; let mut result = None;
let should_reset_partial = self.partial_action.is_some(); 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) && if let Some(mode_config) = config.0.get(&self.mode) &&
let Some(keybinds) = mode_config.0.get(&self.partial_action) && let Some(keybinds) = mode_config.0.get(&self.partial_action) &&
let Some(action) = keybinds.0.get(&event.into()) let Some(action) = keybinds.0.get(&event.into())
{ {
should_redraw = true;
if action.clears_popups() { if action.clears_popups() {
self.popups.clear(); self.popups.clear();
} }
@@ -323,7 +243,7 @@ impl Buffer {
self.partial_action = None; self.partial_action = None;
} }
result (result, should_redraw)
} }
fn handle_repeat( fn handle_repeat(
@@ -375,7 +295,11 @@ impl Buffer {
self.combine_cursors_if_overlapping(); self.combine_cursors_if_overlapping();
self.clamp_screen_to_primary_cursor(window_size); 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 +328,20 @@ impl Buffer {
pub fn combine_cursors_if_overlapping(&mut self) { pub fn combine_cursors_if_overlapping(&mut self) {
let mut index = 0; 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 !self.cursors.is_empty() && index < self.cursors.len() {
while index < self.cursors.len() - 1 && while index < self.cursors.len() - 1 &&
self.cursors[index].range().is_overlapping( self.cursors[index].range().is_overlapping(
&self.cursors[index + 1].range()) &self.cursors[index + 1].range()
)
{ {
let next_cursor = self.cursors[index + 1]; let next_cursor = self.cursors[index + 1];
self.cursors[index].combine_with(next_cursor); self.cursors[index].combine_with(next_cursor);
@@ -419,9 +353,9 @@ impl Buffer {
{ {
self.primary_cursor.combine_with(self.cursors[index]); self.primary_cursor.combine_with(self.cursors[index]);
self.cursors.remove(index); self.cursors.remove(index);
} else {
index += 1;
} }
index += 1;
} }
} }
} }
@@ -451,7 +385,7 @@ mod tests {
#[test] #[test]
fn nybble_from_hex_digits_are_correct() { fn nybble_from_hex_digits_are_correct() {
for (index, character) in ('0'..='9').enumerate() { 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()));
} }
} }
} }
+960
View File
@@ -0,0 +1,960 @@
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(),
BufferAction::StopInspecting => {},
}
}
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,
}
}
fn color555_to_color888(color555: u16) -> [u8; 3] {
[
(u8::try_from((color555 & 0b11111) * 255 / 31).unwrap()),
(u8::try_from((color555 >> 5 & 0b11111) * 255 / 31).unwrap()),
(u8::try_from((color555 >> 10 & 0b11111) * 255 / 31).unwrap())
]
}
+12 -402
View File
@@ -2,8 +2,17 @@ use std::{cmp::min, iter};
use ratatui::{layout::Rect, text::{Line, Text}, widgets::Widget}; use ratatui::{layout::Rect, text::{Line, Text}, widgets::Widget};
use crate::{BYTES_PER_LINE, buffer::Buffer}; use crate::{BYTES_PER_LINE, buffer::Buffer};
mod address;
mod hex;
mod character_panel;
mod status_line;
mod extra_statuses;
impl Widget for &Buffer { impl Widget for &Buffer {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) { fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
// set OSC 6 (current document)
print!("\x1B]6;{}\x07", self.file_path.display());
let screen_end = self.scroll_position + BYTES_PER_LINE * (area.height as usize - 1); let screen_end = self.scroll_position + BYTES_PER_LINE * (area.height as usize - 1);
let bytes_end = min(screen_end, self.contents.len()); let bytes_end = min(screen_end, self.contents.len());
@@ -55,8 +64,8 @@ impl Widget for &Buffer {
let popup_area = popup let popup_area = popup
.area_at( .area_at(
area.x + byte_column_to_screen_column(hex_column) as u16, area.x + byte_column_to_screen_column(hex_column),
area.y + (position_on_screen / BYTES_PER_LINE) as u16 + 1 area.y + u16::try_from(position_on_screen / BYTES_PER_LINE).unwrap() + 1
) )
.clamp(hex_area); .clamp(hex_area);
@@ -102,406 +111,7 @@ impl Buffer {
} }
} }
mod address { fn byte_column_to_screen_column(byte_column: usize) -> u16 {
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 {
match byte_column { match byte_column {
0 => 10, 0 => 10,
1 => 13, 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]
}
+52
View File
@@ -0,0 +1,52 @@
use crate::{buffer::{Buffer, Mode}, utilities::CustomGreys};
use ratatui::{style::{Color, Stylize}, text::{Line, Span}};
impl Buffer {
pub fn render_status_line(&self) -> Line<'_> {
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 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 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}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de::{Error, MapAccess, Unexpected, Visitor}, ser::SerializeMap};
mod default;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct Config( pub struct Config(
@@ -32,7 +34,7 @@ pub struct Keypress {
impl Config { impl Config {
#[cfg(unix)] #[cfg(unix)]
fn path() -> Option<PathBuf> { fn default_path() -> Option<PathBuf> {
env::var_os("XDG_CONFIG_HOME") env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from) .map(PathBuf::from)
.take_if(|xdg_config_home| xdg_config_home.is_absolute()) .take_if(|xdg_config_home| xdg_config_home.is_absolute())
@@ -41,16 +43,36 @@ impl Config {
} }
#[cfg(windows)] #[cfg(windows)]
fn path() -> Option<PathBuf> { fn default_path() -> Option<PathBuf> {
// this isn't technically the right way but it should be good enough // 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> { pub fn path(override_path: Option<PathBuf>) -> Option<PathBuf> {
let path = Self::path().ok_or(ConfigInitError::NoConfigPath)?; 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)?; 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 { impl Serialize for ModeConfig {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?; 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()
}
}
+300
View File
@@ -0,0 +1,300 @@
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()),
(keypress("escape"), StopInspecting.into()),
].into()),
(Some(PartialAction::Goto), [
(keypress("j"), GotoLineStart.into()),
(keypress("l"), GotoLineEnd.into()),
(keypress("g"), GotoFileStart.into()),
(keypress("i"), GotoFileStart.into()),
(keypress("k"), GotoFileEnd.into()),
(keypress("p"), PreviousBuffer.into()),
(keypress("n"), NextBuffer.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("escape"), 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-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"), ExtendLineStart.into()),
(keypress("l"), ExtendLineEnd.into()),
(keypress("g"), ExtendFileStart.into()),
(keypress("i"), ExtendFileStart.into()),
(keypress("k"), ExtendFileEnd.into()),
(keypress("p"), PreviousBuffer.into()),
(keypress("n"), NextBuffer.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 std::{cmp::{max, min}, mem::swap, ops::RangeInclusive};
use crate::{BYTES_PER_LINE, action::CursorAction};
mod actions;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Cursor { pub struct Cursor {
@@ -58,12 +59,9 @@ impl Cursor {
swap(&mut self.head, &mut self.tail); swap(&mut self.head, &mut self.tail);
} }
// TODO: in visual mode, should only clamp head pub fn clamp(&mut self, min: usize, max: usize) {
pub fn clamp(&mut self, scroll_position: usize, screen_size: usize) { self.head = self.head.clamp(min, max);
let max_row = scroll_position + screen_size - 1; self.tail = self.tail.clamp(min, max);
self.head = self.head.clamp(scroll_position, max_row);
self.tail = self.tail.clamp(scroll_position, max_row);
} }
pub fn combine_with(&mut self, other: Self) { pub fn combine_with(&mut self, other: Self) {
@@ -75,443 +73,4 @@ impl Cursor {
self.tail = min(self.tail, other.tail); 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 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)] #[derive(Debug)]
pub enum EditAction { pub enum EditAction {
+37 -51
View File
@@ -1,24 +1,22 @@
#![warn(clippy::pedantic, clippy::nursery)] #![warn(clippy::pedantic, clippy::nursery)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::enum_glob_use)] #![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 app::App;
use crossterm::{QueueableCommand, event::{DisableMouseCapture, EnableMouseCapture}}; use crossterm::{QueueableCommand, event::{DisableMouseCapture, EnableMouseCapture}};
use crate::config::Config;
mod app; mod app;
mod buffer; mod buffer;
mod popup;
mod config; mod config;
mod cursor; mod cursor;
mod action; mod action;
mod edit_action; mod edit_action;
mod arguments;
mod cardinality; mod window_size;
mod empty_span; mod utilities;
mod custom_greys;
const BYTES_PER_LINE: usize = 0x10; const BYTES_PER_LINE: usize = 0x10;
const BYTES_PER_CHUNK: usize = 4; const BYTES_PER_CHUNK: usize = 4;
@@ -27,54 +25,42 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK;
const LINES_OF_PADDING: usize = 5; const LINES_OF_PADDING: usize = 5;
const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE; 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
// - search
// - ascii and bytes (`/` and `A-/`?)
// - diffing
// - doesn't have to be anything fancy, just compare each byte 1:1
// - s/A-k/A-K
// - sm select marks
// - C-a/C-x
// - modifications
// - insert/append
// - mode
// - add to edit history when *leaving* insert mode
// - 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
// - 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() { 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(); let mut terminal = ratatui::init();
crossterm::terminal::enable_raw_mode().unwrap(); crossterm::terminal::enable_raw_mode().unwrap();
terminal.backend_mut().queue(EnableMouseCapture).unwrap(); terminal.backend_mut().queue(EnableMouseCapture).unwrap();
while !app.should_quit { let mut should_redraw = true;
terminal.draw(|frame| {
frame.render_widget(&app, frame.area());
}).unwrap();
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(); 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 std::borrow::Cow;
use ratatui::{style::Style, text::Span}; 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> { pub const fn empty_span() -> Span<'static> {
Span { Span {
style: Style::new(), 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
}
}
+31
View File
@@ -0,0 +1,31 @@
# todo
- v1.0
- `T` Till
- `go` goto entered offset
- search
- `/` hex, `A-/` ASCII
- if non-hex-digit typed, search ASCII
- `A-*` repeat (entered number) times
- copy/paste (`<space>y`/`<space>p`)
- inspector translations for varint [#1](https://github.com/simonomi/hexapoda/issues/1#issue-4232822634)
- `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
- add to edit history when *leaving* insert mode
- replace-and-keep-going
- mode
- change (basically `dh`)
- `p` put
- `A-r` replaces with ASCII
- jumplist (`C-o`/`C-i`)
- `[`/`]` to cycle view offset?