9 Commits

Author SHA1 Message Date
alice pellerin 64f8944e59 set OSC 6 (current document) 2026-05-28 18:28:33 -05:00
alice pellerin 9c6f273703 add brew install to readme 2026-05-23 15:22:24 -05:00
alice pellerin 01a0ab5978 click tab bar to go to buffer 2026-05-16 16:46:09 -05:00
alice pellerin 6517de6b61 allow switching buffers in visual mode 2026-05-16 16:29:57 -05:00
alice pellerin 42a83b1245 normal escape stop inspecting 2026-05-16 16:29:57 -05:00
alice pellerin 906a152a21 fix color555 to 888 conversion 2026-05-16 16:14:52 -05:00
alice pellerin b721091c3c Update README.md 2026-05-05 19:53:06 -05:00
alice pellerin a69409a36f update readme 2026-05-05 18:13:46 -05:00
alice pellerin 1f77123cb0 update Node.js-20-depending github actions 2026-05-05 15:23:14 -05:00
9 changed files with 59 additions and 45 deletions
+2 -2
View File
@@ -45,7 +45,7 @@ jobs:
# cut off the v part of the tag to only search for the number # cut off the v part of the tag to only search for the number
run: grep --quiet "$(echo "${{ github.ref_name }}" | cut -c2-)" Cargo.toml run: grep --quiet "$(echo "${{ github.ref_name }}" | cut -c2-)" Cargo.toml
- name: cache - name: cache
uses: actions/cache@v4 uses: actions/cache@v5
with: with:
path: | path: |
~/.cargo/registry ~/.cargo/registry
@@ -68,7 +68,7 @@ jobs:
if: ${{ matrix.info.package-extension == 'zip' }} if: ${{ matrix.info.package-extension == 'zip' }}
run: tar --create --auto-compress --file "hexapoda-${{ matrix.info.os }}-${{ github.ref_name }}.zip" "completions" "manpage" -C "target/release/" "hexapoda${{ matrix.info.executable-extension }}" run: tar --create --auto-compress --file "hexapoda-${{ matrix.info.os }}-${{ github.ref_name }}.zip" "completions" "manpage" -C "target/release/" "hexapoda${{ matrix.info.executable-extension }}"
- name: release - name: release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
draft: true draft: true
name: "${{ github.ref_name }}" name: "${{ github.ref_name }}"
+7 -18
View File
@@ -1,4 +1,4 @@
# <img height=40 align=top src="https://github.com/simonomi/hexapoda/blob/main/icon/bug%20colored%20large.png?raw=true"> hexapoda # <img height=40 align=top src="https://github.com/simonomi/hexapoda/blob/main/icon/bug%20colored%20large.png?raw=true"> [hexapoda](https://simonomi.dev/hexapoda)
a colorful modal hex editor a colorful modal hex editor
@@ -8,24 +8,13 @@ a colorful modal hex editor
## status ## status
currently, hexapoda is very unpolished, and missing some major features. if you'd be interested in using it, please let me know! if enough people want, i'd be willing to make it more accessible and write some docs still missing some notable features (see [todo.md](https://github.com/simonomi/hexapoda/blob/main/todo.md)), but ready for general use. visit [the website](https://simonomi.dev/hexapoda) for more detailed documentation
## features ## installation
- [color-codes bytes](https://simonomi.dev/blog/color-code-your-bytes) by value - short answer: `brew install hexapoda`, `cargo binstall hexapoda`, or `cargo install hexapoda`
- modal editing - [slightly longer answer](https://simonomi.dev/hexapoda/install)
- selection-first, like [Kakoune](https://kakoune.org) and [Helix](https://helix-editor.com)
- multiple selections
- split selection(s) into #-byte chunks
- undo/redo
- inspect the current selection(s)
- signed, unsigned, binary, fixed-point, UTF-8, color
- mark notable offsets
- jump to selected offset
### notable features that are missing (for now) ## config
- search see [https://simonomi.dev/hexapoda/config](https://simonomi.dev/hexapoda/config)
- diffing
- inserting bytes
- only replacing and deleting right now
+8
View File
@@ -190,6 +190,8 @@ pub enum BufferAction {
InspectSelection, InspectSelection,
InspectSelectionColor, InspectSelectionColor,
StopInspecting,
} }
impl BufferAction { impl BufferAction {
@@ -264,6 +266,8 @@ impl BufferAction {
InspectSelection => true, InspectSelection => true,
InspectSelectionColor => true, InspectSelectionColor => true,
StopInspecting => true,
} }
} }
} }
@@ -339,6 +343,8 @@ impl From<BufferAction> for &str {
InspectSelection => "inspect_selection", InspectSelection => "inspect_selection",
InspectSelectionColor => "inspect_selection_color", InspectSelectionColor => "inspect_selection_color",
StopInspecting => "stop_inspecting",
} }
} }
} }
@@ -423,6 +429,8 @@ impl TryFrom<&str> for BufferAction {
"inspect_selection" => Ok(InspectSelection), "inspect_selection" => Ok(InspectSelection),
"inspect_selection_color" => Ok(InspectSelectionColor), "inspect_selection_color" => Ok(InspectSelectionColor),
"stop_inspecting" => Ok(StopInspecting),
_ => Err(()), _ => Err(()),
} }
} }
+22 -16
View File
@@ -57,7 +57,7 @@ impl App {
let mut buffers: Vec<Buffer> = files let mut buffers: Vec<Buffer> = files
.iter() .iter()
.filter_map(|path| { .filter_map(|path| {
Buffer::from_file_at(path.clone()) Buffer::from_file_at(path)
.inspect_err(|error| { .inspect_err(|error| {
error_alert = Some( error_alert = Some(
Span::raw(format!("error reading '{}': {error}", path.display())).red() Span::raw(format!("error reading '{}': {error}", path.display())).red()
@@ -198,6 +198,7 @@ impl App {
fn handle_mouse(&mut self, mouse_event: MouseEvent) -> bool { fn handle_mouse(&mut self, mouse_event: MouseEvent) -> bool {
let position = self.mouse_event_position(mouse_event); let position = self.mouse_event_position(mouse_event);
let tab_bar_row_count = u16::from(self.buffers.len() > 1);
let current_buffer = &mut self.buffers[self.current_buffer_index]; let current_buffer = &mut self.buffers[self.current_buffer_index];
match mouse_event.kind { match mouse_event.kind {
@@ -207,6 +208,8 @@ impl App {
current_buffer.cursors.clear(); current_buffer.cursors.clear();
current_buffer.clamp_screen_to_primary_cursor(self.window_size); current_buffer.clamp_screen_to_primary_cursor(self.window_size);
self.is_dragging_mouse = true; self.is_dragging_mouse = true;
} else if mouse_event.row < tab_bar_row_count {
self.switch_to_tab_at(mouse_event.column);
} }
true true
@@ -248,10 +251,10 @@ impl App {
} }
fn mouse_event_position(&self, mouse_event: MouseEvent) -> Option<usize> { fn mouse_event_position(&self, mouse_event: MouseEvent) -> Option<usize> {
let tab_bar_rows = usize::from(self.buffers.len() > 1); let tab_bar_row_count = usize::from(self.buffers.len() > 1);
if usize::from(mouse_event.row) < tab_bar_rows || if usize::from(mouse_event.row) < tab_bar_row_count ||
usize::from(mouse_event.row) - tab_bar_rows >= self.window_size.hex_rows() { usize::from(mouse_event.row) - tab_bar_row_count >= self.window_size.hex_rows() {
return None; return None;
} }
@@ -283,20 +286,23 @@ impl App {
byte_column.map(|byte_column| { byte_column.map(|byte_column| {
current_buffer.scroll_position + current_buffer.scroll_position +
(mouse_event.row as usize - tab_bar_rows) * BYTES_PER_LINE + (mouse_event.row as usize - tab_bar_row_count) * BYTES_PER_LINE +
byte_column byte_column
}) })
}
fn switch_to_tab_at(&mut self, column: u16) {
let mut column: usize = column.into();
// if let Some(byte_column) = byte_column && for buffer_index in 0..self.buffers.len() {
// mouse_event.row as usize - tab_bar_rows < self.window_size.hex_rows() let tab_width = self.buffers[buffer_index].file_name.len() + 2;
// {
// Some( if column < tab_width {
// current_buffer.scroll_position + self.current_buffer_index = buffer_index;
// (mouse_event.row as usize - tab_bar_rows) * BYTES_PER_LINE + return;
// byte_column }
// )
// } else { column -= tab_width;
// None }
// }
} }
} }
+4 -2
View File
@@ -1,4 +1,4 @@
use std::{collections::HashSet, fs::File, io::{self, Read}, path::PathBuf}; use std::{collections::HashSet, fs::File, io::{self, Read}, path::{Path, PathBuf}};
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use ratatui::{style::Stylize, text::Span}; use ratatui::{style::Stylize, text::Span};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -75,7 +75,9 @@ impl TryFrom<&str> for PartialAction {
} }
impl Buffer { impl Buffer {
pub fn from_file_at(file_path: PathBuf) -> io::Result<Self> { pub fn from_file_at(file_path: &Path) -> io::Result<Self> {
let file_path = file_path.canonicalize()?;
let mut file = File::open(&file_path)?; let mut file = File::open(&file_path)?;
let mut contents = Vec::new(); let mut contents = Vec::new();
file.read_to_end(&mut contents)?; file.read_to_end(&mut contents)?;
+6 -5
View File
@@ -72,6 +72,8 @@ impl Buffer {
BufferAction::InspectSelection => self.inspect_selection(), BufferAction::InspectSelection => self.inspect_selection(),
BufferAction::InspectSelectionColor => self.inspect_selection_color(), BufferAction::InspectSelectionColor => self.inspect_selection_color(),
BufferAction::StopInspecting => {},
} }
} }
@@ -949,11 +951,10 @@ const fn is_illegal_control_character(character: char) -> bool {
} }
} }
const fn color555_to_color888(color555: u16) -> [u8; 3] { fn color555_to_color888(color555: u16) -> [u8; 3] {
[ [
// 8 is the ratio between the number of colors in 555 vs 888 (32:256) (u8::try_from((color555 & 0b11111) * 255 / 31).unwrap()),
(color555 & 0b11111) as u8 * 8, (u8::try_from((color555 >> 5 & 0b11111) * 255 / 31).unwrap()),
(color555 >> 5 & 0b11111) as u8 * 8, (u8::try_from((color555 >> 10 & 0b11111) * 255 / 31).unwrap())
(color555 >> 10 & 0b11111) as u8 * 8
] ]
} }
+3
View File
@@ -10,6 +10,9 @@ mod extra_statuses;
impl Widget for &Buffer { impl Widget for &Buffer {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) { fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
// set OSC 6 (current document)
print!("\x1B]6;{}\x07", self.file_path.display());
let screen_end = self.scroll_position + BYTES_PER_LINE * (area.height as usize - 1); let screen_end = self.scroll_position + BYTES_PER_LINE * (area.height as usize - 1);
let bytes_end = min(screen_end, self.contents.len()); let bytes_end = min(screen_end, self.contents.len());
+5
View File
@@ -92,6 +92,8 @@ impl Default for Config {
(keypress("C- "), InspectSelection.into()), (keypress("C- "), InspectSelection.into()),
(keypress("A- "), InspectSelectionColor.into()), (keypress("A- "), InspectSelectionColor.into()),
(keypress("escape"), StopInspecting.into()),
].into()), ].into()),
(Some(PartialAction::Goto), [ (Some(PartialAction::Goto), [
(keypress("j"), GotoLineStart.into()), (keypress("j"), GotoLineStart.into()),
@@ -203,6 +205,9 @@ impl Default for Config {
(keypress("u"), Undo.into()), (keypress("u"), Undo.into()),
(keypress("U"), Redo.into()), (keypress("U"), Redo.into()),
(keypress("C-j"), PreviousBuffer.into()),
(keypress("C-l"), NextBuffer.into()),
(keypress("C"), CopySelectionOnNextLine.into()), (keypress("C"), CopySelectionOnNextLine.into()),
(keypress("("), RotateSelectionsBackward.into()), (keypress("("), RotateSelectionsBackward.into()),
+2 -2
View File
@@ -1,12 +1,12 @@
# todo # todo
- v1.0 - v1.0
- `<escape>` dismiss inspectors
- `T` Till - `T` Till
- click on tab to go to buffer
- `go` goto entered offset - `go` goto entered offset
- search - search
- `/` hex, `A-/` ASCII - `/` hex, `A-/` ASCII
- if non-hex-digit typed, search ASCII - if non-hex-digit typed, search ASCII
- `A-*` repeat (entered number) times
- copy/paste (`<space>y`/`<space>p`)
- inspector translations for varint [#1](https://github.com/simonomi/hexapoda/issues/1#issue-4232822634) - inspector translations for varint [#1](https://github.com/simonomi/hexapoda/issues/1#issue-4232822634)
- `M` mark at selected offset? (like `Jm`) - `M` mark at selected offset? (like `Jm`)
- diffing - diffing