diff --git a/editor-challenge/src/main.rs b/editor-challenge/src/main.rs index 7673fc3..640d096 100644 --- a/editor-challenge/src/main.rs +++ b/editor-challenge/src/main.rs @@ -3,7 +3,10 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode}, }; use std::{ - io::{self, Read}, + env, + fs::File, + io::{self, BufRead, BufReader, Read}, + path::PathBuf, sync::mpsc, thread, time::{Duration, Instant}, @@ -20,7 +23,113 @@ const TITLE: &str = "Text Editor Challenge"; const COPYRIGHT: &str = "(c) Savanni D'Gerinel - all rights reserved"; const TICK_RATE_MS: u64 = 200; -fn render(terminal: &mut Terminal) -> Result<(), anyhow::Error> +#[derive(Default)] +struct Document { + rows: Vec +} + +impl Document { + fn contents(&self) -> String { + self.rows.join("\n") + } + + fn row_length(&self, idx: usize) -> usize { + self.rows[idx].len() + } + + fn row_count(&self) -> usize { + self.rows.len() + } +} + +#[derive(Default)] +struct AppState { + path: Option, + cursor: Cursor, + contents: Document, // Obviously this is bad, but it's also only temporary. +} + +impl AppState { + fn open(path: PathBuf) -> Self { + let mut file = File::open(path.clone()).unwrap(); + let mut reader = BufReader::new(file); + let contents = reader + .lines() + .collect::, std::io::Error>>() + .unwrap(); + + Self { + path: Some(path), + cursor: Default::default(), + contents: Document{ rows: contents }, + } + } + + fn cursor_up(&mut self) { + self.cursor.cursor_up(&self.contents); + } + + fn cursor_down(&mut self) { + self.cursor.cursor_down(&self.contents); + } + + fn cursor_right(&mut self) { + self.cursor.cursor_right(&self.contents); + } + + fn cursor_left(&mut self) { + self.cursor.cursor_left(); + } +} + +#[derive(Default)] +struct Cursor { + row: usize, + column: usize, + desired_column: usize, +} + +impl Cursor { + fn cursor_up(&mut self, doc: &Document) { + if self.row > 0 { + self.row -= 1; + } + self.correct_columns(doc); + } + + fn cursor_down(&mut self, doc: &Document) { + if self.row < doc.row_count() - 1 { + self.row += 1; + } + self.correct_columns(doc); + } + + fn correct_columns(&mut self, doc: &Document) { + let row_len = doc.row_length(self.row); + if self.desired_column < row_len { + self.column = self.desired_column; + } else { + self.column = row_len; + } + } + + fn cursor_right(&mut self, doc: &Document) { + if self.column < doc.row_length(self.row) { + self.column += 1; + } + self.desired_column = self.column; + } + + fn cursor_left(&mut self) { + if self.column > 0 { + self.column -= 1; + } + self.desired_column = self.column; + } + +} + +fn render(app_state: &AppState, terminal: &mut Terminal) -> Result<(), anyhow::Error> where T: tui::backend::Backend, { @@ -39,16 +148,22 @@ where ) .split(size); - let title = Paragraph::new(TITLE) - .style(Style::default().fg(Color::LightCyan)) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .style(Style::default().fg(Color::White)) - .title("Copyright") - .border_type(BorderType::Plain), - ); + let title = Paragraph::new( + app_state + .path + .clone() + .map(|path| path.to_string_lossy().to_owned().into()) + .unwrap_or("No file opened".to_owned()), + ) + .style(Style::default().fg(Color::LightCyan)) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .style(Style::default().fg(Color::White)) + .title("Copyright") + .border_type(BorderType::Plain), + ); rect.render_widget(title, chunks[0]); let cp = Paragraph::new(COPYRIGHT) @@ -62,6 +177,15 @@ where .border_type(BorderType::Plain), ); rect.render_widget(cp, chunks[2]); + + // TODO: rework scrolling as soon as I have a working cursor + let contents = Paragraph::new(app_state.contents.contents()).scroll((0, 0)); + rect.render_widget(contents, chunks[1]); + + // TODO: keeping track of the index of the top row and subtract that from the cursor row. + let row = app_state.cursor.row as u16; + let column = app_state.cursor.column as u16; + rect.set_cursor(chunks[1].x + column, chunks[1].y + row); })?; Ok(()) } @@ -94,6 +218,7 @@ fn handle_input(tx: mpsc::Sender>, tick_rate: Duration) { } fn app_loop( + mut app_state: AppState, terminal: &mut Terminal, rx: mpsc::Receiver>, ) -> Result<(), anyhow::Error> @@ -101,7 +226,7 @@ where T: tui::backend::Backend, { loop { - render(terminal)?; + render(&app_state, terminal)?; let event = rx.recv()?; @@ -111,6 +236,18 @@ where { break; } + Event::Input(KeyEvent { code, .. }) if code == KeyCode::Down => { + app_state.cursor_down(); + } + Event::Input(KeyEvent { code, .. }) if code == KeyCode::Up => { + app_state.cursor_up(); + } + Event::Input(KeyEvent { code, .. }) if code == KeyCode::Right => { + app_state.cursor_right(); + } + Event::Input(KeyEvent { code, .. }) if code == KeyCode::Left => { + app_state.cursor_left(); + } _ => {} } } @@ -118,6 +255,15 @@ where } fn main() -> Result<(), anyhow::Error> { + let args = env::args().collect::>(); + + let file_name = if args.len() > 1 { Some(&args[1]) } else { None }; + + let app_state = match file_name { + Some(name) => AppState::open(name.into()), + None => Default::default(), + }; + let (tx, rx) = mpsc::channel(); let tick_rate = Duration::from_millis(TICK_RATE_MS); @@ -132,7 +278,7 @@ fn main() -> Result<(), anyhow::Error> { let mut terminal = Terminal::new(backend)?; let _ = terminal.clear()?; - let result = app_loop(&mut terminal, rx); + let result = app_loop(app_state, &mut terminal, rx); disable_raw_mode()?; terminal.show_cursor()?;