excel_cli/app/
state.rs

1use anyhow::Result;
2use std::collections::HashMap;
3use std::path::PathBuf;
4use tui_textarea::TextArea;
5
6use crate::actions::UndoHistory;
7use crate::app::VimState;
8use crate::excel::Workbook;
9
10/// Represents a cell position in a sheet, including both the selected cell and view position
11#[derive(Clone, Copy)]
12pub struct CellPosition {
13    /// The selected cell coordinates (row, column)
14    pub selected: (usize, usize),
15    /// The view position (start_row, start_col)
16    pub view: (usize, usize),
17}
18
19pub enum InputMode {
20    Normal,
21    Editing,
22    Command,
23    SearchForward,
24    SearchBackward,
25    Help,
26    LazyLoading,
27    CommandInLazyLoading,
28}
29
30pub struct AppState<'a> {
31    pub workbook: Workbook,
32    pub file_path: PathBuf,
33    pub selected_cell: (usize, usize), // (row, col)
34    pub start_row: usize,
35    pub start_col: usize,
36    pub visible_rows: usize,
37    pub visible_cols: usize,
38    pub input_mode: InputMode,
39    pub input_buffer: String,
40    pub text_area: TextArea<'a>,
41    pub should_quit: bool,
42    pub column_widths: Vec<usize>, // Store width for current sheet's columns
43    pub sheet_column_widths: HashMap<String, Vec<usize>>, // Store column widths for each sheet
44    pub sheet_cell_positions: HashMap<String, CellPosition>, // Store cell positions for each sheet
45    pub clipboard: Option<String>, // Store copied/cut cell content
46    pub g_pressed: bool,           // Track if 'g' was pressed for 'gg' command
47    pub row_number_width: usize,   // Width for displaying row numbers
48    pub search_query: String,      // Current search query
49    pub search_results: Vec<(usize, usize)>, // List of cells matching the search query
50    pub current_search_idx: Option<usize>, // Index of current search result
51    pub search_direction: bool,    // true for forward, false for backward
52    pub highlight_enabled: bool,   // Control whether search results are highlighted
53    pub info_panel_height: usize,
54    pub notification_messages: Vec<String>,
55    pub max_notifications: usize,
56    pub help_text: String,
57    pub help_scroll: usize,
58    pub help_visible_lines: usize,
59    pub undo_history: UndoHistory,
60    pub vim_state: Option<VimState>,
61}
62
63impl AppState<'_> {
64    pub fn new(workbook: Workbook, file_path: PathBuf) -> Result<Self> {
65        // Initialize default column widths for current sheet
66        let max_cols = workbook.get_current_sheet().max_cols;
67        let default_width = 15;
68        let column_widths = vec![default_width; max_cols + 1];
69
70        // Initialize column widths for all sheets
71        let mut sheet_column_widths = HashMap::with_capacity(workbook.get_sheet_names().len());
72        let mut sheet_cell_positions = HashMap::with_capacity(workbook.get_sheet_names().len());
73        let sheet_names = workbook.get_sheet_names();
74
75        for (i, name) in sheet_names.iter().enumerate() {
76            if i == workbook.get_current_sheet_index() {
77                sheet_column_widths.insert(name.clone(), column_widths.clone());
78                // Initialize current sheet position with default values
79                sheet_cell_positions.insert(
80                    name.clone(),
81                    CellPosition {
82                        selected: (1, 1),
83                        view: (1, 1),
84                    },
85                );
86            } else {
87                let sheet_max_cols = if let Some(sheet) = workbook.get_sheet_by_index(i) {
88                    sheet.max_cols
89                } else {
90                    max_cols // Fallback to current sheet's max_cols
91                };
92                sheet_column_widths.insert(name.clone(), vec![default_width; sheet_max_cols + 1]);
93                // Initialize other sheets with default positions
94                sheet_cell_positions.insert(
95                    name.clone(),
96                    CellPosition {
97                        selected: (1, 1),
98                        view: (1, 1),
99                    },
100                );
101            }
102        }
103
104        // Initialize TextArea
105        let text_area = TextArea::default();
106
107        // Calculate the width needed for row numbers based on the maximum row number
108        let max_rows = workbook.get_current_sheet().max_rows;
109        let row_number_width = if max_rows < 10 {
110            1
111        } else {
112            max_rows.to_string().len()
113        };
114        // Ensure a minimum width of 4 for row numbers
115        let row_number_width = row_number_width.max(4);
116
117        // Check if the workbook is using lazy loading and the first sheet is not loaded
118        let is_lazy_loading = workbook.is_lazy_loading() && !workbook.is_sheet_loaded(0);
119
120        // Set initial input mode based on lazy loading status
121        let initial_input_mode = if is_lazy_loading {
122            InputMode::LazyLoading
123        } else {
124            InputMode::Normal
125        };
126
127        Ok(Self {
128            workbook,
129            file_path,
130            selected_cell: (1, 1), // Excel uses 1-based indexing
131            start_row: 1,
132            start_col: 1,
133            visible_rows: 30, // Default values, will be adjusted based on window size
134            visible_cols: 15, // Default values, will be adjusted based on window size
135            input_mode: initial_input_mode,
136            input_buffer: String::new(),
137            text_area,
138            should_quit: false,
139            column_widths,
140            sheet_column_widths,
141            sheet_cell_positions,
142            clipboard: None,
143            g_pressed: false,
144            row_number_width,
145            search_query: String::new(),
146            search_results: Vec::new(),
147            current_search_idx: None,
148            search_direction: true,  // Default to forward search
149            highlight_enabled: true, // Default to showing highlights
150            info_panel_height: 10,
151            notification_messages: Vec::new(),
152            max_notifications: 5,
153            help_text: String::new(),
154            help_scroll: 0,
155            help_visible_lines: 20,
156            undo_history: UndoHistory::new(),
157            vim_state: None,
158        })
159    }
160
161    pub fn add_notification(&mut self, message: String) {
162        self.notification_messages.push(message);
163
164        if self.notification_messages.len() > self.max_notifications {
165            self.notification_messages.remove(0);
166        }
167    }
168
169    /// Updates the row number width based on the maximum row number in the current sheet
170    pub fn update_row_number_width(&mut self) {
171        let max_rows = self.workbook.get_current_sheet().max_rows;
172        let width = max_rows.to_string().len();
173        // Ensure a minimum width of 4 for row numbers
174        self.row_number_width = width.max(4);
175    }
176
177    pub fn adjust_info_panel_height(&mut self, delta: isize) {
178        let new_height = (self.info_panel_height as isize + delta).clamp(6, 16) as usize;
179        if new_height != self.info_panel_height {
180            self.info_panel_height = new_height;
181            self.add_notification(format!("Info panel height: {}", self.info_panel_height));
182        }
183    }
184
185    pub fn get_cell_content(&self, row: usize, col: usize) -> String {
186        let sheet = self.workbook.get_current_sheet();
187
188        if row < sheet.data.len() && col < sheet.data[0].len() {
189            let cell = &sheet.data[row][col];
190            if cell.is_formula {
191                let mut result = String::with_capacity(9 + cell.value.len());
192                result.push_str("Formula: ");
193                result.push_str(&cell.value);
194                result
195            } else {
196                cell.value.clone()
197            }
198        } else {
199            String::new()
200        }
201    }
202
203    pub fn get_cell_content_mut(&mut self, row: usize, col: usize) -> String {
204        self.workbook.ensure_cell_exists(row, col);
205
206        self.ensure_column_widths();
207
208        let sheet = self.workbook.get_current_sheet();
209        let cell = &sheet.data[row][col];
210
211        if cell.is_formula {
212            let mut result = String::with_capacity(9 + cell.value.len());
213            result.push_str("Formula: ");
214            result.push_str(&cell.value);
215            result
216        } else {
217            cell.value.clone()
218        }
219    }
220
221    pub fn cancel_input(&mut self) {
222        // If in help mode, just close the help window
223        if let InputMode::Help = self.input_mode {
224            self.input_mode = InputMode::Normal;
225            return;
226        }
227
228        // If in CommandInLazyLoading mode, return to LazyLoading mode
229        if let InputMode::CommandInLazyLoading = self.input_mode {
230            self.input_mode = InputMode::LazyLoading;
231            self.input_buffer = String::new();
232            self.text_area = TextArea::default();
233            return;
234        }
235
236        // Otherwise, cancel the current input
237        self.input_mode = InputMode::Normal;
238        self.input_buffer = String::new();
239        self.text_area = TextArea::default();
240    }
241
242    pub fn add_char_to_input(&mut self, c: char) {
243        self.input_buffer.push(c);
244    }
245
246    pub fn delete_char_from_input(&mut self) {
247        self.input_buffer.pop();
248    }
249
250    pub fn start_command_mode(&mut self) {
251        self.input_mode = InputMode::Command;
252        self.input_buffer = String::new();
253    }
254
255    pub fn start_command_in_lazy_loading_mode(&mut self) {
256        self.input_mode = InputMode::CommandInLazyLoading;
257        self.input_buffer = String::new();
258    }
259}