lk-inside 0.3.1

A terminal user interface (TUI) application for interactive data analysis.
Documentation
//! Main entry point for the `lk-inside` TUI application.
//!
//! This module handles the application's setup, event loop, and UI rendering.

use std::{error::Error, io, path::PathBuf};
use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::{Backend, CrosstermBackend},
    layout::{Constraint, Direction, Layout},
    widgets::{Block, Borders, Paragraph},
    Frame,
    Terminal,
};
use tokio;
use anyhow::Result;
use fern::Dispatch;

use lk_inside::{AppState, AppScreen, InputMode};
use lk_inside::data_core::analyzer::DataFrameAnalyzer;
use lk_inside::utils::config::Settings;
use lk_inside::ui::screens::statistics_screen::handle_input as handle_statistics_input;
use lk_inside::ui::theme; // ADDED




/// Sets up the application's logging infrastructure using the `fern` crate.
///
/// Logs are written to `lk-inside.log` with a DEBUG level or higher.
///
/// # Returns
///
/// `Ok(())` if the logger is successfully set up, otherwise a `fern::InitError`.
fn setup_logger() -> Result<(), fern::InitError> {
    Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "{} {} {} {}",
                chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"),
                record.target(),
                record.level(),
                message
            ))
        })
        .level(log::LevelFilter::Debug)
        .chain(fern::log_file("lk-inside.log")?)
        .apply()?;
    Ok(())
}

/// Main entry point of the `lk-inside` application.
///
/// Sets up the logger, loads configuration, initializes the terminal,
/// runs the TUI event loop, and restores the terminal state on exit.
///
/// # Returns
///
/// `Ok(())` if the application runs successfully, otherwise an error.
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    setup_logger().expect("Failed to set up logger");

    let settings = Settings::new().map_err(|e| format!("Failed to load configuration: {}", e))?;
    let mut app_state = AppState::new(settings);
    app_state.status_message = "Welcome! Use arrow keys to navigate, Enter to select.".to_string();

    // setup terminal
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    // create app and run it
    let res = run_app(&mut terminal, &mut app_state).await;

    // restore terminal
    disable_raw_mode()?;
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )?;
    terminal.show_cursor()?;

    if let Err(err) = res {
        println!("{:?}", err)
    }

    Ok(())
}

