From 6f5674ee7ac3f062049734d1b86e36277bd0b7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Merc=C3=AAs?= Date: Mon, 2 Feb 2026 01:39:37 -0300 Subject: [PATCH 1/6] Basic undo --- src/hex/edit.rs | 38 ++++++++++++++++++-------------------- src/hex/events.rs | 6 ++++++ src/hex/hex_view.rs | 2 ++ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/hex/edit.rs b/src/hex/edit.rs index 052054f..2e34eb1 100644 --- a/src/hex/edit.rs +++ b/src/hex/edit.rs @@ -10,19 +10,17 @@ use std::io::Result; pub fn fill_with(app: &mut App, with: u8, advance: bool) { let s = format!("{:02X}", with); app.hex_view.changed_bytes.insert(app.hex_view.offset, s); + app.hex_view.changed_history.push(app.hex_view.offset); if advance { app.goto(app.hex_view.offset + 1); } } -// This function handles the key presses in the goto dialog -/// ESC will cancel any changes, a regular character is copied -/// to the input pub fn edit_events(app: &mut App, key: KeyEvent) -> Result { match key.code { - KeyCode::Esc => { + KeyCode::Esc | KeyCode::Enter => { app.state = UIState::Normal; - app.hex_view.changed_bytes.clear(); + // app.hex_view.changed_bytes.clear(); app.dialog_renderer = None; app.hex_view.editing_hex = true; } @@ -51,34 +49,38 @@ pub fn edit_events(app: &mut App, key: KeyEvent) -> Result { KeyCode::Char(c) => { if app.hex_view.editing_hex { if c.is_ascii_hexdigit() && !key.modifiers.contains(KeyModifiers::CONTROL) { - // If the hashmap contains the key, it means the user typed two - // chars already. Concatenate the second char to the value + // If the hashmap contains the key, it means the user has typed + // one character if app .hex_view .changed_bytes .contains_key(&app.hex_view.offset) { - // Pega o valor atual para checar se já tem 2 caracteres, o que significa - // que o usuário voltou com a seta para esquerda e vai mudar o que digitou + // Get the current value and check if it has two characters, meaning + // the user navigated back to an already changed offset and will change + // it again let value = app .hex_view .changed_bytes .get_mut(&app.hex_view.offset) - .unwrap(); // Acho que é seguro porque a já testamos que .contains_key() + .unwrap(); // It should be safe as we checked for .contains_key() if value.len() == 2 { - // Já tem dois caracteres lá, então remove e insere o que o usuário digitou - // let _ = app.hex_mode.changed_bytes.remove(&app.hex_mode.offset); + // There are two characters there already, restart the process + // by replacing the value using the same key app.hex_view .changed_bytes .insert(app.hex_view.offset, c.to_ascii_uppercase().to_string()); + // Update history for undo command + app.hex_view.changed_history.push(app.hex_view.offset); } else { - // Não tem dois caracteres lá, então concatena o que o usuário digitou - // com o caractere existente + // If the number of characters there is not two, concatenate + // what's in there with whatever the user typed (*value).push(c.to_ascii_uppercase()); + app.hex_view.changed_history.push(app.hex_view.offset); app.goto(app.hex_view.offset + 1); } } else { - // Primeiro caractere digitado, só coloca no hashmap + // First char was typed, just add it to the hashmap app.hex_view .changed_bytes .insert(app.hex_view.offset, c.to_ascii_uppercase().to_string()); @@ -116,14 +118,10 @@ pub fn edit_events(app: &mut App, key: KeyEvent) -> Result { app.hex_view.editing_hex = true; } } - } else if !app.hex_view.editing_hex { + } else { fill_with(app, c as u8, true); } } - KeyCode::Enter => { - app.state = UIState::Normal; - app.hex_view.editing_hex = true; // just in case it was in ASCII before - } _ => {} } Ok(false) diff --git a/src/hex/events.rs b/src/hex/events.rs index e2b88ee..b63d131 100644 --- a/src/hex/events.rs +++ b/src/hex/events.rs @@ -288,6 +288,12 @@ pub fn hex_mode_events(app: &mut App, key: KeyEvent) -> Result { app.hex_view.selection.end = app.hex_view.offset; } } + // undo + KeyCode::Char('u') => { + if let Some(ofs) = app.hex_view.changed_history.pop() { + let _ = app.hex_view.changed_bytes.remove(&ofs); + } + } _ => {} } Ok(false) diff --git a/src/hex/hex_view.rs b/src/hex/hex_view.rs index 8c519cb..fda574f 100644 --- a/src/hex/hex_view.rs +++ b/src/hex/hex_view.rs @@ -21,6 +21,8 @@ pub struct HexView { #[serde(skip)] pub changed_bytes: HashMap, #[serde(skip)] + pub changed_history: Vec, + #[serde(skip)] pub comment_input: Input, // the input comment widget (tui-input) // `comment_name_list` is used to show comments in Names list From 83a28228c1c9ac7be4a09b7e712657986af012f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Merc=C3=AAs?= Date: Mon, 2 Feb 2026 01:59:48 -0300 Subject: [PATCH 2/6] Update README --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 59a70a7..98ccb35 100644 --- a/README.md +++ b/README.md @@ -98,13 +98,14 @@ If you need permanent settings, create a `$HOME/.dz6init` file containing any of | `End` or `$` | Set the cursor to the end of the current line | | | `Ctrl+Home` or `G` | Go to the first offset | | | `Ctrl+End` or `Shift+G` | Go to the last offset in the file | | -| `Page Down` | Move down one page | A page has 1KB by default | +| `Page Down` | Move down one page | | | `Page Up` | Move up one page | | | `r` | Enter [replace mode](#hex-replace-mode) | | | `z` | Enter replace mode and set the byte under the cursor zero | | | `Ctrl+a` | Enter replace mode and increment byte under the cursor | | | `Ctrl+x` | Enter replace mode and decrement byte under the cursor | | -| `v` | Enter [selection mode](#hex-selection-mode) | | +| `v` | Enter [select mode](#hex-selection-mode) | | +| `u` | Undo a change | | | `/` | Search | Search the entire file. `Tab` cycles between ASCII and hex search | | `n` | Search next | | | `s` | Open [Strings](#strings) window | | @@ -131,17 +132,16 @@ If you need permanent settings, create a `$HOME/.dz6init` file containing any of #### Hex replace mode -| Key | Action | Tips | -| ----------- | ---------------------------------------------------------- | ----------------------------- | -| Arrow keys | Navigation | | -| `Backspace` | The same as navigating left | | -| `z` | Set the byte to zero | | -| `Ctrl+a` | Increment byte | | -| `Ctrl+x` | Decrement byte | | -| `Enter` | Save changes to file | | -| `Esc` | Cancel changes | | -| `Tab` | Cycle through hex and ASCII dump to edit the file in ASCII | | -| `T` | Truncate the file at the selected offset | Be aware this can't be undone | +| Key | Action | Tips | +| ----------- | ---------------------------------------------------------- | -------------------------------------------------------- | +| Arrow keys | Navigation | | +| `Backspace` | The same as navigating left | | +| `z` | Set byte to zero | | +| `Ctrl+a` | Increment byte | | +| `Ctrl+x` | Decrement byte | | +| `Esc` | Go back to normal mode | Changes are saved to buffer, but not written to file yet | +| `Tab` | Cycle through hex and ASCII dump to edit the file in ASCII | | +| `T` | Truncate the file at the selected offset | Be aware this can't be undone | #### Names From 2b256e8206c4246dd48b2ceb2fbccb374b47e3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Merc=C3=AAs?= Date: Tue, 10 Feb 2026 23:59:02 -0300 Subject: [PATCH 3/6] Prevent changed bytes from being cleaned when editing from normal mode --- src/hex/events.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/hex/events.rs b/src/hex/events.rs index b63d131..4b9dbe5 100644 --- a/src/hex/events.rs +++ b/src/hex/events.rs @@ -172,7 +172,6 @@ pub fn hex_mode_events(app: &mut App, key: KeyEvent) -> Result { KeyCode::Char('z') => { if !app.file_info.is_read_only && app.hex_view.offset < app.file_info.size { app.state = UIState::HexEditing; - app.hex_view.changed_bytes.clear(); hex::edit::fill_with(app, 0x00, true); } } @@ -184,7 +183,6 @@ pub fn hex_mode_events(app: &mut App, key: KeyEvent) -> Result { && key.modifiers.contains(KeyModifiers::CONTROL) { app.state = UIState::HexEditing; - app.hex_view.changed_bytes.clear(); let ofs = app.hex_view.offset; if let Some(s) = app.hex_view.changed_bytes.get(&ofs) { if let Ok(b) = u8::from_str_radix(s, 16) { @@ -203,7 +201,6 @@ pub fn hex_mode_events(app: &mut App, key: KeyEvent) -> Result { && key.modifiers.contains(KeyModifiers::CONTROL) { app.state = UIState::HexEditing; - app.hex_view.changed_bytes.clear(); let ofs = app.hex_view.offset; if let Some(s) = app.hex_view.changed_bytes.get(&ofs) { if let Ok(b) = u8::from_str_radix(s, 16) { From 5e7bdd2404216a5006bab3d47613750d4b9986ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Merc=C3=AAs?= Date: Tue, 10 Feb 2026 23:59:32 -0300 Subject: [PATCH 4/6] Beep if there's nothing to undo --- src/hex/events.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hex/events.rs b/src/hex/events.rs index 4b9dbe5..cc40bfd 100644 --- a/src/hex/events.rs +++ b/src/hex/events.rs @@ -217,7 +217,7 @@ pub fn hex_mode_events(app: &mut App, key: KeyEvent) -> Result { app.state = UIState::DialogHelp; app.dialog_renderer = Some(hex::help::dialog_help_draw); } - // reaplce + // replace KeyCode::Char('r') => { if app.file_info.is_read_only { print!("\x07"); // beep @@ -289,6 +289,8 @@ pub fn hex_mode_events(app: &mut App, key: KeyEvent) -> Result { KeyCode::Char('u') => { if let Some(ofs) = app.hex_view.changed_history.pop() { let _ = app.hex_view.changed_bytes.remove(&ofs); + } else { + print!("\x07"); // beep if there's nothing to undo } } _ => {} From ce908c86062f168348a6dc1d532551ea178c761d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Merc=C3=AAs?= Date: Tue, 10 Feb 2026 23:59:47 -0300 Subject: [PATCH 5/6] Handle undo when the user types a nibble in replace mode --- src/hex/draw.rs | 5 +++++ src/hex/edit.rs | 1 + 2 files changed, 6 insertions(+) diff --git a/src/hex/draw.rs b/src/hex/draw.rs index 6785498..ba74f12 100644 --- a/src/hex/draw.rs +++ b/src/hex/draw.rs @@ -85,6 +85,11 @@ pub fn draw_hex_contents(app: &mut App, frame: &mut Frame, area: Rect) { // typed chars in content instead of original ones byte_content = app.hex_view.changed_bytes[&offset].clone(); byte_style = app.config.theme.changed_bytes; + + // prepend a '0' while the user doesn't type the highest nibble + if byte_content.len() == 1 { + byte_content.insert(0, '0'); + } } // TODO: column size (2) keep the separator char from being shown :( diff --git a/src/hex/edit.rs b/src/hex/edit.rs index 2e34eb1..ff16aa5 100644 --- a/src/hex/edit.rs +++ b/src/hex/edit.rs @@ -84,6 +84,7 @@ pub fn edit_events(app: &mut App, key: KeyEvent) -> Result { app.hex_view .changed_bytes .insert(app.hex_view.offset, c.to_ascii_uppercase().to_string()); + app.hex_view.changed_history.push(app.hex_view.offset); } } else if c == 'z' { // zero out bytes From 0bf3844bfab56a5cd75999fc2a8e19901a251f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Merc=C3=AAs?= Date: Wed, 11 Feb 2026 00:04:12 -0300 Subject: [PATCH 6/6] Format code --- src/hex/draw.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hex/draw.rs b/src/hex/draw.rs index ba74f12..ebcaecc 100644 --- a/src/hex/draw.rs +++ b/src/hex/draw.rs @@ -87,7 +87,7 @@ pub fn draw_hex_contents(app: &mut App, frame: &mut Frame, area: Rect) { byte_style = app.config.theme.changed_bytes; // prepend a '0' while the user doesn't type the highest nibble - if byte_content.len() == 1 { + if byte_content.len() == 1 { byte_content.insert(0, '0'); } }