diff --git a/src/app/mod.rs b/src/app/mod.rs index 1facd87..f61905e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,7 +1,7 @@ use std::{cmp::min, env, fs::File, io::Read, process::exit}; use crossterm::event::{self, Event, KeyCode, KeyModifiers}; -use crate::BYTES_PER_LINE; +use crate::{BYTES_PER_LINE, cursor::Cursor}; mod widget; @@ -9,7 +9,7 @@ mod widget; pub struct App { pub contents: Vec, pub scroll_position: usize, - // pub cursor_position: usize, + pub cursor: Cursor, pub should_quit: bool } @@ -33,25 +33,82 @@ impl App { Self { contents, scroll_position: 0, - // cursor_position: 0, - should_quit: false, + cursor: Cursor::default(), + should_quit: false } } pub fn handle_events(&mut self) { + #[allow(clippy::collapsible_match)] match event::read().unwrap() { Event::Key(key_event) if key_event.code == KeyCode::Char('q') => { self.should_quit = true; } - Event::Key(key_event) if key_event.code == KeyCode::Char('e') && - key_event.modifiers.contains(KeyModifiers::CONTROL) => { + Event::Key(key_event) if key_event.modifiers.contains(KeyModifiers::CONTROL) && + key_event.code == KeyCode::Char('e') => { let max_scroll_position = self.contents.len() - 0x50; self.scroll_position = min(self.scroll_position + BYTES_PER_LINE, max_scroll_position); } - Event::Key(key_event) if key_event.code == KeyCode::Char('y') && - key_event.modifiers.contains(KeyModifiers::CONTROL) => { + Event::Key(key_event) if key_event.modifiers.contains(KeyModifiers::CONTROL) && + key_event.code == KeyCode::Char('y') => { self.scroll_position = self.scroll_position.saturating_sub(BYTES_PER_LINE); } + Event::Key(key_event) if key_event.code == KeyCode::Char('i') => { + if self.cursor.head >= 0x10 { + self.cursor.head -= 0x10; + self.cursor.collapse(); + } + } + Event::Key(key_event) if key_event.code == KeyCode::Char('j') => { + if self.cursor.head >= 1 { + self.cursor.head -= 1; + self.cursor.collapse(); + } + } + Event::Key(key_event) if key_event.code == KeyCode::Char('k') => { + if self.contents.len() - 1 - self.cursor.head >= 0x10 { + self.cursor.head += 0x10; + self.cursor.collapse(); + } + } + Event::Key(key_event) if key_event.code == KeyCode::Char('l') => { + if self.contents.len() - 1 - self.cursor.head >= 1 { + self.cursor.head += 1; + self.cursor.collapse(); + } + } + Event::Key(key_event) if key_event.code == KeyCode::Char('I') => { + if self.cursor.head >= 0x10 { + self.cursor.head -= 0x10; + } + } + Event::Key(key_event) if key_event.code == KeyCode::Char('J') => { + if self.cursor.head >= 1 { + self.cursor.head -= 1; + } + } + Event::Key(key_event) if key_event.code == KeyCode::Char('K') => { + if self.contents.len() - 1 - self.cursor.head >= 0x10 { + self.cursor.head += 0x10; + } + } + Event::Key(key_event) if key_event.code == KeyCode::Char('L') => { + if self.contents.len() - 1 - self.cursor.head >= 1 { + self.cursor.head += 1; + } + } + Event::Key(key_event) if key_event.code == KeyCode::Char('w') => { + self.cursor.to_next_word(self.contents.len() - 1); + } + Event::Key(key_event) if key_event.code == KeyCode::Char('e') => { + self.cursor.to_next_end(self.contents.len() - 1); + } + Event::Key(key_event) if key_event.code == KeyCode::Char('b') => { + self.cursor.to_previous_beginning(); + } + Event::Key(key_event) if key_event.code == KeyCode::Char(';') => { + self.cursor.collapse(); + } _ => {} } } diff --git a/src/app/widget.rs b/src/app/widget.rs index 525d681..9eb0019 100644 --- a/src/app/widget.rs +++ b/src/app/widget.rs @@ -19,7 +19,7 @@ impl Widget for &App { let lines: Vec<_> = chunks .iter() .zip((self.scroll_position..).step_by(BYTES_PER_LINE)) - .map(|(bytes, address)| render_line(address, bytes)) + .map(|(bytes, address)| self.render_line(address, bytes)) .collect(); let text = Text::from(lines); @@ -28,15 +28,17 @@ impl Widget for &App { } } -#[allow(mismatched_lifetime_syntaxes)] -fn render_line(address: usize, bytes: &[u8; BYTES_PER_LINE]) -> Line { - let spans: Vec> = iter::once(address::render_address(address)) - .chain(hex::render_chunks(bytes)) - .chain(iter::once(" ".into())) - .chain(character_panel::render_character_panel(bytes)) - .collect(); +impl App { + #[allow(mismatched_lifetime_syntaxes)] + fn render_line(&self, address: usize, bytes: &[u8; BYTES_PER_LINE]) -> Line { + let spans: Vec> = iter::once(address::render_address(address)) + .chain(self.render_chunks(address, bytes)) + .chain(iter::once(" ".into())) + .chain(character_panel::render_character_panel(bytes)) + .collect(); - Line::from(spans) + Line::from(spans) + } } mod address { @@ -53,38 +55,60 @@ mod address { mod hex { use std::{borrow::Cow, mem}; use itertools::Itertools; - use ratatui::{style::{Color, Style}, text::Span}; + use ratatui::{style::{Color, Style, Stylize}, text::Span}; - use crate::{BYTES_PER_LINE, BYTES_PER_CHUNK, cardinality::HasCardinality, empty_span::empty_span}; + use crate::{BYTES_PER_LINE, BYTES_PER_CHUNK, app::App, cardinality::HasCardinality, empty_span::empty_span, cursor::InCursor}; - pub fn render_chunks(bytes: &[u8; BYTES_PER_LINE]) -> impl IntoIterator> { - let (chunks, remainder) = bytes.as_chunks::(); + impl App { + pub fn render_chunks( + &self, + address: usize, + bytes: &[u8; BYTES_PER_LINE] + ) -> impl IntoIterator> { + let (chunks, remainder) = bytes.as_chunks::(); + + assert!(remainder.is_empty()); + + #[allow(unstable_name_collisions)] + chunks + .iter() + .copied() + .zip((address..).step_by(BYTES_PER_CHUNK)) + .map(|(chunk, address)| self.render_chunk(address, chunk)) + .intersperse(vec![" ".into()]) + .flatten() + } - assert!(remainder.is_empty()); + fn render_chunk( + &self, + address: usize, + bytes: [u8; BYTES_PER_CHUNK] + ) -> Vec> { + #[allow(unstable_name_collisions)] + bytes + .iter() + .copied() + .zip(address..) + .map(|(byte, address)| self.render_byte_at(address, byte)) + .intersperse(" ".into()) // TODO: highlight if selected + .collect() + } - #[allow(unstable_name_collisions)] - chunks - .iter() - .copied() - .map(render_chunk) - .intersperse(vec![" ".into()]) - .flatten() - } - - fn render_chunk(bytes: [u8; BYTES_PER_CHUNK]) -> Vec> { - #[allow(unstable_name_collisions)] - bytes - .iter() - .copied() - .map(render_byte) - .intersperse(" ".into()) - .collect() - } - - fn render_byte(byte: u8) -> Span<'static> { - const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_byte_lookup_table(); - - SPAN_FOR_BYTE[byte as usize].clone() + fn render_byte_at( + &self, + address: usize, + byte: u8 + ) -> Span<'static> { + const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_byte_lookup_table(); + + let span = SPAN_FOR_BYTE[byte as usize].clone(); + + match self.cursor.contains(address) { + Some(InCursor::Head) => span.bg(Color::Gray), + Some(InCursor::Rest) => span.bg(Color::Indexed(242)), + None => span, + } + } } const fn create_byte_lookup_table() -> [Span<'static>; u8::CARDINALITY] { @@ -144,7 +168,9 @@ mod character_panel { use crate::{BYTES_PER_LINE, cardinality::HasCardinality, empty_span::empty_span}; - pub fn render_character_panel(bytes: &[u8; BYTES_PER_LINE]) -> impl IntoIterator> { + pub fn render_character_panel( + bytes: &[u8; BYTES_PER_LINE] + ) -> impl IntoIterator> { bytes .iter() .copied() diff --git a/src/cursor.rs b/src/cursor.rs new file mode 100644 index 0000000..f67678b --- /dev/null +++ b/src/cursor.rs @@ -0,0 +1,288 @@ +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Cursor { + pub head: usize, + pub tail: usize +} + +pub enum InCursor { + Head, + Rest +} + +impl Cursor { + pub const fn contains(&self, index: usize) -> Option { + if index == self.head { + Some(InCursor::Head) + } else if (self.head < index && index <= self.tail) || + (self.tail <= index && index < self.head) + { + Some(InCursor::Rest) + } else { + None + } + } + + pub const fn collapse(&mut self) { + self.tail = self.head; + } + + pub fn to_next_word(&mut self, max: usize) { + if self.head == max { return } + + if self.head % 4 == 0 { // at the beginning of a word + self.head = (self.head + 4).min(max); + } { + self.head = self.head.next_multiple_of(4).min(max); + } + self.collapse(); + } + + pub fn to_next_end(&mut self, max: usize) { + if self.head == max { return } + + self.collapse(); + if self.head % 4 == 3 { // at the end of a word + self.tail = self.head + 1; + self.head = (self.head + 4).min(max); + } else { + self.head = ((self.head + 1).next_multiple_of(4) - 1).min(max); + } + } + + pub const fn to_previous_beginning(&mut self) { + if self.head == 0 { return } + + self.collapse(); + if self.head % 4 == 0 { // at the beginning of a word + self.tail = self.head - 1; + self.head -= 4; + } else { + self.head -= self.head % 4; + } + } +} + +mod tests { + use crate::cursor::Cursor; + + impl Cursor { + #[allow(dead_code)] + const fn at(index: usize) -> Self { + Self { head: index, tail: index } + } + } + + #[test] + fn next_word() { + // [a]bcd efgh -> abcd [e]fgh + let mut cursor = Cursor::at(0); + cursor.to_next_word(99); + assert_eq!(cursor, Cursor::at(4)); + + // a[b]cd efgh -> abcd [e]fgh + let mut cursor = Cursor::at(1); + cursor.to_next_word(99); + assert_eq!(cursor, Cursor::at(4)); + + // ab[c]d efgh -> abcd [e]fgh + let mut cursor = Cursor::at(2); + cursor.to_next_word(99); + assert_eq!(cursor, Cursor::at(4)); + + // abc[d] efgh -> abcd [e]fgh + let mut cursor = Cursor::at(3); + cursor.to_next_word(99); + assert_eq!(cursor, Cursor::at(4)); + + // [a]bcd -> abc[d] + let mut cursor = Cursor::at(0); + cursor.to_next_word(3); + assert_eq!(cursor, Cursor::at(3)); + + // [a]bc -> ab[c] + let mut cursor = Cursor::at(0); + cursor.to_next_word(2); + assert_eq!(cursor, Cursor::at(2)); + + // [a]b -> a[b] + let mut cursor = Cursor::at(0); + cursor.to_next_word(1); + assert_eq!(cursor, Cursor::at(1)); + + // [a] -> [a] + let mut cursor = Cursor::at(0); + cursor.to_next_word(0); + assert_eq!(cursor, Cursor::at(0)); + + // ab[c]d -> abc[d] + let mut cursor = Cursor::at(2); + cursor.to_next_word(3); + assert_eq!(cursor, Cursor::at(3)); + + // abc[d] -> abc[d] + let mut cursor = Cursor::at(3); + cursor.to_next_word(3); + assert_eq!(cursor, Cursor::at(3)); + + // ab[c[d] -> ab[c[d] + let mut cursor = Cursor { tail: 2, head: 3 }; + cursor.to_next_word(3); + assert_eq!(cursor, Cursor { tail: 2, head: 3 }); + } + + #[test] + fn next_end() { + // [a]bcd -> [abcd] + let mut cursor = Cursor::at(0); + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 0, head: 3 }); + + // a[b]cd -> [abcd] + let mut cursor = Cursor::at(1); + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 1, head: 3 }); + + // ab[c]d -> [abcd] + let mut cursor = Cursor::at(2); + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 2, head: 3 }); + + // abc[d] efgh -> abcd [efgh] + let mut cursor = Cursor::at(3); + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 4, head: 7 }); + + // abcd [e]fgh -> abcd [efgh] + let mut cursor = Cursor::at(4); + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 4, head: 7 }); + + // abcd e[f]gh -> abcd e[fgh] + let mut cursor = Cursor::at(5); + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 5, head: 7 }); + + // abcd ef[g]h -> abcd ef[gh] + let mut cursor = Cursor::at(6); + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 6, head: 7 }); + + // abcd efg[h] ijkl -> abcd efgh [ijkl] + let mut cursor = Cursor::at(7); + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 8, head: 11 }); + + // abcd efg[h] -> abcd efg[h] + let mut cursor = Cursor::at(7); + cursor.to_next_end(7); + assert_eq!(cursor, Cursor { tail: 7, head: 7 }); + + // abcd e[fgh] -> abcd e[fgh] + let mut cursor = Cursor { tail: 5, head: 7 }; + cursor.to_next_end(7); + assert_eq!(cursor, Cursor { tail: 5, head: 7 }); + + // a[b]c -> a[bc] + let mut cursor = Cursor::at(1); + cursor.to_next_end(2); + assert_eq!(cursor, Cursor { tail: 1, head: 2 }); + + // a[bc] -> a[bc] + let mut cursor = Cursor { tail: 1, head: 2}; + cursor.to_next_end(2); + assert_eq!(cursor, Cursor { tail: 1, head: 2 }); + + // a[b] -> a[b] + let mut cursor = Cursor::at(1); + cursor.to_next_end(1); + assert_eq!(cursor, Cursor::at(1)); + + // [a]b -> [ab] + let mut cursor = Cursor::at(0); + cursor.to_next_end(1); + assert_eq!(cursor, Cursor { tail: 0, head: 1 }); + + // [ab] -> [ab] + let mut cursor = Cursor { tail: 0, head: 1}; + cursor.to_next_end(1); + assert_eq!(cursor, Cursor { tail: 0, head: 1 }); + + // [a] -> [a] + let mut cursor = Cursor::at(0); + cursor.to_next_end(0); + assert_eq!(cursor, Cursor::at(0)); + + // [a]bcd] -> [abc[d] + let mut cursor = Cursor { head: 0, tail: 3 }; + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 0, head: 3 }); + + // [a[b]cd -> a[bc[d] + let mut cursor = Cursor { tail: 0, head: 1 }; + cursor.to_next_end(99); + assert_eq!(cursor, Cursor { tail: 1, head: 3 }); + + // abc[d] ef -> abcd [ef] + let mut cursor = Cursor::at(3); + cursor.to_next_end(5); + assert_eq!(cursor, Cursor { tail: 4, head: 5 }); + } + + #[test] + fn previous_beginning() { + // abcd efgh [i]jkl -> abcd [efgh] ijkl + let mut cursor = Cursor::at(8); + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 4, tail: 7 }); + + // abcd efg[h] -> abcd [efgh] + let mut cursor = Cursor::at(7); + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 4, tail: 7 }); + + // abcd ef[g]h -> abcd [efg]h + let mut cursor = Cursor::at(6); + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 4, tail: 6 }); + + // abcd e[f]gh -> abcd [ef]gh + let mut cursor = Cursor::at(5); + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 4, tail: 5 }); + + // abcd [e]fgh -> [abcd] efgh + let mut cursor = Cursor::at(4); + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 0, tail: 3 }); + + // abc[d] -> [abcd] + let mut cursor = Cursor::at(3); + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 0, tail: 3 }); + + // ab[c]d -> [abc]d + let mut cursor = Cursor::at(2); + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 0, tail: 2 }); + + // a[b]cd -> [ab]cd + let mut cursor = Cursor::at(1); + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 0, tail: 1 }); + + // [a]bcd -> [a]bcd + let mut cursor = Cursor::at(0); + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 0, tail: 0 }); + + // [abc[d] -> [a]bcd] + let mut cursor = Cursor { tail: 0, head: 3 }; + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 0, tail: 3 }); + + // ab[c]d] -> [a]bc]d + let mut cursor = Cursor { head: 2, tail: 3 }; + cursor.to_previous_beginning(); + assert_eq!(cursor, Cursor { head: 0, tail: 2 }); + } +} diff --git a/src/main.rs b/src/main.rs index 5a8aef1..7e6ddff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use app::App; mod cardinality; mod empty_span; mod app; +mod cursor; const BYTES_PER_LINE: usize = 0x10; const BYTES_PER_CHUNK: usize = 4;