Compare commits
7 Commits
v0.1.0
...
7c87d6f1f0
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c87d6f1f0 | |||
| 60723c33bc | |||
| 6195f7b8a3 | |||
| 801544951f | |||
| 65508d263f | |||
| 2bdfbe25d5 | |||
| 12d2577a31 |
43
docs/ai-prompts.md
Normal file
43
docs/ai-prompts.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# AI Prompts Log
|
||||
|
||||
This file stores relevant prompts used during AI-assisted development.
|
||||
|
||||
Only store prompts that:
|
||||
- change architecture
|
||||
- introduce new behavior
|
||||
- modify important logic
|
||||
- affect project structure
|
||||
- debug complex issues
|
||||
|
||||
Do not store internal chain-of-thought reasoning.
|
||||
Store only the user prompt and a short result summary.
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
- prompt summary: Implement a dedicated history mode in `src/app/ui.py` so pressing `h` hides the tree, shows only history entries, supports Up/Down navigation, and uses Enter to select a history entry and return to the normal tree view.
|
||||
- result summary: Added a dedicated history mode with its own selection state, removed the temporary overlay behavior, preserved existing `b`/`f` and normal tree navigation behavior, and added focused UI tests.
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
- prompt summary: Introduce a mandatory AI logging system by updating `instructions-agent.md` and adding repository logs for AI work entries and prompt summaries.
|
||||
- result summary: Added enforceable AI logging rules to `instructions-agent.md`, created `docs/ai-worklog.md` and `docs/ai-prompts.md`, and established a traceable workflow for recording significant AI-assisted tasks.
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
- prompt summary: Introduce a standard AI log entry format in `instructions-agent.md` and record the change in the repository AI logs.
|
||||
- result summary: Added a standardized `AI BUILD ENTRY` and `AI PLAN ENTRY` format, improved consistency expectations for AI logs, and updated the log files to reflect the new format.
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
- prompt summary: Improve keyboard navigation in `src/app/ui.py` so Right expands or enters, Left collapses or goes to the parent, Enter confirms the selected destination, and ESC cancels back to the starting directory.
|
||||
- result summary: Updated the tree-mode key handling in the terminal UI, preserved history mode behavior, and added focused UI tests for the new navigation rules.
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
- prompt summary: Improve vertical scrolling in `src/app/ui.py` so the selected entry always remains visible in tree mode and history mode when the list is taller than the screen.
|
||||
- result summary: Added UI scroll offset handling for tree and history views, kept existing navigation behavior unchanged, and added focused tests for the scroll helper logic.
|
||||
|
||||
## 2026-03-16 00:27
|
||||
|
||||
- prompt summary: Improve terminal UI navigation by refining tree and history keyboard behavior, adding scrolling continuity so selection stays visible, and preserving confirmation, cancellation, and history mode behavior.
|
||||
- result summary: Updated `src/app/ui.py` and `tests/test_ui.py` to support dedicated history selection, improved Right/Left/Enter/ESC behavior, and vertical scrolling for both tree and history views while keeping the AI logs current.
|
||||
164
docs/ai-worklog.md
Normal file
164
docs/ai-worklog.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# AI Worklog
|
||||
|
||||
This file records important AI-assisted development actions.
|
||||
|
||||
Each entry should include:
|
||||
- date
|
||||
- task type (plan/build)
|
||||
- objective
|
||||
- files inspected or modified
|
||||
- key decisions
|
||||
- validation commands executed
|
||||
- result
|
||||
- open issues (if any)
|
||||
|
||||
Entries must be concise and chronological.
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15
|
||||
Task: build
|
||||
Objective: Replace the temporary history debug overlay with a dedicated history mode in the terminal UI.
|
||||
|
||||
Files Modified:
|
||||
- `src/app/ui.py`
|
||||
- `tests/test_ui.py`
|
||||
|
||||
Key Decisions:
|
||||
- Keep `b` and `f` unchanged.
|
||||
- Add separate history-mode selection state inside the UI.
|
||||
- Hide the tree while history mode is active.
|
||||
- Use Enter to select a history entry and return to the tree view.
|
||||
|
||||
Validation:
|
||||
- make fix
|
||||
- make quality
|
||||
|
||||
Result:
|
||||
- History mode now shows only the history list, supports Up/Down selection, allows Enter to jump to a prior directory, and exits cleanly back to normal navigation.
|
||||
|
||||
Open Issues:
|
||||
- None.
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-16 00:27
|
||||
Task: build
|
||||
Objective: Consolidate recent terminal UI navigation improvements and document the updated interaction behavior.
|
||||
|
||||
Files Modified:
|
||||
- `src/app/ui.py`
|
||||
- `tests/test_ui.py`
|
||||
- `docs/ai-worklog.md`
|
||||
- `docs/ai-prompts.md`
|
||||
|
||||
Key Decisions:
|
||||
- Keep dedicated history mode while refining tree-mode keyboard behavior for Right, Left, Enter, and ESC.
|
||||
- Add scroll continuity for both tree and history views so the selected row remains visible on long lists.
|
||||
- Cover the UI-focused behavior with helper-level tests instead of changing navigator or filesystem logic.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- The terminal UI now combines dedicated history mode, clearer confirmation and cancellation behavior, improved tree navigation semantics, and vertical scrolling that keeps the current selection visible in both tree and history views.
|
||||
|
||||
Open Issues:
|
||||
- None.
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15
|
||||
Task: build
|
||||
Objective: Introduce mandatory AI logging rules and document the repository logging workflow.
|
||||
|
||||
Files Modified:
|
||||
- `instructions-agent.md`
|
||||
- `docs/ai-worklog.md`
|
||||
- `docs/ai-prompts.md`
|
||||
|
||||
Key Decisions:
|
||||
- Require AI agents to log significant plan and build tasks.
|
||||
- Preserve separate logs for work performed and prompts received.
|
||||
- Keep entries concise and user-facing.
|
||||
|
||||
Validation:
|
||||
- not run
|
||||
|
||||
Result:
|
||||
- The repository now defines mandatory AI logging in `instructions-agent.md` and includes dedicated log files for AI work history and prompt summaries.
|
||||
|
||||
Open Issues:
|
||||
- None.
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15
|
||||
Task: build
|
||||
Objective: Record the introduction of the standard AI log entry format and improve AI logging consistency.
|
||||
|
||||
Files Modified:
|
||||
- `instructions-agent.md`
|
||||
- `docs/ai-worklog.md`
|
||||
- `docs/ai-prompts.md`
|
||||
|
||||
Key Decisions:
|
||||
- Add a dedicated "Standard AI Log Entry Format" section to define `AI BUILD ENTRY` and `AI PLAN ENTRY` blocks.
|
||||
- Use the new standardized format for this worklog update to establish the pattern in the repository logs.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- The repository now documents a standard AI log entry format and uses it to improve consistency of AI-assisted development logs.
|
||||
|
||||
Open Issues:
|
||||
- None.
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15
|
||||
Task: build
|
||||
Objective: Improve vertical scrolling in the terminal UI so the selected row remains visible in tree mode and history mode.
|
||||
|
||||
Files Modified:
|
||||
- `src/app/ui.py`
|
||||
- `tests/test_ui.py`
|
||||
- `docs/ai-worklog.md`
|
||||
- `docs/ai-prompts.md`
|
||||
|
||||
Key Decisions:
|
||||
- Add separate scroll offsets for tree mode and history mode inside the UI state.
|
||||
- Introduce a small helper to clamp scroll offsets so the selected row always stays within the visible window.
|
||||
- Keep all existing navigation semantics unchanged while limiting the change to UI rendering behavior.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- The terminal UI now scrolls vertically in both tree mode and history mode to keep the current selection visible when the list is taller than the screen.
|
||||
|
||||
Open Issues:
|
||||
- None.
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15
|
||||
Task: build
|
||||
Objective: Improve keyboard navigation behavior in the terminal UI for tree mode.
|
||||
|
||||
Files Modified:
|
||||
- `src/app/ui.py`
|
||||
- `tests/test_ui.py`
|
||||
|
||||
Key Decisions:
|
||||
- Make Right expand collapsed directories and enter already expanded directories.
|
||||
- Make Left collapse expanded directories and otherwise navigate to the parent directory.
|
||||
- Make Enter confirm the selected path as the final destination and make ESC return the original starting directory.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- Tree-mode keyboard navigation now supports expand-or-enter on Right, collapse-or-parent on Left, explicit selection confirmation on Enter, and cancel-to-start behavior on ESC.
|
||||
|
||||
Open Issues:
|
||||
- None.
|
||||
@@ -59,3 +59,132 @@ Agent rules:
|
||||
3. If the specification conflicts with a previous assumption, the specification takes precedence.
|
||||
4. Keep implementation aligned with the MVP scope unless explicitly asked to extend it.
|
||||
5. After code changes, run the appropriate validation workflow defined by this repository.
|
||||
|
||||
## AI Worklog Policy
|
||||
|
||||
This repository maintains an AI-assisted development log.
|
||||
|
||||
Files:
|
||||
- `docs/ai-worklog.md`
|
||||
- `docs/ai-prompts.md`
|
||||
|
||||
Rules:
|
||||
1. After every significant `plan` or `build` task, append a concise worklog entry to `docs/ai-worklog.md`.
|
||||
2. Each worklog entry must include:
|
||||
- date
|
||||
- task type (`plan` or `build`)
|
||||
- short objective
|
||||
- files inspected or modified
|
||||
- key decisions
|
||||
- validation commands run
|
||||
- result
|
||||
- unresolved issues if any
|
||||
3. When a prompt meaningfully changes architecture, behavior, workflow, debugging direction, or project structure, append the prompt (or a concise cleaned version of it) to `docs/ai-prompts.md`.
|
||||
4. Do not store private chain-of-thought or internal reasoning.
|
||||
5. Store only concise, user-facing summaries of what was done.
|
||||
6. Keep entries chronological and easy to scan.
|
||||
|
||||
## Mandatory AI Logging
|
||||
|
||||
Every significant AI interaction in this repository must be logged.
|
||||
|
||||
This applies to:
|
||||
- `plan` tasks
|
||||
- `build` tasks
|
||||
- debugging tasks
|
||||
- documentation tasks
|
||||
- architectural discussions that affect the project
|
||||
|
||||
Required files:
|
||||
- `docs/ai-worklog.md`
|
||||
- `docs/ai-prompts.md`
|
||||
|
||||
Mandatory rules:
|
||||
1. After every significant `plan` task, append a concise entry to `docs/ai-worklog.md`.
|
||||
2. After every significant `build` task, append a concise entry to `docs/ai-worklog.md`.
|
||||
3. If the task was driven by a meaningful prompt, also append an entry to `docs/ai-prompts.md`.
|
||||
4. Logging is part of the task itself and must not be skipped.
|
||||
5. The task is not complete until the logs are updated.
|
||||
|
||||
Each worklog entry must include:
|
||||
- date
|
||||
- task type (`plan` or `build`)
|
||||
- short objective
|
||||
- files inspected or modified
|
||||
- key decisions
|
||||
- validation commands run, if any
|
||||
- result
|
||||
- unresolved issues, if any
|
||||
|
||||
Validation logging rule:
|
||||
- If validation commands such as `make fix`, `make quality`, `pytest`, or other checks are executed during the task, the worklog entry must record them explicitly.
|
||||
- The value `validation: not run` may only be used if no validation commands were executed.
|
||||
- The worklog entry must reflect the actual commands run during the task.
|
||||
|
||||
Each prompt log entry must include:
|
||||
- date
|
||||
- task type (`plan` or `build`)
|
||||
- short prompt summary
|
||||
- scope
|
||||
- result summary
|
||||
|
||||
Rules:
|
||||
- Do not store chain-of-thought or private reasoning.
|
||||
- Store only concise user-facing summaries.
|
||||
- Keep entries chronological.
|
||||
- Even small but meaningful tasks must be logged.
|
||||
|
||||
## Standard AI Log Entry Format
|
||||
|
||||
To ensure consistency and readability of long AI-assisted development histories, all log entries must follow a standardized block format.
|
||||
|
||||
Agents must write log entries using the following structure.
|
||||
|
||||
Example for build tasks:
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: YYYY-MM-DD HH:MM
|
||||
Task: build
|
||||
Objective: short description of the task
|
||||
|
||||
Files Modified:
|
||||
- file/path/example.py
|
||||
- another/file.md
|
||||
|
||||
Key Decisions:
|
||||
- short bullet explaining important choices
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- short description of what changed or was achieved
|
||||
|
||||
Open Issues:
|
||||
- optional list of unresolved problems
|
||||
|
||||
Example for plan tasks:
|
||||
|
||||
### AI PLAN ENTRY
|
||||
Date: YYYY-MM-DD HH:MM
|
||||
Task: plan
|
||||
Objective: short description of planning objective
|
||||
|
||||
Files Inspected:
|
||||
- src/app/example.py
|
||||
- specs/example_spec.md
|
||||
|
||||
Proposed Changes:
|
||||
- summary of planned changes
|
||||
|
||||
Notes:
|
||||
- optional relevant observations
|
||||
|
||||
Rules:
|
||||
- Always use these headers exactly (`AI BUILD ENTRY` or `AI PLAN ENTRY`).
|
||||
- Keep entries concise and structured.
|
||||
- Do not include chain-of-thought reasoning.
|
||||
- Use bullet points when possible.
|
||||
- This format must be used when updating `docs/ai-worklog.md`.
|
||||
- The Date field must include both date and time using the format YYYY-MM-DD HH:MM.
|
||||
|
||||
227
src/app/ui.py
227
src/app/ui.py
@@ -22,6 +22,14 @@ class RenderLine:
|
||||
is_selected: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HistoryLine:
|
||||
"""A rendered history row ready for terminal output."""
|
||||
|
||||
text: str
|
||||
is_selected: bool
|
||||
|
||||
|
||||
def move_selection(current_index: int, step: int, entry_count: int) -> int:
|
||||
"""Move the selected index within the available entry range."""
|
||||
|
||||
@@ -57,9 +65,46 @@ def build_render_lines(navigator: Navigator) -> list[RenderLine]:
|
||||
]
|
||||
|
||||
|
||||
def build_history_lines(
|
||||
entries: tuple[Path, ...], selected_index: int
|
||||
) -> list[HistoryLine]:
|
||||
"""Build all visible lines for history mode."""
|
||||
|
||||
return [
|
||||
HistoryLine(text=str(path), is_selected=index == selected_index)
|
||||
for index, path in enumerate(entries)
|
||||
]
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
@@ -70,53 +115,52 @@ 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 == curses.KEY_UP:
|
||||
navigator.set_selected_index(
|
||||
move_selection(
|
||||
navigator.selected_index,
|
||||
step=-1,
|
||||
entry_count=len(navigator.visible_entries),
|
||||
)
|
||||
)
|
||||
if key == ord("h"):
|
||||
self._toggle_history_mode(navigator)
|
||||
continue
|
||||
|
||||
if key == curses.KEY_DOWN:
|
||||
navigator.set_selected_index(
|
||||
move_selection(
|
||||
navigator.selected_index,
|
||||
step=1,
|
||||
entry_count=len(navigator.visible_entries),
|
||||
)
|
||||
)
|
||||
if self._history_mode:
|
||||
self._handle_history_key(key, navigator)
|
||||
continue
|
||||
|
||||
if key == curses.KEY_RIGHT:
|
||||
navigator.expand_selected_directory()
|
||||
continue
|
||||
|
||||
if key == curses.KEY_LEFT:
|
||||
navigator.collapse_selected_directory()
|
||||
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()
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
if self._history_mode:
|
||||
self._render_history_mode(stdscr, navigator, width=width, height=height)
|
||||
stdscr.refresh()
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
@@ -125,6 +169,133 @@ class TerminalUI:
|
||||
|
||||
stdscr.refresh()
|
||||
|
||||
def _render_history_mode(
|
||||
self, stdscr: curses.window, navigator: Navigator, width: int, height: int
|
||||
) -> None:
|
||||
history_lines = build_history_lines(
|
||||
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[
|
||||
self._history_scroll_offset : self._history_scroll_offset
|
||||
+ visible_count
|
||||
],
|
||||
start=1,
|
||||
):
|
||||
row = start_row + index
|
||||
if row >= height:
|
||||
break
|
||||
|
||||
attributes = curses.A_REVERSE if line.is_selected else curses.A_NORMAL
|
||||
stdscr.addnstr(row, 0, line.text, width - 1, attributes)
|
||||
|
||||
def _toggle_history_mode(self, navigator: Navigator) -> None:
|
||||
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()
|
||||
|
||||
if key == curses.KEY_UP:
|
||||
self._history_selected_index = move_selection(
|
||||
self._history_selected_index,
|
||||
step=-1,
|
||||
entry_count=len(history_entries),
|
||||
)
|
||||
return
|
||||
|
||||
if key == curses.KEY_DOWN:
|
||||
self._history_selected_index = move_selection(
|
||||
self._history_selected_index,
|
||||
step=1,
|
||||
entry_count=len(history_entries),
|
||||
)
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
for index in range(len(history_entries) - 1, -1, -1):
|
||||
if history_entries[index] == navigator.current_path:
|
||||
return index
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@contextmanager
|
||||
def open_terminal_streams() -> Iterator[tuple[TextIO, TextIO]]:
|
||||
|
||||
156
tests/test_ui.py
156
tests/test_ui.py
@@ -1,11 +1,16 @@
|
||||
import curses
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.navigator import Navigator, VisibleEntry
|
||||
from app.ui import (
|
||||
HistoryLine,
|
||||
RenderLine,
|
||||
TerminalUI,
|
||||
build_history_lines,
|
||||
build_render_lines,
|
||||
clamp_scroll_offset,
|
||||
format_entry,
|
||||
move_selection,
|
||||
open_terminal_streams,
|
||||
@@ -65,6 +70,157 @@ def test_build_render_lines_marks_selected_entry(tmp_path: Path) -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_build_history_lines_marks_selected_history_entry(tmp_path: Path) -> None:
|
||||
first = tmp_path / "first"
|
||||
second = tmp_path / "second"
|
||||
|
||||
assert build_history_lines((first, second), selected_index=1) == [
|
||||
HistoryLine(text=str(first), is_selected=False),
|
||||
HistoryLine(text=str(second), is_selected=True),
|
||||
]
|
||||
|
||||
|
||||
def test_clamp_scroll_offset_keeps_selection_visible_when_moving_down() -> None:
|
||||
assert clamp_scroll_offset(5, scroll_offset=0, visible_count=3, total_count=10) == 3
|
||||
|
||||
|
||||
def test_clamp_scroll_offset_keeps_selection_visible_when_moving_up() -> None:
|
||||
assert clamp_scroll_offset(1, scroll_offset=4, visible_count=3, total_count=10) == 1
|
||||
|
||||
|
||||
def test_clamp_scroll_offset_returns_zero_when_all_items_fit() -> None:
|
||||
assert clamp_scroll_offset(2, scroll_offset=1, visible_count=5, total_count=3) == 0
|
||||
|
||||
|
||||
def test_toggle_history_mode_selects_current_history_entry(tmp_path: Path) -> None:
|
||||
root = tmp_path / "root"
|
||||
child = root / "child"
|
||||
root.mkdir()
|
||||
child.mkdir()
|
||||
|
||||
navigator = Navigator(root)
|
||||
navigator.set_selected_index(1)
|
||||
navigator.enter_selected_directory()
|
||||
ui = TerminalUI()
|
||||
|
||||
ui._toggle_history_mode(navigator)
|
||||
|
||||
assert ui._history_mode is True
|
||||
assert ui._history_selected_index == 1
|
||||
|
||||
|
||||
def test_history_mode_enter_selects_entry_and_exits(tmp_path: Path) -> None:
|
||||
root = tmp_path / "root"
|
||||
first = root / "first"
|
||||
second = root / "second"
|
||||
root.mkdir()
|
||||
first.mkdir()
|
||||
second.mkdir()
|
||||
|
||||
navigator = Navigator(root)
|
||||
navigator.set_selected_index(1)
|
||||
navigator.enter_selected_directory()
|
||||
navigator.go_to_parent_directory()
|
||||
navigator.set_selected_index(2)
|
||||
navigator.enter_selected_directory()
|
||||
|
||||
ui = TerminalUI()
|
||||
ui._toggle_history_mode(navigator)
|
||||
ui._handle_history_key(curses.KEY_UP, navigator)
|
||||
ui._handle_history_key(10, navigator)
|
||||
|
||||
assert ui._history_mode is False
|
||||
assert navigator.current_path == root.resolve()
|
||||
assert navigator.selected_index == 0
|
||||
|
||||
|
||||
def test_history_mode_toggle_off_keeps_current_directory(tmp_path: Path) -> None:
|
||||
root = tmp_path / "root"
|
||||
root.mkdir()
|
||||
navigator = Navigator(root)
|
||||
ui = TerminalUI()
|
||||
|
||||
ui._toggle_history_mode(navigator)
|
||||
ui._toggle_history_mode(navigator)
|
||||
|
||||
assert ui._history_mode is False
|
||||
assert navigator.current_path == root.resolve()
|
||||
|
||||
|
||||
def test_right_arrow_expands_collapsed_directory(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "workspace"
|
||||
scripts = workspace / "scripts"
|
||||
build = scripts / "build"
|
||||
workspace.mkdir()
|
||||
scripts.mkdir()
|
||||
build.mkdir()
|
||||
|
||||
navigator = Navigator(workspace)
|
||||
navigator.set_selected_index(1)
|
||||
ui = TerminalUI()
|
||||
|
||||
assert ui._handle_tree_key(curses.KEY_RIGHT, navigator) is None
|
||||
assert navigator.current_path == workspace.resolve()
|
||||
assert [entry.name for entry in navigator.visible_entries] == [
|
||||
"..",
|
||||
"scripts",
|
||||
"build",
|
||||
]
|
||||
|
||||
|
||||
def test_right_arrow_enters_already_expanded_directory(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "workspace"
|
||||
scripts = workspace / "scripts"
|
||||
build = scripts / "build"
|
||||
workspace.mkdir()
|
||||
scripts.mkdir()
|
||||
build.mkdir()
|
||||
|
||||
navigator = Navigator(workspace)
|
||||
navigator.set_selected_index(1)
|
||||
navigator.expand_selected_directory()
|
||||
ui = TerminalUI()
|
||||
|
||||
assert ui._handle_tree_key(curses.KEY_RIGHT, navigator) is None
|
||||
assert navigator.current_path == scripts.resolve()
|
||||
assert navigator.selected_index == 0
|
||||
|
||||
|
||||
def test_left_arrow_goes_to_parent_when_selected_entry_is_collapsed(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
parent = tmp_path / "parent"
|
||||
current = parent / "current"
|
||||
child = current / "child"
|
||||
parent.mkdir()
|
||||
current.mkdir()
|
||||
child.mkdir()
|
||||
|
||||
navigator = Navigator(current)
|
||||
navigator.set_selected_index(1)
|
||||
ui = TerminalUI()
|
||||
|
||||
assert ui._handle_tree_key(curses.KEY_LEFT, navigator) is None
|
||||
assert navigator.current_path == parent.resolve()
|
||||
assert navigator.selected_index == 0
|
||||
|
||||
|
||||
def test_enter_returns_selected_path_without_navigating(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "workspace"
|
||||
child = workspace / "child"
|
||||
workspace.mkdir()
|
||||
child.mkdir()
|
||||
|
||||
navigator = Navigator(workspace)
|
||||
navigator.set_selected_index(1)
|
||||
ui = TerminalUI()
|
||||
|
||||
result = ui._handle_tree_key(10, navigator)
|
||||
|
||||
assert result == child.resolve()
|
||||
assert navigator.current_path == workspace.resolve()
|
||||
|
||||
|
||||
def test_open_terminal_streams_raises_without_terminal(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user