flerp 0.5.0

CLI tool that does XYZ
Documentation
use crossterm::event::KeyCode;
use std::error::Error;
use std::time::Instant;

use crate::app_structs::AppState;
use crate::file_utils::read_file_content;
use crate::settings::AppSettings;
use crate::text_analysis::{
    analyze_structure, extract_keywords, extract_repeated_lines, search_with_options, SearchOptions,
};

pub struct App {
    pub state: AppState,
    pub last_tick: Instant,
}

impl App {
    pub fn new() -> Self {
        let mut state = AppState::default();
        AppSettings::load().apply_to_state(&mut state);

        Self {
            state,
            last_tick: Instant::now(),
        }
    }

    pub fn load_file(&mut self, file_path: &str) -> Result<(), Box<dyn Error>> {
        let content = read_file_content(file_path)?;

        self.state.file_content = content;
        self.state.file_name = file_path.to_string();
        self.refresh_analysis();
        self.state.content_scroll = 0;
        self.update_search();
        self.state.status_message = format!("Loaded {}", self.state.file_name);
        Ok(())
    }

    fn refresh_analysis(&mut self) {
        self.state.structural_analysis = analyze_structure(&self.state.file_content);
        self.state.keywords = extract_keywords(&self.state.file_content, self.state.keyword_limit);
        self.state.repeated_lines = extract_repeated_lines(&self.state.file_content, 8);
        self.state.content_scroll = self.state.content_scroll.min(self.max_content_scroll());
    }

    fn update_settings_selection(&mut self, direction: isize) {
        let max_index = 7;
        let next = (self.state.settings_selection as isize + direction).clamp(0, max_index);
        self.state.settings_selection = next as usize;
    }

    fn adjust_setting(&mut self, increase: bool) {
        match self.state.settings_selection {
            0 => {
                self.state.theme = if increase {
                    self.state.theme.next()
                } else {
                    self.state.theme.previous()
                };
            }
            1 => {
                let step = if increase { 1 } else { -1 };
                let next = (self.state.keyword_limit as isize + step).clamp(5, 30) as usize;
                if next != self.state.keyword_limit {
                    self.state.keyword_limit = next;
                    self.refresh_analysis();
                }
            }
            2 => {
                let step = if increase { 5 } else { -5 };
                self.state.preview_line_count =
                    (self.state.preview_line_count as isize + step).clamp(10, 200) as usize;
                self.state.content_scroll = self.state.content_scroll.min(self.max_content_scroll());
            }
            3 => self.state.case_sensitive = increase,
            4 => self.state.regex_mode = increase,
            5 => self.state.whole_word = increase,
            6 => self.state.line_numbers = increase,
            7 => self.state.wrap_lines = increase,
            _ => {}
        }

        if (3..=5).contains(&self.state.settings_selection) {
            self.update_search();
        }

        self.persist_settings();
    }

    fn persist_settings(&mut self) {
        match AppSettings::from_state(&self.state).save() {
            Ok(()) => {
                self.state.status_message = "Settings saved to XDG config directory.".to_string();
            }
            Err(error) => {
                self.state.status_message = format!("Could not save settings: {error}");
            }
        }
    }

    fn max_content_scroll(&self) -> usize {
        self.state
            .file_content
            .lines()
            .count()
            .saturating_sub(self.state.preview_line_count)
    }

    fn scroll_content(&mut self, delta: isize) {
        let max_scroll = self.max_content_scroll() as isize;
        let next = (self.state.content_scroll as isize + delta).clamp(0, max_scroll);
        self.state.content_scroll = next as usize;
    }

    fn jump_to_selected_result(&mut self) {
        if let Some(selected) = self.state.search_results.get(self.state.selected_result) {
            let target = selected.line_number.saturating_sub(3);
            self.state.content_scroll = target.min(self.max_content_scroll());
            self.state.current_tab = 2;
            self.state.status_message = format!(
                "Jumped to line {} from search results.",
                selected.line_number
            );
        }
    }

    pub fn update_search(&mut self) {
        self.state.search_error = None;

        match search_with_options(
            &self.state.search_query,
            &self.state.file_content,
            SearchOptions {
                case_sensitive: self.state.case_sensitive,
                regex_mode: self.state.regex_mode,
                whole_word: self.state.whole_word,
            },
        ) {
            Ok(results) => {
                self.state.search_results = results;
            }
            Err(error) => {
                self.state.search_results.clear();
                self.state.search_error = Some(error);
            }
        }

        self.state.selected_result = 0;
        self.state.result_list_state.select(if self.state.search_results.is_empty() {
            None
        } else {
            Some(0)
        });
    }

