@@ -2,6 +2,7 @@ use std::collections::HashMap;
22
33use ratatui:: {
44 backend:: Backend ,
5+ crossterm:: event:: { KeyCode , KeyEvent } ,
56 layout:: { Constraint , Layout , Rect } ,
67 style:: { Modifier , Style , Stylize } ,
78 text:: Line ,
@@ -12,7 +13,7 @@ use ratatui::{
1213use crate :: {
1314 color:: { ColorTheme , GraphColorSet } ,
1415 config:: { CoreConfig , CursorType , UiConfig } ,
15- event:: { AppEvent , Receiver , Sender , UserEvent } ,
16+ event:: { AppEvent , Receiver , Sender , UserEvent , UserEventWithCount } ,
1617 external:: copy_to_clipboard,
1718 git:: Repository ,
1819 graph:: { CellWidthType , Graph , GraphImageManager } ,
@@ -43,6 +44,7 @@ pub struct App<'a> {
4344 color_theme : & ' a ColorTheme ,
4445 image_protocol : ImageProtocol ,
4546 tx : Sender ,
47+ numeric_prefix : String ,
4648}
4749
4850impl < ' a > App < ' a > {
@@ -99,6 +101,7 @@ impl<'a> App<'a> {
99101 color_theme,
100102 image_protocol,
101103 tx,
104+ numeric_prefix : String :: new ( ) ,
102105 }
103106 }
104107}
@@ -132,13 +135,31 @@ impl App<'_> {
132135
133136 match self . keybind . get ( & key) {
134137 Some ( UserEvent :: ForceQuit ) => {
138+ self . numeric_prefix . clear ( ) ;
135139 self . tx . send ( AppEvent :: Quit ) ;
136140 }
137141 Some ( ue) => {
138- self . view . handle_event ( * ue, key) ;
142+ let event_with_count = self . process_numeric_prefix ( * ue, key) ;
143+ if let Some ( event_with_count) = event_with_count {
144+ self . view . handle_event_with_count ( event_with_count, key) ;
145+ self . numeric_prefix . clear ( ) ;
146+ }
139147 }
140148 None => {
141- self . view . handle_event ( UserEvent :: Unknown , key) ;
149+ if let KeyCode :: Char ( c) = key. code {
150+ if c. is_ascii_digit ( )
151+ && ( c != '0' || !self . numeric_prefix . is_empty ( ) )
152+ {
153+ self . numeric_prefix . push ( c) ;
154+ continue ;
155+ }
156+ }
157+
158+ self . numeric_prefix . clear ( ) ;
159+ self . view . handle_event_with_count (
160+ UserEventWithCount :: from_event ( UserEvent :: Unknown ) ,
161+ key,
162+ ) ;
142163 }
143164 }
144165 }
@@ -266,6 +287,38 @@ impl App<'_> {
266287}
267288
268289impl App < ' _ > {
290+ fn process_numeric_prefix (
291+ & self ,
292+ user_event : UserEvent ,
293+ _key : KeyEvent ,
294+ ) -> Option < UserEventWithCount > {
295+ let count = if self . numeric_prefix . is_empty ( ) {
296+ 1
297+ } else {
298+ self . numeric_prefix . parse :: < usize > ( ) . unwrap_or ( 1 )
299+ } ;
300+
301+ match user_event {
302+ UserEvent :: NavigateUp
303+ | UserEvent :: NavigateDown
304+ | UserEvent :: NavigateLeft
305+ | UserEvent :: NavigateRight
306+ | UserEvent :: ScrollUp
307+ | UserEvent :: ScrollDown
308+ | UserEvent :: PageUp
309+ | UserEvent :: PageDown
310+ | UserEvent :: HalfPageUp
311+ | UserEvent :: HalfPageDown => Some ( UserEventWithCount :: new ( user_event, count) ) ,
312+ _ => {
313+ if self . numeric_prefix . is_empty ( ) {
314+ Some ( UserEventWithCount :: new ( user_event, 1 ) )
315+ } else {
316+ None
317+ }
318+ }
319+ }
320+ }
321+
269322 fn open_detail ( & mut self ) {
270323 if let View :: List ( ref mut view) = self . view {
271324 let commit_list_state = view. take_list_state ( ) ;
@@ -398,3 +451,151 @@ impl App<'_> {
398451 }
399452 }
400453}
454+
455+ #[ cfg( test) ]
456+ mod tests {
457+ use super :: * ;
458+
459+ // Helper function to test numeric prefix parsing logic
460+ fn test_process_numeric_prefix_logic (
461+ numeric_prefix : & str ,
462+ user_event : UserEvent ,
463+ ) -> Option < UserEventWithCount > {
464+ let count = if numeric_prefix. is_empty ( ) {
465+ 1
466+ } else {
467+ numeric_prefix. parse :: < usize > ( ) . unwrap_or ( 1 )
468+ } ;
469+
470+ match user_event {
471+ UserEvent :: NavigateUp
472+ | UserEvent :: NavigateDown
473+ | UserEvent :: NavigateLeft
474+ | UserEvent :: NavigateRight
475+ | UserEvent :: ScrollUp
476+ | UserEvent :: ScrollDown
477+ | UserEvent :: PageUp
478+ | UserEvent :: PageDown
479+ | UserEvent :: HalfPageUp
480+ | UserEvent :: HalfPageDown => Some ( UserEventWithCount :: new ( user_event, count) ) ,
481+ _ => {
482+ if numeric_prefix. is_empty ( ) {
483+ Some ( UserEventWithCount :: new ( user_event, 1 ) )
484+ } else {
485+ None
486+ }
487+ }
488+ }
489+ }
490+
491+ #[ test]
492+ fn test_process_numeric_prefix_no_prefix ( ) {
493+ let result = test_process_numeric_prefix_logic ( "" , UserEvent :: NavigateDown ) ;
494+
495+ assert ! ( result. is_some( ) ) ;
496+ let event_with_count = result. unwrap ( ) ;
497+ assert_eq ! ( event_with_count. event, UserEvent :: NavigateDown ) ;
498+ assert_eq ! ( event_with_count. count, 1 ) ;
499+ }
500+
501+ #[ test]
502+ fn test_process_numeric_prefix_with_prefix ( ) {
503+ let result = test_process_numeric_prefix_logic ( "5" , UserEvent :: NavigateDown ) ;
504+
505+ assert ! ( result. is_some( ) ) ;
506+ let event_with_count = result. unwrap ( ) ;
507+ assert_eq ! ( event_with_count. event, UserEvent :: NavigateDown ) ;
508+ assert_eq ! ( event_with_count. count, 5 ) ;
509+ }
510+
511+ #[ test]
512+ fn test_process_numeric_prefix_invalid_number ( ) {
513+ let result = test_process_numeric_prefix_logic ( "abc" , UserEvent :: NavigateDown ) ;
514+
515+ assert ! ( result. is_some( ) ) ;
516+ let event_with_count = result. unwrap ( ) ;
517+ assert_eq ! ( event_with_count. event, UserEvent :: NavigateDown ) ;
518+ assert_eq ! ( event_with_count. count, 1 ) ; // Should fallback to 1
519+ }
520+
521+ #[ test]
522+ fn test_process_numeric_prefix_countable_events ( ) {
523+ let countable_events = [
524+ UserEvent :: NavigateUp ,
525+ UserEvent :: NavigateDown ,
526+ UserEvent :: NavigateLeft ,
527+ UserEvent :: NavigateRight ,
528+ UserEvent :: ScrollUp ,
529+ UserEvent :: ScrollDown ,
530+ UserEvent :: PageUp ,
531+ UserEvent :: PageDown ,
532+ UserEvent :: HalfPageUp ,
533+ UserEvent :: HalfPageDown ,
534+ ] ;
535+
536+ for event in countable_events {
537+ let result = test_process_numeric_prefix_logic ( "3" , event) ;
538+ assert ! ( result. is_some( ) ) ;
539+ let event_with_count = result. unwrap ( ) ;
540+ assert_eq ! ( event_with_count. event, event) ;
541+ assert_eq ! ( event_with_count. count, 3 ) ;
542+ }
543+ }
544+
545+ #[ test]
546+ fn test_process_numeric_prefix_non_countable_events ( ) {
547+ let non_countable_events = [
548+ UserEvent :: Quit ,
549+ UserEvent :: Confirm ,
550+ UserEvent :: Cancel ,
551+ UserEvent :: HelpToggle ,
552+ UserEvent :: Search ,
553+ UserEvent :: ShortCopy ,
554+ UserEvent :: FullCopy ,
555+ ] ;
556+
557+ for event in non_countable_events {
558+ let result = test_process_numeric_prefix_logic ( "5" , event) ;
559+ assert ! ( result. is_none( ) ) ; // Should return None when prefix exists but event isn't countable
560+ }
561+ }
562+
563+ #[ test]
564+ fn test_process_numeric_prefix_non_countable_events_no_prefix ( ) {
565+ let result = test_process_numeric_prefix_logic ( "" , UserEvent :: Confirm ) ;
566+ assert ! ( result. is_some( ) ) ;
567+ let event_with_count = result. unwrap ( ) ;
568+ assert_eq ! ( event_with_count. event, UserEvent :: Confirm ) ;
569+ assert_eq ! ( event_with_count. count, 1 ) ;
570+ }
571+
572+ #[ test]
573+ fn test_process_numeric_prefix_large_numbers ( ) {
574+ let result = test_process_numeric_prefix_logic ( "999" , UserEvent :: NavigateDown ) ;
575+
576+ assert ! ( result. is_some( ) ) ;
577+ let event_with_count = result. unwrap ( ) ;
578+ assert_eq ! ( event_with_count. event, UserEvent :: NavigateDown ) ;
579+ assert_eq ! ( event_with_count. count, 999 ) ;
580+ }
581+
582+ #[ test]
583+ fn test_process_numeric_prefix_zero ( ) {
584+ let result = test_process_numeric_prefix_logic ( "0" , UserEvent :: NavigateUp ) ;
585+
586+ assert ! ( result. is_some( ) ) ;
587+ let event_with_count = result. unwrap ( ) ;
588+ assert_eq ! ( event_with_count. event, UserEvent :: NavigateUp ) ;
589+ assert_eq ! ( event_with_count. count, 1 ) ; // UserEventWithCount::new converts 0 to 1
590+ }
591+
592+ #[ test]
593+ fn test_process_numeric_prefix_multi_digit ( ) {
594+ let result = test_process_numeric_prefix_logic ( "42" , UserEvent :: ScrollDown ) ;
595+
596+ assert ! ( result. is_some( ) ) ;
597+ let event_with_count = result. unwrap ( ) ;
598+ assert_eq ! ( event_with_count. event, UserEvent :: ScrollDown ) ;
599+ assert_eq ! ( event_with_count. count, 42 ) ;
600+ }
601+ }
0 commit comments