async fn run_app<B: Backend>(terminal: &mut Terminal<B>, app_state: &mut AppState) -> io::Result<()> {
    loop {
        if app_state.needs_redraw { // MODIFIED
            terminal.draw(|f| {
                if let Err(e) = ui(f, app_state) {
                    app_state.status_message = format!("UI Error: {}", e);
                    app_state.current_screen = AppScreen::Error;
                }
            })?;
            app_state.needs_redraw = false; // MODIFIED
        }

        if let Some(loading_task) = &mut app_state.loading_task {
            if loading_task.is_finished() {
                let task = app_state.loading_task.take().unwrap();
                match task.await.unwrap() {
                    Ok(df) => {
                        match lk_inside::data_core::loader::validate_dataframe(&df) {
                            Ok(_) => {
                                let ids_are_continuous = lk_inside::data_core::loader::validate_ids(&df).unwrap_or(false);
                                app_state.workspace.ids_are_continuous = ids_are_continuous;

                                let column_datatypes = df.get_column_names().iter().map(|name| {
                                    (name.to_string(), df.column(name).unwrap().dtype().to_string())
                                }).collect();
                                app_state.workspace.column_datatypes = column_datatypes;

                                let analyzer = DataFrameAnalyzer::new(df);
                                app_state.workspace.analyzer = Some(analyzer);
                                app_state.current_screen = AppScreen::StatisticsView;
                                app_state.status_message = format!("Successfully loaded data. IDs continuous: {}", ids_are_continuous);
                                app_state.needs_redraw = true; // ADDED
                            }
                            Err(e) => {
                                app_state.status_message = format!("Error validating data: {}", e);
                                app_state.current_screen = AppScreen::Error;
                                app_state.needs_redraw = true; // ADDED
                            }
                        }
                    }
                    Err(e) => {
                        app_state.status_message = format!("Error loading data: {}", e);
                        app_state.current_screen = AppScreen::Error;
                        app_state.needs_redraw = true; // ADDED
                    }
                }
            }
        }

        if event::poll(std::time::Duration::from_millis(100))? {
            if let Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press {
                    // Global keybindings (highest priority)
                    if key.code == KeyCode::Char('q') && key.modifiers.contains(KeyModifiers::ALT) {
                        app_state.current_screen = AppScreen::StartMenu;
                        app_state.status_message = "Returned to Start Menu. Use arrow keys to navigate, Enter to select.".to_string();
                        app_state.needs_redraw = true; // ADDED
                        continue;
                    }
                    if key.code == KeyCode::Char('s') && key.modifiers.contains(KeyModifiers::CONTROL) {
                        app_state.current_screen = AppScreen::SaveState;
                        app_state.input_mode = InputMode::EditingSavePath;
                        app_state.status_message = "Enter file path to save workspace (e.g., 'workspace.json'):".to_string();
                        app_state.save_screen.save_path_input.reset();
                        app_state.needs_redraw = true; // ADDED
                        continue;
                    }
                    if key.code == KeyCode::Char('o') && key.modifiers.contains(KeyModifiers::CONTROL) {
                        app_state.current_screen = AppScreen::LoadState;
                        app_state.input_mode = InputMode::EditingLoadPath;
                        app_state.status_message = "Enter file path to load workspace (e.g., 'workspace.json'):".to_string();
                        app_state.load_state_screen.load_path_input.reset();
                        app_state.needs_redraw = true; // ADDED
                        continue;
                    }
                    if key.code == KeyCode::Char('?') {
                        app_state.current_screen = match app_state.current_screen {
                            AppScreen::Help => app_state.last_screen.unwrap_or(AppScreen::StartMenu),
                            _ => {
                                app_state.last_screen = Some(app_state.current_screen);
                                AppScreen::Help
                            },
                        };
                        app_state.needs_redraw = true; // ADDED
                        continue;
                    }
                    if key.code == KeyCode::Esc {
                        return Ok(());
                    }

                    if handle_input_for_active_mode(app_state, key.code) {
                        app_state.needs_redraw = true; // ADDED
                        continue;
                    }

                    let current_screen = app_state.current_screen;
                    match current_screen {
                        AppScreen::StartMenu => {
                            if let Some(new_screen) = lk_inside::ui::screens::start_menu::handle_input(key.code, app_state) { // MODIFIED
                                if new_screen == AppScreen::Exit {
                                    return Ok(());
                                }
                                app_state.current_screen = new_screen;
                                app_state.input_mode = InputMode::Normal; // Reset input mode
                                // app_state.needs_redraw is handled inside start_menu::handle_input
                            }
                        }
                        AppScreen::LoadData => {
                            // Updated call to load_data_screen::handle_input
                            if let Some(new_screen) = lk_inside::ui::screens::load_data_screen::handle_input(key.code, app_state) { // MODIFIED
                                match new_screen {
                                    AppScreen::Loading => {
                                        // A file was selected, now kick off the async task
                                        if let Some(file_path_str) = app_state.file_browser.selected_path().map(|p| p.to_string_lossy().into_owned()) {
                                            let file_path = PathBuf::from(&file_path_str);
                                            // Status and redraw already set in load_data_screen::handle_input
                                            let format = lk_inside::data_core::loader::detect_file_format(&file_path);
                                            let task = tokio::spawn(lk_inside::data_core::loader::load_data(file_path, format));
                                            app_state.loading_task = Some(task);
                                        } else {
                                            // This case means AppScreen::Loading was returned but selected_path() is None
                                            app_state.current_screen = AppScreen::Error;
                                            app_state.status_message = "File path not provided after Loading screen signal.".to_string();
                                            app_state.needs_redraw = true;
                                        }
                                    }
                                    _ => { // For StartMenu, Error, etc.
                                        app_state.current_screen = new_screen;
                                        app_state.input_mode = InputMode::Normal; // Reset input mode
                                        app_state.needs_redraw = true; // Redraw for screen change
                                    }
                                }
                            }
                            // If load_data_screen::handle_input returned None, it means the LoadData screen remained active
                            // and app_state.needs_redraw should have been set internally by load_data_screen if state changed.
                        }
                        AppScreen::StatisticsView => {
                            handle_statistics_input(key.code, app_state);
                            app_state.needs_redraw = true; // ADDED
                        }
                        AppScreen::FilterView => {
                            if let Some(returned_screen) = lk_inside::ui::screens::filter_view_screen::handle_input(key.code, app_state) {
                                match returned_screen {
                                    AppScreen::Error => {
                                        app_state.status_message = format!("Error applying filter: A problem occurred while processing the filter.");
                                        app_state.current_screen = AppScreen::Error;
                                        app_state.needs_redraw = true; // ADDED
                                    }
                                    _ => {
                                        app_state.current_screen = returned_screen;
                                        app_state.input_mode = InputMode::Normal; // Reset input mode
                                        app_state.needs_redraw = true; // ADDED
                                    }
                                }
                            }
                        }
                        _ => {}
                    }
                }
            }
        }
    }
}

