release: 0.2.6 add advanced navigation and contextual open actions

This commit is contained in:
2026-04-04 01:53:00 +02:00
parent 8259b2e132
commit 6f78f6d9ca
11 changed files with 1090 additions and 50 deletions

View File

@@ -2,7 +2,13 @@ from pathlib import Path
import pytest
from app.filesystem import DirectoryEntry, has_subdirectories, list_directories
from app.filesystem import (
DirectoryEntry,
FileSystemEntry,
has_subdirectories,
list_directories,
list_entries,
)
def test_has_subdirectories_detects_nested_directory(tmp_path: Path) -> None:
@@ -46,6 +52,28 @@ def test_list_directories_raises_for_missing_directory(tmp_path: Path) -> None:
list_directories(missing_path)
def test_list_entries_returns_files_when_include_files_enabled(tmp_path: Path) -> None:
folder = tmp_path / "folder"
file_path = tmp_path / "notes.txt"
folder.mkdir()
file_path.write_text("content", encoding="utf-8")
assert list_entries(tmp_path, include_files=True) == [
FileSystemEntry(
name="folder",
path=folder,
is_directory=True,
has_children=False,
),
FileSystemEntry(
name="notes.txt",
path=file_path,
is_directory=False,
has_children=False,
),
]
def test_list_directories_skips_permission_errors_for_children(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()

View File

@@ -50,6 +50,24 @@ def test_toggle_hidden_directories(tmp_path: Path) -> None:
assert [entry.name for entry in navigator.visible_entries] == ["..", "visible"]
def test_toggle_files_includes_files_in_visible_entries(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
(root / "folder").mkdir()
(root / "notes.txt").write_text("hello", encoding="utf-8")
navigator = Navigator(root)
assert [entry.name for entry in navigator.visible_entries] == ["..", "folder"]
navigator.toggle_files()
assert navigator.show_files is True
assert [entry.name for entry in navigator.visible_entries] == [
"..",
"folder",
"notes.txt",
]
def test_expand_selected_directory_reveals_nested_entries(tmp_path: Path) -> None:
current = tmp_path / "workspace"
current.mkdir()

147
tests/test_opener.py Normal file
View File

@@ -0,0 +1,147 @@
from __future__ import annotations
from pathlib import Path
from subprocess import CompletedProcess
import pytest
from app.opener import (
OpenWithOption,
available_openers,
is_blocking_option,
launch_background_option,
launch_blocking_option,
)
def test_available_openers_headless_file_excludes_gui(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
file_path = tmp_path / "notes.txt"
file_path.write_text("hello", encoding="utf-8")
monkeypatch.setenv("SSH_CONNECTION", "1")
available = {
"antigravity": "/usr/local/bin/antigravity",
"nvim": "/usr/local/bin/nvim",
"nano": "/usr/bin/nano",
"less": "/usr/bin/less",
"bat": "/usr/local/bin/bat",
"code": "/usr/local/bin/code",
"open": "/usr/bin/open",
}
monkeypatch.setattr("app.opener.shutil.which", lambda name: available.get(name))
labels = [option.label for option in available_openers(file_path)]
assert "VS Code" not in labels
assert "Open (system)" not in labels
assert labels == ["Antigravity", "Neovim", "Nano", "Less", "Bat"]
def test_available_openers_gui_directory_filters_file_only_tools(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
folder = tmp_path / "project"
folder.mkdir()
monkeypatch.delenv("SSH_CONNECTION", raising=False)
monkeypatch.setattr("app.opener.sys.platform", "darwin")
available = {
"antigravity": "/usr/local/bin/antigravity",
"nvim": "/usr/local/bin/nvim",
"nano": "/usr/bin/nano",
"less": "/usr/bin/less",
"bat": "/usr/local/bin/bat",
"code": "/usr/local/bin/code",
"open": "/usr/bin/open",
}
monkeypatch.setattr("app.opener.shutil.which", lambda name: available.get(name))
labels = [option.label for option in available_openers(folder)]
assert labels == ["VS Code", "Antigravity", "Neovim"]
def test_launch_background_option_returns_success(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
option = OpenWithOption(
label="VS Code",
command_template="code {path}",
executable="code",
launch_mode="background",
requires_gui=True,
)
monkeypatch.setattr("app.opener.subprocess.Popen", lambda *args, **kwargs: None)
success, message = launch_background_option(tmp_path, option)
assert success is True
assert "VS Code" in message
def test_launch_blocking_option_reports_non_zero_exit(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
option = OpenWithOption(
label="Neovim",
command_template="nvim {path}",
executable="nvim",
launch_mode="blocking",
)
monkeypatch.setattr(
"app.opener.subprocess.run",
lambda *args, **kwargs: CompletedProcess(args=[], returncode=2),
)
success, message = launch_blocking_option(tmp_path, option)
assert success is False
assert "code 2" in message
def test_is_blocking_option_uses_launch_mode() -> None:
blocking = OpenWithOption(
label="Neovim",
command_template="nvim {path}",
executable="nvim",
launch_mode="blocking",
)
background = OpenWithOption(
label="VS Code",
command_template="code {path}",
executable="code",
launch_mode="background",
)
assert is_blocking_option(blocking) is True
assert is_blocking_option(background) is False
def test_launch_blocking_option_handles_paths_with_spaces(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
target_path = tmp_path / "COMPETENCIAS A.LEDO"
option = OpenWithOption(
label="Neovim",
command_template="nvim {path}",
executable="nvim",
launch_mode="blocking",
)
captured: dict[str, list[str]] = {}
def fake_run(command: list[str], check: bool) -> CompletedProcess[object]:
captured["command"] = command
return CompletedProcess(args=[], returncode=0)
monkeypatch.setattr("app.opener.subprocess.run", fake_run)
success, _message = launch_blocking_option(target_path, option)
assert success is True
assert captured["command"] == ["nvim", str(target_path)]

View File

@@ -52,6 +52,19 @@ def test_format_entry_supports_parent_and_tree_indicators(tmp_path: Path) -> Non
assert format_entry(expanded_child) == " build"
def test_format_entry_marks_files_with_file_indicator(tmp_path: Path) -> None:
file_entry = VisibleEntry(
name="notes.txt",
path=tmp_path / "notes.txt",
depth=0,
has_children=False,
is_expanded=False,
is_file=True,
)
assert format_entry(file_entry) == "• notes.txt"
def test_build_render_lines_marks_selected_entry(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
@@ -221,6 +234,112 @@ def test_enter_returns_selected_path_without_navigating(tmp_path: Path) -> None:
assert navigator.current_path == workspace.resolve()
def test_toggle_files_key_updates_navigator_state(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
(root / "folder").mkdir()
(root / "notes.txt").write_text("content", encoding="utf-8")
navigator = Navigator(root)
ui = TerminalUI()
assert navigator.show_files is False
assert ui._handle_tree_key(ord("a"), navigator) is None
assert navigator.show_files is True
def test_filter_matching_is_case_insensitive(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
(root / "Alpha").mkdir()
(root / "beta").mkdir()
navigator = Navigator(root)
ui = TerminalUI()
ui._filter_mode = True
ui._filter_query = "alp"
matches = ui._matching_indices(navigator.visible_entries)
assert matches == [1]
def test_filter_mode_uses_g_and_g_upper_as_query_characters(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
(root / "alpha").mkdir()
(root / "gamma").mkdir()
navigator = Navigator(root)
ui = TerminalUI()
ui._filter_mode = True
assert ui._handle_filter_key(ord("g"), navigator) is None
assert ui._handle_filter_key(ord("G"), navigator) is None
assert ui._filter_query == "gG"
def test_filter_enter_on_directory_navigates_and_exits_filter(tmp_path: Path) -> None:
root = tmp_path / "root"
child = root / "child"
root.mkdir()
child.mkdir()
navigator = Navigator(root)
ui = TerminalUI()
ui._filter_mode = True
ui._filter_query = "child"
result = ui._handle_filter_key(10, navigator)
assert result is None
assert ui._filter_mode is False
assert navigator.current_path == child.resolve()
def test_filter_enter_on_file_opens_menu_and_exits_filter(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
root = tmp_path / "root"
root.mkdir()
file_path = root / "notes.txt"
file_path.write_text("hello", encoding="utf-8")
navigator = Navigator(root)
navigator.toggle_files()
ui = TerminalUI()
ui._filter_mode = True
ui._filter_query = "notes"
opened: dict[str, bool] = {"called": False}
def fake_open_with_selected(
_navigator: Navigator, _stdscr: curses.window | None
) -> None:
opened["called"] = True
monkeypatch.setattr(ui, "_open_with_selected", fake_open_with_selected)
result = ui._handle_filter_key(10, navigator)
assert result is None
assert ui._filter_mode is False
assert opened["called"] is True
assert navigator.selected_entry.path == file_path.resolve()
def test_end_key_jumps_to_last_entry(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
for name in ("alpha", "beta", "gamma"):
(root / name).mkdir()
navigator = Navigator(root)
ui = TerminalUI()
assert ui._handle_tree_key(curses.KEY_END, navigator) is None
assert navigator.selected_index == len(navigator.visible_entries) - 1
def test_open_terminal_streams_raises_without_terminal(
monkeypatch: pytest.MonkeyPatch,
) -> None: