Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.0] - 2026-03-10

### Added

- Added `:addsheet <name>` to create a new sheet after the current sheet
- Added undo and redo support for sheet creation

### Fixed

- Load all lazy-loaded sheets before saving to avoid writing unloaded sheets as blank
- Count sheet name length by characters so non-ASCII names such as Chinese sheet names are validated correctly

## [0.3.0] - 2025-05-07

### Added
Expand Down Expand Up @@ -74,7 +86,8 @@ This is the initial release of excel-cli, a lightweight terminal-based Excel vie
- Copy, cut, and paste functionality with `y`, `d`, and `p` keys
- Support for pipe operator when exporting to JSON

[Unreleased]: https://github.com/fuhan666/excel-cli/compare/v0.3.0...HEAD
[Unreleased]: https://github.com/fuhan666/excel-cli/compare/v0.4.0...HEAD
[0.4.0]: https://github.com/fuhan666/excel-cli/releases/tag/v0.4.0
[0.3.0]: https://github.com/fuhan666/excel-cli/releases/tag/v0.3.0
[0.2.0]: https://github.com/fuhan666/excel-cli/releases/tag/v0.2.0
[0.1.1]: https://github.com/fuhan666/excel-cli/releases/tag/v0.1.1
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ English | [中文](README_zh.md)
## Features

- Browse and navigate Excel worksheets with Vim-like hotkeys
- Switch between sheets in multi-sheet workbooks
- Create, switch, and delete sheets in multi-sheet workbooks
- Edit cell contents directly in the terminal
- Export data to JSON format
- Delete rows, columns, and sheets
- Delete rows and columns
- Search functionality with highlighting
- Command mode for advanced operations

Expand Down Expand Up @@ -105,7 +105,7 @@ The application has a simple and intuitive interface:
- `y`: Copy current cell content
- `d`: Cut current cell content
- `p`: Paste clipboard content to current cell
- `u`: Undo the last operation (edit, row/column/sheet deletion)
- `u`: Undo the last operation (edit, row/column changes, sheet creation/deletion)
- `Ctrl+r`: Redo the last undone operation
- `/`: Start forward search
- `?`: Start backward search
Expand Down Expand Up @@ -218,6 +218,7 @@ The JSON files are saved in the same directory as the original Excel file.

### Sheet Management Commands

- `:addsheet [name]` - Add a new sheet after the current sheet
- `:sheet [name/number]` - Switch to sheet by name or index (1-based)
- `:delsheet` - Delete the current sheet

Expand All @@ -241,6 +242,7 @@ Excel-CLI uses a non-destructive approach to file saving:

- When you save a file (using `:w`, `:wq`, or `:x`), the application checks if any changes have been made
- If no changes have been made, no new file is created, and a "No changes to save" message is displayed
- If lazy loading is enabled, all unloaded sheets are loaded before saving so the workbook content is preserved
- If changes have been made, a new file is created with a timestamp in the filename, following the format `original_filename_YYYYMMDD_HHMMSS.xlsx`
- The new file is created without any styling
- The original file is never modified
Expand Down
8 changes: 5 additions & 3 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
## 功能特点

- 使用类 Vim 快捷键浏览 Excel 工作表
- 在多工作表的 Excel 文件中切换工作表
- 在多工作表的 Excel 文件中创建、切换和删除工作表
- 直接在终端中编辑单元格内容
- 将数据导出为 JSON 格式
- 删除行、列和工作表
- 删除行和列
- 搜索并高亮显示结果
- 支持高级操作的命令模式

