Render the contents and set up a functioning cursor

This commit is contained in:
Savanni D'Gerinel 2024-05-06 00:50:45 -04:00
parent 612713ab1b
commit 61127339bc
1 changed files with 160 additions and 14 deletions

View File

@ -3,7 +3,10 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode}, terminal::{disable_raw_mode, enable_raw_mode},
}; };
use std::{ use std::{
io::{self, Read}, env,
fs::File,
io::{self, BufRead, BufReader, Read},
path::PathBuf,
sync::mpsc, sync::mpsc,
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
@ -20,7 +23,113 @@ const TITLE: &str = "Text Editor Challenge";
const COPYRIGHT: &str = "(c) Savanni D'Gerinel - all rights reserved"; const COPYRIGHT: &str = "(c) Savanni D'Gerinel - all rights reserved";
const TICK_RATE_MS: u64 = 200; const TICK_RATE_MS: u64 = 200;
fn render<T>(terminal: &mut Terminal<T>) -> Result<(), anyhow::Error> #[derive(Default)]
struct Document {
rows: Vec<String>
}
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<PathBuf>,
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::<Result<Vec<String>, 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<T>(app_state: &AppState, terminal: &mut Terminal<T>) -> Result<(), anyhow::Error>
where where
T: tui::backend::Backend, T: tui::backend::Backend,
{ {
@ -39,7 +148,13 @@ where
) )
.split(size); .split(size);
let title = Paragraph::new(TITLE) 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)) .style(Style::default().fg(Color::LightCyan))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.block( .block(
@ -62,6 +177,15 @@ where
.border_type(BorderType::Plain), .border_type(BorderType::Plain),
); );
rect.render_widget(cp, chunks[2]); 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(()) Ok(())
} }
@ -94,6 +218,7 @@ fn handle_input(tx: mpsc::Sender<Event<KeyEvent>>, tick_rate: Duration) {
} }
fn app_loop<T>( fn app_loop<T>(
mut app_state: AppState,
terminal: &mut Terminal<T>, terminal: &mut Terminal<T>,
rx: mpsc::Receiver<Event<KeyEvent>>, rx: mpsc::Receiver<Event<KeyEvent>>,
) -> Result<(), anyhow::Error> ) -> Result<(), anyhow::Error>
@ -101,7 +226,7 @@ where
T: tui::backend::Backend, T: tui::backend::Backend,
{ {
loop { loop {
render(terminal)?; render(&app_state, terminal)?;
let event = rx.recv()?; let event = rx.recv()?;
@ -111,6 +236,18 @@ where
{ {
break; 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> { fn main() -> Result<(), anyhow::Error> {
let args = env::args().collect::<Vec<String>>();
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 (tx, rx) = mpsc::channel();
let tick_rate = Duration::from_millis(TICK_RATE_MS); 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 mut terminal = Terminal::new(backend)?;
let _ = terminal.clear()?; let _ = terminal.clear()?;
let result = app_loop(&mut terminal, rx); let result = app_loop(app_state, &mut terminal, rx);
disable_raw_mode()?; disable_raw_mode()?;
terminal.show_cursor()?; terminal.show_cursor()?;