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            _ => {
57                // Handle commands with parameters
58                if command.starts_with("cw ") {
59                    self.handle_column_width_command(&command);
60                } else if command.starts_with("ej") {
61                    self.handle_json_export_command(&command);
62                } else if command.starts_with("sheet ") {
63                    let sheet_name = command.strip_prefix("sheet ").unwrap().trim();
64                    self.switch_to_sheet(sheet_name);
65                } else if command.starts_with("dr") {
66                    self.handle_delete_row_command(&command);
67                } else if command.starts_with("dc") {
68                    self.handle_delete_column_command(&command);
69                } else {
70                    self.add_notification(format!("Unknown command: {}", command));
71                }
72            }
73        }
74    }
75
76    fn handle_column_width_command(&mut self, cmd: &str) {
77        let parts: Vec<&str> = cmd.split_whitespace().collect();
78
79        if parts.len() < 2 {
80            self.add_notification("Usage: :cw [fit|min|number] [all]".to_string());
81            return;
82        }
83
84        let action = parts[1];
85        let apply_to_all = parts.len() > 2 && parts[2] == "all";
86
87        match action {
88            "fit" => {
89                if apply_to_all {
90                    self.auto_adjust_column_width(None);
91                } else {
92                    self.auto_adjust_column_width(Some(self.selected_cell.1));
93                }
94            }
95            "min" => {
96                if apply_to_all {
97                    // Set all columns to minimum width
98                    let sheet = self.workbook.get_current_sheet();
99                    for col in 1..=sheet.max_cols {
100                        self.column_widths[col] = 5; // Minimum width
101                    }
102                    self.add_notification("All columns set to minimum width".to_string());
103                } else {
104                    // Set current column to minimum width
105                    let col = self.selected_cell.1;
106                    self.column_widths[col] = 5; // Minimum width
107                    self.add_notification(format!("Column {} set to minimum width", col));
108                }
109            }
110            _ => {
111                // Try to parse as a number
112                if let Ok(width) = action.parse::<usize>() {
113                    let col = self.selected_cell.1;
114                    self.column_widths[col] = width.clamp(5, 50); // Clamp between 5 and 50
115                    self.add_notification(format!("Column {} width set to {}", col, width));
116                } else {
117                    self.add_notification(format!("Invalid column width: {}", action));
118                }
119            }
120        }
121    }
122
123    fn handle_delete_row_command(&mut self, cmd: &str) {
124        let parts: Vec<&str> = cmd.split_whitespace().collect();
125
126        if parts.len() == 1 {
127            // Delete current row
128            if let Err(e) = self.delete_current_row() {
129                self.add_notification(format!("Failed to delete row: {e}"));
130            }
131            return;
132        }
133
134        if parts.len() == 2 {
135            // Delete specific row
136            if let Ok(row) = parts[1].parse::<usize>() {
137                if let Err(e) = self.delete_row(row) {
138                    self.add_notification(format!("Failed to delete row {}: {}", row, e));
139                }
140            } else {
141                self.add_notification(format!("Invalid row number: {}", parts[1]));
142            }
143            return;
144        }
145
146        if parts.len() == 3 {
147            // Delete range of rows
148            if let (Ok(start_row), Ok(end_row)) =
149                (parts[1].parse::<usize>(), parts[2].parse::<usize>())
150            {
151                if let Err(e) = self.delete_rows(start_row, end_row) {
152                    self.add_notification(format!(
153                        "Failed to delete rows {} to {}: {}",
154                        start_row, end_row, e
155                    ));
156                }
157            } else {
158                self.add_notification("Invalid row range".to_string());
159            }
160            return;
161        }
162
163        self.add_notification("Usage: :dr [row] [end_row]".to_string());
164    }
165
166    fn handle_delete_column_command(&mut self, cmd: &str) {
167        let parts: Vec<&str> = cmd.split_whitespace().collect();
168
169        if parts.len() == 1 {
170            // Delete current column
171            if let Err(e) = self.delete_current_column() {
172                self.add_notification(format!("Failed to delete column: {e}"));
173            }
174            return;
175        }
176
177        if parts.len() == 2 {
178            // Delete specific column
179            let col_str = parts[1].to_uppercase();
180
181            // Try to parse as a column letter (A, B, C, etc.)
182            if let Some(col) = col_name_to_index(&col_str) {
183                if let Err(e) = self.delete_column(col) {
184                    self.add_notification(format!("Failed to delete column {}: {}", col_str, e));
185                }
186                return;
187            }
188
189            // Try to parse as a column number
190            if let Ok(col) = col_str.parse::<usize>() {
191                if let Err(e) = self.delete_column(col) {
192                    self.add_notification(format!("Failed to delete column {}: {}", col, e));
193                }
194                return;
195            }
196
197            self.add_notification(format!("Invalid column: {}", col_str));
198            return;
199        }
200
201        if parts.len() == 3 {
202            // Delete range of columns
203            let start_col_str = parts[1].to_uppercase();
204            let end_col_str = parts[2].to_uppercase();
205
206            let start_col =
207                col_name_to_index(&start_col_str).or_else(|| start_col_str.parse::<usize>().ok());
208            let end_col =
209                col_name_to_index(&end_col_str).or_else(|| end_col_str.parse::<usize>().ok());
210
211            if let (Some(start), Some(end)) = (start_col, end_col) {
212                if let Err(e) = self.delete_columns(start, end) {
213                    self.add_notification(format!(
214                        "Failed to delete columns {} to {}: {}",
215                        start_col_str, end_col_str, e
216                    ));
217                }
218            } else {
219                self.add_notification("Invalid column range".to_string());
220            }
221            return;
222        }
223
224        self.add_notification("Usage: :dc [col] [end_col]".to_string());
225    }
226
227    fn handle_json_export_command(&mut self, cmd: &str) {
228        // Check if this is an export all command
229        let export_all = cmd.starts_with("eja ") || cmd == "eja";
230
231        // Parse command
232        let parts: Vec<&str> = if cmd.starts_with("ej ") {
233            cmd.strip_prefix("ej ")
234                .unwrap()
235                .split_whitespace()
236                .collect()
237        } else if cmd == "ej" {
238            // No arguments provided, use default values
239            vec!["h", "1"] // Default to horizontal headers with 1 header row
240        } else if cmd.starts_with("eja ") {
241            cmd.strip_prefix("eja ")
242                .unwrap()
243                .split_whitespace()
244                .collect()
245        } else if cmd == "eja" {
246            // No arguments provided, use default values
247            vec!["h", "1"] // Default to horizontal headers with 1 header row
248        } else {
249            self.add_notification("Invalid JSON export command".to_string());
250            return;
251        };
252
253        // Check if we have enough arguments for direction and header count
254        if parts.len() < 2 {
255            if export_all {
256                self.add_notification("Usage: :eja [h|v] [rows]".to_string());
257            } else {
258                self.add_notification("Usage: :ej [h|v] [rows]".to_string());
259            }
260            return;
261        }
262
263        let direction_str = parts[0];
264        let header_count_str = parts[1];
265
266        let direction = match direction_str.parse::<HeaderDirection>() {
267            Ok(dir) => dir,
268            Err(_) => {
269                self.add_notification(format!(
270                    "Invalid header direction: {}. Use 'h' or 'v'",
271                    direction_str
272                ));
273                return;
274            }
275        };
276
277        let header_count = match header_count_str.parse::<usize>() {
278            Ok(count) => count,
279            Err(_) => {
280                self.add_notification(format!("Invalid header count: {}", header_count_str));
281                return;
282            }
283        };
284
285        let sheet_name = self.workbook.get_current_sheet_name();
286
287        let file_path = self.workbook.get_file_path().to_string();
288        let original_file = Path::new(&file_path);
289        let file_stem = original_file
290            .file_stem()
291            .and_then(|s| s.to_str())
292            .unwrap_or("export");
293
294        let parent_dir = original_file.parent().unwrap_or_else(|| Path::new(""));
295
296        let now = chrono::Local::now();
297        let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
298
299        let filename = if export_all {
300            format!("{}_all_sheets_{}.json", file_stem, timestamp)
301        } else {
302            format!("{}_sheet_{}_{}.json", file_stem, sheet_name, timestamp)
303        };
304
305        // Create the full path in the same directory as the original Excel file
306        let new_filepath = parent_dir.join(filename);
307
308        // Export to JSON
309        let result = if export_all {
310            export_all_sheets_json(&self.workbook, direction, header_count, &new_filepath)
311        } else {
312            export_json(
313                self.workbook.get_current_sheet(),
314                direction,
315                header_count,
316                &new_filepath,
317            )
318        };
319
320        match result {
321            Ok(_) => {
322                self.add_notification(format!("Exported to {}", new_filepath.display()));
323            }
324            Err(e) => {
325                self.add_notification(format!("Export failed: {e}"));
326            }
327        }
328    }
329
330    fn jump_to_cell(&mut self, cell_ref: (usize, usize)) {
331        let (row, col) = cell_ref; // Fixed: cell_ref is already (row, col)
332
333        let sheet = self.workbook.get_current_sheet();
334
335        // Validate row and column
336        if row > sheet.max_rows || col > sheet.max_cols {
337            self.add_notification(format!(
338                "Cell reference out of range: {}{}",
339                crate::utils::index_to_col_name(col),
340                row
341            ));
342            return;
343        }
344
345        self.selected_cell = (row, col);
346        // Handle scrolling
347        if self.selected_cell.0 < self.start_row {
348            self.start_row = self.selected_cell.0;
349        } else if self.selected_cell.0 >= self.start_row + self.visible_rows {
350            self.start_row = self.selected_cell.0 - self.visible_rows + 1;
351        }
352
353        self.ensure_column_visible(self.selected_cell.1);
354
355        self.add_notification(format!(
356            "Jumped to cell {}{}",
357            crate::utils::index_to_col_name(col),
358            row
359        ));
360    }
361}
362
363// Parse a cell reference like "A1", "B10", etc.
364fn parse_cell_reference(input: &str) -> Option<(usize, usize)> {
365    // Cell references should have at least 2 characters (e.g., A1)
366    if input.len() < 2 {
367        return None;
368    }
369
370    // Find the first digit to separate column and row parts
371    let mut col_end = 0;
372    for (i, c) in input.chars().enumerate() {
373        if c.is_ascii_digit() {
374            col_end = i;
375            break;
376        }
377    }
378
379    if col_end == 0 {
380        return None; // No digits found
381    }
382
383    let col_part = &input[0..col_end];
384    let row_part = &input[col_end..];
385
386    // Convert column letters to index
387    let col = col_name_to_index(&col_part.to_uppercase())?;
388
389    // Parse row number
390    let row = row_part.parse::<usize>().ok()?;
391
392    Some((row, col))
393}