mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-21 20:39:47 -04:00
wip: feat: prompt for saving if file is not saved yet
This commit is contained in:
parent
56979103d6
commit
2bf2f792bd
@ -86,6 +86,7 @@ impl<'book> AddressRange<'book> {
|
||||
pub struct Book {
|
||||
pub(crate) model: UserModel,
|
||||
pub location: crate::ui::Address,
|
||||
pub dirty: bool,
|
||||
}
|
||||
|
||||
impl Book {
|
||||
@ -94,6 +95,7 @@ impl Book {
|
||||
Self {
|
||||
model,
|
||||
location: Address::default(),
|
||||
dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,12 +124,13 @@ impl Book {
|
||||
}
|
||||
|
||||
/// Save book to an xlsx file.
|
||||
pub fn save_to_xlsx(&self, path: &str) -> Result<()> {
|
||||
// TODO(zaphar): Currently overwrites. Should we prompty in this case?
|
||||
pub fn save_to_xlsx(&mut self, path: &str) -> Result<()> {
|
||||
// TODO(zaphar): Currently overwrites. Should we prompt in this case?
|
||||
let file_path = std::path::Path::new(path);
|
||||
let file = std::fs::File::create(file_path)?;
|
||||
let writer = std::io::BufWriter::new(file);
|
||||
save_xlsx_to_writer(self.model.get_model(), writer)?;
|
||||
self.dirty = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -150,6 +153,7 @@ impl Book {
|
||||
self.model
|
||||
.rename_sheet(idx, sheet_name)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -162,6 +166,7 @@ impl Book {
|
||||
self.model
|
||||
.set_selected_sheet(self.location.sheet)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -175,6 +180,7 @@ impl Book {
|
||||
// FIXME(zaphar): Check that this is safe first.
|
||||
self.location.row = *row;
|
||||
self.location.col = *col;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -209,18 +215,22 @@ impl Book {
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
}
|
||||
self.evaluate();
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_current_cell(&mut self) -> Result<()> {
|
||||
self.dirty = true;
|
||||
self.clear_cell_contents(self.location.clone())
|
||||
}
|
||||
|
||||
pub fn clear_current_cell_all(&mut self) -> Result<()> {
|
||||
self.dirty = true;
|
||||
self.clear_cell_all(self.location.clone())
|
||||
}
|
||||
|
||||
pub fn clear_cell_contents(&mut self, Address { sheet, row, col }: Address) -> Result<()> {
|
||||
self.dirty = true;
|
||||
Ok(self
|
||||
.model
|
||||
.range_clear_contents(&Area {
|
||||
@ -238,10 +248,12 @@ impl Book {
|
||||
self.model
|
||||
.range_clear_contents(&area)
|
||||
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_cell_all(&mut self, Address { sheet, row, col }: Address) -> Result<()> {
|
||||
self.dirty = true;
|
||||
Ok(self
|
||||
.model
|
||||
.range_clear_all(&Area {
|
||||
@ -259,6 +271,7 @@ impl Book {
|
||||
self.model
|
||||
.range_clear_all(&area)
|
||||
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -299,6 +312,7 @@ impl Book {
|
||||
.update_range_style(area, path, val)
|
||||
.map_err(|s| anyhow!("Unable to format cell {}", s))?;
|
||||
}
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -343,6 +357,7 @@ impl Book {
|
||||
) -> Result<()> {
|
||||
let area = self.get_col_range(sheet, col_idx);
|
||||
self.set_cell_style(style, &area)?;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -367,6 +382,7 @@ impl Book {
|
||||
) -> Result<()> {
|
||||
let area = self.get_row_range(sheet, row_idx);
|
||||
self.set_cell_style(style, &area)?;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -401,6 +417,7 @@ impl Book {
|
||||
/// Update the current cell in a book.
|
||||
/// This update won't be reflected until you call `Book::evaluate`.
|
||||
pub fn edit_current_cell<S: AsRef<str>>(&mut self, value: S) -> Result<()> {
|
||||
self.dirty = true;
|
||||
self.update_cell(&self.location.clone(), value)?;
|
||||
Ok(())
|
||||
}
|
||||
@ -417,6 +434,7 @@ impl Book {
|
||||
value.as_ref(),
|
||||
)
|
||||
.map_err(|e| anyhow!("Invalid cell contents: {}", e))?;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -434,6 +452,7 @@ impl Book {
|
||||
col: self.location.col,
|
||||
})?;
|
||||
}
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -451,6 +470,7 @@ impl Book {
|
||||
col: self.location.col + count,
|
||||
})?;
|
||||
}
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -489,6 +509,7 @@ impl Book {
|
||||
self.model
|
||||
.set_column_width(sheet, col as i32, width as f64 * COL_PIXELS)
|
||||
.map_err(|e| anyhow!("Error setting column width: {:?}", e))?;
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
//! Ui rendering logic
|
||||
use std::{path::PathBuf, process::ExitCode};
|
||||
use std::{path::PathBuf, process::ExitCode, str::FromStr};
|
||||
|
||||
use crate::book::{self, AddressRange, Book};
|
||||
|
||||
@ -29,6 +29,7 @@ pub enum Modality {
|
||||
Command,
|
||||
Dialog,
|
||||
RangeSelect,
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@ -93,7 +94,7 @@ impl<'ws> Default for AppState<'ws> {
|
||||
char_queue: Default::default(),
|
||||
range_select: Default::default(),
|
||||
dialog_scroll: 0,
|
||||
dirty: Default::default(),
|
||||
dirty: false,
|
||||
popup: Default::default(),
|
||||
clipboard: Default::default(),
|
||||
}
|
||||
@ -188,7 +189,7 @@ impl<'ws> Workspace<'ws> {
|
||||
pub fn new_empty(locale: &str, tz: &str) -> Result<Self> {
|
||||
Ok(Self::new(
|
||||
Book::from_model(Model::new_empty("", locale, tz).map_err(|e| anyhow!("{}", e))?),
|
||||
PathBuf::default(),
|
||||
PathBuf::from_str("Untitled.xlsx").unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
@ -291,6 +292,7 @@ impl<'ws> Workspace<'ws> {
|
||||
Modality::Command => self.handle_command_input(key)?,
|
||||
Modality::Dialog => self.handle_dialog_input(key)?,
|
||||
Modality::RangeSelect => self.handle_range_select_input(key)?,
|
||||
Modality::Quit => self.handle_quit_dialog(key)?,
|
||||
};
|
||||
return Ok(result);
|
||||
}
|
||||
@ -327,6 +329,21 @@ impl<'ws> Workspace<'ws> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_quit_dialog(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => return Ok(Some(ExitCode::SUCCESS)),
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||
// We have been asked to save the file first.
|
||||
self.save_file()?;
|
||||
return Ok(Some(ExitCode::SUCCESS));
|
||||
},
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_dialog_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
@ -433,7 +450,7 @@ impl<'ws> Workspace<'ws> {
|
||||
self.book.select_sheet_by_name(name);
|
||||
Ok(None)
|
||||
}
|
||||
Ok(Some(Cmd::Quit)) => Ok(Some(ExitCode::SUCCESS)),
|
||||
Ok(Some(Cmd::Quit)) => self.quit_app(),
|
||||
Ok(Some(Cmd::ColorRows(count, color))) => {
|
||||
let row_count = count.unwrap_or(1);
|
||||
let row = self.book.location.row;
|
||||
@ -767,7 +784,7 @@ impl<'ws> Workspace<'ws> {
|
||||
})?;
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(Some(ExitCode::SUCCESS));
|
||||
return self.quit_app();
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down if key.modifiers != KeyModifiers::CONTROL => {
|
||||
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||
@ -911,6 +928,14 @@ impl<'ws> Workspace<'ws> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enter_quit_mode(&mut self) -> bool {
|
||||
if self.book.dirty {
|
||||
self.state.modality_stack.push(Modality::Quit);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn enter_command_mode(&mut self) {
|
||||
self.state.modality_stack.push(Modality::Command);
|
||||
self.state.command_state.truncate();
|
||||
@ -944,6 +969,11 @@ impl<'ws> Workspace<'ws> {
|
||||
self.text_area.move_cursor(CursorMove::End);
|
||||
}
|
||||
|
||||
fn exit_quit_mode(&mut self) -> Result<Option<ExitCode>> {
|
||||
self.state.pop_modality();
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn exit_command_mode(&mut self) -> Result<Option<ExitCode>> {
|
||||
let cmd = self.state.command_state.value().to_owned();
|
||||
self.state.command_state.blur();
|
||||
@ -997,16 +1027,24 @@ impl<'ws> Workspace<'ws> {
|
||||
self.text_area = reset_text_area(contents);
|
||||
}
|
||||
|
||||
fn save_file(&self) -> Result<()> {
|
||||
fn save_file(&mut self) -> Result<()> {
|
||||
self.book
|
||||
.save_to_xlsx(&self.name.to_string_lossy().to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_to<S: Into<String>>(&self, path: S) -> Result<()> {
|
||||
fn save_to<S: Into<String>>(&mut self, path: S) -> Result<()> {
|
||||
self.book.save_to_xlsx(path.into().as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn quit_app(&mut self) -> std::result::Result<Option<ExitCode>, anyhow::Error> {
|
||||
if self.enter_quit_mode() {
|
||||
return Ok(None);
|
||||
}
|
||||
return Ok(Some(ExitCode::SUCCESS))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn load_book(path: &PathBuf, locale: &str, tz: &str) -> Result<Book, anyhow::Error> {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use ratatui::{
|
||||
self,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
style::{Style, Stylize},
|
||||
text::Text,
|
||||
widgets::{Block, Clear, Paragraph, Widget, Wrap},
|
||||
};
|
||||
@ -9,6 +9,7 @@ use ratatui::{
|
||||
pub struct Dialog<'w> {
|
||||
content: Text<'w>,
|
||||
title: &'w str,
|
||||
bottom_title: &'w str,
|
||||
scroll: (u16, u16),
|
||||
}
|
||||
|
||||
@ -17,10 +18,15 @@ impl<'w> Dialog<'w> {
|
||||
Self {
|
||||
content,
|
||||
title,
|
||||
bottom_title: "j,k or up,down to scroll",
|
||||
scroll: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_bottom_title(mut self, title: &'w str) -> Self {
|
||||
self.bottom_title = title;
|
||||
self
|
||||
}
|
||||
pub fn scroll(mut self, line: u16) -> Self {
|
||||
self.scroll.0 = line;
|
||||
self
|
||||
|
@ -99,10 +99,12 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
||||
Self: Sized,
|
||||
{
|
||||
if self.state.modality() == &Modality::Dialog {
|
||||
// Use a popup here.
|
||||
let lines = Text::from_iter(self.state.popup.iter().cloned());
|
||||
let popup = dialog::Dialog::new(lines, "Help").scroll(self.state.dialog_scroll);
|
||||
//let popup = Paragraph::new(lines);
|
||||
popup.render(area, buf);
|
||||
} else if self.state.modality() == &Modality::Quit {
|
||||
let popup = dialog::Dialog::new(Text::raw("File is not yet saved. Save it first?"), "Quit")
|
||||
.with_bottom_title("Y/N");
|
||||
popup.render(area, buf);
|
||||
} else {
|
||||
let outer_block = Block::bordered()
|
||||
@ -118,6 +120,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
|
||||
Modality::Command => "command",
|
||||
Modality::Dialog => "",
|
||||
Modality::RangeSelect => "range-copy",
|
||||
Modality::Quit => "",
|
||||
})
|
||||
.title_bottom(
|
||||
Line::from(format!(
|
||||
|
@ -861,17 +861,6 @@ fn test_sheet_column_sizing() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quit() {
|
||||
let mut ws =
|
||||
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook");
|
||||
let result = script()
|
||||
.char('q')
|
||||
.run(&mut ws)
|
||||
.expect("Failed to run input script");
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cell_replace() {
|
||||
let mut ws = new_workspace();
|
||||
@ -1311,6 +1300,63 @@ fn test_italic_text() {
|
||||
assert!(!before_style.font.i);
|
||||
}
|
||||
|
||||
//#[test]
|
||||
//fn test_quit() {
|
||||
// let mut ws =
|
||||
// Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook");
|
||||
// let result = script()
|
||||
// .char('q')
|
||||
// .run(&mut ws)
|
||||
// .expect("Failed to run input script");
|
||||
// assert!(result.is_some());
|
||||
//}
|
||||
|
||||
#[test]
|
||||
fn test_quit_dialog() {
|
||||
let mut ws = new_workspace();
|
||||
assert!(!ws.book.dirty);
|
||||
let mut result = script()
|
||||
.char('q')
|
||||
.run(&mut ws)
|
||||
.expect("Failed to run input script");
|
||||
assert!(result.is_some());
|
||||
|
||||
script()
|
||||
.chars("efoo")
|
||||
.enter()
|
||||
.run(&mut ws)
|
||||
.expect("Failed to modify book");
|
||||
assert!(ws.book.dirty);
|
||||
result = script()
|
||||
.char('q')
|
||||
.run(&mut ws)
|
||||
.expect("Failed to run input script");
|
||||
assert!(!result.is_some());
|
||||
assert_eq!(ws.state.modality(), &Modality::Quit);
|
||||
assert!(result.is_some());
|
||||
|
||||
script()
|
||||
.char('n')
|
||||
.run(&mut ws)
|
||||
.expect("Failed to run input script");
|
||||
assert_eq!(ws.state.modality(), &Modality::default());
|
||||
assert!(ws.book.dirty);
|
||||
script()
|
||||
.char('q')
|
||||
.esc()
|
||||
.run(&mut ws)
|
||||
.expect("Failed to run input script");
|
||||
assert_eq!(ws.state.modality(), &Modality::default());
|
||||
assert!(ws.book.dirty);
|
||||
// TODO(zaphar): The below will write to disk. so commenting it out for now.
|
||||
//script()
|
||||
// .char('q')
|
||||
// .char('y')
|
||||
// .run(&mut ws)
|
||||
// .expect("Failed to run input script");
|
||||
//assert!(!ws.book.dirty);
|
||||
}
|
||||
|
||||
fn new_workspace<'a>() -> Workspace<'a> {
|
||||
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook")
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user