Base working commit with a UI thread (Tokio), Network thread (Tokio), and an input events thread (std).

This commit is contained in:
2023-08-08 10:50:03 -06:00
parent f436a66069
commit 0d4e283c21
15 changed files with 532 additions and 1 deletions
+25
View File
@@ -0,0 +1,25 @@
use crate::event::Key;
macro_rules! generate_keybindings {
($($field:ident),+) => {
pub struct KeyBindings {
$(pub $field: KeyBinding),+
}
};
}
generate_keybindings! {
quit
}
pub struct KeyBinding {
key: Key,
desc: &'static str
}
pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
quit: KeyBinding {
key: Key::Char('q'),
desc: "Quit",
}
};
+99
View File
@@ -0,0 +1,99 @@
use log::error;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::Sender;
use crate::app::radarr::RadarrData;
use super::network::RadarrEvent;
pub mod radarr;
pub(crate) mod key_binding;
#[derive(Debug)]
pub struct App {
network_tx: Option<Sender<RadarrEvent>>,
pub client: Client,
pub title: &'static str,
pub tick_until_poll: u64,
pub tick_count: u64,
pub config: AppConfig,
pub data: Data,
}
impl App {
pub fn new(
network_tx: Sender<RadarrEvent>,
tick_until_poll: u64,
config: AppConfig
) -> Self {
App {
network_tx: Some(network_tx),
tick_until_poll,
config,
..App::default()
}
}
pub async fn dispatch(&mut self, action: RadarrEvent) {
if let Some(network_tx) = &self.network_tx {
if let Err(e) = network_tx.send(action).await {
error!("Failed to send event. {:?}", e);
}
}
}
pub fn reset(&mut self) {
self.tick_count = 0;
// self.data = Data::default();
}
pub async fn on_tick(&mut self) {
if self.tick_count % self.tick_until_poll == 0 {
self.dispatch(RadarrEvent::GetOverview).await;
}
self.tick_count += 1;
}
}
impl Default for App {
fn default() -> Self {
App {
network_tx: None,
client: Client::new(),
title: "DevTools",
tick_until_poll: 0,
tick_count: 0,
config: AppConfig::default(),
data: Data::default()
}
}
}
#[derive(Default, Debug)]
pub struct Data {
pub radarr_data: RadarrData,
}
#[derive(Debug, Deserialize, Serialize, Default)]
pub struct AppConfig {
pub radarr: RadarrConfig,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct RadarrConfig {
pub host: String,
pub port: Option<u16>,
pub api_token: String
}
impl Default for RadarrConfig {
fn default() -> Self {
RadarrConfig {
host: "localhost".to_string(),
port: Some(7878),
api_token: "".to_string()
}
}
}
+16
View File
@@ -0,0 +1,16 @@
use crate::network::radarr::DiskSpace;
#[derive(Default, Debug)]
pub struct RadarrData {
pub free_space: u64,
pub total_space: u64,
}
impl From<&DiskSpace> for RadarrData {
fn from(disk_space: &DiskSpace) -> Self {
RadarrData {
free_space: disk_space.free_space.as_u64().unwrap(),
total_space: disk_space.total_space.as_u64().unwrap()
}
}
}
+52
View File
@@ -0,0 +1,52 @@
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::thread;
use std::time::{Duration, Instant};
use crossterm::event;
use crossterm::event::Event as CrosstermEvent;
use crate::event::Key;
pub enum InputEvent<T> {
KeyEvent(T),
Tick
}
pub struct Events {
_tx: Sender<InputEvent<Key>>,
rx: Receiver<InputEvent<Key>>
}
impl Events {
pub fn new() -> Self {
let (tx, rx) = mpsc::channel();
let tick_rate: Duration = Duration::from_millis(250);
let event_tx = tx.clone();
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
let timeout = tick_rate.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout).unwrap() {
if let CrosstermEvent::Key(key) = event::read().unwrap() {
let key = Key::from(key);
event_tx.send(InputEvent::KeyEvent(key)).unwrap();
}
}
if last_tick.elapsed() >= tick_rate {
event_tx.send(InputEvent::Tick).unwrap();
last_tick = Instant::now();
}
}
});
Events { _tx: tx, rx }
}
pub fn next(&self) -> Result<InputEvent<Key>, mpsc::RecvError> {
self.rx.recv()
}
}
+48
View File
@@ -0,0 +1,48 @@
use std::fmt;
use std::fmt::{Display, Formatter};
use crossterm::event::{KeyCode, KeyEvent};
#[derive(Debug, PartialEq, Eq)]
pub enum Key {
Char(char),
Unknown
}
impl Display for Key {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Key::Char(c) => write!(f, "<{}>", c),
_ => write!(f, "<{:?}>", self)
}
}
}
impl From<KeyEvent> for Key {
fn from(key_event: KeyEvent) -> Self {
match key_event {
KeyEvent {
code: KeyCode::Char(c),
..
} => Key::Char(c),
_ => Key::Unknown
}
}
}
#[cfg(test)]
mod tests {
use crossterm::event::{KeyCode, KeyEvent};
use crate::event::key::Key;
#[test]
fn test_key_formatter() {
assert_eq!(format!("{}", Key::Char('q')), "<q>");
}
#[test]
fn test_key_from() {
assert_eq!(Key::from(KeyEvent::from(KeyCode::Char('q'))), Key::Char('q'))
}
}
+8
View File
@@ -0,0 +1,8 @@
pub use self::{
input_event::{Events, InputEvent},
key::Key
};
mod key;
pub mod input_event;
View File
+106
View File
@@ -0,0 +1,106 @@
use std::io;
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::Result;
use clap::Parser;
use crossterm::execute;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use log::{debug, info};
use tokio::sync::{mpsc, Mutex};
use tokio::sync::mpsc::Receiver;
use tui::Terminal;
use tui::backend::CrosstermBackend;
use utils::init_logging_config;
use crate::app::App;
use crate::event::input_event::{Events, InputEvent};
use crate::event::Key;
use crate::network::{Network, RadarrEvent};
use crate::ui::ui;
mod app;
mod event;
mod handlers;
mod network;
mod ui;
mod utils;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
}
#[tokio::main]
async fn main() -> Result<()> {
log4rs::init_config(init_logging_config())?;
Cli::parse();
let config = confy::load("managarr", "config")?;
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
let app = Arc::new(Mutex::new(App::new(
sync_network_tx,
5000 / 250,
config
)));
let app_nw = Arc::clone(&app);
std::thread::spawn(move || start_networking(sync_network_rx, &app_nw));
info!("Checking if Radarr server is up and running...");
app.lock().await.dispatch(RadarrEvent::HealthCheck).await;
simple_ui(&app).await?;
Ok(())
}
#[tokio::main]
async fn start_networking(mut network_rx: Receiver<RadarrEvent>, app: &Arc<Mutex<App>>) {
let network = Network::new(reqwest::Client::new(), app);
while let Some(network_event) = network_rx.recv().await {
debug!("Received network event: {:?}", network_event);
network.handle_radarr_event(network_event).await;
}
}
async fn simple_ui(app: &Arc<Mutex<App>>) -> Result<()> {
let mut stdout = io::stdout();
enable_raw_mode()?;
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
terminal.hide_cursor()?;
let input_events = Events::new();
loop {
let mut app = app.lock().await;
terminal.draw(|f| ui(f, &app))?;
match input_events.next()? {
InputEvent::KeyEvent(key) => {
if key == Key::Char('q') {
break;
}
}
InputEvent::Tick => {
app.on_tick().await
}
}
}
terminal.show_cursor()?;
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
+26
View File
@@ -0,0 +1,26 @@
use std::sync::Arc;
use reqwest::Client;
use tokio::sync::Mutex;
use crate::app::App;
pub(crate) mod radarr;
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum RadarrEvent {
HealthCheck,
GetOverview
}
pub struct Network<'a> {
pub client: Client,
pub app: &'a Arc<Mutex<App>>
}
impl<'a> Network<'a> {
pub fn new(client: Client, app: &'a Arc<Mutex<App>>) -> Self {
Network { client, app }
}
}
+79
View File
@@ -0,0 +1,79 @@
use anyhow::Result;
use log::{debug, error};
use reqwest::RequestBuilder;
use serde::{Deserialize, Serialize};
use serde_json::Number;
use crate::app::radarr::RadarrData;
use crate::app::RadarrConfig;
use crate::network::{Network, RadarrEvent};
impl RadarrEvent {
const fn resource(self) -> &'static str {
match self {
RadarrEvent::HealthCheck => "/health",
RadarrEvent::GetOverview => "/diskspace"
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DiskSpace {
pub path: String,
pub label: String,
pub free_space: Number,
pub total_space: Number
}
impl<'a> Network<'a> {
pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) {
match radarr_event {
RadarrEvent::HealthCheck => {
self.healthcheck(RadarrEvent::HealthCheck.resource()).await;
}
RadarrEvent::GetOverview => {
match self.diskspace(RadarrEvent::GetOverview.resource()).await {
Ok(disk_space_vec) => {
let mut app = self.app.lock().await;
app.data.radarr_data = RadarrData::from(&disk_space_vec[0]);
}
Err(e) => {
error!("Failed to fetch disk space. {:?}", e);
}
}
}
}
let mut app = self.app.lock().await;
app.reset();
}
async fn healthcheck(&self, resource: &str) {
if let Err(e) = self.call_radarr_api(resource).await.send().await {
error!("Healthcheck failed. {:?}", e)
}
}
async fn diskspace(&self, resource: &str) -> Result<Vec<DiskSpace>> {
debug!("Handling diskspace event: {:?}", resource);
Ok(
self.call_radarr_api(resource)
.await
.send()
.await?
.json::<Vec<DiskSpace>>()
.await?
)
}
async fn call_radarr_api(&self, resource: &str) -> RequestBuilder {
debug!("Creating RequestBuilder for resource: {:?}", resource);
let app = self.app.lock().await;
let RadarrConfig { host, port, api_token } = &app.config.radarr;
app.client.get(format!("http://{}:{}/api/v3{}", host, port.unwrap_or(7878), resource))
.header("X-Api-Key", api_token)
}
}
+31
View File
@@ -0,0 +1,31 @@
use tui::backend::Backend;
use tui::Frame;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Gauge};
use crate::app::App;
use crate::app::radarr::RadarrData;
pub fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let RadarrData { free_space, total_space } = app.data.radarr_data;
let ratio = if total_space == 0 {
0f64
} else {
1f64 - (free_space as f64 / total_space as f64)
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
let gauge = Gauge::default()
.block(Block::default()
.title("Free Space")
.borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Cyan))
.ratio(ratio);
f.render_widget(gauge, chunks[0]);
}
+19
View File
@@ -0,0 +1,19 @@
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
use log::LevelFilter;
pub fn init_logging_config() -> log4rs::Config {
let file_path = "/tmp/devtools.log";
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{l} - {m}\n")))
.build(file_path)
.unwrap();
log4rs::Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.build(Root::builder()
.appender("logfile")
.build(LevelFilter::Debug))
.unwrap()
}