mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-22 21:09: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)?;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 input = if path.exists() {
|
||||||
|
if path.is_file() {
|
||||||
let mut f = File::open(path)?;
|
let mut f = File::open(path)?;
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let _ = f.read_to_end(&mut buf)?;
|
let _ = f.read_to_end(&mut buf)?;
|
||||||
let input = String::from_utf8(buf).context(format!("Error reading file: {:?}", path))?;
|
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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user