mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-22 04:39:48 -04:00
feat: Saving the file and some todos
This commit is contained in:
parent
01c145c6d1
commit
4ac9c4361f
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user