diff --git a/src/main.rs b/src/main.rs index 4f42ec2..421a33d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ fn run(terminal: &mut ratatui::DefaultTerminal, name: PathBuf) -> anyhow::Result let mut ws = Workspace::load(&name)?; loop { terminal.draw(|frame| ui::draw(frame, &mut ws))?; - if let Some(code) = ws.handle_event()? { + if let Some(code) = ws.handle_input()? { return Ok(code); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7db35ea..e04463d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,11 +1,17 @@ //! Ui rendering logic -use std::{fs::File, io::Read, path::PathBuf, process::ExitCode}; +use std::{ + fs::File, + io::Read, + path::PathBuf, + process::ExitCode, + time::{Duration, Instant}, +}; use super::sheet::{Address, Tbl}; -use anyhow::{Context, Result}; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use anyhow::{anyhow, Context, Result}; +use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use ratatui::{ self, layout::{Constraint, Flex, Layout}, @@ -14,13 +20,16 @@ use ratatui::{ widgets::{Block, Cell, Row, Table, Widget}, Frame, }; -use tui_textarea::TextArea; +use tui_textarea::{CursorMove, TextArea}; + +const DEFAULT_KEY_TIMEOUT: Duration = Duration::from_millis(30); #[derive(Default, Debug, PartialEq)] pub enum Modality { #[default] Navigate, CellEdit, + // TODO(zaphar): Command Mode? } #[derive(Default, Debug)] @@ -32,7 +41,7 @@ pub struct AppState { // * Navigate // * Edit pub struct Workspace<'ws> { - name: String, + name: PathBuf, tbl: Tbl, state: AppState, text_area: TextArea<'ws>, @@ -40,10 +49,10 @@ pub struct Workspace<'ws> { } impl<'ws> Workspace<'ws> { - pub fn new>(tbl: Tbl, name: S) -> Self { + pub fn new(tbl: Tbl, name: PathBuf) -> Self { let mut ws = Self { tbl, - name: name.into(), + name, state: AppState::default(), text_area: reset_text_area("".to_owned()), dirty: false, @@ -53,22 +62,27 @@ impl<'ws> Workspace<'ws> { } pub fn load(path: &PathBuf) -> Result { - let mut f = File::open(path)?; - let mut buf = Vec::new(); - let _ = f.read_to_end(&mut buf)?; - let input = String::from_utf8(buf).context(format!("Error reading file: {:?}", path))?; + let input = if path.exists() { + if path.is_file() { + let mut f = File::open(path)?; + let mut buf = Vec::new(); + let _ = f.read_to_end(&mut buf)?; + String::from_utf8(buf).context(format!("Error reading file: {:?}", path))? + } else { + return Err(anyhow!("Not a valid path: {}", path.to_string_lossy().to_string())); + } + } else { + String::from(",,,\n,,,\n") + }; let mut tbl = Tbl::from_str(input)?; tbl.move_to(Address { row: 0, col: 0 })?; Ok(Workspace::new( tbl, - path.file_name() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|| "Unknown".to_string()), + path.clone(), )) } pub fn move_down(&mut self) -> Result<()> { - // TODO(jwall): Add a row automatically if necessary? let mut loc = self.tbl.location.clone(); let (row, _) = self.tbl.dimensions(); if loc.row < row - 1 { @@ -97,7 +111,6 @@ impl<'ws> Workspace<'ws> { } pub fn move_right(&mut self) -> Result<()> { - // TODO(jwall): Add a column automatically if necessary? let mut loc = self.tbl.location.clone(); let (_, col) = self.tbl.dimensions(); if loc.col < col - 1 { @@ -107,17 +120,18 @@ impl<'ws> Workspace<'ws> { Ok(()) } - pub fn handle_event(&mut self) -> Result> { + pub fn handle_input(&mut self) -> Result> { if let Event::Key(key) = event::read()? { - return Ok(match self.state.modality { - Modality::Navigate => self.handle_navigation_event(key)?, - Modality::CellEdit => self.handle_edit_event(key)?, - }); + let result = match self.state.modality { + Modality::Navigate => self.handle_navigation_input(key)?, + Modality::CellEdit => self.handle_edit_input(key)?, + }; + return Ok(result); } Ok(None) } - fn handle_edit_event(&mut self, key: event::KeyEvent) -> Result> { + fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result> { if key.kind == KeyEventKind::Press { if let KeyCode::Esc = key.code { self.state.modality = Modality::Navigate; @@ -131,13 +145,17 @@ impl<'ws> Workspace<'ws> { return Ok(None); } } + // TODO(zaphar): Some specialized editing keybinds + // * Select All + // * Copy + // * Paste if self.text_area.input(key) { self.dirty = true; } Ok(None) } - fn handle_navigation_event(&mut self, key: event::KeyEvent) -> Result> { + fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result> { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Char('e') => { @@ -149,6 +167,9 @@ impl<'ws> Workspace<'ws> { self.text_area.move_cursor(CursorMove::Bottom); self.text_area.move_cursor(CursorMove::End); } + KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => { + self.save_file()?; + } KeyCode::Char('q') => { return Ok(Some(ExitCode::SUCCESS)); } @@ -173,6 +194,12 @@ impl<'ws> Workspace<'ws> { } } } + // TODO(jeremy): Handle some useful navigation operations. + // * Copy Cell reference + // * Copy Cell Range reference + // * Extend Cell {down,up} + // * Insert row + // * Insert column return Ok(None); } @@ -180,6 +207,12 @@ impl<'ws> Workspace<'ws> { let contents = self.tbl.get_raw_value(&self.tbl.location); self.text_area = reset_text_area(contents); } + + fn save_file(&self) -> Result<()> { + let contents = self.tbl.csv.export_raw_table().map_err(|e| anyhow::anyhow!("Error serializing to csv: {:?}", e))?; + std::fs::write(&self.name, contents)?; + Ok(()) + } } fn reset_text_area<'a>(content: String) -> TextArea<'a> { @@ -195,7 +228,9 @@ impl<'widget, 'ws: 'widget> Widget for &'widget Workspace<'ws> { Self: Sized, { let outer_block = Block::bordered() - .title(Line::from(self.name.as_str())) + .title(Line::from(self.name + .file_name().map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| String::from("Unknown")))) .title_bottom(match &self.state.modality { Modality::Navigate => "navigate", Modality::CellEdit => "edit",