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)?;
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);
}
}

View File

@ -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<S: Into<String>>(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<Self> {
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<Option<ExitCode>> {
pub fn handle_input(&mut self) -> Result<Option<ExitCode>> {
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<Option<ExitCode>> {
fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
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<Option<ExitCode>> {
fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
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",