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;
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(())
}
#[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();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let res = run_app(&mut terminal, &mut app_state).await;
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 { 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; }
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; }
Err(e) => {
app_state.status_message = format!("Error validating data: {}", e);
app_state.current_screen = AppScreen::Error;
app_state.needs_redraw = true; }
}
}
Err(e) => {
app_state.status_message = format!("Error loading data: {}", e);
app_state.current_screen = AppScreen::Error;
app_state.needs_redraw = true; }
}
}
}
if event::poll(std::time::Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
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; 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; 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; 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; continue;
}
if key.code == KeyCode::Esc {
return Ok(());
}
if handle_input_for_active_mode(app_state, key.code) {
app_state.needs_redraw = true; 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) { if new_screen == AppScreen::Exit {
return Ok(());
}
app_state.current_screen = new_screen;
app_state.input_mode = InputMode::Normal; }
}
AppScreen::LoadData => {
if let Some(new_screen) = lk_inside::ui::screens::load_data_screen::handle_input(key.code, app_state) { match new_screen {
AppScreen::Loading => {
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);
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 {
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;
}
}
_ => { app_state.current_screen = new_screen;
app_state.input_mode = InputMode::Normal; app_state.needs_redraw = true; }
}
}
}
AppScreen::StatisticsView => {
handle_statistics_input(key.code, app_state);
app_state.needs_redraw = true; }
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; }
_ => {
app_state.current_screen = returned_screen;
app_state.input_mode = InputMode::Normal; app_state.needs_redraw = true; }
}
}
}
_ => {}
}
}
}
}
}
}
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; 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; true
}
_ => false,
}
}
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); let paragraph = Paragraph::new("Loading data... Please wait.")
.block(block)
.style(theme::TEXT_STYLE); 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); let paragraph = Paragraph::new(format!("An error occurred: {}", app_state.status_message))
.block(block)
.style(theme::ERROR_STYLE); 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); }
AppScreen::LoadState => {
lk_inside::ui::screens::load_state_screen::render_load_state_screen(f, &app_state.load_state_screen, app_state); }
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 };
let status_bar = Paragraph::new(app_state.status_message.as_str())
.block(Block::default().borders(Borders::TOP).border_style(status_bar_style)) .style(status_bar_style); f.render_widget(status_bar, status_bar_area);
Ok(())
}