Compare commits

...

8 Commits

10 changed files with 302 additions and 38 deletions

View File

@ -35,6 +35,13 @@ Options:
sheetui path/to/file.xlsx # edit/view a spreadsheet sheetui path/to/file.xlsx # edit/view a spreadsheet
``` ```
### Supported files
Currently we only support the [ironcalc](https://docs.ironcalc.com/) xlsx
features for spreadsheet. CSV import and expor are planned.
### Screenshot
<img src="./assets/screenshot.png" /> <img src="./assets/screenshot.png" />
## Reference ## Reference

View File

@ -20,6 +20,13 @@ Options:
-V, --version Print version -V, --version Print version
``` ```
## Supported formats
Currently we only support the [ironcalc](https://docs.ironcalc.com/) xlsx
features for spreadsheet. I plan to handle csv import and export at some point.
I also might support other export formats as well but for the moment just csv
and it's variants such as tsv are in the roadmap.
## User Interface ## User Interface
The sheetui user interface is loosely inspired by vim. It is a modal interface The sheetui user interface is loosely inspired by vim. It is a modal interface
@ -37,6 +44,8 @@ table and between the sheets using the following keybinds:
* `l` and, ➡️ will move one cell to the right. * `l` and, ➡️ will move one cell to the right.
* `j`, ⬇️, and `Enter` will move one cell down. * `j`, ⬇️, and `Enter` will move one cell down.
* `k` ⬆️, will move one cell up. * `k` ⬆️, will move one cell up.
* `d` will delete the contents of the selected cell leaving style untouched
* `D` will delete the contents of the selected cell including any style
**Sheet Navigation** **Sheet Navigation**
@ -61,6 +70,9 @@ will clear the numeric prefix if you want to cancel it.
* `Ctrl-r` will enter range selection mode * `Ctrl-r` will enter range selection mode
* `Ctrl-s` will save the sheet. * `Ctrl-s` will save the sheet.
* `Ctrl-c`, `y` Copy the cell or range contents.
* `Ctrl-v`, `p` Paste into the sheet.
* `Ctrl-Shift-C` Copy the cell or range formatted content.
* `q` will exit the application. * `q` will exit the application.
* `:` will enter CommandMode. * `:` will enter CommandMode.
@ -122,12 +134,16 @@ will be discarded if you have not saved first.</aside>
### Range Select Mode ### Range Select Mode
Range Select mode copies a range reference for use later. You can enter range Range Select mode copies a range reference for use later or delete a range's contents. You can enter range
select mode from CellEdit mode with `CTRL-r`. select mode from CellEdit mode with `CTRL-r`.
* `h`, `j`, `k`, `l` will navigate around the sheet. * `h`, `j`, `k`, `l` will navigate around the sheet.
* `Ctrl-n`, `Ctrl-p` will navigate between sheets. * `Ctrl-n`, `Ctrl-p` will navigate between sheets.
* `Ctrl-c`, `y` Copy the cell or range contents.
* `Ctrl-Shift-C`, 'Y' Copy the cell or range formatted content.
* `The spacebar will select the start and end of the range respectively. * `The spacebar will select the start and end of the range respectively.
* `d` will delete the contents of the range leaving any style untouched
* `D` will delete the contents of the range including any style
When you have selected the end of the range you will exit range select mode and When you have selected the end of the range you will exit range select mode and
the range reference will be placed into the cell contents you are editing. the range reference will be placed into the cell contents you are editing.

View File

@ -104,6 +104,48 @@ impl Book {
Ok(()) Ok(())
} }
pub fn clear_current_cell(&mut self) -> Result<()> {
self.clear_cell_contents(self.current_sheet as u32, self.location.clone())
}
pub fn clear_current_cell_all(&mut self) -> Result<()> {
self.clear_cell_all(self.current_sheet as u32, self.location.clone())
}
pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col, }: Address) -> Result<()> {
Ok(self
.model
.cell_clear_contents(sheet, row as i32, col as i32)
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?)
}
pub fn clear_cell_range(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> {
for row in start.row..=end.row {
for col in start.col..=end.col {
self.clear_cell_contents(sheet, Address { row, col })?;
}
}
Ok(())
}
pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col, }: Address) -> Result<()> {
Ok(self
.model
.cell_clear_all(sheet, row as i32, col as i32)
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?)
}
pub fn clear_cell_range_all(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> {
for row in start.row..=end.row {
for col in start.col..=end.col {
self.clear_cell_all(sheet, Address { row, col })?;
}
}
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)?) Ok(self.get_cell_addr_rendered(&self.location)?)
@ -116,6 +158,15 @@ impl Book {
.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))?)
} }
/// Get a cells actual content unformatted as a string.
pub fn get_cell_addr_contents(&self, Address { row, col }: &Address) -> Result<String> {
Ok(self
.model
.get_cell_content(self.current_sheet, *row as i32, *col as i32)
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
}
/// Get a cells actual content as a string. /// Get a cells actual content as a string.
pub fn get_current_cell_contents(&self) -> Result<String> { pub fn get_current_cell_contents(&self) -> Result<String> {
@ -132,13 +183,13 @@ impl Book {
/// Update the current cell in a book. /// Update the current cell in a book.
/// This update won't be reflected until you call `Book::evaluate`. /// This update won't be reflected until you call `Book::evaluate`.
pub fn edit_current_cell<S: Into<String>>(&mut self, value: S) -> Result<()> { pub fn edit_current_cell<S: Into<String>>(&mut self, value: S) -> Result<()> {
self.update_entry(&self.location.clone(), value)?; self.update_cell(&self.location.clone(), value)?;
Ok(()) Ok(())
} }
/// Update an entry in the current sheet for a book. /// Update an entry in the current sheet for a book.
/// This update won't be reflected until you call `Book::evaluate`. /// This update won't be reflected until you call `Book::evaluate`.
pub fn update_entry<S: Into<String>>(&mut self, location: &Address, value: S) -> Result<()> { pub fn update_cell<S: Into<String>>(&mut self, location: &Address, value: S) -> Result<()> {
self.model self.model
.set_user_input( .set_user_input(
self.current_sheet, self.current_sheet,
@ -307,7 +358,7 @@ impl Default for Book {
fn default() -> Self { fn default() -> Self {
let mut book = let mut book =
Book::new(Model::new_empty("default_name", "en", "America/New_York").unwrap()); Book::new(Model::new_empty("default_name", "en", "America/New_York").unwrap());
book.update_entry(&Address { row: 1, col: 1 }, "").unwrap(); book.update_cell(&Address { row: 1, col: 1 }, "").unwrap();
book book
} }
} }

View File

@ -36,7 +36,7 @@ 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_entry(&Address { row: 2, col: 1 }, "1") book.update_cell(&Address { row: 2, col: 1 }, "1")
.expect("failed to edit cell"); .expect("failed to edit cell");
book.evaluate(); book.evaluate();
let WorksheetDimension { let WorksheetDimension {
@ -52,7 +52,7 @@ 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_entry(&Address { row: 1, col: 2 }, "1") book.update_cell(&Address { row: 1, col: 2 }, "1")
.expect("failed to edit cell"); .expect("failed to edit cell");
let WorksheetDimension { let WorksheetDimension {
min_row, min_row,
@ -67,7 +67,7 @@ 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_entry(&Address { row: 2, col: 2 }, "1") book.update_cell(&Address { row: 2, col: 2 }, "1")
.expect("failed to edit cell"); .expect("failed to edit cell");
book.move_to(&Address { row: 2, col: 2 }) book.move_to(&Address { row: 2, col: 2 })
.expect("Failed to move to location"); .expect("Failed to move to location");
@ -85,7 +85,7 @@ 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_entry(&Address { row: 2, col: 2 }, "1") book.update_cell(&Address { row: 2, col: 2 }, "1")
.expect("failed to edit cell"); .expect("failed to edit cell");
book.move_to(&Address { row: 2, col: 2 }) book.move_to(&Address { row: 2, col: 2 })
.expect("Failed to move to location"); .expect("Failed to move to location");
@ -103,7 +103,7 @@ 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_entry(&Address { row: 2, col: 2 }, "1") book.update_cell(&Address { row: 2, col: 2 }, "1")
.expect("failed to edit cell"); .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"));

View File

@ -5,7 +5,7 @@ use slice_utils::{Measured, Peekable, Seekable, Span, StrCursor};
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> { pub enum Cmd<'a> {
Write(Option<&'a str>), Write(Option<&'a str>),
InsertRow(usize), InsertRows(usize),
InsertColumns(usize), InsertColumns(usize),
RenameSheet(Option<usize>, &'a str), RenameSheet(Option<usize>, &'a str),
NewSheet(Option<&'a str>), NewSheet(Option<&'a str>),
@ -155,7 +155,7 @@ fn try_consume_insert_row<'cmd, 'i: 'cmd>(
return Err("Invalid command: Did you mean to type `insert-rows <arg>`?"); return Err("Invalid command: Did you mean to type `insert-rows <arg>`?");
} }
let arg = input.span(0..).trim(); let arg = input.span(0..).trim();
return Ok(Some(Cmd::InsertRow(if arg.is_empty() { return Ok(Some(Cmd::InsertRows(if arg.is_empty() {
1 1
} else { } else {
if let Ok(count) = arg.parse() { if let Ok(count) = arg.parse() {

View File

@ -66,6 +66,12 @@ impl RangeSelection {
} }
} }
#[derive(Debug)]
pub enum ClipboardContents {
Cell(String),
Range(Vec<Vec<String>>),
}
#[derive(Debug)] #[derive(Debug)]
pub struct AppState<'ws> { pub struct AppState<'ws> {
pub modality_stack: Vec<Modality>, pub modality_stack: Vec<Modality>,
@ -75,6 +81,7 @@ pub struct AppState<'ws> {
pub range_select: RangeSelection, pub range_select: RangeSelection,
dirty: bool, dirty: bool,
popup: Vec<String>, popup: Vec<String>,
clipboard: Option<ClipboardContents>,
} }
impl<'ws> Default for AppState<'ws> { impl<'ws> Default for AppState<'ws> {
@ -87,6 +94,7 @@ impl<'ws> Default for AppState<'ws> {
range_select: Default::default(), range_select: Default::default(),
dirty: Default::default(), dirty: Default::default(),
popup: Default::default(), popup: Default::default(),
clipboard: Default::default(),
} }
} }
} }
@ -201,7 +209,11 @@ impl<'ws> Workspace<'ws> {
pub fn selected_range_to_string(&self) -> String { pub fn selected_range_to_string(&self) -> String {
let state = &self.state; let state = &self.state;
if let Some((start, end)) = state.range_select.get_range() { if let Some((start, end)) = state.range_select.get_range() {
let a1 = format!("{}{}", start.to_range_part(), format!(":{}", end.to_range_part())); let a1 = format!(
"{}{}",
start.to_range_part(),
format!(":{}", end.to_range_part())
);
if let Some(range_sheet) = state.range_select.sheet { if let Some(range_sheet) = state.range_select.sheet {
if range_sheet != self.book.current_sheet { if range_sheet != self.book.current_sheet {
return format!( return format!(
@ -215,7 +227,7 @@ impl<'ws> Workspace<'ws> {
} }
return a1; return a1;
} }
return String::new() return String::new();
} }
/// Move a row down in the current sheet. /// Move a row down in the current sheet.
@ -281,6 +293,8 @@ impl<'ws> Workspace<'ws> {
"* ENTER/RETURN: Go down one cell".to_string(), "* ENTER/RETURN: Go down one cell".to_string(),
"* TAB: Go over one cell".to_string(), "* TAB: Go over one cell".to_string(),
"* h,j,k,l: vim style navigation".to_string(), "* h,j,k,l: vim style navigation".to_string(),
"* d: clear cell contents leaving style untouched".to_string(),
"* D: clear cell contents including style".to_string(),
"* CTRl-r: Add a row".to_string(), "* CTRl-r: Add a row".to_string(),
"* CTRl-c: Add a column".to_string(), "* CTRl-c: Add a column".to_string(),
"* CTRl-l: Grow column width by 1".to_string(), "* CTRl-l: Grow column width by 1".to_string(),
@ -308,6 +322,8 @@ impl<'ws> Workspace<'ws> {
"Range Selection Mode:".to_string(), "Range Selection Mode:".to_string(),
"* ESC: Exit command mode".to_string(), "* ESC: Exit command mode".to_string(),
"* h,j,k,l: vim style navigation".to_string(), "* h,j,k,l: vim style navigation".to_string(),
"* d: delete the contents of the range leaving style untouched".to_string(),
"* D: clear cell contents including style".to_string(),
"* Spacebar: Select start and end of range".to_string(), "* Spacebar: Select start and end of range".to_string(),
"* CTRl-n: Next sheet. Starts over at beginning if at end.".to_string(), "* CTRl-n: Next sheet. Starts over at beginning if at end.".to_string(),
"* CTRl-p: Previous sheet. Starts over at end if at beginning.".to_string(), "* CTRl-p: Previous sheet. Starts over at end if at beginning.".to_string(),
@ -409,7 +425,7 @@ impl<'ws> Workspace<'ws> {
self.book.evaluate(); self.book.evaluate();
Ok(None) Ok(None)
} }
Ok(Some(Cmd::InsertRow(count))) => { Ok(Some(Cmd::InsertRows(count))) => {
self.book.insert_rows(self.book.location.row, count)?; self.book.insert_rows(self.book.location.row, count)?;
self.book.evaluate(); self.book.evaluate();
Ok(None) Ok(None)
@ -435,7 +451,6 @@ impl<'ws> Workspace<'ws> {
Ok(None) Ok(None)
} }
Ok(Some(Cmd::Quit)) => { Ok(Some(Cmd::Quit)) => {
// TODO(zaphar): We probably need to do better than this
Ok(Some(ExitCode::SUCCESS)) Ok(Some(ExitCode::SUCCESS))
} }
Ok(None) => { Ok(None) => {
@ -472,6 +487,30 @@ impl<'ws> Workspace<'ws> {
KeyCode::Char(d) if d.is_ascii_digit() => { KeyCode::Char(d) if d.is_ascii_digit() => {
self.handle_numeric_prefix(d); self.handle_numeric_prefix(d);
} }
KeyCode::Char('D') => {
if let Some((start, end)) = self.state.range_select.get_range() {
self.book.clear_cell_range_all(
self.state
.range_select
.sheet
.unwrap_or_else(|| self.book.current_sheet),
start,
end,
)?;
}
}
KeyCode::Char('d') => {
if let Some((start, end)) = self.state.range_select.get_range() {
self.book.clear_cell_range(
self.state
.range_select
.sheet
.unwrap_or_else(|| self.book.current_sheet),
start,
end,
)?;
}
}
KeyCode::Char('h') => { KeyCode::Char('h') => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_left()?; ws.move_left()?;
@ -501,12 +540,7 @@ impl<'ws> Workspace<'ws> {
self.maybe_update_range_end(); self.maybe_update_range_end();
} }
KeyCode::Char(' ') | KeyCode::Enter => { KeyCode::Char(' ') | KeyCode::Enter => {
if self.state.range_select.start.is_none() { self.update_range_selection()?;
self.state.range_select.start = Some(self.book.location.clone());
} else {
self.state.range_select.end = Some(self.book.location.clone());
self.exit_range_select_mode()?;
}
} }
KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => {
self.state.range_select.reset_range_selection(); self.state.range_select.reset_range_selection();
@ -524,6 +558,19 @@ impl<'ws> Workspace<'ws> {
})?; })?;
self.state.range_select.sheet = Some(self.book.current_sheet); self.state.range_select.sheet = Some(self.book.current_sheet);
} }
KeyCode::Char('C')
if key
.modifiers
.contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT) =>
{
// TODO(zaphar): Share the algorithm below between both copies
self.copy_range(true)?;
}
KeyCode::Char('Y') => self.copy_range(true)?,
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
self.copy_range(false)?;
}
KeyCode::Char('y') => self.copy_range(false)?,
_ => { _ => {
// moop // moop
} }
@ -532,6 +579,59 @@ impl<'ws> Workspace<'ws> {
Ok(None) Ok(None)
} }
fn copy_range(&mut self, formatted: bool) -> Result<(), anyhow::Error> {
self.update_range_selection()?;
match &self.state.range_select.get_range() {
Some((
Address {
row: row_start,
col: col_start,
},
Address {
row: row_end,
col: col_end,
},
)) => {
let mut rows = Vec::new();
for ri in (*row_start)..=(*row_end) {
let mut cols = Vec::new();
for ci in (*col_start)..=(*col_end) {
cols.push(if formatted {
self.book
.get_cell_addr_rendered(&Address { row: ri, col: ci })?
} else {
self.book
.get_cell_addr_contents(&Address { row: ri, col: ci })?
});
}
rows.push(cols);
}
self.state.clipboard = Some(ClipboardContents::Range(rows));
}
None => {
self.state.clipboard = Some(ClipboardContents::Cell(if formatted {
self.book
.get_current_cell_rendered()?
} else {
self.book
.get_current_cell_contents()?
}));
}
}
self.exit_range_select_mode()?;
Ok(())
}
fn update_range_selection(&mut self) -> Result<(), anyhow::Error> {
Ok(if self.state.range_select.start.is_none() {
self.state.range_select.start = Some(self.book.location.clone());
self.state.range_select.end = Some(self.book.location.clone());
} else {
self.state.range_select.end = Some(self.book.location.clone());
self.exit_range_select_mode()?;
})
}
fn maybe_update_range_end(&mut self) { fn maybe_update_range_end(&mut self) {
if self.state.range_select.start.is_some() { if self.state.range_select.start.is_some() {
self.state.range_select.end = Some(self.book.location.clone()); self.state.range_select.end = Some(self.book.location.clone());
@ -556,9 +656,47 @@ impl<'ws> Workspace<'ws> {
KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => {
self.save_file()?; self.save_file()?;
} }
KeyCode::Char('s') if key.modifiers != KeyModifiers::CONTROL => {
self.book.clear_current_cell()?;
self.text_area = reset_text_area(String::new());
self.enter_edit_mode();
}
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
self.enter_range_select_mode(); self.enter_range_select_mode();
} }
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_contents()?,
));
}
KeyCode::Char('y') => {
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_contents()?,
));
}
KeyCode::Char('Y') => {
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_rendered()?,
));
}
KeyCode::Char('C')
if key
.modifiers
.contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT) =>
{
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_rendered()?,
));
}
KeyCode::Char('v') if key.modifiers != KeyModifiers::CONTROL => {
self.enter_range_select_mode()
}
KeyCode::Char('p') if key.modifiers != KeyModifiers::CONTROL => {
self.paste_range()?;
}
KeyCode::Char('v') if key.modifiers == KeyModifiers::CONTROL => {
self.paste_range()?;
}
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => { KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
self.enter_dialog_mode(self.render_help_text()); self.enter_dialog_mode(self.render_help_text());
} }
@ -568,6 +706,12 @@ impl<'ws> Workspace<'ws> {
Ok(()) Ok(())
})?; })?;
} }
KeyCode::Char('d') => {
self.book.clear_current_cell()?;
}
KeyCode::Char('D') => {
self.book.clear_current_cell_all()?;
}
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.book.select_prev_sheet(); ws.book.select_prev_sheet();
@ -665,6 +809,36 @@ impl<'ws> Workspace<'ws> {
return Ok(None); return Ok(None);
} }
fn paste_range(&mut self) -> Result<(), anyhow::Error> {
match &self.state.clipboard {
Some(ClipboardContents::Cell(contents)) => {
self.book.edit_current_cell(contents)?;
}
Some(ClipboardContents::Range(ref rows)) => {
let Address { row, col } = self.book.location.clone();
let row_len = rows.len();
for ri in 0..row_len {
let columns = &rows[ri];
let col_len = columns.len();
for ci in 0..col_len {
self.book.update_cell(
&Address {
row: ri + row,
col: ci + col,
},
columns[ci].clone(),
)?;
}
}
}
None => {
// NOOP
}
}
self.state.clipboard = None;
Ok(())
}
fn run_with_prefix( fn run_with_prefix(
&mut self, &mut self,
action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>, action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>,

View File

@ -28,25 +28,40 @@ impl<'ws> Workspace<'ws> {
]; ];
let mut rs: Vec<Box<dyn Fn(Rect, &mut Buffer, &mut Self)>> = vec![ let mut rs: Vec<Box<dyn Fn(Rect, &mut Buffer, &mut Self)>> = vec![
Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| { Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| {
let tabs = Tabs::new(ws.book.get_sheet_names().iter().enumerate().map(|(idx, name)| format!("{} {}", name, idx)).collect::<Vec<String>>()) let tabs = Tabs::new(
.select(Some(ws.book.current_sheet as usize)); ws.book
.get_sheet_names()
.iter()
.enumerate()
.map(|(idx, name)| format!("{} {}", name, idx))
.collect::<Vec<String>>(),
)
.select(Some(ws.book.current_sheet as usize));
tabs.render(rect, buf); tabs.render(rect, buf);
}), }),
Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| { Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| {
let [text_rect, info_rect] = Layout::horizontal(vec![Constraint::Fill(1),Constraint::Fill(1)]).areas(rect); let [text_rect, info_rect] =
Layout::horizontal(vec![Constraint::Fill(1), Constraint::Fill(1)]).areas(rect);
ws.text_area.render(text_rect, buf); ws.text_area.render(text_rect, buf);
let hint = Paragraph::new(vec![ let hint = Paragraph::new(vec![
Line::from(""), Line::from(""),
Line::from("ALT-h to toggle help dialog").centered() Line::from("ALT-h to toggle help dialog").centered(),
]); ]);
hint.render(info_rect, buf); hint.render(info_rect, buf);
}), }),
Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| { Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| {
let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown"); let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown");
let table_block = Block::bordered().title_top(sheet_name); let table_block = Block::bordered().title_top(sheet_name);
let viewport = Viewport::new(&ws.book, &ws.state.range_select) let viewport = Viewport::new(
.with_selected(ws.book.location.clone()) &ws.book,
.block(table_block); if ws.state.modality() == &Modality::RangeSelect {
Some(&ws.state.range_select)
} else {
None
},
)
.with_selected(ws.book.location.clone())
.block(table_block);
StatefulWidget::render(viewport, rect, buf, &mut ws.state.viewport_state); StatefulWidget::render(viewport, rect, buf, &mut ws.state.viewport_state);
}), }),
]; ];

