Skip to main content

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, EXCEL_MAX_COLS, EXCEL_MAX_ROWS};
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 help_total_lines: usize,
60    pub undo_history: UndoHistory,
61    pub vim_state: Option<VimState>,
62}
63
64impl AppState<'_> {
65    pub fn new(workbook: Workbook, file_path: PathBuf) -> Result<Self> {
66        // Initialize default column widths for current sheet
67        let max_cols = workbook.get_current_sheet().max_cols;
68        let default_width = 15;
69        let column_widths = vec![default_width; max_cols + 1];
70
71        // Initialize column widths for all sheets
72        let mut sheet_column_widths = HashMap::with_capacity(workbook.get_sheet_names().len());
73        let mut sheet_cell_positions = HashMap::with_capacity(workbook.get_sheet_names().len());
74        let sheet_names = workbook.get_sheet_names();
75
76        for (i, name) in sheet_names.iter().enumerate() {
77            if i == workbook.get_current_sheet_index() {
78                sheet_column_widths.insert(name.clone(), column_widths.clone());
79                // Initialize current sheet position with default values
80                sheet_cell_positions.insert(
81                    name.clone(),
82                    CellPosition {
83                        selected: (1, 1),
84                        view: (1, 1),
85                    },
86                );
87            } else {
88                let sheet_max_cols = if let Some(sheet) = workbook.get_sheet_by_index(i) {
89                    sheet.max_cols
90                } else {
91                    max_cols // Fallback to current sheet's max_cols
92                };
93                sheet_column_widths.insert(name.clone(), vec![default_width; sheet_max_cols + 1]);
94                // Initialize other sheets with default positions
95                sheet_cell_positions.insert(
96                    name.clone(),
97                    CellPosition {
98                        selected: (1, 1),
99                        view: (1, 1),
100                    },
101                );
102            }
103        }
104
105        // Initialize TextArea
106        let text_area = TextArea::default();
107
108        // Calculate the width needed for row numbers based on the maximum row number
109        let max_rows = workbook.get_current_sheet().max_rows;
110        let row_number_width = if max_rows < 10 {
111            1
112        } else {
113            max_rows.to_string().len()
114        };
115        // Ensure a minimum width of 4 for row numbers
116        let row_number_width = row_number_width.max(4);
117
118        // Check if the workbook is using lazy loading and the first sheet is not loaded
119        let is_lazy_loading = workbook.is_lazy_loading() && !workbook.is_sheet_loaded(0);
120
121        // Set initial input mode based on lazy loading status
122        let initial_input_mode = if is_lazy_loading {
123            InputMode::LazyLoading
124        } else {
125            InputMode::Normal
126        };
127
128        Ok(Self {
129            workbook,
130            file_path,
131            selected_cell: (1, 1), // Excel uses 1-based indexing
132            start_row: 1,
133            start_col: 1,
134            visible_rows: 30, // Default values, will be adjusted based on window size
135            visible_cols: 15, // Default values, will be adjusted based on window size
136            input_mode: initial_input_mode,
137            input_buffer: String::new(),
138            text_area,
139            should_quit: false,
140            column_widths,
141            sheet_column_widths,
142            sheet_cell_positions,
143            clipboard: None,
144            g_pressed: false,
145            row_number_width,
146            search_query: String::new(),
147            search_results: Vec::new(),
148            current_search_idx: None,
149            search_direction: true,  // Default to forward search
150            highlight_enabled: true, // Default to showing highlights
151            info_panel_height: 10,
152            notification_messages: Vec::new(),
153            max_notifications: 5,
154            help_text: String::new(),
155            help_scroll: 0,
156            help_visible_lines: 20,
157            help_total_lines: 0,
158            undo_history: UndoHistory::new(),
159            vim_state: None,
160        })
161    }
162
163    pub fn add_notification(&mut self, message: String) {
164        self.notification_messages.push(message);
165
166        if self.notification_messages.len() > self.max_notifications {
167            self.notification_messages.remove(0);
168        }
169    }
170
171    /// Updates the row number width based on the maximum row number in the current sheet
172    pub fn update_row_number_width(&mut self) {
173        let max_rows = self
174            .workbook
175            .get_current_sheet()
176            .max_rows
177            .max(self.selected_cell.0)
178            .max(self.start_row)
179            .clamp(1, EXCEL_MAX_ROWS);
180        let width = max_rows.to_string().len();
181        // Ensure a minimum width of 4 for row numbers
182        self.row_number_width = width.max(4);
183    }
184
185    pub fn clamp_cell_to_excel_bounds((row, col): (usize, usize)) -> (usize, usize) {
186        (row.clamp(1, EXCEL_MAX_ROWS), col.clamp(1, EXCEL_MAX_COLS))
187    }
188
189    pub fn clamp_selected_cell_to_excel_bounds(&mut self) {
190        self.selected_cell = Self::clamp_cell_to_excel_bounds(self.selected_cell);
191        self.start_row = self.start_row.clamp(1, EXCEL_MAX_ROWS);
192        self.start_col = self.start_col.clamp(1, EXCEL_MAX_COLS);
193    }
194
195    pub fn adjust_info_panel_height(&mut self, delta: isize) {
196        let new_height = (self.info_panel_height as isize + delta).clamp(6, 16) as usize;
197        if new_height != self.info_panel_height {
198            self.info_panel_height = new_height;
199            self.add_notification(format!("Info panel height: {}", self.info_panel_height));
200        }
201    }
202
203    pub fn get_cell_content(&self, row: usize, col: usize) -> String {
204        let sheet = self.workbook.get_current_sheet();
205
206        if row < sheet.data.len() && col < sheet.data[0].len() {
207            let cell = &sheet.data[row][col];
208            if cell.is_formula {
209                let mut result = String::with_capacity(9 + cell.value.len());
210                result.push_str("Formula: ");
211                result.push_str(&cell.value);
212                result
213            } else {
214                cell.value.clone()
215            }
216        } else {
217            String::new()
218        }
219    }
220
221    pub fn get_cell_content_mut(&mut self, row: usize, col: usize) -> String {
222        self.workbook.ensure_cell_exists(row, col);
223
224        self.ensure_column_widths();
225
226        let sheet = self.workbook.get_current_sheet();
227        let cell = &sheet.data[row][col];
228
229        if cell.is_formula {
230            let mut result = String::with_capacity(9 + cell.value.len());
231            result.push_str("Formula: ");
232            result.push_str(&cell.value);
233            result
234        } else {
235            cell.value.clone()
236        }
237    }
238
239    pub fn cancel_input(&mut self) {
240        match self.input_mode {
241            InputMode::Help => {
242                self.input_mode = InputMode::Normal;
243                return;
244            }
245            InputMode::CommandInLazyLoading => {
246                self.input_mode = InputMode::LazyLoading;
247                self.input_buffer = String::new();
248                self.text_area = TextArea::default();
249                return;
250            }
251            _ => {}
252        }
253
254        // Otherwise, cancel the current input
255        self.input_mode = InputMode::Normal;
256        self.input_buffer = String::new();
257        self.text_area = TextArea::default();
258    }
259
260    pub fn add_char_to_input(&mut self, c: char) {
261        self.input_buffer.push(c);
262    }
263
264    pub fn delete_char_from_input(&mut self) {
265        self.input_buffer.pop();
266    }
267
268    pub fn start_command_mode(&mut self) {
269        self.input_mode = InputMode::Command;
270        self.input_buffer = String::new();
271    }
272
273    pub fn start_command_in_lazy_loading_mode(&mut self) {
274        self.input_mode = InputMode::CommandInLazyLoading;
275        self.input_buffer = String::new();
276    }
277}