AdGuardian Term is a single-binary Rust application built with Tokio for async I/O, ratatui for terminal rendering, and crossterm for keyboard and mouse events. The architecture cleanly separates data fetching from UI rendering via Tokio MPSC channels — the main loop fetches from the AdGuard Home API on a configurable interval and sends results down channels, while a dedicated UI task consumes those results and redraws the terminal on every tick.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/lissy93/adguardian-term/llms.txt
Use this file to discover all available pages before exploring further.
Module Structure
The project source lives entirely undersrc/ and is divided into four top-level modules:
src/main.rs — entry point and fetch loop
src/main.rs — entry point and fetch loop
Reads environment variables (
ADGUARD_IP, ADGUARD_PORT, ADGUARD_USERNAME, ADGUARD_PASSWORD, ADGUARD_PROTOCOL, ADGUARD_UPDATE_INTERVAL, ADGUARD_TIMEOUT, ADGUARD_QUERYLOG_LIMIT), constructs a reqwest::Client with a configurable per-request timeout, and wires everything together.The run() async function:- Creates three MPSC channels (
queries,stats,status) and a shutdown watch channel. - Spawns
draw_ui()as a Tokio task. - Runs the main fetch loop, calling
fetch_all()on atokio::time::interval(default 2 seconds, minimum 1 second). - Stops when the shutdown watch fires or a channel send fails (indicating the UI has exited).
fetch_all() is a helper that calls fetch_adguard_query_log, fetch_adguard_stats, and fetch_adguard_status together. If any one of the three fails, the function returns an error and the UI data is not partially updated — ensuring the dashboard never shows a mixed snapshot from different points in time.src/welcome.rs — startup sequence
src/welcome.rs — startup sequence
Runs before the main fetch loop. It:
- Prints the AdGuardian ASCII art banner.
- Checks for updates by querying
crates.io/api/v1/crates/adguardianand comparing the installed version against the latest published version usingsemver. - Parses command-line flags (
--adguard-ip,--adguard-port,--adguard-username,--adguard-password) and maps them to environment variables. - Prompts interactively for any environment variables that are still unset, masking password input on interactive terminals.
- Verifies the connection to AdGuard Home via
verify_connection(), retrying up to 3 times with a 5-second delay between attempts using thewith_retries()helper. - Checks the reported AdGuard Home version is at least
v0.107.29— exits with an error if the version is too old or unparseable.
with_retries() function is also reused in main.rs when fetching the initial filter list at startup.src/fetch/ — AdGuard Home API clients
src/fetch/ — AdGuard Home API clients
Four submodules, one per AdGuard Home API endpoint. Each module defines a typed response struct (deriving
The
serde::Deserialize with #[serde(default)] so missing fields fall back to Default rather than erroring) and an async fetch function that builds a Basic Auth header and sends a reqwest GET request.| Module | Endpoint | Response type |
|---|---|---|
fetch_stats.rs | GET /control/stats | StatsResponse |
fetch_query_log.rs | GET /control/querylog?limit=N | QueryResponse containing Vec<Query> |
fetch_status.rs | GET /control/status | StatusResponse |
fetch_filters.rs | GET /control/filtering/status | AdGuardFilteringStatus |
DomainData struct (a name/count pair used by the stats top-lists) and the deserialize_domains helper that converts AdGuard’s Vec<HashMap<String, i32>> format into Vec<DomainData> are both defined in fetch_stats.rs alongside StatsResponse.src/ui.rs — terminal rendering
src/ui.rs — terminal rendering
Exports
draw_ui(), an async function that owns the terminal lifecycle. It uses TerminalGuard — a struct that enables raw mode and switches to the alternate screen buffer on construction, and restores them via Drop even if the function returns early due to an error.On each iteration of the render loop, draw_ui():- Waits for data from all three MPSC channels (
queries_rx,stats_rx,status_rx). - Calls
prepare_chart_data()to compute interpolated time-series data for the history chart. - Calls
terminal.draw()to compose the full layout and render every widget in a single frame.
crossterm::event::EventStream. Quit keys (q, Q, or Ctrl+C) trigger the shutdown watch channel, which causes both the render loop and the main fetch loop in main.rs to exit cleanly.The layout is built from nested ratatui Layout splits:- Top row (30% height): left column (status paragraph + block-rate gauge) and right column (history chart, 70% width).
- Middle row (remaining space): scrollable live query log table.
- Bottom row (20% height, only shown if terminal height > 42 lines): four equal columns for filter list, top queried domains, top blocked domains, and top clients.
src/widgets/ — individual UI panels
src/widgets/ — individual UI panels
One file per widget type. Each exports a function that takes references to the latest data and returns a ratatui widget ready to be rendered.
| File | Widget | Description |
|---|---|---|
chart.rs | Chart | 30-day time-series history of total queries and blocked queries, with 3-point interpolation to smooth the Braille-marker line |
status.rs | Paragraph | Coloured status panel: running state, protection enabled, DHCP availability, average processing time, version, DNS/HTTP ports, and query counts by category |
gauge.rs | Gauge | Horizontal bar showing the block rate as a percentage of total queries |
table.rs | Table | Scrollable live query log showing client, domain, type, reason, upstream, and elapsed time for each recent DNS query |
list.rs | List | Ranked list widget reused for top queried domains, top blocked domains, and top clients |
filters.rs | List | Active AdGuard filter subscriptions with their names and rule counts |
Data Flow
The following sequence describes how data moves from AdGuard Home to the terminal on every update tick:Startup checks
welcome() runs synchronously (within the Tokio runtime) before the main application starts. It verifies credentials, confirms the AdGuard Home version, and fetches the filter list once (with retries). The filter list is passed directly to draw_ui() since it does not change during a session.Channel creation
Three bounded MPSC channels (capacity 1) are created for
queries, stats, and status. A tokio::sync::watch channel is created for the shutdown signal.UI task spawned
draw_ui() is spawned as a Tokio task. It blocks waiting for data on the three receive ends of the MPSC channels.Fetch loop starts
The main task enters a
tokio::select! loop ticking at the configured interval (default 2 seconds, minimum 1 second). On each tick it calls fetch_all().Atomic fetch and send
fetch_all() awaits all three API calls. If all succeed, the results are sent down the three channels. If any call fails, the tick is skipped entirely — the UI retains the last good data rather than showing a partial update. A send error (channel closed) means the UI has exited, so the loop breaks.UI renders
On receiving a full set of data,
draw_ui() calls prepare_chart_data() to compute the interpolated chart datasets, then calls terminal.draw() to paint all widgets to the screen in a single atomic frame.AdGuard Home API Endpoints
All requests use HTTP Basic Authentication. The credentials are base64-encoded in anAuthorization header on every request.
| Endpoint | Method | Returns |
|---|---|---|
/control/stats | GET | Aggregated query counts, average processing time, 30-day per-day history arrays, and top queried domains, top blocked domains, and top clients |
/control/querylog?limit=N | GET | The most recent N DNS query log entries, each with client IP, domain, query type, reason, upstream resolver, elapsed time, and timestamp |
/control/status | GET | Current running state, AdGuard Home version string, DNS port, HTTP port, protection-enabled flag, and DHCP-available flag |
/control/filtering/status | GET | List of active filter subscriptions, each with a name, enabled state, and total rule count |
AdGuardian Term requires AdGuard Home v0.107.29 or later. The version is checked at startup via the
/control/status endpoint. Older versions will cause the app to exit with an error message.Key Dependencies
The full dependency list is inCargo.toml. The most significant crates are:
| Crate | Version | Role |
|---|---|---|
ratatui (aliased as tui) | 0.20.1 | Terminal UI widgets, layout engine, and drawing backend |
crossterm | 0.22.0 | Raw mode, alternate screen buffer, keyboard and mouse event stream |
tokio | 1 (full) | Async runtime, MPSC channels, watch channels, and interval timers |
reqwest | 0.13 | Async HTTP client using rustls (no native TLS dependency) |
serde / serde_json | 1.0 | Derive macros and JSON deserialization of API responses |
chrono | 0.4 | Date and time parsing for query log entry timestamps |
semver | 1.0 | Semantic version parsing for the startup version compatibility check |
anyhow | 1.0 | Ergonomic error propagation throughout the codebase |
base64 | 0.22 | Encoding credentials into the HTTP Basic Auth header value |
colored | 3 | ANSI colour output in the startup and welcome sequence |
futures | 0.3 | StreamExt extension trait used to poll the crossterm event stream |