diff --git a/src/action.rs b/src/action.rs index 7da24bd..f39636d 100644 --- a/src/action.rs +++ b/src/action.rs @@ -9,9 +9,9 @@ pub enum Action { NormalMode, SelectMode, - GPartial, - ZPartial, - RPartial, + Goto, + Zview, + Replace, MoveByteUp, MoveByteDown, @@ -61,9 +61,9 @@ impl App { Action::NormalMode => self.normal_mode(), Action::SelectMode => self.select_mode(), - Action::GPartial => self.g_partial(), - Action::ZPartial => self.z_partial(), - Action::RPartial => self.r_partial(), + Action::Goto => self.goto(), + Action::Zview => self.zview(), + Action::Replace => self.replace(), Action::MoveByteUp => self.move_byte_up(), Action::MoveByteDown => self.move_byte_down(), @@ -118,15 +118,15 @@ impl App { self.mode = Mode::Select; } - const fn g_partial(&mut self) { + const fn goto(&mut self) { self.partial_action = Some(PartialAction::Goto); } - const fn z_partial(&mut self) { + const fn zview(&mut self) { self.partial_action = Some(PartialAction::Zview); } - const fn r_partial(&mut self) { + const fn replace(&mut self) { self.partial_action = Some(PartialAction::Replace); } @@ -343,7 +343,7 @@ impl App { self.execute_and_add( EditAction::Delete { cursor: self.cursor, - data: self.contents[self.cursor.range()].into() + old_data: self.contents[self.cursor.range()].into() } ); diff --git a/src/app/mod.rs b/src/app/mod.rs index 7e07759..1b61f28 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -20,6 +20,7 @@ pub struct App { pub mode: Mode, pub partial_action: Option, + pub partial_replace: Option, pub edit_history: Vec, // some index to keep track of where we are? edit_prophecy? @@ -27,18 +28,18 @@ pub struct App { pub logs: Vec, } -#[derive(Hash, PartialEq, Eq)] +#[derive(Clone, Copy, Hash, PartialEq, Eq)] pub enum Mode { Normal, Select, Insert } -#[derive(Hash, PartialEq, Eq)] +#[derive(Clone, Copy, Hash, PartialEq, Eq)] pub enum PartialAction { Goto, Zview, Replace } impl Mode { - pub const fn label(&self) -> &'static str { + pub const fn label(self) -> &'static str { match self { Self::Normal => " NORMAL ", Self::Select => " SELECT ", @@ -46,7 +47,7 @@ impl Mode { } } - pub const fn color(&self) -> Color { + pub const fn color(self) -> Color { match self { Self::Normal => Color::Blue, Self::Select => Color::Yellow, @@ -56,7 +57,7 @@ impl Mode { } impl PartialAction { - pub const fn label(&self) -> &'static str { + pub const fn label(self) -> &'static str { match self { Self::Goto => "g", Self::Zview => "z", @@ -98,6 +99,7 @@ impl App { mode: Mode::Normal, partial_action: None, + partial_replace: None, edit_history: Vec::new(), @@ -122,17 +124,69 @@ impl App { } fn handle_key(&mut self, event: KeyEvent) { - let should_reset_partial = self.partial_action.is_some(); - - if let Some(mode_config) = self.config.0.get(&self.mode) && - let Some(keybinds) = mode_config.0.get(&self.partial_action) && - let Some(action) = keybinds.0.get(&event.into()) - { - self.execute(*action); - } - - if should_reset_partial { - self.partial_action = None; + if self.partial_action == Some(PartialAction::Replace) { + if let Some(hex_character) = event.code.as_char() && + let Some(nybble) = nybble_from_hex(hex_character) + { + if let Some(partial_replace) = self.partial_replace.take() { + self.execute_and_add( + EditAction::Replace { + cursor: self.cursor, + old_data: self.contents[self.cursor.range()].into(), + new_byte: partial_replace << 4 | nybble + } + ); + self.partial_action = None; + } else { + self.partial_replace = Some(nybble); + } + } else { + self.partial_action = None; + self.partial_replace = None; + } + } else { + let should_reset_partial = self.partial_action.is_some(); + + if let Some(mode_config) = self.config.0.get(&self.mode) && + let Some(keybinds) = mode_config.0.get(&self.partial_action) && + let Some(action) = keybinds.0.get(&event.into()) + { + self.execute(*action); + } + + if should_reset_partial { + self.partial_action = None; + } + } + } +} + +fn nybble_from_hex(hex: char) -> Option { + if !hex.is_ascii() { return None } + + match hex { + '0'..='9' => Some(u8::try_from(hex).unwrap() - u8::try_from('0').unwrap()), + 'a'..='f' => Some(u8::try_from(hex).unwrap() - u8::try_from('a').unwrap() + 10), + 'A'..='F' => Some(u8::try_from(hex).unwrap() - u8::try_from('A').unwrap() + 10), + _ => None + } +} + +mod tests { + #[allow(unused_imports)] + use crate::app::nybble_from_hex; + + #[test] + fn nybble_from_hex_case_doesnt_matter() { + for character in 'a'..='f' { + assert_eq!(nybble_from_hex(character), nybble_from_hex(character.to_ascii_uppercase())); + } + } + + #[test] + fn nybble_from_hex_digits_are_correct() { + for (index, character) in ('0'..='9').enumerate() { + assert_eq!(nybble_from_hex(character), Some(index as u8)); } } } diff --git a/src/app/widget.rs b/src/app/widget.rs index 68fa3d8..b8e9cb3 100644 --- a/src/app/widget.rs +++ b/src/app/widget.rs @@ -394,7 +394,7 @@ mod status_line { } mod extra_statuses { - use crate::app::{App, PartialAction}; + use crate::app::App; use ratatui::text::Line; impl App { @@ -404,7 +404,7 @@ mod extra_statuses { let partial_action = self.partial_action .as_ref() - .map_or("", PartialAction::label); + .map_or("", |partial_action| partial_action.label()); format!("{partial_action} {percentage:.0}% ").into() } diff --git a/src/config.rs b/src/config.rs index 173e49f..40ff008 100644 --- a/src/config.rs +++ b/src/config.rs @@ -103,9 +103,9 @@ impl Default for Config { ("v".try_into().unwrap(), Action::SelectMode), - ("g".try_into().unwrap(), Action::GPartial), - ("z".try_into().unwrap(), Action::ZPartial), - ("r".try_into().unwrap(), Action::RPartial), + ("g".try_into().unwrap(), Action::Goto), + ("z".try_into().unwrap(), Action::Zview), + ("r".try_into().unwrap(), Action::Replace), ("i".try_into().unwrap(), Action::MoveByteUp), ("k".try_into().unwrap(), Action::MoveByteDown), @@ -147,9 +147,9 @@ impl Default for Config { ("v".try_into().unwrap(), Action::NormalMode), - ("g".try_into().unwrap(), Action::GPartial), - ("z".try_into().unwrap(), Action::ZPartial), - ("r".try_into().unwrap(), Action::RPartial), + ("g".try_into().unwrap(), Action::Goto), + ("z".try_into().unwrap(), Action::Zview), + ("r".try_into().unwrap(), Action::Replace), ("i".try_into().unwrap(), Action::ExtendByteUp), ("k".try_into().unwrap(), Action::ExtendByteDown), diff --git a/src/cursor.rs b/src/cursor.rs index 5ebabe8..86c716c 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,6 +1,6 @@ use std::ops::RangeInclusive; -#[derive(Clone, Copy, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Cursor { pub head: usize, pub tail: usize diff --git a/src/edit_action.rs b/src/edit_action.rs index c402b6e..cdac870 100644 --- a/src/edit_action.rs +++ b/src/edit_action.rs @@ -4,7 +4,12 @@ use crate::{app::App, cursor::Cursor}; pub enum EditAction { Delete { cursor: Cursor, - data: Vec + old_data: Vec + }, + Replace { + cursor: Cursor, + old_data: Vec, + new_byte: u8 } } @@ -17,6 +22,9 @@ impl App { fn execute_edit(&mut self, edit_action: &EditAction) { match edit_action { EditAction::Delete { cursor, .. } => self.delete_at(*cursor), + EditAction::Replace { + cursor, old_data: _, new_byte + } => self.replace_at_with(*cursor, *new_byte), } } @@ -26,4 +34,10 @@ impl App { self.cursor.head = min(cursor.head, cursor.tail); self.cursor.collapse(); } + + fn replace_at_with(&mut self, cursor: Cursor, new_byte: u8) { + self.contents[self.cursor.range()].fill(new_byte); + + self.cursor = cursor; + } } diff --git a/src/main.rs b/src/main.rs index 189d7ec..f1d61c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,23 +18,23 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // TODO: // - undo/redo -// - modes -// - select -// - insert -// - zz/zt/zb // - modifications +// - replace +// - partial action(s) // - insert/append // - mode -// - replace -// - partial action +// - how this works with edit history is strange :/ +// - add to edit history when *leaving* insert mode // - replace-and-keep-going // - mode -// - delete // - change +// - saving +// - search // - edit character panel // - modifier on existing keys like teehee? or jump to panel? // - if jump to panel, space? -// - search +// - zz/zt/zb +// - visual gg/G // - jumplist // - f/t // - ascii? @@ -53,6 +53,8 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // - utf8? // - diffing +// when AsciiChar is stabilized, use it instead of char everywhere + fn main() { let mut app = App::init(); let mut terminal = ratatui::init();