@@ -199,7 +199,12 @@ def __init__(
199199 self ._status_color = 0
200200 self ._screen : Window = curses .initscr ()
201201 self ._screen .timeout (refresh )
202+ self ._screen .keypad (True ) # Enable keypad mode for proper key handling
202203 self ._one_line_input = FormHandlerText (screen = self ._screen , ui_config = self ._ui_config )
204+ # When cursor navigation is enabled, this tracks which visible row to highlight
205+ self ._highlight_line_offset : int | None = None
206+ # When cursor navigation is enabled, this tracks the cursor position in menus
207+ self ._menu_cursor_pos : int = 0
203208
204209 def clear (self ) -> None :
205210 """Clear the screen."""
@@ -464,7 +469,7 @@ def _display(
464469 index_width = len (str (count ))
465470
466471 keypad = {str (x ) for x in range (10 )}
467- other_valid_keys = ["+" , "-" , "_" , "KEY_F(5)" , "^[" , "\x1b " ]
472+ other_valid_keys = ["+" , "-" , "_" , "KEY_F(5)" , "^[" , "\x1b " , "CURSOR_ENTER" ]
468473
469474 while True :
470475 self ._screen .erase ()
@@ -479,10 +484,26 @@ def _display(
479484 line_index = line_numbers [idx ]
480485 line_index_str = str (line_index ).rjust (index_width )
481486 prefix = f"{ line_index_str } \u2502 "
487+ # Apply highlight decoration when enabled and this is the selected row
488+ if (indent_heading and self ._ui_config .cursor_navigation and
489+ self ._highlight_line_offset is not None and idx == self ._highlight_line_offset ):
490+ # Rebuild the line with reverse-video decoration added
491+ highlighted_parts = tuple (
492+ CursesLinePart (
493+ column = lp .column ,
494+ string = lp .string ,
495+ color = lp .color ,
496+ decoration = lp .decoration | curses .A_REVERSE ,
497+ )
498+ for lp in line
499+ )
500+ line_to_draw = CursesLine (highlighted_parts )
501+ else :
502+ line_to_draw = line
482503 self ._add_line (
483504 window = self ._screen ,
484505 lineno = idx + len (heading ),
485- line = line ,
506+ line = line_to_draw ,
486507 prefix = prefix ,
487508 )
488509
@@ -504,6 +525,16 @@ def _display(
504525 if await_input :
505526 char = self ._screen .getch ()
506527 key = "KEY_F(5)" if char == - 1 else curses .keyname (char ).decode ()
528+ # Debug: log the raw char and converted key for troubleshooting
529+ if self ._ui_config .cursor_navigation :
530+ self ._logger .debug ("Raw char: %s, Converted key: '%s'" , char , key )
531+ # Check for Enter key codes and return a special value for cursor navigation
532+ if (self ._ui_config .cursor_navigation and
533+ char in [10 , 13 ]): # Enter key codes: 10=LF, 13=CR
534+ self ._logger .debug (
535+ "Enter key detected! Raw char: %s, setting key to CURSOR_ENTER" , char
536+ )
537+ key = "CURSOR_ENTER"
507538 else :
508539 key = "KEY_F(5)"
509540
@@ -915,6 +946,17 @@ def _show_menu(
915946 showing_indices ,
916947 )
917948
949+ # Determine which row to highlight, if enabled
950+ self ._highlight_line_offset = None
951+ if self ._ui_config .cursor_navigation and self ._menu_indices :
952+ self ._menu_cursor_pos = max (0 , min (self ._menu_cursor_pos ,
953+ len (self ._menu_indices ) - 1 ))
954+ selected_global_index = self ._menu_indices [self ._menu_cursor_pos ]
955+ try :
956+ self ._highlight_line_offset = showing_indices .index (selected_global_index )
957+ except ValueError :
958+ self ._highlight_line_offset = None
959+
918960 entry = self ._display (
919961 lines = menu_lines ,
920962 line_numbers = line_numbers ,
@@ -925,9 +967,45 @@ def _show_menu(
925967 await_input = await_input ,
926968 )
927969
970+ # Debug: log what entry we received
971+ if self ._ui_config .cursor_navigation :
972+ self ._logger .debug ("Received entry: '%s'" , entry )
973+
974+ # Handle arrow navigation for menus when enabled
928975 if entry in ["KEY_RESIZE" , "KEY_DOWN" , "KEY_UP" , "KEY_NPAGE" , "KEY_PPAGE" , "^F" , "^B" ]:
976+ if (entry in ["KEY_DOWN" , "KEY_UP" ] and self ._ui_config .cursor_navigation and
977+ self ._menu_indices ):
978+ # Move the cursor position
979+ if entry == "KEY_DOWN" and self ._menu_cursor_pos < len (self ._menu_indices ) - 1 :
980+ self ._menu_cursor_pos += 1
981+ # If moved past the last visible item, scroll down one
982+ if (self ._highlight_line_offset is None or
983+ self ._highlight_line_offset >= len (showing_indices ) - 1 ):
984+ # Mimic _display scroll down
985+ viewport_height = self ._screen_height - len (menu_heading ) - 1
986+ self .scroll (max (min (self .scroll () + 1 , len (self ._menu_indices )),
987+ viewport_height ))
988+ elif entry == "KEY_UP" and self ._menu_cursor_pos > 0 :
989+ self ._menu_cursor_pos -= 1
990+ # If moved before the first visible item, scroll up one
991+ if self ._highlight_line_offset is None or self ._highlight_line_offset <= 0 :
992+ viewport_height = self ._screen_height - len (menu_heading ) - 1
993+ self .scroll (max (self .scroll () - 1 , viewport_height ))
994+ # Re-render with updated highlight
995+ continue
996+ # Otherwise, preserve prior behavior
929997 continue
930998
999+ # Enter key should select the highlighted item when cursor nav is enabled
1000+ if (self ._ui_config .cursor_navigation and
1001+ (entry == "CURSOR_ENTER" or entry in ["^J" , "^M" , "KEY_ENTER" , "KEY_RETURN" ]) and
1002+ self ._menu_indices ):
1003+ self ._logger .debug ("Enter key selection triggered! Entry: '%s', Selecting index %s" ,
1004+ entry , self ._menu_cursor_pos )
1005+ index_to_select = self ._menu_indices [self ._menu_cursor_pos ]
1006+ entry = str (index_to_select )
1007+ self ._logger .debug ("Changed entry to: '%s'" , entry )
1008+
9311009 name , action = self ._template_match_action (entry , current )
9321010 if name and action :
9331011 if name == "select" :
0 commit comments