├── .gitignore ├── hexhog.gif ├── src ├── app │ ├── mod.rs │ ├── change.rs │ ├── state.rs │ ├── utils.rs │ ├── events.rs │ └── render.rs ├── main.rs ├── byte.rs └── config.rs ├── Cargo.toml ├── .github ├── dependabot.yml └── workflows │ ├── main.yml │ └── ci.yml ├── LICENSE ├── CHANGELOG.md ├── README.md ├── hexhog.tape └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /6502.bin 3 | -------------------------------------------------------------------------------- /hexhog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DVDTSB/hexhog/HEAD/hexhog.gif -------------------------------------------------------------------------------- /src/app/mod.rs: -------------------------------------------------------------------------------- 1 | mod change; 2 | mod events; 3 | mod render; 4 | mod state; 5 | mod utils; 6 | pub use state::{App, Args}; 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hexhog" 3 | version = "0.1.3" 4 | description = "hex viewer/editor" 5 | authors = ["dvdtsb"] 6 | license = "MIT" 7 | edition = "2024" 8 | readme-file = "README.md" 9 | repository = "https://github.com/DVDTSB/hexhog" 10 | 11 | [dependencies] 12 | crossterm = "0.28.1" 13 | ratatui = "0.29.0" 14 | color-eyre = "0.6.3" 15 | clap = { version = "4.5.47", features = ["derive"] } 16 | dirs = "6.0.0" 17 | toml = "0.9.7" 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for Cargo 9 | - package-ecosystem: "cargo" 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "weekly" 13 | # Maintain dependencies for GitHub Actions 14 | - package-ecosystem: github-actions 15 | directory: "/" 16 | schedule: 17 | interval: weekly 18 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod byte; 3 | mod config; 4 | 5 | use app::{App, Args}; 6 | use clap::Parser; 7 | use color_eyre::Result; 8 | use config::Config; 9 | 10 | fn main() -> Result<()> { 11 | color_eyre::install()?; 12 | let args = Args::parse(); 13 | 14 | let config_file_path = dirs::config_dir() 15 | .unwrap() 16 | .join("hexhog") 17 | .join("config.toml"); 18 | 19 | let config = Config::read_config(config_file_path.to_str().unwrap()).unwrap_or_else(|e| { 20 | eprintln!("Error reading config: {e}"); 21 | eprintln!("Using default config"); 22 | Config::default() 23 | }); 24 | 25 | let app = App::new(args, config)?; 26 | let terminal = ratatui::init(); 27 | let result = app.run(terminal); 28 | ratatui::restore(); 29 | result 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) dvdtsb <2025> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/byte.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::Style; 2 | 3 | use crate::config::Config; 4 | 5 | #[derive(Clone, Copy, PartialEq, Eq)] 6 | pub struct Byte(u8); 7 | 8 | pub enum ByteType { 9 | Null, 10 | AsciiPrintable, 11 | AsciiWhitespace, 12 | AsciiOther, 13 | NonAscii, 14 | } 15 | 16 | impl Byte { 17 | pub fn new(value: u8) -> Self { 18 | Byte(value) 19 | } 20 | 21 | pub fn value(&self) -> u8 { 22 | self.0 23 | } 24 | 25 | pub fn get_bytetype(self) -> ByteType { 26 | match self.0 { 27 | 0 => ByteType::Null, 28 | c if c.is_ascii_graphic() => ByteType::AsciiPrintable, 29 | c if c.is_ascii_whitespace() => ByteType::AsciiWhitespace, 30 | c if c.is_ascii() => ByteType::AsciiOther, 31 | _ => ByteType::NonAscii, 32 | } 33 | } 34 | 35 | pub fn get_hex(self) -> String { 36 | format!("{:02X}", self.0) 37 | } 38 | 39 | pub fn get_char(self, config: &Config) -> char { 40 | config.charset.get_char(&self) 41 | } 42 | 43 | pub fn get_style(&self, config: &Config) -> Style { 44 | config.colorscheme.get_style(self) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/change.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum Change { 5 | Edit(usize, Vec, Vec), 6 | Insert(usize, Vec), 7 | Delete(usize, Vec), 8 | } 9 | 10 | impl App { 11 | pub fn do_change(&mut self, change: Change) { 12 | self.changes.push(change.clone()); 13 | match change { 14 | Change::Edit(idx, _old, new) => self.replace_data(idx, new), 15 | Change::Insert(idx, new) => self.insert_data(idx, new), 16 | Change::Delete(idx, old) => self.delete_data(idx, old.len()), 17 | } 18 | } 19 | 20 | pub fn undo_change(&mut self, change: Change) { 21 | self.made_changes.push(change.clone()); 22 | match change { 23 | Change::Edit(idx, old, _new) => self.replace_data(idx, old), 24 | Change::Insert(idx, new) => self.delete_data(idx, new.len()), 25 | Change::Delete(idx, old) => self.insert_data(idx, old), 26 | } 27 | } 28 | 29 | pub fn undo(&mut self) { 30 | if let Some(change) = self.changes.pop() { 31 | self.undo_change(change); 32 | } 33 | } 34 | 35 | pub fn redo(&mut self) { 36 | if let Some(change) = self.made_changes.pop() { 37 | self.do_change(change); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | 10 | # Release unpublished packages. 11 | release-plz-release: 12 | name: Release-plz release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | steps: 17 | - &checkout 18 | name: Checkout repository 19 | uses: actions/checkout@v5 20 | with: 21 | fetch-depth: 0 22 | persist-credentials: false 23 | - &install-rust 24 | name: Install Rust toolchain 25 | uses: dtolnay/rust-toolchain@stable 26 | - name: Run release-plz 27 | uses: release-plz/action@v0.5 28 | with: 29 | command: release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 33 | 34 | # Create a PR with the new versions and changelog, preparing the next release. 35 | release-plz-pr: 36 | name: Release-plz PR 37 | runs-on: ubuntu-latest 38 | permissions: 39 | contents: write 40 | pull-requests: write 41 | concurrency: 42 | group: release-plz-${{ github.ref }} 43 | cancel-in-progress: false 44 | steps: 45 | - *checkout 46 | - *install-rust 47 | - name: Run release-plz 48 | uses: release-plz/action@v0.5 49 | with: 50 | command: release-pr 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 54 | -------------------------------------------------------------------------------- /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 adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.3](https://github.com/DVDTSB/hexhog/compare/v0.1.2...v0.1.3) - 2025-11-06 11 | 12 | ### Fixed 13 | 14 | - fix fmt 15 | - fix fmt 16 | - fix fmt! 17 | - fix fmt 18 | - fix unexistent file thing 19 | - fix clippy 20 | - fixed workflow 21 | - fixed gif 22 | - fix clippy 23 | 24 | ### Other 25 | 26 | - gif 27 | - organize render 28 | - remove state from status 29 | - organize stuff, fix backspace 30 | - add temp gif 31 | - stuff lol 32 | - colors! 33 | - update help 34 | - rework changes (should probably rename to actions), add copy pasting 35 | - exit selection 36 | - selection ish 37 | - selection start, make stuff that should be usize usize 38 | - merge insert and edit modes. also fix insert offset thing 39 | - add delete, fix newline thing 40 | - added pageup/pagedown key support for page-wise navigation 41 | - release v0.1.2 42 | - added writing to back of file and refactored a bit of drawing logic 43 | - meow 44 | - add insert mode 45 | - release v0.1.1 46 | - finished config 47 | - change options to results 48 | - structure+basic config 49 | - Add GitHub Actions workflow for releases 50 | - meow 51 | - add gif 52 | - basic functionality 53 | - added editing, saving, undo and help 54 | - meow 55 | - init 56 | - init 57 | 58 | ## [0.1.2](https://github.com/DVDTSB/hexhog/compare/v0.1.1...v0.1.2) - 2025-10-09 59 | 60 | ### Fixed 61 | 62 | - fix unexistent file thing 63 | - fix clippy 64 | 65 | ### Other 66 | 67 | - added writing to back of file and refactored a bit of drawing logic 68 | - meow 69 | - add insert mode 70 | 71 | ## [0.1.1](https://github.com/DVDTSB/hexhog/compare/v0.1.0...v0.1.1) - 2025-09-27 72 | 73 | ### Other 74 | 75 | - change options to results 76 | - structure+basic config 77 | - Add GitHub Actions workflow for releases 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hexhog 2 | 3 | A configurable hex viewer/editor 4 | 5 | ![hexhog lol](hexhog.gif) 6 | 7 | To run `hexhog`, use the following command: 8 | ``` 9 | hexhog 10 | ``` 11 | 12 | ## Installation 13 | If you have cargo installed, you can run the following command: 14 | ``` 15 | cargo install hexhog 16 | ``` 17 | 18 | It is also available on AUR and Homebrew, thanks to [@dhopcs](https://github.com/dhopcs) and [@chenrui333](https://github.com/chenrui333). 19 | ``` 20 | yay -S hexhog 21 | ``` 22 | 23 | ``` 24 | brew install hexhog 25 | ``` 26 | 27 | I hope to make this tool available on other package managers soon. 28 | 29 | ## Features 30 | For now, `hexhog` allows for basic hex editing features for files, such as editing/deleting/inserting bytes, as well as selecting and copy/pasting bytes. I'm look forward to adding other features, including (but not only): 31 | - moving the selection 32 | - find/replace 33 | - bookmarks 34 | - better navigation 35 | - CP437 36 | - other coloring options 37 | 38 | While I do love (and use) modal editors, `hexhog` does not attempt to be one. I am trying to make it as intuitive as possible :) 39 | 40 | ## Configuration 41 | 42 | You can find the configuration file in the following locations: 43 | - Linux: `/home/user/.config/hexhog/config.toml` 44 | - Windows: `C:\Users\user\AppData\Roaming\hexhog\config.toml` 45 | - MacOS: `/Users/user/Library/Application Support/hexhog/config.toml` 46 | 47 | An example configuration file: 48 | ```toml 49 | [theme] 50 | null = "dark_gray" 51 | ascii_printable = "blue" 52 | ascii_whitespace = [67, 205, 128] # rgb 53 | ascii_other = 162 # ansi 54 | non_ascii = "red" 55 | accent = "blue" 56 | primary = "green" 57 | background = "black" 58 | border = "cyan" 59 | 60 | [charset] 61 | null = "." 62 | ascii_whitespace = "·" 63 | ascii_other = "°" 64 | non_ascii = "×" 65 | ``` 66 | 67 | ## Feedback 68 | 69 | Feedback on `hexhog` is highly appreciated. Thanks! :D 70 | 71 | ## License 72 | 73 | Copyright © dvdtsb 2025 74 | 75 | This project uses the MIT license ([LICENSE] or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)). 76 | 77 | [LICENSE]: ./LICENSE 78 | -------------------------------------------------------------------------------- /src/app/state.rs: -------------------------------------------------------------------------------- 1 | use super::change::Change; 2 | use crate::config::Config; 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use ratatui::DefaultTerminal; 6 | use std::{fs::File, io::Read, path::Path}; 7 | 8 | #[derive(Parser, Debug)] 9 | #[command(version, about)] 10 | pub struct Args { 11 | pub file: String, 12 | } 13 | 14 | #[derive(Debug, PartialEq, Eq)] 15 | pub enum AppState { 16 | Move, 17 | Edit, 18 | Help, 19 | } 20 | 21 | pub struct App { 22 | pub config: Config, 23 | pub file_name: String, 24 | pub data: Vec, 25 | pub starting_line: usize, 26 | pub cursor_x: usize, 27 | pub cursor_y: usize, 28 | pub frame_height: usize, 29 | pub running: bool, 30 | pub state: AppState, 31 | pub buffer: [char; 2], 32 | pub changes: Vec, 33 | pub made_changes: Vec, 34 | pub is_inserting: bool, 35 | pub is_selecting: bool, 36 | pub selection_start: usize, 37 | pub clipboard: Vec, 38 | } 39 | 40 | impl App { 41 | pub fn new(args: Args, config: Config) -> Result { 42 | let path = Path::new(&args.file); 43 | let mut data = Vec::new(); 44 | 45 | if path.exists() { 46 | let mut file = File::open(&args.file)?; 47 | file.read_to_end(&mut data)?; 48 | } 49 | 50 | Ok(Self { 51 | file_name: args.file, 52 | running: true, 53 | data, 54 | starting_line: 0, 55 | cursor_x: 0, 56 | cursor_y: 0, 57 | frame_height: 0, 58 | state: AppState::Move, 59 | buffer: [' ', ' '], 60 | changes: Vec::new(), 61 | made_changes: Vec::new(), 62 | config, 63 | is_inserting: false, 64 | is_selecting: false, 65 | selection_start: 0, 66 | clipboard: Vec::new(), 67 | }) 68 | } 69 | pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { 70 | self.running = true; 71 | while self.running { 72 | terminal.draw(|frame| self.render(frame))?; 73 | self.handle_crossterm_events()?; 74 | //maybe this will become an update() func if i need more stuff 75 | self.set_startingline(); 76 | } 77 | Ok(()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | - develop 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | # ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel 15 | # and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | fmt: 22 | name: fmt 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | - name: Install Rust stable 28 | uses: dtolnay/rust-toolchain@stable 29 | with: 30 | components: rustfmt 31 | - name: check formatting 32 | run: cargo fmt -- --check 33 | - name: Cache Cargo dependencies 34 | uses: Swatinem/rust-cache@v2 35 | clippy: 36 | name: clippy 37 | runs-on: ubuntu-latest 38 | permissions: 39 | contents: read 40 | checks: write 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | - name: Install Rust stable 45 | uses: dtolnay/rust-toolchain@stable 46 | with: 47 | components: clippy 48 | - name: Run clippy action 49 | uses: clechasseur/rs-clippy-check@v3 50 | - name: Cache Cargo dependencies 51 | uses: Swatinem/rust-cache@v2 52 | doc: 53 | # run docs generation on nightly rather than stable. This enables features like 54 | # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an 55 | # API be documented as only available in some specific platforms. 56 | name: doc 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | - name: Install Rust nightly 61 | uses: dtolnay/rust-toolchain@nightly 62 | - name: Run cargo doc 63 | run: cargo doc --no-deps --all-features 64 | env: 65 | RUSTDOCFLAGS: --cfg docsrs 66 | test: 67 | runs-on: ${{ matrix.os }} 68 | name: test ${{ matrix.os }} 69 | strategy: 70 | fail-fast: false 71 | matrix: 72 | os: [macos-latest, windows-latest] 73 | steps: 74 | # if your project needs OpenSSL, uncomment this to fix Windows builds. 75 | # it's commented out by default as the install command takes 5-10m. 76 | # - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append 77 | # if: runner.os == 'Windows' 78 | # - run: vcpkg install openssl:x64-windows-static-md 79 | # if: runner.os == 'Windows' 80 | - uses: actions/checkout@v4 81 | - name: Install Rust 82 | uses: dtolnay/rust-toolchain@stable 83 | # enable this ci template to run regardless of whether the lockfile is checked in or not 84 | - name: cargo generate-lockfile 85 | if: hashFiles('Cargo.lock') == '' 86 | run: cargo generate-lockfile 87 | - name: cargo test --locked 88 | run: cargo test --locked --all-features --all-targets 89 | - name: Cache Cargo dependencies 90 | uses: Swatinem/rust-cache@v2 91 | -------------------------------------------------------------------------------- /hexhog.tape: -------------------------------------------------------------------------------- 1 | # VHS documentation 2 | # 3 | # Output: 4 | # Output .gif Create a GIF output at the given 5 | # Output .mp4 Create an MP4 output at the given 6 | # Output .webm Create a WebM output at the given 7 | # 8 | # Require: 9 | # Require Ensure a program is on the $PATH to proceed 10 | # 11 | # Settings: 12 | # Set FontSize Set the font size of the terminal 13 | # Set FontFamily Set the font family of the terminal 14 | # Set Height Set the height of the terminal 15 | # Set Width Set the width of the terminal 16 | # Set LetterSpacing Set the font letter spacing (tracking) 17 | # Set LineHeight Set the font line height 18 | # Set LoopOffset % Set the starting frame offset for the GIF loop 19 | # Set Theme Set the theme of the terminal 20 | # Set Padding Set the padding of the terminal 21 | # Set Framerate Set the framerate of the recording 22 | # Set PlaybackSpeed Set the playback speed of the recording 23 | # Set MarginFill Set the file or color the margin will be filled with. 24 | # Set Margin Set the size of the margin. Has no effect if MarginFill isn't set. 25 | # Set BorderRadius Set terminal border radius, in pixels. 26 | # Set WindowBar Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight) 27 | # Set WindowBarSize Set window bar size, in pixels. Default is 40. 28 | # Set TypingSpeed