Skip to main content

excel_cli/commands/
executor.rs

1use 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        // Handle cell navigation (e.g., :A1, :B10)
18        if let Some(cell_ref) = parse_cell_reference(&command) {
19            self.jump_to_cell(cell_ref);
20            return;
21        }
22
23        // Handle commands
24        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                // Handle commands with parameters
59                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                    // Set all columns to minimum width
101                    let sheet = self.workbook.get_current_sheet();
102                    for col in 1..=sheet.max_cols {
103                        self.column_widths[col] = 5; // Minimum width
104                    }
105                    self.add_notification("All columns set to minimum width".to_string());
106                } else {
107                    // Set current column to minimum width
108                    let col = self.selected_cell.1;
109                    self.column_widths[col] = 5; // Minimum width
110                    self.add_notification(format!("Column {} set to minimum width", col));
111                }
112            }
113            _ => {
114                // Try to parse as a number
115                if let Ok(width) = action.parse::<usize>() {
116                    let col = self.selected_cell.1;
117                    self.column_widths[col] = width.clamp(5, 50); // Clamp between 5 and 50
118                    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            // Delete current row
131            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            // Delete specific row
139            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            // Delete range of rows
151            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            // Delete current column
174            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            // Delete specific column
182            let col_str = parts[1].to_uppercase();
183
184            // Try to parse as a column letter (A, B, C, etc.)
185            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            // Try to parse as a column number
193            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            // Delete range of columns
206            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        // Check if this is an export all command
232        let export_all = cmd.starts_with("eja ") || cmd == "eja";
233
234        // Parse command
235        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            // No arguments provided, use default values
242            vec!["h", "1"] // Default to horizontal headers with 1 header row
243        } else if cmd.starts_with("eja ") {
244            cmd.strip_prefix("eja ")
245                .unwrap()
246                .split_whitespace()
247                .collect()
248        } else if cmd == "eja" {
249            // No arguments provided, use default values
250            vec!["h", "1"] // Default to horizontal headers with 1 header row
251        } else {
252            self.add_notification("Invalid JSON export command".to_string());
253            return;
254        };
255
256        // Check if we have enough arguments for direction and header count
257        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        // Create the full path in the same directory as the original Excel file
309        let new_filepath = parent_dir.join(filename);
310
311        // Export to JSON
312        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; // Fixed: cell_ref is already (row, col)
335
336        let sheet = self.workbook.get_current_sheet();
337
338        // Validate row and column
339        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        // Handle scrolling
350        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
366// Parse a cell reference like "A1", "B10", etc.
367fn parse_cell_reference(input: &str) -> Option<(usize, usize)> {
368    // Cell references should have at least 2 characters (e.g., A1)
369    if input.chars().count() < 2 {
370        return None;
371    }
372
373    // Find the first digit to separate column and row parts
374    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; // No digits found
381    }
382
383    let (col_part, row_part) = input.split_at(col_end);
384
385    // Convert column letters to index
386    let col = col_name_to_index(&col_part.to_uppercase())?;
387
388    // Parse row number
389    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}