mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 05:19:48 -04:00
chore: cleanup, reorg, and doc comments
This commit is contained in:
parent
185bac14fc
commit
25a8160f0a
@ -35,10 +35,13 @@ impl Book {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct a new book from an xlsx file.
|
||||||
pub fn new_from_xlsx(path: &str) -> Result<Self> {
|
pub fn new_from_xlsx(path: &str) -> Result<Self> {
|
||||||
Ok(Self::new(load_from_xlsx(path, "en", "America/New_York")?))
|
Ok(Self::new(load_from_xlsx(path, "en", "America/New_York")?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluate the spreadsheet calculating formulas and style changes.
|
||||||
|
/// This can be an expensive operation.
|
||||||
pub fn evaluate(&mut self) {
|
pub fn evaluate(&mut self) {
|
||||||
self.model.evaluate();
|
self.model.evaluate();
|
||||||
}
|
}
|
||||||
@ -59,6 +62,8 @@ impl Book {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all the sheet identiers a `Vec<(String, u32)>` where the string
|
||||||
|
/// is the sheet name and the u32 is the sheet index.
|
||||||
pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> {
|
pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> {
|
||||||
self.model
|
self.model
|
||||||
.workbook
|
.workbook
|
||||||
@ -68,7 +73,7 @@ impl Book {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the currently set sheets name.
|
/// Get the current sheets name.
|
||||||
pub fn get_sheet_name(&self) -> Result<&str> {
|
pub fn get_sheet_name(&self) -> Result<&str> {
|
||||||
Ok(&self.get_sheet()?.name)
|
Ok(&self.get_sheet()?.name)
|
||||||
}
|
}
|
||||||
@ -78,23 +83,24 @@ impl Book {
|
|||||||
Ok(&self.get_sheet()?.sheet_data)
|
Ok(&self.get_sheet()?.sheet_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_to(&mut self, loc: Address) -> Result<()> {
|
/// Move to a specific sheel location in the current sheet
|
||||||
|
pub fn move_to(&mut self, Address { row, col }: &Address) -> Result<()> {
|
||||||
// FIXME(zaphar): Check that this is safe first.
|
// FIXME(zaphar): Check that this is safe first.
|
||||||
self.location.row = loc.row;
|
self.location.row = *row;
|
||||||
self.location.col = loc.col;
|
self.location.col = *col;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a cells formatted content.
|
/// Get a cells formatted content.
|
||||||
pub fn get_current_cell_rendered(&self) -> Result<String> {
|
pub fn get_current_cell_rendered(&self) -> Result<String> {
|
||||||
Ok(self.get_cell_addr_rendered(self.location.row, self.location.col)?)
|
Ok(self.get_cell_addr_rendered(&self.location)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(zaphar): Use Address here too
|
/// Get a cells rendered content for display.
|
||||||
pub fn get_cell_addr_rendered(&self, row: usize, col: usize) -> Result<String> {
|
pub fn get_cell_addr_rendered(&self, Address { row, col }: &Address) -> Result<String> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.model
|
.model
|
||||||
.get_formatted_cell_value(self.current_sheet, row as i32, col as i32)
|
.get_formatted_cell_value(self.current_sheet, *row as i32, *col as i32)
|
||||||
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
|
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,12 +137,13 @@ impl Book {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert `count` rows at a `row_idx`.
|
||||||
pub fn insert_rows(&mut self, row_idx: usize, count: usize) -> Result<()> {
|
pub fn insert_rows(&mut self, row_idx: usize, count: usize) -> Result<()> {
|
||||||
self.model
|
self.model
|
||||||
.insert_rows(self.current_sheet, row_idx as i32, count as i32)
|
.insert_rows(self.current_sheet, row_idx as i32, count as i32)
|
||||||
.map_err(|e| anyhow!("Unable to insert row(s): {}", e))?;
|
.map_err(|e| anyhow!("Unable to insert row(s): {}", e))?;
|
||||||
if self.location.row >= row_idx {
|
if self.location.row >= row_idx {
|
||||||
self.move_to(Address {
|
self.move_to(&Address {
|
||||||
row: self.location.row + count,
|
row: self.location.row + count,
|
||||||
col: self.location.col,
|
col: self.location.col,
|
||||||
})?;
|
})?;
|
||||||
@ -144,12 +151,13 @@ impl Book {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert `count` columns at a `col_idx`.
|
||||||
pub fn insert_columns(&mut self, col_idx: usize, count: usize) -> Result<()> {
|
pub fn insert_columns(&mut self, col_idx: usize, count: usize) -> Result<()> {
|
||||||
self.model
|
self.model
|
||||||
.insert_columns(self.current_sheet, col_idx as i32, count as i32)
|
.insert_columns(self.current_sheet, col_idx as i32, count as i32)
|
||||||
.map_err(|e| anyhow!("Unable to insert column(s): {}", e))?;
|
.map_err(|e| anyhow!("Unable to insert column(s): {}", e))?;
|
||||||
if self.location.col >= col_idx {
|
if self.location.col >= col_idx {
|
||||||
self.move_to(Address {
|
self.move_to(&Address {
|
||||||
row: self.location.row,
|
row: self.location.row,
|
||||||
col: self.location.col + count,
|
col: self.location.col + count,
|
||||||
})?;
|
})?;
|
||||||
@ -211,6 +219,7 @@ impl Book {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current `Worksheet`.
|
||||||
pub(crate) fn get_sheet(&self) -> Result<&Worksheet> {
|
pub(crate) fn get_sheet(&self) -> Result<&Worksheet> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.model
|
.model
|
||||||
|
@ -69,7 +69,7 @@ fn test_book_insert_rows() {
|
|||||||
let mut book = Book::default();
|
let mut book = Book::default();
|
||||||
book.update_entry(&Address { row: 2, col: 2 }, "1")
|
book.update_entry(&Address { row: 2, col: 2 }, "1")
|
||||||
.expect("failed to edit cell");
|
.expect("failed to edit cell");
|
||||||
book.move_to(Address { row: 2, col: 2 }).expect("Failed to move to location");
|
book.move_to(&Address { row: 2, col: 2 }).expect("Failed to move to location");
|
||||||
assert_eq!((2, 2), book.get_size().expect("Failed to get size"));
|
assert_eq!((2, 2), book.get_size().expect("Failed to get size"));
|
||||||
book.insert_rows(1, 5).expect("Failed to insert rows");
|
book.insert_rows(1, 5).expect("Failed to insert rows");
|
||||||
assert_eq!((7, 2), book.get_size().expect("Failed to get size"));
|
assert_eq!((7, 2), book.get_size().expect("Failed to get size"));
|
||||||
@ -82,7 +82,7 @@ fn test_book_insert_columns() {
|
|||||||
let mut book = Book::default();
|
let mut book = Book::default();
|
||||||
book.update_entry(&Address { row: 2, col: 2 }, "1")
|
book.update_entry(&Address { row: 2, col: 2 }, "1")
|
||||||
.expect("failed to edit cell");
|
.expect("failed to edit cell");
|
||||||
book.move_to(Address { row: 2, col: 2 }).expect("Failed to move to location");
|
book.move_to(&Address { row: 2, col: 2 }).expect("Failed to move to location");
|
||||||
assert_eq!((2, 2), book.get_size().expect("Failed to get size"));
|
assert_eq!((2, 2), book.get_size().expect("Failed to get size"));
|
||||||
book.insert_columns(1, 5).expect("Failed to insert rows");
|
book.insert_columns(1, 5).expect("Failed to insert rows");
|
||||||
assert_eq!((2, 7), book.get_size().expect("Failed to get size"));
|
assert_eq!((2, 7), book.get_size().expect("Failed to get size"));
|
||||||
|
@ -22,7 +22,7 @@ fn run(terminal: &mut ratatui::DefaultTerminal, args: Args) -> anyhow::Result<Ex
|
|||||||
let mut ws = Workspace::load(&args.workbook, &args.locale_name, &args.timezone_name)?;
|
let mut ws = Workspace::load(&args.workbook, &args.locale_name, &args.timezone_name)?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| ui::draw(frame, &mut ws))?;
|
terminal.draw(|frame| ui::render::draw(frame, &mut ws))?;
|
||||||
if let Some(code) = ws.handle_input()? {
|
if let Some(code) = ws.handle_input()? {
|
||||||
return Ok(code);
|
return Ok(code);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
//! Command mode command parsers.
|
//! Command mode command parsers.
|
||||||
use slice_utils::{Measured, Peekable, Seekable, Span, StrCursor};
|
use slice_utils::{Measured, Peekable, Seekable, Span, StrCursor};
|
||||||
|
|
||||||
|
/// A parsed command entered in during command mode.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Cmd<'a> {
|
pub enum Cmd<'a> {
|
||||||
Write(Option<&'a str>),
|
Write(Option<&'a str>),
|
||||||
@ -11,6 +12,7 @@ pub enum Cmd<'a> {
|
|||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse command text into a `Cmd`.
|
||||||
pub fn parse<'cmd, 'i: 'cmd>(input: &'i str) -> Result<Option<Cmd<'cmd>>, &'static str> {
|
pub fn parse<'cmd, 'i: 'cmd>(input: &'i str) -> Result<Option<Cmd<'cmd>>, &'static str> {
|
||||||
let cursor = StrCursor::new(input);
|
let cursor = StrCursor::new(input);
|
||||||
// try consume write command.
|
// try consume write command.
|
||||||
|
161
src/ui/mod.rs
161
src/ui/mod.rs
@ -1,24 +1,21 @@
|
|||||||
//! Ui rendering logic
|
//! Ui rendering logic
|
||||||
|
|
||||||
use std::{path::PathBuf, process::ExitCode};
|
use std::{path::PathBuf, process::ExitCode};
|
||||||
|
|
||||||
use crate::book::Book;
|
use crate::book::Book;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
self,
|
self,
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Constraint, Flex, Layout, Rect},
|
layout::{Constraint, Flex, Layout, Rect},
|
||||||
style::{Color, Modifier, Style, Stylize},
|
style::{Modifier, Style},
|
||||||
text::{Line, Text},
|
widgets::{Block, Table, TableState, Widget},
|
||||||
widgets::{Block, Cell, Paragraph, Row, Table, TableState, Widget},
|
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
use tui_popup::Popup;
|
|
||||||
use tui_prompts::{State, Status, TextPrompt, TextState};
|
use tui_prompts::{State, Status, TextPrompt, TextState};
|
||||||
use tui_textarea::{CursorMove, TextArea};
|
use tui_textarea::{CursorMove, TextArea};
|
||||||
|
|
||||||
|
pub mod render;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
@ -31,7 +28,6 @@ pub enum Modality {
|
|||||||
Navigate,
|
Navigate,
|
||||||
CellEdit,
|
CellEdit,
|
||||||
Command,
|
Command,
|
||||||
// TODO(zaphar): Command Mode?
|
|
||||||
Dialog,
|
Dialog,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +36,8 @@ pub struct AppState<'ws> {
|
|||||||
pub modality_stack: Vec<Modality>,
|
pub modality_stack: Vec<Modality>,
|
||||||
pub table_state: TableState,
|
pub table_state: TableState,
|
||||||
pub command_state: TextState<'ws>,
|
pub command_state: TextState<'ws>,
|
||||||
|
dirty: bool,
|
||||||
|
popup: Vec<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'ws> Default for AppState<'ws> {
|
impl<'ws> Default for AppState<'ws> {
|
||||||
@ -47,7 +45,9 @@ impl<'ws> Default for AppState<'ws> {
|
|||||||
AppState {
|
AppState {
|
||||||
modality_stack: vec![Modality::default()],
|
modality_stack: vec![Modality::default()],
|
||||||
table_state: Default::default(),
|
table_state: Default::default(),
|
||||||
command_state: Default::default()
|
command_state: Default::default(),
|
||||||
|
dirty: Default::default(),
|
||||||
|
popup: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,37 +83,34 @@ impl Default for Address {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interaction Modalities
|
/// A workspace defining our UI state.
|
||||||
// * Navigate
|
|
||||||
// * Edit
|
|
||||||
pub struct Workspace<'ws> {
|
pub struct Workspace<'ws> {
|
||||||
name: PathBuf,
|
name: PathBuf,
|
||||||
book: Book,
|
book: Book,
|
||||||
state: AppState<'ws>,
|
state: AppState<'ws>,
|
||||||
text_area: TextArea<'ws>,
|
text_area: TextArea<'ws>,
|
||||||
dirty: bool,
|
|
||||||
popup: Vec<String>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'ws> Workspace<'ws> {
|
impl<'ws> Workspace<'ws> {
|
||||||
|
/// Constructs a new Workspace from an `Book` with a path for the name.
|
||||||
pub fn new(book: Book, name: PathBuf) -> Self {
|
pub fn new(book: Book, name: PathBuf) -> Self {
|
||||||
let mut ws = Self {
|
let mut ws = Self {
|
||||||
book,
|
book,
|
||||||
name,
|
name,
|
||||||
state: AppState::default(),
|
state: AppState::default(),
|
||||||
text_area: reset_text_area("".to_owned()),
|
text_area: reset_text_area("".to_owned()),
|
||||||
dirty: false,
|
|
||||||
popup: Vec::new(),
|
|
||||||
};
|
};
|
||||||
ws.handle_movement_change();
|
ws.handle_movement_change();
|
||||||
ws
|
ws
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads a workspace from a path.
|
||||||
pub fn load(path: &PathBuf, locale: &str, tz: &str) -> Result<Self> {
|
pub fn load(path: &PathBuf, locale: &str, tz: &str) -> Result<Self> {
|
||||||
let book = load_book(path, locale, tz)?;
|
let book = load_book(path, locale, tz)?;
|
||||||
Ok(Workspace::new(book, path.clone()))
|
Ok(Workspace::new(book, path.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads a new `Book` into a `Workspace` from a path.
|
||||||
pub fn load_into<P: Into<PathBuf>>(&mut self, path: P) -> Result<()> {
|
pub fn load_into<P: Into<PathBuf>>(&mut self, path: P) -> Result<()> {
|
||||||
let path: PathBuf = path.into();
|
let path: PathBuf = path.into();
|
||||||
// FIXME(zaphar): This should be managed better.
|
// FIXME(zaphar): This should be managed better.
|
||||||
@ -123,45 +120,52 @@ impl<'ws> Workspace<'ws> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move a row down in the current sheet.
|
||||||
pub fn move_down(&mut self) -> Result<()> {
|
pub fn move_down(&mut self) -> Result<()> {
|
||||||
let mut loc = self.book.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
let (row_count, _) = self.book.get_size()?;
|
let (row_count, _) = self.book.get_size()?;
|
||||||
if loc.row < row_count {
|
if loc.row < row_count {
|
||||||
loc.row += 1;
|
loc.row += 1;
|
||||||
self.book.move_to(loc)?;
|
self.book.move_to(&loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move a row up in the current sheet.
|
||||||
pub fn move_up(&mut self) -> Result<()> {
|
pub fn move_up(&mut self) -> Result<()> {
|
||||||
let mut loc = self.book.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
if loc.row > 1 {
|
if loc.row > 1 {
|
||||||
loc.row -= 1;
|
loc.row -= 1;
|
||||||
self.book.move_to(loc)?;
|
self.book.move_to(&loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move a column to the left in the current sheet.
|
||||||
pub fn move_left(&mut self) -> Result<()> {
|
pub fn move_left(&mut self) -> Result<()> {
|
||||||
let mut loc = self.book.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
if loc.col > 1 {
|
if loc.col > 1 {
|
||||||
loc.col -= 1;
|
loc.col -= 1;
|
||||||
self.book.move_to(loc)?;
|
self.book.move_to(&loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move a column to the left in the current sheet.
|
||||||
pub fn move_right(&mut self) -> Result<()> {
|
pub fn move_right(&mut self) -> Result<()> {
|
||||||
let mut loc = self.book.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
let (_, col_count) = self.book.get_size()?;
|
let (_, col_count) = self.book.get_size()?;
|
||||||
if loc.col < col_count {
|
if loc.col < col_count {
|
||||||
loc.col += 1;
|
loc.col += 1;
|
||||||
self.book.move_to(loc)?;
|
self.book.move_to(&loc)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle input in our ui loop.
|
||||||
pub fn handle_input(&mut self) -> Result<Option<ExitCode>> {
|
pub fn handle_input(&mut self) -> Result<Option<ExitCode>> {
|
||||||
|
// TODO(jwall): We probably want to separate this out into
|
||||||
|
// a pure function so we can script various testing scenarios.
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
let result = match self.state.modality() {
|
let result = match self.state.modality() {
|
||||||
Modality::Navigate => self.handle_navigation_input(key)?,
|
Modality::Navigate => self.handle_navigation_input(key)?,
|
||||||
@ -242,7 +246,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
// * Copy
|
// * Copy
|
||||||
// * Paste
|
// * Paste
|
||||||
if self.text_area.input(key) {
|
if self.text_area.input(key) {
|
||||||
self.dirty = true;
|
self.state.dirty = true;
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@ -321,7 +325,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
let mut loc = self.book.location.clone();
|
let mut loc = self.book.location.clone();
|
||||||
if loc.row < row as usize {
|
if loc.row < row as usize {
|
||||||
loc.row = row as usize;
|
loc.row = row as usize;
|
||||||
self.book.move_to(loc)?;
|
self.book.move_to(&loc)?;
|
||||||
}
|
}
|
||||||
self.handle_movement_change();
|
self.handle_movement_change();
|
||||||
}
|
}
|
||||||
@ -359,11 +363,6 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO(jeremy): Handle some useful navigation operations.
|
|
||||||
// * Copy Cell reference
|
|
||||||
// * Copy Cell Range reference
|
|
||||||
// * Extend Cell {down,up}
|
|
||||||
// * Goto location. (Command modality?)
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,7 +378,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn enter_dialog_mode(&mut self, msg: Vec<String>) {
|
fn enter_dialog_mode(&mut self, msg: Vec<String>) {
|
||||||
self.popup = msg;
|
self.state.popup = msg;
|
||||||
self.state.modality_stack.push(Modality::Dialog);
|
self.state.modality_stack.push(Modality::Dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,10 +410,10 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.text_area.set_cursor_line_style(Style::default());
|
self.text_area.set_cursor_line_style(Style::default());
|
||||||
self.text_area.set_cursor_style(Style::default());
|
self.text_area.set_cursor_style(Style::default());
|
||||||
let contents = self.text_area.lines().join("\n");
|
let contents = self.text_area.lines().join("\n");
|
||||||
if self.dirty {
|
if self.state.dirty {
|
||||||
self.book.edit_current_cell(contents)?;
|
self.book.edit_current_cell(contents)?;
|
||||||
self.book.evaluate();
|
self.book.evaluate();
|
||||||
self.dirty = false;
|
self.state.dirty = false;
|
||||||
}
|
}
|
||||||
self.enter_navigation_mode();
|
self.enter_navigation_mode();
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -504,105 +503,3 @@ fn reset_text_area<'a>(content: String) -> TextArea<'a> {
|
|||||||
text_area.set_block(Block::bordered());
|
text_area.set_block(Block::bordered());
|
||||||
text_area
|
text_area
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
|
||||||
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer)
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
let outer_block = Block::bordered()
|
|
||||||
.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",
|
|
||||||
Modality::Command => "command",
|
|
||||||
Modality::Dialog => "",
|
|
||||||
})
|
|
||||||
.title_bottom(
|
|
||||||
Line::from(format!(
|
|
||||||
"{},{}",
|
|
||||||
self.book.location.row, self.book.location.col
|
|
||||||
))
|
|
||||||
.right_aligned(),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (rect, f) in self.get_render_parts(area.clone()) {
|
|
||||||
f(rect, buf, self);
|
|
||||||
}
|
|
||||||
|
|
||||||
outer_block.render(area, buf);
|
|
||||||
|
|
||||||
if self.state.modality() == &Modality::Dialog {
|
|
||||||
let lines = Text::from_iter(self.popup.iter().cloned());
|
|
||||||
let popup = Popup::new(lines);
|
|
||||||
popup.render(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const COLNAMES: [&'static str; 26] = [
|
|
||||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
|
|
||||||
"T", "U", "V", "W", "X", "Y", "Z",
|
|
||||||
];
|
|
||||||
|
|
||||||
impl<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> {
|
|
||||||
fn try_from(value: &'book Book) -> std::result::Result<Self, Self::Error> {
|
|
||||||
// TODO(zaphar): This is apparently expensive. Maybe we can cache it somehow?
|
|
||||||
// We should do the correct thing here if this fails
|
|
||||||
let (row_count, col_count) = value.get_size()?;
|
|
||||||
let rows: Vec<Row> = (1..=row_count)
|
|
||||||
.into_iter()
|
|
||||||
.map(|ri| {
|
|
||||||
let mut cells = vec![Cell::new(Text::from(ri.to_string()))];
|
|
||||||
cells.extend((1..=col_count).into_iter().map(|ci| {
|
|
||||||
// TODO(zaphar): Is this safe?
|
|
||||||
let content = value.get_cell_addr_rendered(ri, ci).unwrap();
|
|
||||||
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 {
|
|
||||||
Color::Rgb(57, 61, 71)
|
|
||||||
} else {
|
|
||||||
Color::Rgb(165, 169, 160)
|
|
||||||
})
|
|
||||||
.fg(if ri % 2 == 0 {
|
|
||||||
Color::White
|
|
||||||
} else {
|
|
||||||
Color::Rgb(31, 32, 34)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
.bold()
|
|
||||||
}));
|
|
||||||
Row::new(cells)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let mut constraints: Vec<Constraint> = Vec::new();
|
|
||||||
constraints.push(Constraint::Max(5));
|
|
||||||
for _ in 0..col_count {
|
|
||||||
constraints.push(Constraint::Min(5));
|
|
||||||
}
|
|
||||||
let mut header = Vec::with_capacity(col_count as usize);
|
|
||||||
header.push(Cell::new(""));
|
|
||||||
header.extend((0..(col_count as usize)).map(|i| {
|
|
||||||
let count = (i / 26) + 1;
|
|
||||||
Cell::new(COLNAMES[i % 26].repeat(count))
|
|
||||||
}));
|
|
||||||
Ok(Table::new(rows, constraints)
|
|
||||||
.block(Block::bordered())
|
|
||||||
.header(Row::new(header).underlined())
|
|
||||||
.column_spacing(1)
|
|
||||||
.flex(Flex::SpaceAround))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw(frame: &mut Frame, ws: &mut Workspace) {
|
|
||||||
frame.render_widget(ws, frame.area());
|
|
||||||
}
|
|
||||||
|
113
src/ui/render.rs
Normal file
113
src/ui/render.rs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
use ratatui::{
|
||||||
|
self,
|
||||||
|
layout::{Constraint, Flex, Rect},
|
||||||
|
style::{Color, Stylize},
|
||||||
|
text::{Line, Text},
|
||||||
|
widgets::{Block, Cell, Row, Table, Widget},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use tui_popup::Popup;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
||||||
|
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let outer_block = Block::bordered()
|
||||||
|
.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",
|
||||||
|
Modality::Command => "command",
|
||||||
|
Modality::Dialog => "",
|
||||||
|
})
|
||||||
|
.title_bottom(
|
||||||
|
Line::from(format!(
|
||||||
|
"{},{}",
|
||||||
|
self.book.location.row, self.book.location.col
|
||||||
|
))
|
||||||
|
.right_aligned(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (rect, f) in self.get_render_parts(area.clone()) {
|
||||||
|
f(rect, buf, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
outer_block.render(area, buf);
|
||||||
|
|
||||||
|
if self.state.modality() == &Modality::Dialog {
|
||||||
|
let lines = Text::from_iter(self.state.popup.iter().cloned());
|
||||||
|
let popup = Popup::new(lines);
|
||||||
|
popup.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLNAMES: [&'static str; 26] = [
|
||||||
|
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
|
||||||
|
"T", "U", "V", "W", "X", "Y", "Z",
|
||||||
|
];
|
||||||
|
|
||||||
|
impl<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> {
|
||||||
|
fn try_from(value: &'book Book) -> std::result::Result<Self, Self::Error> {
|
||||||
|
// TODO(zaphar): This is apparently expensive. Maybe we can cache it somehow?
|
||||||
|
// We should do the correct thing here if this fails
|
||||||
|
let (row_count, col_count) = value.get_size()?;
|
||||||
|
let rows: Vec<Row> = (1..=row_count)
|
||||||
|
.into_iter()
|
||||||
|
.map(|ri| {
|
||||||
|
let mut cells = vec![Cell::new(Text::from(ri.to_string()))];
|
||||||
|
cells.extend((1..=col_count).into_iter().map(|ci| {
|
||||||
|
// TODO(zaphar): Is this safe?
|
||||||
|
let content = value.get_cell_addr_rendered(&Address{ row: ri, col: ci }).unwrap();
|
||||||
|
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 {
|
||||||
|
Color::Rgb(57, 61, 71)
|
||||||
|
} else {
|
||||||
|
Color::Rgb(165, 169, 160)
|
||||||
|
})
|
||||||
|
.fg(if ri % 2 == 0 {
|
||||||
|
Color::White
|
||||||
|
} else {
|
||||||
|
Color::Rgb(31, 32, 34)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.bold()
|
||||||
|
}));
|
||||||
|
Row::new(cells)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut constraints: Vec<Constraint> = Vec::new();
|
||||||
|
constraints.push(Constraint::Max(5));
|
||||||
|
for _ in 0..col_count {
|
||||||
|
constraints.push(Constraint::Min(5));
|
||||||
|
}
|
||||||
|
let mut header = Vec::with_capacity(col_count as usize);
|
||||||
|
header.push(Cell::new(""));
|
||||||
|
header.extend((0..(col_count as usize)).map(|i| {
|
||||||
|
let count = (i / 26) + 1;
|
||||||
|
Cell::new(COLNAMES[i % 26].repeat(count))
|
||||||
|
}));
|
||||||
|
Ok(Table::new(rows, constraints)
|
||||||
|
.block(Block::bordered())
|
||||||
|
.header(Row::new(header).underlined())
|
||||||
|
.column_spacing(1)
|
||||||
|
.flex(Flex::SpaceAround))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(frame: &mut Frame, ws: &mut Workspace) {
|
||||||
|
frame.render_widget(ws, frame.area());
|
||||||
|
}
|
@ -44,6 +44,7 @@ fn test_insert_rows_cmd_short() {
|
|||||||
assert_eq!(cmd, Cmd::InsertRow(1));
|
assert_eq!(cmd, Cmd::InsertRow(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
fn test_insert_cols_cmd() {
|
fn test_insert_cols_cmd() {
|
||||||
let input = "insert-cols 1";
|
let input = "insert-cols 1";
|
||||||
let result = parse(input);
|
let result = parse(input);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user