add simple ui implementation for docker

This commit is contained in:
monosans
2025-04-11 12:44:25 +03:00
parent 5222cdabbc
commit 8bd302b7b7
13 changed files with 358 additions and 95 deletions
+4 -31
View File
@@ -1,7 +1,6 @@
use std::collections::HashMap;
use crate::proxy::ProxyType;
#[allow(dead_code)]
pub(crate) enum AppEvent {
GeoDbTotal(Option<u64>),
GeoDbDownloaded(usize),
@@ -17,36 +16,10 @@ pub(crate) enum AppEvent {
}
pub(crate) enum Event {
#[cfg(feature = "tui")]
Tick,
#[cfg(feature = "tui")]
Crossterm(crossterm::event::Event),
#[allow(dead_code)]
App(AppEvent),
}
#[derive(Default)]
pub(crate) enum AppMode {
#[default]
Running,
Done,
Quit,
}
#[derive(Default)]
pub(crate) struct AppState {
pub(crate) mode: AppMode,
pub(crate) geodb_total: u64,
pub(crate) geodb_downloaded: usize,
pub(crate) sources_total: HashMap<ProxyType, usize>,
pub(crate) sources_scraped: HashMap<ProxyType, usize>,
pub(crate) proxies_total: HashMap<ProxyType, usize>,
pub(crate) proxies_checked: HashMap<ProxyType, usize>,
pub(crate) proxies_working: HashMap<ProxyType, usize>,
}
impl AppState {
pub(crate) fn new() -> Self {
AppState::default()
}
}
+12 -21
View File
@@ -9,8 +9,9 @@ pub(crate) mod proxy;
pub(crate) mod raw_config;
pub(crate) mod scraper;
pub(crate) mod storage;
pub(crate) mod tui;
pub(crate) mod ui;
pub(crate) mod utils;
use ui::UI;
pub(crate) const APP_DIRECTORY_NAME: &str = "proxy_scraper_checker";
pub(crate) const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; \
@@ -31,9 +32,16 @@ fn create_reqwest_client() -> reqwest::Result<reqwest::Client> {
.build()
}
async fn run(terminal: ratatui::DefaultTerminal) -> color_eyre::Result<()> {
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
color_eyre::eyre::Context::wrap_err(
color_eyre::install(),
"failed to install color_eyre hooks",
)?;
let ui_impl = ui::UIImpl::new()?;
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let ui_task = tokio::task::spawn(tui::run(terminal, tx.clone(), rx));
let ui_task = tokio::task::spawn(ui_impl.run(tx.clone(), rx));
let http_client = color_eyre::eyre::Context::wrap_err(
create_reqwest_client(),
@@ -52,7 +60,7 @@ async fn run(terminal: ratatui::DefaultTerminal) -> color_eyre::Result<()> {
)?);
if !config.debug {
tui_logger::set_default_level(log::LevelFilter::Info);
ui::UIImpl::set_log_level(log::LevelFilter::Info);
}
let maybe_geodb_task = if config.enable_geolocation {
@@ -96,20 +104,3 @@ async fn run(terminal: ratatui::DefaultTerminal) -> color_eyre::Result<()> {
ui_task.await??;
Ok(())
}
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
color_eyre::eyre::Context::wrap_err(
color_eyre::install(),
"failed to install color_eyre hooks",
)?;
color_eyre::eyre::Context::wrap_err(
tui_logger::init_logger(log::LevelFilter::Debug),
"failed to initialize logger",
)?;
tui_logger::set_default_level(log::LevelFilter::Debug);
let terminal = ratatui::init();
let result = run(terminal).await;
ratatui::restore();
result
}
+20
View File
@@ -0,0 +1,20 @@
use crate::event::Event;
pub(crate) struct DummyUI {}
impl super::UI for DummyUI {
fn new() -> color_eyre::Result<Self> {
Ok(Self {})
}
fn set_log_level(_log_level: log::LevelFilter) {}
async fn run(
self,
_tx: tokio::sync::mpsc::UnboundedSender<Event>,
mut rx: tokio::sync::mpsc::UnboundedReceiver<Event>,
) -> color_eyre::Result<()> {
while rx.recv().await.is_some() {}
Ok(())
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::event::Event;
pub(crate) struct LoggerUI {}
impl super::UI for LoggerUI {
fn new() -> color_eyre::Result<Self> {
env_logger::Builder::new().filter_level(log::LevelFilter::Debug).init();
Ok(Self {})
}
fn set_log_level(log_level: log::LevelFilter) {
log::set_max_level(log_level);
}
async fn run(
self,
_tx: tokio::sync::mpsc::UnboundedSender<Event>,
mut rx: tokio::sync::mpsc::UnboundedReceiver<Event>,
) -> color_eyre::Result<()> {
while rx.recv().await.is_some() {}
Ok(())
}
}
+30
View File
@@ -0,0 +1,30 @@
use crate::event::Event;
pub(crate) trait UI {
fn new() -> color_eyre::Result<Self>
where
Self: std::marker::Sized;
fn set_log_level(log_level: log::LevelFilter);
async fn run(
self,
tx: tokio::sync::mpsc::UnboundedSender<Event>,
rx: tokio::sync::mpsc::UnboundedReceiver<Event>,
) -> color_eyre::Result<()>;
}
cfg_if::cfg_if! {
if #[cfg(all(feature="tui", feature="logger"))] {
compile_error!("feature \"logger\" and feature \"tui\" cannot be enaled at the same time");
} else if #[cfg(feature="tui" )] {
mod tui;
pub(crate) use self::tui::Tui as UIImpl;
} else if #[cfg(feature = "logger")] {
mod logger;
pub(crate) use self::logger::LoggerUI as UIImpl;
} else {
mod dummy;
pub(crate) use self::dummy::DummyUI as UIImpl;
}
}
+97 -31
View File
@@ -1,10 +1,12 @@
use std::collections::HashMap;
use color_eyre::eyre::WrapErr;
use crossterm::event::{
Event as CrosstermEvent, KeyCode, KeyModifiers, MouseEventKind,
};
use futures::{FutureExt, StreamExt};
use ratatui::{
Frame,
DefaultTerminal, Frame,
layout::{Alignment, Constraint, Direction, Flex, Layout},
style::{Color, Style},
text::{Line, Text},
@@ -13,13 +15,96 @@ use ratatui::{
use tui_logger::{TuiLoggerWidget, TuiWidgetEvent, TuiWidgetState};
use crate::{
event::{AppEvent, AppMode, AppState, Event},
event::{AppEvent, Event},
proxy::ProxyType,
utils::is_docker,
};
const FPS: f64 = 30.0;
pub(crate) struct Tui {
terminal: DefaultTerminal,
}
impl super::UI for Tui {
fn new() -> color_eyre::Result<Self> {
tui_logger::init_logger(log::LevelFilter::Debug)
.wrap_err("failed to initialize logger")?;
tui_logger::set_default_level(log::LevelFilter::Debug);
Ok(Self { terminal: ratatui::init() })
}
fn set_log_level(log_level: log::LevelFilter) {
log::set_max_level(log_level);
tui_logger::set_default_level(log_level);
}
async fn run(
mut self,
tx: tokio::sync::mpsc::UnboundedSender<Event>,
mut rx: tokio::sync::mpsc::UnboundedReceiver<Event>,
) -> color_eyre::Result<()> {
let tick_task = tokio::spawn(tick_event_listener(tx.clone()));
let crossterm_task = tokio::spawn(crossterm_event_listener(tx));
let mut app_state = AppState::new();
let logger_state = TuiWidgetState::default();
while !matches!(app_state.mode, AppMode::Quit) {
if let Some(event) = rx.recv().await {
if handle_event(event, &mut app_state, &logger_state).await {
self.terminal
.draw(|frame| draw(frame, &app_state, &logger_state))
.wrap_err("failed to draw tui")?;
}
} else {
break;
}
}
drop(rx);
tick_task.await.wrap_err("failed to spawn tui tick task")??;
crossterm_task
.await
.wrap_err("failed to spawn tui crossterm task")??;
Ok(())
}
}
impl Drop for Tui {
fn drop(&mut self) {
ratatui::restore();
}
}
#[derive(Default)]
pub(crate) enum AppMode {
#[default]
Running,
/// Wait for the user confirmation to close the UI
Done,
/// Close the UI
Quit,
}
#[derive(Default)]
pub(crate) struct AppState {
pub(crate) mode: AppMode,
pub(crate) geodb_total: u64,
pub(crate) geodb_downloaded: usize,
pub(crate) sources_total: HashMap<ProxyType, usize>,
pub(crate) sources_scraped: HashMap<ProxyType, usize>,
pub(crate) proxies_total: HashMap<ProxyType, usize>,
pub(crate) proxies_checked: HashMap<ProxyType, usize>,
pub(crate) proxies_working: HashMap<ProxyType, usize>,
}
impl AppState {
pub(crate) fn new() -> Self {
AppState::default()
}
}
async fn tick_event_listener(
tx: tokio::sync::mpsc::UnboundedSender<Event>,
) -> color_eyre::Result<()> {
@@ -55,33 +140,6 @@ async fn crossterm_event_listener(
}
}
pub(crate) async fn run(
mut terminal: ratatui::DefaultTerminal,
tx: tokio::sync::mpsc::UnboundedSender<Event>,
mut rx: tokio::sync::mpsc::UnboundedReceiver<Event>,
) -> color_eyre::Result<()> {
let tick_task = tokio::spawn(tick_event_listener(tx.clone()));
let crossterm_task = tokio::spawn(crossterm_event_listener(tx));
let mut app_state = AppState::new();
let logger_state = TuiWidgetState::default();
let is_docker = is_docker().await;
while (is_docker && matches!(app_state.mode, AppMode::Running))
|| (!is_docker && !matches!(app_state.mode, AppMode::Quit))
{
if let Some(event) = rx.recv().await {
if handle_event(event, &mut app_state, &logger_state) {
terminal
.draw(|frame| draw(frame, &app_state, &logger_state))
.wrap_err("failed to draw tui")?;
}
}
}
drop(rx);
tick_task.await.wrap_err("failed to spawn tui tick task")??;
crossterm_task.await.wrap_err("failed to spawn tui crossterm task")??;
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::too_many_lines)]
@@ -250,7 +308,11 @@ fn draw(f: &mut Frame, state: &AppState, logger_state: &TuiWidgetState) {
f.render_widget(table, result_layout[1]);
}
fn handle_event(
async fn is_interactive() -> bool {
is_docker().await
}
async fn handle_event(
event: Event,
state: &mut AppState,
logger_state: &TuiWidgetState,
@@ -318,7 +380,11 @@ fn handle_event(
*state.proxies_working.entry(proxy_type).or_default() += 1;
}
AppEvent::Done => {
state.mode = AppMode::Done;
state.mode = if is_interactive().await {
AppMode::Quit
} else {
AppMode::Done
};
}
}
false