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 let data = match data_rx.recv().await {
47 Some(data) => data,
48 None => break, };
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_chart_data(&mut stats);
61
62 terminal.draw(|f| {
63 let size = f.size();
64
65 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 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 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 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 if poll(Duration::from_millis(100))? {
150 match read()? {
151 Event::Key(KeyEvent {
152 code: KeyCode::Char('q'),
153 ..
154 }) => {
155 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(_, _) => {}, _ => {}
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