adguardian/
ui.rs

1use std::{
2  io::stdout,
3  sync::Arc,
4  time::Duration,
5};
6use crossterm::{
7  event::{poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers},
8  execute,
9  terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
10};
11use tui::{
12  backend::CrosstermBackend,
13  layout::{Constraint, Direction, Layout},
14  style::Color,
15  Terminal,
16};
17
18use crate::fetch::fetch_stats::StatsResponse;
19use crate::fetch::fetch_query_log::Query;
20use crate::fetch::fetch_status::StatusResponse;
21use crate::fetch::fetch_filters::{AdGuardFilteringStatus, Filter};
22
23use crate::widgets::gauge::make_gauge;
24use crate::widgets::table::make_query_table;
25use crate::widgets::chart::{make_history_chart, prepare_chart_data};
26use crate::widgets::status::render_status_paragraph;
27use crate::widgets::filters::make_filters_list;
28use crate::widgets::list::make_list;
29
30pub async fn draw_ui(
31    mut data_rx: tokio::sync::mpsc::Receiver<Vec<Query>>,
32    mut stats_rx: tokio::sync::mpsc::Receiver<StatsResponse>,
33    mut status_rx: tokio::sync::mpsc::Receiver<StatusResponse>,
34    filters: AdGuardFilteringStatus,
35    shutdown: Arc<tokio::sync::Notify>
36) -> Result<(), anyhow::Error> {
37    enable_raw_mode()?;
38    let mut stdout = stdout();
39    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
40    let backend = CrosstermBackend::new(stdout);
41    let mut terminal = Terminal::new(backend)?;
42    terminal.clear()?;
43
44    loop {
45        // Recieve query log and stats data from the fetcher
46        let data = match data_rx.recv().await {
47            Some(data) => data,
48            None => break, // Channel has been closed, so we break the loop
49        };
50        let mut stats = match stats_rx.recv().await {
51            Some(stats) => stats,
52            None => break,
53        };
54        let status = match status_rx.recv().await {
55            Some(status) => status,
56            None => break,
57        };
58
59        // Prepare the data for the chart
60        prepare_chart_data(&mut stats);
61
62        terminal.draw(|f| {
63            let size = f.size();
64
65            // Make the charts
66            let gauge = make_gauge(&stats);
67            let table = make_query_table(&data, size.width);
68            let graph = make_history_chart(&stats);
69            let paragraph = render_status_paragraph(&status, &stats);
70            let filter_items: &[Filter] = filters
71                .filters
72                .as_deref()
73                .unwrap_or(&[]);
74            let filters_list = make_filters_list(filter_items, size.width);
75            let top_queried_domains = make_list("Top Queried Domains", &stats.top_queried_domains, Color::Green, size.width);
76            let top_blocked_domains = make_list("Top Blocked Domains", &stats.top_blocked_domains, Color::Red, size.width);
77            let top_clients = make_list("Top Clients", &stats.top_clients, Color::Cyan, size.width);
78
79            let constraints = if size.height > 42 {
80                vec![
81                    Constraint::Percentage(30),
82                    Constraint::Min(1),
83                    Constraint::Percentage(20)
84                ]
85            } else {
86                vec![
87                    Constraint::Percentage(30),
88                    Constraint::Min(1),
89                    Constraint::Percentage(0)
90                ]
91            };
92
93            let chunks = Layout::default()
94            .direction(Direction::Vertical)
95            .constraints(&*constraints)
96            .split(size);
97
98            // Split the top part (charts + gauge) into left (gauge + block) and right (line chart)
99            let top_chunks = Layout::default()
100            .direction(Direction::Horizontal)
101            .constraints(
102                [
103                    Constraint::Percentage(30), 
104                    Constraint::Percentage(70), 
105                ]
106                .as_ref(),
107            )
108            .split(chunks[0]);
109
110            // Split the left part of top (gauge + block) into top (gauge) and bottom (block)
111            let left_chunks = Layout::default()
112                .direction(Direction::Vertical)
113                .constraints(
114                    [
115                        Constraint::Min(0),
116                        Constraint::Length(3),
117                    ]
118                    .as_ref(),
119                )
120                .split(top_chunks[0]);
121
122            let bottom_chunks = Layout::default()
123                .direction(Direction::Horizontal)
124                .constraints(
125                    [
126                        Constraint::Percentage(25), 
127                        Constraint::Percentage(25), 
128                        Constraint::Percentage(25), 
129                        Constraint::Percentage(25), 
130                    ]
131                    .as_ref(),
132                )
133                .split(chunks[2]);
134
135            // Render the widgets to the UI
136            f.render_widget(paragraph, left_chunks[0]);
137            f.render_widget(gauge, left_chunks[1]);
138            f.render_widget(graph, top_chunks[1]);
139            f.render_widget(table, chunks[1]);
140            if size.height > 42 {
141                f.render_widget(filters_list, bottom_chunks[0]);
142                f.render_widget(top_queried_domains, bottom_chunks[1]);
143                f.render_widget(top_blocked_domains, bottom_chunks[2]);
144                f.render_widget(top_clients, bottom_chunks[3]);
145            }
146        })?;
147
148        // Check for user input events
149        if poll(Duration::from_millis(100))? {
150            match read()? {
151                Event::Key(KeyEvent {
152                    code: KeyCode::Char('q'),
153                    ..
154                }) => {
155                    // std::process::exit(0);
156                    shutdown.notify_waiters();
157                    break;
158                }
159                Event::Key(KeyEvent {
160                    code: KeyCode::Char('Q'),
161                    ..
162                }) => {
163                    shutdown.notify_waiters();
164                    break;
165                }
166                Event::Key(KeyEvent {
167                    code: KeyCode::Char('c'),
168                    modifiers: KeyModifiers::CONTROL,
169                }) => {
170                    shutdown.notify_waiters();
171                    break;
172                }
173                Event::Resize(_, _) => {}, // Handle resize event, loop will redraw the UI
174                _ => {}
175            }
176        }
177
178    }
179
180    terminal.show_cursor()?;
181    execute!(
182        terminal.backend_mut(),
183        LeaveAlternateScreen,
184        DisableMouseCapture
185    )?;
186    disable_raw_mode()?;
187    Ok(())
188}
189