1818use crate :: { bpf_program:: { BpfProgram , Process } , helpers:: program_type_to_string} ;
1919use circular_buffer:: CircularBuffer ;
2020use libbpf_rs:: { query:: ProgInfoIter , Iter , Link } ;
21+ use ratatui:: widgets:: ScrollbarState ;
2122use ratatui:: widgets:: TableState ;
2223use std:: {
2324 collections:: HashMap ,
@@ -33,6 +34,8 @@ use tui_input::Input;
3334pub struct App {
3435 pub mode : Mode ,
3536 pub table_state : TableState ,
37+ pub vertical_scroll : usize ,
38+ pub vertical_scroll_state : ScrollbarState ,
3639 pub header_columns : [ String ; 7 ] ,
3740 pub items : Arc < Mutex < Vec < BpfProgram > > > ,
3841 pub data_buf : Arc < Mutex < CircularBuffer < 20 , PeriodMeasure > > > ,
@@ -119,6 +122,8 @@ impl App {
119122 pub fn new ( ) -> App {
120123 let mut app = App {
121124 mode : Mode :: Table ,
125+ vertical_scroll : 0 ,
126+ vertical_scroll_state : ScrollbarState :: new ( 0 ) ,
122127 table_state : TableState :: default ( ) ,
123128 header_columns : [
124129 String :: from ( "ID" ) ,
@@ -302,14 +307,16 @@ impl App {
302307 let i = match self . table_state . selected ( ) {
303308 Some ( i) => {
304309 if i >= items. len ( ) - 1 {
305- 0
310+ items . len ( ) - 1
306311 } else {
312+ self . vertical_scroll = self . vertical_scroll . saturating_add ( 1 ) ;
307313 i + 1
308314 }
309315 }
310316 None => 0 ,
311317 } ;
312318 self . table_state . select ( Some ( i) ) ;
319+ self . vertical_scroll_state = self . vertical_scroll_state . position ( self . vertical_scroll ) ;
313320 }
314321 }
315322
@@ -319,14 +326,16 @@ impl App {
319326 let i = match self . table_state . selected ( ) {
320327 Some ( i) => {
321328 if i == 0 {
322- items . len ( ) - 1
329+ 0
323330 } else {
331+ self . vertical_scroll = self . vertical_scroll . saturating_sub ( 1 ) ;
324332 i - 1
325333 }
326334 }
327- None => items . len ( ) - 1 ,
335+ None => return , // do nothing if table_state == None && previous_program () called
328336 } ;
329337 self . table_state . select ( Some ( i) ) ;
338+ self . vertical_scroll_state = self . vertical_scroll_state . position ( self . vertical_scroll ) ;
330339 }
331340 }
332341
@@ -473,19 +482,24 @@ mod tests {
473482 app. items . lock ( ) . unwrap ( ) . push ( prog_2. clone ( ) ) ;
474483
475484 // Initially no item is selected
476- assert_eq ! ( app. selected_program( ) , None ) ;
485+ assert_eq ! ( app. selected_program( ) , None , "expected no program" ) ;
486+ assert_eq ! ( app. vertical_scroll, 0 , "expected init with 0, got: {}" , app. vertical_scroll) ;
477487
478488 // After calling next, the first item should be selected
479489 app. next_program ( ) ;
480- assert_eq ! ( app. selected_program( ) , Some ( prog_1. clone( ) ) ) ;
490+ assert_eq ! ( app. selected_program( ) , Some ( prog_1. clone( ) ) , "expected prog_1" ) ;
491+ assert_eq ! ( app. vertical_scroll, 0 , "expected scroll 0, got: {}" , app. vertical_scroll) ;
481492
482493 // After calling next again, the second item should be selected
483494 app. next_program ( ) ;
484- assert_eq ! ( app. selected_program( ) , Some ( prog_2. clone( ) ) ) ;
495+ assert_eq ! ( app. selected_program( ) , Some ( prog_2. clone( ) ) , "expected prog_2" ) ;
496+ assert_eq ! ( app. vertical_scroll, 1 , "expected scroll 1, got: {}" , app. vertical_scroll) ;
485497
486- // After calling next again, we should wrap around to the first item
498+ // After calling next again, the second item should still be selected without wrapping
487499 app. next_program ( ) ;
488- assert_eq ! ( app. selected_program( ) , Some ( prog_1. clone( ) ) ) ;
500+ assert_eq ! ( app. selected_program( ) , Some ( prog_2. clone( ) ) , "expected prog_2; no wrap around" ) ;
501+ assert_eq ! ( app. vertical_scroll, 1 , "expected scroll 1, got: {}" , app. vertical_scroll) ;
502+
489503 }
490504
491505 #[ test]
@@ -494,10 +508,17 @@ mod tests {
494508
495509 // Initially no item is selected
496510 assert_eq ! ( app. selected_program( ) , None ) ;
511+
512+ // Initially ScrollbarState is 0
513+ assert_eq ! ( app. vertical_scroll_state, ScrollbarState :: new( 0 ) , "unexpected ScrollbarState" ) ;
514+ assert_eq ! ( app. vertical_scroll, 0 , "expected 0 vertical_scroll, got: {}" , app. vertical_scroll) ;
497515
498516 // After calling previous, no item should be selected
499517 app. previous_program ( ) ;
500518 assert_eq ! ( app. selected_program( ) , None ) ;
519+
520+ assert_eq ! ( app. vertical_scroll_state, ScrollbarState :: new( 0 ) , "unexpected ScrollbarState" ) ;
521+ assert_eq ! ( app. vertical_scroll, 0 , "expected 0 vertical_scroll, got: {}" , app. vertical_scroll) ;
501522 }
502523
503524 #[ test]
@@ -534,19 +555,27 @@ mod tests {
534555 app. items . lock ( ) . unwrap ( ) . push ( prog_2. clone ( ) ) ;
535556
536557 // Initially no item is selected
537- assert_eq ! ( app. selected_program( ) , None ) ;
558+ assert_eq ! ( app. selected_program( ) , None , "expected no program" ) ;
559+ assert_eq ! ( app. vertical_scroll, 0 , "expected init with 0" ) ;
538560
539- // After calling previous, the last item should be selected
561+ // After calling previous with no table state, nothing should be selected
540562 app. previous_program ( ) ;
541- assert_eq ! ( app. selected_program( ) , Some ( prog_2. clone( ) ) ) ;
563+ assert_eq ! ( app. selected_program( ) , None , "expected None" ) ;
564+ assert_eq ! ( app. vertical_scroll, 0 , "still 0, no wrapping" ) ;
542565
543- // After calling previous again, the first item should be selected
566+ // After calling previous again, still nothing should be selected
544567 app. previous_program ( ) ;
545- assert_eq ! ( app. selected_program( ) , Some ( prog_1. clone( ) ) ) ;
568+ assert_eq ! ( app. selected_program( ) , None , "still None" ) ;
569+ assert_eq ! ( app. vertical_scroll, 0 , "still 0, no wrapping" ) ;
570+
571+ app. next_program ( ) ; // populate table state and expect prog_1 selected
572+ assert_eq ! ( app. selected_program( ) , Some ( prog_1. clone( ) ) , "expected prog_1" ) ;
573+ assert_eq ! ( app. vertical_scroll, 0 , "expected scroll 0" ) ;
546574
547- // After calling previous again, we should wrap around to the last item
575+ // After calling previous again, prog_1 should still be selected (0th index)
548576 app. previous_program ( ) ;
549- assert_eq ! ( app. selected_program( ) , Some ( prog_2. clone( ) ) ) ;
577+ assert_eq ! ( app. selected_program( ) , Some ( prog_1. clone( ) ) , "still expecting prog_1" ) ;
578+ assert_eq ! ( app. vertical_scroll, 0 , "still 0, no wrapping" ) ;
550579 }
551580
552581 #[ test]
0 commit comments