Skip to main content

zellij_sheets/
state.rs

1//! Core state management module for the spreadsheet viewer
2//!
3//! This module provides the `SheetsState` struct which manages all aspects of
4//! spreadsheet data, display configuration, and user interaction state.
5//!
6//! ## Data Model
7//!
8//! The state stores:
9//! - Headers and rows from the loaded spreadsheet
10//! - Scroll position and selection state
11//! - User preferences (view mode, colors, etc.)
12//! - File metadata and modification tracking
13//!
14//! ## State Management
15//!
16//! The state is designed to be:
17//! - Serializable for persistence across sessions
18//! - Thread-safe for concurrent access
19//! - Efficient for large datasets
20
21use 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    /// Epoch seconds at creation time, used for expiry checks.
76    pub timestamp: SystemTime,
77    pub level: StatusLevel,
78    /// How long this message should be displayed, in seconds.
79    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/// Serializable snapshot of `SheetsState`, excluding runtime-only fields
91/// (`Arc<SheetsConfig>`, `StatusMessage`s, and `SystemTime`s) that cannot
92/// round-trip through serde without custom impls.
93#[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        // Expire messages using each message's own duration, not a shared one.
475        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}