Expand Down Expand Up @@ -105,7 +105,7 @@ excel-cli path/to/your/file.xlsx -j > data.json # (示例)将JSON输出保
- `y`:复制当前单元格内容
- `d`:剪切当前单元格内容
- `p`:将剪贴板内容粘贴到当前单元格
- `u`:撤销上一次操作(编辑、行/列/工作表删除
- `u`:撤销上一次操作(编辑、行列变更、工作表创建/删除
- `Ctrl+r`:重做上一次被撤销的操作
- `/`:开始向前搜索
- `?`:开始向后搜索
Expand Down Expand Up @@ -218,6 +218,7 @@ JSON 文件保存在原始 Excel 文件所在的目录中。

### 工作表管理命令

- `:addsheet [名称]` - 在当前工作表后新增一个工作表
- `:sheet [名称/编号]` - 按名称或索引切换工作表(基于 1 的索引)
- `:delsheet` - 删除当前工作表

Expand All @@ -241,6 +242,7 @@ Excel-CLI 使用非破坏性的文件保存方法:

- 当您保存文件(使用`:w`,`:wq`或`:x`)时,应用程序会检查是否进行了任何更改
- 如果没有进行更改,则不会创建新文件,并显示"No changes to save"消息
- 如果启用了懒加载,保存前会先加载尚未读取的工作表,以保留完整工作簿内容
- 如果进行了更改,则会创建一个文件名中带有时间戳的新文件,格式为`original_filename_YYYYMMDD_HHMMSS.xlsx`
- 创建的新文件不带任何样式
- 原始文件永远不会被修改
Expand Down
5 changes: 3 additions & 2 deletions src/actions/command.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::types::{ActionCommand, ActionType};
use super::types::ActionCommand;
use super::{ActionType, Command};

impl ActionCommand {
// Returns the action type of this command
Expand All @@ -13,7 +14,7 @@ impl ActionCommand {
ActionCommand::MultiRow(_) => ActionType::DeleteMultiRows,
ActionCommand::Column(_) => ActionType::DeleteColumn,
ActionCommand::MultiColumn(_) => ActionType::DeleteMultiColumns,
ActionCommand::Sheet(_) => ActionType::DeleteSheet,
ActionCommand::Sheet(action) => action.action_type(),
}
}
}
2 changes: 1 addition & 1 deletion src/actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ pub use cell::CellAction;
pub use column::{ColumnAction, MultiColumnAction};
pub use history::UndoHistory;
pub use row::{MultiRowAction, RowAction};
pub use sheet::SheetAction;
pub use sheet::{SheetAction, SheetOperation};
pub use types::{ActionCommand, ActionExecutor, ActionType, Command};
12 changes: 11 additions & 1 deletion src/actions/sheet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ use super::{ActionType, Command};
use crate::excel::Sheet;
use anyhow::Result;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SheetOperation {
Create,
Delete,
}

#[derive(Clone)]
pub struct SheetAction {
pub sheet_index: usize,
pub sheet_name: String,
pub sheet_data: Sheet,
pub column_widths: Vec<usize>,
pub operation: SheetOperation,
}

impl Command for SheetAction {
Expand All @@ -20,6 +27,9 @@ impl Command for SheetAction {
}

fn action_type(&self) -> ActionType {
ActionType::DeleteSheet
match self.operation {
SheetOperation::Create => ActionType::CreateSheet,
SheetOperation::Delete => ActionType::DeleteSheet,
}
}
}
1 change: 1 addition & 0 deletions src/actions/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub enum ActionType {
Edit,
Cut,
Paste,
CreateSheet,
DeleteRow,
DeleteColumn,
DeleteSheet,
Expand Down
81 changes: 81 additions & 0 deletions src/app/sheet.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::actions::{
ActionCommand, ColumnAction, MultiColumnAction, MultiRowAction, RowAction, SheetAction,
SheetOperation,
};
use crate::app::AppState;
use crate::utils::index_to_col_name;
Expand Down Expand Up @@ -150,6 +151,56 @@ impl AppState<'_> {
self.add_notification(format!("Sheet '{name_or_index}' not found"));
}

pub fn create_sheet(&mut self, name: &str) {
let insert_index = self.workbook.get_current_sheet_index() + 1;

match self.workbook.add_sheet(name, insert_index) {
Ok(sheet_name) => {
let default_width = 15;
let max_cols = self
.workbook
.get_sheet_by_index(insert_index)
.map(|sheet| sheet.max_cols)
.unwrap_or(1);

self.sheet_column_widths
.insert(sheet_name.clone(), vec![default_width; max_cols + 1]);
self.sheet_cell_positions.insert(
sheet_name.clone(),
crate::app::CellPosition {
selected: (1, 1),
view: (1, 1),
},
);

if let Err(e) = self.switch_sheet_by_index(insert_index) {
self.sheet_column_widths.remove(&sheet_name);
self.sheet_cell_positions.remove(&sheet_name);
let _ = self.workbook.delete_sheet_at_index(insert_index);
self.add_notification(format!("Failed to switch to new sheet: {e}"));
return;
}

self.notification_messages.pop();

let sheet_action = SheetAction {
sheet_index: insert_index,
sheet_name: sheet_name.clone(),
sheet_data: self.workbook.get_current_sheet().clone(),
column_widths: self.column_widths.clone(),
operation: SheetOperation::Create,
};

self.undo_history.push(ActionCommand::Sheet(sheet_action));
self.input_mode = crate::app::InputMode::Normal;
self.add_notification(format!("Created sheet: {sheet_name}"));
}
Err(e) => {
self.add_notification(format!("Failed to add sheet: {e}"));
}
}
}

pub fn delete_current_sheet(&mut self) {
let current_sheet_name = self.workbook.get_current_sheet_name();
let sheet_index = self.workbook.get_current_sheet_index();
Expand All @@ -166,6 +217,7 @@ impl AppState<'_> {
sheet_name: current_sheet_name.clone(),
sheet_data,
column_widths,
operation: SheetOperation::Delete,
};

self.undo_history.push(ActionCommand::Sheet(sheet_action));
Expand Down Expand Up @@ -210,6 +262,7 @@ impl AppState<'_> {
// Clear search results as they're specific to the previous sheet
self.search_results.clear();
self.current_search_idx = None;
self.update_row_number_width();

// Check if the new current sheet is loaded when using lazy loading
if self.workbook.is_lazy_loading() && !is_new_sheet_loaded {
Expand Down Expand Up @@ -722,3 +775,31 @@ impl AppState<'_> {
}
}
}

