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
+10 -3
View File
@@ -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:
+1
View File
@@ -0,0 +1 @@
{ "rust-analyzer.cargo.features": ["tui"] }
Generated
+146 -2
View File
@@ -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"
+12 -3
View File
@@ -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",
+1 -1
View File
@@ -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 .
+2 -2
View File
@@ -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).
-1
View File
@@ -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
+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