├── .gitignore ├── rustfmt.toml ├── clippy.toml ├── demo.png ├── src ├── lib.rs ├── chunk.rs ├── windows │ ├── jump_to_byte.rs │ ├── unsaved_changes.rs │ ├── mod.rs │ ├── search.rs │ └── editor.rs ├── character.rs ├── main.rs ├── decoder.rs ├── buffer.rs ├── label.rs ├── app.rs ├── input.rs └── screen.rs ├── .github ├── dependabot.yml ├── codecov.yml ├── workflows │ ├── release.yml │ ├── check.yml │ └── test.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── Cargo.toml ├── examples └── demo.rs ├── LICENSE ├── CHANGELOG.md ├── README.md ├── CONTRIBUTING.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | .vscode -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | doc-valid-idents = ["HEx", "GHex", "CNTRLq", ".."] -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndd7xv/heh/HEAD/demo.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | mod buffer; 3 | mod character; 4 | mod chunk; 5 | pub mod decoder; 6 | pub mod input; 7 | pub mod label; 8 | pub mod screen; 9 | pub mod windows; 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: cargo 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | # Should be fixed with https://github.com/ndd7xv/heh/issues/48 3 | range: 85..100 4 | round: down 5 | precision: 1 6 | status: 7 | project: 8 | default: 9 | threshold: 1% 10 | 11 | ignore: 12 | - "tests" 13 | 14 | comment: 15 | layout: "files" 16 | require_changes: yes 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | name: Publish on crates.io 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Install toolchain 16 | uses: dtolnay/rust-toolchain@stable 17 | - name: Publish 18 | run: cargo publish --locked --token ${{ secrets.CARGO_REGISTRY_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an unexpected problem when using heh 4 | title: "[BUG] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. iOS] 24 | - Version [e.g. 22] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | Are there any potential alternatives? Provide a clear and concise description of any solutions or feature designs you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/chunk.rs: -------------------------------------------------------------------------------- 1 | /// Split a slice into overlapping chunks where each chunk is of size `size + overlap`. 2 | pub(crate) struct OverlappingChunks<'a, T> { 3 | slice: &'a [T], 4 | cursor: usize, 5 | size: usize, 6 | overlap: usize, 7 | } 8 | 9 | impl<'a, T> OverlappingChunks<'a, T> { 10 | pub(crate) fn new(slice: &'a [T], size: usize, overlap: usize) -> Self { 11 | Self { slice, cursor: 0, size, overlap } 12 | } 13 | } 14 | 15 | impl<'a, T> Iterator for OverlappingChunks<'a, T> { 16 | type Item = &'a [T]; 17 | 18 | fn next(&mut self) -> Option { 19 | let offset = self.cursor; 20 | if offset < self.slice.len() { 21 | self.cursor += self.size; 22 | Some(&self.slice[offset..self.slice.len().min(offset + self.size + self.overlap)]) 23 | } else { 24 | None 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "heh" 3 | version = "0.6.2" 4 | edition = "2024" 5 | description = "A cross-platform terminal UI used for modifying file data in hex or ASCII." 6 | readme = "README.md" 7 | repository = "https://github.com/ndd7xv/heh" 8 | license = "MIT" 9 | categories = ["command-line-utilities", "text-editors"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | ratatui = "0.29.0" 15 | clap = { version = "4.5.40", features = ["derive"] } 16 | arboard = { version = "3.6.0", default-features = false } 17 | memmap2 = "0.9.7" 18 | crossbeam = "0.8.4" 19 | hex = "0.4.3" 20 | 21 | [profile.dev] 22 | opt-level = 1 # Default would excessively lag 23 | 24 | # cargo +nightly build -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target x86_64-unknown-linux-gnu --release 25 | [profile.release] 26 | strip = true 27 | lto = true 28 | codegen-units = 1 29 | panic = "abort" 30 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use heh::app::Application as Heh; 4 | use heh::decoder::Encoding; 5 | 6 | use ratatui::{ 7 | Frame, 8 | crossterm::event::{self, Event, KeyCode}, 9 | }; 10 | 11 | fn main() { 12 | let path = PathBuf::from("Cargo.toml"); 13 | let file = std::fs::OpenOptions::new().read(true).write(true).open(path).unwrap(); 14 | let mut heh = Heh::new(file, Encoding::Ascii, 0).unwrap(); 15 | 16 | let mut terminal = ratatui::init(); 17 | loop { 18 | terminal 19 | .draw(|frame: &mut Frame| { 20 | heh.render_frame(frame, frame.area()); 21 | }) 22 | .expect("failed to draw frame"); 23 | if let Event::Key(key) = event::read().expect("failed to read event") { 24 | if key.code == KeyCode::Char('q') { 25 | break; 26 | } 27 | heh.handle_input(&ratatui::crossterm::event::Event::Key(key)).unwrap(); 28 | } 29 | } 30 | ratatui::restore(); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | name: check 11 | jobs: 12 | check: 13 | name: ${{ matrix.platform }} / check 14 | strategy: 15 | matrix: 16 | platform: [ubuntu-latest, macos-latest, windows-latest] 17 | toolchain: [stable] 18 | runs-on: ${{ matrix.platform }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: dtolnay/rust-toolchain@master 22 | with: 23 | toolchain: ${{ matrix.toolchain }} 24 | - name: Check 25 | run: cargo check 26 | fmt: 27 | runs-on: ubuntu-latest 28 | name: stable / fmt 29 | steps: 30 | - uses: actions/checkout@v4 31 | with: 32 | submodules: true 33 | - name: Install Stable 34 | uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: rustfmt 37 | - name: Format 38 | run: cargo fmt --all -- --check 39 | clippy: 40 | runs-on: ubuntu-latest 41 | name: stable / clippy 42 | permissions: 43 | contents: read 44 | checks: write 45 | strategy: 46 | fail-fast: false 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | submodules: true 51 | - name: Install Stable 52 | uses: dtolnay/rust-toolchain@master 53 | with: 54 | toolchain: stable 55 | components: clippy 56 | - name: Lint 57 | uses: actions-rs/clippy-check@v1 58 | with: 59 | token: ${{ secrets.GITHUB_TOKEN }} 60 | args: --all-targets --all-features -- -W clippy::pedantic -D warnings 61 | doc: 62 | runs-on: ubuntu-latest 63 | name: nightly / doc 64 | steps: 65 | - uses: actions/checkout@v4 66 | with: 67 | submodules: true 68 | - name: Install Nightly 69 | uses: dtolnay/rust-toolchain@nightly 70 | - name: cargo doc 71 | run: cargo doc --no-deps --all-features 72 | env: 73 | RUSTDOCFLAGS: --cfg docsrs 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project _somewhat_ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.6.2] - 2025-12-11 11 | 12 | - Add demo example and update documentation 13 | - Fix compiler warnings & Clippy warnings 14 | - Bump ratatui from 0.28.1 to 0.29.0 15 | 16 | ## [0.6.1] - 2024-08-12 17 | 18 | - Bump ratatui from 0.27.0 to 0.28.0 19 | 20 | ## [0.6.0] - 2024-07-28 21 | 22 | - Support searching by hex 23 | - Remove 0x searching functionality 24 | - Bump ratatui from 0.26.1 to 0.26.2 25 | 26 | ## [0.5.0] - 2023-04-08 27 | 28 | - Fixed a bug where '--version' did not print `heh`'s version (thanks for the report, [@davidak](https://github.com/davidak)) 29 | - Documentation updates with regards contributing, the state of the repo, and typos 30 | - Distros section for README added (thanks [@orhun](https://github.com/orhun)!) 31 | - `heh` can now be used as a library!!! (BIG thanks [@orhun](https://github.com/orhun)!) 32 | 33 | ## [0.4.1] - 2023-07-31 34 | 35 | - Fix windows double input (thanks [@programingjd](https://github.com/programingjd)!) 36 | - Re-added a saved changed warning when backspacing/deleting bytes 37 | 38 | ## [0.4.0] - 2023-07-25 39 | 40 | ### Added 41 | 42 | - This CHANGELOG file. This may be retroactively modified solely include changes earlier versions. 43 | - You can now switch the endianness displayed on the labels with `control e` (thanks, [@JungleTryne](https://github.com/JungleTryne))! 44 | - You can now open big files (thanks, [@Programatic](https://github.com/Programatic))! Note that this isn't a fully completed and perfected feature, and some things may still run slowly. Please [submit a bug report](https://github.com/ndd7xv/heh/issues) if you notice an issue. 45 | - There are now templates for submitting issues; not required but there in case someone finds it useful 46 | 47 | ### Changed 48 | 49 | - Search allows for iterating through all matched patterns with `control n` and `control p` (thanks, [@joooooooooooooooooooooooooooooooooooosh](https://github.com/joooooooooooooooooooooooooooooooooooosh)!) 50 | - `heh` switched its dependency from `tui` to `ratatui` 51 | 52 | ### Other Notes 53 | 54 | - MSRV bumped from 1.64.0 to 1.65.0 55 | - memmap2 and crossbeam added as dependencies 56 | - There was an error with CI that was fixed 57 | - This release is 1191568 bytes on my machine, up from 1121936 in 0.3.0 58 | -------------------------------------------------------------------------------- /src/windows/jump_to_byte.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | style::{Color, Style}, 3 | text::Span, 4 | widgets::{Block, Borders, Paragraph}, 5 | }; 6 | 7 | use crate::{app::Data, label::Handler as LabelHandler, screen::Handler as ScreenHandler}; 8 | 9 | use super::{KeyHandler, PopupOutput, Window, adjust_offset}; 10 | 11 | /// A window that can accept input and attempt to move the cursor to the inputted byte. 12 | /// 13 | /// This can be opened by pressing `CNTRLj`. 14 | /// 15 | /// The input is either parsed as hexadecimal if it is preceded with "0x", or decimal if not. 16 | #[derive(PartialEq, Eq)] 17 | pub(crate) struct JumpToByte { 18 | pub(crate) input: String, 19 | } 20 | 21 | impl KeyHandler for JumpToByte { 22 | fn is_focusing(&self, window_type: Window) -> bool { 23 | window_type == Window::JumpToByte 24 | } 25 | fn char(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler, c: char) { 26 | self.input.push(c); 27 | } 28 | fn get_user_input(&self) -> PopupOutput<'_> { 29 | PopupOutput::Str(&self.input) 30 | } 31 | fn backspace(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) { 32 | self.input.pop(); 33 | } 34 | fn enter(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 35 | let new_offset = if self.input.starts_with("0x") { 36 | usize::from_str_radix(&self.input[2..], 16) 37 | } else { 38 | self.input.parse() 39 | }; 40 | if let Ok(new_offset) = new_offset { 41 | if new_offset >= app.contents.len() { 42 | labels.notification = String::from("Invalid range!"); 43 | } else { 44 | app.offset = new_offset; 45 | labels.update_all(&app.contents[app.offset..]); 46 | adjust_offset(app, display, labels); 47 | } 48 | } else { 49 | labels.notification = format!("Error: {:?}", new_offset.unwrap_err()); 50 | } 51 | } 52 | fn dimensions(&self) -> Option<(u16, u16)> { 53 | Some((50, 3)) 54 | } 55 | fn widget(&self) -> Paragraph<'_> { 56 | Paragraph::new(Span::styled(&self.input, Style::default().fg(Color::White))).block( 57 | Block::default() 58 | .title("Jump to Byte:") 59 | .borders(Borders::ALL) 60 | .style(Style::default().fg(Color::Yellow)), 61 | ) 62 | } 63 | } 64 | 65 | impl JumpToByte { 66 | pub(crate) fn new() -> Self { 67 | Self { input: String::new() } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/windows/unsaved_changes.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::Alignment, 3 | style::{Color, Modifier, Style}, 4 | text::{Line, Span}, 5 | widgets::{Block, Borders, Paragraph}, 6 | }; 7 | 8 | use crate::{app::Data, label::Handler as LabelHandler, screen::Handler as ScreenHandler}; 9 | 10 | use super::{KeyHandler, PopupOutput, Window}; 11 | 12 | pub(crate) struct UnsavedChanges { 13 | pub(crate) should_quit: bool, 14 | } 15 | 16 | impl KeyHandler for UnsavedChanges { 17 | fn is_focusing(&self, window_type: Window) -> bool { 18 | window_type == Window::UnsavedChanges 19 | } 20 | fn left(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) { 21 | if !self.should_quit { 22 | self.should_quit = true; 23 | } 24 | } 25 | fn right(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) { 26 | if self.should_quit { 27 | self.should_quit = false; 28 | } 29 | } 30 | fn get_user_input(&self) -> PopupOutput<'_> { 31 | PopupOutput::Boolean(self.should_quit) 32 | } 33 | fn dimensions(&self) -> Option<(u16, u16)> { 34 | Some((50, 5)) 35 | } 36 | fn widget(&self) -> Paragraph<'_> { 37 | let message = vec![ 38 | Line::from(Span::styled( 39 | "Are you sure you want to quit?", 40 | Style::default().fg(Color::White), 41 | )), 42 | Line::from(Span::from("")), 43 | Line::from(vec![ 44 | Span::styled( 45 | " Yes ", 46 | if self.should_quit { 47 | Style::default() 48 | } else { 49 | Style::default().fg(Color::White) 50 | }, 51 | ), 52 | Span::styled( 53 | " No ", 54 | if self.should_quit { 55 | Style::default().fg(Color::White) 56 | } else { 57 | Style::default() 58 | }, 59 | ), 60 | ]), 61 | ]; 62 | Paragraph::new(message).alignment(Alignment::Center).block( 63 | Block::default() 64 | .title(Span::styled( 65 | "You Have Unsaved Changes.", 66 | Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), 67 | )) 68 | .title_alignment(Alignment::Center) 69 | .borders(Borders::ALL) 70 | .style(Style::default().fg(Color::Yellow)), 71 | ) 72 | } 73 | } 74 | 75 | impl UnsavedChanges { 76 | pub(crate) fn new() -> Self { 77 | UnsavedChanges { should_quit: false } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | name: test 11 | jobs: 12 | required: 13 | runs-on: ubuntu-latest 14 | name: ubuntu / ${{ matrix.toolchain }} 15 | strategy: 16 | matrix: 17 | toolchain: [stable, beta] 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: true 22 | - name: Install ${{ matrix.toolchain }} 23 | uses: dtolnay/rust-toolchain@master 24 | with: 25 | toolchain: ${{ matrix.toolchain }} 26 | - name: cargo generate-lockfile 27 | if: hashFiles('Cargo.lock') == '' 28 | run: cargo generate-lockfile 29 | # https://twitter.com/jonhoo/status/1571290371124260865 30 | - name: cargo test --locked 31 | run: cargo test --locked --all-features --all-targets 32 | # https://github.com/rust-lang/cargo/issues/6669 33 | # This crate doesn't have doc tests 34 | # - name: cargo test --doc 35 | # run: cargo test --locked --all-features --doc 36 | minimal: 37 | runs-on: ubuntu-latest 38 | name: ubuntu / stable / minimal-versions 39 | steps: 40 | - uses: actions/checkout@v4 41 | with: 42 | submodules: true 43 | - name: Install stable 44 | uses: dtolnay/rust-toolchain@stable 45 | - name: Install nightly for -Zdirect-minimal-versions 46 | uses: dtolnay/rust-toolchain@nightly 47 | - name: rustup default stable 48 | run: rustup default stable 49 | - name: cargo update -Zdirect-minimal-versions 50 | run: cargo +nightly update -Zdirect-minimal-versions 51 | - name: cargo test 52 | run: cargo test --locked --all-features --all-targets 53 | os-check: 54 | runs-on: ${{ matrix.os }} 55 | name: ${{ matrix.os }} / stable 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | os: [macos-latest, windows-latest] 60 | steps: 61 | - uses: actions/checkout@v4 62 | with: 63 | submodules: true 64 | - name: Install stable 65 | uses: dtolnay/rust-toolchain@stable 66 | - name: cargo generate-lockfile 67 | if: hashFiles('Cargo.lock') == '' 68 | run: cargo generate-lockfile 69 | - name: cargo test 70 | run: cargo test --locked --all-features --all-targets 71 | coverage: 72 | runs-on: ubuntu-latest 73 | name: ubuntu / stable / coverage 74 | steps: 75 | - uses: actions/checkout@v4 76 | with: 77 | submodules: true 78 | - name: Install stable 79 | uses: dtolnay/rust-toolchain@stable 80 | with: 81 | components: llvm-tools-preview 82 | - name: cargo install cargo-llvm-cov 83 | uses: taiki-e/install-action@cargo-llvm-cov 84 | - name: cargo generate-lockfile 85 | if: hashFiles('Cargo.lock') == '' 86 | run: cargo generate-lockfile 87 | - name: cargo llvm-cov 88 | run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info 89 | - name: Upload to codecov.io 90 | uses: codecov/codecov-action@v5 91 | with: 92 | fail_ci_if_error: true 93 | token: ${{ secrets.CODECOV_TOKEN }} 94 | env_vars: OS,RUST 95 | -------------------------------------------------------------------------------- /src/character.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter}; 2 | 3 | use ratatui::style::Color; 4 | 5 | pub(crate) const CHARACTER_NULL: char = '0'; 6 | pub(crate) const CHARACTER_WHITESPACE: char = '_'; 7 | pub(crate) const CHARACTER_CONTROL: char = '⍾'; 8 | pub(crate) const CHARACTER_FILL: char = '•'; 9 | pub(crate) const CHARACTER_UNKNOWN: char = '�'; 10 | 11 | const COLOR_NULL: Color = Color::DarkGray; 12 | const COLOR_ASCII: Color = Color::Cyan; 13 | const COLOR_UNICODE: Color = Color::LightCyan; 14 | const COLOR_WHITESPACE: Color = Color::Green; 15 | const COLOR_CONTROL: Color = Color::Magenta; 16 | const COLOR_FILL: Color = Color::LightCyan; 17 | const COLOR_UNKNOWN: Color = Color::Yellow; 18 | 19 | #[derive(Clone, Debug, PartialEq)] 20 | pub(crate) enum Type { 21 | Ascii, 22 | Unicode(usize), 23 | Unknown, 24 | } 25 | 26 | impl Type { 27 | pub(crate) fn size(&self) -> usize { 28 | match self { 29 | Type::Ascii | Type::Unknown => 1, 30 | Type::Unicode(size) => *size, 31 | } 32 | } 33 | } 34 | 35 | #[derive(Clone, Debug, PartialEq)] 36 | pub(crate) enum Category { 37 | Null, 38 | Ascii, 39 | Unicode, 40 | Whitespace, 41 | Control, 42 | Fill, 43 | Unknown, 44 | } 45 | 46 | impl From<&char> for Category { 47 | fn from(character: &char) -> Self { 48 | if character == &'\0' { 49 | Category::Null 50 | } else if character.is_whitespace() { 51 | Category::Whitespace 52 | } else if character.is_control() { 53 | Category::Control 54 | } else if character.is_ascii() { 55 | Category::Ascii 56 | } else { 57 | Category::Unicode 58 | } 59 | } 60 | } 61 | 62 | impl Category { 63 | pub(crate) fn escape(&self, character: char) -> char { 64 | match self { 65 | Category::Null => CHARACTER_NULL, 66 | Category::Ascii | Category::Unicode => character, 67 | Category::Whitespace if character == ' ' => ' ', 68 | Category::Whitespace => CHARACTER_WHITESPACE, 69 | Category::Control => CHARACTER_CONTROL, 70 | Category::Fill => CHARACTER_FILL, 71 | Category::Unknown => CHARACTER_UNKNOWN, 72 | } 73 | } 74 | 75 | pub(crate) fn color(&self) -> &'static Color { 76 | match self { 77 | Category::Null => &COLOR_NULL, 78 | Category::Ascii => &COLOR_ASCII, 79 | Category::Unicode => &COLOR_UNICODE, 80 | Category::Whitespace => &COLOR_WHITESPACE, 81 | Category::Control => &COLOR_CONTROL, 82 | Category::Fill => &COLOR_FILL, 83 | Category::Unknown => &COLOR_UNKNOWN, 84 | } 85 | } 86 | } 87 | 88 | #[derive(Clone, Debug)] 89 | pub(crate) struct RichChar { 90 | character: char, 91 | category: Category, 92 | } 93 | 94 | impl RichChar { 95 | pub(crate) fn new(character: char, category: Category) -> Self { 96 | Self { character, category } 97 | } 98 | 99 | pub(crate) fn escape(&self) -> char { 100 | self.category.escape(self.character) 101 | } 102 | 103 | pub(crate) fn color(&self) -> &'static Color { 104 | self.category.color() 105 | } 106 | } 107 | 108 | impl Display for RichChar { 109 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 110 | Display::fmt(&self.character, f) 111 | } 112 | } 113 | 114 | impl From for char { 115 | fn from(rich_char: RichChar) -> Self { 116 | rich_char.character 117 | } 118 | } 119 | 120 | impl From<&RichChar> for char { 121 | fn from(rich_char: &RichChar) -> Self { 122 | rich_char.character 123 | } 124 | } 125 | 126 | impl From for String { 127 | fn from(rich_char: RichChar) -> Self { 128 | rich_char.character.to_string() 129 | } 130 | } 131 | 132 | impl From<&RichChar> for String { 133 | fn from(rich_char: &RichChar) -> Self { 134 | rich_char.character.to_string() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! The HEx Helper is a cross-platform terminal UI used for modifying file data in hex or ASCII. 2 | //! It aims to replicate some of the look of [hexyl](https://github.com/sharkdp/hexyl) while 3 | //! functionaly acting like a terminal UI version of [GHex](https://wiki.gnome.org/Apps/Ghex). 4 | //! 5 | //! **heh is currently in alpha** - it's not ready to be used in any production manner. It lacks a 6 | //! variety of quality of life features and does not store backups if killed or crashing. 7 | 8 | use std::{error::Error, fs::OpenOptions, io, process}; 9 | 10 | use clap::{Parser, ValueEnum}; 11 | use ratatui::crossterm::tty::IsTty; 12 | 13 | use heh::app::Application; 14 | use heh::decoder::Encoding; 15 | 16 | const ABOUT: &str = " 17 | A HEx Helper to edit bytes by the nibble. 18 | 19 | Do --help for more information."; 20 | 21 | const LONG_ABOUT: &str = " 22 | The HEx Helper is a terminal tool used for modifying binaries by 23 | the nibble. It aims to replicate some of the look of hexyl while 24 | functionally acting like a terminal UI version of GHex. 25 | 26 | Note that the octal and hexadecimal labels are slightly 27 | different in heh; they interpret the stream as if 0s were filled 28 | to the end of the byte (i.e. stream length 9 on FF FF would 29 | produce octal 377 200 and hexadecimal FF 80). 30 | 31 | Like GHex, you cannot create files with heh, only modify them. 32 | 33 | Terminal UI Commands: 34 | ALT= Increase the stream length by 1 35 | ALT- Decrease the stream length by 1 36 | CNTRLs Save 37 | CNTRLq Quit 38 | CNTRLj Jump to Byte 39 | CNTRLe Switch Endianness 40 | CNTRLd Page Down 41 | CNTRLu Page Up 42 | CNTRLf or / Search 43 | CNTRLn or Enter Next Search Match 44 | CNTRLp Prev Search Match 45 | 46 | Left-clicking on a label will copy the contents to the clipboard. 47 | Left-clicking on the ASCII or hex table will focus it. 48 | 49 | Zooming in and out will change the size of the components."; 50 | 51 | #[derive(Parser)] 52 | #[command(version, about = ABOUT, long_about = LONG_ABOUT)] 53 | struct Cli { 54 | #[arg( 55 | value_enum, 56 | short = 'e', 57 | long = "encoding", 58 | default_value = "ascii", 59 | help = "Encoding used for text editor" 60 | )] 61 | encoding: EncodingOption, 62 | #[arg( 63 | value_parser = parse_hex_or_dec, 64 | long = "offset", 65 | default_value = "0", 66 | help="Read file at offset (indicated by a decimal or hexadecimal number)" 67 | )] 68 | offset: usize, 69 | 70 | // Positional argument. 71 | #[arg(help = "File to open")] 72 | file: String, 73 | } 74 | 75 | /// Opens the specified file, creates a new application and runs it! 76 | fn main() -> Result<(), Box> { 77 | if !io::stdout().is_tty() { 78 | eprintln!("Stdout is not a TTY device."); 79 | process::exit(1); 80 | } 81 | 82 | let cli = Cli::parse(); 83 | let file = OpenOptions::new().read(true).write(true).open(cli.file)?; 84 | let mut app = Application::new(file, cli.encoding.into(), cli.offset)?; 85 | app.run()?; 86 | 87 | Ok(()) 88 | } 89 | 90 | #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 91 | pub enum EncodingOption { 92 | Ascii, 93 | Utf8, 94 | } 95 | 96 | impl From for Encoding { 97 | fn from(encoding: EncodingOption) -> Self { 98 | match encoding { 99 | EncodingOption::Ascii => Encoding::Ascii, 100 | EncodingOption::Utf8 => Encoding::Utf8, 101 | } 102 | } 103 | } 104 | 105 | fn parse_hex_or_dec(arg: &str) -> Result { 106 | if let Some(stripped) = arg.strip_prefix("0x") { 107 | usize::from_str_radix(stripped, 16).map_err(|e| format!("Invalid hexadecimal number: {e}")) 108 | } else { 109 | arg.parse().map_err(|e| format!("Invalid decimal number: {e}")) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # heh 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/heh.svg)](https://crates.io/crates/heh) 4 | [![Codecov](https://codecov.io/github/ndd7xv/heh/coverage.svg?branch=master)](https://codecov.io/gh/ndd7xv/heh) 5 | [![Dependency status](https://deps.rs/repo/github/ndd7xv/heh/status.svg)](https://deps.rs/repo/github/ndd7xv/heh) 6 | 7 | 8 | The HEx Helper is a cross-platform terminal [hex editor](https://en.wikipedia.org/wiki/Hex_editor) used for modifying file data in hex or ASCII. It aims to replicate some of the look of hexyl while functionally acting like a terminal UI version of GHex. 9 | 10 | > [!WARNING] 11 | > heh is currently in alpha - it's not ready to be used in any production manner. It lacks a variety of quality of life features and does not store backups if killed or crashing. 12 | 13 | ![screenshot of heh](demo.png) 14 | 15 | # Installation and Usage 16 | 17 | heh is available via cargo: 18 | 19 | ``` 20 | cargo install heh 21 | ``` 22 | 23 | From `heh --help`: 24 | ``` 25 | ... 26 | Terminal UI Commands: 27 | ALT= Increase the stream length by 1 28 | ALT- Decrease the stream length by 1 29 | CNTRLs Save 30 | CNTRLq Quit 31 | CNTRLj Jump to Byte 32 | CNTRLe Change endianness 33 | CNTRLd Page Down 34 | CNTRLu Page Up 35 | CNTRLf or / Search 36 | CNTRLn or Enter Next Search Match 37 | CNTRLp Prev Search Match 38 | 39 | Left-clicking on a label will copy the contents to the clipboard. 40 | Left-clicking on the ASCII or hex table will focus it. 41 | 42 | Zooming in and out will change the size of the components. 43 | 44 | USAGE: 45 | heh 46 | 47 | ARGS: 48 | 49 | 50 | 51 | OPTIONS: 52 | -h, --help 53 | Print help information 54 | 55 | -V, --version 56 | Print version information 57 | 58 | ``` 59 | 60 | ## Distro packages 61 | 62 |
63 | Packaging status 64 | 65 | [![Packaging status](https://repology.org/badge/vertical-allrepos/heh.svg)](https://repology.org/project/heh/versions) 66 | 67 |
68 | 69 | If your distribution has packaged `heh`, you can use that package for the installation. 70 | 71 | ### Arch Linux 72 | 73 | You can use [pacman](https://wiki.archlinux.org/title/Pacman) to install from the [extra repository](https://archlinux.org/packages/extra/x86_64/heh/): 74 | 75 | ``` 76 | pacman -S heh 77 | ``` 78 | 79 | ### Alpine Linux 80 | 81 | `heh` is available for [Alpine Edge](https://pkgs.alpinelinux.org/packages?name=heh&branch=edge). It can be installed via [apk](https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper) after enabling the [testing repository](https://wiki.alpinelinux.org/wiki/Repositories). 82 | 83 | ``` 84 | apk add heh 85 | ``` 86 | 87 | ## Using as a Ratatui widget 88 | 89 | `heh` can be used a library and embedded into other TUI applications which use [Ratatui](https://ratatui.rs) and [crossterm](https://github.com/crossterm-rs/crossterm). 90 | 91 | Add `heh` to your dependencies in `Cargo.toml`: 92 | 93 | ```toml 94 | [dependencies] 95 | ratatui = "0.28" 96 | heh = "0.6" 97 | ``` 98 | 99 | Create the application: 100 | 101 | ```rust 102 | use heh::app::Application as Heh; 103 | use heh::decoder::Encoding; 104 | 105 | let file = std::fs::OpenOptions::new().read(true).write(true).open(path).unwrap(); 106 | let mut heh = Heh::new(file, Encoding::Ascii, 0).unwrap(); 107 | ``` 108 | 109 | Then you can render a frame as follows: 110 | 111 | ```rust 112 | terminal.draw(|frame| { 113 | heh.render_frame(frame, frame.area()); 114 | }); 115 | ``` 116 | 117 | To handle key events: 118 | 119 | ```rust 120 | heh.handle_input(&ratatui::crossterm::event::Event::Key(/* */)).unwrap(); 121 | ``` 122 | 123 | See the [demo example](examples/demo.rs) for full code. 124 | 125 | See the [binsider](https://github.com/orhun/binsider) project for an example application that uses `heh`. 126 | 127 | # Contributing 128 | 129 | See [CONTRIBUTING.md](CONTRIBUTING.md). 130 | -------------------------------------------------------------------------------- /src/windows/mod.rs: -------------------------------------------------------------------------------- 1 | //! The components that implement [`KeyHandler`], which allow them to uniquely react to user input. 2 | //! Example of a component include the Hex/ASCII editors and the Unsaved Changes warning. 3 | 4 | pub(crate) mod editor; 5 | pub(crate) mod jump_to_byte; 6 | pub(crate) mod search; 7 | pub(crate) mod unsaved_changes; 8 | 9 | use ratatui::widgets::Paragraph; 10 | 11 | use crate::{app::Data, label::Handler as LabelHandler, screen::Handler as ScreenHandler}; 12 | 13 | /// An enumeration of all the potential components that can be clicked. Used to identify which 14 | /// component has been most recently clicked, and is also used to detmine which window is 15 | /// focused in the `Application`'s input field. 16 | #[derive(PartialEq, Eq, Copy, Clone)] 17 | pub enum Window { 18 | Ascii, 19 | Hex, 20 | JumpToByte, 21 | Search, 22 | UnsavedChanges, 23 | Label(usize), 24 | Unhandled, 25 | } 26 | 27 | /// Represents the possible output of a variety of different popups. 28 | #[derive(PartialEq, Eq)] 29 | pub enum PopupOutput<'a> { 30 | Str(&'a str), 31 | Boolean(bool), 32 | NoOutput, 33 | } 34 | 35 | /// A trait for objects which handle input. 36 | /// 37 | /// Depending on what is currently focused, user input can be handled in different ways. For 38 | /// example, pressing enter should not modify the opened file in any form, but doing so while the 39 | /// "Jump To Byte" popup is focused should attempt to move the cursor to the inputted byte. 40 | pub trait KeyHandler { 41 | /// Checks if the current [`KeyHandler`] is a certain [`Window`]. 42 | fn is_focusing(&self, window_type: Window) -> bool; 43 | 44 | // Methods that handle their respective keypresses. 45 | fn left(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 46 | fn right(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 47 | fn up(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 48 | fn down(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 49 | fn home(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 50 | fn end(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 51 | fn page_up(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 52 | fn page_down(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 53 | fn backspace(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 54 | fn delete(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 55 | fn enter(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) {} 56 | fn char(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler, _: char) {} 57 | 58 | /// Returns user input. Is currently used to get information from popups. 59 | fn get_user_input(&self) -> PopupOutput<'_> { 60 | PopupOutput::NoOutput 61 | } 62 | 63 | /// Returns the dimensions of to be used in displaying a popup. Returns None if an editor. 64 | fn dimensions(&self) -> Option<(u16, u16)> { 65 | None 66 | } 67 | 68 | /// Returns the contents to display on the screen 69 | fn widget(&self) -> Paragraph<'_> { 70 | Paragraph::new("") 71 | } 72 | } 73 | 74 | /// Moves the starting address of the editor viewports (Hex and ASCII) to include the cursor. 75 | /// This is helpful because some window actions (search, jump to byte) move the cursor, and we 76 | /// want to move the screen along with it. 77 | /// 78 | /// If the cursor's location is before the viewports start, the viewports will move so that the 79 | /// cursor is included in the first row. 80 | /// 81 | /// If the cursor's location is past the end of the viewports, the viewports will move so that 82 | /// the cursor is included in the final row. 83 | pub(crate) fn adjust_offset( 84 | app: &mut Data, 85 | display: &mut ScreenHandler, 86 | labels: &mut LabelHandler, 87 | ) { 88 | let bytes_per_line = display.comp_layouts.bytes_per_line; 89 | let bytes_per_screen = bytes_per_line * display.comp_layouts.lines_per_screen; 90 | 91 | if app.offset < app.start_address { 92 | app.start_address = (app.offset / bytes_per_line) * bytes_per_line; 93 | } else if app.offset >= app.start_address + (bytes_per_screen) { 94 | app.start_address = 95 | (app.offset / bytes_per_line) * bytes_per_line - bytes_per_screen + bytes_per_line; 96 | } 97 | 98 | labels.offset = format!("{:#X}", app.offset); 99 | } 100 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for you interest in contributing! Changes of all types are welcome, from feature additions and major code refactoring to the tiniest typo fix. If you want to make change, 2 | 3 | 1. Fork this repository 4 | 2. Make desired changes in (a) descriptive commit(s) 5 | 3. Make a PR, linking any issues that may be related 6 | 7 | ...and follow the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct) all the way through! 8 | 9 | ## Goals and Roadmap 10 | 11 | This was initially a hobby project, though now I believe it has the potential to be a **fully-fledged terminal hex editor.** There are many improvements that could make using `heh` better. The main problems are: 12 | 13 | 1. I don't use hex editors often, so I don't know the pain points and wants of a developer who regularly utilizes a hex editor. 14 | 1. I have other commitments (and will probably until mid 2025) that keep me busy from contributing as much as I'd like to. 15 | 16 | **Because of this, major updates to `heh` will be less frequent.** I will keep versions updated, code review pull requests, and enhance `heh` when I find the time. However, larger quality of life improvements will probably be rare until I'm no longer preoccupied and have the time to carefully research for and develop the terminal hex editor. 17 | 18 | I do plan on returning to work on `heh` and open source in general. If you are interested in contributing, I've listed items that I think could use improvement and am willing to elaborate on them if needed (just create a PR or issue). **I'm also open to adding maintainers,** although I'm doubtful finding any for a project I myself cannot actively dedicate time towards. 19 | 20 | Any other suggestions and ideas not included are gladly welcome in the form of issues and PRs! 21 | 22 | ### Ideas 23 | 24 | - Crash Handling 25 | - More test coverage 26 | - Tests in a seperate folder 27 | - Better Documentation (what little is written is written by [me](https://github.com/ndd7xv), and there may be some seemingly obvious things I've left out because I can't work on this project with fresh eyes) 28 | - Enforce unsafety documentation through clippy (maybe via [lint configuration in cargo](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html#lint-configuration-through-cargo)) 29 | - Customizable shortcuts 30 | - More shortcuts (maybe vim like; for example, `gg` or `GG` to go to the top of bottom) 31 | - Explore the use of [ratatui's scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Scrollbar.html) (`heh` was made prior to the creation of that widget) 32 | - An investigation into using [Miri](https://github.com/rust-lang/miri) (does running it have any problems) 33 | - Automate releases (currently I just create a release from GitHub in my browser) 34 | 35 | ## Structure 36 | 37 | While not a comprehensive list, some of the pointers may help you get started navigating the code base. 38 | 39 | - Each `.rs` file in the `src/windows` represents a different window that can show up -- for example, [`jump_to_byte.rs`](src/windows/jump_to_byte.rs) displays a popup where a user can specify the offset in a file they want to jump to. If you want to add another window: 40 | - Use the other window.rs files as reference (a relatively simple example is [`unsaved_changes.rs`](src/windows/unsaved_changes.rs)) 41 | - Implement the KeyHandler trait for your window 42 | - Add your window into the Window enum 43 | - Modify [`app.rs`](src/app.rs) and [`input.rs`](src/input.rs) to register keys to set the window to the one you created 44 | - At a high level from the [`src/`](src/) directory: 45 | - [`input.rs`](src/input.rs) is for input handling (i.e. keyboard and mouse) 46 | - [`screen.rs`](src/screen.rs) renders what is seen in the terminal 47 | - [`app.rs`](src/app.rs) culminates everything and maintains the state of the application 48 | 49 | These 3 files work pretty closely with one another; input from the user modifies the state of the app, and the state of the app is read by the screen to display content to the user. 50 | 51 | - Other files include: 52 | - [`label.rs`](src/label.rs) contain the logic for figuring out the information (Signed 8 bit, Offset, etc) at the cursor 53 | - [`character.rs`](src/character.rs) converts file bytes into ASCII/Unicode 54 | - [`buffer.rs`](src/buffer.rs) maintains the relevant part of the file that is being viewed/edited in order to work on large files 55 | - [`chunk.rs`](src/chunk.rs) and `decoder.rs` are used to read the file and display its contents in hex and ASCII/Unicode 56 | 57 | - Run `cargo doc --open` to get a more human readable view of the code descriptions - if you find anything ambiguous, confusing, or obsure, please edit or let me know (doc related or not)! 58 | -------------------------------------------------------------------------------- /src/decoder.rs: -------------------------------------------------------------------------------- 1 | //! Decoder utilities. 2 | 3 | use std::str::from_utf8; 4 | 5 | use crate::character::{CHARACTER_FILL, CHARACTER_UNKNOWN, Category, RichChar, Type}; 6 | 7 | struct LossyASCIIDecoder<'a> { 8 | bytes: &'a [u8], 9 | cursor: usize, 10 | } 11 | 12 | impl<'a> From<&'a [u8]> for LossyASCIIDecoder<'a> { 13 | fn from(bytes: &'a [u8]) -> Self { 14 | Self { bytes, cursor: 0 } 15 | } 16 | } 17 | 18 | impl Iterator for LossyASCIIDecoder<'_> { 19 | type Item = (char, Type); 20 | 21 | fn next(&mut self) -> Option { 22 | if self.cursor < self.bytes.len() { 23 | let byte = self.bytes[self.cursor]; 24 | self.cursor += 1; 25 | if byte.is_ascii() { 26 | Some((byte as char, Type::Ascii)) 27 | } else { 28 | Some((CHARACTER_UNKNOWN, Type::Unknown)) 29 | } 30 | } else { 31 | None 32 | } 33 | } 34 | } 35 | 36 | struct LossyUTF8Decoder<'a> { 37 | bytes: &'a [u8], 38 | cursor: usize, 39 | } 40 | 41 | impl<'a> From<&'a [u8]> for LossyUTF8Decoder<'a> { 42 | fn from(bytes: &'a [u8]) -> Self { 43 | LossyUTF8Decoder { bytes, cursor: 0 } 44 | } 45 | } 46 | 47 | impl Iterator for LossyUTF8Decoder<'_> { 48 | type Item = (char, Type); 49 | 50 | fn next(&mut self) -> Option { 51 | if self.cursor < self.bytes.len() { 52 | let typ = match self.bytes[self.cursor] { 53 | 0x00..=0x7F => Type::Ascii, 54 | 0xC0..=0xDF => Type::Unicode(2), 55 | 0xE0..=0xEF => Type::Unicode(3), 56 | 0xF0..=0xF7 => Type::Unicode(4), 57 | _ => { 58 | self.cursor += 1; 59 | return Some((CHARACTER_UNKNOWN, Type::Unknown)); 60 | } 61 | }; 62 | 63 | let new_cursor = self.bytes.len().min(self.cursor + typ.size()); 64 | let chunk = &self.bytes[self.cursor..new_cursor]; 65 | 66 | if let Ok(mut chars) = from_utf8(chunk).map(str::chars) { 67 | let char = chars.next().expect("the string must contain exactly one character"); 68 | debug_assert!( 69 | chars.next().is_none(), 70 | "the string must contain exactly one character" 71 | ); 72 | self.cursor += typ.size(); 73 | Some((char, typ)) 74 | } else { 75 | self.cursor += 1; 76 | Some((CHARACTER_UNKNOWN, Type::Unknown)) 77 | } 78 | } else { 79 | None 80 | } 81 | } 82 | } 83 | 84 | #[derive(Copy, Clone, Debug)] 85 | pub enum Encoding { 86 | Ascii, 87 | Utf8, 88 | } 89 | 90 | pub(crate) struct ByteAlignedDecoder> { 91 | decoder: D, 92 | to_fill: usize, 93 | } 94 | 95 | type BoxedDecoder<'a> = Box + 'a>; 96 | 97 | impl<'a> ByteAlignedDecoder> { 98 | pub(crate) fn new(bytes: &'a [u8], encoding: Encoding) -> Self { 99 | match encoding { 100 | Encoding::Ascii => Box::new(LossyASCIIDecoder::from(bytes)) as BoxedDecoder, 101 | Encoding::Utf8 => Box::new(LossyUTF8Decoder::from(bytes)) as BoxedDecoder, 102 | } 103 | .into() 104 | } 105 | } 106 | 107 | impl> From for ByteAlignedDecoder { 108 | fn from(decoder: D) -> Self { 109 | Self { decoder, to_fill: 0 } 110 | } 111 | } 112 | 113 | impl> Iterator for ByteAlignedDecoder { 114 | type Item = RichChar; 115 | 116 | fn next(&mut self) -> Option { 117 | if self.to_fill == 0 { 118 | let (character, typ) = self.decoder.next()?; 119 | let category = match typ { 120 | Type::Unknown => Category::Unknown, 121 | _ => Category::from(&character), 122 | }; 123 | self.to_fill = typ.size() - 1; 124 | Some(RichChar::new(character, category)) 125 | } else { 126 | self.to_fill -= 1; 127 | Some(RichChar::new(CHARACTER_FILL, Category::Fill)) 128 | } 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::*; 135 | 136 | const TEST_BYTES: &[u8] = b"text, controls \n \r\n, space \t, unicode \xC3\xA4h \xC3\xA0 la \xF0\x9F\x92\xA9, null \x00, invalid \xC0\xF8\xEE"; 137 | 138 | #[test] 139 | fn test_decoder_ascii() { 140 | let decoder = ByteAlignedDecoder::new(TEST_BYTES, Encoding::Ascii); 141 | let characters: Vec<_> = decoder.collect(); 142 | 143 | assert_eq!(TEST_BYTES.len(), characters.len()); 144 | assert_eq!( 145 | characters.iter().map(RichChar::escape).collect::(), 146 | "text, controls _ __, space _, unicode ��h �� la ����, null 0, invalid ���" 147 | ); 148 | } 149 | 150 | #[test] 151 | fn test_decoder_utf8() { 152 | let decoder = ByteAlignedDecoder::new(TEST_BYTES, Encoding::Utf8); 153 | let characters: Vec<_> = decoder.collect(); 154 | 155 | assert_eq!(TEST_BYTES.len(), characters.len()); 156 | assert_eq!( 157 | characters.iter().map(RichChar::escape).collect::(), 158 | "text, controls _ __, space _, unicode ä•h à• la 💩•••, null 0, invalid ���" 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/windows/search.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | style::{Color, Style}, 3 | text::Span, 4 | widgets::{Block, Borders, Paragraph}, 5 | }; 6 | 7 | use crate::{app::Data, label::Handler as LabelHandler, screen::Handler as ScreenHandler}; 8 | 9 | use super::{KeyHandler, PopupOutput, Window, adjust_offset}; 10 | 11 | /// A window that accepts either a hexadecimal or an ASCII sequence and moves cursor to the next 12 | /// occurrence of this sequence 13 | /// 14 | /// This can be opened by pressing `CNTRLf`. 15 | /// 16 | /// Each symbol group is either parsed as hexadecimal if it is preceded with "0x", or decimal if 17 | /// not. 18 | /// 19 | /// Replace ASCII "0x", with "0x30x", (0x30 is hexadecimal for ascii 0) e.g. to search for "0xFF" 20 | /// in ASCII, search for "0x30xFF" instead. 21 | #[derive(PartialEq, Eq)] 22 | pub(crate) struct Search { 23 | pub(crate) input: String, 24 | } 25 | 26 | impl Search { 27 | pub(crate) fn new() -> Self { 28 | Self { input: String::new() } 29 | } 30 | } 31 | 32 | impl KeyHandler for Search { 33 | fn is_focusing(&self, window_type: super::Window) -> bool { 34 | window_type == Window::Search 35 | } 36 | fn char(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler, c: char) { 37 | self.input.push(c); 38 | } 39 | fn get_user_input(&self) -> PopupOutput<'_> { 40 | PopupOutput::Str(&self.input) 41 | } 42 | fn backspace(&mut self, _: &mut Data, _: &mut ScreenHandler, _: &mut LabelHandler) { 43 | self.input.pop(); 44 | } 45 | fn enter(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 46 | let byte_sequence_to_search = self.input.as_bytes(); 47 | if byte_sequence_to_search.is_empty() { 48 | labels.notification = "Empty search query".into(); 49 | return; 50 | } 51 | 52 | app.search_term.clone_from(&self.input); 53 | app.reindex_search(); 54 | 55 | perform_search(app, display, labels, &SearchDirection::Forward); 56 | } 57 | fn dimensions(&self) -> Option<(u16, u16)> { 58 | Some((50, 3)) 59 | } 60 | fn widget(&self) -> Paragraph<'_> { 61 | Paragraph::new(Span::styled(&self.input, Style::default().fg(Color::White))).block( 62 | Block::default() 63 | .title("Search:") 64 | .borders(Borders::ALL) 65 | .style(Style::default().fg(Color::Yellow)), 66 | ) 67 | } 68 | } 69 | 70 | pub(crate) enum SearchDirection { 71 | Forward, 72 | Backward, 73 | } 74 | 75 | pub(crate) fn perform_search( 76 | app: &mut Data, 77 | display: &mut ScreenHandler, 78 | labels: &mut LabelHandler, 79 | search_direction: &SearchDirection, 80 | ) { 81 | if app.search_term.is_empty() { 82 | return; 83 | } 84 | 85 | // Cached search data may be invalidated if contents have changed 86 | if app.dirty { 87 | app.reindex_search(); 88 | } 89 | 90 | // This check needs to happen after reindexing search 91 | if app.search_offsets.is_empty() { 92 | labels.notification = "Query not found".into(); 93 | return; 94 | } 95 | 96 | let idx = get_next_match_index(&app.search_offsets, app.offset, search_direction); 97 | let found_position = *app.search_offsets.get(idx).expect("There should be at least one result"); 98 | 99 | labels.notification = 100 | format!("Search: {} [{}/{}]", app.search_term, idx + 1, app.search_offsets.len()); 101 | 102 | app.offset = found_position; 103 | labels.update_all(&app.contents[app.offset..]); 104 | adjust_offset(app, display, labels); 105 | } 106 | 107 | // Find closest index of a match to the current offset, wrapping to the other end of the file if necessary 108 | // This performs a binary search for the current offset in the list of matches. If the current 109 | // offset is not present, the binary search will return the index in the list of matches where 110 | // the current offset would fit, and from that we either pick the index to the left or right 111 | // depending on whether we're searching forwards or backwards from the current offset. 112 | fn get_next_match_index( 113 | search_offsets: &[usize], 114 | current_offset: usize, 115 | search_direction: &SearchDirection, 116 | ) -> usize { 117 | match search_direction { 118 | SearchDirection::Forward => search_offsets 119 | .binary_search(&(current_offset + 1)) 120 | .unwrap_or_else(|i| if i >= search_offsets.len() { 0 } else { i }), 121 | SearchDirection::Backward => search_offsets 122 | .binary_search(&(current_offset.checked_sub(1).unwrap_or(usize::MAX))) 123 | .unwrap_or_else(|i| if i == 0 { search_offsets.len() - 1 } else { i - 1 }), 124 | } 125 | } 126 | 127 | #[cfg(test)] 128 | mod tests { 129 | use super::{SearchDirection, get_next_match_index}; 130 | 131 | #[test] 132 | fn test_search() { 133 | fn search( 134 | search_offsets: &[usize], 135 | current_offset: usize, 136 | search_direction: &SearchDirection, 137 | ) -> usize { 138 | let idx = get_next_match_index(search_offsets, current_offset, search_direction); 139 | search_offsets[idx] 140 | } 141 | 142 | let search_offsets = vec![1, 4, 5, 7]; 143 | // positioned in between matches 144 | assert_eq!(search(&search_offsets, 2, &SearchDirection::Backward), 1); 145 | assert_eq!(search(&search_offsets, 3, &SearchDirection::Backward), 1); 146 | 147 | // positioned on a match 148 | assert_eq!(search(&search_offsets, 4, &SearchDirection::Backward), 1); 149 | assert_eq!(search(&search_offsets, 4, &SearchDirection::Forward), 5); 150 | 151 | // wrap around 152 | assert_eq!(search(&search_offsets, 7, &SearchDirection::Forward), 1); 153 | assert_eq!(search(&search_offsets, 1, &SearchDirection::Backward), 7); 154 | assert_eq!(search(&search_offsets, 0, &SearchDirection::Backward), 7); 155 | 156 | // singular match 157 | let search_offsets = vec![3]; 158 | assert_eq!(search(&search_offsets, 4, &SearchDirection::Backward), 3); 159 | assert_eq!(search(&search_offsets, 3, &SearchDirection::Backward), 3); 160 | assert_eq!(search(&search_offsets, 2, &SearchDirection::Backward), 3); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | ops::{Deref, DerefMut}, 4 | sync::{ 5 | Arc, 6 | atomic::{AtomicBool, AtomicUsize, Ordering}, 7 | }, 8 | }; 9 | 10 | use memmap2::{MmapMut, MmapOptions}; 11 | 12 | const SYNC_BUFF_LEN: usize = 0x10000; 13 | 14 | /// Messages that the background thread processes to modify the buffer outside 15 | /// of the main rendering thread. 16 | enum EditMessage { 17 | Remove, 18 | Add(u8), // The byte to add to the front of the buffer that got cut off synchronously 19 | ModifyWindow(usize), // New offset to sync the window to 20 | } 21 | 22 | /// Struct to encapsulate a memory mapped buffer. Memmap is unsafe due to the fact 23 | /// that it is backed by a file that could be removed. To make it safer, the file 24 | /// can be locked. This struct also implements deref to much more easily control 25 | /// the content the rest of the application can see without massively restructuring. 26 | pub(crate) struct AsyncBuffer { 27 | /// The mmap backed by the file that is being edited 28 | content_buf: MmapMut, 29 | /// The length of the content. Used for when elements are deleted 30 | len: usize, 31 | /// A mpsc channel that allows sending messages to a thread that finishes 32 | /// updating the buffer if it is very large. Makes it much more responsive 33 | tx: crossbeam::channel::Sender, 34 | /// An atomic that denotes whether the background buffer is actively engaged in work 35 | has_work: Arc, 36 | /// An offset shared between the processing thread and the main thread. This is to safely 37 | /// work on the ultimately same buffer by splitting it into 2 independent slices 38 | window_end: Arc, 39 | } 40 | 41 | impl Deref for AsyncBuffer { 42 | type Target = [u8]; 43 | 44 | fn deref(&self) -> &Self::Target { 45 | &self.content_buf[..self.len] 46 | } 47 | } 48 | 49 | impl DerefMut for AsyncBuffer { 50 | fn deref_mut(&mut self) -> &mut Self::Target { 51 | &mut self.content_buf[..self.len] 52 | } 53 | } 54 | 55 | impl AsyncBuffer { 56 | /// Create 2 copy-on-write memmaps of the same file. Since they are shared, 57 | /// they edit the same underlying buffer. Store one of the buffers for use 58 | /// for background processing by [`AsyncBuffer::process_messages`] 59 | pub fn new(file: &std::fs::File) -> Result> { 60 | let mut content_buf = unsafe { MmapOptions::new().map_copy(file)? }; 61 | let internal_buf = content_buf.as_mut_ptr(); 62 | 63 | let has_work = Arc::new(AtomicBool::new(false)); 64 | 65 | // This is ok, because it is the len of a memmap buffer, it is limited 66 | // by the size of the addressing space anyways. 67 | #[allow(clippy::cast_possible_truncation)] 68 | let window_end = 69 | Arc::new(AtomicUsize::new(SYNC_BUFF_LEN.min(file.metadata()?.len() as usize))); 70 | 71 | let (tx, rx) = crossbeam::channel::unbounded(); 72 | 73 | AsyncBuffer::process_messages( 74 | #[allow(clippy::cast_possible_truncation)] 75 | (internal_buf, file.metadata()?.len() as usize), 76 | rx, 77 | has_work.clone(), 78 | window_end.clone(), 79 | ); 80 | 81 | #[allow(clippy::cast_possible_truncation)] 82 | Ok(Self { content_buf, len: file.metadata()?.len() as usize, tx, has_work, window_end }) 83 | } 84 | 85 | /// Receives messages of type [`EditMessage`], and processes the buffer in the 86 | /// background. Although this uses unsafe in both the ptr copy and the 2 mutable 87 | /// buffers, it is still safe. This uses a channel to receive the messages in order. 88 | /// Once received, it does the copy to insert / remove where the main thread stopped. 89 | /// This *vastly* improves snappiness and feel and does not overlap in read / write 90 | /// with the main thread, thus preventing any UB in writing to the same section of 91 | /// the buffer 92 | fn process_messages( 93 | internal_buf: (*mut u8, usize), 94 | rx: crossbeam::channel::Receiver, 95 | has_work: Arc, 96 | window_offset: Arc, 97 | ) { 98 | let internal_buf = 99 | unsafe { std::slice::from_raw_parts_mut(internal_buf.0, internal_buf.1) }; 100 | let mut internal_start = window_offset.load(Ordering::SeqCst); 101 | 102 | std::thread::spawn(move || { 103 | loop { 104 | for rcv in &rx { 105 | has_work.store(true, Ordering::SeqCst); 106 | 107 | let start = window_offset.load(Ordering::SeqCst); 108 | let internal_buf = &mut internal_buf[start..]; 109 | 110 | match rcv { 111 | EditMessage::Remove => unsafe { 112 | debug_assert!(internal_start >= start); 113 | 114 | std::ptr::copy( 115 | internal_buf.as_ptr().add(internal_start - start), 116 | internal_buf.as_mut_ptr(), 117 | internal_buf.len() - (internal_start - start), 118 | ); 119 | 120 | internal_start = start; 121 | }, 122 | EditMessage::Add(byte) => unsafe { 123 | debug_assert_eq!(internal_start, window_offset.load(Ordering::SeqCst)); 124 | 125 | std::ptr::copy( 126 | internal_buf.as_ptr(), 127 | internal_buf.as_mut_ptr().add(1), 128 | internal_buf.len() - 1, 129 | ); 130 | 131 | internal_buf[0] = byte; 132 | }, 133 | EditMessage::ModifyWindow(new_window) => { 134 | window_offset.store(new_window, Ordering::SeqCst); 135 | internal_start = new_window; 136 | } 137 | } 138 | 139 | has_work.store(rx.is_full(), Ordering::SeqCst); 140 | } 141 | } 142 | }); 143 | } 144 | 145 | /// Returns the length accounting for deletes 146 | pub fn len(&self) -> usize { 147 | self.len 148 | } 149 | 150 | /// Removes the value, and then copies the rest of the buffer 1 previous 151 | /// up to the offset of the internal window. After this, it has the background 152 | /// thread process the rest. 153 | pub fn remove(&mut self, offset: usize) -> u8 { 154 | let val = self.content_buf[offset]; 155 | 156 | unsafe { 157 | std::ptr::copy( 158 | self.content_buf.as_ptr().add(offset + 1), 159 | self.content_buf.as_mut_ptr().add(offset), 160 | self.window_end.fetch_sub(1, Ordering::SeqCst) - offset, 161 | ); 162 | } 163 | 164 | self.tx.send(EditMessage::Remove).unwrap(); 165 | self.len -= 1; 166 | 167 | val 168 | } 169 | 170 | /// At the moment, only used for undoing deletions. With that in mind, 171 | /// no need to worry about increasing the size of the buffer. Copies 172 | /// up to the window so a single byte will be cut off at the end. Sends 173 | /// this byte so the background thread can re-insert it once it is safe. 174 | pub fn insert(&mut self, offset: usize, byte: u8) { 175 | let window_end = self.window_end.load(Ordering::SeqCst); 176 | self.tx.send(EditMessage::Add(self.content_buf[window_end - 1])).unwrap(); 177 | self.len += 1; 178 | 179 | unsafe { 180 | std::ptr::copy( 181 | self.content_buf.as_ptr().add(offset), 182 | self.content_buf.as_mut_ptr().add(offset + 1), 183 | window_end.saturating_sub(offset).saturating_sub(1), 184 | ); 185 | } 186 | 187 | self.content_buf[offset] = byte; 188 | } 189 | 190 | /// Compute whether the window needs to be extended, blocks if so until there is no 191 | /// more work to prevent data from being inserter / removed in the wrong places. 192 | pub fn compute_new_window(&mut self, new_offset: usize) { 193 | let window_end = self.window_end.load(Ordering::SeqCst); 194 | // If the distance of the current offset to the end of the window is less than a 195 | // third of the SYNC_BUFF_LEN then increase the window. 196 | // OR 197 | // If the new offset is sufficiently far away from the end of the window, shrink the 198 | // window. We want to do this because if we never shrink the window, then editing at 199 | // the end of the file creates a large buffer that will have to sync on deletions, 200 | // thus blocking the user and defeating the point of all this. 201 | // 202 | // This is set behind this if to prevent rerunning every single frame and cause a 203 | // potential performance hit. 204 | if window_end.saturating_sub(new_offset) < SYNC_BUFF_LEN / 3 205 | || window_end.saturating_sub(new_offset) > SYNC_BUFF_LEN * 4 / 3 206 | { 207 | self.block(); 208 | self.tx 209 | .send(EditMessage::ModifyWindow((new_offset + SYNC_BUFF_LEN).min(self.len))) 210 | .unwrap(); 211 | } 212 | } 213 | 214 | /// Wait until the background thread has finished processing messages 215 | pub fn block(&self) { 216 | while self.has_work.load(Ordering::SeqCst) { 217 | std::thread::sleep(std::time::Duration::from_millis(1)); 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/windows/editor.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use crate::{ 4 | app::{Action, Data, Nibble}, 5 | label::Handler as LabelHandler, 6 | screen::Handler as ScreenHandler, 7 | }; 8 | 9 | use super::{ 10 | KeyHandler, Window, adjust_offset, 11 | search::{SearchDirection, perform_search}, 12 | }; 13 | 14 | /// The main windows that allow users to edit HEX and ASCII. 15 | #[derive(PartialEq, Eq, Clone, Copy)] 16 | pub(crate) enum Editor { 17 | Ascii, 18 | Hex, 19 | } 20 | 21 | impl KeyHandler for Editor { 22 | fn is_focusing(&self, window_type: Window) -> bool { 23 | match self { 24 | Self::Ascii => window_type == Window::Ascii, 25 | Self::Hex => window_type == Window::Hex, 26 | } 27 | } 28 | fn left(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 29 | app.last_drag = None; 30 | app.drag_nibble = None; 31 | match self { 32 | Self::Ascii => { 33 | app.offset = app.offset.saturating_sub(1); 34 | labels.update_all(&app.contents[app.offset..]); 35 | adjust_offset(app, display, labels); 36 | } 37 | Self::Hex => { 38 | if app.nibble == Nibble::Beginning { 39 | app.offset = app.offset.saturating_sub(1); 40 | labels.update_all(&app.contents[app.offset..]); 41 | adjust_offset(app, display, labels); 42 | } 43 | app.nibble.toggle(); 44 | } 45 | } 46 | } 47 | fn right(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 48 | app.last_drag = None; 49 | app.drag_nibble = None; 50 | match self { 51 | Self::Ascii => { 52 | app.offset = cmp::min(app.offset.saturating_add(1), app.contents.len() - 1); 53 | labels.update_all(&app.contents[app.offset..]); 54 | adjust_offset(app, display, labels); 55 | } 56 | Self::Hex => { 57 | if app.nibble == Nibble::End { 58 | app.offset = cmp::min(app.offset.saturating_add(1), app.contents.len() - 1); 59 | labels.update_all(&app.contents[app.offset..]); 60 | adjust_offset(app, display, labels); 61 | } 62 | app.nibble.toggle(); 63 | } 64 | } 65 | } 66 | fn up(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 67 | app.last_drag = None; 68 | app.drag_nibble = None; 69 | if let Some(new_offset) = app.offset.checked_sub(display.comp_layouts.bytes_per_line) { 70 | app.offset = new_offset; 71 | labels.update_all(&app.contents[app.offset..]); 72 | adjust_offset(app, display, labels); 73 | } 74 | } 75 | fn down(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 76 | app.last_drag = None; 77 | app.drag_nibble = None; 78 | if let Some(new_offset) = app.offset.checked_add(display.comp_layouts.bytes_per_line) 79 | && new_offset < app.contents.len() 80 | { 81 | app.offset = new_offset; 82 | labels.update_all(&app.contents[app.offset..]); 83 | adjust_offset(app, display, labels); 84 | } 85 | } 86 | fn home(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 87 | app.last_drag = None; 88 | app.drag_nibble = None; 89 | let bytes_per_line = display.comp_layouts.bytes_per_line; 90 | app.offset = app.offset / bytes_per_line * bytes_per_line; 91 | labels.update_all(&app.contents[app.offset..]); 92 | adjust_offset(app, display, labels); 93 | 94 | if self.is_focusing(Window::Hex) { 95 | app.nibble = Nibble::Beginning; 96 | } 97 | } 98 | fn end(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 99 | app.last_drag = None; 100 | app.drag_nibble = None; 101 | let bytes_per_line = display.comp_layouts.bytes_per_line; 102 | app.offset = cmp::min( 103 | app.offset + (bytes_per_line - 1 - app.offset % bytes_per_line), 104 | app.contents.len() - 1, 105 | ); 106 | labels.update_all(&app.contents[app.offset..]); 107 | adjust_offset(app, display, labels); 108 | 109 | if self.is_focusing(Window::Hex) { 110 | app.nibble = Nibble::End; 111 | } 112 | } 113 | fn page_up(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 114 | app.last_drag = None; 115 | app.drag_nibble = None; 116 | app.offset = app.offset.saturating_sub( 117 | display.comp_layouts.bytes_per_line * display.comp_layouts.lines_per_screen, 118 | ); 119 | labels.update_all(&app.contents[app.offset..]); 120 | adjust_offset(app, display, labels); 121 | } 122 | fn page_down( 123 | &mut self, 124 | app: &mut Data, 125 | display: &mut ScreenHandler, 126 | labels: &mut LabelHandler, 127 | ) { 128 | app.last_drag = None; 129 | app.drag_nibble = None; 130 | app.offset = cmp::min( 131 | app.offset.saturating_add( 132 | display.comp_layouts.bytes_per_line * display.comp_layouts.lines_per_screen, 133 | ), 134 | app.contents.len() - 1, 135 | ); 136 | labels.update_all(&app.contents[app.offset..]); 137 | adjust_offset(app, display, labels); 138 | } 139 | fn backspace( 140 | &mut self, 141 | app: &mut Data, 142 | display: &mut ScreenHandler, 143 | labels: &mut LabelHandler, 144 | ) { 145 | if app.offset > 0 { 146 | app.actions.push(Action::Delete( 147 | app.offset.saturating_sub(1), 148 | app.contents.remove(app.offset - 1), 149 | )); 150 | app.offset = app.offset.saturating_sub(1); 151 | labels.update_all(&app.contents[app.offset..]); 152 | adjust_offset(app, display, labels); 153 | app.dirty = true; 154 | } 155 | } 156 | fn delete(&mut self, app: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 157 | if app.contents.len() > 1 { 158 | app.actions.push(Action::Delete(app.offset, app.contents.remove(app.offset))); 159 | labels.update_all(&app.contents[app.offset..]); 160 | adjust_offset(app, display, labels); 161 | app.dirty = true; 162 | } 163 | } 164 | fn char( 165 | &mut self, 166 | app: &mut Data, 167 | display: &mut ScreenHandler, 168 | labels: &mut LabelHandler, 169 | c: char, 170 | ) { 171 | app.last_drag = None; 172 | app.drag_nibble = None; 173 | match *self { 174 | Self::Ascii => { 175 | app.actions.push(Action::CharacterInput( 176 | app.offset, 177 | app.contents[app.offset], 178 | None, 179 | )); 180 | app.contents[app.offset] = c as u8; 181 | app.dirty = true; 182 | app.offset = cmp::min(app.offset.saturating_add(1), app.contents.len() - 1); 183 | labels.update_all(&app.contents[app.offset..]); 184 | adjust_offset(app, display, labels); 185 | } 186 | Self::Hex => { 187 | app.actions.push(Action::CharacterInput( 188 | app.offset, 189 | app.contents[app.offset], 190 | Some(app.nibble), 191 | )); 192 | if c.is_ascii_hexdigit() { 193 | // This can probably be optimized... 194 | match app.nibble { 195 | Nibble::Beginning => { 196 | let mut src = c.to_string(); 197 | src.push( 198 | format!("{:02X}", app.contents[app.offset]).chars().last().unwrap(), 199 | ); 200 | let changed = u8::from_str_radix(src.as_str(), 16).unwrap(); 201 | app.contents[app.offset] = changed; 202 | } 203 | Nibble::End => { 204 | let mut src = format!("{:02X}", app.contents[app.offset]) 205 | .chars() 206 | .take(1) 207 | .collect::(); 208 | src.push(c); 209 | let changed = u8::from_str_radix(src.as_str(), 16).unwrap(); 210 | app.contents[app.offset] = changed; 211 | 212 | // Move to the next byte 213 | app.offset = 214 | cmp::min(app.offset.saturating_add(1), app.contents.len() - 1); 215 | labels.update_all(&app.contents[app.offset..]); 216 | adjust_offset(app, display, labels); 217 | } 218 | } 219 | app.nibble.toggle(); 220 | app.dirty = true; 221 | } else { 222 | labels.notification = format!("Invalid Hex: {c}"); 223 | } 224 | } 225 | } 226 | } 227 | 228 | fn enter(&mut self, data: &mut Data, display: &mut ScreenHandler, labels: &mut LabelHandler) { 229 | perform_search(data, display, labels, &SearchDirection::Forward); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/label.rs: -------------------------------------------------------------------------------- 1 | //! Labels in the bottom half of the terminal UI that provide information based on cursor position. 2 | 3 | #![allow(clippy::cast_possible_wrap)] 4 | 5 | use std::fmt::Formatter; 6 | use std::fmt::{self, Write}; 7 | use std::ops::Index; 8 | 9 | pub(crate) static LABEL_TITLES: [&str; 16] = [ 10 | "Signed 8 bit", 11 | "Unsigned 8 bit", 12 | "Signed 16 bit", 13 | "Unsigned 16 bit", 14 | "Signed 32 bit", 15 | "Unsigned 32 bit", 16 | "Signed 64 bit", 17 | "Unsigned 64 bit", 18 | "Hexadecimal", 19 | "Octal", 20 | "Binary", 21 | "Stream Length", 22 | "Float 32 bit", 23 | "Float 64 bit", 24 | "Offset", 25 | "Notifications", 26 | ]; 27 | 28 | #[derive(Default)] 29 | pub(crate) enum Endianness { 30 | #[default] 31 | LittleEndian, 32 | BigEndian, 33 | } 34 | 35 | impl fmt::Display for Endianness { 36 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 37 | match self { 38 | Endianness::LittleEndian => write!(f, "Little Endian"), 39 | Endianness::BigEndian => write!(f, "Big Endian"), 40 | } 41 | } 42 | } 43 | 44 | #[derive(Default)] 45 | pub struct Handler { 46 | signed_eight: String, 47 | signed_sixteen: String, 48 | signed_thirtytwo: String, 49 | signed_sixtyfour: String, 50 | unsigned_eight: String, 51 | unsigned_sixteen: String, 52 | unsigned_thirtytwo: String, 53 | unsigned_sixtyfour: String, 54 | float_thirtytwo: String, 55 | float_sixtyfour: String, 56 | binary: String, 57 | octal: String, 58 | hexadecimal: String, 59 | stream_length: usize, 60 | stream_length_string: String, 61 | pub(crate) offset: String, 62 | pub notification: String, 63 | pub(crate) endianness: Endianness, 64 | } 65 | 66 | impl Index<&str> for Handler { 67 | type Output = String; 68 | 69 | fn index(&self, index: &str) -> &Self::Output { 70 | match index { 71 | "Signed 8 bit" => &self.signed_eight, 72 | "Unsigned 8 bit" => &self.unsigned_eight, 73 | "Signed 16 bit" => &self.signed_sixteen, 74 | "Unsigned 16 bit" => &self.unsigned_sixteen, 75 | "Signed 32 bit" => &self.signed_thirtytwo, 76 | "Unsigned 32 bit" => &self.unsigned_thirtytwo, 77 | "Signed 64 bit" => &self.signed_sixtyfour, 78 | "Unsigned 64 bit" => &self.unsigned_sixtyfour, 79 | "Hexadecimal" => &self.hexadecimal, 80 | "Octal" => &self.octal, 81 | "Binary" => &self.binary, 82 | "Stream Length" => &self.stream_length_string, 83 | "Float 32 bit" => &self.float_thirtytwo, 84 | "Float 64 bit" => &self.float_sixtyfour, 85 | "Offset" => &self.offset, 86 | "Notifications" => &self.notification, 87 | _ => panic!(), 88 | } 89 | } 90 | } 91 | 92 | impl Handler { 93 | pub(crate) fn new(bytes: &[u8], offset: usize) -> Self { 94 | let mut labels = Self { ..Default::default() }; 95 | labels.update_stream_length(8); 96 | labels.update_all(&bytes[offset..]); 97 | labels.offset = format!("{offset:#X?}"); 98 | labels 99 | } 100 | pub(crate) fn update_all(&mut self, bytes: &[u8]) { 101 | let filled_bytes = fill_slice(bytes, 8); 102 | self.update_signed_eight(&filled_bytes[0..1]); 103 | self.update_signed_sixteen(&filled_bytes[0..2]); 104 | self.update_signed_thirtytwo(&filled_bytes[0..4]); 105 | self.update_signed_sixtyfour(&filled_bytes[0..8]); 106 | 107 | self.update_unsigned_eight(&filled_bytes[0..1]); 108 | self.update_unsigned_sixteen(&filled_bytes[0..2]); 109 | self.update_unsigned_thirtytwo(&filled_bytes[0..4]); 110 | self.update_unsigned_sixtyfour(&filled_bytes[0..8]); 111 | 112 | self.update_float_thirtytwo(&filled_bytes[0..4]); 113 | self.update_float_sixtyfour(&filled_bytes[0..8]); 114 | 115 | self.update_streams(bytes); 116 | } 117 | pub(crate) fn update_streams(&mut self, bytes: &[u8]) { 118 | let mut filled_bytes = fill_slice(bytes, self.stream_length / 8); 119 | let remaining_bits = self.stream_length % 8; 120 | if remaining_bits != 0 { 121 | let bits_to_clear = 8 - remaining_bits; 122 | filled_bytes.push( 123 | bytes.get(self.stream_length / 8).unwrap_or(&0) >> bits_to_clear << bits_to_clear, 124 | ); 125 | } 126 | 127 | self.update_binary(&filled_bytes); 128 | self.update_octal(&filled_bytes); 129 | self.update_hexadecimal(&filled_bytes); 130 | } 131 | pub(crate) fn update_stream_length(&mut self, length: usize) { 132 | self.stream_length = length; 133 | self.stream_length_string = self.stream_length.to_string(); 134 | } 135 | pub(crate) fn switch_endianness(&mut self) { 136 | self.endianness = match self.endianness { 137 | Endianness::LittleEndian => Endianness::BigEndian, 138 | Endianness::BigEndian => Endianness::LittleEndian, 139 | }; 140 | } 141 | pub(crate) const fn get_stream_length(&self) -> usize { 142 | self.stream_length 143 | } 144 | fn update_signed_eight(&mut self, bytes: &[u8]) { 145 | self.signed_eight = (bytes[0] as i8).to_string(); 146 | } 147 | fn update_signed_sixteen(&mut self, bytes: &[u8]) { 148 | self.signed_sixteen = match self.endianness { 149 | Endianness::LittleEndian => i16::from_le_bytes(bytes.try_into().unwrap()), 150 | Endianness::BigEndian => i16::from_be_bytes(bytes.try_into().unwrap()), 151 | } 152 | .to_string(); 153 | } 154 | fn update_signed_thirtytwo(&mut self, bytes: &[u8]) { 155 | self.signed_thirtytwo = match self.endianness { 156 | Endianness::LittleEndian => i32::from_le_bytes(bytes.try_into().unwrap()), 157 | Endianness::BigEndian => i32::from_be_bytes(bytes.try_into().unwrap()), 158 | } 159 | .to_string(); 160 | } 161 | fn update_signed_sixtyfour(&mut self, bytes: &[u8]) { 162 | self.signed_sixtyfour = match self.endianness { 163 | Endianness::LittleEndian => i64::from_le_bytes(bytes.try_into().unwrap()), 164 | Endianness::BigEndian => i64::from_be_bytes(bytes.try_into().unwrap()), 165 | } 166 | .to_string(); 167 | } 168 | fn update_unsigned_eight(&mut self, bytes: &[u8]) { 169 | self.unsigned_eight = (bytes[0]).to_string(); 170 | } 171 | fn update_unsigned_sixteen(&mut self, bytes: &[u8]) { 172 | self.unsigned_sixteen = match self.endianness { 173 | Endianness::LittleEndian => u16::from_le_bytes(bytes.try_into().unwrap()), 174 | Endianness::BigEndian => u16::from_be_bytes(bytes.try_into().unwrap()), 175 | } 176 | .to_string(); 177 | } 178 | fn update_unsigned_thirtytwo(&mut self, bytes: &[u8]) { 179 | self.unsigned_thirtytwo = match self.endianness { 180 | Endianness::LittleEndian => u32::from_le_bytes(bytes.try_into().unwrap()), 181 | Endianness::BigEndian => u32::from_be_bytes(bytes.try_into().unwrap()), 182 | } 183 | .to_string(); 184 | } 185 | fn update_unsigned_sixtyfour(&mut self, bytes: &[u8]) { 186 | self.unsigned_sixtyfour = match self.endianness { 187 | Endianness::LittleEndian => u64::from_le_bytes(bytes.try_into().unwrap()), 188 | Endianness::BigEndian => u64::from_be_bytes(bytes.try_into().unwrap()), 189 | } 190 | .to_string(); 191 | } 192 | fn update_float_thirtytwo(&mut self, bytes: &[u8]) { 193 | let value = match self.endianness { 194 | Endianness::LittleEndian => f32::from_le_bytes(bytes.try_into().unwrap()), 195 | Endianness::BigEndian => f32::from_be_bytes(bytes.try_into().unwrap()), 196 | }; 197 | self.float_thirtytwo = format!("{value:e}"); 198 | } 199 | fn update_float_sixtyfour(&mut self, bytes: &[u8]) { 200 | let value = match self.endianness { 201 | Endianness::LittleEndian => f64::from_le_bytes(bytes.try_into().unwrap()), 202 | Endianness::BigEndian => f64::from_be_bytes(bytes.try_into().unwrap()), 203 | }; 204 | self.float_sixtyfour = format!("{value:e}"); 205 | } 206 | fn update_binary(&mut self, bytes: &[u8]) { 207 | self.binary = bytes 208 | .iter() 209 | .fold(String::new(), |mut binary, byte| { 210 | let _ = write!(&mut binary, "{byte:08b}"); 211 | binary 212 | }) 213 | .chars() 214 | .take(self.stream_length) 215 | .collect(); 216 | } 217 | fn update_octal(&mut self, bytes: &[u8]) { 218 | self.octal = 219 | bytes.iter().map(|byte| format!("{byte:03o}")).collect::>().join(" "); 220 | } 221 | fn update_hexadecimal(&mut self, bytes: &[u8]) { 222 | self.hexadecimal = 223 | bytes.iter().map(|byte| format!("{byte:02X}")).collect::>().join(" "); 224 | } 225 | } 226 | 227 | fn fill_slice(bytes: &[u8], len: usize) -> Vec { 228 | if bytes.len() < len { 229 | let mut fill = vec![0; len]; 230 | for (i, byte) in bytes.iter().enumerate() { 231 | fill[i] = *byte; 232 | } 233 | return fill; 234 | } 235 | bytes[0..len].to_vec() 236 | } 237 | 238 | #[cfg(test)] 239 | mod tests { 240 | use super::*; 241 | 242 | #[test] 243 | fn test_binary_label() { 244 | // Given a label handler with the content 'hello' and offset of 0 245 | let content = "hello".as_bytes(); 246 | let mut label_handler = Handler::new(content, 0); 247 | // The binary label should contain the binary veresion of the first character 248 | assert!(label_handler.binary.eq("01101000")); 249 | 250 | // When the stream_length is changed to include 8 more binary digits, 251 | label_handler.stream_length = 16; 252 | label_handler.update_binary(content); 253 | 254 | // The second character should also be represented 255 | assert!(label_handler.binary.eq("0110100001100101")); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | //! The terminal hex editor in its entirety. 2 | //! 3 | //! The application holds the main components of the other modules, like the [`ScreenHandler`], 4 | //! [`LabelHandler`], and input handling, as well as the state data that each of them need. 5 | //! 6 | //! [`ScreenHandler`]: crate::screen::Handler 7 | //! [`LabelHandler`]: crate::label::Handler 8 | 9 | use std::{error::Error, fs::File, process}; 10 | 11 | use arboard::Clipboard; 12 | use ratatui::Frame; 13 | use ratatui::crossterm::event::{self, Event, KeyEventKind}; 14 | use ratatui::layout::Rect; 15 | 16 | use crate::buffer::AsyncBuffer; 17 | use crate::decoder::Encoding; 18 | use crate::windows::search::Search; 19 | use crate::{ 20 | input, 21 | label::Handler as LabelHandler, 22 | screen::Handler as ScreenHandler, 23 | windows::{ 24 | KeyHandler, Window, editor::Editor, jump_to_byte::JumpToByte, 25 | unsaved_changes::UnsavedChanges, 26 | }, 27 | }; 28 | 29 | /// Enum that represent grouping of 4 bits in a byte. 30 | /// 31 | /// For example, the first nibble in 0XF4 is 1111, or the F in hexadecimal. This is specified by 32 | /// [`Nibble::Beginning`]. The last four bits (or 4 in hex) would be specified by [`Nibble::End`]. 33 | #[derive(PartialEq, Copy, Clone, Debug)] 34 | pub(crate) enum Nibble { 35 | Beginning, 36 | End, 37 | } 38 | 39 | impl Nibble { 40 | pub(crate) fn toggle(&mut self) { 41 | match self { 42 | Self::Beginning => *self = Self::End, 43 | Self::End => *self = Self::Beginning, 44 | } 45 | } 46 | } 47 | 48 | /// An instance of a user action, used to implement the undo feature. 49 | /// 50 | /// These actions record the previous state - deleting the first byte (x00) correlates to 51 | /// Delete(0, x00). 52 | pub(crate) enum Action { 53 | /// Tracks a user keypress to modify the contents of the file. 54 | CharacterInput(usize, u8, Option), 55 | 56 | /// Tracks when a user deletes a byte.. 57 | Delete(usize, u8), 58 | } 59 | 60 | /// State Information needed by the [`ScreenHandler`] and [`KeyHandler`]. 61 | pub struct Data { 62 | /// The file under editing. 63 | pub file: File, 64 | 65 | /// The file content. 66 | pub(crate) contents: AsyncBuffer, 67 | 68 | /// The decoding used for the editor. 69 | pub(crate) encoding: Encoding, 70 | 71 | /// The dirty flag, used when the buffer is edited and is not flushed to disk. 72 | pub(crate) dirty: bool, 73 | 74 | /// Offset of the first content byte that is visible on the screen. 75 | pub(crate) start_address: usize, 76 | 77 | /// Offset of the content byte under cursor. 78 | pub(crate) offset: usize, 79 | 80 | /// The nibble that is currently selected in the Hex viewport. 81 | pub(crate) nibble: Nibble, 82 | 83 | /// The last clicked (key down AND key up) label/window. 84 | pub(crate) last_click: Window, 85 | 86 | /// A flag to enable dragging, only when a click is first valid. 87 | pub(crate) drag_enabled: bool, 88 | 89 | /// The most recent cursor location where a drag occurred 90 | pub(crate) last_drag: Option, 91 | 92 | /// The nibble that was last hovered from the drag. 93 | pub(crate) drag_nibble: Option, 94 | 95 | /// Copies label data to your clipboard. 96 | pub(crate) clipboard: Option, 97 | 98 | /// The editor that is currently selected. This editor will be refocused upon a popup closing. 99 | pub(crate) editor: Editor, 100 | 101 | /// A series of actions that keep track of what the user does. 102 | pub(crate) actions: Vec, 103 | 104 | /// Term the user is searching for. 105 | pub(crate) search_term: String, 106 | 107 | /// List of all offsets that the search term was found at. 108 | pub(crate) search_offsets: Vec, 109 | } 110 | 111 | impl Data { 112 | /// Reindexes contents to find locations of the user's search term. 113 | pub(crate) fn reindex_search(&mut self) { 114 | self.search_offsets = self 115 | .contents 116 | .windows(self.search_term.len()) 117 | .enumerate() 118 | .filter_map(|(idx, w)| (w == self.search_term.as_bytes()).then_some(idx)) 119 | .collect(); 120 | 121 | if let Ok(hex_search_term) = hex::decode(self.search_term.replace(' ', "")) { 122 | self.search_offsets.extend( 123 | self.contents 124 | .windows(hex_search_term.len()) 125 | .enumerate() 126 | .filter_map(|(idx, w)| (w == hex_search_term).then_some(idx)) 127 | .collect::>(), 128 | ); 129 | } 130 | } 131 | } 132 | 133 | /// Application provides the user interaction interface and renders the terminal screen in response 134 | /// to user actions. 135 | pub struct Application { 136 | /// The application's state and data. 137 | pub data: Data, 138 | 139 | /// Renders and displays objects to the terminal. 140 | pub(crate) display: ScreenHandler, 141 | 142 | /// The labels at the bottom of the UI that provide information 143 | /// based on the current offset. 144 | pub labels: LabelHandler, 145 | 146 | /// The window that handles keyboard input. This is usually in the form of the Hex/ASCII editor 147 | /// or popups. 148 | pub key_handler: Box, 149 | } 150 | 151 | impl Application { 152 | /// Creates a new application, focusing the Hex editor and starting with an offset of 0 by 153 | /// default. This is called once at the beginning of the program. 154 | /// 155 | /// # Errors 156 | /// 157 | /// This errors out if the file specified is empty. 158 | pub fn new(file: File, encoding: Encoding, offset: usize) -> Result> { 159 | let contents = AsyncBuffer::new(&file)?; 160 | if contents.is_empty() { 161 | eprintln!("heh does not support editing empty files"); 162 | process::exit(1); 163 | } else if offset >= contents.len() { 164 | eprintln!( 165 | "The specified offset ({offset}) is too large! (must be less than {})", 166 | contents.len() 167 | ); 168 | process::exit(1); 169 | } 170 | 171 | let mut labels = LabelHandler::new(&contents, offset); 172 | let clipboard = Clipboard::new().ok(); 173 | if clipboard.is_none() { 174 | labels.notification = String::from("Can't find clipboard!"); 175 | } 176 | 177 | let display = ScreenHandler::new()?; 178 | 179 | let app = Self { 180 | data: Data { 181 | file, 182 | contents, 183 | encoding, 184 | dirty: false, 185 | start_address: (offset / display.comp_layouts.bytes_per_line) 186 | * display.comp_layouts.bytes_per_line, 187 | offset, 188 | nibble: Nibble::Beginning, 189 | last_click: Window::Unhandled, 190 | drag_enabled: false, 191 | last_drag: None, 192 | drag_nibble: None, 193 | clipboard, 194 | editor: Editor::Hex, 195 | actions: vec![], 196 | search_term: String::new(), 197 | search_offsets: Vec::new(), 198 | }, 199 | display, 200 | labels, 201 | key_handler: Box::from(Editor::Hex), 202 | }; 203 | 204 | Ok(app) 205 | } 206 | 207 | /// A loop that repeatedly renders the terminal and modifies state based on input. Is stopped 208 | /// when input handling receives CNTRLq, the command to stop. 209 | /// 210 | /// # Errors 211 | /// 212 | /// This errors when the UI fails to render. 213 | pub fn run(&mut self) -> Result<(), Box> { 214 | ScreenHandler::setup()?; 215 | loop { 216 | self.render_display()?; 217 | let event = event::read()?; 218 | if !self.handle_input(&event)? { 219 | break; 220 | } 221 | } 222 | self.display.teardown()?; 223 | Ok(()) 224 | } 225 | 226 | /// Renders the display. This is a wrapper around [`ScreenHandler`'s 227 | /// render](ScreenHandler::render) method. 228 | fn render_display(&mut self) -> Result<(), Box> { 229 | self.display.render(&mut self.data, &self.labels, self.key_handler.as_ref()) 230 | } 231 | 232 | /// Renders a single frame for the given area. 233 | pub fn render_frame(&mut self, frame: &mut Frame, area: Rect) { 234 | self.data.contents.compute_new_window(self.data.offset); 235 | // We check if we need to recompute the terminal size in the case that the saved off 236 | // variable differs from the current frame, which can occur when a terminal is resized 237 | // between an event handling and a rendering. 238 | if area != self.display.terminal_size { 239 | self.display.terminal_size = area; 240 | self.display.comp_layouts = 241 | ScreenHandler::calculate_dimensions(area, self.key_handler.as_ref()); 242 | // We change the start_address here to ensure that 0 is ALWAYS the first start 243 | // address. We round to preventing constant resizing always moving to 0. 244 | self.data.start_address = (self.data.start_address 245 | + (self.display.comp_layouts.bytes_per_line / 2)) 246 | / self.display.comp_layouts.bytes_per_line 247 | * self.display.comp_layouts.bytes_per_line; 248 | } 249 | ScreenHandler::render_frame( 250 | frame, 251 | self.display.terminal_size, 252 | &mut self.data, 253 | &self.labels, 254 | self.key_handler.as_ref(), 255 | &self.display.comp_layouts, 256 | ); 257 | } 258 | 259 | /// Handles all forms of user input. This calls out to code in [input], which uses 260 | /// [Application's `key_handler` method](Application::key_handler) to determine what to do for 261 | /// key input. 262 | /// 263 | /// # Errors 264 | /// 265 | /// This errors when handling the key event fails. 266 | pub fn handle_input(&mut self, event: &Event) -> Result> { 267 | match event { 268 | Event::Key(key) => { 269 | if key.kind == KeyEventKind::Press { 270 | self.labels.notification.clear(); 271 | return input::handle_key_input(self, *key); 272 | } 273 | } 274 | Event::Mouse(mouse) => { 275 | self.labels.notification.clear(); 276 | input::handle_mouse_input(self, *mouse); 277 | } 278 | Event::Resize(_, _) | Event::FocusGained | Event::FocusLost | Event::Paste(_) => {} 279 | } 280 | Ok(true) 281 | } 282 | 283 | /// Sets the current [`KeyHandler`]. This should be used when trying to focus another window. 284 | /// Setting the [`KeyHandler`] directly could cause errors. 285 | /// 286 | /// Popup dimensions are also changed here and are safe to do so because there are currently 287 | /// no popups that have dimensions based off of the size of the terminal frame. 288 | pub(crate) fn set_focused_window(&mut self, window: Window) { 289 | match window { 290 | Window::Hex => { 291 | self.key_handler = Box::from(Editor::Hex); 292 | self.data.editor = Editor::Hex; 293 | } 294 | Window::Ascii => { 295 | self.key_handler = Box::from(Editor::Ascii); 296 | self.data.editor = Editor::Ascii; 297 | } 298 | Window::JumpToByte => { 299 | self.key_handler = Box::from(JumpToByte::new()); 300 | self.display.comp_layouts.popup = ScreenHandler::calculate_popup_dimensions( 301 | self.display.terminal_size, 302 | self.key_handler.as_ref(), 303 | ); 304 | } 305 | Window::Search => { 306 | self.key_handler = Box::from(Search::new()); 307 | self.display.comp_layouts.popup = ScreenHandler::calculate_popup_dimensions( 308 | self.display.terminal_size, 309 | self.key_handler.as_ref(), 310 | ); 311 | } 312 | Window::UnsavedChanges => { 313 | self.key_handler = Box::from(UnsavedChanges::new()); 314 | self.display.comp_layouts.popup = ScreenHandler::calculate_popup_dimensions( 315 | self.display.terminal_size, 316 | self.key_handler.as_ref(), 317 | ); 318 | } 319 | // We should never try and focus these windows to accept input. 320 | Window::Unhandled | Window::Label(_) => { 321 | panic!() 322 | } 323 | } 324 | } 325 | 326 | /// Focuses the previously selected editor and is usually invoked after closing a popup. 327 | pub(crate) fn focus_editor(&mut self) { 328 | self.key_handler = Box::from(self.data.editor); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | //! Handles user input. 2 | //! 3 | //! This is where mouse actions are programmed. It's also a wrapper around calls to a dynamic 4 | //! [`KeyHandler`](crate::windows::KeyHandler), which handles keyboared input. 5 | 6 | use std::{ 7 | cmp, 8 | error::Error, 9 | io::{Seek, Write}, 10 | }; 11 | 12 | use ratatui::crossterm::event::{ 13 | KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, 14 | }; 15 | 16 | use crate::{ 17 | app::{Action, Application, Nibble}, 18 | label::LABEL_TITLES, 19 | windows::{ 20 | PopupOutput, Window, adjust_offset, 21 | search::{SearchDirection, perform_search}, 22 | }, 23 | }; 24 | 25 | /// Wrapper function that calls the corresponding [`KeyHandler`](crate::windows::KeyHandler) methods of 26 | /// [the application's `key_handler`.](Application::key_handler) 27 | pub(crate) fn handle_key_input( 28 | app: &mut Application, 29 | key: KeyEvent, 30 | ) -> Result> { 31 | match key.code { 32 | // Arrow key input 33 | KeyCode::Left => { 34 | app.key_handler.left(&mut app.data, &mut app.display, &mut app.labels); 35 | } 36 | KeyCode::Right => { 37 | app.key_handler.right(&mut app.data, &mut app.display, &mut app.labels); 38 | } 39 | KeyCode::Up => { 40 | app.key_handler.up(&mut app.data, &mut app.display, &mut app.labels); 41 | } 42 | KeyCode::Down => { 43 | app.key_handler.down(&mut app.data, &mut app.display, &mut app.labels); 44 | } 45 | 46 | // Cursor shortcuts 47 | KeyCode::Home => { 48 | app.key_handler.home(&mut app.data, &mut app.display, &mut app.labels); 49 | } 50 | KeyCode::End => { 51 | app.key_handler.end(&mut app.data, &mut app.display, &mut app.labels); 52 | } 53 | KeyCode::PageUp => { 54 | app.key_handler.page_up(&mut app.data, &mut app.display, &mut app.labels); 55 | } 56 | KeyCode::PageDown => { 57 | app.key_handler.page_down(&mut app.data, &mut app.display, &mut app.labels); 58 | } 59 | 60 | // Removals 61 | KeyCode::Backspace => { 62 | app.key_handler.backspace(&mut app.data, &mut app.display, &mut app.labels); 63 | } 64 | KeyCode::Delete => { 65 | app.key_handler.delete(&mut app.data, &mut app.display, &mut app.labels); 66 | } 67 | KeyCode::Esc => { 68 | app.focus_editor(); 69 | } 70 | 71 | KeyCode::Enter => { 72 | if app.key_handler.is_focusing(Window::UnsavedChanges) 73 | && app.key_handler.get_user_input() == PopupOutput::Boolean(true) 74 | { 75 | return Ok(false); 76 | } 77 | app.key_handler.enter(&mut app.data, &mut app.display, &mut app.labels); 78 | app.focus_editor(); 79 | } 80 | 81 | KeyCode::Char(char) => { 82 | // Because CNTRLq is the signal to quit, we propogate the message 83 | // if this handling method returns false 84 | return handle_character_input(app, char, key.modifiers); 85 | } 86 | _ => {} 87 | } 88 | Ok(true) 89 | } 90 | 91 | /// Handles a character key press. While used predominantly to edit a file, it also checks for 92 | /// any shortcut commands being used. 93 | pub(crate) fn handle_character_input( 94 | app: &mut Application, 95 | char: char, 96 | modifiers: KeyModifiers, 97 | ) -> Result> { 98 | if modifiers == KeyModifiers::CONTROL { 99 | return handle_control_options(char, app); 100 | } else if modifiers == KeyModifiers::ALT { 101 | match char { 102 | '=' => { 103 | app.labels.update_stream_length(cmp::min(app.labels.get_stream_length() + 1, 64)); 104 | app.labels.update_streams(&app.data.contents[app.data.offset..]); 105 | } 106 | '-' => { 107 | app.labels.update_stream_length(cmp::max( 108 | app.labels.get_stream_length().saturating_sub(1), 109 | 0, 110 | )); 111 | app.labels.update_streams(&app.data.contents[app.data.offset..]); 112 | } 113 | _ => {} 114 | } 115 | } else if modifiers | KeyModifiers::NONE | KeyModifiers::SHIFT 116 | == KeyModifiers::NONE | KeyModifiers::SHIFT 117 | { 118 | let is_hex = app.key_handler.is_focusing(Window::Hex); 119 | 120 | match char { 121 | 'q' if is_hex => { 122 | if !app.key_handler.is_focusing(Window::UnsavedChanges) { 123 | if !app.data.dirty { 124 | return Ok(false); 125 | } 126 | app.set_focused_window(Window::UnsavedChanges); 127 | } 128 | } 129 | 'h' if is_hex => { 130 | app.key_handler.left(&mut app.data, &mut app.display, &mut app.labels); 131 | } 132 | 'l' if is_hex => { 133 | app.key_handler.right(&mut app.data, &mut app.display, &mut app.labels); 134 | } 135 | 'k' if is_hex => { 136 | app.key_handler.up(&mut app.data, &mut app.display, &mut app.labels); 137 | } 138 | 'j' if is_hex => { 139 | app.key_handler.down(&mut app.data, &mut app.display, &mut app.labels); 140 | } 141 | '^' if is_hex => { 142 | app.key_handler.home(&mut app.data, &mut app.display, &mut app.labels); 143 | } 144 | '$' if is_hex => { 145 | app.key_handler.end(&mut app.data, &mut app.display, &mut app.labels); 146 | } 147 | '/' if is_hex => { 148 | app.set_focused_window(Window::Search); 149 | } 150 | _ => { 151 | app.key_handler.char(&mut app.data, &mut app.display, &mut app.labels, char); 152 | } 153 | } 154 | } 155 | Ok(true) 156 | } 157 | 158 | fn handle_control_options(char: char, app: &mut Application) -> Result> { 159 | match char { 160 | 'j' => { 161 | if app.key_handler.is_focusing(Window::JumpToByte) { 162 | app.focus_editor(); 163 | } else { 164 | app.set_focused_window(Window::JumpToByte); 165 | } 166 | } 167 | 'f' => { 168 | if app.key_handler.is_focusing(Window::Search) { 169 | app.focus_editor(); 170 | } else { 171 | app.set_focused_window(Window::Search); 172 | } 173 | } 174 | 'q' => { 175 | if !app.key_handler.is_focusing(Window::UnsavedChanges) { 176 | if !app.data.dirty { 177 | return Ok(false); 178 | } 179 | app.set_focused_window(Window::UnsavedChanges); 180 | } 181 | } 182 | 's' => { 183 | app.data.contents.block(); 184 | app.data.file.rewind()?; 185 | app.data.file.write_all(&app.data.contents)?; 186 | app.data.file.set_len(app.data.contents.len() as u64)?; 187 | 188 | app.data.dirty = false; 189 | 190 | app.labels.notification = String::from("Saved!"); 191 | } 192 | 'e' => { 193 | app.labels.switch_endianness(); 194 | app.labels.update_all(&app.data.contents[app.data.offset..]); 195 | 196 | app.labels.notification = app.labels.endianness.to_string(); 197 | } 198 | 'd' => { 199 | app.key_handler.page_down(&mut app.data, &mut app.display, &mut app.labels); 200 | } 201 | 'u' => { 202 | app.key_handler.page_up(&mut app.data, &mut app.display, &mut app.labels); 203 | } 204 | 'n' => { 205 | perform_search( 206 | &mut app.data, 207 | &mut app.display, 208 | &mut app.labels, 209 | &SearchDirection::Forward, 210 | ); 211 | } 212 | 'p' => { 213 | perform_search( 214 | &mut app.data, 215 | &mut app.display, 216 | &mut app.labels, 217 | &SearchDirection::Backward, 218 | ); 219 | } 220 | 'z' => { 221 | if let Some(action) = app.data.actions.pop() { 222 | match action { 223 | Action::CharacterInput(offset, byte, nibble) => { 224 | app.data.offset = offset; 225 | if let Some(nibble) = nibble { 226 | app.data.nibble = nibble; 227 | } 228 | app.data.contents[offset] = byte; 229 | } 230 | Action::Delete(offset, byte) => { 231 | app.data.contents.insert(offset, byte); 232 | app.data.offset = offset; 233 | } 234 | } 235 | } 236 | } 237 | _ => {} 238 | } 239 | Ok(true) 240 | } 241 | 242 | /// Handles the mouse input, which consists of things like scrolling and focusing components 243 | /// based on a left and right click. 244 | pub(crate) fn handle_mouse_input(app: &mut Application, mouse: MouseEvent) { 245 | let component = 246 | app.display.identify_clicked_component(mouse.row, mouse.column, app.key_handler.as_ref()); 247 | match mouse.kind { 248 | MouseEventKind::Down(MouseButton::Left) => { 249 | app.data.last_click = component; 250 | match app.data.last_click { 251 | Window::Ascii => { 252 | if let Some((cursor_pos, _)) = handle_editor_click(Window::Ascii, app, mouse) { 253 | app.data.offset = cursor_pos; 254 | } 255 | } 256 | Window::Hex => { 257 | if let Some((cursor_pos, nibble)) = handle_editor_click(Window::Hex, app, mouse) 258 | { 259 | app.data.offset = cursor_pos; 260 | app.data.nibble = nibble.expect("Clicking on Hex should return a nibble!"); 261 | } 262 | } 263 | Window::Label(_) 264 | | Window::Unhandled 265 | | Window::JumpToByte 266 | | Window::Search 267 | | Window::UnsavedChanges => {} 268 | } 269 | } 270 | MouseEventKind::Drag(MouseButton::Left) => { 271 | if app.data.drag_enabled { 272 | match app.data.last_click { 273 | Window::Ascii => { 274 | if let Some((cursor_pos, _)) = handle_editor_drag(Window::Ascii, app, mouse) 275 | { 276 | if app.data.last_drag.is_none() { 277 | app.data.last_drag = Some(app.data.offset); 278 | } 279 | app.data.offset = cursor_pos; 280 | app.labels.update_all(&app.data.contents[app.data.offset..]); 281 | adjust_offset(&mut app.data, &mut app.display, &mut app.labels); 282 | } 283 | } 284 | Window::Hex => { 285 | if let Some((cursor_pos, nibble)) = 286 | handle_editor_drag(Window::Hex, app, mouse) 287 | { 288 | if app.data.last_drag.is_none() { 289 | app.data.last_drag = Some(app.data.offset); 290 | app.data.drag_nibble = Some(app.data.nibble); 291 | } 292 | app.data.offset = cursor_pos; 293 | app.data.nibble = nibble.unwrap(); 294 | app.labels.update_all(&app.data.contents[app.data.offset..]); 295 | adjust_offset(&mut app.data, &mut app.display, &mut app.labels); 296 | } 297 | } 298 | Window::Label(_) 299 | | Window::Unhandled 300 | | Window::JumpToByte 301 | | Window::Search 302 | | Window::UnsavedChanges => {} 303 | } 304 | } 305 | } 306 | MouseEventKind::Up(MouseButton::Left) => { 307 | match component { 308 | Window::Label(i) => { 309 | if app.data.last_click == component { 310 | // Put string into clipboard 311 | if let Some(clipboard) = app.data.clipboard.as_mut() { 312 | clipboard.set_text(app.labels[LABEL_TITLES[i]].clone()).unwrap(); 313 | app.labels.notification = format!("{} copied!", LABEL_TITLES[i]); 314 | } else { 315 | app.labels.notification = String::from("Can't find clipboard!"); 316 | } 317 | } 318 | } 319 | Window::Hex 320 | | Window::Ascii 321 | | Window::Unhandled 322 | | Window::JumpToByte 323 | | Window::Search 324 | | Window::UnsavedChanges => {} 325 | } 326 | } 327 | MouseEventKind::ScrollUp => { 328 | let bytes_per_line = app.display.comp_layouts.bytes_per_line; 329 | 330 | // Scroll up a line in the viewport without changing cursor. 331 | app.data.start_address = app.data.start_address.saturating_sub(bytes_per_line); 332 | } 333 | MouseEventKind::ScrollDown => { 334 | let bytes_per_line = app.display.comp_layouts.bytes_per_line; 335 | let lines_per_screen = app.display.comp_layouts.lines_per_screen; 336 | 337 | let content_lines = app.data.contents.len() / bytes_per_line + 1; 338 | let start_row = app.data.start_address / bytes_per_line; 339 | 340 | // Scroll down a line in the viewport without changing cursor. 341 | // Until the viewport contains the last page of content. 342 | if start_row + lines_per_screen < content_lines { 343 | app.data.start_address = app.data.start_address.saturating_add(bytes_per_line); 344 | } 345 | } 346 | _ => {} 347 | } 348 | } 349 | 350 | /// A wrapper around [`handle_editor_cursor_action`] that does the additional things that come with a click. 351 | #[allow(clippy::cast_possible_truncation)] 352 | fn handle_editor_click( 353 | window: Window, 354 | app: &mut Application, 355 | mut mouse: MouseEvent, 356 | ) -> Option<(usize, Option)> { 357 | app.set_focused_window(window); 358 | 359 | let (editor, word_size) = match window { 360 | Window::Ascii => (&app.display.comp_layouts.ascii, 1), 361 | Window::Hex => (&app.display.comp_layouts.hex, 3), 362 | _ => { 363 | panic!("Trying to move cursor on unhandled window!") 364 | } 365 | }; 366 | 367 | // In the hex editor, a cursor click in between two bytes will select the first nibble of the 368 | // latter one. In the case that we're at the end of the row, this is just a tweak so that the 369 | // cursor is selected as the last nibble of the first byte. 370 | let end_of_row = editor.x + app.display.comp_layouts.bytes_per_line as u16 * word_size; 371 | if mouse.column == end_of_row { 372 | mouse.column = end_of_row; 373 | } 374 | let res = handle_editor_cursor_action(window, app, mouse); 375 | if res.is_some() { 376 | // Reset the dragged highlighting. 377 | app.data.last_drag = None; 378 | app.data.drag_nibble = None; 379 | app.data.drag_enabled = true; 380 | } else { 381 | app.data.drag_enabled = false; 382 | } 383 | res 384 | } 385 | 386 | /// A wrapper around [`handle_editor_cursor_action`] that does the additional things that come with a drag. 387 | #[allow(clippy::cast_possible_truncation)] 388 | fn handle_editor_drag( 389 | window: Window, 390 | app: &mut Application, 391 | mut mouse: MouseEvent, 392 | ) -> Option<(usize, Option)> { 393 | let (editor, word_size) = match window { 394 | Window::Ascii => (&app.display.comp_layouts.ascii, 1), 395 | Window::Hex => (&app.display.comp_layouts.hex, 3), 396 | _ => { 397 | panic!("Trying to move cursor on unhandled window!") 398 | } 399 | }; 400 | 401 | let click_past_contents = app.display.comp_layouts.bytes_per_line 402 | * app.display.comp_layouts.lines_per_screen 403 | + app.data.start_address 404 | > app.data.contents.len(); 405 | 406 | let mut editor_last_col = app.display.comp_layouts.bytes_per_line as u16; 407 | let mut end_of_row = 1 + editor.x + (editor_last_col * word_size); 408 | 409 | // Allows cursor x position to be tracked outside of the initially selected viewport when 410 | // dragged. Quickly dragging to the right will select everything to the end of the row. 411 | if mouse.column <= editor.left() { 412 | mouse.column = editor.x + 1; 413 | } else if mouse.column >= end_of_row { 414 | mouse.column = end_of_row; 415 | } 416 | 417 | // Allows the view port to be moved up and down depending on the cursor has been dragged way 418 | // above or below it. 419 | let editor_bottom_row = editor.top() 420 | + 1 421 | + cmp::min( 422 | app.display.comp_layouts.lines_per_screen, 423 | (app.data.contents.len() - app.data.start_address) 424 | / app.display.comp_layouts.bytes_per_line, 425 | ) as u16; 426 | if mouse.row == 0 { 427 | mouse.row = 1; 428 | if let Some(mut result) = handle_editor_cursor_action(window, app, mouse) { 429 | if let Some(new_y) = result.0.checked_sub(app.display.comp_layouts.bytes_per_line) { 430 | result.0 = new_y; 431 | return Some(result); 432 | } 433 | return Some(result); 434 | } 435 | None 436 | } else if mouse.row > editor_bottom_row { 437 | // When the mouse is dragged past the end of the contents, we need to update drag, but not 438 | // change the start address/scroll. 439 | if click_past_contents { 440 | editor_last_col = ((app.data.contents.len() - app.data.start_address) 441 | % app.display.comp_layouts.bytes_per_line) as u16; 442 | end_of_row = 1 + editor.x + (editor_last_col * word_size); 443 | if mouse.column >= end_of_row { 444 | mouse.column = end_of_row; 445 | } 446 | } 447 | mouse.row = editor_bottom_row - u16::from(!click_past_contents); 448 | if let Some(mut result) = handle_editor_cursor_action(window, app, mouse) { 449 | if let Some(new_y) = result.0.checked_add(app.display.comp_layouts.bytes_per_line) 450 | && new_y < app.data.contents.len() 451 | { 452 | result.0 = new_y; 453 | return Some(result); 454 | } 455 | return Some(result); 456 | } 457 | None 458 | } else { 459 | handle_editor_cursor_action(window, app, mouse) 460 | } 461 | } 462 | 463 | /// Determines if the relative cursor/drag position should be updated. 464 | #[allow(clippy::cast_possible_truncation)] 465 | fn handle_editor_cursor_action( 466 | window: Window, 467 | app: &mut Application, 468 | mouse: MouseEvent, 469 | ) -> Option<(usize, Option)> { 470 | let (editor, word_size) = match window { 471 | Window::Ascii => (&app.display.comp_layouts.ascii, 1), 472 | Window::Hex => (&app.display.comp_layouts.hex, 3), 473 | _ => { 474 | panic!("Trying to move cursor on unhandled window!") 475 | } 476 | }; 477 | // Identify the byte that was clicked on based on the relative position. 478 | let (mut rel_x, mut rel_y) = 479 | (mouse.column.saturating_sub(editor.x), mouse.row.saturating_sub(editor.y)); 480 | 481 | // Do not consider a click to the space after the last byte of a full viewport to be a click. 482 | // The space after the last byte of every row is generally considered a click for the first 483 | // byte on the next row for dragging purposes. 484 | if rel_y == editor.height - 2 485 | && rel_x 486 | > app.display.comp_layouts.bytes_per_line as u16 * word_size 487 | - u16::from(window == Window::Hex) 488 | { 489 | return None; 490 | } 491 | 492 | // Account for the border of the viewport and only allow clicks to the end of the last 493 | // character. 494 | if rel_y > 0 && rel_x > 0 && editor.height - 1 > rel_y && editor.width - 1 > rel_x { 495 | match window { 496 | Window::Ascii => { 497 | (rel_x, rel_y) = (rel_x - 1, rel_y - 1); 498 | let content_pos = app.data.start_address 499 | + (rel_y as usize * app.display.comp_layouts.bytes_per_line) 500 | + (rel_x as usize); 501 | if content_pos < app.data.contents.len() { 502 | return Some((content_pos, None)); 503 | } 504 | } 505 | Window::Hex => { 506 | (rel_x, rel_y) = (rel_x, rel_y - 1); 507 | let content_pos = app.data.start_address 508 | + (rel_y as usize * app.display.comp_layouts.bytes_per_line) 509 | + (rel_x as usize / 3); 510 | if content_pos < app.data.contents.len() { 511 | if rel_x % 3 < 2 { 512 | return Some((content_pos, Some(Nibble::Beginning))); 513 | } 514 | return Some((content_pos, Some(Nibble::End))); 515 | } 516 | } 517 | _ => { 518 | panic!() 519 | } 520 | } 521 | } 522 | None 523 | } 524 | -------------------------------------------------------------------------------- /src/screen.rs: -------------------------------------------------------------------------------- 1 | //! In charge of calculating dimensions and displaying everything. 2 | 3 | use std::{ 4 | cmp, 5 | error::Error, 6 | io::{self, Stdout}, 7 | rc::Rc, 8 | }; 9 | 10 | use ratatui::{ 11 | Frame, Terminal, 12 | backend::CrosstermBackend, 13 | crossterm::{ 14 | event::{DisableMouseCapture, EnableMouseCapture}, 15 | execute, 16 | terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, 17 | }, 18 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 19 | style::{Color, Style}, 20 | text::{Line, Span, Text}, 21 | widgets::{Block, Borders, Clear, Paragraph}, 22 | }; 23 | 24 | use crate::chunk::OverlappingChunks; 25 | use crate::{ 26 | app::{Data, Nibble}, 27 | decoder::ByteAlignedDecoder, 28 | label::{Handler as LabelHandler, LABEL_TITLES}, 29 | windows::{KeyHandler, Window, editor::Editor}, 30 | }; 31 | 32 | const COLOR_NULL: Color = Color::DarkGray; 33 | 34 | pub struct Handler { 35 | pub terminal: Terminal>, 36 | pub terminal_size: Rect, 37 | pub comp_layouts: ComponentLayouts, 38 | } 39 | 40 | pub struct ComponentLayouts { 41 | line_numbers: Rect, 42 | pub(crate) hex: Rect, 43 | pub(crate) ascii: Rect, 44 | labels: Rc>, 45 | pub(crate) popup: Rect, 46 | pub(crate) bytes_per_line: usize, 47 | pub(crate) lines_per_screen: usize, 48 | } 49 | 50 | impl Handler { 51 | /// Creates a new screen handler. 52 | /// 53 | /// # Errors 54 | /// 55 | /// This errors when constructing the terminal or retrieving the terminal size fails. 56 | pub fn new() -> Result> { 57 | let terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; 58 | let size = terminal.size()?; 59 | let terminal_size = Rect::new(0, 0, size.width, size.height); 60 | Ok(Self { 61 | terminal, 62 | terminal_size, 63 | comp_layouts: Self::calculate_dimensions(terminal_size, &Editor::Hex), 64 | }) 65 | } 66 | pub(crate) fn setup() -> Result<(), Box> { 67 | enable_raw_mode()?; 68 | execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; 69 | Ok(()) 70 | } 71 | pub(crate) fn teardown(&mut self) -> Result<(), Box> { 72 | disable_raw_mode()?; 73 | execute!(self.terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; 74 | self.terminal.show_cursor()?; 75 | Ok(()) 76 | } 77 | pub(crate) fn identify_clicked_component( 78 | &self, 79 | row: u16, 80 | col: u16, 81 | window: &dyn KeyHandler, 82 | ) -> Window { 83 | let click = Rect::new(col, row, 0, 0); 84 | let popup_enabled = !(window.is_focusing(Window::Hex) || window.is_focusing(Window::Ascii)); 85 | if popup_enabled && self.comp_layouts.popup.union(click) == self.comp_layouts.popup { 86 | return Window::Unhandled; 87 | } else if self.comp_layouts.hex.union(click) == self.comp_layouts.hex { 88 | return Window::Hex; 89 | } else if self.comp_layouts.ascii.union(click) == self.comp_layouts.ascii { 90 | return Window::Ascii; 91 | } 92 | for (i, &label) in self.comp_layouts.labels.iter().enumerate() { 93 | if label.union(click) == label { 94 | return Window::Label(i); 95 | } 96 | } 97 | Window::Unhandled 98 | } 99 | 100 | /// Calculates the dimensions of the components that will be continually displayed. 101 | /// 102 | /// This includes the editors, labels, and address table. 103 | pub fn calculate_dimensions(frame: Rect, window: &dyn KeyHandler) -> ComponentLayouts { 104 | // Establish Constraints 105 | let sections = Layout::default() 106 | .direction(Direction::Vertical) 107 | .constraints([Constraint::Min(3), Constraint::Length(12)]) 108 | .split(frame); 109 | let editors = Layout::default() 110 | .direction(Direction::Horizontal) 111 | .constraints([ 112 | Constraint::Length(10), 113 | // The address table is Length(10) as specified above. Because the hex editor takes 114 | // 3 graphemes for every 1 that ASCII takes (each nibble plus a space), we multiply 115 | // the editors by those ratios. 116 | Constraint::Length((frame.width - 10) * 3 / 4), 117 | Constraint::Length((frame.width - 10) / 4 + 1), 118 | ]) 119 | .split(sections[0]); 120 | let mut labels = Rc::new(Vec::with_capacity(12)); 121 | let label_columns = Layout::default() 122 | .direction(Direction::Horizontal) 123 | .constraints([ 124 | Constraint::Ratio(1, 4), 125 | Constraint::Ratio(1, 4), 126 | Constraint::Ratio(1, 4), 127 | Constraint::Ratio(1, 4), 128 | ]) 129 | .split(sections[1]); 130 | for label in &*label_columns { 131 | let column_layout = &mut Layout::default() 132 | .direction(Direction::Vertical) 133 | .constraints([ 134 | Constraint::Ratio(1, 4), 135 | Constraint::Ratio(1, 4), 136 | Constraint::Ratio(1, 4), 137 | Constraint::Ratio(1, 4), 138 | ]) 139 | .split(*label); 140 | 141 | if let Some(labels) = Rc::get_mut(&mut labels) { 142 | labels.extend_from_slice(column_layout); 143 | } 144 | } 145 | 146 | // Calculate popup dimensions 147 | let popup = Self::calculate_popup_dimensions(frame, window); 148 | 149 | // Calculate bytes per line 150 | let bytes_per_line = ((editors[1].width - 2) / 3) as usize; 151 | let lines_per_screen = (editors[1].height - 2) as usize; 152 | 153 | ComponentLayouts { 154 | line_numbers: editors[0], 155 | hex: editors[1], 156 | ascii: editors[2], 157 | popup, 158 | bytes_per_line, 159 | lines_per_screen, 160 | labels: labels.to_vec().into(), 161 | } 162 | } 163 | 164 | /// Calculates the dimensions of the popup that is being focused. Currently used in 165 | /// [`calculate_dimensions`](Self::calculate_dimensions) and 166 | /// [`set_focused_window`](crate::app::Application::set_focused_window) 167 | /// since the dimensions are constant and are only changed when the popup changes. 168 | /// 169 | /// In the case that window is a an editor and not a popup, returns the default Rect, 170 | /// which is essentially not displayed at all. 171 | pub(crate) fn calculate_popup_dimensions(frame: Rect, window: &dyn KeyHandler) -> Rect { 172 | window.dimensions().map_or_else(Rect::default, |dimensions| popup_rect(dimensions, frame)) 173 | } 174 | 175 | /// Generates all the visuals of the file contents to be displayed to user by calling 176 | /// [`generate_hex`] and [`generate_decoded`]. 177 | fn generate_text( 178 | app_info: &mut Data, 179 | bytes_per_line: usize, 180 | lines_per_screen: usize, 181 | ) -> (Text<'_>, Text<'_>, Text<'_>) { 182 | let content_lines = app_info.contents.len() / bytes_per_line + 1; 183 | let start_row = app_info.start_address / bytes_per_line; 184 | 185 | // Generate address lines 186 | let address_text = (0..cmp::min(lines_per_screen, content_lines - start_row)) 187 | .map(|i| { 188 | let row_address = app_info.start_address + i * bytes_per_line; 189 | let mut span = Span::from(format!("{row_address:08X?}\n")); 190 | // Highlight the address row that the cursor is in for visibility 191 | if (row_address..row_address + bytes_per_line).contains(&app_info.offset) { 192 | span.style = span.style.fg(Color::Black).bg(Color::White); 193 | } 194 | Line::from(span) 195 | }) 196 | .collect::>(); 197 | 198 | let hex_text = generate_hex(app_info, bytes_per_line, lines_per_screen); 199 | let decoded_text = generate_decoded(app_info, bytes_per_line, lines_per_screen); 200 | 201 | (address_text.into(), hex_text.into(), decoded_text.into()) 202 | } 203 | 204 | /// Display the addresses, editors, labels, and popups based off of the specifications of 205 | /// [`ComponentLayouts`], defined by 206 | /// [`calculate_dimensions`](Self::calculate_dimensions). 207 | pub(crate) fn render( 208 | &mut self, 209 | app_info: &mut Data, 210 | labels: &LabelHandler, 211 | window: &dyn KeyHandler, 212 | ) -> Result<(), Box> { 213 | app_info.contents.compute_new_window(app_info.offset); 214 | 215 | self.terminal.draw(|frame| { 216 | // We check if we need to recompute the terminal size in the case that the saved off 217 | // variable differs from the current frame, which can occur when a terminal is resized 218 | // between an event handling and a rendering. 219 | let size = frame.area(); 220 | if size != self.terminal_size { 221 | self.terminal_size = size; 222 | self.comp_layouts = Self::calculate_dimensions(self.terminal_size, window); 223 | 224 | // We change the start_address here to ensure that 0 is ALWAYS the first start 225 | // address. We round to preventing constant resizing always moving to 0. 226 | app_info.start_address = (app_info.start_address 227 | + (self.comp_layouts.bytes_per_line / 2)) 228 | / self.comp_layouts.bytes_per_line 229 | * self.comp_layouts.bytes_per_line; 230 | } 231 | 232 | Self::render_frame( 233 | frame, 234 | self.terminal_size, 235 | app_info, 236 | labels, 237 | window, 238 | &self.comp_layouts, 239 | ); 240 | })?; 241 | Ok(()) 242 | } 243 | 244 | /// Display the addresses, editors, labels, and popups based off of the specifications of 245 | /// [`ComponentLayouts`], defined by 246 | /// [`calculate_dimensions`](Self::calculate_dimensions). 247 | pub(crate) fn render_frame( 248 | frame: &mut Frame, 249 | area: Rect, 250 | app_info: &mut Data, 251 | labels: &LabelHandler, 252 | window: &dyn KeyHandler, 253 | comp_layouts: &ComponentLayouts, 254 | ) { 255 | // Check if terminal is large enough 256 | if area.width < 50 || area.height < 15 { 257 | let dimension_notification = Paragraph::new("Terminal dimensions must be larger!") 258 | .block(Block::default()) 259 | .alignment(Alignment::Center); 260 | let vertical_center = Layout::default() 261 | .direction(Direction::Vertical) 262 | .constraints([ 263 | Constraint::Percentage(40), 264 | Constraint::Percentage(20), 265 | Constraint::Percentage(40), 266 | ]) 267 | .split(area); 268 | frame.render_widget(dimension_notification, vertical_center[1]); 269 | return; 270 | } 271 | 272 | let (address_text, hex_text, ascii_text) = Self::generate_text( 273 | app_info, 274 | comp_layouts.bytes_per_line, 275 | comp_layouts.lines_per_screen, 276 | ); 277 | 278 | // Render Line Numbers 279 | frame.render_widget( 280 | Paragraph::new(address_text) 281 | .block(Block::default().borders(Borders::ALL).title("Address")), 282 | comp_layouts.line_numbers, 283 | ); 284 | 285 | // Render Hex 286 | frame.render_widget( 287 | Paragraph::new(hex_text).block( 288 | Block::default().borders(Borders::ALL).title("Hex").style( 289 | if window.is_focusing(Window::Hex) { 290 | Style::default().fg(Color::Yellow) 291 | } else { 292 | Style::default() 293 | }, 294 | ), 295 | ), 296 | comp_layouts.hex, 297 | ); 298 | 299 | // Render ASCII 300 | frame.render_widget( 301 | Paragraph::new(ascii_text).block( 302 | Block::default().borders(Borders::ALL).title("ASCII").style( 303 | if window.is_focusing(Window::Ascii) { 304 | Style::default().fg(Color::Yellow) 305 | } else { 306 | Style::default() 307 | }, 308 | ), 309 | ), 310 | comp_layouts.ascii, 311 | ); 312 | 313 | // Render Info 314 | for (i, label) in comp_layouts.labels.iter().enumerate() { 315 | frame.render_widget( 316 | Paragraph::new(labels[LABEL_TITLES[i]].clone()) 317 | .block(Block::default().borders(Borders::ALL).title(LABEL_TITLES[i])), 318 | *label, 319 | ); 320 | } 321 | 322 | // Render Popup 323 | if !window.is_focusing(Window::Hex) && !window.is_focusing(Window::Ascii) { 324 | frame.render_widget(Clear, comp_layouts.popup); 325 | frame.render_widget(window.widget(), comp_layouts.popup); 326 | } 327 | } 328 | } 329 | 330 | /// Display hex bytes with correct highlighting and colors by chunking the bytes into rows and 331 | /// formatting them into hex. 332 | /// 333 | /// NOTE: In UTF-8, a character takes up to 4 bytes and thus the encoding can break at the ends of a 334 | /// chunk. Increasing the chunk size by 3 bytes at both ends before decoding and cropping them of 335 | /// afterwards solves the issue for the visible parts. 336 | fn generate_hex(app_info: &Data, bytes_per_line: usize, lines_per_screen: usize) -> Vec> { 337 | let initial_offset = app_info.start_address.min(3); 338 | OverlappingChunks::new( 339 | &app_info.contents[(app_info.start_address - initial_offset)..], 340 | bytes_per_line, 341 | 6, 342 | ) 343 | .take(lines_per_screen) 344 | .enumerate() 345 | .map(|(row, chunk)| { 346 | let spans = chunk 347 | .iter() 348 | .zip(ByteAlignedDecoder::new(chunk, app_info.encoding)) 349 | .skip(initial_offset) 350 | .take(bytes_per_line) 351 | .enumerate() 352 | .flat_map(|(col, (&byte, character))| { 353 | // We don't want an extra space at the end of each row. 354 | if col < bytes_per_line - 1 { 355 | format!("{byte:02X?} ") 356 | } else { 357 | format!("{byte:02X?}") 358 | } 359 | .chars() 360 | .enumerate() 361 | .map(|(nibble_pos, c)| { 362 | let byte_pos = app_info.start_address + (row * bytes_per_line) + col; 363 | let mut span = 364 | Span::styled(c.to_string(), Style::default().fg(*character.color())); 365 | let is_cursor = byte_pos == app_info.offset 366 | && ((nibble_pos == 0 && app_info.nibble == Nibble::Beginning) 367 | || (nibble_pos == 1 && app_info.nibble == Nibble::End)); 368 | 369 | // Determine if the specified nibble (or space) should have a 370 | // lighter foreground because it is in the user's dragged range. 371 | // The logic is more complicated for hex because users can select 372 | // a single nibble from a byte. 373 | let mut in_drag = false; 374 | if let Some(drag) = app_info.last_drag { 375 | let drag_nibble = app_info.drag_nibble.unwrap_or(Nibble::End); 376 | if !(drag == app_info.offset && app_info.nibble == drag_nibble) { 377 | let mut start = drag; 378 | let mut end = app_info.offset; 379 | let mut start_nibble = drag_nibble; 380 | let mut end_nibble = app_info.nibble; 381 | 382 | if app_info.offset < drag { 383 | start = app_info.offset; 384 | end = drag; 385 | start_nibble = app_info.nibble; 386 | end_nibble = drag_nibble; 387 | } 388 | 389 | // The only time the starting byte would not entirely be in 390 | // drag range is when the first nibble is not highlighted. 391 | // Similarly, the last nibble is only partially highlighted 392 | // when the second (and last) nibble is not selected. 393 | if byte_pos == start { 394 | in_drag = !(nibble_pos == 0 && start_nibble == Nibble::End); 395 | } 396 | if byte_pos == end { 397 | in_drag |= !(nibble_pos == 1 && end_nibble == Nibble::Beginning) 398 | && nibble_pos != 2; 399 | } 400 | if start == end && nibble_pos == 2 { 401 | in_drag = false; 402 | } else if end - start > 1 { 403 | in_drag |= (start + 1..end).contains(&byte_pos); 404 | } 405 | } 406 | } 407 | if is_cursor || in_drag { 408 | span.style = span.style.bg(COLOR_NULL); 409 | } 410 | span 411 | }) 412 | .collect::>() 413 | }) 414 | .collect::>(); 415 | Line::from(spans) 416 | }) 417 | .collect::>() 418 | } 419 | 420 | /// Display decoded bytes with correct highlighting and colors. 421 | /// 422 | /// NOTE: In UTF-8, a character takes up to 4 bytes and thus the encoding can break at the ends of a 423 | /// chunk. Increasing the chunk size by 3 bytes at both ends before decoding and cropping them of 424 | /// afterwards solves the issue for the visible parts. 425 | fn generate_decoded( 426 | app_info: &Data, 427 | bytes_per_line: usize, 428 | lines_per_screen: usize, 429 | ) -> Vec> { 430 | let initial_offset = app_info.start_address.min(3); 431 | OverlappingChunks::new( 432 | &app_info.contents[(app_info.start_address - initial_offset)..], 433 | bytes_per_line, 434 | 6, 435 | ) 436 | .take(lines_per_screen) 437 | .enumerate() 438 | .map(|(row, chunk)| { 439 | Line::from( 440 | ByteAlignedDecoder::new(chunk, app_info.encoding) 441 | .skip(initial_offset) 442 | .take(bytes_per_line) 443 | .enumerate() 444 | .map(|(col, character)| { 445 | let byte_pos = app_info.start_address + (row * bytes_per_line) + col; 446 | let mut span = Span::styled( 447 | character.escape().to_string(), 448 | Style::default().fg(*character.color()), 449 | ); 450 | // Highlight the selected byte in the ASCII table 451 | let last_drag = app_info.last_drag.unwrap_or(app_info.offset); 452 | if byte_pos == app_info.offset 453 | || (app_info.offset..=last_drag).contains(&byte_pos) 454 | || (last_drag..=app_info.offset).contains(&byte_pos) 455 | { 456 | span.style = span.style.bg(COLOR_NULL); 457 | } 458 | span 459 | }) 460 | .collect::>(), 461 | ) 462 | }) 463 | .collect::>() 464 | } 465 | 466 | /// Generates the dimensions of an x by y popup that is centered in Rect r. 467 | fn popup_rect((x, y): (u16, u16), r: Rect) -> Rect { 468 | let popup_layout = Layout::default() 469 | .direction(Direction::Vertical) 470 | .constraints( 471 | [ 472 | Constraint::Length(r.height.saturating_sub(y) / 2), 473 | Constraint::Length(y), 474 | Constraint::Min(r.height.saturating_sub(y) / 2), 475 | ] 476 | .as_ref(), 477 | ) 478 | .split(r); 479 | 480 | Layout::default() 481 | .direction(Direction::Horizontal) 482 | .constraints( 483 | [ 484 | Constraint::Min(r.width.saturating_sub(x) / 2), 485 | Constraint::Length(x), 486 | Constraint::Min(r.width.saturating_sub(x) / 2), 487 | ] 488 | .as_ref(), 489 | ) 490 | .split(popup_layout[1])[1] 491 | } 492 | 493 | #[cfg(test)] 494 | mod tests { 495 | use super::*; 496 | 497 | #[test] 498 | fn test_calculate_dimensions_no_popup() { 499 | let width = 100; 500 | let height = 100; 501 | 502 | // Given a terminal size of 100 x 100, when dimensions are calculated 503 | let key_handler: Box = Box::from(Editor::Ascii); 504 | let layout = Handler::calculate_dimensions(Rect::new(0, 0, width, height), &*key_handler); 505 | 506 | // The "editors" section, which consists of the line number column, Hex input box, and 507 | // ASCII input box should have a size of height - 12 (there are 4 labels per column and 508 | // each label takes 3 lines; each takes the vertical space alongside these components). 509 | assert_eq!(layout.line_numbers.height, height - 12); 510 | assert_eq!(layout.hex.height, height - 12); 511 | assert_eq!(layout.ascii.height, height - 12); 512 | 513 | // The width of the line numbers column is hard coded to 10, 514 | assert_eq!(layout.line_numbers.width, 10); 515 | // The Hex editor takes up 3/4ths of the remaining horizontal space (rounded down as to not 516 | // overflow)... 517 | assert_eq!(layout.hex.width, (width - 10) * 3 / 4); 518 | // And the ASCII editor takes up the remaining 1/4th. In some instances, the dimensions 519 | // are larger than the layout, so instead of asserting (width - 10) / 4 we assert the 520 | // remaining space. 521 | assert_eq!(layout.ascii.width, width - (10 + ((width - 10) * 3 / 4))); 522 | 523 | // The remaining space should consist of the labels in a 4 by 4 grid. Since the height 524 | // of each label column is hard set to 12, 4 labels in a column should have a width of 3. 525 | for label in &*layout.labels { 526 | assert_eq!(label.width, width / 4); 527 | assert_eq!(label.height, 3); 528 | } 529 | } 530 | 531 | // TODO: Create a test for asserting the dimension of each popup 532 | } 533 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "allocator-api2" 19 | version = "0.2.16" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 22 | 23 | [[package]] 24 | name = "anstream" 25 | version = "0.6.13" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 28 | dependencies = [ 29 | "anstyle", 30 | "anstyle-parse", 31 | "anstyle-query", 32 | "anstyle-wincon", 33 | "colorchoice", 34 | "utf8parse", 35 | ] 36 | 37 | [[package]] 38 | name = "anstyle" 39 | version = "1.0.8" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 42 | 43 | [[package]] 44 | name = "anstyle-parse" 45 | version = "0.2.3" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 48 | dependencies = [ 49 | "utf8parse", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-query" 54 | version = "1.0.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 57 | dependencies = [ 58 | "windows-sys", 59 | ] 60 | 61 | [[package]] 62 | name = "anstyle-wincon" 63 | version = "3.0.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 66 | dependencies = [ 67 | "anstyle", 68 | "windows-sys", 69 | ] 70 | 71 | [[package]] 72 | name = "arboard" 73 | version = "3.6.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" 76 | dependencies = [ 77 | "clipboard-win", 78 | "log", 79 | "objc2", 80 | "objc2-app-kit", 81 | "objc2-foundation", 82 | "parking_lot", 83 | "percent-encoding", 84 | "x11rb", 85 | ] 86 | 87 | [[package]] 88 | name = "autocfg" 89 | version = "1.2.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 92 | 93 | [[package]] 94 | name = "bitflags" 95 | version = "1.3.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 98 | 99 | [[package]] 100 | name = "bitflags" 101 | version = "2.5.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 104 | 105 | [[package]] 106 | name = "cassowary" 107 | version = "0.3.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 110 | 111 | [[package]] 112 | name = "castaway" 113 | version = "0.2.3" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 116 | dependencies = [ 117 | "rustversion", 118 | ] 119 | 120 | [[package]] 121 | name = "cfg-if" 122 | version = "1.0.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 125 | 126 | [[package]] 127 | name = "clap" 128 | version = "4.5.40" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" 131 | dependencies = [ 132 | "clap_builder", 133 | "clap_derive", 134 | ] 135 | 136 | [[package]] 137 | name = "clap_builder" 138 | version = "4.5.40" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" 141 | dependencies = [ 142 | "anstream", 143 | "anstyle", 144 | "clap_lex", 145 | "strsim", 146 | ] 147 | 148 | [[package]] 149 | name = "clap_derive" 150 | version = "4.5.40" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" 153 | dependencies = [ 154 | "heck", 155 | "proc-macro2", 156 | "quote", 157 | "syn", 158 | ] 159 | 160 | [[package]] 161 | name = "clap_lex" 162 | version = "0.7.4" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 165 | 166 | [[package]] 167 | name = "clipboard-win" 168 | version = "5.3.1" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" 171 | dependencies = [ 172 | "error-code", 173 | ] 174 | 175 | [[package]] 176 | name = "colorchoice" 177 | version = "1.0.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 180 | 181 | [[package]] 182 | name = "compact_str" 183 | version = "0.8.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" 186 | dependencies = [ 187 | "castaway", 188 | "cfg-if", 189 | "itoa", 190 | "rustversion", 191 | "ryu", 192 | "static_assertions", 193 | ] 194 | 195 | [[package]] 196 | name = "crossbeam" 197 | version = "0.8.4" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" 200 | dependencies = [ 201 | "crossbeam-channel", 202 | "crossbeam-deque", 203 | "crossbeam-epoch", 204 | "crossbeam-queue", 205 | "crossbeam-utils", 206 | ] 207 | 208 | [[package]] 209 | name = "crossbeam-channel" 210 | version = "0.5.12" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" 213 | dependencies = [ 214 | "crossbeam-utils", 215 | ] 216 | 217 | [[package]] 218 | name = "crossbeam-deque" 219 | version = "0.8.5" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 222 | dependencies = [ 223 | "crossbeam-epoch", 224 | "crossbeam-utils", 225 | ] 226 | 227 | [[package]] 228 | name = "crossbeam-epoch" 229 | version = "0.9.18" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 232 | dependencies = [ 233 | "crossbeam-utils", 234 | ] 235 | 236 | [[package]] 237 | name = "crossbeam-queue" 238 | version = "0.3.11" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" 241 | dependencies = [ 242 | "crossbeam-utils", 243 | ] 244 | 245 | [[package]] 246 | name = "crossbeam-utils" 247 | version = "0.8.19" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 250 | 251 | [[package]] 252 | name = "crossterm" 253 | version = "0.28.1" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 256 | dependencies = [ 257 | "bitflags 2.5.0", 258 | "crossterm_winapi", 259 | "mio", 260 | "parking_lot", 261 | "rustix", 262 | "signal-hook", 263 | "signal-hook-mio", 264 | "winapi", 265 | ] 266 | 267 | [[package]] 268 | name = "crossterm_winapi" 269 | version = "0.9.1" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 272 | dependencies = [ 273 | "winapi", 274 | ] 275 | 276 | [[package]] 277 | name = "dispatch2" 278 | version = "0.3.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" 281 | dependencies = [ 282 | "bitflags 2.5.0", 283 | "objc2", 284 | ] 285 | 286 | [[package]] 287 | name = "either" 288 | version = "1.10.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 291 | 292 | [[package]] 293 | name = "errno" 294 | version = "0.3.8" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 297 | dependencies = [ 298 | "libc", 299 | "windows-sys", 300 | ] 301 | 302 | [[package]] 303 | name = "error-code" 304 | version = "3.2.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" 307 | 308 | [[package]] 309 | name = "gethostname" 310 | version = "0.4.3" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" 313 | dependencies = [ 314 | "libc", 315 | "windows-targets 0.48.5", 316 | ] 317 | 318 | [[package]] 319 | name = "hashbrown" 320 | version = "0.14.3" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 323 | dependencies = [ 324 | "ahash", 325 | "allocator-api2", 326 | ] 327 | 328 | [[package]] 329 | name = "heck" 330 | version = "0.5.0" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 333 | 334 | [[package]] 335 | name = "heh" 336 | version = "0.6.2" 337 | dependencies = [ 338 | "arboard", 339 | "clap", 340 | "crossbeam", 341 | "hex", 342 | "memmap2", 343 | "ratatui", 344 | ] 345 | 346 | [[package]] 347 | name = "hermit-abi" 348 | version = "0.3.9" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 351 | 352 | [[package]] 353 | name = "hex" 354 | version = "0.4.3" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 357 | 358 | [[package]] 359 | name = "indoc" 360 | version = "2.0.5" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 363 | 364 | [[package]] 365 | name = "instability" 366 | version = "0.3.2" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" 369 | dependencies = [ 370 | "quote", 371 | "syn", 372 | ] 373 | 374 | [[package]] 375 | name = "itertools" 376 | version = "0.12.1" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 379 | dependencies = [ 380 | "either", 381 | ] 382 | 383 | [[package]] 384 | name = "itertools" 385 | version = "0.13.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 388 | dependencies = [ 389 | "either", 390 | ] 391 | 392 | [[package]] 393 | name = "itoa" 394 | version = "1.0.11" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 397 | 398 | [[package]] 399 | name = "libc" 400 | version = "0.2.153" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 403 | 404 | [[package]] 405 | name = "linux-raw-sys" 406 | version = "0.4.13" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 409 | 410 | [[package]] 411 | name = "lock_api" 412 | version = "0.4.11" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 415 | dependencies = [ 416 | "autocfg", 417 | "scopeguard", 418 | ] 419 | 420 | [[package]] 421 | name = "log" 422 | version = "0.4.21" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 425 | 426 | [[package]] 427 | name = "lru" 428 | version = "0.12.3" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 431 | dependencies = [ 432 | "hashbrown", 433 | ] 434 | 435 | [[package]] 436 | name = "memmap2" 437 | version = "0.9.7" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" 440 | dependencies = [ 441 | "libc", 442 | ] 443 | 444 | [[package]] 445 | name = "mio" 446 | version = "1.0.1" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" 449 | dependencies = [ 450 | "hermit-abi", 451 | "libc", 452 | "log", 453 | "wasi", 454 | "windows-sys", 455 | ] 456 | 457 | [[package]] 458 | name = "objc2" 459 | version = "0.6.1" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" 462 | dependencies = [ 463 | "objc2-encode", 464 | ] 465 | 466 | [[package]] 467 | name = "objc2-app-kit" 468 | version = "0.3.1" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" 471 | dependencies = [ 472 | "bitflags 2.5.0", 473 | "objc2", 474 | "objc2-core-graphics", 475 | "objc2-foundation", 476 | ] 477 | 478 | [[package]] 479 | name = "objc2-core-foundation" 480 | version = "0.3.1" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" 483 | dependencies = [ 484 | "bitflags 2.5.0", 485 | "dispatch2", 486 | "objc2", 487 | ] 488 | 489 | [[package]] 490 | name = "objc2-core-graphics" 491 | version = "0.3.1" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" 494 | dependencies = [ 495 | "bitflags 2.5.0", 496 | "dispatch2", 497 | "objc2", 498 | "objc2-core-foundation", 499 | "objc2-io-surface", 500 | ] 501 | 502 | [[package]] 503 | name = "objc2-encode" 504 | version = "4.1.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" 507 | 508 | [[package]] 509 | name = "objc2-foundation" 510 | version = "0.3.1" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" 513 | dependencies = [ 514 | "bitflags 2.5.0", 515 | "objc2", 516 | "objc2-core-foundation", 517 | ] 518 | 519 | [[package]] 520 | name = "objc2-io-surface" 521 | version = "0.3.1" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" 524 | dependencies = [ 525 | "bitflags 2.5.0", 526 | "objc2", 527 | "objc2-core-foundation", 528 | ] 529 | 530 | [[package]] 531 | name = "once_cell" 532 | version = "1.19.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 535 | 536 | [[package]] 537 | name = "parking_lot" 538 | version = "0.12.1" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 541 | dependencies = [ 542 | "lock_api", 543 | "parking_lot_core", 544 | ] 545 | 546 | [[package]] 547 | name = "parking_lot_core" 548 | version = "0.9.9" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 551 | dependencies = [ 552 | "cfg-if", 553 | "libc", 554 | "redox_syscall", 555 | "smallvec", 556 | "windows-targets 0.48.5", 557 | ] 558 | 559 | [[package]] 560 | name = "paste" 561 | version = "1.0.14" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 564 | 565 | [[package]] 566 | name = "percent-encoding" 567 | version = "2.3.1" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 570 | 571 | [[package]] 572 | name = "proc-macro2" 573 | version = "1.0.79" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 576 | dependencies = [ 577 | "unicode-ident", 578 | ] 579 | 580 | [[package]] 581 | name = "quote" 582 | version = "1.0.35" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 585 | dependencies = [ 586 | "proc-macro2", 587 | ] 588 | 589 | [[package]] 590 | name = "ratatui" 591 | version = "0.29.0" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 594 | dependencies = [ 595 | "bitflags 2.5.0", 596 | "cassowary", 597 | "compact_str", 598 | "crossterm", 599 | "indoc", 600 | "instability", 601 | "itertools 0.13.0", 602 | "lru", 603 | "paste", 604 | "strum", 605 | "unicode-segmentation", 606 | "unicode-truncate", 607 | "unicode-width 0.2.0", 608 | ] 609 | 610 | [[package]] 611 | name = "redox_syscall" 612 | version = "0.4.1" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 615 | dependencies = [ 616 | "bitflags 1.3.2", 617 | ] 618 | 619 | [[package]] 620 | name = "rustix" 621 | version = "0.38.34" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 624 | dependencies = [ 625 | "bitflags 2.5.0", 626 | "errno", 627 | "libc", 628 | "linux-raw-sys", 629 | "windows-sys", 630 | ] 631 | 632 | [[package]] 633 | name = "rustversion" 634 | version = "1.0.14" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 637 | 638 | [[package]] 639 | name = "ryu" 640 | version = "1.0.17" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 643 | 644 | [[package]] 645 | name = "scopeguard" 646 | version = "1.2.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 649 | 650 | [[package]] 651 | name = "signal-hook" 652 | version = "0.3.17" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 655 | dependencies = [ 656 | "libc", 657 | "signal-hook-registry", 658 | ] 659 | 660 | [[package]] 661 | name = "signal-hook-mio" 662 | version = "0.2.4" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 665 | dependencies = [ 666 | "libc", 667 | "mio", 668 | "signal-hook", 669 | ] 670 | 671 | [[package]] 672 | name = "signal-hook-registry" 673 | version = "1.4.1" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 676 | dependencies = [ 677 | "libc", 678 | ] 679 | 680 | [[package]] 681 | name = "smallvec" 682 | version = "1.13.2" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 685 | 686 | [[package]] 687 | name = "static_assertions" 688 | version = "1.1.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 691 | 692 | [[package]] 693 | name = "strsim" 694 | version = "0.11.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 697 | 698 | [[package]] 699 | name = "strum" 700 | version = "0.26.3" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 703 | dependencies = [ 704 | "strum_macros", 705 | ] 706 | 707 | [[package]] 708 | name = "strum_macros" 709 | version = "0.26.4" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 712 | dependencies = [ 713 | "heck", 714 | "proc-macro2", 715 | "quote", 716 | "rustversion", 717 | "syn", 718 | ] 719 | 720 | [[package]] 721 | name = "syn" 722 | version = "2.0.57" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" 725 | dependencies = [ 726 | "proc-macro2", 727 | "quote", 728 | "unicode-ident", 729 | ] 730 | 731 | [[package]] 732 | name = "unicode-ident" 733 | version = "1.0.12" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 736 | 737 | [[package]] 738 | name = "unicode-segmentation" 739 | version = "1.11.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 742 | 743 | [[package]] 744 | name = "unicode-truncate" 745 | version = "1.0.0" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" 748 | dependencies = [ 749 | "itertools 0.12.1", 750 | "unicode-width 0.1.13", 751 | ] 752 | 753 | [[package]] 754 | name = "unicode-width" 755 | version = "0.1.13" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 758 | 759 | [[package]] 760 | name = "unicode-width" 761 | version = "0.2.0" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 764 | 765 | [[package]] 766 | name = "utf8parse" 767 | version = "0.2.1" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 770 | 771 | [[package]] 772 | name = "version_check" 773 | version = "0.9.4" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 776 | 777 | [[package]] 778 | name = "wasi" 779 | version = "0.11.0+wasi-snapshot-preview1" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 782 | 783 | [[package]] 784 | name = "winapi" 785 | version = "0.3.9" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 788 | dependencies = [ 789 | "winapi-i686-pc-windows-gnu", 790 | "winapi-x86_64-pc-windows-gnu", 791 | ] 792 | 793 | [[package]] 794 | name = "winapi-i686-pc-windows-gnu" 795 | version = "0.4.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 798 | 799 | [[package]] 800 | name = "winapi-x86_64-pc-windows-gnu" 801 | version = "0.4.0" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 804 | 805 | [[package]] 806 | name = "windows-sys" 807 | version = "0.52.0" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 810 | dependencies = [ 811 | "windows-targets 0.52.4", 812 | ] 813 | 814 | [[package]] 815 | name = "windows-targets" 816 | version = "0.48.5" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 819 | dependencies = [ 820 | "windows_aarch64_gnullvm 0.48.5", 821 | "windows_aarch64_msvc 0.48.5", 822 | "windows_i686_gnu 0.48.5", 823 | "windows_i686_msvc 0.48.5", 824 | "windows_x86_64_gnu 0.48.5", 825 | "windows_x86_64_gnullvm 0.48.5", 826 | "windows_x86_64_msvc 0.48.5", 827 | ] 828 | 829 | [[package]] 830 | name = "windows-targets" 831 | version = "0.52.4" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 834 | dependencies = [ 835 | "windows_aarch64_gnullvm 0.52.4", 836 | "windows_aarch64_msvc 0.52.4", 837 | "windows_i686_gnu 0.52.4", 838 | "windows_i686_msvc 0.52.4", 839 | "windows_x86_64_gnu 0.52.4", 840 | "windows_x86_64_gnullvm 0.52.4", 841 | "windows_x86_64_msvc 0.52.4", 842 | ] 843 | 844 | [[package]] 845 | name = "windows_aarch64_gnullvm" 846 | version = "0.48.5" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 849 | 850 | [[package]] 851 | name = "windows_aarch64_gnullvm" 852 | version = "0.52.4" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 855 | 856 | [[package]] 857 | name = "windows_aarch64_msvc" 858 | version = "0.48.5" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 861 | 862 | [[package]] 863 | name = "windows_aarch64_msvc" 864 | version = "0.52.4" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 867 | 868 | [[package]] 869 | name = "windows_i686_gnu" 870 | version = "0.48.5" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 873 | 874 | [[package]] 875 | name = "windows_i686_gnu" 876 | version = "0.52.4" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 879 | 880 | [[package]] 881 | name = "windows_i686_msvc" 882 | version = "0.48.5" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 885 | 886 | [[package]] 887 | name = "windows_i686_msvc" 888 | version = "0.52.4" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 891 | 892 | [[package]] 893 | name = "windows_x86_64_gnu" 894 | version = "0.48.5" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 897 | 898 | [[package]] 899 | name = "windows_x86_64_gnu" 900 | version = "0.52.4" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 903 | 904 | [[package]] 905 | name = "windows_x86_64_gnullvm" 906 | version = "0.48.5" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 909 | 910 | [[package]] 911 | name = "windows_x86_64_gnullvm" 912 | version = "0.52.4" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 915 | 916 | [[package]] 917 | name = "windows_x86_64_msvc" 918 | version = "0.48.5" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 921 | 922 | [[package]] 923 | name = "windows_x86_64_msvc" 924 | version = "0.52.4" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 927 | 928 | [[package]] 929 | name = "x11rb" 930 | version = "0.13.0" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" 933 | dependencies = [ 934 | "gethostname", 935 | "rustix", 936 | "x11rb-protocol", 937 | ] 938 | 939 | [[package]] 940 | name = "x11rb-protocol" 941 | version = "0.13.0" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" 944 | 945 | [[package]] 946 | name = "zerocopy" 947 | version = "0.7.32" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 950 | dependencies = [ 951 | "zerocopy-derive", 952 | ] 953 | 954 | [[package]] 955 | name = "zerocopy-derive" 956 | version = "0.7.32" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 959 | dependencies = [ 960 | "proc-macro2", 961 | "quote", 962 | "syn", 963 | ] 964 | --------------------------------------------------------------------------------