wip: cell editing

This commit is contained in:
Jeremy Wall 2024-11-02 21:42:17 -04:00
parent a5177bda18
commit 1aa9224c15
5 changed files with 113 additions and 101 deletions

12
Cargo.lock generated
View File

@ -660,6 +660,7 @@ dependencies = [
"futures", "futures",
"ratatui", "ratatui",
"thiserror", "thiserror",
"tui-textarea",
] ]
[[package]] [[package]]
@ -772,6 +773,17 @@ dependencies = [
"syn", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"

View File

@ -13,3 +13,4 @@ csvx = "0.1.17"
futures = "0.3.31" futures = "0.3.31"
ratatui = "0.29.0" ratatui = "0.29.0"
thiserror = "1.0.65" thiserror = "1.0.65"
tui-textarea = "0.7.0"

View File

@ -11,40 +11,6 @@ use csvx;
use std::borrow::Borrow; 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<S: Into<String>>(value: S) -> CellValue {
CellValue::Text(Into::<String>::into(value))
}
pub fn formula<S: Into<String>>(value: S) -> CellValue {
CellValue::Formula(Into::<String>::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]. /// The Address in a [Tbl].
#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)] #[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)]
pub struct Address { 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<()> { pub fn move_to(&mut self, addr: Address) -> Result<()> {
let (row, col) = self.dimensions(); let (row, col) = self.dimensions();
if addr.row >= row || addr.col >= col { if addr.row >= row || addr.col >= col {
@ -96,8 +66,7 @@ impl Tbl {
Ok(()) Ok(())
} }
pub fn update_entry(&mut self, address: Address, value: CellValue) -> Result<()> { pub fn update_entry(&mut self, address: &Address, value: String) -> Result<()> {
// TODO(zaphar): At some point we'll need to store the graph of computation
let (row, col) = self.dimensions(); let (row, col) = self.dimensions();
if address.row >= row { if address.row >= row {
// then we need to add rows. // then we need to add rows.
@ -112,7 +81,7 @@ impl Tbl {
} }
Ok(self Ok(self
.csv .csv
.update(address.col, address.row, value.to_csv_value())?) .update(address.col, address.row, value.trim())?)
} }
} }

View File

@ -3,11 +3,11 @@ use super::*;
#[test] #[test]
fn test_dimensions_calculation() { fn test_dimensions_calculation() {
let mut tbl = Tbl::new(); 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()); 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()); 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()); assert_eq!((21, 11), tbl.dimensions());
} }

View File

@ -8,12 +8,13 @@ use anyhow::{Context, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::{ use ratatui::{
self, self,
layout::{Constraint, Flex}, layout::{Constraint, Flex, Layout},
style::{Color, Stylize}, style::{Color, Modifier, Style, Stylize},
text::{Line, Text}, text::{Line, Text},
widgets::{Block, Cell, Row, Table, Widget}, widgets::{Block, Cell, Row, Table, Widget},
Frame, Frame,
}; };
use tui_textarea::TextArea;
#[derive(Default, Debug, PartialEq)] #[derive(Default, Debug, PartialEq)]
pub enum Modality { pub enum Modality {
@ -30,19 +31,25 @@ pub struct AppState {
// Interaction Modalities // Interaction Modalities
// * Navigate // * Navigate
// * Edit // * Edit
pub struct Workspace { pub struct Workspace<'ws> {
name: String, name: String,
tbl: Tbl, tbl: Tbl,
state: AppState, state: AppState,
text_area: TextArea<'ws>,
dirty: bool,
} }
impl Workspace { impl<'ws> Workspace<'ws> {
pub fn new<S: Into<String>>(tbl: Tbl, name: S) -> Self { pub fn new<S: Into<String>>(tbl: Tbl, name: S) -> Self {
Self { let mut ws = Self {
tbl, tbl,
name: name.into(), name: name.into(),
state: AppState::default(), state: AppState::default(),
} text_area: reset_text_area("".to_owned()),
dirty: false,
};
ws.handle_movement_change();
ws
} }
pub fn load(path: &PathBuf) -> Result<Self> { pub fn load(path: &PathBuf) -> Result<Self> {
@ -64,13 +71,13 @@ impl Workspace {
// TODO(jwall): Add a row automatically if necessary? // 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 {
loc.row += 1; loc.row += 1;
self.tbl.move_to(loc)?; self.tbl.move_to(loc)?;
} }
Ok(()) Ok(())
} }
pub fn move_up(&mut self) -> Result<()> { pub fn move_up(&mut self) -> Result<()> {
let mut loc = self.tbl.location.clone(); let mut loc = self.tbl.location.clone();
if loc.row > 0 { if loc.row > 0 {
@ -79,7 +86,7 @@ impl Workspace {
} }
Ok(()) Ok(())
} }
pub fn move_left(&mut self) -> Result<()> { pub fn move_left(&mut self) -> Result<()> {
let mut loc = self.tbl.location.clone(); let mut loc = self.tbl.location.clone();
if loc.col > 0 { if loc.col > 0 {
@ -88,13 +95,13 @@ impl Workspace {
} }
Ok(()) Ok(())
} }
pub fn move_right(&mut self) -> Result<()> { pub fn move_right(&mut self) -> Result<()> {
// TODO(jwall): Add a column automatically if necessary? // 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 {
loc.col += 1; loc.col += 1;
self.tbl.move_to(loc)?; self.tbl.move_to(loc)?;
} }
Ok(()) Ok(())
@ -112,54 +119,53 @@ impl Workspace {
fn handle_edit_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> { fn handle_edit_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
match key.code { if let KeyCode::Esc = key.code {
KeyCode::Esc => { self.state.modality = Modality::Navigate;
self.state.modality = Modality::Navigate; self.text_area.set_cursor_line_style(Style::default());
}, self.text_area.set_cursor_style(Style::default());
KeyCode::Char('j') => { let contents = self.text_area.lines().join("\n");
self.move_down()?; if self.dirty {
}, let loc = self.tbl.location.clone();
KeyCode::Char('k') => { self.tbl.update_entry(&loc, contents)?;
self.move_up()?;
},
KeyCode::Char('h') => {
self.move_left()?;
},
KeyCode::Char('l') => {
self.move_right()?;
},
_ => {
// noop
} }
return Ok(None);
} }
} }
if self.text_area.input(key) {
self.dirty = true;
}
Ok(None) Ok(None)
} }
fn handle_navigation_event(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> { fn handle_navigation_event(&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::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') => { KeyCode::Char('e') => {
self.state.modality = Modality::CellEdit; 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 // noop
} }
@ -168,23 +174,48 @@ impl Workspace {
return Ok(None); 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) fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where where
Self: Sized, Self: Sized,
{ {
let block = Block::bordered() let outer_block = Block::bordered()
.title(Line::from(self.name.as_str())) .title(Line::from(self.name.as_str()))
.title_bottom(match &self.state.modality { .title_bottom(match &self.state.modality {
Modality::Navigate => "navigate", Modality::Navigate => "navigate",
Modality::CellEdit => "edit", Modality::CellEdit => "edit",
}) })
.title_bottom(Line::from(format!("{},{}", self.tbl.location.row, self.tbl.location.col)).right_aligned()); .title_bottom(
let table = Table::from(&self.tbl).block(block); Line::from(format!(
table.render(area, buf); "{},{}",
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 content = format!("{}", v);
let cell = Cell::new(Text::raw(content)); let cell = Cell::new(Text::raw(content));
match (value.location.row == ri, value.location.col == ci) { match (value.location.row == ri, value.location.col == ci) {
(true, true) => (true, true) => cell.fg(Color::White).underlined(),
cell _ => cell
.fg(Color::White).underlined(), .bg(if ri % 2 == 0 {
_ =>
cell.bg(if ri % 2 == 0 {
Color::Rgb(57, 61, 71) Color::Rgb(57, 61, 71)
} else { } else {
Color::Rgb(165, 169, 160) Color::Rgb(165, 169, 160)
@ -223,7 +252,8 @@ impl<'t> From<&Tbl> for Table<'t> {
} else { } else {
Color::Rgb(31, 32, 34) Color::Rgb(31, 32, 34)
}), }),
}.bold() }
.bold()
})); }));
Row::new(cells) Row::new(cells)
}) })