feat: improve terminal navigation and UI behavior
- refine tree and history navigation behavior - improve scrolling continuity in the terminal UI - keep AI worklog and prompt log updated - validate changes with project quality checks
This commit is contained in:
154
src/app/ui.py
154
src/app/ui.py
@@ -76,12 +76,34 @@ def build_history_lines(
|
||||
]
|
||||
|
||||
|
||||
def clamp_scroll_offset(
|
||||
selected_index: int, scroll_offset: int, visible_count: int, total_count: int
|
||||
) -> int:
|
||||
"""Keep the selected row within the visible scrolling window."""
|
||||
|
||||
if visible_count <= 0 or total_count <= visible_count:
|
||||
return 0
|
||||
|
||||
max_offset = total_count - visible_count
|
||||
offset = max(0, min(scroll_offset, max_offset))
|
||||
|
||||
if selected_index < offset:
|
||||
return selected_index
|
||||
|
||||
if selected_index >= offset + visible_count:
|
||||
return selected_index - visible_count + 1
|
||||
|
||||
return offset
|
||||
|
||||
|
||||
class TerminalUI:
|
||||
"""Minimal curses UI for the directory navigator MVP."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._history_mode = False
|
||||
self._history_selected_index = 0
|
||||
self._tree_scroll_offset = 0
|
||||
self._history_scroll_offset = 0
|
||||
|
||||
def run(self, navigator: Navigator) -> Path:
|
||||
"""Start the interactive session and return the final directory path."""
|
||||
@@ -93,13 +115,14 @@ class TerminalUI:
|
||||
def _run_session(self, stdscr: curses.window, navigator: Navigator) -> Path:
|
||||
curses.curs_set(0)
|
||||
stdscr.keypad(True)
|
||||
original_path = navigator.current_path
|
||||
|
||||
while True:
|
||||
self._render(stdscr, navigator)
|
||||
key = stdscr.getch()
|
||||
|
||||
if key == ESCAPE_KEY:
|
||||
return navigator.current_path
|
||||
return original_path
|
||||
|
||||
if key == ord("h"):
|
||||
self._toggle_history_mode(navigator)
|
||||
@@ -109,44 +132,9 @@ class TerminalUI:
|
||||
self._handle_history_key(key, navigator)
|
||||
continue
|
||||
|
||||
if key == curses.KEY_UP:
|
||||
navigator.set_selected_index(
|
||||
move_selection(
|
||||
navigator.selected_index,
|
||||
step=-1,
|
||||
entry_count=len(navigator.visible_entries),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if key == curses.KEY_DOWN:
|
||||
navigator.set_selected_index(
|
||||
move_selection(
|
||||
navigator.selected_index,
|
||||
step=1,
|
||||
entry_count=len(navigator.visible_entries),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if key == curses.KEY_RIGHT:
|
||||
navigator.expand_selected_directory()
|
||||
continue
|
||||
|
||||
if key == curses.KEY_LEFT:
|
||||
navigator.collapse_selected_directory()
|
||||
continue
|
||||
|
||||
if key == ord("b"):
|
||||
navigator.go_back()
|
||||
continue
|
||||
|
||||
if key == ord("f"):
|
||||
navigator.go_forward()
|
||||
continue
|
||||
|
||||
if key in (curses.KEY_ENTER, 10, 13):
|
||||
navigator.enter_selected_directory()
|
||||
tree_result = self._handle_tree_key(key, navigator)
|
||||
if tree_result is not None:
|
||||
return tree_result
|
||||
|
||||
def _render(self, stdscr: curses.window, navigator: Navigator) -> None:
|
||||
stdscr.erase()
|
||||
@@ -160,7 +148,19 @@ class TerminalUI:
|
||||
path_text = str(navigator.current_path)
|
||||
stdscr.addnstr(0, 0, path_text, width - 1)
|
||||
|
||||
for row, line in enumerate(build_render_lines(navigator), start=2):
|
||||
lines = build_render_lines(navigator)
|
||||
visible_count = max(0, height - 2)
|
||||
self._tree_scroll_offset = clamp_scroll_offset(
|
||||
navigator.selected_index,
|
||||
self._tree_scroll_offset,
|
||||
visible_count,
|
||||
len(lines),
|
||||
)
|
||||
|
||||
for row, line in enumerate(
|
||||
lines[self._tree_scroll_offset : self._tree_scroll_offset + visible_count],
|
||||
start=2,
|
||||
):
|
||||
if row >= height:
|
||||
break
|
||||
|
||||
@@ -176,10 +176,23 @@ class TerminalUI:
|
||||
navigator.history.entries(), self._history_selected_index
|
||||
)
|
||||
start_row = 1
|
||||
visible_count = max(0, height - start_row - 1)
|
||||
self._history_scroll_offset = clamp_scroll_offset(
|
||||
self._history_selected_index,
|
||||
self._history_scroll_offset,
|
||||
visible_count,
|
||||
len(history_lines),
|
||||
)
|
||||
|
||||
stdscr.addnstr(start_row, 0, "History:", width - 1, curses.A_BOLD)
|
||||
|
||||
for index, line in enumerate(history_lines, start=1):
|
||||
for index, line in enumerate(
|
||||
history_lines[
|
||||
self._history_scroll_offset : self._history_scroll_offset
|
||||
+ visible_count
|
||||
],
|
||||
start=1,
|
||||
):
|
||||
row = start_row + index
|
||||
if row >= height:
|
||||
break
|
||||
@@ -191,6 +204,7 @@ class TerminalUI:
|
||||
self._history_mode = not self._history_mode
|
||||
if self._history_mode:
|
||||
self._history_selected_index = self._get_current_history_index(navigator)
|
||||
self._history_scroll_offset = 0
|
||||
|
||||
def _handle_history_key(self, key: int, navigator: Navigator) -> None:
|
||||
history_entries = navigator.history.entries()
|
||||
@@ -214,6 +228,64 @@ class TerminalUI:
|
||||
if key in (curses.KEY_ENTER, 10, 13) and history_entries:
|
||||
navigator.select_history_entry(self._history_selected_index)
|
||||
self._history_mode = False
|
||||
self._tree_scroll_offset = 0
|
||||
|
||||
def _handle_tree_key(self, key: int, navigator: Navigator) -> Path | None:
|
||||
if key == curses.KEY_UP:
|
||||
navigator.set_selected_index(
|
||||
move_selection(
|
||||
navigator.selected_index,
|
||||
step=-1,
|
||||
entry_count=len(navigator.visible_entries),
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
if key == curses.KEY_DOWN:
|
||||
navigator.set_selected_index(
|
||||
move_selection(
|
||||
navigator.selected_index,
|
||||
step=1,
|
||||
entry_count=len(navigator.visible_entries),
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
if key == curses.KEY_RIGHT:
|
||||
was_expanded = navigator.selected_entry.is_expanded
|
||||
|
||||
if not navigator.expand_selected_directory():
|
||||
navigator.enter_selected_directory()
|
||||
self._tree_scroll_offset = 0
|
||||
return None
|
||||
|
||||
if was_expanded:
|
||||
navigator.enter_selected_directory()
|
||||
self._tree_scroll_offset = 0
|
||||
|
||||
return None
|
||||
|
||||
if key == curses.KEY_LEFT:
|
||||
if not navigator.collapse_selected_directory():
|
||||
navigator.go_to_parent_directory()
|
||||
self._tree_scroll_offset = 0
|
||||
|
||||
return None
|
||||
|
||||
if key == ord("b"):
|
||||
navigator.go_back()
|
||||
self._tree_scroll_offset = 0
|
||||
return None
|
||||
|
||||
if key == ord("f"):
|
||||
navigator.go_forward()
|
||||
self._tree_scroll_offset = 0
|
||||
return None
|
||||
|
||||
if key in (curses.KEY_ENTER, 10, 13):
|
||||
return navigator.selected_entry.path
|
||||
|
||||
return None
|
||||
|
||||
def _get_current_history_index(self, navigator: Navigator) -> int:
|
||||
history_entries = navigator.history.entries()
|
||||
|
||||
Reference in New Issue
Block a user