From 9d5b2b6decd116250b7c1995fa21821609b0caa8 Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Tue, 17 Mar 2026 20:20:08 -0500 Subject: [PATCH] refactor input system --- src/action.rs | 261 ++++++++++++++++++++++++++++++++++++++++++++++ src/app/mod.rs | 243 +++++------------------------------------- src/app/widget.rs | 2 + src/config.rs | 172 ++++++++++++++++++++++++++++++ src/main.rs | 3 +- 5 files changed, 462 insertions(+), 219 deletions(-) create mode 100644 src/action.rs create mode 100644 src/config.rs diff --git a/src/action.rs b/src/action.rs new file mode 100644 index 0000000..2189375 --- /dev/null +++ b/src/action.rs @@ -0,0 +1,261 @@ +use std::cmp::min; + +use crate::{BYTES_PER_LINE, app::{App, Mode, PartialAction}}; + +#[derive(Clone, Copy)] +pub enum Action { + Quit, + + NormalMode, + SelectMode, + + GPartial, + ZPartial, + RPartial, + + MoveByteUp, + MoveByteDown, + MoveByteLeft, + MoveByteRight, + + GotoLineStart, + GotoLineEnd, + GotoFileStart, + GotoFileEnd, + + ScrollDown, + ScrollUp, + + PageCursorHalfDown, + PageCursorHalfUp, + + PageDown, + PageUp, + + MoveNextWordStart, + MoveNextWordEnd, + MovePreviousWordStart, + + CollapseSelection, +} + +impl App { + pub fn execute(&mut self, action: Action) { + match action { + Action::Quit => self.quit(), + + 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::MoveByteUp => self.move_byte_up(), + Action::MoveByteDown => self.move_byte_down(), + Action::MoveByteLeft => self.move_byte_left(), + Action::MoveByteRight => self.move_byte_right(), + + Action::GotoLineStart => self.goto_line_start(), + Action::GotoLineEnd => self.goto_line_end(), + Action::GotoFileStart => self.goto_file_start(), + Action::GotoFileEnd => self.goto_file_end(), + + Action::ScrollDown => self.scroll_down(), + Action::ScrollUp => self.scroll_up(), + + Action::PageCursorHalfDown => self.page_cursor_half_down(), + Action::PageCursorHalfUp => self.page_cursor_half_up(), + + Action::PageDown => self.page_down(), + Action::PageUp => self.page_up(), + + Action::MoveNextWordStart => self.move_next_word_start(), + Action::MoveNextWordEnd => self.move_next_word_end(), + Action::MovePreviousWordStart => self.move_previous_word_start(), + + Action::CollapseSelection => self.collapse_selection(), + } + } + + const fn quit(&mut self) { + self.should_quit = true; + } + + const fn normal_mode(&mut self) { + self.mode = Mode::Normal; + } + + const fn select_mode(&mut self) { + self.mode = Mode::Select; + } + + const fn g_partial(&mut self) { + self.partial_action = Some(PartialAction::Goto); + } + + const fn z_partial(&mut self) { + self.partial_action = Some(PartialAction::Zview); + } + + const fn r_partial(&mut self) { + self.partial_action = Some(PartialAction::Replace); + } + + const fn move_byte_up(&mut self) { + if self.cursor.head >= BYTES_PER_LINE { + self.cursor.head -= BYTES_PER_LINE; + self.cursor.collapse(); + + self.clamp_screen_to_cursor(); + } + } + + const fn move_byte_down(&mut self) { + if self.contents.len() - 1 - self.cursor.head >= BYTES_PER_LINE { + self.cursor.head += BYTES_PER_LINE; + self.cursor.collapse(); + + self.clamp_screen_to_cursor(); + } + } + + const fn move_byte_left(&mut self) { + if self.cursor.head >= 1 { + self.cursor.head -= 1; + self.cursor.collapse(); + + self.clamp_screen_to_cursor(); + } + } + + const fn move_byte_right(&mut self) { + if self.contents.len() - 1 - self.cursor.head >= 1 { + self.cursor.head += 1; + self.cursor.collapse(); + + self.clamp_screen_to_cursor(); + } + } + + const fn goto_line_start(&mut self) { + self.cursor.head -= self.cursor.head % BYTES_PER_LINE; + self.cursor.collapse(); + } + + const fn goto_line_end(&mut self) { + self.cursor.head += BYTES_PER_LINE - 1 - (self.cursor.head % BYTES_PER_LINE); + self.cursor.collapse(); + } + + const fn goto_file_start(&mut self) { + self.cursor.head %= BYTES_PER_LINE; + self.cursor.collapse(); + self.clamp_screen_to_cursor(); + } + + const fn goto_file_end(&mut self) { + self.cursor.head = previous_multiple_of(BYTES_PER_LINE, self.contents.len()) + + (self.cursor.head % BYTES_PER_LINE); + + self.cursor.collapse(); + self.clamp_screen_to_cursor(); + } + + fn scroll_down(&mut self) { + self.scroll_position = min( + self.scroll_position + BYTES_PER_LINE, + self.contents.len() - (5 * BYTES_PER_LINE) + ); + self.cursor.clamp(self.scroll_position, self.screen_size()); + } + + fn scroll_up(&mut self) { + self.scroll_position = self.scroll_position.saturating_sub(BYTES_PER_LINE); + self.cursor.clamp(self.scroll_position, self.screen_size()); + } + + fn page_cursor_half_down(&mut self) { + let head_offset = self.cursor.head - self.scroll_position; + let tail_offset = self.cursor.tail - self.scroll_position; + + self.scroll_position = min( + self.scroll_position + (self.screen_size() / 2).next_multiple_of(BYTES_PER_LINE), + self.contents.len() - (5 * BYTES_PER_LINE) + ); + + self.cursor.head = (self.scroll_position + head_offset).min(self.contents.len() - 1); + self.cursor.tail = (self.scroll_position + tail_offset).min(self.contents.len() - 1); + } + + fn page_cursor_half_up(&mut self) { + let head_offset = self.cursor.head - self.scroll_position; + let tail_offset = self.cursor.tail - self.scroll_position; + + self.scroll_position = self.scroll_position.saturating_sub( + (self.screen_size() / 2).next_multiple_of(BYTES_PER_LINE) + ); + + self.cursor.head = (self.scroll_position + head_offset).min(self.contents.len() - 1); + self.cursor.tail = (self.scroll_position + tail_offset).min(self.contents.len() - 1); + } + + fn page_down(&mut self) { + self.scroll_position = min( + self.scroll_position + self.screen_size(), + self.contents.len() - (5 * BYTES_PER_LINE) + ); + self.cursor.clamp(self.scroll_position, self.screen_size()); + } + + fn page_up(&mut self) { + self.scroll_position = self.scroll_position.saturating_sub( + self.screen_size() + ); + self.cursor.clamp(self.scroll_position, self.screen_size()); + } + + fn move_next_word_start(&mut self) { + self.cursor.move_to_next_word(self.contents.len() - 1); + self.clamp_screen_to_cursor(); + } + + fn move_next_word_end(&mut self) { + self.cursor.move_to_next_end(self.contents.len() - 1); + self.clamp_screen_to_cursor(); + } + + const fn move_previous_word_start(&mut self) { + self.cursor.move_to_previous_beginning(); + self.clamp_screen_to_cursor(); + } + + const fn collapse_selection(&mut self) { + self.cursor.collapse(); + } +} + +// helpers +impl App { + // in bytes + const fn screen_size(&self) -> usize { + self.window_rows * BYTES_PER_LINE + } + + const fn clamp_screen_to_cursor(&mut self) { + if self.cursor.head < self.scroll_position { + self.scroll_position -= (self.scroll_position - self.cursor.head).next_multiple_of(BYTES_PER_LINE); + } else if self.cursor.head > self.scroll_position + self.screen_size() - 1 { + let screen_edge_offset_to_cursor = self.cursor.head - (self.scroll_position + self.screen_size() - 1); + self.scroll_position += screen_edge_offset_to_cursor.next_multiple_of(BYTES_PER_LINE); + } + } +} + +const fn previous_multiple_of(multiple: usize, number: usize) -> usize { + if number == 0 { + 0 + } else { + (number - 1) - ((number - 1) % multiple) + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 9943ef1..c8e2e1c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,13 +1,13 @@ -use std::{cmp::min, env, fs::File, io::Read, path::PathBuf, process::exit}; -use crossterm::{event::{self, Event, KeyCode, KeyModifiers}, terminal::window_size}; +use std::{env, fs::File, io::Read, path::PathBuf, process::exit}; +use crossterm::{event::{self, Event, KeyEvent}, terminal::window_size}; use ratatui::style::Color; -use crate::{BYTES_PER_LINE, cursor::Cursor}; +use crate::{config::Config, cursor::Cursor}; mod widget; -#[derive(Debug)] pub struct App { + pub config: Config, pub file_name: String, pub contents: Vec, pub window_rows: usize, @@ -19,12 +19,12 @@ pub struct App { pub logs: Vec, } -#[derive(Debug)] +#[derive(Hash, PartialEq, Eq)] pub enum Mode { Normal, Select, Insert } -#[derive(Debug)] +#[derive(Hash, PartialEq, Eq)] pub enum PartialAction { Goto, Zview, Replace } @@ -75,6 +75,7 @@ impl App { file.unwrap().read_to_end(&mut contents).unwrap(); Self { + config: Config::default(), file_name: file_path.file_name().unwrap().to_str().unwrap().to_owned(), contents, // -1 because of the status line @@ -88,228 +89,34 @@ impl App { } } - // in bytes - const fn screen_size(&self) -> usize { - self.window_rows * BYTES_PER_LINE - } - #[allow(clippy::too_many_lines)] pub fn handle_events(&mut self) { #[allow(clippy::collapsible_match)] - match (&self.mode, event::read().unwrap(), &self.partial_action) { - (Mode::Normal, Event::Resize(_, height), _) => { + match event::read().unwrap() { + Event::Resize(_, height) => { // -1 because of the status line self.window_rows = height as usize - 1; } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('q') => { - self.should_quit = true; - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.modifiers.contains(KeyModifiers::CONTROL) && - key_event.code == KeyCode::Char('e') => { - self.scroll_position = min( - self.scroll_position + BYTES_PER_LINE, - self.contents.len() - (5 * BYTES_PER_LINE) - ); - self.cursor.clamp(self.scroll_position, self.screen_size()); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.modifiers.contains(KeyModifiers::CONTROL) && - key_event.code == KeyCode::Char('y') => { - self.scroll_position = self.scroll_position.saturating_sub(BYTES_PER_LINE); - self.cursor.clamp(self.scroll_position, self.screen_size()); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.modifiers.contains(KeyModifiers::CONTROL) && - key_event.code == KeyCode::Char('d') => { - let head_offset = self.cursor.head - self.scroll_position; - let tail_offset = self.cursor.tail - self.scroll_position; - - self.scroll_position = min( - self.scroll_position + (self.screen_size() / 2).next_multiple_of(BYTES_PER_LINE), - self.contents.len() - (5 * BYTES_PER_LINE) - ); - - self.cursor.head = (self.scroll_position + head_offset).min(self.contents.len() - 1); - self.cursor.tail = (self.scroll_position + tail_offset).min(self.contents.len() - 1); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.modifiers.contains(KeyModifiers::CONTROL) && - key_event.code == KeyCode::Char('u') => { - let head_offset = self.cursor.head - self.scroll_position; - let tail_offset = self.cursor.tail - self.scroll_position; - - self.scroll_position = self.scroll_position.saturating_sub( - (self.screen_size() / 2).next_multiple_of(BYTES_PER_LINE) - ); - - self.cursor.head = (self.scroll_position + head_offset).min(self.contents.len() - 1); - self.cursor.tail = (self.scroll_position + tail_offset).min(self.contents.len() - 1); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.modifiers.contains(KeyModifiers::CONTROL) && - key_event.code == KeyCode::Char('f') => { - self.scroll_position = min( - self.scroll_position + self.screen_size(), - self.contents.len() - (5 * BYTES_PER_LINE) - ); - self.cursor.clamp(self.scroll_position, self.screen_size()); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.modifiers.contains(KeyModifiers::CONTROL) && - key_event.code == KeyCode::Char('b') => { - self.scroll_position = self.scroll_position.saturating_sub( - self.screen_size() - ); - self.cursor.clamp(self.scroll_position, self.screen_size()); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('g') => { - self.partial_action = Some(PartialAction::Goto); - } - - (Mode::Normal, Event::Key(key_event), Some(PartialAction::Goto)) - if key_event.code == KeyCode::Char('g') => { - self.partial_action = None; - self.cursor.head %= BYTES_PER_LINE; - self.cursor.collapse(); - self.clamp_screen_to_cursor(); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('G') => { - self.cursor.head = previous_multiple_of(BYTES_PER_LINE, self.contents.len()) + - (self.cursor.head % BYTES_PER_LINE); - - self.cursor.collapse(); - self.clamp_screen_to_cursor(); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('z') => { - self.partial_action = Some(PartialAction::Zview); - } - - (Mode::Normal, Event::Key(key_event), Some(PartialAction::Zview)) - if key_event.code == KeyCode::Char('z') => { - self.partial_action = None; - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('i') || - key_event.code == KeyCode::Up => { - self.partial_action = None; - if self.cursor.head >= BYTES_PER_LINE { - self.cursor.head -= BYTES_PER_LINE; - self.cursor.collapse(); - - self.clamp_screen_to_cursor(); - } - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('j') || - key_event.code == KeyCode::Left => { - if self.cursor.head >= 1 { - self.cursor.head -= 1; - self.cursor.collapse(); - - self.clamp_screen_to_cursor(); - } - } - - (Mode::Normal, Event::Key(key_event), Some(PartialAction::Goto)) - if key_event.code == KeyCode::Char('j') || - key_event.code == KeyCode::Left => { - self.partial_action = None; - self.cursor.head -= self.cursor.head % BYTES_PER_LINE; - self.cursor.collapse(); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('k') || - key_event.code == KeyCode::Down => { - if self.contents.len() - 1 - self.cursor.head >= BYTES_PER_LINE { - self.cursor.head += BYTES_PER_LINE; - self.cursor.collapse(); - - self.clamp_screen_to_cursor(); - } - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('l') || - key_event.code == KeyCode::Right => { - if self.contents.len() - 1 - self.cursor.head >= 1 { - self.cursor.head += 1; - self.cursor.collapse(); - - self.clamp_screen_to_cursor(); - } - } - - (Mode::Normal, Event::Key(key_event), Some(PartialAction::Goto)) - if key_event.code == KeyCode::Char('l') || - key_event.code == KeyCode::Right => { - self.partial_action = None; - self.cursor.head += BYTES_PER_LINE - 1 - (self.cursor.head % BYTES_PER_LINE); - self.cursor.collapse(); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('w') => { - self.cursor.move_to_next_word(self.contents.len() - 1); - self.clamp_screen_to_cursor(); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('e') => { - self.cursor.move_to_next_end(self.contents.len() - 1); - self.clamp_screen_to_cursor(); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char('b') => { - self.cursor.move_to_previous_beginning(); - self.clamp_screen_to_cursor(); - } - - (Mode::Normal, Event::Key(key_event), None) - if key_event.code == KeyCode::Char(';') => { - self.cursor.collapse(); - } - - (Mode::Normal, Event::Key(_), Some(_)) => { - self.partial_action = None; - } - + Event::Key(key_event) => self.handle_key(key_event), + // Event::Mouse(mouse_event) => { + // mouse_event.kind + // }, _ => {} } } - const fn clamp_screen_to_cursor(&mut self) { - if self.cursor.head < self.scroll_position { - self.scroll_position -= (self.scroll_position - self.cursor.head).next_multiple_of(BYTES_PER_LINE); - } else if self.cursor.head > self.scroll_position + self.screen_size() - 1 { - let screen_edge_offset_to_cursor = self.cursor.head - (self.scroll_position + self.screen_size() - 1); - self.scroll_position += screen_edge_offset_to_cursor.next_multiple_of(BYTES_PER_LINE); + 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; } } } - -const fn previous_multiple_of(multiple: usize, number: usize) -> usize { - if number == 0 { - 0 - } else { - (number - 1) - ((number - 1) % multiple) - } -} diff --git a/src/app/widget.rs b/src/app/widget.rs index c7c663a..9014dc4 100644 --- a/src/app/widget.rs +++ b/src/app/widget.rs @@ -19,6 +19,7 @@ impl Widget for &App { .map(|(bytes, address)| self.render_line(address, bytes)); let remainder_address = bytes_end - remainder.len(); + #[allow(clippy::if_not_else)] let remainder_line = if !remainder.is_empty() { Some(self.render_partial_line(remainder_address, remainder)) } else { @@ -113,6 +114,7 @@ mod hex { let (chunks, remainder) = bytes.as_chunks::(); let remainder_address = address + chunks.len() * BYTES_PER_CHUNK; + #[allow(clippy::if_not_else)] let remainder_chunks: Option> = if !remainder.is_empty() { Some(self.render_partial_chunk(remainder_address, remainder).collect()) } else { diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..2e15bd1 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,172 @@ +use std::collections::HashMap; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crate::{action::Action, app::{Mode, PartialAction}}; + +pub struct Config( + pub HashMap +); + +pub struct ModeConfig( + pub HashMap, Keybinds> +); + +pub struct Keybinds( + pub HashMap +); + +#[derive(PartialEq, Eq, Hash)] +pub struct Keypress { + code: KeyCode, + modifiers: KeyModifiers +} + +impl From<[(Mode, ModeConfig); N]> for Config { + fn from(array: [(Mode, ModeConfig); N]) -> Self { + Self(array.into()) + } +} + +impl From<[(Option, Keybinds); N]> for ModeConfig { + fn from(array: [(Option, Keybinds); N]) -> Self { + Self(array.into()) + } +} + +impl From<[(Keypress, Action); N]> for Keybinds { + fn from(array: [(Keypress, Action); N]) -> Self { + Self(array.into()) + } +} + +impl From for Keypress { + fn from(key_code: KeyCode) -> Self { + Self { + code: key_code, + modifiers: KeyModifiers::NONE + } + } +} + +const fn modifier_from_character(character: char) -> Option { + match character { + 'A' => Some(KeyModifiers::ALT), + 'C' => Some(KeyModifiers::CONTROL), + _ => None + } +} + +impl TryFrom<&str> for Keypress { + type Error = (); + + fn try_from(string: &str) -> Result { + match string.len() { + 3 => { + Ok(Self { + code: KeyCode::Char( + string.chars().nth(2).unwrap() + ), + modifiers: modifier_from_character( + string.chars().nth(0).unwrap() + ).ok_or(())?, + }) + } + 1 => { + Ok( + KeyCode::Char( + string.chars().nth(0).unwrap() + ).into() + ) + } + _ => Err(()) + } + } +} + +impl From for Keypress { + fn from(event: KeyEvent) -> Self { + Self { + code: event.code, + modifiers: match event.modifiers { + KeyModifiers::SHIFT => KeyModifiers::NONE, + x => x, + }, + } + } +} + +impl Default for Config { + fn default() -> Self { + [ + (Mode::Normal, [ + (None, [ + ("q".try_into().unwrap(), Action::Quit), + + ("v".try_into().unwrap(), Action::SelectMode), + + ("g".try_into().unwrap(), Action::GPartial), + ("z".try_into().unwrap(), Action::ZPartial), + ("r".try_into().unwrap(), Action::RPartial), + + ("i".try_into().unwrap(), Action::MoveByteUp), + ("k".try_into().unwrap(), Action::MoveByteDown), + ("j".try_into().unwrap(), Action::MoveByteLeft), + ("l".try_into().unwrap(), Action::MoveByteRight), + + ("G".try_into().unwrap(), Action::GotoFileEnd), + + ("C-e".try_into().unwrap(), Action::ScrollDown), + ("C-y".try_into().unwrap(), Action::ScrollUp), + + ("C-d".try_into().unwrap(), Action::PageCursorHalfDown), + ("C-u".try_into().unwrap(), Action::PageCursorHalfUp), + + ("C-f".try_into().unwrap(), Action::PageDown), + ("C-b".try_into().unwrap(), Action::PageUp), + + ("w".try_into().unwrap(), Action::MoveNextWordStart), + ("e".try_into().unwrap(), Action::MoveNextWordEnd), + ("b".try_into().unwrap(), Action::MovePreviousWordStart), + + (";".try_into().unwrap(), Action::CollapseSelection), + ].into()), + (Some(PartialAction::Goto), [ + ("j".try_into().unwrap(), Action::GotoLineStart), + ("l".try_into().unwrap(), Action::GotoLineEnd), + + ("g".try_into().unwrap(), Action::GotoFileStart), + ].into()) + ].into()), + (Mode::Select, [ + (None, [ + ("q".try_into().unwrap(), Action::Quit), + + ("v".try_into().unwrap(), Action::NormalMode), + + ("g".try_into().unwrap(), Action::GPartial), + ("z".try_into().unwrap(), Action::ZPartial), + ("r".try_into().unwrap(), Action::RPartial), + + // ("i".try_into().unwrap(), Action::ExtendByteUp), + // ("k".try_into().unwrap(), Action::ExtendByteDown), + // ("j".try_into().unwrap(), Action::ExtendByteLeft), + // ("l".try_into().unwrap(), Action::ExtendByteRight), + + ("C-e".try_into().unwrap(), Action::ScrollDown), + ("C-y".try_into().unwrap(), Action::ScrollUp), + + ("C-d".try_into().unwrap(), Action::PageCursorHalfDown), + ("C-u".try_into().unwrap(), Action::PageCursorHalfUp), + + ("C-f".try_into().unwrap(), Action::PageDown), + ("C-b".try_into().unwrap(), Action::PageUp), + + // ("w".try_into().unwrap(), Action::ExtendNextWordStart), + // ("e".try_into().unwrap(), Action::ExtendNextWordEnd), + // ("b".try_into().unwrap(), Action::ExtendPreviousWordStart), + + (";".try_into().unwrap(), Action::CollapseSelection), + ].into()) + ].into()) + ].into() + } +} diff --git a/src/main.rs b/src/main.rs index 9145a28..1815973 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,14 +7,15 @@ mod cardinality; mod empty_span; mod custom_greys; mod app; +mod config; mod cursor; +mod action; const BYTES_PER_LINE: usize = 0x10; const BYTES_PER_CHUNK: usize = 4; const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // TODO: -// - refactor input system // - undo/redo // - x/X // - modes