chore: initialise rust project and TUI

This commit is contained in:
Muhammad Nauman Raza 2024-05-28 15:11:07 +01:00
parent bbcb18bde4
commit f2a8cf9b06
11 changed files with 1962 additions and 2 deletions

3
.cargo/config.toml Normal file
View 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

File diff suppressed because it is too large Load diff

View file

@ -4,3 +4,10 @@ version = "0.1.0"
edition = "2021"
[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
View 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
View 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
View 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
View 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
View 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;

View file

@ -1,3 +1,38 @@
fn main() {
println!("Hello, world!");
use std::io;
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
View 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
View 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(),
)
}