View File

@ -14,7 +14,7 @@ fn test_viewport_get_visible_columns() {
let width = dbg!(dbg!(default_size) * 12 / 2); let width = dbg!(dbg!(default_size) * 12 / 2);
let app_state = AppState::default(); let app_state = AppState::default();
let viewport = let viewport =
Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 17 }); Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { 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,7 +31,7 @@ fn test_viewport_get_visible_rows() {
let height = 6; let height = 6;
let app_state = AppState::default(); let app_state = AppState::default();
let viewport = let viewport =
Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 17, col: 1 }); Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 17, col: 1 });
let rows = dbg!(viewport.get_visible_rows(height as u16, &mut state)); let rows = dbg!(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!(
@ -51,7 +51,7 @@ fn test_viewport_visible_columns_after_length_change() {
let width = dbg!(dbg!(default_size) * 12 / 2); let width = dbg!(dbg!(default_size) * 12 / 2);
{ {
let app_state = AppState::default(); let app_state = AppState::default();
let viewport = Viewport::new(&book, &app_state.range_select) let viewport = Viewport::new(&book, Some(&app_state.range_select))
.with_selected(Address { row: 1, col: 17 }); .with_selected(Address { 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)
@ -65,7 +65,7 @@ fn test_viewport_visible_columns_after_length_change() {
{ {
let app_state = AppState::default(); let app_state = AppState::default();
let viewport = let viewport =
Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 1 }); Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { 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");

View File

@ -37,7 +37,7 @@ pub struct ViewportState {
pub struct Viewport<'ws> { pub struct Viewport<'ws> {
pub(crate) selected: Address, pub(crate) selected: Address,
book: &'ws Book, book: &'ws Book,
range_selection: &'ws RangeSelection, range_selection: Option<&'ws RangeSelection>,
block: Option<Block<'ws>>, block: Option<Block<'ws>>,
} }
@ -47,7 +47,7 @@ pub(crate) const COLNAMES: [&'static str; 26] = [
]; ];
impl<'ws> Viewport<'ws> { impl<'ws> Viewport<'ws> {
pub fn new(book: &'ws Book, app_state: &'ws RangeSelection) -> Self { pub fn new(book: &'ws Book, app_state: Option<&'ws RangeSelection>) -> Self {
Self { Self {
book, book,
range_selection: app_state, range_selection: app_state,
@ -155,13 +155,14 @@ impl<'ws> Viewport<'ws> {
let mut cells = vec![Cell::new(Text::from(ri.to_string()))]; let mut cells = vec![Cell::new(Text::from(ri.to_string()))];
cells.extend(visible_columns.iter().map( cells.extend(visible_columns.iter().map(
|VisibleColumn { idx: ci, length: _ }| { |VisibleColumn { idx: ci, length: _ }| {
// TODO(zaphar): Is this safe?
let content = self let content = self
.book .book
.get_cell_addr_rendered(&Address { row: ri, col: *ci }) .get_cell_addr_rendered(&Address { row: ri, col: *ci })
.unwrap(); .unwrap();
let mut cell = Cell::new(Text::raw(content)); let mut cell = Cell::new(Text::raw(content));
if let Some((start, end)) = &self.range_selection.get_range() { if let Some((start, end)) =
&self.range_selection.map_or(None, |r| r.get_range())
{
if ri >= start.row if ri >= start.row
&& ri <= end.row && ri <= end.row
&& *ci >= start.col && *ci >= start.col

View File

@ -35,7 +35,7 @@ fn test_insert_rows_cmd() {
let output = result.unwrap(); let output = result.unwrap();
assert!(output.is_some()); assert!(output.is_some());
let cmd = output.unwrap(); let cmd = output.unwrap();
assert_eq!(cmd, Cmd::InsertRow(1)); assert_eq!(cmd, Cmd::InsertRows(1));
} }
#[test] #[test]
@ -46,7 +46,7 @@ fn test_insert_rows_cmd_short() {
let output = result.unwrap(); let output = result.unwrap();
assert!(output.is_some()); assert!(output.is_some());
let cmd = output.unwrap(); let cmd = output.unwrap();
assert_eq!(cmd, Cmd::InsertRow(1)); assert_eq!(cmd, Cmd::InsertRows(1));
} }
#[test] #[test]