rearchitect into buffers, allow opening multiple files
This commit is contained in:
+44
-169
@@ -1,122 +1,55 @@
|
||||
use std::{env, fs::File, io::Read, path::PathBuf, process::exit};
|
||||
use std::{cmp::min, env, process::exit};
|
||||
use crossterm::{event::{self, Event, KeyEvent}, terminal::window_size};
|
||||
use ratatui::{style::Color, text::Span};
|
||||
use crate::{config::Config, cursor::Cursor, edit_action::EditAction};
|
||||
use crate::{BYTES_PER_LINE, buffer::Buffer, config::Config};
|
||||
|
||||
mod widget;
|
||||
|
||||
pub struct App {
|
||||
pub config: Config,
|
||||
pub file_name: String,
|
||||
pub file_path: PathBuf,
|
||||
|
||||
pub contents: Vec<u8>,
|
||||
pub buffers: Vec<Buffer>,
|
||||
pub current_buffer_index: usize,
|
||||
|
||||
pub window_rows: usize,
|
||||
pub covered_window_rows: usize,
|
||||
|
||||
pub scroll_position: usize,
|
||||
pub cursor: Cursor,
|
||||
pub window_size: WindowSize,
|
||||
|
||||
pub should_quit: bool,
|
||||
|
||||
pub mode: Mode,
|
||||
pub partial_action: Option<PartialAction>,
|
||||
pub partial_replace: Option<u8>,
|
||||
|
||||
pub edit_history: Vec<EditAction>,
|
||||
// the index *after* the latest edit action
|
||||
pub time_traveling: Option<usize>,
|
||||
// the index *after* the last saved edit action
|
||||
pub last_saved_at: Option<usize>,
|
||||
|
||||
pub alert_message: Span<'static>,
|
||||
|
||||
pub logs: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Normal, Select, Insert
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum PartialAction {
|
||||
Goto, View, Replace, Space
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Normal => " NORMAL ",
|
||||
Self::Select => " SELECT ",
|
||||
Self::Insert => " INSERT ",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn color(self) -> Color {
|
||||
match self {
|
||||
Self::Normal => Color::Blue,
|
||||
Self::Select => Color::Yellow,
|
||||
Self::Insert => Color::Green,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialAction {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Goto => "g",
|
||||
Self::View => "z",
|
||||
Self::Replace => "r",
|
||||
Self::Space => "␠",
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct WindowSize {
|
||||
pub rows: usize,
|
||||
pub covered_rows: usize,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn init() -> Self {
|
||||
let input_files: Vec<_> = env::args().skip(1).collect();
|
||||
pub fn new() -> Self {
|
||||
let buffers: Vec<Buffer> = env::args()
|
||||
.skip(1)
|
||||
.map(Into::into)
|
||||
.map(Buffer::new)
|
||||
.collect();
|
||||
|
||||
if input_files.is_empty() {
|
||||
if buffers.is_empty() {
|
||||
println!("please provide at least one file as input");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
assert!(input_files.len() == 1);
|
||||
|
||||
let file_path: PathBuf = input_files.first().unwrap().into();
|
||||
|
||||
let file = File::open(&file_path);
|
||||
let mut contents = Vec::new();
|
||||
file.unwrap().read_to_end(&mut contents).unwrap();
|
||||
|
||||
Self {
|
||||
config: Config::default(),
|
||||
file_name: file_path.file_name().unwrap().to_str().unwrap().to_owned(),
|
||||
file_path,
|
||||
|
||||
contents,
|
||||
buffers,
|
||||
current_buffer_index: 0,
|
||||
|
||||
window_rows: window_size().unwrap().rows as usize,
|
||||
// 1 because of the status line
|
||||
covered_window_rows: 1,
|
||||
|
||||
scroll_position: 0,
|
||||
cursor: Cursor::default(),
|
||||
window_size: WindowSize {
|
||||
rows: window_size().unwrap().rows as usize,
|
||||
// 1 because of the status line
|
||||
covered_rows: 1,
|
||||
},
|
||||
|
||||
should_quit: false,
|
||||
|
||||
mode: Mode::Normal,
|
||||
partial_action: None,
|
||||
partial_replace: None,
|
||||
|
||||
edit_history: Vec::new(),
|
||||
time_traveling: None,
|
||||
last_saved_at: Some(0),
|
||||
|
||||
alert_message: "".into(),
|
||||
|
||||
logs: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -126,7 +59,7 @@ impl App {
|
||||
#[allow(clippy::collapsible_match)]
|
||||
match event::read().unwrap() {
|
||||
Event::Resize(_, height) => {
|
||||
self.window_rows = height as usize;
|
||||
self.window_size.rows = height as usize;
|
||||
}
|
||||
Event::Key(key_event) => self.handle_key(key_event),
|
||||
// Event::Mouse(mouse_event) => {
|
||||
@@ -136,93 +69,35 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, event: KeyEvent) {
|
||||
self.alert_message = "".into();
|
||||
fn handle_key(&mut self, key_event: KeyEvent) {
|
||||
self.buffers[self.current_buffer_index]
|
||||
.handle_key(key_event, &self.config, self.window_size);
|
||||
|
||||
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);
|
||||
}
|
||||
if self.current_buffer().should_close {
|
||||
self.buffers.remove(self.current_buffer_index);
|
||||
|
||||
if self.buffers.is_empty() {
|
||||
self.should_quit = true;
|
||||
} 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;
|
||||
self.current_buffer_index = min(
|
||||
self.current_buffer_index,
|
||||
self.buffers.len() - 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn has_unsaved_changes(&self) -> bool {
|
||||
!self.all_changes_saved()
|
||||
}
|
||||
|
||||
pub const fn all_changes_saved(&self) -> bool {
|
||||
if let Some(last_saved_at) = self.last_saved_at {
|
||||
if let Some(time_traveling) = self.time_traveling {
|
||||
last_saved_at == time_traveling
|
||||
} else {
|
||||
last_saved_at == self.edit_history.len()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// returns 0 if empty
|
||||
pub const fn max_contents_index(&self) -> usize {
|
||||
self.contents.len().saturating_sub(1)
|
||||
fn current_buffer(&self) -> &Buffer {
|
||||
&self.buffers[self.current_buffer_index]
|
||||
}
|
||||
}
|
||||
|
||||
fn nybble_from_hex(hex: char) -> Option<u8> {
|
||||
if !hex.is_ascii() { return None; }
|
||||
impl WindowSize {
|
||||
pub const fn visible_byte_count(&self) -> usize {
|
||||
self.hex_rows() * BYTES_PER_LINE
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
pub const fn hex_rows(&self) -> usize {
|
||||
self.rows - self.covered_rows
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user