pub mod palette;
pub mod render;
pub mod sampler;
use std::time::Duration;
pub use playr_app::dispatch::Confirm;
pub use playr_app::model::{Input, Snapshot};
pub use playr_app::{Theme, View};
use ratatui::crossterm::event::{
self, Event as TermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
};
use rusqlite::Connection;
use playr_app::action::{Action, Key, Keymap, Modifiers};
use playr_app::command::CommandLine;
use playr_app::config::Config;
use playr_app::dispatch::Frontend;
use playr_app::message::Message;
use playr_app::model::Model;
use playr_app::sampler::Sampler;
use playr_core::audio::{Player, State};
use playr_core::db::query::Playlist;
use playr_core::db::Track;
pub struct App {
model: Model,
offsets: Offsets,
help_scroll: usize,
colour: bool,
}
#[derive(Debug, Clone, Copy, Default)]
struct Offsets {
library: usize,
selection: usize,
playlists: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Scroll {
pub row: Option<usize>,
pub offset: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Lists {
pub library: Scroll,
pub selection: Scroll,
pub playlists: Scroll,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Drawn {
pub lists: Lists,
pub help_scroll: usize,
pub zoom: u32,
pub scale: Option<playr_app::sampler::Scale>,
}
static NO_INPUT: Input = Input::None;
pub struct Screen<'a> {
pub view: View,
pub snapshot: &'a Snapshot,
pub all: &'a [Track],
pub results: Option<&'a [Track]>,
pub playing: &'a [Track],
pub selection: &'a [Track],
pub playlists: &'a [Playlist],
pub input: &'a Input,
pub keys: &'a Keymap,
pub sampler: &'a Sampler,
pub help_scroll: usize,
pub message: Option<&'a str>,
pub lists: Lists,
pub colour: bool,
pub theme: Theme,
}
impl<'a> Screen<'a> {
pub fn new(
view: View,
snapshot: &'a Snapshot,
keys: &'a Keymap,
sampler: &'a Sampler,
) -> Screen<'a> {
Screen {
view,
snapshot,
all: &[],
results: None,
playing: &[],
selection: &[],
playlists: &[],
input: &NO_INPUT,
keys,
sampler,
help_scroll: 0,
message: None,
lists: Lists::default(),
colour: true,
theme: Theme::Dark,
}
}
pub fn palette(&self) -> &'static palette::Palette {
palette::of(self.theme)
}
pub fn visible(&self) -> &[Track] {
self.results.unwrap_or(self.all)
}
}
impl App {
pub fn new(conn: Connection, player: Player) -> Self {
Self::with_selection(conn, player, Vec::new())
}
pub fn with_selection(conn: Connection, player: Player, tracks: Vec<Track>) -> Self {
Self::configured(conn, player, tracks, Config::default())
}
pub fn configured(
conn: Connection,
player: Player,
tracks: Vec<Track>,
config: Config,
) -> Self {
let mut model = Model::new(conn, player, tracks, config);
model.set_display(playr_app::Display::Braille);
App {
model,
offsets: Offsets::default(),
help_scroll: 0,
colour: true,
}
}
pub fn set_library_path(&mut self, path: std::path::PathBuf) {
self.model.session_mut().set_library_path(path);
}
pub fn set_colour(&mut self, colour: bool) {
self.colour = colour;
}
pub fn screen(&self) -> Screen<'_> {
let m = &self.model;
let c = m.cursors();
let o = self.offsets;
let scroll = |row, offset| Scroll { row, offset };
Screen {
all: m.session().tracks(),
results: m.results(),
playing: m.playing(),
selection: m.session().selection(),
playlists: m.session().playlists(),
input: m.input(),
help_scroll: self.help_scroll,
colour: self.colour,
theme: m.theme(),
message: m.message_text(),
lists: Lists {
library: scroll(c.library, o.library),
selection: scroll(c.selection, o.selection),
playlists: scroll(c.playlists, o.playlists),
},
..Screen::new(m.view(), m.snapshot(), m.keymap(), m.sampler())
}
}
pub fn drawn(&mut self, drawn: Drawn) {
let Lists {
library,
selection,
playlists,
} = drawn.lists;
self.offsets = Offsets {
library: library.offset,
selection: selection.offset,
playlists: playlists.offset,
};
self.model.set_cursor(View::Library, library.row);
self.model.set_cursor(View::Selection, selection.row);
self.model.set_cursor(View::Playlists, playlists.row);
self.model.set_zoom(drawn.zoom);
if let Some(scale) = drawn.scale {
self.model.set_scale(scale);
}
self.help_scroll = drawn.help_scroll;
}
pub fn run(mut self, terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
while !self.model.quitting() {
self.refresh();
let mut drawn = None;
terminal.draw(|f| drawn = Some(render::draw(&self.screen(), f)))?;
if let Some(drawn) = drawn {
self.drawn(drawn);
}
if event::poll(Duration::from_millis(200))? {
if let TermEvent::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
self.on_key(key);
}
}
}
self.model.expire_message();
}
Ok(())
}
pub fn refresh(&mut self) {
self.model.refresh();
}
pub fn message(&self) -> Option<&Message> {
self.model.message()
}
pub fn quitting(&self) -> bool {
self.model.quitting()
}
pub fn on_key(&mut self, key: KeyEvent) {
self.model.follow_player();
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
self.model.quit();
return;
}
match self.model.input() {
Input::Confirm(_) => self.model.answer(typed(&key) == Some('y')),
Input::Help | Input::CommandHelp => {
match key.code {
KeyCode::Char('j') | KeyCode::Down => self.help_scroll += 1,
KeyCode::Char('k') | KeyCode::Up => {
self.help_scroll = self.help_scroll.saturating_sub(1)
}
KeyCode::PageDown => self.help_scroll += 10,
KeyCode::PageUp => self.help_scroll = self.help_scroll.saturating_sub(10),
_ => self.model.set_input(Input::None),
}
}
Input::Command(line) => {
let line = line.clone();
self.command_key(key, line);
}
Input::Search(buf) => {
let buf = buf.clone();
self.search_key(key, buf);
}
Input::SavePlaylist(buf) => {
let buf = buf.clone();
self.save_key(key, buf);
}
Input::RenamePlaylist { from, name } => {
let (from, name) = (from.clone(), name.clone());
self.rename_key(key, from, name);
}
Input::None => {
let bound = key_of(&key)
.and_then(|k| self.model.keymap().lookup(k, self.model.view()).cloned());
if let Some(action) = bound {
self.model.perform(action);
}
}
}
self.follow_help();
}
pub fn perform(&mut self, action: Action) {
self.model.perform(action);
self.follow_help();
}
fn follow_help(&mut self) {
if !matches!(self.model.input(), Input::Help | Input::CommandHelp) {
self.help_scroll = 0;
}
}
fn command_key(&mut self, key: KeyEvent, mut line: CommandLine) {
match key.code {
KeyCode::Esc => return self.model.set_input(Input::None),
KeyCode::Enter => return self.model.run_command(&line.text),
KeyCode::Backspace if !line.pop() => return self.model.set_input(Input::None),
KeyCode::Tab | KeyCode::BackTab => {
let names: Vec<String> = self
.model
.session()
.playlists()
.iter()
.map(|p| p.name.clone())
.collect();
line.complete(key.code == KeyCode::Tab, self.model.view(), &names);
}
KeyCode::Up => line.recall(true, self.model.history()),
KeyCode::Down => line.recall(false, self.model.history()),
_ => {
if let Some(c) = typed(&key) {
line.push(c);
}
}
}
self.model.set_input(Input::Command(line));
}
fn search_key(&mut self, key: KeyEvent, mut buf: String) {
match key.code {
KeyCode::Esc => self.model.end_search(false),
KeyCode::Enter => self.model.end_search(true),
KeyCode::Backspace => {
buf.pop();
self.model.search_as_typed(buf);
}
KeyCode::Char(c) if typed(&key).is_some() => {
buf.push(c);
self.model.search_as_typed(buf);
}
_ => self.model.set_input(Input::Search(buf)),
}
}
fn save_key(&mut self, key: KeyEvent, mut buf: String) {
match key.code {
KeyCode::Esc => self.model.set_input(Input::None),
KeyCode::Enter => self.model.save_as(&buf),
KeyCode::Backspace => {
buf.pop();
self.model.set_input(Input::SavePlaylist(buf));
}
KeyCode::Char(c) if typed(&key).is_some() => {
buf.push(c);
self.model.set_input(Input::SavePlaylist(buf));
}
_ => self.model.set_input(Input::SavePlaylist(buf)),
}
}
fn rename_key(&mut self, key: KeyEvent, from: Playlist, mut name: String) {
match key.code {
KeyCode::Esc => self.model.set_input(Input::None),
KeyCode::Enter => self.model.rename_to(&from, &name),
KeyCode::Backspace => {
name.pop();
self.model.set_input(Input::RenamePlaylist { from, name });
}
KeyCode::Char(c) if typed(&key).is_some() => {
name.push(c);
self.model.set_input(Input::RenamePlaylist { from, name });
}
_ => self.model.set_input(Input::RenamePlaylist { from, name }),
}
}
}
pub fn key_of(event: &KeyEvent) -> Option<Key> {
use playr_app::action::KeyCode as K;
let code = match event.code {
KeyCode::Char(c) => K::Char(c),
KeyCode::F(n) => K::F(n),
KeyCode::Enter => K::Enter,
KeyCode::Esc => K::Esc,
KeyCode::Tab => K::Tab,
KeyCode::BackTab => K::BackTab,
KeyCode::Backspace => K::Backspace,
KeyCode::Delete => K::Delete,
KeyCode::Insert => K::Insert,
KeyCode::Up => K::Up,
KeyCode::Down => K::Down,
KeyCode::Left => K::Left,
KeyCode::Right => K::Right,
KeyCode::Home => K::Home,
KeyCode::End => K::End,
KeyCode::PageUp => K::PageUp,
KeyCode::PageDown => K::PageDown,
_ => return None,
};
let mods = Modifiers {
ctrl: event.modifiers.contains(KeyModifiers::CONTROL),
alt: event.modifiers.contains(KeyModifiers::ALT),
shift: event.modifiers.contains(KeyModifiers::SHIFT),
};
Some(Key::new(code, mods))
}
pub fn view_title(view: View) -> &'static str {
view.title()
}
pub fn confirm_prompt(confirm: &Confirm) -> String {
format!("{} (y/n)", confirm.question())
}
fn typed(key: &KeyEvent) -> Option<char> {
match key.code {
KeyCode::Char(c)
if !key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
{
Some(c)
}
_ => None,
}
}
pub fn state_glyph(s: State) -> &'static str {
match s {
State::Playing => ">",
State::Paused => "||",
State::Stopped => "#",
}
}