Skip to content

Commit b17ed37

Browse files
committed
TUI: enable arrows + Enter navigation: Assisted by AI
1 parent b58aef4 commit b17ed37

File tree

2 files changed

+94
-2
lines changed

2 files changed

+94
-2
lines changed

src/ansible_navigator/data/help.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,15 @@
3333
- `[0-9]` Go to menu item
3434
- `:<number> ` Go to menu item
3535
- `:{{ n|filter }} ` Template the menu item
36+
- `arrow up, arrow down` Move cursor
37+
- `enter` Select highlighted item
3638

3739
# Content and tasks
3840

3941
- `[0-9]` Go to task number
4042
- `:<number>` Go to task number
4143
- `+, - ` Next/Previous task
44+
- `arrow up, arrow down` Next/Previous task
4245
- `_, :_` Toggle hidden keys
4346
- `:{{ key|filter }}` Template the key's value
4447
- `:d, :doc` Show the doc for the current task's module

src/ansible_navigator/ui_framework/ui.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# cspell:ignore KEY_NPAGE, KEY_PPAGE
2+
# pylint: disable=too-many-lines
23
"""The main UI renderer."""
34

45
from __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

Comments
 (0)