1
1
use std:: {
2
+ cmp,
2
3
sync:: { atomic:: AtomicBool , Arc , Mutex } ,
3
4
thread,
4
5
time:: Duration ,
@@ -8,11 +9,12 @@ use crossterm::event::{Event, KeyCode, KeyEvent};
8
9
use tui_input:: { backend:: crossterm:: EventHandler , Input } ;
9
10
10
11
use ratatui:: {
11
- layout:: { Alignment , Constraint , Direction , Flex , Layout , Rect } ,
12
+ layout:: { Alignment , Constraint , Direction , Flex , Layout , Margin , Rect } ,
12
13
style:: { Color , Style , Stylize } ,
13
14
text:: Line ,
14
15
widgets:: {
15
- Bar , BarChart , BarGroup , Block , Borders , Cell , Clear , HighlightSpacing , Padding , Row , Table ,
16
+ Bar , BarChart , BarGroup , Block , BorderType , Borders , Cell , Clear , HighlightSpacing ,
17
+ Padding , Row , Table ,
16
18
} ,
17
19
Frame ,
18
20
} ;
@@ -26,12 +28,20 @@ use crate::{
26
28
} ,
27
29
} ;
28
30
31
+ #[ derive( Debug , Default ) ]
32
+ struct ListState {
33
+ offset : usize ,
34
+ selected : Option < usize > ,
35
+ }
36
+
29
37
#[ derive( Debug ) ]
30
38
pub struct Metrics {
31
39
user_input : UserInput ,
32
40
app_packets : Arc < Mutex < Vec < AppPacket > > > ,
33
- port_count : Option < Arc < Mutex < PortCountMetric > > > ,
41
+ metrics : Vec < Arc < Mutex < PortCountMetric > > > ,
34
42
terminate : Arc < AtomicBool > ,
43
+ state : ListState ,
44
+ window_height : usize ,
35
45
}
36
46
37
47
#[ derive( Debug , Clone , Default ) ]
@@ -72,29 +82,51 @@ impl Metrics {
72
82
Self {
73
83
user_input : UserInput :: default ( ) ,
74
84
app_packets : packets,
75
- port_count : None ,
85
+ metrics : Vec :: new ( ) ,
76
86
terminate : Arc :: new ( AtomicBool :: new ( false ) ) ,
87
+ state : ListState :: default ( ) ,
88
+ window_height : 0 ,
77
89
}
78
90
}
79
91
80
- pub fn render ( & self , frame : & mut Frame , block : Rect ) {
81
- let layout = Layout :: default ( )
82
- . direction ( Direction :: Vertical )
83
- . constraints ( [ Constraint :: Length ( 4 ) , Constraint :: Fill ( 1 ) ] )
84
- . flex ( ratatui:: layout:: Flex :: SpaceBetween )
85
- . split ( block) ;
92
+ pub fn render ( & mut self , frame : & mut Frame , block : Rect ) {
93
+ self . window_height = block. height . saturating_sub ( 4 ) as usize / 8 ;
86
94
87
- let block = Layout :: default ( )
88
- . direction ( Direction :: Horizontal )
89
- . constraints ( [
90
- Constraint :: Fill ( 1 ) ,
91
- Constraint :: Percentage ( 90 ) ,
92
- Constraint :: Fill ( 1 ) ,
93
- ] )
94
- . flex ( ratatui:: layout:: Flex :: SpaceBetween )
95
- . split ( layout[ 1 ] ) [ 1 ] ;
95
+ let constraints: Vec < Constraint > = ( 0 ..self . window_height )
96
+ . map ( |_| Constraint :: Length ( 8 ) )
97
+ . collect ( ) ;
96
98
97
- if let Some ( port_count_metric) = & self . port_count {
99
+ let chunks = Layout :: default ( )
100
+ . direction ( Direction :: Vertical )
101
+ . constraints ( constraints)
102
+ . split ( block. inner ( Margin {
103
+ horizontal : 0 ,
104
+ vertical : 2 ,
105
+ } ) ) ;
106
+
107
+ let blocks: Vec < _ > = chunks
108
+ . iter ( )
109
+ . map ( |b| {
110
+ Layout :: default ( )
111
+ . direction ( Direction :: Horizontal )
112
+ . constraints ( [
113
+ Constraint :: Fill ( 1 ) ,
114
+ Constraint :: Percentage ( 90 ) ,
115
+ Constraint :: Fill ( 1 ) ,
116
+ ] )
117
+ . flex ( ratatui:: layout:: Flex :: SpaceBetween )
118
+ . split ( * b) [ 1 ]
119
+ } )
120
+ . collect ( ) ;
121
+
122
+ let metrics_to_display = if self . metrics . len ( ) <= self . window_height {
123
+ self . metrics . clone ( )
124
+ } else {
125
+ self . metrics [ self . state . offset ..self . state . offset + self . window_height ] . to_vec ( )
126
+ } ;
127
+
128
+ // for (index, port_count_metric) in self.metrics[self.state.offset..].iter().enumerate() {
129
+ for ( index, port_count_metric) in metrics_to_display. iter ( ) . enumerate ( ) {
98
130
let metric = { port_count_metric. lock ( ) . unwrap ( ) . clone ( ) } ;
99
131
100
132
let chart = BarChart :: default ( )
@@ -121,21 +153,88 @@ impl Metrics {
121
153
. block (
122
154
Block :: new ( )
123
155
. title_alignment ( Alignment :: Center )
124
- . padding ( Padding :: vertical ( 1 ) )
156
+ . borders ( Borders :: LEFT )
157
+ . border_style ( {
158
+ if self . state . selected . unwrap ( ) - self . state . offset == index {
159
+ Style :: new ( ) . fg ( Color :: Magenta )
160
+ } else {
161
+ Style :: new ( ) . fg ( Color :: Gray )
162
+ }
163
+ } )
164
+ . border_type ( {
165
+ if self . state . selected . unwrap ( ) - self . state . offset == index {
166
+ BorderType :: Thick
167
+ } else {
168
+ BorderType :: Plain
169
+ }
170
+ } )
171
+ . padding ( Padding :: uniform ( 1 ) )
125
172
. title_top ( format ! ( "Port: {}" , metric. port) ) ,
126
173
) ;
127
- frame. render_widget ( chart, block) ;
174
+ frame. render_widget (
175
+ chart,
176
+ blocks[ index] . inner ( Margin {
177
+ horizontal : 0 ,
178
+ vertical : 1 ,
179
+ } ) ,
180
+ ) ;
128
181
}
129
182
}
130
183
131
184
pub fn handle_keys ( & mut self , key_event : KeyEvent ) {
132
- if let KeyCode :: Char ( 'd' ) = key_event. code {
133
- self . terminate
134
- . store ( true , std:: sync:: atomic:: Ordering :: Relaxed ) ;
135
- self . port_count = None ;
136
- self . user_input . clear ( ) ;
137
- self . terminate
138
- . store ( false , std:: sync:: atomic:: Ordering :: Relaxed ) ;
185
+ match key_event. code {
186
+ KeyCode :: Char ( 'd' ) => {
187
+ if let Some ( selected_item_index) = & mut self . state . selected {
188
+ self . terminate
189
+ . store ( true , std:: sync:: atomic:: Ordering :: Relaxed ) ;
190
+
191
+ let _ = self . metrics . remove ( * selected_item_index) ;
192
+
193
+ self . user_input . clear ( ) ;
194
+
195
+ self . state . selected = Some ( selected_item_index. saturating_sub ( 1 ) ) ;
196
+
197
+ self . terminate
198
+ . store ( false , std:: sync:: atomic:: Ordering :: Relaxed ) ;
199
+ }
200
+ }
201
+
202
+ KeyCode :: Char ( 'k' ) => {
203
+ let i = match self . state . selected {
204
+ Some ( i) => {
205
+ if i > self . state . offset {
206
+ i - 1
207
+ } else if i == self . state . offset && self . state . offset > 0 {
208
+ self . state . offset -= 1 ;
209
+ i - 1
210
+ } else {
211
+ 0
212
+ }
213
+ }
214
+ None => 0 ,
215
+ } ;
216
+
217
+ self . state . selected = Some ( i) ;
218
+ }
219
+
220
+ KeyCode :: Char ( 'j' ) => {
221
+ let i = match self . state . selected {
222
+ Some ( i) => {
223
+ if i < self . window_height - 1 {
224
+ cmp:: min ( i + 1 , self . metrics . len ( ) - 1 )
225
+ } else if self . metrics . len ( ) - 1 == i {
226
+ i
227
+ } else {
228
+ self . state . offset += 1 ;
229
+ i + 1
230
+ }
231
+ }
232
+ None => 0 ,
233
+ } ;
234
+
235
+ self . state . selected = Some ( i) ;
236
+ }
237
+ _ => { }
139
238
}
140
239
}
141
240
@@ -150,14 +249,14 @@ impl Metrics {
150
249
151
250
let port: u16 = self . user_input . value ( ) ;
152
251
153
- let port_count = Arc :: new ( Mutex :: new ( PortCountMetric {
252
+ let port_count_metric = Arc :: new ( Mutex :: new ( PortCountMetric {
154
253
port,
155
254
tcp_count : 0 ,
156
255
udp_count : 0 ,
157
256
} ) ) ;
158
257
159
258
thread:: spawn ( {
160
- let port_count = port_count . clone ( ) ;
259
+ let port_count_metric = port_count_metric . clone ( ) ;
161
260
let terminate = self . terminate . clone ( ) ;
162
261
let packets = self . app_packets . clone ( ) ;
163
262
move || {
@@ -170,7 +269,7 @@ impl Metrics {
170
269
if app_packets. is_empty ( ) {
171
270
continue ;
172
271
}
173
- let mut metric = port_count . lock ( ) . unwrap ( ) ;
272
+ let mut metric = port_count_metric . lock ( ) . unwrap ( ) ;
174
273
for app_packet in app_packets[ last_index..] . iter ( ) {
175
274
if terminate. load ( std:: sync:: atomic:: Ordering :: Relaxed ) {
176
275
break ' main;
@@ -218,7 +317,12 @@ impl Metrics {
218
317
}
219
318
} ) ;
220
319
221
- self . port_count = Some ( port_count) ;
320
+ self . metrics . push ( port_count_metric) ;
321
+ if self . metrics . len ( ) == 1 {
322
+ self . state . selected = Some ( 0 ) ;
323
+ }
324
+
325
+ self . user_input . clear ( ) ;
222
326
}
223
327
224
328
_ => {
0 commit comments