chore: initialise rust project and TUI
This commit is contained in:
parent
bbcb18bde4
commit
f2a8cf9b06
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
linker = "clang"
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
1604
Cargo.lock
generated
Normal file
1604
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -4,3 +4,10 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
crossterm = { version = "0.27.0", features = [ "event-stream" ] }
|
||||||
|
futures = "0.3.30"
|
||||||
|
ratatui = "0.26.3"
|
||||||
|
reqwest = { version = "0.12.4", features = [ "json" ] }
|
||||||
|
serde = { version = "1.0.203", features = [ "derive" ] }
|
||||||
|
tokio = { version = "1.37.0", features = [ "full" ] }
|
||||||
|
tui-input = "0.8.0"
|
||||||
|
|
28
src/api.rs
Normal file
28
src/api.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use reqwest::Response;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct SearchResults {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub url: String,
|
||||||
|
pub image: String,
|
||||||
|
pub releaseDate: String,
|
||||||
|
pub subOrDub: String,
|
||||||
|
}
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct Search {
|
||||||
|
pub currentPage: String,
|
||||||
|
pub hasNextPage: bool,
|
||||||
|
pub results: Vec<SearchResults>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_anime() -> Result<Search, reqwest::Error> {
|
||||||
|
let resp = reqwest::get("https://altoku-api.vercel.app/anime/gogoanime/mushoku?page=1")
|
||||||
|
.await.expect("Failed to get from API")
|
||||||
|
.json::<Search>()
|
||||||
|
.await;
|
||||||
|
return resp;
|
||||||
|
}
|
41
src/app.rs
Normal file
41
src/app.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use std::error;
|
||||||
|
|
||||||
|
use tui_input::Input;
|
||||||
|
|
||||||
|
/// Application result type.
|
||||||
|
pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
|
||||||
|
|
||||||
|
/// Application state
|
||||||
|
pub struct App {
|
||||||
|
/// Current value of the input box
|
||||||
|
pub input: Input,
|
||||||
|
/// Search query
|
||||||
|
pub query: String,
|
||||||
|
/// Is the app running?
|
||||||
|
pub running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for App {
|
||||||
|
fn default() -> App {
|
||||||
|
App {
|
||||||
|
input: Input::default(),
|
||||||
|
query: "".to_string(),
|
||||||
|
running: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// Constructs a new instance of [`App`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles the tick event of the terminal.
|
||||||
|
pub fn tick(&self) {}
|
||||||
|
|
||||||
|
/// Set running to false to quit the application.
|
||||||
|
pub fn quit(&mut self) {
|
||||||
|
self.running = false;
|
||||||
|
}
|
||||||
|
}
|
97
src/event.rs
Normal file
97
src/event.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent};
|
||||||
|
use futures::{FutureExt, StreamExt};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::app::AppResult;
|
||||||
|
|
||||||
|
/// Terminal events.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
/// Terminal tick.
|
||||||
|
Tick,
|
||||||
|
/// Key press.
|
||||||
|
Key(KeyEvent),
|
||||||
|
/// Mouse click/scroll.
|
||||||
|
Mouse(MouseEvent),
|
||||||
|
/// Terminal resize.
|
||||||
|
Resize(u16, u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminal event handler.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EventHandler {
|
||||||
|
/// Event sender channel.
|
||||||
|
sender: mpsc::UnboundedSender<Event>,
|
||||||
|
/// Event receiver channel.
|
||||||
|
receiver: mpsc::UnboundedReceiver<Event>,
|
||||||
|
/// Event handler thread.
|
||||||
|
handler: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventHandler {
|
||||||
|
/// Constructs a new instance of [`EventHandler`].
|
||||||
|
pub fn new(tick_rate: u64) -> Self {
|
||||||
|
let tick_rate = Duration::from_millis(tick_rate);
|
||||||
|
let (sender, receiver) = mpsc::unbounded_channel();
|
||||||
|
let _sender = sender.clone();
|
||||||
|
let handler = tokio::spawn(async move {
|
||||||
|
let mut reader = crossterm::event::EventStream::new();
|
||||||
|
let mut tick = tokio::time::interval(tick_rate);
|
||||||
|
loop {
|
||||||
|
let tick_delay = tick.tick();
|
||||||
|
let crossterm_event = reader.next().fuse();
|
||||||
|
tokio::select! {
|
||||||
|
_ = _sender.closed() => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = tick_delay => {
|
||||||
|
_sender.send(Event::Tick).unwrap();
|
||||||
|
}
|
||||||
|
Some(Ok(evt)) = crossterm_event => {
|
||||||
|
match evt {
|
||||||
|
CrosstermEvent::Key(key) => {
|
||||||
|
if key.kind == crossterm::event::KeyEventKind::Press {
|
||||||
|
_sender.send(Event::Key(key)).unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CrosstermEvent::Mouse(mouse) => {
|
||||||
|
_sender.send(Event::Mouse(mouse)).unwrap();
|
||||||
|
},
|
||||||
|
CrosstermEvent::Resize(x, y) => {
|
||||||
|
_sender.send(Event::Resize(x, y)).unwrap();
|
||||||
|
},
|
||||||
|
CrosstermEvent::FocusLost => {
|
||||||
|
},
|
||||||
|
CrosstermEvent::FocusGained => {
|
||||||
|
},
|
||||||
|
CrosstermEvent::Paste(_) => {
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Self {
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive the next event from the handler thread.
|
||||||
|
///
|
||||||
|
/// This function will always block the current thread if
|
||||||
|
/// there is no data available and it's possible for more data to be sent.
|
||||||
|
pub async fn next(&mut self) -> AppResult<Event> {
|
||||||
|
self.receiver
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.ok_or(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
"This is an IO error",
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
30
src/handler.rs
Normal file
30
src/handler.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
use crate::app::{App, AppResult};
|
||||||
|
use crate::api::query_anime;
|
||||||
|
|
||||||
|
use crossterm::event::*;
|
||||||
|
use tui_input::backend::crossterm::EventHandler;
|
||||||
|
|
||||||
|
/// Handles the key events and updates the state of [`App`].
|
||||||
|
pub async fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
||||||
|
match key_event.code {
|
||||||
|
// Exit application on `ESC`
|
||||||
|
KeyCode::Esc => {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
// Exit application on `Ctrl-C`
|
||||||
|
KeyCode::Char('c') | KeyCode::Char('C') => {
|
||||||
|
if key_event.modifiers == KeyModifiers::CONTROL {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
query_anime().await.unwrap().currentPage;
|
||||||
|
app.input.reset();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
app.input.handle_event(&Event::Key(key_event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
17
src/lib.rs
Normal file
17
src/lib.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/// Application.
|
||||||
|
pub mod app;
|
||||||
|
|
||||||
|
/// Terminal events handler.
|
||||||
|
pub mod event;
|
||||||
|
|
||||||
|
/// Widget renderer.
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
|
/// Terminal user interface.
|
||||||
|
pub mod tui;
|
||||||
|
|
||||||
|
/// Event handler.
|
||||||
|
pub mod handler;
|
||||||
|
|
||||||
|
/// Interactions with the altoku API (consumet).
|
||||||
|
pub mod api;
|
39
src/main.rs
39
src/main.rs
|
@ -1,3 +1,38 @@
|
||||||
fn main() {
|
use std::io;
|
||||||
println!("Hello, world!");
|
|
||||||
|
use ratatui::{
|
||||||
|
prelude::*,
|
||||||
|
backend::{Backend, CrosstermBackend},
|
||||||
|
};
|
||||||
|
|
||||||
|
use altoku::app::{App, AppResult};
|
||||||
|
use altoku::tui::Tui;
|
||||||
|
use altoku::event::{Event, EventHandler};
|
||||||
|
use altoku::handler::handle_key_events;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> AppResult<()> {
|
||||||
|
let mut app = App::new();
|
||||||
|
|
||||||
|
let backend = CrosstermBackend::new(io::stderr());
|
||||||
|
let terminal = Terminal::new(backend)?;
|
||||||
|
let events = EventHandler::new(250);
|
||||||
|
let mut tui = Tui::new(terminal, events);
|
||||||
|
tui.init()?;
|
||||||
|
|
||||||
|
// Start the main loop.
|
||||||
|
while app.running {
|
||||||
|
// Render the user interface.
|
||||||
|
tui.draw(&mut app)?;
|
||||||
|
// Handle events.
|
||||||
|
match tui.events.next().await? {
|
||||||
|
Event::Tick => app.tick(),
|
||||||
|
Event::Key(key_event) => handle_key_events(key_event, &mut app).await?,
|
||||||
|
Event::Mouse(_) => {}
|
||||||
|
Event::Resize(_, _) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tui.exit()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
73
src/tui.rs
Normal file
73
src/tui.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use crate::app::{App, AppResult};
|
||||||
|
use crate::event::EventHandler;
|
||||||
|
use crate::ui;
|
||||||
|
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||||
|
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
|
use ratatui::backend::Backend;
|
||||||
|
use ratatui::Terminal;
|
||||||
|
use std::io;
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
/// Representation of a terminal user interface.
|
||||||
|
///
|
||||||
|
/// It is responsible for setting up the terminal,
|
||||||
|
/// initializing the interface and handling the draw events.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Tui<B: Backend> {
|
||||||
|
/// Interface to the Terminal.
|
||||||
|
terminal: Terminal<B>,
|
||||||
|
/// Terminal event handler.
|
||||||
|
pub events: EventHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: Backend> Tui<B> {
|
||||||
|
/// Constructs a new instance of [`Tui`].
|
||||||
|
pub fn new(terminal: Terminal<B>, events: EventHandler) -> Self {
|
||||||
|
Self { terminal, events }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes the terminal interface.
|
||||||
|
///
|
||||||
|
/// It enables the raw mode and sets terminal properties.
|
||||||
|
pub fn init(&mut self) -> AppResult<()> {
|
||||||
|
terminal::enable_raw_mode()?;
|
||||||
|
crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
let panic_hook = panic::take_hook();
|
||||||
|
panic::set_hook(Box::new(move |panic| {
|
||||||
|
Self::reset().expect("failed to reset the terminal");
|
||||||
|
panic_hook(panic);
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.terminal.hide_cursor()?;
|
||||||
|
self.terminal.clear()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Draw`] the terminal interface by [`rendering`] the widgets.
|
||||||
|
///
|
||||||
|
/// [`Draw`]: ratatui::Terminal::draw
|
||||||
|
/// [`rendering`]: crate::ui::render
|
||||||
|
pub fn draw(&mut self, app: &mut App) -> AppResult<()> {
|
||||||
|
self.terminal.draw(|frame| ui::render(app, frame))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the terminal interface.
|
||||||
|
///
|
||||||
|
/// This function is also used for the panic hook to revert
|
||||||
|
/// the terminal properties if unexpected errors occur.
|
||||||
|
fn reset() -> AppResult<()> {
|
||||||
|
terminal::disable_raw_mode()?;
|
||||||
|
crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exits the terminal interface.
|
||||||
|
///
|
||||||
|
/// It disables the raw mode and reverts back the terminal properties.
|
||||||
|
pub fn exit(&mut self) -> AppResult<()> {
|
||||||
|
Self::reset()?;
|
||||||
|
self.terminal.show_cursor()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
25
src/ui.rs
Normal file
25
src/ui.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use ratatui::{
|
||||||
|
layout::Alignment,
|
||||||
|
style::{Color, Style},
|
||||||
|
widgets::{Block, BorderType, Paragraph},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
|
/// Renders the user interface widgets.
|
||||||
|
pub fn render(app: &mut App, frame: &mut Frame) {
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(format!(
|
||||||
|
"This is just a placeholder."
|
||||||
|
))
|
||||||
|
.block(
|
||||||
|
Block::bordered()
|
||||||
|
.title("Template")
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.border_type(BorderType::Rounded),
|
||||||
|
)
|
||||||
|
.centered(),
|
||||||
|
frame.size(),
|
||||||
|
)
|
||||||
|
}
|
Reference in a new issue