/// Handles key input events when an input mode is active.
///
/// This function processes key presses specific to the current `InputMode`,
/// such as editing text fields for export paths, anomaly detection, clustering, etc.
///
/// # Arguments
///
/// * `app_state` - A mutable reference to the application's `AppState`.
/// * `key_code` - The `KeyCode` of the pressed key.
///
/// # Returns
///
/// `true` if the key event was handled by an active input mode, `false` otherwise.
fn handle_input_for_active_mode(app_state: &mut AppState, key_code: KeyCode) -> bool {
    match app_state.input_mode {
        InputMode::EditingSavePath => {
            if let Some(returned_screen) = lk_inside::ui::screens::save_screen::handle_input(key_code, app_state) {
                app_state.current_screen = returned_screen;
            }
            app_state.needs_redraw = true; // ADDED
            true
        }
        InputMode::EditingLoadPath => {
            if let Some(returned_screen) = lk_inside::ui::screens::load_state_screen::handle_input(key_code, app_state) {
                app_state.current_screen = returned_screen;
            }
            app_state.needs_redraw = true; // ADDED
            true
        }
        _ => false,
    }
}

/// Renders the application's user interface to the terminal frame.
///
/// This function is responsible for determining which screen to render based on
/// the `app_state.current_screen` and drawing all UI elements, including
/// the main content, status bar, and help bar.
///
/// # Arguments
///
/// * `f` - A mutable reference to the `Frame` to draw on.
/// * `app_state` - A mutable reference to the application's `AppState`.
///
/// # Returns
///
/// `Ok(())` if rendering is successful, otherwise an `anyhow::Result` error.
fn ui(f: &mut Frame, app_state: &mut AppState) -> anyhow::Result<()> {
    let size = f.area();

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(0),
            Constraint::Length(1),
            Constraint::Length(1),
        ])
        .split(size);

    let main_layout_area = chunks[0];
    let status_bar_area = chunks[1];

    match app_state.current_screen {
        AppScreen::StartMenu => {
            lk_inside::ui::screens::start_menu::render_start_menu(f, &app_state.start_menu, &app_state);
        }
        AppScreen::LoadData => {
            lk_inside::ui::screens::load_data_screen::render_load_data_screen(f, &mut app_state.file_browser);
        }
        AppScreen::Loading => {
            let block = Block::default()
                .title("Loading")
                .borders(Borders::ALL)
                .border_style(theme::BORDER_STYLE); // Ensure consistency
            let paragraph = Paragraph::new("Loading data... Please wait.")
                .block(block)
                .style(theme::TEXT_STYLE); // Ensure consistency
            f.render_widget(paragraph, main_layout_area);
        }
        AppScreen::StatisticsView => {
            lk_inside::ui::screens::statistics_screen::render_statistics_screen(f, app_state);
        }
        AppScreen::FilterView => {
            lk_inside::ui::screens::filter_view_screen::render_filter_screen(f, &app_state.filter_screen, app_state)?;
        }
        AppScreen::Error => {
            let block = Block::default()
                .title("Error")
                .borders(Borders::ALL)
                .border_style(theme::ERROR_STYLE); // ADDED
            let paragraph = Paragraph::new(format!("An error occurred: {}", app_state.status_message))
                .block(block)
                .style(theme::ERROR_STYLE); // ADDED for text color
            f.render_widget(paragraph, main_layout_area);
        }
        AppScreen::SaveState => {
            lk_inside::ui::screens::save_screen::render_save_screen(f, &app_state.save_screen, app_state); // MODIFIED
        }
        AppScreen::LoadState => {
            lk_inside::ui::screens::load_state_screen::render_load_state_screen(f, &app_state.load_state_screen, app_state); // MODIFIED
        }
        AppScreen::Exit => {}
        AppScreen::Help => {
            lk_inside::ui::screens::help_screen::render_help_screen(f, &lk_inside::ui::screens::help_screen::HelpScreen::new());
        }
        _ => {}
    }

    let status_bar_style = if app_state.current_screen == AppScreen::Error {
        theme::ERROR_STYLE
    } else {
        theme::TEXT_STYLE // Using TEXT_STYLE as default for status messages
    };
    let status_bar = Paragraph::new(app_state.status_message.as_str())
        .block(Block::default().borders(Borders::TOP).border_style(status_bar_style)) // Apply border style
        .style(status_bar_style); // Apply text style
    f.render_widget(status_bar, status_bar_area);

    Ok(())
}