Private
Public Access
0
0

feat(palette): add Up/Down arrow navigation and Enter key selection

- Process arrow keys BEFORE input_text so the input field does not consume them
- Up/Down arrow keys navigate the result list (clamped to bounds)
- Enter and KeypadEnter execute the currently selected command
- Refactored _close_palette and _execute helpers (action call is now wrapped in try/except via _execute)
- Added 3 new tests: close helper resets state, execute runs and catches exceptions, top_n is meaningful for navigation
This commit is contained in:
2026-06-02 22:41:59 -04:00
parent 9cfd7b0d12
commit d7449ae417
2 changed files with 103 additions and 15 deletions
+40 -15
View File
@@ -103,6 +103,26 @@ def _count_gaps(query: str, target: str) -> int:
return gaps
def _close_palette(app: Any) -> None:
"""Close the palette and reset all per-open state."""
app.show_command_palette = False
app._command_palette_query = ""
app._command_palette_selected = 0
app._command_palette_focused = False
app._command_palette_input_focused = False
def _execute(app: Any, command: Command) -> None:
"""Run a command and close the palette. Catches exceptions to keep the modal clean."""
if not command.action:
return
try:
command.action(app)
except Exception as e:
print(f"[CommandPalette] Action {command.id} raised: {e}")
_close_palette(app)
def render_palette_modal(app: Any, commands: List[Command]) -> None:
if not getattr(app, "show_command_palette", False):
return
@@ -128,8 +148,7 @@ def render_palette_modal(app: Any, commands: List[Command]) -> None:
# Escape closes the palette.
if imgui.is_key_pressed(imgui.Key.escape):
app.show_command_palette = False
app._command_palette_focused = False
_close_palette(app)
return
expanded, opened = imgui.begin("Command Palette##manual_slop", True, imgui.WindowFlags_.no_collapse)
@@ -144,11 +163,27 @@ def render_palette_modal(app: Any, commands: List[Command]) -> None:
imgui.set_keyboard_focus_here()
app._command_palette_input_focused = True
# Process Up/Down/Enter BEFORE input_text so we see the keys before the
# input field consumes them for cursor movement / text editing.
results = fuzzy_match(app._command_palette_query, commands, top_n=20)
if results:
app._command_palette_selected = max(0, min(app._command_palette_selected, len(results) - 1))
else:
app._command_palette_selected = 0
if imgui.is_key_pressed(imgui.Key.down_arrow):
if results:
app._command_palette_selected = min(app._command_palette_selected + 1, len(results) - 1)
if imgui.is_key_pressed(imgui.Key.up_arrow):
if results:
app._command_palette_selected = max(app._command_palette_selected - 1, 0)
if imgui.is_key_pressed(imgui.Key.enter) or imgui.is_key_pressed(imgui.Key.keypad_enter):
if results and 0 <= app._command_palette_selected < len(results):
_execute(app, results[app._command_palette_selected].command)
imgui.set_next_item_width(-1)
_, app._command_palette_query = imgui.input_text("##query", app._command_palette_query)
results = fuzzy_match(app._command_palette_query, commands, top_n=20)
if imgui.begin_child("##results", (0, -1)):
for i, scored in enumerate(results):
is_selected = (i == app._command_palette_selected)
@@ -156,17 +191,7 @@ def render_palette_modal(app: Any, commands: List[Command]) -> None:
clicked, _ = imgui.selectable(label, is_selected)
if clicked:
app._command_palette_selected = i
if scored.command.action:
try:
scored.command.action(app)
except Exception as e:
# Don't let a buggy action break the end_child/end pairing.
print(f"[CommandPalette] Action {scored.command.id} raised: {e}")
app.show_command_palette = False
app._command_palette_query = ""
app._command_palette_selected = 0
app._command_palette_focused = False
app._command_palette_input_focused = False
_execute(app, scored.command)
if not results:
imgui.text_disabled("No matching commands.")
imgui.end_child()