From 8bd302b7b74caae2db3bb40e2c1292e7c142af03 Mon Sep 17 00:00:00 2001 From: monosans Date: Fri, 11 Apr 2025 12:44:25 +0300 Subject: [PATCH] add simple ui implementation for docker --- .github/workflows/ci.yml | 13 +++- .vscode/settings.json | 1 + Cargo.lock | 148 ++++++++++++++++++++++++++++++++++++++- Cargo.toml | 15 +++- Dockerfile | 2 +- README.md | 4 +- compose.yaml | 1 - src/event.rs | 35 ++------- src/main.rs | 33 ++++----- src/ui/dummy.rs | 20 ++++++ src/ui/logger.rs | 23 ++++++ src/ui/mod.rs | 30 ++++++++ src/{ => ui}/tui.rs | 128 +++++++++++++++++++++++++-------- 13 files changed, 358 insertions(+), 95 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/ui/dummy.rs create mode 100644 src/ui/logger.rs create mode 100644 src/ui/mod.rs rename src/{ => ui}/tui.rs (79%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 139e2f0..9bf4d1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,9 +147,9 @@ jobs: - if: ${{ matrix.cross }} run: cargo +beta install cross --git https://github.com/cross-rs/cross - if: ${{ matrix.cross }} - run: cross +beta build --release --target '${{ matrix.target }}' --locked + run: cross +beta build --features tui --release --target '${{ matrix.target }}' --locked - if: ${{ !matrix.cross }} - run: cargo +beta build --release --target '${{ matrix.target }}' --locked + run: cargo +beta build --features tui --release --target '${{ matrix.target }}' --locked - run: mkdir dist - run: echo -n '${{ github.sha }}' > dist/commit-sha.txt shell: bash @@ -178,6 +178,13 @@ jobs: jobs: ${{ toJSON(needs) }} clippy: runs-on: ubuntu-24.04 + strategy: + matrix: + features: + - "--features tui" + - "--features logger" + - "" + fail-fast: false env: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" @@ -187,7 +194,7 @@ jobs: persist-credentials: false - run: rustup toolchain install beta --profile minimal --component clippy --allow-downgrade - uses: mozilla-actions/sccache-action@v0.0.9 - - run: cargo +beta clippy --all-targets --all-features -- -Dwarnings -Wclippy::pedantic + - run: cargo +beta clippy --all-targets ${{ matrix.features }} -- -Dwarnings -Wclippy::pedantic pre-commit: runs-on: ubuntu-24.04 steps: diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6c3c64c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{ "rust-analyzer.cargo.features": ["tui"] } diff --git a/Cargo.lock b/Cargo.lock index b4ca9da..64e325e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "async-compression" version = "0.4.22" @@ -203,9 +253,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.18" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "shlex", ] @@ -261,6 +311,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "compact_str" version = "0.8.1" @@ -437,6 +493,29 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1054,6 +1133,12 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.13.0" @@ -1069,6 +1154,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1291,6 +1400,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1313,10 +1437,12 @@ dependencies = [ name = "proxy-scraper-checker" version = "0.1.0" dependencies = [ + "cfg-if", "color-eyre", "crossterm", "derivative", "dirs", + "env_logger", "fancy-regex", "futures", "log", @@ -1502,6 +1628,18 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.9" @@ -2258,6 +2396,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 1855008..5e40a7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,23 +6,32 @@ license = "MIT" publish = false [dependencies] +cfg-if = "=1.0.0" color-eyre = "=0.6.3" -crossterm = { version = "=0.28.1", features = ["event-stream"] } +crossterm = { version = "=0.28.1", features = [ + "event-stream", +], optional = true } derivative = "=2.2.0" dirs = "=6.0.0" +env_logger = { version = "=0.11.8", optional = true } fancy-regex = "=0.14.0" futures = "=0.3.31" log = "=0.4.27" maxminddb = "=0.26.0" -ratatui = "=0.29.0" +ratatui = { version = "=0.29.0", optional = true } rlimit = "=0.10.2" serde = "=1.0.219" serde_json = "=1.0.140" tokio = { version = "=1.44.2", features = ["full"] } toml = "=0.8.20" -tui-logger = "=0.17.0" +tui-logger = { version = "=0.17.0", optional = true } url = "=2.5.4" +[features] +default = [] +logger = ["dep:env_logger"] +tui = ["dep:crossterm", "dep:ratatui", "dep:tui-logger"] + [target.'cfg(not(target_os = "android"))'.dependencies] reqwest = { version = "=0.12.15", default-features = false, features = [ "brotli", diff --git a/Dockerfile b/Dockerfile index 3be3c8f..4fe4f59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN --mount=source=src,target=src \ --mount=source=Cargo.lock,target=Cargo.lock \ --mount=type=cache,target=/app/target \ --mount=type=cache,target=/usr/local/cargo/registry \ - cargo build --release --locked \ + cargo build --features logger --release --locked \ && cp target/release/proxy-scraper-checker . diff --git a/README.md b/README.md index af74666..99db888 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ You can get proxies obtained using this project in [monosans/proxy-list](https:/ ### Docker -> [!WARNING] -> User interface does not work with Docker. +> [!NOTE] +> Only a simple user interface in the form of logs is implemented for Docker. 1. [Install `Docker Compose`](https://docs.docker.com/compose/install/). 1. Download [the archive with the program](https://github.com/monosans/proxy-scraper-checker/archive/refs/heads/main.zip). diff --git a/compose.yaml b/compose.yaml index 0010fbf..e21e30f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -8,7 +8,6 @@ services: init: true logging: driver: local - tty: true volumes: - proxy_scraper_checker_cache:/home/app/.cache/proxy_scraper_checker - ./out:/home/app/.local/share/proxy_scraper_checker diff --git a/src/event.rs b/src/event.rs index 1f46d28..c314da8 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,7 +1,6 @@ -use std::collections::HashMap; - use crate::proxy::ProxyType; +#[allow(dead_code)] pub(crate) enum AppEvent { GeoDbTotal(Option), 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, - pub(crate) sources_scraped: HashMap, - - pub(crate) proxies_total: HashMap, - pub(crate) proxies_checked: HashMap, - pub(crate) proxies_working: HashMap, -} - -impl AppState { - pub(crate) fn new() -> Self { - AppState::default() - } -} diff --git a/src/main.rs b/src/main.rs index 6c68818..1e0d185 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { .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 -} diff --git a/src/ui/dummy.rs b/src/ui/dummy.rs new file mode 100644 index 0000000..05a02a7 --- /dev/null +++ b/src/ui/dummy.rs @@ -0,0 +1,20 @@ +use crate::event::Event; + +pub(crate) struct DummyUI {} + +impl super::UI for DummyUI { + fn new() -> color_eyre::Result { + Ok(Self {}) + } + + fn set_log_level(_log_level: log::LevelFilter) {} + + async fn run( + self, + _tx: tokio::sync::mpsc::UnboundedSender, + mut rx: tokio::sync::mpsc::UnboundedReceiver, + ) -> color_eyre::Result<()> { + while rx.recv().await.is_some() {} + Ok(()) + } +} diff --git a/src/ui/logger.rs b/src/ui/logger.rs new file mode 100644 index 0000000..460f44f --- /dev/null +++ b/src/ui/logger.rs @@ -0,0 +1,23 @@ +use crate::event::Event; + +pub(crate) struct LoggerUI {} + +impl super::UI for LoggerUI { + fn new() -> color_eyre::Result { + 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, + mut rx: tokio::sync::mpsc::UnboundedReceiver, + ) -> color_eyre::Result<()> { + while rx.recv().await.is_some() {} + Ok(()) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..528152b --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,30 @@ +use crate::event::Event; + +pub(crate) trait UI { + fn new() -> color_eyre::Result + where + Self: std::marker::Sized; + + fn set_log_level(log_level: log::LevelFilter); + + async fn run( + self, + tx: tokio::sync::mpsc::UnboundedSender, + rx: tokio::sync::mpsc::UnboundedReceiver, + ) -> 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; + } +} diff --git a/src/tui.rs b/src/ui/tui.rs similarity index 79% rename from src/tui.rs rename to src/ui/tui.rs index 00d85ca..e499dfe 100644 --- a/src/tui.rs +++ b/src/ui/tui.rs @@ -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 { + 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, + mut rx: tokio::sync::mpsc::UnboundedReceiver, + ) -> 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, + pub(crate) sources_scraped: HashMap, + + pub(crate) proxies_total: HashMap, + pub(crate) proxies_checked: HashMap, + pub(crate) proxies_working: HashMap, +} + +impl AppState { + pub(crate) fn new() -> Self { + AppState::default() + } +} + async fn tick_event_listener( tx: tokio::sync::mpsc::UnboundedSender, ) -> 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, - mut rx: tokio::sync::mpsc::UnboundedReceiver, -) -> 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