songbook 0.1.1

Songbook with TUI and CLI
Documentation
mod song_formater;
mod config;
mod song_event_handler;
mod lib_event_handler;
mod screen_painter;


use std::path::PathBuf;
use std::time::{Instant, Duration};
use anyhow::Result;

use ratatui::{DefaultTerminal, Frame};
use ratatui::widgets::{ListState, TableState};

use crossterm::event::{Event, KeyEvent, KeyCode};

use songbook::song_library::lib_functions::*;
use songbook::Song;

use config::Config;


const DEFAULT_AUTOSCROLL_SPEED: Duration = Duration::from_millis(2500);



pub fn main() -> Result<()> {
    let mut terminal = ratatui::init();
    let mut app = App::new()?;
    let app_result = app.run(&mut terminal);

    ratatui::restore();

    return app_result
}


#[derive(PartialEq)]
enum Focus {
    Library,
    Song
}

#[derive(PartialEq)]
enum Screen {
    Main,
    Help
}

#[derive(PartialEq)]
enum ActionWithSelectedPaths {
    Cp,
    Mv,
    Nothing
}

struct App {
    exit: bool,
    config: Config,

    focus: Focus,
    current_screen: Screen,
    hide_lib: bool,

    is_long_command: bool,
    long_command: String,

    help_table_state: TableState,

    lib_list_state: ListState,
    lib_list: Vec<(String, PathBuf)>,
    current_dir: PathBuf,
    last_dirs: Vec<PathBuf>,
    cutted_path: Option<PathBuf>,
    copied_path: Option<PathBuf>,
    selected_paths: Vec<PathBuf>,
    action_with_selected_paths: ActionWithSelectedPaths,

    current_song: Option<(Song, PathBuf)>,
    song_area_height: Option<usize>,
    song_area_width: Option<usize>,
    show_chords: bool,
    show_rhythm: bool,
    show_fingerings: bool,
    show_notes: bool,

    scroll_y: u16,
    scroll_x: u16,
    scroll_y_max: usize,
    scroll_x_max: usize,

    autoscroll: bool,
    autoscroll_speed: Duration,
    last_scroll_time: Instant
}

impl App {
    pub fn new() -> Result<Self> {
        let (lib_list, current_dir) = get_files_in_dir(None)?;

        Ok( Self {
            exit: false,
            config: Config::new(),
            focus: Focus::Library,
            current_screen: Screen::Main,
            hide_lib: false,
            is_long_command: false,
            long_command: String::new(),
            help_table_state: TableState::new().with_selected(Some(0)),
            lib_list_state: ListState::default().with_selected(Some(0)),
            lib_list,
            current_dir,
            last_dirs: Vec::new(),
            cutted_path: None,
            copied_path: None,
            selected_paths: Vec::new(),
            action_with_selected_paths: ActionWithSelectedPaths::Nothing,
            current_song: None,
            song_area_height: None,
            song_area_width: None,
            show_chords: true,
            show_rhythm: true,
            show_fingerings: false,
            show_notes: true,
            scroll_y: 0,
            scroll_x: 0,
            scroll_y_max: 0,
            scroll_x_max: 0,
            autoscroll: false,
            autoscroll_speed: DEFAULT_AUTOSCROLL_SPEED,
            last_scroll_time: Instant::now()
        })
    }
    fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
        while !self.exit {
            terminal.draw(|frame| self.draw(frame))?;
            self.update_scroll();
            if crossterm::event::poll(Duration::from_millis(10))? {
                match crossterm::event::read()? {
                    Event::Key(key_event) => self.handle_key_event(key_event, terminal)?,
                    _ => {}
                }
            }
        }

        Ok(())
    }

    fn draw(&mut self, frame: &mut Frame) {
        match self.current_screen {
            Screen::Main => self.draw_main_screen(frame),
            Screen::Help => self.draw_help_screen(frame),
        }
    }

    fn handle_key_event(&mut self, key_event: KeyEvent, terminal: &mut DefaultTerminal) -> Result<()> {
        match self.current_screen {
            Screen::Main => self.handle_main_key_event(key_event, terminal)?,
            Screen::Help => self.handle_help_key_event(key_event)?,
        }
        Ok(())
    }

    fn handle_main_key_event(&mut self, key_event: KeyEvent, terminal: &mut DefaultTerminal) -> Result<()> {
        let mut is_song_changed = false;
        if key_event.kind.is_press() {
            match key_event.code {
                KeyCode::F(1) => self.current_screen = Screen::Help,


                KeyCode::Char(c) if self.is_long_command => self.long_command.push(c),
                KeyCode::Backspace if self.is_long_command => {
                    self.long_command.pop();
                    if self.long_command.is_empty() { self.is_long_command = false }
                },
                KeyCode::Enter if self.is_long_command => {
                    if !self.long_command.is_empty() {
                        match self.focus {
                            Focus::Library => self.handle_long_command_in_library()?,
                            Focus::Song => self.handle_long_command_in_song(&mut is_song_changed)?
                        }
                    }

                    self.is_long_command = false;
                    self.long_command.clear();
                },
                _ if self.is_long_command => {},


                KeyCode::Char('q') => self.exit = true,
                KeyCode::Tab => if !self.hide_lib { self.switch_focus() },
                _ => {
                    match self.focus {
                        Focus::Library => self.handle_lib_key_event(key_event)?,
                        Focus::Song => self.handle_song_key_event(key_event, terminal, &mut is_song_changed)?
                    }
                }
            }
        }

        if let Some( (song, _p) ) = &mut self.current_song {
            if let Some(speed) = song.metadata.autoscroll_speed &&
                Duration::from_millis(speed) == self.autoscroll_speed {
            } else {
                if let Ok(new_speed) = self.autoscroll_speed.as_millis().try_into() {
                    is_song_changed = true;
                    song.metadata.autoscroll_speed = Some(new_speed);
                }
            }
        }


        if is_song_changed {
            if let Some( (song, path) ) = &self.current_song {
                save(song, path)?;
            }
        }

        Ok(())
    }

    fn handle_help_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
        if key_event.kind.is_press() {
            match key_event.code {
                KeyCode::Esc => self.current_screen = Screen::Main,
                KeyCode::Char('j') | KeyCode::Down => self.help_table_state.select_next(),
                KeyCode::Char('k') | KeyCode::Up => self.help_table_state.select_previous(),
                _ => {},
            }
        }
        Ok(())
    }



    fn update_scroll(&mut self) {
        if !self.autoscroll { return }
        if self.last_scroll_time.elapsed() < self.autoscroll_speed { return }

        if self.scroll_y_max > self.scroll_y.into() {
            self.scroll_y += 1;
            self.last_scroll_time = Instant::now();
        } else { self.autoscroll = false }
    }

    fn switch_focus(&mut self) {
        self.focus = match self.focus {
            Focus::Library => Focus::Song,
            Focus::Song => Focus::Library
        }
    }

    fn switch_lib(&mut self) {
        if self.hide_lib {
            self.hide_lib = false;
            self.focus = Focus::Library;
        } else {
            self.hide_lib = true
        }
    }

    fn update_lib_list(&mut self) -> Result<()> {
        (self.lib_list, self.current_dir) = get_files_in_dir( Some(&self.current_dir) )?;
        Ok(())
    }
}