1use crate::config::SheetsConfig;
22use crate::data_loader::{load_data, LoadedData};
23use crate::layout::LayoutCache;
24use serde::{Deserialize, Serialize};
25use std::path::PathBuf;
26use std::sync::Arc;
27use std::time::SystemTime;
28use thiserror::Error;
29
30#[derive(Debug, Error)]
31pub enum StateError {
32 #[error("IO error: {0}")]
33 IoError(#[from] std::io::Error),
34
35 #[error("Data loading error: {0}")]
36 DataLoadError(#[from] crate::data_loader::DataLoaderError),
37
38 #[error("State error: {0}")]
39 StateError(String),
40}
41
42pub type Result<T> = std::result::Result<T, StateError>;
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub enum ViewMode {
46 Grid,
47 List,
48 Compact,
49 Raw,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub enum SortDirection {
54 Ascending,
55 Descending,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59pub enum StatusLevel {
60 Info,
61 Success,
62 Warning,
63 Error,
64}
65
66#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
67pub enum SearchDirection {
68 Forward,
69 Backward,
70}
71
72#[derive(Debug, Clone)]
73pub struct StatusMessage {
74 pub message: String,
75 pub timestamp: SystemTime,
77 pub level: StatusLevel,
78 pub duration_secs: u64,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83pub enum DataType {
84 Number,
85 Boolean,
86 Empty,
87 String,
88}
89
90#[derive(Clone, Serialize, Deserialize)]
94struct SheetsStateSnapshot {
95 headers: Vec<String>,
96 rows: Vec<Vec<String>>,
97 scroll_row: usize,
98 selected_row: usize,
99 selected_col: usize,
100 col_offset: usize,
101 max_scroll_row: usize,
102 max_col_offset: usize,
103 file_name: String,
104 width: usize,
105 height: usize,
106 view_mode: ViewMode,
107 sort_column: Option<String>,
108 sort_direction: SortDirection,
109 filter_expr: Option<String>,
110 search_query: Option<String>,
111 search_active: bool,
112 search_direction: SearchDirection,
113 file_path: Option<PathBuf>,
114 last_error: Option<String>,
115 show_row_numbers: bool,
116 show_column_numbers: bool,
117 show_grid_lines: bool,
118 show_data_types: bool,
119}
120
121#[derive(Clone)]
122pub struct SheetsState {
123 headers: Vec<String>,
124 rows: Vec<Vec<String>>,
125 scroll_row: usize,
126 selected_row: usize,
127 selected_col: usize,
128 col_offset: usize,
129 max_scroll_row: usize,
130 max_col_offset: usize,
131 file_name: String,
132 width: usize,
133 height: usize,
134 config: Arc<SheetsConfig>,
135 view_mode: ViewMode,
136 sort_column: Option<String>,
137 sort_direction: SortDirection,
138 filter_expr: Option<String>,
139 search_query: Option<String>,
140 search_active: bool,
141 search_direction: SearchDirection,
142 file_path: Option<PathBuf>,
143 file_mod_time: Option<SystemTime>,
144 last_error: Option<String>,
145 status_messages: Vec<StatusMessage>,
146 show_row_numbers: bool,
147 show_column_numbers: bool,
148 show_grid_lines: bool,
149 show_data_types: bool,
150 pub layout_cache: LayoutCache,
151}
152
153impl Default for SheetsState {
154 fn default() -> Self {
155 Self::new(Arc::new(SheetsConfig::default()))
156 }
157}
158
159impl SheetsState {
160 pub fn new(config: Arc<SheetsConfig>) -> Self {
161 Self {
162 headers: Vec::new(),
163 rows: Vec::new(),
164 scroll_row: 0,
165 selected_row: 0,
166 selected_col: 0,
167 col_offset: 0,
168 max_scroll_row: 0,
169 max_col_offset: 0,
170 file_name: String::new(),
171 width: 80,
172 height: 24,
173 config,
174 view_mode: ViewMode::Grid,
175 sort_column: None,
176 sort_direction: SortDirection::Ascending,
177 filter_expr: None,
178 search_query: None,
179 search_active: false,
180 search_direction: SearchDirection::Forward,
181 file_path: None,
182 file_mod_time: None,
183 last_error: None,
184 status_messages: Vec::new(),
185 show_row_numbers: false,
186 show_column_numbers: true,
187 show_grid_lines: true,
188 show_data_types: false,
189 layout_cache: LayoutCache::default(),
190 }
191 }
192
193 pub fn init(&mut self, data: LoadedData) -> Result<()> {
194 self.headers = data.headers;
195 self.rows = data.rows;
196 self.selected_row = 0;
197 self.selected_col = 0;
198 self.scroll_row = 0;
199 self.col_offset = 0;
200 self.layout_cache = LayoutCache::prepare(&self.headers, &self.rows);
201 self.sync_bounds();
202 Ok(())
203 }
204
205 pub fn load_file(&mut self, path: PathBuf) -> Result<()> {
206 let data = load_data(&path)?;
207 self.file_name = path
208 .file_name()
209 .and_then(|name| name.to_str())
210 .unwrap_or("unknown")
211 .to_string();
212 self.file_mod_time = std::fs::metadata(&path).and_then(|m| m.modified()).ok();
213 self.file_path = Some(path.clone());
214 self.init(data)?;
215 self.add_status_message(StatusMessage {
216 message: format!("Loaded {}", path.display()),
217 timestamp: SystemTime::now(),
218 level: StatusLevel::Success,
219 duration_secs: 5,
220 });
221 Ok(())
222 }
223
224 pub fn resize(&mut self, width: usize, height: usize) {
225 self.width = width.max(20);
226 self.height = height.max(8);
227 self.sync_bounds();
228 }
229
230 pub fn scroll_up(&mut self) {
231 if self.scroll_row > 0 {
232 self.scroll_row -= 1;
233 }
234 }
235
236 pub fn scroll_down(&mut self) {
237 if self.scroll_row < self.max_scroll_row {
238 self.scroll_row += 1;
239 }
240 }
241
242 pub fn scroll_left(&mut self) {
243 if self.col_offset > 0 {
244 self.col_offset -= 1;
245 self.selected_col = self.selected_col.max(self.col_offset);
246 self.selected_col = self.selected_col.min(self.last_visible_col());
247 }
248 }
249
250 pub fn scroll_right(&mut self) {
251 if self.col_offset < self.max_col_offset {
252 self.col_offset += 1;
253 self.selected_col = self.selected_col.max(self.col_offset);
254 self.selected_col = self.selected_col.min(self.last_visible_col());
255 }
256 }
257
258 pub fn page_up(&mut self) {
259 let page_size = self.config.behavior.page_size.max(1);
260 self.scroll_row = self.scroll_row.saturating_sub(page_size);
261 self.selected_row = self.selected_row.saturating_sub(page_size);
262 self.adjust_scroll_for_selection();
263 }
264
265 pub fn page_down(&mut self) {
266 let page_size = self.config.behavior.page_size.max(1);
267 self.scroll_row = (self.scroll_row + page_size).min(self.max_scroll_row);
268 self.selected_row = (self.selected_row + page_size).min(self.last_row_index());
269 self.adjust_scroll_for_selection();
270 }
271
272 pub fn half_page_up(&mut self) {
273 let page_size = (self.visible_rows() / 2).max(1);
274 self.scroll_row = self.scroll_row.saturating_sub(page_size);
275 self.selected_row = self.selected_row.saturating_sub(page_size);
276 self.adjust_scroll_for_selection();
277 }
278
279 pub fn half_page_down(&mut self) {
280 let page_size = (self.visible_rows() / 2).max(1);
281 self.scroll_row = (self.scroll_row + page_size).min(self.max_scroll_row);
282 self.selected_row = (self.selected_row + page_size).min(self.last_row_index());
283 self.adjust_scroll_for_selection();
284 }
285
286 pub fn go_to_top(&mut self) {
287 self.scroll_row = 0;
288 self.selected_row = 0;
289 self.adjust_scroll_for_selection();
290 }
291
292 pub fn go_to_bottom(&mut self) {
293 self.scroll_row = self.max_scroll_row;
294 self.selected_row = self.last_row_index();
295 self.adjust_scroll_for_selection();
296 }
297
298 pub fn go_to_first_col(&mut self) {
299 self.selected_col = 0;
300 self.adjust_scroll_for_selection();
301 }
302
303 pub fn go_to_last_col(&mut self) {
304 self.selected_col = self.last_col_index();
305 self.adjust_scroll_for_selection();
306 }
307
308 pub fn go_to_top_visible(&mut self) {
309 self.selected_row = self.scroll_row.min(self.last_row_index());
310 self.adjust_scroll_for_selection();
311 }
312
313 pub fn go_to_middle_visible(&mut self) {
314 let (start, end) = self.row_range();
315 let visible_len = end.saturating_sub(start);
316 if visible_len == 0 {
317 self.selected_row = 0;
318 } else {
319 self.selected_row = start + ((visible_len - 1) / 2);
320 }
321 self.adjust_scroll_for_selection();
322 }
323
324 pub fn go_to_bottom_visible(&mut self) {
325 let (_, end) = self.row_range();
326 self.selected_row = end.saturating_sub(1);
327 self.adjust_scroll_for_selection();
328 }
329
330 pub fn select_up(&mut self) {
331 if self.selected_row > 0 {
332 self.selected_row -= 1;
333 self.adjust_scroll_for_selection();
334 }
335 }
336
337 pub fn select_down(&mut self) {
338 if self.selected_row < self.last_row_index() {
339 self.selected_row += 1;
340 self.adjust_scroll_for_selection();
341 }
342 }
343
344 pub fn select_left(&mut self) {
345 if self.selected_col > 0 {
346 self.selected_col -= 1;
347 self.adjust_scroll_for_selection();
348 }
349 }
350
351 pub fn select_right(&mut self) {
352 if self.selected_col < self.last_col_index() {
353 self.selected_col += 1;
354 self.adjust_scroll_for_selection();
355 }
356 }
357
358 pub fn quit(&mut self) {
359 self.add_status_message(StatusMessage {
360 message: "Exiting".to_string(),
361 timestamp: SystemTime::now(),
362 level: StatusLevel::Info,
363 duration_secs: 1,
364 });
365 }
366
367 pub fn scroll_row(&self) -> usize {
368 self.scroll_row
369 }
370
371 pub fn selected_row(&self) -> usize {
372 self.selected_row
373 }
374
375 pub fn selected_col(&self) -> usize {
376 self.selected_col
377 }
378
379 pub fn col_offset(&self) -> usize {
380 self.col_offset
381 }
382
383 pub fn max_col_offset(&self) -> usize {
384 self.max_col_offset
385 }
386
387 pub fn row_count(&self) -> usize {
388 self.rows.len()
389 }
390
391 pub fn col_count(&self) -> usize {
392 self.headers.len()
393 }
394
395 pub fn headers(&self) -> Option<&Vec<String>> {
396 (!self.headers.is_empty()).then_some(&self.headers)
397 }
398
399 pub fn file_name(&self) -> &str {
400 if self.file_name.is_empty() {
401 "No file loaded"
402 } else {
403 &self.file_name
404 }
405 }
406
407 pub fn visible_rows(&self) -> usize {
408 self.height.saturating_sub(5).max(1)
409 }
410
411 pub fn visible_cols(&self) -> usize {
412 self.visible_cols_from_offset(self.col_offset)
413 }
414
415 pub fn visible_cols_from_offset(&self, offset: usize) -> usize {
416 if self.col_count() == 0 || offset >= self.col_count() {
417 return 0;
418 }
419
420 let layouts = crate::layout::LayoutEngine::new().resolve(&self.layout_cache, self.width);
421 let mut used_width = 0;
422 let mut visible_cols = 0;
423
424 for layout in layouts.iter().skip(offset) {
425 let separator_width = usize::from(visible_cols > 0) * 3;
426 let next_width = used_width + separator_width + layout.resolved_width;
427 if next_width > self.width {
428 break;
429 }
430 used_width = next_width;
431 visible_cols += 1;
432 }
433
434 visible_cols.max(1)
435 }
436
437 pub fn row_range(&self) -> (usize, usize) {
438 let start = self.scroll_row;
439 let end = (start + self.visible_rows()).min(self.row_count());
440 (start, end)
441 }
442
443 pub fn get_cell(&self, row: usize, col: usize) -> Option<String> {
444 self.rows.get(row)?.get(col).cloned()
445 }
446
447 pub fn get_row(&self, row: usize) -> Option<Vec<String>> {
448 self.rows.get(row).cloned()
449 }
450
451 pub fn get_data_type(&self, col: usize) -> Option<DataType> {
452 if col >= self.col_count() {
453 return None;
454 }
455
456 self.rows
457 .iter()
458 .filter_map(|row| row.get(col))
459 .find(|value| !value.trim().is_empty())
460 .map(|value| infer_data_type(value))
461 .or(Some(DataType::Empty))
462 }
463
464 pub fn at_top(&self) -> bool {
465 self.scroll_row == 0
466 }
467
468 pub fn at_bottom(&self) -> bool {
469 self.scroll_row >= self.max_scroll_row
470 }
471
472 pub fn add_status_message(&mut self, message: StatusMessage) {
473 self.status_messages.push(message);
474 self.status_messages.retain(|msg| {
476 msg.timestamp
477 .elapsed()
478 .map(|elapsed| elapsed.as_secs() < msg.duration_secs)
479 .unwrap_or(true)
480 });
481 }
482
483 pub fn get_status_messages(&self) -> Result<Vec<StatusMessage>> {
484 Ok(self.status_messages.clone())
485 }
486
487 pub fn clear_status_messages(&mut self) {
488 self.status_messages.clear();
489 }
490
491 pub fn set_view_mode(&mut self, mode: ViewMode) {
492 self.view_mode = mode;
493 }
494
495 pub fn get_view_mode(&self) -> Result<ViewMode> {
496 Ok(self.view_mode.clone())
497 }
498
499 pub fn set_search_query(&mut self, query: Option<String>) {
500 self.search_query = query;
501 }
502
503 pub fn get_search_query(&self) -> Result<Option<String>> {
504 Ok(self.search_query.clone())
505 }
506
507 pub fn is_search_active(&self) -> bool {
508 self.search_active
509 }
510
511 pub fn search_direction(&self) -> SearchDirection {
512 self.search_direction
513 }
514
515 pub fn begin_search(&mut self, direction: SearchDirection) {
516 self.search_active = true;
517 self.search_direction = direction;
518 self.search_query = Some(String::new());
519 }
520
521 pub fn search_append(&mut self, ch: char) {
522 if !self.search_active {
523 return;
524 }
525
526 self.search_query.get_or_insert_with(String::new).push(ch);
527 }
528
529 pub fn search_backspace(&mut self) {
530 if !self.search_active {
531 return;
532 }
533
534 if let Some(query) = &mut self.search_query {
535 query.pop();
536 }
537 }
538
539 pub fn search_commit(&mut self) -> bool {
540 self.search_active = false;
541
542 if self.search_query.as_deref().unwrap_or_default().is_empty() {
543 self.search_query = None;
544 return false;
545 }
546
547 match self.search_direction {
548 SearchDirection::Forward => self.search_next(),
549 SearchDirection::Backward => self.search_prev(),
550 }
551 }
552
553 pub fn search_cancel(&mut self) {
554 self.search_active = false;
555 self.search_query = None;
556 }
557
558 pub fn search_next(&mut self) -> bool {
559 self.search_direction = SearchDirection::Forward;
560 self.find_and_select_match(SearchDirection::Forward)
561 }
562
563 pub fn search_prev(&mut self) -> bool {
564 self.search_direction = SearchDirection::Backward;
565 self.find_and_select_match(SearchDirection::Backward)
566 }
567
568 pub fn set_filter_expr(&mut self, expr: Option<String>) {
569 self.filter_expr = expr;
570 }
571
572 pub fn get_filter_expr(&self) -> Result<Option<String>> {
573 Ok(self.filter_expr.clone())
574 }
575
576 pub fn set_sort(&mut self, column: Option<String>, direction: SortDirection) {
577 self.sort_column = column;
578 self.sort_direction = direction;
579 }
580
581 pub fn get_sort_column(&self) -> Result<Option<String>> {
582 Ok(self.sort_column.clone())
583 }
584
585 pub fn get_sort_direction(&self) -> Result<SortDirection> {
586 Ok(self.sort_direction.clone())
587 }
588
589 pub fn set_file_path(&mut self, path: PathBuf) {
590 self.file_path = Some(path);
591 }
592
593 pub fn get_file_path(&self) -> Result<Option<PathBuf>> {
594 Ok(self.file_path.clone())
595 }
596
597 pub fn set_file_mod_time(&mut self, time: Option<SystemTime>) {
598 self.file_mod_time = time;
599 }
600
601 pub fn get_file_mod_time(&self) -> Result<Option<SystemTime>> {
602 Ok(self.file_mod_time)
603 }
604
605 pub fn get_column_names(&self) -> Result<Vec<String>> {
606 Ok(self.headers.clone())
607 }
608
609 pub fn get_row_count(&self) -> Result<usize> {
610 Ok(self.row_count())
611 }
612
613 pub fn get_column_count(&self) -> Result<usize> {
614 Ok(self.col_count())
615 }
616
617 pub fn get_selected_row(&self) -> Result<usize> {
618 Ok(self.selected_row)
619 }
620
621 pub fn get_selected_col(&self) -> Result<usize> {
622 Ok(self.selected_col)
623 }
624
625 pub fn get_row_range(&self) -> Result<(usize, usize)> {
626 Ok(self.row_range())
627 }
628
629 pub fn get_width(&self) -> Result<usize> {
630 Ok(self.width)
631 }
632
633 pub fn get_height(&self) -> Result<usize> {
634 Ok(self.height)
635 }
636
637 pub fn get_file_name(&self) -> Result<String> {
638 Ok(self.file_name().to_string())
639 }
640
641 pub fn get_config(&self) -> Result<SheetsConfig> {
642 Ok((*self.config).clone())
643 }
644
645 pub fn set_config(&mut self, config: SheetsConfig) {
646 self.config = Arc::new(config);
647 }
648
649 pub fn get_last_error(&self) -> Result<Option<String>> {
650 Ok(self.last_error.clone())
651 }
652
653 pub fn set_last_error(&mut self, error: Option<String>) {
654 self.last_error = error;
655 }
656
657 pub fn clear_last_error(&mut self) {
658 self.last_error = None;
659 }
660
661 pub fn set_show_row_numbers(&mut self, show: bool) {
662 self.show_row_numbers = show;
663 }
664
665 pub fn get_show_row_numbers(&self) -> Result<bool> {
666 Ok(self.show_row_numbers)
667 }
668
669 pub fn set_show_column_numbers(&mut self, show: bool) {
670 self.show_column_numbers = show;
671 }
672
673 pub fn get_show_column_numbers(&self) -> Result<bool> {
674 Ok(self.show_column_numbers)
675 }
676
677 pub fn set_show_grid_lines(&mut self, show: bool) {
678 self.show_grid_lines = show;
679 }
680
681 pub fn get_show_grid_lines(&self) -> Result<bool> {
682 Ok(self.show_grid_lines)
683 }
684
685 pub fn set_show_data_types(&mut self, show: bool) {
686 self.show_data_types = show;
687 }
688
689 pub fn get_show_data_types(&self) -> Result<bool> {
690 Ok(self.show_data_types)
691 }
692
693 pub fn is_file_modified(&self) -> Result<bool> {
694 let Some(path) = self.file_path.as_ref() else {
695 return Ok(false);
696 };
697 let Some(last_mod_time) = self.file_mod_time else {
698 return Ok(false);
699 };
700 let current_mod_time = std::fs::metadata(path).and_then(|m| m.modified())?;
701 Ok(current_mod_time > last_mod_time)
702 }
703
704 fn sync_bounds(&mut self) {
705 self.max_scroll_row = self.row_count().saturating_sub(self.visible_rows());
706 self.max_col_offset = self
707 .col_count()
708 .saturating_sub(self.visible_cols_from_offset(0));
709 self.scroll_row = self.scroll_row.min(self.max_scroll_row);
710 self.col_offset = self.col_offset.min(self.max_col_offset);
711 self.selected_row = self.selected_row.min(self.last_row_index());
712 self.selected_col = self.selected_col.min(self.last_col_index());
713 self.adjust_scroll_for_selection();
714 }
715
716 fn last_row_index(&self) -> usize {
717 self.row_count().saturating_sub(1)
718 }
719
720 fn last_col_index(&self) -> usize {
721 self.col_count().saturating_sub(1)
722 }
723
724 fn last_visible_col(&self) -> usize {
725 self.col_offset
726 .saturating_add(self.visible_cols().saturating_sub(1))
727 .min(self.last_col_index())
728 }
729
730 fn adjust_scroll_for_selection(&mut self) {
731 if self.selected_row < self.scroll_row {
732 self.scroll_row = self.selected_row;
733 } else if self.selected_row >= self.scroll_row + self.visible_rows() {
734 self.scroll_row = self
735 .selected_row
736 .saturating_sub(self.visible_rows().saturating_sub(1));
737 }
738
739 if self.selected_col < self.col_offset {
740 self.col_offset = self.selected_col;
741 } else {
742 while self.selected_col > self.last_visible_col()
743 && self.col_offset < self.max_col_offset
744 {
745 self.col_offset += 1;
746 }
747 }
748
749 self.scroll_row = self.scroll_row.min(self.max_scroll_row);
750 self.col_offset = self.col_offset.min(self.max_col_offset);
751 }
752
753 fn find_and_select_match(&mut self, direction: SearchDirection) -> bool {
754 let Some(query) = self.search_query.as_deref() else {
755 return false;
756 };
757 if query.is_empty() || self.row_count() == 0 || self.col_count() == 0 {
758 return false;
759 }
760
761 let row_count = self.row_count();
762 let col_count = self.col_count();
763 let total_cells = row_count * col_count;
764 if total_cells == 0 {
765 return false;
766 }
767
768 let start_index = self.selected_row * col_count + self.selected_col;
769 for step in 1..=total_cells {
770 let index = match direction {
771 SearchDirection::Forward => (start_index + step) % total_cells,
772 SearchDirection::Backward => {
773 (start_index + total_cells - (step % total_cells)) % total_cells
774 }
775 };
776 let row = index / col_count;
777 let col = index % col_count;
778
779 if self
780 .rows
781 .get(row)
782 .and_then(|values| values.get(col))
783 .is_some_and(|value| cell_matches_query(value, query))
784 {
785 self.selected_row = row;
786 self.selected_col = col;
787 self.adjust_scroll_for_selection();
788 return true;
789 }
790 }
791
792 self.add_status_message(StatusMessage {
793 message: format!("Pattern not found: {query}"),
794 timestamp: SystemTime::now(),
795 level: StatusLevel::Warning,
796 duration_secs: 3,
797 });
798 false
799 }
800}
801
802pub fn serialize_state(state: &SheetsState) -> Result<String> {
803 let snapshot = SheetsStateSnapshot {
804 headers: state.headers.clone(),
805 rows: state.rows.clone(),
806 scroll_row: state.scroll_row,
807 selected_row: state.selected_row,
808 selected_col: state.selected_col,
809 col_offset: state.col_offset,
810 max_scroll_row: state.max_scroll_row,
811 max_col_offset: state.max_col_offset,
812 file_name: state.file_name.clone(),
813 width: state.width,
814 height: state.height,
815 view_mode: state.view_mode.clone(),
816 sort_column: state.sort_column.clone(),
817 sort_direction: state.sort_direction.clone(),
818 filter_expr: state.filter_expr.clone(),
819 search_query: state.search_query.clone(),
820 search_active: state.search_active,
821 search_direction: state.search_direction,
822 file_path: state.file_path.clone(),
823 last_error: state.last_error.clone(),
824 show_row_numbers: state.show_row_numbers,
825 show_column_numbers: state.show_column_numbers,
826 show_grid_lines: state.show_grid_lines,
827 show_data_types: state.show_data_types,
828 };
829 serde_json::to_string_pretty(&snapshot)
830 .map_err(|e| StateError::StateError(format!("Serialization error: {}", e)))
831}
832
833pub fn deserialize_state(json: &str) -> Result<SheetsState> {
834 let snapshot: SheetsStateSnapshot = serde_json::from_str(json)
835 .map_err(|e| StateError::StateError(format!("Deserialization error: {}", e)))?;
836 let mut state = SheetsState::new(Arc::new(SheetsConfig::default()));
837 state.headers = snapshot.headers;
838 state.rows = snapshot.rows;
839 state.scroll_row = snapshot.scroll_row;
840 state.selected_row = snapshot.selected_row;
841 state.selected_col = snapshot.selected_col;
842 state.col_offset = snapshot.col_offset;
843 state.max_scroll_row = snapshot.max_scroll_row;
844 state.max_col_offset = snapshot.max_col_offset;
845 state.file_name = snapshot.file_name;
846 state.width = snapshot.width;
847 state.height = snapshot.height;
848 state.view_mode = snapshot.view_mode;
849 state.sort_column = snapshot.sort_column;
850 state.sort_direction = snapshot.sort_direction;
851 state.filter_expr = snapshot.filter_expr;
852 state.search_query = snapshot.search_query;
853 state.search_active = snapshot.search_active;
854 state.search_direction = snapshot.search_direction;
855 state.file_path = snapshot.file_path;
856 state.last_error = snapshot.last_error;
857 state.show_row_numbers = snapshot.show_row_numbers;
858 state.show_column_numbers = snapshot.show_column_numbers;
859 state.show_grid_lines = snapshot.show_grid_lines;
860 state.show_data_types = snapshot.show_data_types;
861 state.layout_cache = LayoutCache::prepare(&state.headers, &state.rows);
862 Ok(state)
863}
864
865pub fn save_state(state: &SheetsState, path: &PathBuf) -> Result<()> {
866 let json = serialize_state(state)?;
867 std::fs::write(path, json)?;
868 Ok(())
869}
870
871pub fn load_state(path: &PathBuf) -> Result<SheetsState> {
872 let json = std::fs::read_to_string(path)?;
873 deserialize_state(&json)
874}
875
876fn infer_data_type(value: &str) -> DataType {
877 let trimmed = value.trim();
878 if trimmed.is_empty() {
879 return DataType::Empty;
880 }
881 if trimmed.eq_ignore_ascii_case("true") || trimmed.eq_ignore_ascii_case("false") {
882 return DataType::Boolean;
883 }
884 if trimmed.parse::<i64>().is_ok() || trimmed.parse::<f64>().is_ok() {
885 return DataType::Number;
886 }
887 DataType::String
888}
889
890pub fn cell_matches_query(value: &str, query: &str) -> bool {
891 let trimmed_query = query.trim();
892 !trimmed_query.is_empty() && value.to_lowercase().contains(&trimmed_query.to_lowercase())
893}