#[cfg(test)]
mod tests {
use crate::app::AppState;
use crate::excel::{Sheet, Workbook};
use std::path::PathBuf;

#[test]
fn create_sheet_can_be_undone_and_redone() {
let workbook = Workbook::from_sheets_for_test(vec![Sheet::blank("Sheet1".to_string())]);
let mut app = AppState::new(workbook, PathBuf::from("test.xlsx")).unwrap();

app.create_sheet("Report");
assert_eq!(app.workbook.get_sheet_names(), vec!["Sheet1", "Report"]);
assert_eq!(app.workbook.get_current_sheet_name(), "Report");
assert!(app.workbook.is_modified());

app.undo().unwrap();
assert_eq!(app.workbook.get_sheet_names(), vec!["Sheet1"]);
assert_eq!(app.workbook.get_current_sheet_name(), "Sheet1");
assert!(!app.workbook.is_modified());

app.redo().unwrap();
assert_eq!(app.workbook.get_sheet_names(), vec!["Sheet1", "Report"]);
assert_eq!(app.workbook.get_current_sheet_name(), "Report");
assert!(app.workbook.is_modified());
}
}
1 change: 1 addition & 0 deletions src/app/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ impl AppState<'_> {
h=horizontal (default), v=vertical\n\
[rows]=number of header rows (default: 1)\n\n\
SHEET OPERATIONS:\n\
:addsheet [name] - Add a new sheet after the current sheet\n\
:delsheet - Delete the current sheet\n\n\
UI ADJUSTMENTS:\n\
+/= - Increase info panel height\n\
Expand Down
Loading