Skip to content

Commit f07eefd

Browse files
committed
TUI: add argument to enable arrows + Enter to choose items: Assisted by AI
1 parent b58aef4 commit f07eefd

File tree

11 files changed

+141
-13
lines changed

11 files changed

+141
-13
lines changed

.config/pydoclint-baseline.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,6 @@ src/ansible_navigator/ui_framework/ui.py
333333
--------------------
334334
src/ansible_navigator/ui_framework/ui_config.py
335335
DOC601: Class `UIConfig`: Class docstring contains fewer class attributes than actual class attributes. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)
336-
DOC603: Class `UIConfig`: Class docstring attributes are different from actual class attributes. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Attributes in the class definition but not in the docstring: [color: bool, colors_initialized: bool, grammar_dir: Traversable, osc4: bool, terminal_colors_path: Traversable, theme_path: Traversable]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)
337336
--------------------
338337
src/ansible_navigator/ui_framework/ui_constants.py
339338
DOC601: Class `Color`: Class docstring contains fewer class attributes than actual class attributes. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)

src/ansible_navigator/action_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def initialize_ui(self, refresh: int) -> None:
6767
osc4=self._args.osc4,
6868
terminal_colors_path=TERMINAL_COLORS_PATH,
6969
theme_path=THEME_PATH,
70+
cursor_navigation=bool(getattr(self._args, "cursor_navigation", False)),
7071
)
7172
self._logger.debug("grammar path = %s", config.grammar_dir)
7273
self._logger.debug("theme path = %s", config.theme_path)

src/ansible_navigator/configuration_subsystem/navigator_configuration.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,15 @@ class Internals:
316316
value=SettingsEntryValue(),
317317
version_added="v2.0",
318318
),
319+
SettingsEntry(
320+
name="cursor_navigation",
321+
choices=[True, False],
322+
cli_parameters=CliParameters(short="--cn", action="store_true"),
323+
settings_file_path_override="ui.cursor-navigation",
324+
short_description="Enable arrow-key cursor navigation and Enter selection in menus",
325+
value=SettingsEntryValue(default=False),
326+
version_added="v2.5",
327+
),
319328
SettingsEntry(
320329
name="display_color",
321330
change_after_initial=False,

src/ansible_navigator/data/ansible-navigator.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,14 +518,29 @@
518518
"default": "UTC",
519519
"description": "Specify the IANA time zone to use or 'local' to use the system time zone",
520520
"type": "string"
521+
},
522+
"ui": {
523+
"additionalProperties": false,
524+
"properties": {
525+
"cursor-navigation": {
526+
"default": false,
527+
"description": "Enable arrow-key cursor navigation and Enter selection in menus",
528+
"enum": [
529+
true,
530+
false
531+
],
532+
"type": "boolean"
533+
}
534+
},
535+
"type": "object"
521536
}
522537
}
523538
}
524539
},
525540
"required": [
526541
"ansible-navigator"
527542
],
528-
"title": "ansible-navigator settings v25",
543+
"title": "ansible-navigator settings v24",
529544
"type": "object",
530-
"version": "25"
545+
"version": "24"
531546
}

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 (when cursor navigation enabled)
37+
- `enter` Select highlighted item (when cursor navigation enabled)
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 (when cursor navigation enabled)
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/data/settings-sample.template.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ ansible-navigator:
6363
enable: True
6464
# {{ color.osc4 }}
6565
osc4: True
66+
ui:
67+
# {{ ui.cursor-navigation }}
68+
cursor-navigation: False
6669
editor:
6770
# {{ editor.command }}
6871
command: vim_from_setting

src/ansible_navigator/data/settings-schema.partial.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,15 @@
284284
"time-zone": {
285285
"type": "string"
286286
},
287+
"ui": {
288+
"additionalProperties": false,
289+
"properties": {
290+
"cursor-navigation": {
291+
"type": "boolean"
292+
}
293+
},
294+
"type": "object"
295+
},
287296
"settings": {
288297
"additionalProperties": false,
289298
"properties": {

src/ansible_navigator/ui_framework/ui.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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":

src/ansible_navigator/ui_framework/ui_config.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,29 @@
77

88
@dataclass
99
class UIConfig:
10-
"""Object to hold basic UI settings.
10+
"""The UI configuration.
1111
12-
Used to determine properties about rendering things. An instance of this
13-
class gets threaded throughout most of the UI system, so it can be used for
14-
fairly global things, such as "should we render color, ever?"
12+
Args:
13+
color: Enable the use of color for mode interactive and stdout
14+
colors_initialized: Whether colors have been initialized
15+
cursor_navigation: Enable arrow-key cursor navigation and Enter selection in menus
16+
grammar_dir: The path to the grammar directory
17+
osc4: Enable or disable terminal color changing support with OSC 4
18+
terminal_colors_path: The path to the terminal colors file
19+
theme_path: The path to the theme file
1520
"""
1621

17-
#: Indicates coloring is enabled or disabled
22+
#: Enable the use of color for mode interactive and stdout
1823
color: bool
19-
#: Indicates if the curses colors have been initialized
24+
#: Whether colors have been initialized
2025
colors_initialized: bool
2126
#: The path to the grammar directory
2227
grammar_dir: Traversable
23-
#: Indicates if terminal support for OSC4 is enabled
28+
#: Enable or disable terminal color changing support with OSC 4
2429
osc4: bool
25-
#: The path to the 16 terminal color map
30+
#: The path to the terminal colors file
2631
terminal_colors_path: Traversable
2732
#: The path to the theme file
2833
theme_path: Traversable
34+
#: Enable arrow-key cursor navigation and Enter selection in menus
35+
cursor_navigation: bool = False

tests/fixtures/unit/configuration_subsystem/ansible-navigator.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ ansible-navigator:
3434
color:
3535
enable: False
3636
osc4: False
37+
ui:
38+
cursor-navigation: true
3739
editor:
3840
command: vim_from_setting
3941
console: False

0 commit comments

Comments
 (0)