mod cache; mod config; mod player; mod scanner; mod state; mod ui; use anyhow::{Context, Result}; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{backend::CrosstermBackend, Terminal}; use state::{AppState, PlayerState}; use std::io; use tracing_subscriber; #[tokio::main] async fn main() -> Result<()> { // Initialize logging tracing_subscriber::fmt::init(); // Get config and cache paths let config_path = config::get_config_path()?; let cache_dir = cache::get_cache_dir()?; // Load config let config = config::Config::load(&config_path) .context("Failed to load config")?; tracing::info!("Loaded config from {:?}", config_path); // Load cache let mut cache = cache::Cache::load(&cache_dir) .context("Failed to load cache")?; tracing::info!("Loaded cache from {:?}", cache_dir); // If cache is empty and we have scan paths, perform initial scan if cache.file_tree.is_empty() && !config.scan_paths.paths.is_empty() { tracing::info!("Cache is empty, performing initial scan..."); cache = scanner::scan_paths(&config.scan_paths.paths)?; cache.save(&cache_dir)?; tracing::info!("Initial scan complete, cache saved"); } // Initialize player let _player = player::Player::new()?; // Initialize app state let mut state = AppState::new(cache, config); // 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)?; // Run app let result = run_app(&mut terminal, &mut state).await; // Restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; result } async fn run_app( terminal: &mut Terminal, state: &mut AppState, ) -> Result<()> { loop { terminal.draw(|f| ui::render(f, state))?; if event::poll(std::time::Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { handle_key_event(state, key.code).await?; } } } if state.should_quit { break; } } Ok(()) } async fn handle_key_event(state: &mut AppState, key_code: KeyCode) -> Result<()> { match key_code { KeyCode::Char('q') => { state.should_quit = true; } KeyCode::Char('k') => { state.move_selection_up(); } KeyCode::Char('j') => { state.move_selection_down(); } KeyCode::Char('h') => { state.collapse_selected(); } KeyCode::Char('l') => { state.expand_selected(); } KeyCode::Char('t') => { state.toggle_mark(); } KeyCode::Char('c') => { state.clear_marks(); } KeyCode::Char('n') => { state.play_next(); if let Some(ref path) = state.current_file { tracing::info!("Next track: {:?}", path); } } KeyCode::Char('p') => { state.play_previous(); if let Some(ref path) = state.current_file { tracing::info!("Previous track: {:?}", path); } } KeyCode::Enter => { state.play_selection(); if let Some(ref path) = state.current_file { tracing::info!("Playing: {:?} (playlist: {} tracks)", path, state.playlist.len()); } } KeyCode::Char(' ') => { match state.player_state { PlayerState::Playing => { state.player_state = PlayerState::Paused; tracing::info!("Paused"); } PlayerState::Paused => { state.player_state = PlayerState::Playing; tracing::info!("Resumed"); } PlayerState::Stopped => {} } } KeyCode::Char('r') => { tracing::info!("Rescanning..."); let cache_dir = cache::get_cache_dir()?; let new_cache = scanner::scan_paths(&state.config.scan_paths.paths)?; new_cache.save(&cache_dir)?; state.cache = new_cache; state.refresh_flattened_items(); tracing::info!("Rescan complete"); } _ => {} } Ok(()) }