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 _ => {
57 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 let sheet = self.workbook.get_current_sheet();
99 for col in 1..=sheet.max_cols {
100 self.column_widths[col] = 5; }
102 self.add_notification("All columns set to minimum width".to_string());
103 } else {
104 let col = self.selected_cell.1;
106 self.column_widths[col] = 5; self.add_notification(format!("Column {} set to minimum width", col));
108 }
109 }
110 _ => {
111 if let Ok(width) = action.parse::<usize>() {
113 let col = self.selected_cell.1;
114 self.column_widths[col] = width.clamp(5, 50); 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 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 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 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 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 let col_str = parts[1].to_uppercase();
180
181 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 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 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 let export_all = cmd.starts_with("eja ") || cmd == "eja";
230
231 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 vec!["h", "1"] } else if cmd.starts_with("eja ") {
241 cmd.strip_prefix("eja ")
242 .unwrap()
243 .split_whitespace()
244 .collect()
245 } else if cmd == "eja" {
246 vec!["h", "1"] } else {
249 self.add_notification("Invalid JSON export command".to_string());
250 return;
251 };
252
253 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 let new_filepath = parent_dir.join(filename);
307
308 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; let sheet = self.workbook.get_current_sheet();
334
335 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 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
363fn parse_cell_reference(input: &str) -> Option<(usize, usize)> {
365 if input.len() < 2 {
367 return None;
368 }
369
370 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; }
382
383 let col_part = &input[0..col_end];
384 let row_part = &input[col_end..];
385
386 let col = col_name_to_index(&col_part.to_uppercase())?;
388
389 let row = row_part.parse::<usize>().ok()?;
391
392 Some((row, col))
393}