#![warn(clippy::pedantic)]
#![allow(
clippy::wildcard_imports,
clippy::float_cmp,
clippy::cast_lossless,
clippy::too_many_lines,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
use browser::Browser;
use crossterm::{event::*, terminal::*, *};
use gonk_database::Index;
use gonk_player::Player;
use playlist::{Mode as PlaylistMode, Playlist};
use queue::Queue;
use search::{Mode as SearchMode, Search};
use settings::Settings;
use std::{
io::{stdout, Stdout},
path::Path,
thread,
time::{Duration, Instant},
};
use tui::{backend::CrosstermBackend, layout::*, style::Color, Terminal};
mod browser;
mod log;
mod playlist;
mod queue;
mod search;
mod settings;
mod widgets;
type Frame<'a> = tui::Frame<'a, CrosstermBackend<Stdout>>;
pub struct Colors {
pub number: Color,
pub title: Color,
pub album: Color,
pub artist: Color,
pub seeker: Color,
}
const COLORS: Colors = Colors {
number: Color::Green,
title: Color::Cyan,
album: Color::Magenta,
artist: Color::Blue,
seeker: Color::White,
};
#[derive(PartialEq, Eq)]
pub enum Mode {
Browser,
Queue,
Search,
Playlist,
Settings,
}
pub trait Input {
fn up(&mut self);
fn down(&mut self);
fn left(&mut self);
fn right(&mut self);
}
fn save_queue(player: &Player) {
gonk_database::update_queue(
&player.songs.data,
player.songs.index().unwrap_or(0) as u16,
player.elapsed().as_secs_f32(),
);
}
fn draw_log(f: &mut Frame) -> Rect {
if log::message().is_some() {
let area = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(2), Constraint::Length(3)])
.split(f.size());
log::draw(area[1], f);
area[0]
} else {
f.size()
}
}
fn main() {
if gonk_database::init().is_err() {
return println!("Database is corrupted! Please close all instances of gonk then relaunch or run `gonk reset`.");
}
log::init();
let mut scan_handle = None;
let args: Vec<String> = std::env::args().skip(1).collect();
if !args.is_empty() {
match args[0].as_str() {
"add" => {
if args.len() == 1 {
return println!("Usage: gonk add <path>");
}
let path = args[1..].join(" ");
if Path::new(&path).exists() {
gonk_database::update_music_folder(path.as_str());
scan_handle = Some(gonk_database::scan(path));
} else {
return println!("Invalid path.");
}
}
"reset" => {
return match gonk_database::reset() {
Ok(_) => println!("Files reset!"),
Err(e) => println!("Failed to reset database! {}", e),
};
}
"help" | "--help" => {
println!("Usage");
println!(" gonk [<command> <args>]");
println!();
println!("Options");
println!(" add <path> Add music to the library");
println!(" reset Reset the database");
return;
}
_ if !args.is_empty() => return println!("Invalid command."),
_ => (),
}
}
let orig_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
disable_raw_mode().unwrap();
execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
orig_hook(panic_info);
std::process::exit(1);
}));
let mut terminal = Terminal::new(CrosstermBackend::new(stdout())).unwrap();
execute!(
terminal.backend_mut(),
EnterAlternateScreen,
EnableMouseCapture,
)
.unwrap();
enable_raw_mode().unwrap();
terminal.clear().unwrap();
let (songs, index, elapsed) = gonk_database::get_queue();
let songs = Index::new(songs, index);
let volume = gonk_database::volume();
let device = gonk_database::get_output_device();
let player_thread = thread::spawn(move || Player::new(device, volume, songs, elapsed));
let mut browser = Browser::new();
let mut queue = Queue::new();
let mut playlist = Playlist::new();
let mut settings = Settings::new();
let mut search = Search::new();
let mut mode = Mode::Browser;
let mut last_tick = Instant::now();
let mut busy = false;
let mut dots: usize = 1;
let mut scan_timer: Option<Instant> = None;
let mut player = player_thread.join().unwrap();
if !player.songs.is_empty() && scan_handle.is_none() {
mode = Mode::Queue;
}
loop {
if let Some(h) = &scan_handle {
if h.is_finished() {
browser::refresh(&mut browser);
search::refresh_cache(&mut search);
search::refresh_results(&mut search);
if let Some(time) = scan_timer {
log!(
"Finished adding {} files in {:.2} seconds.",
gonk_database::len(),
time.elapsed().as_secs_f32()
);
}
scan_timer = None;
scan_handle = None;
} else {
busy = true;
if scan_timer.is_none() {
scan_timer = Some(Instant::now());
log!("Scanning for files.");
}
}
} else {
busy = false;
}
if last_tick.elapsed() >= Duration::from_millis(150) {
if busy && scan_timer.is_some() {
if dots < 3 {
dots += 1;
} else {
dots = 1;
}
log!("Scanning for files{}", ".".repeat(dots));
}
last_tick = Instant::now();
}
queue.len = player.songs.len();
match player.update() {
Ok(_) => (),
Err(e) => log!("{}", e),
};
terminal
.draw(|f| {
let top = draw_log(f);
match mode {
Mode::Browser => browser::draw(&mut browser, top, f, None),
Mode::Queue => queue::draw(&mut queue, &mut player, f, None),
Mode::Search => search::draw(&mut search, top, f, None),
Mode::Playlist => playlist::draw(&mut playlist, top, f, None),
Mode::Settings => settings::draw(&mut settings, top, f),
};
})
.unwrap();
let input_search = search.mode == SearchMode::Search && mode == Mode::Search;
let input_playlist = playlist.mode == PlaylistMode::Popup && mode == Mode::Playlist;
let input = match mode {
Mode::Browser => &mut browser as &mut dyn Input,
Mode::Queue => &mut queue as &mut dyn Input,
Mode::Search => &mut search as &mut dyn Input,
Mode::Playlist => &mut playlist as &mut dyn Input,
Mode::Settings => &mut settings as &mut dyn Input,
};
if event::poll(Duration::from_millis(2)).unwrap() {
match event::read().unwrap() {
Event::Key(event) => {
let shift = event.modifiers == KeyModifiers::SHIFT;
let control = event.modifiers == KeyModifiers::CONTROL;
match event.code {
KeyCode::Char('c') if control => break,
KeyCode::Char(c) if input_search => {
if control && c == 'w' {
search::on_backspace(&mut search, true);
} else {
match c {
',' if search.query.is_empty() => mode = Mode::Settings,
'.' if search.query.is_empty() => mode = Mode::Playlist,
'/' if search.query.is_empty() => (),
_ => {
search.query.push(c);
search.query_changed = true;
}
};
}
}
KeyCode::Char(c) if input_playlist => {
if control && c == 'w' {
playlist::on_backspace(&mut playlist, true);
} else {
playlist.changed = true;
playlist.search_query.push(c);
}
}
KeyCode::Char(' ') => match player.toggle_playback() {
Ok(_) => (),
Err(e) => log!("{}", e),
},
KeyCode::Char('C') if shift => {
player.clear_except_playing();
queue.ui.select(Some(0));
}
KeyCode::Char('c') => {
player.clear();
queue.ui.select(Some(0));
}
KeyCode::Char('x') => match mode {
Mode::Queue => queue::delete(&mut queue, &mut player),
Mode::Playlist => playlist::delete(&mut playlist, false),
_ => (),
},
KeyCode::Char('X') => {
if let Mode::Playlist = mode {
playlist::delete(&mut playlist, true);
}
}
KeyCode::Char('u') if mode == Mode::Browser || mode == Mode::Playlist => {
let folder = gonk_database::get_music_folder().to_string();
scan_handle = Some(gonk_database::scan(folder));
playlist.playlists = Index::from(gonk_database::playlists());
}
KeyCode::Char('q') => match player.seek_by(-10.0) {
Ok(_) => (),
Err(e) => log!("{}", e),
},
KeyCode::Char('e') => match player.seek_by(10.0) {
Ok(_) => (),
Err(e) => log!("{}", e),
},
KeyCode::Char('a') => match player.previous() {
Ok(_) => (),
Err(e) => log!("{}", e),
},
KeyCode::Char('d') => match player.next() {
Ok(_) => (),
Err(e) => log!("{}", e),
},
KeyCode::Char('w') => {
player.volume_up();
gonk_database::update_volume(player.volume);
}
KeyCode::Char('s') => {
player.volume_down();
gonk_database::update_volume(player.volume);
}
KeyCode::Char(',') => mode = Mode::Settings,
KeyCode::Char('.') => mode = Mode::Playlist,
KeyCode::Char('/') => {
if mode == Mode::Search {
if search.mode == SearchMode::Select {
search.results.select(None);
search.mode = SearchMode::Search;
}
} else {
mode = Mode::Search;
}
}
KeyCode::Tab => {
mode = match mode {
Mode::Browser | Mode::Settings | Mode::Search => Mode::Queue,
Mode::Queue | Mode::Playlist => Mode::Browser,
};
}
KeyCode::Esc => match mode {
Mode::Search => search::on_escape(&mut search),
Mode::Playlist => playlist::on_escape(&mut playlist),
_ => (),
},
KeyCode::Enter if shift => match mode {
Mode::Browser => {
let songs = browser::get_selected(&browser);
playlist::add(&mut playlist, &songs);
mode = Mode::Playlist;
}
Mode::Queue => {
if let Some(song) = player.songs.selected() {
playlist::add(&mut playlist, &[song.clone()]);
mode = Mode::Playlist;
}
}
Mode::Search => {
if let Some(songs) = search::on_enter(&mut search) {
playlist::add(&mut playlist, &songs);
mode = Mode::Playlist;
}
}
_ => (),
},
KeyCode::Enter => match mode {
Mode::Browser => {
let songs = browser::get_selected(&browser);
match player.add_songs(&songs) {
Ok(_) => (),
Err(e) => log!("{}", e),
}
save_queue(&player);
}
Mode::Queue => {
if let Some(i) = queue.ui.index() {
match player.play_index(i) {
Ok(_) => (),
Err(e) => log!("{}", e),
}
}
}
Mode::Search => {
if let Some(songs) = search::on_enter(&mut search) {
match player.add_songs(&songs) {
Ok(_) => (),
Err(e) => log!("{}", e),
}
save_queue(&player);
}
}
Mode::Settings => settings::on_enter(&mut settings, &mut player),
Mode::Playlist => playlist::on_enter(&mut playlist, &mut player),
},
KeyCode::Backspace => match mode {
Mode::Search => search::on_backspace(&mut search, control),
Mode::Playlist => playlist::on_backspace(&mut playlist, control),
_ => (),
},
KeyCode::Up => input.up(),
KeyCode::Down => input.down(),
KeyCode::Left => input.left(),
KeyCode::Right => input.right(),
KeyCode::Char('1' | '!') => {
queue::constraint(&mut queue, 0, shift);
}
KeyCode::Char('2' | '@') => {
queue::constraint(&mut queue, 1, shift);
}
KeyCode::Char('3' | '#') => {
queue::constraint(&mut queue, 2, shift);
}
KeyCode::Char(c) => match c {
'h' => input.left(),
'j' => input.down(),
'k' => input.up(),
'l' => input.right(),
_ => (),
},
_ => (),
}
}
Event::Mouse(event) => match event.kind {
MouseEventKind::ScrollUp => input.up(),
MouseEventKind::ScrollDown => input.down(),
MouseEventKind::Down(_) => match mode {
Mode::Browser => {
terminal
.draw(|f| {
let top = draw_log(f);
browser::draw(&mut browser, top, f, Some(event));
})
.unwrap();
}
Mode::Queue => {
terminal
.draw(|f| queue::draw(&mut queue, &mut player, f, Some(event)))
.unwrap();
}
Mode::Playlist => {
terminal
.draw(|f| {
let top = draw_log(f);
playlist::draw(&mut playlist, top, f, Some(event));
})
.unwrap();
}
Mode::Search => {
terminal
.draw(|f| {
let top = draw_log(f);
search::draw(&mut search, top, f, Some(event));
})
.unwrap();
}
Mode::Settings => (),
},
_ => (),
},
Event::Resize(..) => (),
}
}
}
save_queue(&player);
disable_raw_mode().unwrap();
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)
.unwrap();
}