excel_cli/commands/
executor.rs1use std::path::Path;
2
3use crate::app::AppState;
4use crate::json_export::{export_all_sheets_json, export_json, HeaderDirection};
5use crate::utils::col_name_to_index;
6
7impl AppState<'_> {
8 pub fn execute_command(&mut self) {
9 let command = self.input_buffer.clone();
10 self.input_mode = crate::app::InputMode::Normal;
11 self.input_buffer = String::new();
12
13 if command.is_empty() {
14 return;
15 }
16
17 if let Some(cell_ref) = parse_cell_reference(&command) {
19 self.jump_to_cell(cell_ref);
20 return;
21 }
22
23 match command.as_str() {
25 "w" => {
26 if let Err(e) = self.save() {
27 self.add_notification(format!("Save failed: {e}"));
28 }
29 }
30 "wq" | "x" => self.save_and_exit(),
31 "q" => {
32 if self.workbook.is_modified() {
33 self.add_notification(
34 "File has unsaved changes. Use :q! to force quit or :wq to save and quit."
35 .to_string(),
36 );
37 } else {
38 self.should_quit = true;
39 }
40 }
41 "q!" => self.exit_without_saving(),
42 "y" => self.copy_cell(),
43 "d" => {
44 if let Err(e) = self.cut_cell() {
45 self.add_notification(format!("Cut failed: {e}"));
46 }
47 }
48 "put" | "pu" => {
49 if let Err(e) = self.paste_cell() {
50 self.add_notification(format!("Paste failed: {e}"));
51 }
52 }
53 "nohlsearch" | "noh" => self.disable_search_highlight(),
54 "help" => self.show_help(),
55 "delsheet" => self.delete_current_sheet(),
56 "addsheet" => self.add_notification("Usage: :addsheet <name>".to_string()),
57 _ => {
58 if command.starts_with("cw ") {
60 self.handle_column_width_command(&command);
61 } else if command.starts_with("ej") {
62 self.handle_json_export_command(&command);
63 } else if let Some(sheet_name) = command.strip_prefix("addsheet ") {
64 self.create_sheet(sheet_name.trim());
65 } else if command.starts_with("sheet ") {
66 let sheet_name = command.strip_prefix("sheet ").unwrap().trim();
67 self.switch_to_sheet(sheet_name);
68 } else if command.starts_with("dr") {
69 self.handle_delete_row_command(&command);
70 } else if command.starts_with("dc") {
71 self.handle_delete_column_command(&command);
72 } else {
73 self.add_notification(format!("Unknown command: {}", command));
74 }
75 }
76 }
77 }
78
79 fn handle_column_width_command(&mut self, cmd: &str) {
80 let parts: Vec<&str> = cmd.split_whitespace().collect();
81
82 if parts.len() < 2 {
83 self.add_notification("Usage: :cw [fit|min|number] [all]".to_string());
84 return;
85 }
86
87 let action = parts[1];
88 let apply_to_all = parts.len() > 2 && parts[2] == "all";
89
90 match action {
91 "fit" => {
92 if apply_to_all {
93 self.auto_adjust_column_width(None);
94 } else {
95 self.auto_adjust_column_width(Some(self.selected_cell.1));
96 }
97 }
98 "min" => {
99 if apply_to_all {
100 let sheet = self.workbook.get_current_sheet();
102 for col in 1..=sheet.max_cols {
103 self.column_widths[col] = 5; }
105 self.add_notification("All columns set to minimum width".to_string());
106 } else {
107 let col = self.selected_cell.1;
109 self.column_widths[col] = 5; self.add_notification(format!("Column {} set to minimum width", col));
111 }
112 }
113 _ => {
114 if let Ok(width) = action.parse::<usize>() {
116 let col = self.selected_cell.1;
117 self.column_widths[col] = width.clamp(5, 50); self.add_notification(format!("Column {} width set to {}", col, width));
119 } else {
120 self.add_notification(format!("Invalid column width: {}", action));
121 }
122 }
123 }
124 }
125
126 fn handle_delete_row_command(&mut self, cmd: &str) {
127 let parts: Vec<&str> = cmd.split_whitespace().collect();
128
129 if parts.len() == 1 {
130 if let Err(e) = self.delete_current_row() {
132 self.add_notification(format!("Failed to delete row: {e}"));
133 }
134 return;
135 }
136
137 if parts.len() == 2 {
138 if let Ok(row) = parts[1].parse::<usize>() {
140 if let Err(e) = self.delete_row(row) {
141 self.add_notification(format!("Failed to delete row {}: {}", row, e));
142 }
143 } else {
144 self.add_notification(format!("Invalid row number: {}", parts[1]));
145 }
146 return;
147 }
148
149 if parts.len() == 3 {
150 if let (Ok(start_row), Ok(end_row)) =
152 (parts[1].parse::<usize>(), parts[2].parse::<usize>())
153 {
154 if let Err(e) = self.delete_rows(start_row, end_row) {
155 self.add_notification(format!(
156 "Failed to delete rows {} to {}: {}",
157 start_row, end_row, e
158 ));
159 }
160 } else {
161 self.add_notification("Invalid row range".to_string());
162 }
163 return;
164 }
165
166 self.add_notification("Usage: :dr [row] [end_row]".to_string());
167 }
168
169 fn handle_delete_column_command(&mut self, cmd: &str) {
170 let parts: Vec<&str> = cmd.split_whitespace().collect();
171
172 if parts.len() == 1 {
173 if let Err(e) = self.delete_current_column() {
175 self.add_notification(format!("Failed to delete column: {e}"));
176 }
177 return;
178 }
179
180 if parts.len() == 2 {
181 let col_str = parts[1].to_uppercase();
183
184 if let Some(col) = col_name_to_index(&col_str) {
186 if let Err(e) = self.delete_column(col) {
187 self.add_notification(format!("Failed to delete column {}: {}", col_str, e));
188 }
189 return;
190 }
191
192 if let Ok(col) = col_str.parse::<usize>() {
194 if let Err(e) = self.delete_column(col) {
195 self.add_notification(format!("Failed to delete column {}: {}", col, e));
196 }
197 return;
198 }
199
200 self.add_notification(format!("Invalid column: {}", col_str));
201 return;
202 }
203
204 if parts.len() == 3 {
205 let start_col_str = parts[1].to_uppercase();
207 let end_col_str = parts[2].to_uppercase();
208
209 let start_col =
210 col_name_to_index(&start_col_str).or_else(|| start_col_str.parse::<usize>().ok());
211 let end_col =
212 col_name_to_index(&end_col_str).or_else(|| end_col_str.parse::<usize>().ok());
213
214 if let (Some(start), Some(end)) = (start_col, end_col) {
215 if let Err(e) = self.delete_columns(start, end) {
216 self.add_notification(format!(
217 "Failed to delete columns {} to {}: {}",
218 start_col_str, end_col_str, e
219 ));
220 }
221 } else {
222 self.add_notification("Invalid column range".to_string());
223 }
224 return;
225 }
226
227 self.add_notification("Usage: :dc [col] [end_col]".to_string());
228 }
229
230 fn handle_json_export_command(&mut self, cmd: &str) {
231 let export_all = cmd.starts_with("eja ") || cmd == "eja";
233
234 let parts: Vec<&str> = if cmd.starts_with("ej ") {
236 cmd.strip_prefix("ej ")
237 .unwrap()
238 .split_whitespace()
239 .collect()
240 } else if cmd == "ej" {
241 vec!["h", "1"] } else if cmd.starts_with("eja ") {
244 cmd.strip_prefix("eja ")
245 .unwrap()
246 .split_whitespace()
247 .collect()
248 } else if cmd == "eja" {
249 vec!["h", "1"] } else {
252 self.add_notification("Invalid JSON export command".to_string());
253 return;
254 };
255
256 if parts.len() < 2 {
258 if export_all {
259 self.add_notification("Usage: :eja [h|v] [rows]".to_string());
260 } else {
261 self.add_notification("Usage: :ej [h|v] [rows]".to_string());
262 }
263 return;
264 }
265
266 let direction_str = parts[0];
267 let header_count_str = parts[1];
268
269 let direction = match direction_str.parse::<HeaderDirection>() {
270 Ok(dir) => dir,
271 Err(_) => {
272 self.add_notification(format!(
273 "Invalid header direction: {}. Use 'h' or 'v'",
274 direction_str
275 ));
276 return;
277 }
278 };
279
280 let header_count = match header_count_str.parse::<usize>() {
281 Ok(count) => count,
282 Err(_) => {
283 self.add_notification(format!("Invalid header count: {}", header_count_str));
284 return;
285 }
286 };
287
288 let sheet_name = self.workbook.get_current_sheet_name();
289
290 let file_path = self.workbook.get_file_path().to_string();
291 let original_file = Path::new(&file_path);
292 let file_stem = original_file
293 .file_stem()
294 .and_then(|s| s.to_str())
295 .unwrap_or("export");
296
297 let parent_dir = original_file.parent().unwrap_or_else(|| Path::new(""));
298
299 let now = chrono::Local::now();
300 let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
301
302 let filename = if export_all {
303 format!("{}_all_sheets_{}.json", file_stem, timestamp)
304 } else {
305 format!("{}_sheet_{}_{}.json", file_stem, sheet_name, timestamp)
306 };
307
308 let new_filepath = parent_dir.join(filename);
310
311 let result = if export_all {
313 export_all_sheets_json(&self.workbook, direction, header_count, &new_filepath)
314 } else {
315 export_json(
316 self.workbook.get_current_sheet(),
317 direction,
318 header_count,
319 &new_filepath,
320 )
321 };
322
323 match result {
324 Ok(_) => {
325 self.add_notification(format!("Exported to {}", new_filepath.display()));
326 }
327 Err(e) => {
328 self.add_notification(format!("Export failed: {e}"));
329 }
330 }
331 }
332
333 fn jump_to_cell(&mut self, cell_ref: (usize, usize)) {
334 let (row, col) = cell_ref; let sheet = self.workbook.get_current_sheet();
337
338 if row > sheet.max_rows || col > sheet.max_cols {
340 self.add_notification(format!(
341 "Cell reference out of range: {}{}",
342 crate::utils::index_to_col_name(col),
343 row
344 ));
345 return;
346 }
347
348 self.selected_cell = (row, col);
349 if self.selected_cell.0 < self.start_row {
351 self.start_row = self.selected_cell.0;
352 } else if self.selected_cell.0 >= self.start_row + self.visible_rows {
353 self.start_row = self.selected_cell.0 - self.visible_rows + 1;
354 }
355
356 self.ensure_column_visible(self.selected_cell.1);
357
358 self.add_notification(format!(
359 "Jumped to cell {}{}",
360 crate::utils::index_to_col_name(col),
361 row
362 ));
363 }
364}
365
366fn parse_cell_reference(input: &str) -> Option<(usize, usize)> {
368 if input.chars().count() < 2 {
370 return None;
371 }
372
373 let col_end = input
375 .char_indices()
376 .find(|(_, c)| c.is_ascii_digit())
377 .map(|(index, _)| index)?;
378
379 if col_end == 0 {
380 return None; }
382
383 let (col_part, row_part) = input.split_at(col_end);
384
385 let col = col_name_to_index(&col_part.to_uppercase())?;
387
388 let row = row_part.parse::<usize>().ok()?;
390
391 Some((row, col))
392}
393
394#[cfg(test)]
395mod tests {
396 use super::parse_cell_reference;
397
398 #[test]
399 fn parses_valid_cell_references() {
400 assert_eq!(parse_cell_reference("A1"), Some((1, 1)));
401 assert_eq!(parse_cell_reference("BC12"), Some((12, 55)));
402 }
403
404 #[test]
405 fn ignores_commands_with_non_ascii_arguments() {
406 assert_eq!(parse_cell_reference("addsheet 测试1"), None);
407 assert_eq!(parse_cell_reference("测试1"), None);
408 }
409}