    pub fn handle_key(&mut self, key: KeyCode) -> bool {
        match key {
            KeyCode::Char('q') if !self.state.search_mode => return false,
            KeyCode::Esc => {
                if self.state.search_mode {
                    self.state.search_mode = false;
                } else {
                    return false;
                }
            }
            KeyCode::Char('/') if !self.state.search_mode => {
                self.state.search_mode = true;
                self.state.current_tab = 1;
            }
            KeyCode::Tab if !self.state.search_mode => {
                self.state.current_tab = (self.state.current_tab + 1) % 5;
            }
            KeyCode::Char('c') if !self.state.search_mode => {
                self.state.case_sensitive = !self.state.case_sensitive;
                self.update_search();
                self.persist_settings();
            }
            KeyCode::Char('r') if !self.state.search_mode => {
                self.state.regex_mode = !self.state.regex_mode;
                self.update_search();
                self.persist_settings();
            }
            KeyCode::Char('w') if !self.state.search_mode => {
                self.state.whole_word = !self.state.whole_word;
                self.update_search();
                self.persist_settings();
            }
            KeyCode::Char('l') if !self.state.search_mode => {
                self.state.line_numbers = !self.state.line_numbers;
                self.persist_settings();
            }
            KeyCode::Char('z') if !self.state.search_mode => {
                self.state.wrap_lines = !self.state.wrap_lines;
                self.persist_settings();
            }
            KeyCode::Enter if self.state.search_mode => {
                self.state.search_mode = false;
                self.update_search();
            }
            KeyCode::Enter if !self.state.search_mode && self.state.current_tab == 1 => {
                self.jump_to_selected_result();
            }
            KeyCode::Backspace if self.state.search_mode => {
                self.state.search_query.pop();
                self.update_search();
            }
            KeyCode::Char(c) if self.state.search_mode => {
                self.state.search_query.push(c);
                self.update_search();
            }
            KeyCode::Up if !self.state.search_mode && self.state.current_tab == 1 => {
                if !self.state.search_results.is_empty() {
                    self.state.selected_result = self.state.selected_result.saturating_sub(1);
                    self.state
                        .result_list_state
                        .select(Some(self.state.selected_result));
                }
            }
            KeyCode::Down if !self.state.search_mode && self.state.current_tab == 1 => {
                if !self.state.search_results.is_empty() {
                    self.state.selected_result = (self.state.selected_result + 1)
                        .min(self.state.search_results.len().saturating_sub(1));
                    self.state
                        .result_list_state
                        .select(Some(self.state.selected_result));
                }
            }
            KeyCode::Up if !self.state.search_mode && self.state.current_tab == 2 => {
                self.scroll_content(-1);
            }
            KeyCode::Down if !self.state.search_mode && self.state.current_tab == 2 => {
                self.scroll_content(1);
            }
            KeyCode::PageUp if !self.state.search_mode && self.state.current_tab == 2 => {
                self.scroll_content(-(self.state.preview_line_count as isize));
            }
            KeyCode::PageDown if !self.state.search_mode && self.state.current_tab == 2 => {
                self.scroll_content(self.state.preview_line_count as isize);
            }
            KeyCode::Home if !self.state.search_mode && self.state.current_tab == 2 => {
                self.state.content_scroll = 0;
            }
            KeyCode::End if !self.state.search_mode && self.state.current_tab == 2 => {
                self.state.content_scroll = self.max_content_scroll();
            }
            KeyCode::Up if !self.state.search_mode && self.state.current_tab == 4 => {
                self.update_settings_selection(-1);
            }
            KeyCode::Down if !self.state.search_mode && self.state.current_tab == 4 => {
                self.update_settings_selection(1);
            }
            KeyCode::Left if !self.state.search_mode && self.state.current_tab == 4 => {
                self.adjust_setting(false);
            }
            KeyCode::Right if !self.state.search_mode && self.state.current_tab == 4 => {
                self.adjust_setting(true);
            }
            _ => {}
        }
        true
    }

    pub fn tick(&mut self) {
        self.last_tick = Instant::now();
    }
}