feat: Saving the file and some todos

This commit is contained in:
Jeremy Wall 2024-11-03 14:08:46 -05:00
parent 01c145c6d1
commit 4ac9c4361f
2 changed files with 60 additions and 25 deletions

View File

@ -18,7 +18,7 @@ fn run(terminal: &mut ratatui::DefaultTerminal, name: PathBuf) -> anyhow::Result
let mut ws = Workspace::load(&name)?; let mut ws = Workspace::load(&name)?;
loop { loop {
terminal.draw(|frame| ui::draw(frame, &mut ws))?; 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); return Ok(code);
} }
} }

View File

@ -1,11 +1,17 @@
//! Ui rendering logic //! 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 super::sheet::{Address, Tbl};
use anyhow::{Context, Result}; use anyhow::{anyhow, Context, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use ratatui::{ use ratatui::{
self, self,
layout::{Constraint, Flex, Layout}, layout::{Constraint, Flex, Layout},
@ -14,13 +20,16 @@ use ratatui::{
widgets::{Block, Cell, Row, Table, Widget}, widgets::{Block, Cell, Row, Table, Widget},
Frame, Frame,
}; };
use tui_textarea::TextArea; use tui_textarea::{CursorMove, TextArea};
const DEFAULT_KEY_TIMEOUT: Duration = Duration::from_millis(30);
#[derive(Default, Debug, PartialEq)] #[derive(Default, Debug, PartialEq)]
pub enum Modality { pub enum Modality {
#[default] #[default]
Navigate, Navigate,
CellEdit, CellEdit,
// TODO(zaphar): Command Mode?
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
@ -32,7 +41,7 @@ pub struct AppState {
// * Navigate // * Navigate
// * Edit // * Edit
pub struct Workspace<'ws> { pub struct Workspace<'ws> {
name: String, name: PathBuf,
tbl: Tbl, tbl: Tbl,
state: AppState, state: AppState,
text_area: TextArea<'ws>, text_area: TextArea<'ws>,
@ -40,10 +49,10 @@ pub struct Workspace<'ws> {
} }
impl<'ws> Workspace<'ws> { impl<'ws> Workspace<'ws> {
pub fn new<S: Into<String>>(tbl: Tbl, name: S) -> Self { pub fn new(tbl: Tbl, name: PathBuf) -> Self {
let mut ws = Self { let mut ws = Self {
tbl, tbl,
name: name.into(), name,
state: AppState::default(), state: AppState::default(),
text_area: reset_text_area("".to_owned()), text_area: reset_text_area("".to_owned()),
dirty: false, dirty: false,
@ -53,22 +62,27 @@ impl<'ws> Workspace<'ws> {
} }
pub fn load(path: &PathBuf) -> Result<Self> { pub fn load(path: &PathBuf) -> Result<Self> {
let mut f = File::open(path)?; let input = if path.exists() {
let mut buf = Vec::new(); if path.is_file() {
let _ = f.read_to_end(&mut buf)?; let mut f = File::open(path)?;
let input = String::from_utf8(buf).context(format!("Error reading file: {:?}", 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)?; let mut tbl = Tbl::from_str(input)?;
tbl.move_to(Address { row: 0, col: 0 })?; tbl.move_to(Address { row: 0, col: 0 })?;
Ok(Workspace::new( Ok(Workspace::new(
tbl, tbl,
path.file_name() path.clone(),
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "Unknown".to_string()),
)) ))
} }
pub fn move_down(&mut self) -> Result<()> { pub fn move_down(&mut self) -> Result<()> {
// TODO(jwall): Add a row automatically if necessary?
let mut loc = self.tbl.location.clone(); let mut loc = self.tbl.location.clone();
let (row, _) = self.tbl.dimensions(); let (row, _) = self.tbl.dimensions();
if loc.row < row - 1 { if loc.row < row - 1 {
@ -97,7 +111,6 @@ impl<'ws> Workspace<'ws> {
} }
pub fn move_right(&mut self) -> Result<()> { pub fn move_right(&mut self) -> Result<()> {
// TODO(jwall): Add a column automatically if necessary?
let mut loc = self.tbl.location.clone(); let mut loc = self.tbl.location.clone();
let (_, col) = self.tbl.dimensions(); let (_, col) = self.tbl.dimensions();
if loc.col < col - 1 { if loc.col < col - 1 {
@ -107,17 +120,18 @@ impl<'ws> Workspace<'ws> {
Ok(()) Ok(())
} }
pub fn handle_event(&mut self) -> Result<Option<ExitCode>> { pub fn handle_input(&mut self) -> Result<Option<ExitCode>> {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
return Ok(match self.state.modality { let result = match self.state.modality {
Modality::Navigate => self.handle_navigation_event(key)?, Modality::Navigate => self.handle_navigation_input(key)?,
Modality::CellEdit => self.handle_edit_event(key)?, Modality::CellEdit => self.handle_edit_input(key)?,
}); };
return Ok(result);
} }
Ok(None) Ok(None)
} }
fn handle_edit_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> { fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
if let KeyCode::Esc = key.code { if let KeyCode::Esc = key.code {
self.state.modality = Modality::Navigate; self.state.modality = Modality::Navigate;
@ -131,13 +145,17 @@ impl<'ws> Workspace<'ws> {
return Ok(None); return Ok(None);
} }
} }
// TODO(zaphar): Some specialized editing keybinds
// * Select All
// * Copy
// * Paste
if self.text_area.input(key) { if self.text_area.input(key) {
self.dirty = true; self.dirty = true;
} }
Ok(None) Ok(None)
} }
fn handle_navigation_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> { fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
match key.code { match key.code {
KeyCode::Char('e') => { KeyCode::Char('e') => {
@ -149,6 +167,9 @@ impl<'ws> Workspace<'ws> {
self.text_area.move_cursor(CursorMove::Bottom); self.text_area.move_cursor(CursorMove::Bottom);
self.text_area.move_cursor(CursorMove::End); self.text_area.move_cursor(CursorMove::End);
} }
KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => {
self.save_file()?;
}
KeyCode::Char('q') => { KeyCode::Char('q') => {
return Ok(Some(ExitCode::SUCCESS)); 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); return Ok(None);
} }
@ -180,6 +207,12 @@ impl<'ws> Workspace<'ws> {
let contents = self.tbl.get_raw_value(&self.tbl.location); let contents = self.tbl.get_raw_value(&self.tbl.location);
self.text_area = reset_text_area(contents); 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> { fn reset_text_area<'a>(content: String) -> TextArea<'a> {
@ -195,7 +228,9 @@ impl<'widget, 'ws: 'widget> Widget for &'widget Workspace<'ws> {
Self: Sized, Self: Sized,
{ {
let outer_block = Block::bordered() 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 { .title_bottom(match &self.state.modality {
Modality::Navigate => "navigate", Modality::Navigate => "navigate",
Modality::CellEdit => "edit", Modality::CellEdit => "edit",