Add cbd-tui

This commit is contained in:
chmanie 2023-05-17 19:08:48 +02:00
parent 91e988f722
commit c542b10738
6 changed files with 3001 additions and 1 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
/target
target

1521
cbd-tui/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
cbd-tui/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "cbd-tui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
crossterm = "0.26.1"
cynic = { version = "3.0.0-beta.3", features = ["http-reqwest"] }
ratatui = "0.20.1"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }

1167
cbd-tui/schema.graphql Normal file

File diff suppressed because it is too large Load Diff

26
cbd-tui/src/graphql.rs Normal file
View File

@ -0,0 +1,26 @@
#[cynic::schema_for_derives(file = r#"schema.graphql"#, module = "schema")]
pub mod queries {
use super::schema;
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Root")]
pub struct AllFilmsQuery {
pub all_films: Option<FilmsConnection>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct FilmsConnection {
pub films: Option<Vec<Option<Film>>>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct Film {
pub id: cynic::Id,
pub title: Option<String>,
}
}
#[allow(non_snake_case, non_camel_case_types)]
mod schema {
cynic::use_schema!(r#"schema.graphql"#);
}

273
cbd-tui/src/main.rs Normal file
View File

@ -0,0 +1,273 @@
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<T> {
state: ListState,
items: Vec<T>,
prev_selected: usize,
}
impl<T> StatefulList<T> {
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<Vec<ListNode>>,
}
struct App {
library: StatefulList<ListNode>,
queue: StatefulList<ListNode>,
}
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<B: Backend>(f: &mut Frame<B>, 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<ListItem> = 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<ListItem> = 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]);
}