11# cspell:ignore KEY_NPAGE, KEY_PPAGE
2+ # pylint: disable=too-many-lines
23"""The main UI renderer."""
34
45from __future__ import annotations
@@ -199,7 +200,12 @@ def __init__(
199200 self ._status_color = 0
200201 self ._screen : Window = curses .initscr ()
201202 self ._screen .timeout (refresh )
203+ self ._screen .keypad (True ) # Enable keypad mode for proper key handling
202204 self ._one_line_input = FormHandlerText (screen = self ._screen , ui_config = self ._ui_config )
205+ # This tracks which visible row to highlight
206+ self ._highlight_line_offset : int | None = None
207+ # This tracks the cursor position in menus
208+ self ._menu_cursor_pos : int = 0
203209
204210 def clear (self ) -> None :
205211 """Clear the screen."""
@@ -464,7 +470,7 @@ def _display(
464470 index_width = len (str (count ))
465471
466472 keypad = {str (x ) for x in range (10 )}
467- other_valid_keys = ["+" , "-" , "_" , "KEY_F(5)" , "^[" , "\x1b " ]
473+ other_valid_keys = ["+" , "-" , "_" , "KEY_F(5)" , "^[" , "\x1b " , "CURSOR_ENTER" ]
468474
469475 while True :
470476 self ._screen .erase ()
@@ -479,10 +485,29 @@ def _display(
479485 line_index = line_numbers [idx ]
480486 line_index_str = str (line_index ).rjust (index_width )
481487 prefix = f"{ line_index_str } \u2502 "
488+ # Apply highlight decoration when this is the selected row
489+ if (
490+ indent_heading
491+ and self ._highlight_line_offset is not None
492+ and idx == self ._highlight_line_offset
493+ ):
494+ # Rebuild the line with reverse-video decoration added
495+ highlighted_parts = tuple (
496+ CursesLinePart (
497+ column = lp .column ,
498+ string = lp .string ,
499+ color = lp .color ,
500+ decoration = lp .decoration | curses .A_REVERSE ,
501+ )
502+ for lp in line
503+ )
504+ line_to_draw = CursesLine (highlighted_parts )
505+ else :
506+ line_to_draw = line
482507 self ._add_line (
483508 window = self ._screen ,
484509 lineno = idx + len (heading ),
485- line = line ,
510+ line = line_to_draw ,
486511 prefix = prefix ,
487512 )
488513
@@ -504,6 +529,14 @@ def _display(
504529 if await_input :
505530 char = self ._screen .getch ()
506531 key = "KEY_F(5)" if char == - 1 else curses .keyname (char ).decode ()
532+ # Debug: log the raw char and converted key for troubleshooting
533+ self ._logger .debug ("Raw char: %s, Converted key: '%s'" , char , key )
534+ # Check for Enter key codes and return a special value for cursor navigation
535+ if char in [10 , 13 ]: # Enter key codes: 10=LF, 13=CR
536+ self ._logger .debug (
537+ "Enter key detected! Raw char: %s, setting key to CURSOR_ENTER" , char
538+ )
539+ key = "CURSOR_ENTER"
507540 else :
508541 key = "KEY_F(5)"
509542
@@ -915,6 +948,19 @@ def _show_menu(
915948 showing_indices ,
916949 )
917950
951+ # Determine which row to highlight, if enabled
952+ self ._highlight_line_offset = None
953+ if self ._menu_indices :
954+ self ._menu_cursor_pos = max (
955+ 0 ,
956+ min (self ._menu_cursor_pos , len (self ._menu_indices ) - 1 ),
957+ )
958+ selected_global_index = self ._menu_indices [self ._menu_cursor_pos ]
959+ try :
960+ self ._highlight_line_offset = showing_indices .index (selected_global_index )
961+ except ValueError :
962+ self ._highlight_line_offset = None
963+
918964 entry = self ._display (
919965 lines = menu_lines ,
920966 line_numbers = line_numbers ,
@@ -925,9 +971,52 @@ def _show_menu(
925971 await_input = await_input ,
926972 )
927973
974+ # Debug: log what entry we received
975+ self ._logger .debug ("Received entry: '%s'" , entry )
976+
977+ # Handle arrow navigation for menus when enabled
928978 if entry in ["KEY_RESIZE" , "KEY_DOWN" , "KEY_UP" , "KEY_NPAGE" , "KEY_PPAGE" , "^F" , "^B" ]:
979+ if entry in ["KEY_DOWN" , "KEY_UP" ] and self ._menu_indices :
980+ # Move the cursor position
981+ if entry == "KEY_DOWN" and self ._menu_cursor_pos < len (self ._menu_indices ) - 1 :
982+ self ._menu_cursor_pos += 1
983+ # If moved past the last visible item, scroll down one
984+ if (
985+ self ._highlight_line_offset is None
986+ or self ._highlight_line_offset >= len (showing_indices ) - 1
987+ ):
988+ # Mimic _display scroll down
989+ viewport_height = self ._screen_height - len (menu_heading ) - 1
990+ self .scroll (
991+ max (
992+ min (self .scroll () + 1 , len (self ._menu_indices )),
993+ viewport_height ,
994+ ),
995+ )
996+ elif entry == "KEY_UP" and self ._menu_cursor_pos > 0 :
997+ self ._menu_cursor_pos -= 1
998+ # If moved before the first visible item, scroll up one
999+ if self ._highlight_line_offset is None or self ._highlight_line_offset <= 0 :
1000+ viewport_height = self ._screen_height - len (menu_heading ) - 1
1001+ self .scroll (max (self .scroll () - 1 , viewport_height ))
1002+ # Re-render with updated highlight
1003+ continue
1004+ # Otherwise, preserve prior behavior
9291005 continue
9301006
1007+ # Enter key should select the highlighted item for cursor navigation
1008+ if (
1009+ entry == "CURSOR_ENTER" or entry in ["^J" , "^M" , "KEY_ENTER" , "KEY_RETURN" ]
1010+ ) and self ._menu_indices :
1011+ self ._logger .debug (
1012+ "Enter key selection triggered! Entry: '%s', Selecting index %s" ,
1013+ entry ,
1014+ self ._menu_cursor_pos ,
1015+ )
1016+ index_to_select = self ._menu_indices [self ._menu_cursor_pos ]
1017+ entry = str (index_to_select )
1018+ self ._logger .debug ("Changed entry to: '%s'" , entry )
1019+
9311020 name , action = self ._template_match_action (entry , current )
9321021 if name and action :
9331022 if name == "select" :
0 commit comments