diff --git a/Cargo.lock b/Cargo.lock index f7c1163..8134152 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -660,6 +660,7 @@ dependencies = [ "futures", "ratatui", "thiserror", + "tui-textarea", ] [[package]] @@ -772,6 +773,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tui-textarea" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" +dependencies = [ + "crossterm", + "ratatui", + "unicode-width 0.2.0", +] + [[package]] name = "unicode-ident" version = "1.0.13" diff --git a/Cargo.toml b/Cargo.toml index f98afe6..51c46c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ csvx = "0.1.17" futures = "0.3.31" ratatui = "0.29.0" thiserror = "1.0.65" +tui-textarea = "0.7.0" diff --git a/src/sheet/mod.rs b/src/sheet/mod.rs index 68136fe..553631a 100644 --- a/src/sheet/mod.rs +++ b/src/sheet/mod.rs @@ -11,40 +11,6 @@ use csvx; use std::borrow::Borrow; -pub enum CellValue { - Text(String), - Float(f64), - Integer(i64), - Formula(String), -} - -impl CellValue { - pub fn to_csv_value(&self) -> String { - match self { - CellValue::Text(v) => format!("\"{}\"", v), - CellValue::Float(v) => format!("{}", v), - CellValue::Integer(v) => format!("{}", v), - CellValue::Formula(v) => format!("{}", v), - } - } - - pub fn text>(value: S) -> CellValue { - CellValue::Text(Into::::into(value)) - } - - pub fn formula>(value: S) -> CellValue { - CellValue::Formula(Into::::into(value)) - } - - pub fn float(value: f64) -> CellValue { - CellValue::Float(value) - } - - pub fn int(value: i64) -> CellValue { - CellValue::Integer(value) - } -} - /// The Address in a [Tbl]. #[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)] pub struct Address { @@ -87,6 +53,10 @@ impl Tbl { }) } + pub fn get_raw_value(&self, Address {row, col}: &Address) -> String { + self.csv.get_raw_table()[*row][*col].clone() + } + pub fn move_to(&mut self, addr: Address) -> Result<()> { let (row, col) = self.dimensions(); if addr.row >= row || addr.col >= col { @@ -96,8 +66,7 @@ impl Tbl { Ok(()) } - pub fn update_entry(&mut self, address: Address, value: CellValue) -> Result<()> { - // TODO(zaphar): At some point we'll need to store the graph of computation + pub fn update_entry(&mut self, address: &Address, value: String) -> Result<()> { let (row, col) = self.dimensions(); if address.row >= row { // then we need to add rows. @@ -112,7 +81,7 @@ impl Tbl { } Ok(self .csv - .update(address.col, address.row, value.to_csv_value())?) + .update(address.col, address.row, value.trim())?) } } diff --git a/src/sheet/tests.rs b/src/sheet/tests.rs index adc7b21..f430d53 100644 --- a/src/sheet/tests.rs +++ b/src/sheet/tests.rs @@ -3,11 +3,11 @@ use super::*; #[test] fn test_dimensions_calculation() { let mut tbl = Tbl::new(); - tbl.update_entry(Address::new(0, 0), CellValue::Text(String::new())).unwrap(); + tbl.update_entry(&Address::new(0, 0), String::new()).unwrap(); assert_eq!((1, 1), tbl.dimensions()); - tbl.update_entry(Address::new(0, 10), CellValue::Text(String::new())).unwrap(); + tbl.update_entry(&Address::new(0, 10), String::new()).unwrap(); assert_eq!((1, 11), tbl.dimensions()); - tbl.update_entry(Address::new(20, 5), CellValue::Text(String::new())).unwrap(); + tbl.update_entry(&Address::new(20, 5), String::new()).unwrap(); assert_eq!((21, 11), tbl.dimensions()); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 47e2054..3095b31 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,12 +8,13 @@ use anyhow::{Context, Result}; use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use ratatui::{ self, - layout::{Constraint, Flex}, - style::{Color, Stylize}, + layout::{Constraint, Flex, Layout}, + style::{Color, Modifier, Style, Stylize}, text::{Line, Text}, widgets::{Block, Cell, Row, Table, Widget}, Frame, }; +use tui_textarea::TextArea; #[derive(Default, Debug, PartialEq)] pub enum Modality { @@ -30,19 +31,25 @@ pub struct AppState { // Interaction Modalities // * Navigate // * Edit -pub struct Workspace { +pub struct Workspace<'ws> { name: String, tbl: Tbl, state: AppState, + text_area: TextArea<'ws>, + dirty: bool, } -impl Workspace { +impl<'ws> Workspace<'ws> { pub fn new>(tbl: Tbl, name: S) -> Self { - Self { + let mut ws = Self { tbl, name: name.into(), state: AppState::default(), - } + text_area: reset_text_area("".to_owned()), + dirty: false, + }; + ws.handle_movement_change(); + ws } pub fn load(path: &PathBuf) -> Result { @@ -64,13 +71,13 @@ impl Workspace { // 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 { + if loc.row < row - 1 { loc.row += 1; self.tbl.move_to(loc)?; } Ok(()) } - + pub fn move_up(&mut self) -> Result<()> { let mut loc = self.tbl.location.clone(); if loc.row > 0 { @@ -79,7 +86,7 @@ impl Workspace { } Ok(()) } - + pub fn move_left(&mut self) -> Result<()> { let mut loc = self.tbl.location.clone(); if loc.col > 0 { @@ -88,13 +95,13 @@ impl Workspace { } Ok(()) } - + 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 { - loc.col += 1; + if loc.col < col - 1 { + loc.col += 1; self.tbl.move_to(loc)?; } Ok(()) @@ -112,54 +119,53 @@ impl Workspace { fn handle_edit_event(&mut self, key: event::KeyEvent) -> Result> { if key.kind == KeyEventKind::Press { - match key.code { - KeyCode::Esc => { - self.state.modality = Modality::Navigate; - }, - KeyCode::Char('j') => { - self.move_down()?; - }, - KeyCode::Char('k') => { - self.move_up()?; - }, - KeyCode::Char('h') => { - self.move_left()?; - }, - KeyCode::Char('l') => { - self.move_right()?; - }, - _ => { - // noop + if let KeyCode::Esc = key.code { + self.state.modality = Modality::Navigate; + self.text_area.set_cursor_line_style(Style::default()); + self.text_area.set_cursor_style(Style::default()); + let contents = self.text_area.lines().join("\n"); + if self.dirty { + let loc = self.tbl.location.clone(); + self.tbl.update_entry(&loc, contents)?; } + return Ok(None); } } + if self.text_area.input(key) { + self.dirty = true; + } Ok(None) } fn handle_navigation_event(&mut self, key: event::KeyEvent) -> Result> { if key.kind == KeyEventKind::Press { match key.code { - KeyCode::Esc => { - self.state.modality = Modality::Navigate; - }, - KeyCode::Char('q') => { - return Ok(Some(ExitCode::SUCCESS)); - }, - KeyCode::Char('j') => { - self.move_down()?; - }, - KeyCode::Char('k') => { - self.move_up()?; - }, - KeyCode::Char('h') => { - self.move_left()?; - }, - KeyCode::Char('l') => { - self.move_right()?; - }, KeyCode::Char('e') => { self.state.modality = Modality::CellEdit; - }, + self.text_area + .set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED)); + self.text_area + .set_cursor_style(Style::default().add_modifier(Modifier::SLOW_BLINK)); + } + KeyCode::Char('q') => { + return Ok(Some(ExitCode::SUCCESS)); + } + KeyCode::Char('j') => { + self.move_down()?; + self.handle_movement_change(); + } + KeyCode::Char('k') => { + self.move_up()?; + self.handle_movement_change(); + } + KeyCode::Char('h') => { + self.move_left()?; + self.handle_movement_change(); + } + KeyCode::Char('l') => { + self.move_right()?; + self.handle_movement_change(); + } _ => { // noop } @@ -168,23 +174,48 @@ impl Workspace { return Ok(None); } - // navigation methods left, right, up, down + fn handle_movement_change(&mut self) { + let contents = self.tbl.get_raw_value(&self.tbl.location); + self.text_area = reset_text_area(contents); + } } -impl<'a> Widget for &'a Workspace { +fn reset_text_area<'a>(content: String) -> TextArea<'a> { + let mut text_area = TextArea::from(content.lines()); + text_area.set_cursor_line_style(Style::default()); + text_area.set_cursor_style(Style::default()); + text_area +} + +impl<'widget, 'ws: 'widget> Widget for &'widget Workspace<'ws> { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) where Self: Sized, { - let block = Block::bordered() + let outer_block = Block::bordered() .title(Line::from(self.name.as_str())) .title_bottom(match &self.state.modality { Modality::Navigate => "navigate", Modality::CellEdit => "edit", }) - .title_bottom(Line::from(format!("{},{}", self.tbl.location.row, self.tbl.location.col)).right_aligned()); - let table = Table::from(&self.tbl).block(block); - table.render(area, buf); + .title_bottom( + Line::from(format!( + "{},{}", + self.tbl.location.row, self.tbl.location.col + )) + .right_aligned(), + ); + let [edit_rect, table_rect] = + Layout::vertical(&[Constraint::Fill(1), Constraint::Fill(20)]) + .vertical_margin(2) + .horizontal_margin(2) + .flex(Flex::Legacy) + .areas(area.clone()); + outer_block.render(area, buf); + self.text_area.render(edit_rect, buf); + let table_block = Block::bordered(); + let table = Table::from(&self.tbl).block(table_block); + table.render(table_rect, buf); } } @@ -209,11 +240,9 @@ impl<'t> From<&Tbl> for Table<'t> { let content = format!("{}", v); let cell = Cell::new(Text::raw(content)); match (value.location.row == ri, value.location.col == ci) { - (true, true) => - cell - .fg(Color::White).underlined(), - _ => - cell.bg(if ri % 2 == 0 { + (true, true) => cell.fg(Color::White).underlined(), + _ => cell + .bg(if ri % 2 == 0 { Color::Rgb(57, 61, 71) } else { Color::Rgb(165, 169, 160) @@ -223,7 +252,8 @@ impl<'t> From<&Tbl> for Table<'t> { } else { Color::Rgb(31, 32, 34) }), - }.bold() + } + .bold() })); Row::new(cells) })