mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-25 14:29:49 -04:00
Compare commits
7 Commits
00ed15d75f
...
3c23142f32
Author | SHA1 | Date | |
---|---|---|---|
3c23142f32 | |||
f3f18da254 | |||
27d30302f5 | |||
3b2c65a9fa | |||
![]() |
0177ed0847 | ||
![]() |
6090042327 | ||
![]() |
539ee9d279 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
result/
|
/result
|
||||||
*.json
|
*.json
|
||||||
tarpaulin-report.*
|
tarpaulin-report.*
|
||||||
*.profraw
|
*.profraw
|
||||||
|
26
Cargo.lock
generated
26
Cargo.lock
generated
@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
@ -806,8 +806,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironcalc"
|
name = "ironcalc"
|
||||||
version = "0.3.0"
|
version = "0.5.0"
|
||||||
source = "git+https://github.com/ironcalc/IronCalc#b2c5027f56a16a0c606b01a071b816b941972aef"
|
source = "git+https://github.com/ironcalc/IronCalc#efc925a0460fa5ea915376f386f948e988f2be2f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcode",
|
"bitcode",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -822,8 +822,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironcalc_base"
|
name = "ironcalc_base"
|
||||||
version = "0.3.0"
|
version = "0.5.0"
|
||||||
source = "git+https://github.com/ironcalc/IronCalc#b2c5027f56a16a0c606b01a071b816b941972aef"
|
source = "git+https://github.com/ironcalc/IronCalc#efc925a0460fa5ea915376f386f948e988f2be2f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcode",
|
"bitcode",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1210,6 +1210,19 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"getopts",
|
||||||
|
"memchr",
|
||||||
|
"pulldown-cmark-escape",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark-escape"
|
name = "pulldown-cmark-escape"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -1500,6 +1513,7 @@ dependencies = [
|
|||||||
"csv",
|
"csv",
|
||||||
"futures",
|
"futures",
|
||||||
"ironcalc",
|
"ironcalc",
|
||||||
|
"pulldown-cmark 0.13.0",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"slice-utils",
|
"slice-utils",
|
||||||
@ -1758,7 +1772,7 @@ dependencies = [
|
|||||||
"ansi-to-tui",
|
"ansi-to-tui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark 0.12.2",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rstest",
|
"rstest",
|
||||||
"syntect",
|
"syntect",
|
||||||
|
@ -10,7 +10,7 @@ anyhow = { version = "1.0.91", features = ["backtrace"] }
|
|||||||
clap = { version = "4.5.20", features = ["derive"] }
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
crossterm = { version = "0.28.1", features = ["event-stream", "serde"] }
|
crossterm = { version = "0.28.1", features = ["event-stream", "serde"] }
|
||||||
# this revision introduces a way to get the Model back out of the UserModel
|
# this revision introduces a way to get the Model back out of the UserModel
|
||||||
ironcalc = { git = "https://github.com/ironcalc/IronCalc" }
|
ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.5.0"}
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
thiserror = "1.0.65"
|
thiserror = "1.0.65"
|
||||||
@ -21,3 +21,4 @@ serde_json = "1.0.133"
|
|||||||
colorsys = "0.6.7"
|
colorsys = "0.6.7"
|
||||||
tui-markdown = { version = "0.3.1", features = [] }
|
tui-markdown = { version = "0.3.1", features = [] }
|
||||||
csv = "1.3.1"
|
csv = "1.3.1"
|
||||||
|
pulldown-cmark = "0.13.0"
|
||||||
|
@ -11,7 +11,7 @@ nix profile install github:zaphar/sheetsui
|
|||||||
### Cargo
|
### Cargo
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install --git https://github.com/zaphar/sheetsui
|
cargo install --git https://github.com/zaphar/sheetsui --locked
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
@ -575,7 +575,7 @@ impl Book {
|
|||||||
width: usize,
|
width: usize,
|
||||||
) -> std::result::Result<(), anyhow::Error> {
|
) -> std::result::Result<(), anyhow::Error> {
|
||||||
self.model
|
self.model
|
||||||
.set_column_width(sheet, col as i32, width as f64 * COL_PIXELS)
|
.set_columns_width(sheet, col as i32, col as i32, width as f64 * COL_PIXELS)
|
||||||
.map_err(|e| anyhow!("Error setting column width: {:?}", e))?;
|
.map_err(|e| anyhow!("Error setting column width: {:?}", e))?;
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
126
src/book/test.rs
126
src/book/test.rs
@ -36,8 +36,15 @@ fn test_book_default() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_book_insert_cell_new_row() {
|
fn test_book_insert_cell_new_row() {
|
||||||
let mut book = Book::default();
|
let mut book = Book::default();
|
||||||
book.update_cell(&Address { sheet: 0, row: 2, col: 1 }, "1")
|
book.update_cell(
|
||||||
.expect("failed to edit cell");
|
&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 2,
|
||||||
|
col: 1,
|
||||||
|
},
|
||||||
|
"1",
|
||||||
|
)
|
||||||
|
.expect("failed to edit cell");
|
||||||
book.evaluate();
|
book.evaluate();
|
||||||
let WorksheetDimension {
|
let WorksheetDimension {
|
||||||
min_row,
|
min_row,
|
||||||
@ -52,8 +59,15 @@ fn test_book_insert_cell_new_row() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_book_insert_cell_new_column() {
|
fn test_book_insert_cell_new_column() {
|
||||||
let mut book = Book::default();
|
let mut book = Book::default();
|
||||||
book.update_cell(&Address { sheet: 0, row: 1, col: 2 }, "1")
|
book.update_cell(
|
||||||
.expect("failed to edit cell");
|
&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 2,
|
||||||
|
},
|
||||||
|
"1",
|
||||||
|
)
|
||||||
|
.expect("failed to edit cell");
|
||||||
let WorksheetDimension {
|
let WorksheetDimension {
|
||||||
min_row,
|
min_row,
|
||||||
max_row,
|
max_row,
|
||||||
@ -67,14 +81,32 @@ fn test_book_insert_cell_new_column() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_book_insert_rows() {
|
fn test_book_insert_rows() {
|
||||||
let mut book = Book::default();
|
let mut book = Book::default();
|
||||||
book.update_cell(&Address { sheet: 0, row: 2, col: 2 }, "1")
|
book.update_cell(
|
||||||
.expect("failed to edit cell");
|
&Address {
|
||||||
book.move_to(&Address { sheet: 0, row: 2, col: 2 })
|
sheet: 0,
|
||||||
.expect("Failed to move to location");
|
row: 2,
|
||||||
|
col: 2,
|
||||||
|
},
|
||||||
|
"1",
|
||||||
|
)
|
||||||
|
.expect("failed to edit cell");
|
||||||
|
book.move_to(&Address {
|
||||||
|
sheet: 0,
|
||||||
|
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"));
|
||||||
assert_eq!(Address { sheet: 0, row: 7, col: 2 }, book.location);
|
assert_eq!(
|
||||||
|
Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 7,
|
||||||
|
col: 2
|
||||||
|
},
|
||||||
|
book.location
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"1",
|
"1",
|
||||||
book.get_current_cell_rendered()
|
book.get_current_cell_rendered()
|
||||||
@ -85,14 +117,32 @@ fn test_book_insert_rows() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_book_insert_columns() {
|
fn test_book_insert_columns() {
|
||||||
let mut book = Book::default();
|
let mut book = Book::default();
|
||||||
book.update_cell(&Address { sheet: 0, row: 2, col: 2 }, "1")
|
book.update_cell(
|
||||||
.expect("failed to edit cell");
|
&Address {
|
||||||
book.move_to(&Address { sheet: 0, row: 2, col: 2 })
|
sheet: 0,
|
||||||
.expect("Failed to move to location");
|
row: 2,
|
||||||
|
col: 2,
|
||||||
|
},
|
||||||
|
"1",
|
||||||
|
)
|
||||||
|
.expect("failed to edit cell");
|
||||||
|
book.move_to(&Address {
|
||||||
|
sheet: 0,
|
||||||
|
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"));
|
||||||
assert_eq!(Address { sheet: 0, row: 2, col: 7 }, book.location);
|
assert_eq!(
|
||||||
|
Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 2,
|
||||||
|
col: 7
|
||||||
|
},
|
||||||
|
book.location
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"1",
|
"1",
|
||||||
book.get_current_cell_rendered()
|
book.get_current_cell_rendered()
|
||||||
@ -103,8 +153,15 @@ fn test_book_insert_columns() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_book_col_size() {
|
fn test_book_col_size() {
|
||||||
let mut book = Book::default();
|
let mut book = Book::default();
|
||||||
book.update_cell(&Address { sheet: 0, row: 2, col: 2 }, "1")
|
book.update_cell(
|
||||||
.expect("failed to edit cell");
|
&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 2,
|
||||||
|
col: 2,
|
||||||
|
},
|
||||||
|
"1",
|
||||||
|
)
|
||||||
|
.expect("failed to edit cell");
|
||||||
book.set_col_size(1, 20).expect("Failed to set column size");
|
book.set_col_size(1, 20).expect("Failed to set column size");
|
||||||
assert_eq!(20, book.get_col_size(1).expect("Failed to get column size"));
|
assert_eq!(20, book.get_col_size(1).expect("Failed to get column size"));
|
||||||
}
|
}
|
||||||
@ -112,17 +169,34 @@ fn test_book_col_size() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_book_get_exportable_rows() {
|
fn test_book_get_exportable_rows() {
|
||||||
let mut book = Book::default();
|
let mut book = Book::default();
|
||||||
book.update_cell(&Address { sheet: 0, row: 1, col: 3 }, "1-3")
|
book.update_cell(
|
||||||
.expect("failed to edit cell");
|
&Address {
|
||||||
book.update_cell(&Address { sheet: 0, row: 3, col: 6 }, "3-6")
|
sheet: 0,
|
||||||
.expect("failed to edit cell");
|
row: 1,
|
||||||
|
col: 3,
|
||||||
|
},
|
||||||
|
"1-3",
|
||||||
|
)
|
||||||
|
.expect("failed to edit cell");
|
||||||
|
book.update_cell(
|
||||||
|
&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
col: 6,
|
||||||
|
},
|
||||||
|
"3-6",
|
||||||
|
)
|
||||||
|
.expect("failed to edit cell");
|
||||||
|
|
||||||
let rows = book.get_export_rows().expect("Failed to get export rows");
|
let rows = book.get_export_rows().expect("Failed to get export rows");
|
||||||
assert_eq!(4, rows.len());
|
assert_eq!(4, rows.len());
|
||||||
assert_eq!(rows, vec![
|
assert_eq!(
|
||||||
vec!["", "" , "", "", "", "", ""],
|
rows,
|
||||||
vec!["", "" , "", "1-3", "", "", ""],
|
vec![
|
||||||
vec!["", "" , "", "", "", "", ""],
|
vec!["", "", "", "", "", "", ""],
|
||||||
vec!["", "" , "", "", "", "", "3-6"],
|
vec!["", "", "", "1-3", "", "", ""],
|
||||||
]);
|
vec!["", "", "", "", "", "", ""],
|
||||||
|
vec!["", "", "", "", "", "", "3-6"],
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
use ratatui::text::Text;
|
use crate::ui::render::markdown::Markdown;
|
||||||
use tui_markdown;
|
|
||||||
|
|
||||||
pub fn render_topic(topic: &str) -> Text<'static> {
|
pub fn to_widget(topic: &str) -> Markdown {
|
||||||
match topic {
|
match topic {
|
||||||
"navigate" => tui_markdown::from_str(include_str!("../../../docs/navigation.md")),
|
"navigate" => Markdown::from_str(include_str!("../../../docs/navigation.md")),
|
||||||
"edit" => tui_markdown::from_str(include_str!("../../../docs/edit.md")),
|
"edit" => Markdown::from_str(include_str!("../../../docs/edit.md")),
|
||||||
"command" => tui_markdown::from_str(include_str!("../../../docs/command.md")),
|
"command" => Markdown::from_str(include_str!("../../../docs/command.md")),
|
||||||
"visual" => tui_markdown::from_str(include_str!("../../../docs/visual.md")),
|
"visual" => Markdown::from_str(include_str!("../../../docs/visual.md")),
|
||||||
_ => tui_markdown::from_str(include_str!("../../../docs/intro.md")),
|
_ => Markdown::from_str(include_str!("../../../docs/intro.md")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
108
src/ui/mod.rs
108
src/ui/mod.rs
@ -7,19 +7,22 @@ use anyhow::{anyhow, Result};
|
|||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use ironcalc::base::{expressions::types::Area, Model};
|
use ironcalc::base::{expressions::types::Area, Model};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer, layout::{Constraint, Flex, Layout}, style::{Modifier, Style}, text::{Line, Text}, widgets::Block
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Flex, Layout},
|
||||||
|
style::{Modifier, Style},
|
||||||
|
widgets::Block,
|
||||||
};
|
};
|
||||||
use tui_prompts::{State, Status, TextPrompt, TextState};
|
use tui_prompts::{State, Status, TextPrompt, TextState};
|
||||||
use tui_textarea::{CursorMove, TextArea};
|
use tui_textarea::{CursorMove, TextArea};
|
||||||
|
|
||||||
mod help;
|
|
||||||
mod cmd;
|
mod cmd;
|
||||||
|
mod help;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
use cmd::Cmd;
|
use cmd::Cmd;
|
||||||
use render::viewport::ViewportState;
|
use render::{markdown::Markdown, viewport::ViewportState};
|
||||||
|
|
||||||
#[derive(Default, Debug, PartialEq, Clone)]
|
#[derive(Default, Debug, PartialEq, Clone)]
|
||||||
pub enum Modality {
|
pub enum Modality {
|
||||||
@ -80,7 +83,7 @@ pub struct AppState<'ws> {
|
|||||||
pub range_select: RangeSelection,
|
pub range_select: RangeSelection,
|
||||||
pub dialog_scroll: u16,
|
pub dialog_scroll: u16,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
popup: Text<'ws>,
|
popup: Option<Markdown>,
|
||||||
clipboard: Option<ClipboardContents>,
|
clipboard: Option<ClipboardContents>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,16 +302,16 @@ impl<'ws> Workspace<'ws> {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_help_text(&self) -> Text<'static> {
|
fn render_help_text(&self) -> Markdown {
|
||||||
// TODO(zaphar): We should be sourcing these from our actual help documentation.
|
// TODO(zaphar): We should be sourcing these from our actual help documentation.
|
||||||
// Ideally we would also render the markdown content properly.
|
// Ideally we would also render the markdown content properly.
|
||||||
// https://github.com/zaphar/sheetsui/issues/22
|
// https://github.com/zaphar/sheetsui/issues/22
|
||||||
match self.state.modality() {
|
match self.state.modality() {
|
||||||
Modality::Navigate => help::render_topic("navigate"),
|
Modality::Navigate => help::to_widget("navigate"),
|
||||||
Modality::CellEdit => help::render_topic("edit"),
|
Modality::CellEdit => help::to_widget("edit"),
|
||||||
Modality::Command => help::render_topic("command"),
|
Modality::Command => help::to_widget("command"),
|
||||||
Modality::RangeSelect => help::render_topic("visual"),
|
Modality::RangeSelect => help::to_widget("visual"),
|
||||||
_ => help::render_topic(""),
|
_ => help::to_widget(""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,16 +335,16 @@ impl<'ws> Workspace<'ws> {
|
|||||||
fn handle_quit_dialog(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
fn handle_quit_dialog(&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 | KeyCode::Char('n') | KeyCode::Char('N') => {
|
KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => {
|
||||||
self.exit_quit_mode()?;
|
self.exit_quit_mode()?;
|
||||||
return Ok(Some(ExitCode::SUCCESS))
|
return Ok(Some(ExitCode::SUCCESS));
|
||||||
},
|
}
|
||||||
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||||
// We have been asked to save the file first.
|
// We have been asked to save the file first.
|
||||||
self.save_file()?;
|
self.save_file()?;
|
||||||
self.exit_quit_mode()?;
|
self.exit_quit_mode()?;
|
||||||
return Ok(Some(ExitCode::SUCCESS));
|
return Ok(Some(ExitCode::SUCCESS));
|
||||||
},
|
}
|
||||||
_ => return Ok(None),
|
_ => return Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -361,8 +364,10 @@ impl<'ws> Workspace<'ws> {
|
|||||||
KeyCode::Char('k') | KeyCode::Up => {
|
KeyCode::Char('k') | KeyCode::Up => {
|
||||||
self.state.dialog_scroll = self.state.dialog_scroll.saturating_sub(1);
|
self.state.dialog_scroll = self.state.dialog_scroll.saturating_sub(1);
|
||||||
}
|
}
|
||||||
_ => {
|
code => {
|
||||||
// NOOP
|
if let Some(widget) = &self.state.popup {
|
||||||
|
widget.handle_input(code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,7 +419,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(Some(Cmd::Help(maybe_topic))) => {
|
Ok(Some(Cmd::Help(maybe_topic))) => {
|
||||||
self.enter_dialog_mode(help::render_topic(maybe_topic.unwrap_or("")));
|
self.enter_dialog_mode(help::to_widget(maybe_topic.unwrap_or("")));
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(Some(Cmd::Write(maybe_path))) => {
|
Ok(Some(Cmd::Write(maybe_path))) => {
|
||||||
@ -426,7 +431,8 @@ impl<'ws> Workspace<'ws> {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(Some(Cmd::ExportCsv(path))) => {
|
Ok(Some(Cmd::ExportCsv(path))) => {
|
||||||
self.book.save_sheet_to_csv(self.book.location.sheet, path)?;
|
self.book
|
||||||
|
.save_sheet_to_csv(self.book.location.sheet, path)?;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(Some(Cmd::InsertColumns(count))) => {
|
Ok(Some(Cmd::InsertColumns(count))) => {
|
||||||
@ -508,11 +514,14 @@ impl<'ws> Workspace<'ws> {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
self.enter_dialog_mode(vec![Line::from(format!("Unrecognized commmand {}", cmd_text))]);
|
self.enter_dialog_mode(Markdown::from_str(&format!(
|
||||||
|
"Unrecognized commmand {}",
|
||||||
|
cmd_text
|
||||||
|
)));
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
self.enter_dialog_mode(vec![Line::from(msg.to_owned())]);
|
self.enter_dialog_mode(Markdown::from_str(msg));
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -543,18 +552,12 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('D') => {
|
KeyCode::Char('D') => {
|
||||||
if let Some((start, end)) = self.state.range_select.get_range() {
|
if let Some((start, end)) = self.state.range_select.get_range() {
|
||||||
self.book.clear_cell_range_all(
|
self.book.clear_cell_range_all(start, end)?;
|
||||||
start,
|
|
||||||
end,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('d') => {
|
KeyCode::Char('d') => {
|
||||||
if let Some((start, end)) = self.state.range_select.get_range() {
|
if let Some((start, end)) = self.state.range_select.get_range() {
|
||||||
self.book.clear_cell_range(
|
self.book.clear_cell_range(start, end)?;
|
||||||
start,
|
|
||||||
end,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('h') => {
|
KeyCode::Char('h') => {
|
||||||
@ -775,7 +778,11 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('l') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('l') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
let Address { sheet: _, row: _, col } = &ws.book.location;
|
let Address {
|
||||||
|
sheet: _,
|
||||||
|
row: _,
|
||||||
|
col,
|
||||||
|
} = &ws.book.location;
|
||||||
ws.book
|
ws.book
|
||||||
.set_col_size(*col, ws.book.get_col_size(*col)? + 1)?;
|
.set_col_size(*col, ws.book.get_col_size(*col)? + 1)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -783,7 +790,11 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('h') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('h') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
let Address { sheet: _, row: _, col } = &ws.book.location;
|
let Address {
|
||||||
|
sheet: _,
|
||||||
|
row: _,
|
||||||
|
col,
|
||||||
|
} = &ws.book.location;
|
||||||
let curr_size = ws.book.get_col_size(*col)?;
|
let curr_size = ws.book.get_col_size(*col)?;
|
||||||
if curr_size > 1 {
|
if curr_size > 1 {
|
||||||
ws.book.set_col_size(*col, curr_size - 1)?;
|
ws.book.set_col_size(*col, curr_size - 1)?;
|
||||||
@ -874,21 +885,31 @@ impl<'ws> Workspace<'ws> {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_bool_style(&mut self, current_val: Option<bool>, path: &str, address: &Address) -> Result<(), anyhow::Error> {
|
fn toggle_bool_style(
|
||||||
|
&mut self,
|
||||||
|
current_val: Option<bool>,
|
||||||
|
path: &str,
|
||||||
|
address: &Address,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
let value = if let Some(b_val) = current_val {
|
let value = if let Some(b_val) = current_val {
|
||||||
if b_val { "false" } else { "true" }
|
if b_val {
|
||||||
|
"false"
|
||||||
|
} else {
|
||||||
|
"true"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
"true"
|
"true"
|
||||||
};
|
};
|
||||||
self.book.set_cell_style(
|
self.book.set_cell_style(
|
||||||
&[(path, value)],
|
&[(path, value)],
|
||||||
&Area {
|
&Area {
|
||||||
sheet: address.sheet,
|
sheet: address.sheet,
|
||||||
row: address.row as i32,
|
row: address.row as i32,
|
||||||
column: address.col as i32,
|
column: address.col as i32,
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
})?;
|
},
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -951,8 +972,8 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.state.command_state.focus();
|
self.state.command_state.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter_dialog_mode<T: Into<Text<'ws>>>(&mut self, msg: T) {
|
fn enter_dialog_mode(&mut self, msg: Markdown) {
|
||||||
self.state.popup = msg.into();
|
self.state.popup = Some(msg);
|
||||||
self.state.modality_stack.push(Modality::Dialog);
|
self.state.modality_stack.push(Modality::Dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -981,7 +1002,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.state.pop_modality();
|
self.state.pop_modality();
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit_command_mode(&mut self) -> Result<Option<ExitCode>> {
|
fn exit_command_mode(&mut self) -> Result<Option<ExitCode>> {
|
||||||
let cmd = self.state.command_state.value().to_owned();
|
let cmd = self.state.command_state.value().to_owned();
|
||||||
self.state.command_state.blur();
|
self.state.command_state.blur();
|
||||||
@ -1045,14 +1066,13 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.book.save_to_xlsx(path.into().as_str())?;
|
self.book.save_to_xlsx(path.into().as_str())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quit_app(&mut self) -> std::result::Result<Option<ExitCode>, anyhow::Error> {
|
fn quit_app(&mut self) -> std::result::Result<Option<ExitCode>, anyhow::Error> {
|
||||||
if self.enter_quit_mode() {
|
if self.enter_quit_mode() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
return Ok(Some(ExitCode::SUCCESS))
|
return Ok(Some(ExitCode::SUCCESS));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_book(path: &PathBuf, locale: &str, tz: &str) -> Result<Book, anyhow::Error> {
|
fn load_book(path: &PathBuf, locale: &str, tz: &str) -> Result<Book, anyhow::Error> {
|
||||||
|
@ -42,16 +42,22 @@ impl<'w> Widget for Dialog<'w> {
|
|||||||
let content_width = self.content.width();
|
let content_width = self.content.width();
|
||||||
let content_height = self.content.height();
|
let content_height = self.content.height();
|
||||||
let vertical_margin = if ((content_height as u16) + 2) <= area.height {
|
let vertical_margin = if ((content_height as u16) + 2) <= area.height {
|
||||||
area.height.saturating_sub((content_height as u16) + 2).saturating_div(2)
|
area.height
|
||||||
|
.saturating_sub((content_height as u16) + 2)
|
||||||
|
.saturating_div(2)
|
||||||
} else {
|
} else {
|
||||||
area.height - 2
|
area.height - 2
|
||||||
};
|
};
|
||||||
let horizontal_margin = area.width.saturating_sub((content_width as u16) + 2).saturating_div(2);
|
let horizontal_margin = area
|
||||||
|
.width
|
||||||
|
.saturating_sub((content_width as u16) + 2)
|
||||||
|
.saturating_div(2);
|
||||||
let [_, dialog_vertical, _] = Layout::vertical(vec![
|
let [_, dialog_vertical, _] = Layout::vertical(vec![
|
||||||
Constraint::Length(vertical_margin),
|
Constraint::Length(vertical_margin),
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
Constraint::Length(vertical_margin),
|
Constraint::Length(vertical_margin),
|
||||||
]).areas(area);
|
])
|
||||||
|
.areas(area);
|
||||||
let [_, dialog_area, _] = Layout::horizontal(vec![
|
let [_, dialog_area, _] = Layout::horizontal(vec![
|
||||||
Constraint::Length(horizontal_margin),
|
Constraint::Length(horizontal_margin),
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
|
106
src/ui/render/markdown.rs
Normal file
106
src/ui/render/markdown.rs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
use ratatui::{text::Text, widgets::Widget};
|
||||||
|
|
||||||
|
use pulldown_cmark::{Event,LinkType, Parser, Tag, TextMergeStream};
|
||||||
|
|
||||||
|
//enum State {
|
||||||
|
// Para,
|
||||||
|
// NumberList,
|
||||||
|
// BulletList,
|
||||||
|
// Heading,
|
||||||
|
// BlockQuote,
|
||||||
|
//}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Markdown {
|
||||||
|
input: String,
|
||||||
|
links: BTreeSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Markdown {
|
||||||
|
pub fn from_str(input: &str) -> Self {
|
||||||
|
let mut me = Self {
|
||||||
|
input: input.to_owned(),
|
||||||
|
links: Default::default(),
|
||||||
|
};
|
||||||
|
me.parse();
|
||||||
|
me
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(&mut self) {
|
||||||
|
let input = self.input.clone();
|
||||||
|
let iter = TextMergeStream::new(Parser::new(&input));
|
||||||
|
for event in iter {
|
||||||
|
match event {
|
||||||
|
Event::Start(tag) => {
|
||||||
|
self.start_tag(&tag);
|
||||||
|
}
|
||||||
|
_ => { /* noop */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_tag(&mut self, tag: &Tag<'_>) {
|
||||||
|
match tag {
|
||||||
|
Tag::Link {
|
||||||
|
link_type,
|
||||||
|
dest_url,
|
||||||
|
title,
|
||||||
|
id,
|
||||||
|
} => {
|
||||||
|
let dest = match link_type {
|
||||||
|
// [foo](bar)
|
||||||
|
LinkType::Inline => format!("({})", dest_url),
|
||||||
|
// [foo][bar]
|
||||||
|
LinkType::Reference => format!("[{}]", id),
|
||||||
|
// [foo]
|
||||||
|
LinkType::Shortcut => format!("[{}]", title),
|
||||||
|
// These are unsupported right now
|
||||||
|
LinkType::ReferenceUnknown => todo!(),
|
||||||
|
LinkType::Collapsed => todo!(),
|
||||||
|
LinkType::CollapsedUnknown => todo!(),
|
||||||
|
LinkType::ShortcutUnknown => todo!(),
|
||||||
|
LinkType::Autolink => todo!(),
|
||||||
|
LinkType::Email => todo!(),
|
||||||
|
LinkType::WikiLink { has_pothole: _ } => todo!(),
|
||||||
|
};
|
||||||
|
self.links.insert(dest);
|
||||||
|
}
|
||||||
|
_ => { /* noop */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_input(&self, code: KeyCode) -> Option<String> {
|
||||||
|
let num = match code {
|
||||||
|
KeyCode::Char('0') => 0,
|
||||||
|
KeyCode::Char('1') => 1,
|
||||||
|
KeyCode::Char('2') => 2,
|
||||||
|
KeyCode::Char('3') => 3,
|
||||||
|
KeyCode::Char('4') => 4,
|
||||||
|
KeyCode::Char('5') => 5,
|
||||||
|
KeyCode::Char('6') => 6,
|
||||||
|
KeyCode::Char('7') => 7,
|
||||||
|
KeyCode::Char('8') => 8,
|
||||||
|
KeyCode::Char('9') => 9,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
self.links.iter().nth(num).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_text<'w>(&'w self) -> Text<'_> {
|
||||||
|
Text::raw(&self.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jwall): We need this to be lines instead of just a render.
|
||||||
|
impl Widget for Markdown {
|
||||||
|
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let text = Text::raw(self.input);
|
||||||
|
text.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ use super::*;
|
|||||||
pub mod viewport;
|
pub mod viewport;
|
||||||
pub use viewport::Viewport;
|
pub use viewport::Viewport;
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
|
pub mod markdown;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
@ -99,12 +100,18 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
|||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
if self.state.modality() == &Modality::Dialog {
|
if self.state.modality() == &Modality::Dialog {
|
||||||
let lines = Text::from_iter(self.state.popup.iter().cloned());
|
let lines = self
|
||||||
|
.state
|
||||||
|
.popup
|
||||||
|
.as_ref()
|
||||||
|
.map(|md| md.get_text())
|
||||||
|
.unwrap_or_else(|| Text::raw("Popup message here"));
|
||||||
let popup = dialog::Dialog::new(lines, "Help").scroll(self.state.dialog_scroll);
|
let popup = dialog::Dialog::new(lines, "Help").scroll(self.state.dialog_scroll);
|
||||||
popup.render(area, buf);
|
popup.render(area, buf);
|
||||||
} else if self.state.modality() == &Modality::Quit {
|
} else if self.state.modality() == &Modality::Quit {
|
||||||
let popup = dialog::Dialog::new(Text::raw("File is not yet saved. Save it first?"), "Quit")
|
let popup =
|
||||||
.with_bottom_title("Y/N");
|
dialog::Dialog::new(Text::raw("File is not yet saved. Save it first?"), "Quit")
|
||||||
|
.with_bottom_title("Y/N");
|
||||||
popup.render(area, buf);
|
popup.render(area, buf);
|
||||||
} else {
|
} else {
|
||||||
let outer_block = Block::bordered()
|
let outer_block = Block::bordered()
|
||||||
|
@ -14,8 +14,11 @@ fn test_viewport_get_visible_columns() {
|
|||||||
let default_size = book.get_col_size(1).expect("Failed to get column size");
|
let default_size = book.get_col_size(1).expect("Failed to get column size");
|
||||||
let width = default_size * 12 / 2;
|
let width = default_size * 12 / 2;
|
||||||
let app_state = AppState::default();
|
let app_state = AppState::default();
|
||||||
let viewport = Viewport::new(&book, Some(&app_state.range_select))
|
let viewport = Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address {
|
||||||
.with_selected(Address { sheet: 0, row: 1, col: 17 });
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 17,
|
||||||
|
});
|
||||||
let cols = viewport
|
let cols = viewport
|
||||||
.get_visible_columns((width + 5) as u16, &mut state)
|
.get_visible_columns((width + 5) as u16, &mut state)
|
||||||
.expect("Failed to get visible columns");
|
.expect("Failed to get visible columns");
|
||||||
@ -31,8 +34,11 @@ fn test_viewport_get_visible_rows() {
|
|||||||
);
|
);
|
||||||
let height = 6;
|
let height = 6;
|
||||||
let app_state = AppState::default();
|
let app_state = AppState::default();
|
||||||
let viewport = Viewport::new(&book, Some(&app_state.range_select))
|
let viewport = Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address {
|
||||||
.with_selected(Address { sheet: 0, row: 17, col: 1 });
|
sheet: 0,
|
||||||
|
row: 17,
|
||||||
|
col: 1,
|
||||||
|
});
|
||||||
let rows = viewport.get_visible_rows(height as u16, &mut state);
|
let rows = viewport.get_visible_rows(height as u16, &mut state);
|
||||||
assert_eq!(height - 1, rows.len());
|
assert_eq!(height - 1, rows.len());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -52,8 +58,11 @@ fn test_viewport_visible_columns_after_length_change() {
|
|||||||
let width = default_size * 12 / 2;
|
let width = default_size * 12 / 2;
|
||||||
{
|
{
|
||||||
let app_state = AppState::default();
|
let app_state = AppState::default();
|
||||||
let viewport = Viewport::new(&book, Some(&app_state.range_select))
|
let viewport = Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address {
|
||||||
.with_selected(Address { sheet: 0, row: 1, col: 17 });
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 17,
|
||||||
|
});
|
||||||
let cols = viewport
|
let cols = viewport
|
||||||
.get_visible_columns((width + 5) as u16, &mut state)
|
.get_visible_columns((width + 5) as u16, &mut state)
|
||||||
.expect("Failed to get visible columns");
|
.expect("Failed to get visible columns");
|
||||||
@ -65,8 +74,11 @@ fn test_viewport_visible_columns_after_length_change() {
|
|||||||
.expect("Failed to set column size");
|
.expect("Failed to set column size");
|
||||||
{
|
{
|
||||||
let app_state = AppState::default();
|
let app_state = AppState::default();
|
||||||
let viewport = Viewport::new(&book, Some(&app_state.range_select))
|
let viewport = Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address {
|
||||||
.with_selected(Address { sheet: 0, row: 1, col: 1 });
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 1,
|
||||||
|
});
|
||||||
let cols = viewport
|
let cols = viewport
|
||||||
.get_visible_columns((width + 5) as u16, &mut state)
|
.get_visible_columns((width + 5) as u16, &mut state)
|
||||||
.expect("Failed to get visible columns");
|
.expect("Failed to get visible columns");
|
||||||
|
@ -7,8 +7,8 @@ use ratatui::{
|
|||||||
widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
|
widgets::{Block, Cell, Row, StatefulWidget, Table, Widget},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::book;
|
|
||||||
use super::{Address, Book, RangeSelection};
|
use super::{Address, Book, RangeSelection};
|
||||||
|
use crate::book;
|
||||||
|
|
||||||
/// A visible column to show in our Viewport.
|
/// A visible column to show in our Viewport.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -150,7 +150,11 @@ impl<'ws> Viewport<'ws> {
|
|||||||
|VisibleColumn { idx: ci, length: _ }| {
|
|VisibleColumn { idx: ci, length: _ }| {
|
||||||
let content = self
|
let content = self
|
||||||
.book
|
.book
|
||||||
.get_cell_addr_rendered(&Address { row: ri, col: *ci, sheet: self.book.location.sheet})
|
.get_cell_addr_rendered(&Address {
|
||||||
|
row: ri,
|
||||||
|
col: *ci,
|
||||||
|
sheet: self.book.location.sheet,
|
||||||
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.compute_cell_style(ri, *ci, Cell::new(Text::raw(content)))
|
self.compute_cell_style(ri, *ci, Cell::new(Text::raw(content)))
|
||||||
},
|
},
|
||||||
@ -192,29 +196,27 @@ impl<'ws> Viewport<'ws> {
|
|||||||
mut cell: Cell<'widget>,
|
mut cell: Cell<'widget>,
|
||||||
) -> Cell<'widget> {
|
) -> Cell<'widget> {
|
||||||
// TODO(zaphar): Should probably create somekind of formatter abstraction.
|
// TODO(zaphar): Should probably create somekind of formatter abstraction.
|
||||||
if let Some(style) = self
|
if let Some(style) = self.book.get_cell_style(&Address {
|
||||||
.book
|
sheet: self.book.location.sheet,
|
||||||
.get_cell_style(&Address { sheet: self.book.location.sheet, row: ri, col: ci }) {
|
row: ri,
|
||||||
|
col: ci,
|
||||||
|
}) {
|
||||||
cell = self.compute_cell_colors(&style, ri, ci, cell);
|
cell = self.compute_cell_colors(&style, ri, ci, cell);
|
||||||
cell = if style.font.b {
|
cell = if style.font.b { cell.bold() } else { cell };
|
||||||
cell.bold()
|
cell = if style.font.i { cell.italic() } else { cell };
|
||||||
} else { cell };
|
|
||||||
cell = if style.font.i {
|
|
||||||
cell.italic()
|
|
||||||
} else { cell };
|
|
||||||
}
|
}
|
||||||
cell
|
cell
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_cell_colors<'widget>(&self, style: &ironcalc::base::types::Style, ri: usize, ci: usize, mut cell: Cell<'widget>) -> Cell<'widget> {
|
fn compute_cell_colors<'widget>(
|
||||||
let bg_color = map_color(
|
&self,
|
||||||
style.fill.bg_color.as_ref(),
|
style: &ironcalc::base::types::Style,
|
||||||
Color::Rgb(35, 33, 54),
|
ri: usize,
|
||||||
);
|
ci: usize,
|
||||||
let fg_color = map_color(
|
mut cell: Cell<'widget>,
|
||||||
style.fill.fg_color.as_ref(),
|
) -> Cell<'widget> {
|
||||||
Color::White,
|
let bg_color = map_color(style.fill.bg_color.as_ref(), Color::Rgb(35, 33, 54));
|
||||||
);
|
let fg_color = map_color(style.fill.fg_color.as_ref(), Color::White);
|
||||||
if let Some((start, end)) = &self.range_selection.map_or(None, |r| r.get_range()) {
|
if let Some((start, end)) = &self.range_selection.map_or(None, |r| r.get_range()) {
|
||||||
if ri >= start.row && ri <= end.row && ci >= start.col && ci <= end.col {
|
if ri >= start.row && ri <= end.row && ci >= start.col && ci <= end.col {
|
||||||
// This is a selected range
|
// This is a selected range
|
||||||
|
215
src/ui/test.rs
215
src/ui/test.rs
@ -395,7 +395,7 @@ macro_rules! assert_help_dialog {
|
|||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("Failed to handle 'alt-h' key event");
|
.expect("Failed to handle 'alt-h' key event");
|
||||||
assert_eq!(Some(&Modality::Dialog), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Dialog), ws.state.modality_stack.last());
|
||||||
assert_eq!(edit_help, ws.state.popup);
|
assert_eq!(Some(edit_help), ws.state.popup);
|
||||||
$exit.run(&mut ws).expect("Failed to handle key event");
|
$exit.run(&mut ws).expect("Failed to handle key event");
|
||||||
assert_eq!(Some(&Modality::CellEdit), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::CellEdit), ws.state.modality_stack.last());
|
||||||
}};
|
}};
|
||||||
@ -431,7 +431,7 @@ fn test_navigation_mode_help_keycode() {
|
|||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("Failed to handle 'alt-h' key event");
|
.expect("Failed to handle 'alt-h' key event");
|
||||||
assert_eq!(Some(&Modality::Dialog), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Dialog), ws.state.modality_stack.last());
|
||||||
assert_eq!(help_text, ws.state.popup);
|
assert_eq!(Some(help_text), ws.state.popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -449,7 +449,7 @@ fn test_command_mode_help_keycode() {
|
|||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("Failed to handle 'alt-h' key event");
|
.expect("Failed to handle 'alt-h' key event");
|
||||||
assert_eq!(Some(&Modality::Dialog), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Dialog), ws.state.modality_stack.last());
|
||||||
assert_eq!(edit_help, ws.state.popup);
|
assert_eq!(Some(edit_help), ws.state.popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -554,9 +554,7 @@ fn test_range_copy() {
|
|||||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
|
|
||||||
let address = Address::default();
|
let address = Address::default();
|
||||||
ws.book
|
ws.book.move_to(&address).expect("Failed to move to row");
|
||||||
.move_to(&address)
|
|
||||||
.expect("Failed to move to row");
|
|
||||||
let original_loc = ws.book.location.clone();
|
let original_loc = ws.book.location.clone();
|
||||||
script()
|
script()
|
||||||
.ctrl('r')
|
.ctrl('r')
|
||||||
@ -576,7 +574,11 @@ fn test_range_copy() {
|
|||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("Failed to handle key sequence");
|
.expect("Failed to handle key sequence");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Address { sheet: 0, row: 1, col: 2 }),
|
Some(Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 2
|
||||||
|
}),
|
||||||
ws.state.range_select.start
|
ws.state.range_select.start
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -588,18 +590,40 @@ fn test_range_copy() {
|
|||||||
|
|
||||||
assert!(ws.state.range_select.original_location.is_none());
|
assert!(ws.state.range_select.original_location.is_none());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Address { sheet: 0, row: 1, col: 2 }),
|
Some(Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 2
|
||||||
|
}),
|
||||||
ws.state.range_select.start
|
ws.state.range_select.start
|
||||||
);
|
);
|
||||||
assert_eq!(Some(Address { sheet: 0, row: 2, col: 2 }), ws.state.range_select.end);
|
assert_eq!(
|
||||||
|
Some(Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 2,
|
||||||
|
col: 2
|
||||||
|
}),
|
||||||
|
ws.state.range_select.end
|
||||||
|
);
|
||||||
assert_eq!(original_loc, ws.book.location);
|
assert_eq!(original_loc, ws.book.location);
|
||||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
|
|
||||||
ws.book
|
ws.book
|
||||||
.move_to(&Address { sheet: 0, row: 5, col: 5 })
|
.move_to(&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 5,
|
||||||
|
col: 5,
|
||||||
|
})
|
||||||
.expect("Failed to move to row");
|
.expect("Failed to move to row");
|
||||||
let original_loc_2 = ws.book.location.clone();
|
let original_loc_2 = ws.book.location.clone();
|
||||||
assert_eq!(Address { sheet: 0, row: 5, col: 5 }, original_loc_2);
|
assert_eq!(
|
||||||
|
Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 5,
|
||||||
|
col: 5
|
||||||
|
},
|
||||||
|
original_loc_2
|
||||||
|
);
|
||||||
|
|
||||||
script()
|
script()
|
||||||
.char('v')
|
.char('v')
|
||||||
@ -619,7 +643,11 @@ fn test_range_copy() {
|
|||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("Failed to handle key sequence");
|
.expect("Failed to handle key sequence");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Address { sheet: 0, row: 5, col: 5 }),
|
Some(Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 5,
|
||||||
|
col: 5
|
||||||
|
}),
|
||||||
ws.state.range_select.start
|
ws.state.range_select.start
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -631,11 +659,29 @@ fn test_range_copy() {
|
|||||||
|
|
||||||
assert!(ws.state.range_select.original_location.is_none());
|
assert!(ws.state.range_select.original_location.is_none());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(Address { sheet: 0, row: 5, col: 5 }),
|
Some(Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 5,
|
||||||
|
col: 5
|
||||||
|
}),
|
||||||
ws.state.range_select.start
|
ws.state.range_select.start
|
||||||
);
|
);
|
||||||
assert_eq!(Some(Address { sheet: 0, row: 5, col: 4 }), ws.state.range_select.end);
|
assert_eq!(
|
||||||
assert_eq!(Address { sheet: 0, row: 4, col: 5 }, ws.book.location);
|
Some(Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 5,
|
||||||
|
col: 4
|
||||||
|
}),
|
||||||
|
ws.state.range_select.end
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 4,
|
||||||
|
col: 5
|
||||||
|
},
|
||||||
|
ws.book.location
|
||||||
|
);
|
||||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,14 +710,28 @@ fn test_gg_movement() {
|
|||||||
.char('j')
|
.char('j')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("failed to handle event sequence");
|
.expect("failed to handle event sequence");
|
||||||
assert_eq!(ws.book.location, Address { sheet: 0, row: 3, col: 1 });
|
assert_eq!(
|
||||||
|
ws.book.location,
|
||||||
|
Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
col: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
script()
|
script()
|
||||||
.char('l')
|
.char('l')
|
||||||
.char('g')
|
.char('g')
|
||||||
.char('g')
|
.char('g')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("failed to handle event sequence");
|
.expect("failed to handle event sequence");
|
||||||
assert_eq!(ws.book.location, Address { sheet: 0, row: 1, col: 2 });
|
assert_eq!(
|
||||||
|
ws.book.location,
|
||||||
|
Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 2
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -684,14 +744,28 @@ fn test_h_j_k_l_movement() {
|
|||||||
.char('l')
|
.char('l')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("failed to handle event sequence");
|
.expect("failed to handle event sequence");
|
||||||
assert_eq!(ws.book.location, Address { sheet: 0, row: 3, col: 2 });
|
assert_eq!(
|
||||||
|
ws.book.location,
|
||||||
|
Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
col: 2
|
||||||
|
}
|
||||||
|
);
|
||||||
script()
|
script()
|
||||||
.char('h')
|
.char('h')
|
||||||
.char('2')
|
.char('2')
|
||||||
.char('k')
|
.char('k')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("failed to handle event sequence");
|
.expect("failed to handle event sequence");
|
||||||
assert_eq!(ws.book.location, Address { sheet: 0, row: 1, col: 1 });
|
assert_eq!(
|
||||||
|
ws.book.location,
|
||||||
|
Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! assert_copy_paste {
|
macro_rules! assert_copy_paste {
|
||||||
@ -930,8 +1004,16 @@ fn test_command_mode_enter() {
|
|||||||
fn test_edit_mode_paste() {
|
fn test_edit_mode_paste() {
|
||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
ws.state.range_select.start = Some(Address { sheet: 0, row: 1, col: 1 });
|
ws.state.range_select.start = Some(Address {
|
||||||
ws.state.range_select.end = Some(Address { sheet: 0, row: 2, col: 2 });
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 1,
|
||||||
|
});
|
||||||
|
ws.state.range_select.end = Some(Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 2,
|
||||||
|
col: 2,
|
||||||
|
});
|
||||||
script()
|
script()
|
||||||
.char('e')
|
.char('e')
|
||||||
.ctrl('p')
|
.ctrl('p')
|
||||||
@ -979,8 +1061,16 @@ macro_rules! assert_range_clear {
|
|||||||
($script : expr) => {{
|
($script : expr) => {{
|
||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
let first_corner = Address { sheet: 0, row: 1, col: 1 };
|
let first_corner = Address {
|
||||||
let second_corner = Address { sheet: 0, row: 2, col: 2 };
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 1,
|
||||||
|
};
|
||||||
|
let second_corner = Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 2,
|
||||||
|
col: 2,
|
||||||
|
};
|
||||||
ws.book
|
ws.book
|
||||||
.update_cell(&first_corner, "foo")
|
.update_cell(&first_corner, "foo")
|
||||||
.expect("Failed to update cell");
|
.expect("Failed to update cell");
|
||||||
@ -1050,7 +1140,14 @@ fn test_range_select_movement() {
|
|||||||
.char('k')
|
.char('k')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("failed to run script");
|
.expect("failed to run script");
|
||||||
assert_eq!(&Address { sheet: 0, row: 3, col: 3 }, &ws.book.location);
|
assert_eq!(
|
||||||
|
&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
col: 3
|
||||||
|
},
|
||||||
|
&ws.book.location
|
||||||
|
);
|
||||||
script()
|
script()
|
||||||
.ctrl('n')
|
.ctrl('n')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
@ -1071,8 +1168,16 @@ fn test_range_select_clear_lower_d() {
|
|||||||
macro_rules! assert_range_copy {
|
macro_rules! assert_range_copy {
|
||||||
($script: expr) => {{
|
($script: expr) => {{
|
||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
let top_left_addr = Address { sheet: 0, row: 2, col: 2 };
|
let top_left_addr = Address {
|
||||||
let bot_right_addr = Address { sheet: 0, row: 4, col: 4 };
|
sheet: 0,
|
||||||
|
row: 2,
|
||||||
|
col: 2,
|
||||||
|
};
|
||||||
|
let bot_right_addr = Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 4,
|
||||||
|
col: 4,
|
||||||
|
};
|
||||||
ws.book
|
ws.book
|
||||||
.update_cell(&top_left_addr, "top_left")
|
.update_cell(&top_left_addr, "top_left")
|
||||||
.expect("Failed to update top left");
|
.expect("Failed to update top left");
|
||||||
@ -1111,7 +1216,11 @@ macro_rules! assert_range_copy {
|
|||||||
.expect("Didn't find a start of range")
|
.expect("Didn't find a start of range")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&Address { sheet: 0, row: 1, col: 1 },
|
&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 1
|
||||||
|
},
|
||||||
ws.state
|
ws.state
|
||||||
.range_select
|
.range_select
|
||||||
.original_location
|
.original_location
|
||||||
@ -1179,7 +1288,11 @@ fn test_extend_to_range() {
|
|||||||
.expect("Unable to run script");
|
.expect("Unable to run script");
|
||||||
let extended_cell = ws
|
let extended_cell = ws
|
||||||
.book
|
.book
|
||||||
.get_cell_addr_contents(&Address { sheet: 0, row: 2, col: 1 })
|
.get_cell_addr_contents(&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 2,
|
||||||
|
col: 1,
|
||||||
|
})
|
||||||
.expect("Failed to get cell contents");
|
.expect("Failed to get cell contents");
|
||||||
assert_eq!("=B2+1".to_string(), extended_cell);
|
assert_eq!("=B2+1".to_string(), extended_cell);
|
||||||
}
|
}
|
||||||
@ -1199,7 +1312,11 @@ fn test_color_cells() {
|
|||||||
for ci in 1..=3 {
|
for ci in 1..=3 {
|
||||||
let style = ws
|
let style = ws
|
||||||
.book
|
.book
|
||||||
.get_cell_style(&Address { sheet: ws.book.location.sheet, row: ri, col: ci })
|
.get_cell_style(&Address {
|
||||||
|
sheet: ws.book.location.sheet,
|
||||||
|
row: ri,
|
||||||
|
col: ci,
|
||||||
|
})
|
||||||
.expect("failed to get style");
|
.expect("failed to get style");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#800000",
|
"#800000",
|
||||||
@ -1225,7 +1342,11 @@ fn test_color_row() {
|
|||||||
for ci in [1, book::LAST_COLUMN] {
|
for ci in [1, book::LAST_COLUMN] {
|
||||||
let style = ws
|
let style = ws
|
||||||
.book
|
.book
|
||||||
.get_cell_style(&Address { sheet: ws.book.location.sheet, row: 1, col: ci as usize })
|
.get_cell_style(&Address {
|
||||||
|
sheet: ws.book.location.sheet,
|
||||||
|
row: 1,
|
||||||
|
col: ci as usize,
|
||||||
|
})
|
||||||
.expect("failed to get style");
|
.expect("failed to get style");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#800000",
|
"#800000",
|
||||||
@ -1250,7 +1371,11 @@ fn test_color_col() {
|
|||||||
for ri in [1, book::LAST_ROW] {
|
for ri in [1, book::LAST_ROW] {
|
||||||
let style = ws
|
let style = ws
|
||||||
.book
|
.book
|
||||||
.get_cell_style(&Address { sheet: ws.book.location.sheet, row: ri as usize, col: 1 })
|
.get_cell_style(&Address {
|
||||||
|
sheet: ws.book.location.sheet,
|
||||||
|
row: ri as usize,
|
||||||
|
col: 1,
|
||||||
|
})
|
||||||
.expect("failed to get style");
|
.expect("failed to get style");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#800000",
|
"#800000",
|
||||||
@ -1268,7 +1393,11 @@ fn test_bold_text() {
|
|||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
let before_style = ws
|
let before_style = ws
|
||||||
.book
|
.book
|
||||||
.get_cell_style(&Address { sheet: 0, row: 1, col: 1 })
|
.get_cell_style(&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 1,
|
||||||
|
})
|
||||||
.expect("Failed to get style");
|
.expect("Failed to get style");
|
||||||
assert!(!before_style.font.b);
|
assert!(!before_style.font.b);
|
||||||
script()
|
script()
|
||||||
@ -1277,7 +1406,11 @@ fn test_bold_text() {
|
|||||||
.expect("Unable to run script");
|
.expect("Unable to run script");
|
||||||
let style = ws
|
let style = ws
|
||||||
.book
|
.book
|
||||||
.get_cell_style(&Address { sheet: 0, row: 1, col: 1 })
|
.get_cell_style(&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 1,
|
||||||
|
})
|
||||||
.expect("Failed to get style");
|
.expect("Failed to get style");
|
||||||
assert!(style.font.b);
|
assert!(style.font.b);
|
||||||
script()
|
script()
|
||||||
@ -1292,7 +1425,11 @@ fn test_italic_text() {
|
|||||||
let mut ws = new_workspace();
|
let mut ws = new_workspace();
|
||||||
let before_style = ws
|
let before_style = ws
|
||||||
.book
|
.book
|
||||||
.get_cell_style(&Address { sheet: 0, row: 1, col: 1 })
|
.get_cell_style(&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 1,
|
||||||
|
})
|
||||||
.expect("Failed to get style");
|
.expect("Failed to get style");
|
||||||
assert!(!before_style.font.i);
|
assert!(!before_style.font.i);
|
||||||
script()
|
script()
|
||||||
@ -1301,7 +1438,11 @@ fn test_italic_text() {
|
|||||||
.expect("Unable to run script");
|
.expect("Unable to run script");
|
||||||
let style = ws
|
let style = ws
|
||||||
.book
|
.book
|
||||||
.get_cell_style(&Address { sheet: 0, row: 1, col: 1 })
|
.get_cell_style(&Address {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
col: 1,
|
||||||
|
})
|
||||||
.expect("Failed to get style");
|
.expect("Failed to get style");
|
||||||
assert!(style.font.i);
|
assert!(style.font.i);
|
||||||
script()
|
script()
|
||||||
@ -1331,7 +1472,7 @@ fn test_quit_dialog() {
|
|||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
.expect("Failed to run input script");
|
.expect("Failed to run input script");
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
|
|
||||||
script()
|
script()
|
||||||
.chars("efoo")
|
.chars("efoo")
|
||||||
.enter()
|
.enter()
|
||||||
@ -1344,7 +1485,7 @@ fn test_quit_dialog() {
|
|||||||
.expect("Failed to run input script");
|
.expect("Failed to run input script");
|
||||||
assert!(!result.is_some());
|
assert!(!result.is_some());
|
||||||
assert_eq!(ws.state.modality(), &Modality::Quit);
|
assert_eq!(ws.state.modality(), &Modality::Quit);
|
||||||
|
|
||||||
script()
|
script()
|
||||||
.char('n')
|
.char('n')
|
||||||
.run(&mut ws)
|
.run(&mut ws)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user