mod graphql; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Corner, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, Frame, Terminal, }; use std::{ error::Error, io, println, time::{Duration, Instant}, vec, }; use cynic::{http::ReqwestExt, QueryBuilder}; use tokio::task; struct StatefulList { state: ListState, items: Vec, prev_selected: usize, } impl StatefulList { fn default() -> Self { let mut state = ListState::default(); Self { state, items: Vec::default(), prev_selected: 0, } } fn next(&mut self) { if let Some(i) = self.state.selected() { let next = if i == self.items.len() - 1 { 0 } else { i + 1 }; self.state.select(Some(next)); } else { self.state.select(Some(0)); } } fn prev(&mut self) { if let Some(i) = self.state.selected() { let prev = if i == 0 { self.items.len() - 1 } else { i - 1 }; self.state.select(Some(prev)); } else { self.state.select(Some(0)); } } fn is_focused(&self) -> bool { self.state.selected().is_some() } fn focus(&mut self) { if self.is_focused() { return; } self.state.select(Some(self.prev_selected)); } fn blur(&mut self) { if !self.is_focused() { return; } if let Some(i) = self.state.selected() { self.prev_selected = i; } else { self.prev_selected = 0; } self.state.select(None); } } struct ListNode { name: String, children: Option>, } struct App { library: StatefulList, queue: StatefulList, } impl App { fn new() -> App { let mut library = StatefulList::default(); library.items.push(ListNode { name: "Tidal".to_owned(), children: None, }); library.items.push(ListNode { name: "YouTube".to_owned(), children: None, }); library.focus(); let mut queue = StatefulList::default(); queue.items.push(ListNode { name: "GLaDOS - Still Alive".to_owned(), children: None, }); queue.items.push(ListNode { name: "Floppotron - Star Wars Theme".to_owned(), children: None, }); queue.items.push(ListNode { name: "Starcop - Starcop".to_owned(), children: None, }); queue.items.push(ListNode { name: "Efence - Airglow".to_owned(), children: None, }); App { library, queue } } fn cycle_active(&mut self) { if self.library.is_focused() { self.library.blur(); self.queue.focus(); } else { self.library.focus(); self.queue.blur(); } } } #[tokio::main] async fn main() { task::spawn_blocking(run_ui).await.unwrap(); } fn run_ui() { // let operation = graphql::queries::AllFilmsQuery::build(()); // // let client = reqwest::Client::new(); // // let response = client // .post("https://swapi-graphql.netlify.app/.netlify/functions/index") // .run_graphql(operation) // .await // .unwrap(); // // if let Some(data) = response.data { // if let Some(films) = data.all_films { // if let Some(list) = films.films { // list.iter().for_each(|mut f| { // if let Some(film) = f { // if let Some(title) = &film.title { // app.library.items.push(ListNode { // name: title.to_string(), // children: None, // }) // } // } // }) // } // } // } // setup terminal enable_raw_mode().unwrap(); let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend).unwrap(); // create app and run it let mut app = App::new(); loop { terminal.draw(|f| ui(f, &mut app)).unwrap(); if let Event::Key(key) = event::read().unwrap() { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Char('q') => break, KeyCode::Char('j') => { if app.library.is_focused() { app.library.next(); } else { app.queue.next(); } } KeyCode::Char('k') => { if app.library.is_focused() { app.library.prev(); } else { app.queue.prev(); } } KeyCode::Tab => app.cycle_active(), _ => {} } } } } // restore terminal disable_raw_mode().unwrap(); execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture ) .unwrap(); terminal.show_cursor().unwrap(); } fn ui(f: &mut Frame, app: &mut App) { let size = f.size(); let main = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(size); let library_items: Vec = app .library .items .iter() // FIXME: why to_string() ?? .map(|i| ListItem::new(Span::from(i.name.to_string()))) .collect(); let library_list = List::new(library_items) .block(Block::default().borders(Borders::ALL).title("Library")) .highlight_style( Style::default() .bg(Color::LightBlue) .add_modifier(Modifier::BOLD), ); f.render_stateful_widget(library_list, main[0], &mut app.library.state); let now_playing = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(70), Constraint::Max(10)].as_ref()) .split(main[1]); let queue_items: Vec = app .queue .items .iter() // FIXME: why to_string() ?? .map(|i| ListItem::new(Span::from(i.name.to_string()))) .collect(); let queue_list = List::new(queue_items) .block(Block::default().borders(Borders::ALL).title("Queue")) .highlight_style( Style::default() .bg(Color::LightBlue) .add_modifier(Modifier::BOLD), ); f.render_stateful_widget(queue_list, now_playing[0], &mut app.queue.state); let media_info = Block::default() .title("Now playing") .borders(Borders::ALL) .style(Style::default().bg(Color::Black)); f.render_widget(media_info, now_playing[1]); }