diff --git a/src/IO/Input.py b/src/IO/Input.py index 4bedd7f..22fca9f 100644 --- a/src/IO/Input.py +++ b/src/IO/Input.py @@ -1,4 +1,5 @@ from __future__ import annotations +import re from typing import TYPE_CHECKING from ..items.Folder import Folder from ..core.Prompt import Prompt @@ -26,6 +27,9 @@ class Input: "new_folder": self._new_folder, "delete_folder": self._delete_folder, "new_file": self._new_file, + "search": self._search, + "search_next": self._search_next, + "search_previous": self._search_previous, } def handle(self, key: str) -> bool: @@ -156,6 +160,39 @@ class Input: self.app.open_prompt(Prompt("New file name: ", "text", on_submit)) return True + def _search(self) -> bool: + navigator = self.app.navigator + + def on_submit(pattern: str) -> None: + if not pattern: + return + try: + regex = re.compile(pattern) + except re.error as e: + self.app.open_prompt(Prompt.error(f"Invalid regex: {e}")) + return + if not navigator.search(regex): + self.app.open_prompt(Prompt.error(f"No match: {pattern}")) + + self.app.open_prompt(Prompt("Search: ", "text", on_submit)) + return True + + def _search_next(self) -> bool: + navigator = self.app.navigator + if navigator.last_pattern is None: + return True + if not navigator.search_next(): + self.app.open_prompt(Prompt.error(f"No match: {navigator.last_pattern}")) + return True + + def _search_previous(self) -> bool: + navigator = self.app.navigator + if navigator.last_pattern is None: + return True + if not navigator.search_previous(): + self.app.open_prompt(Prompt.error(f"No match: {navigator.last_pattern}")) + return True + @property def app(self) -> App: return self._app diff --git a/src/core/Config.py b/src/core/Config.py index 663dbd5..66efec4 100644 --- a/src/core/Config.py +++ b/src/core/Config.py @@ -18,6 +18,9 @@ DEFAULT_KEYBINDS: dict[str, list[str]] = { "new_folder": ["d"], "delete_folder": ["D"], "new_file": ["%"], + "search": ["/"], + "search_next": ["n"], + "search_previous": ["N"], } DEFAULT_CURSOR_RESET = "top" diff --git a/src/core/Cursor.py b/src/core/Cursor.py index 471110a..1366ae2 100644 --- a/src/core/Cursor.py +++ b/src/core/Cursor.py @@ -9,6 +9,9 @@ class Cursor: def clamp(self, count: int) -> None: self._pos = min(self._pos, max(count - 1, 0)) + def set(self, pos: int) -> None: + self._pos = pos + def up(self) -> None: self._pos -= 1 if self.pos >= 1 else 0 diff --git a/src/core/Navigator.py b/src/core/Navigator.py index 35c5b3e..f803421 100644 --- a/src/core/Navigator.py +++ b/src/core/Navigator.py @@ -1,4 +1,5 @@ import os +import re from pathlib import Path from ..items.AItems import AItem from ..items.Folder import Folder @@ -19,6 +20,7 @@ class Navigator: self._current: Folder | None = None self._items: list[AItem] = [] self._error: str | None = None + self._last_regex: re.Pattern | None = None self._load() def _load(self) -> None: @@ -49,6 +51,35 @@ class Navigator: self._sort_grouping = Sort.next_grouping(self._sort_grouping) self._resort() + def search(self, regex: re.Pattern) -> bool: + self._last_regex = regex + return self._find(regex, 1) + + def search_next(self) -> bool: + if self._last_regex is None: + return False + return self._find(self._last_regex, 1) + + def search_previous(self) -> bool: + if self._last_regex is None: + return False + return self._find(self._last_regex, -1) + + def _find(self, regex: re.Pattern, direction: int) -> bool: + count = len(self._items) + if count == 0: + return False + for step in range(1, count + 1): + index = (self._cursor.pos + direction * step) % count + if regex.search(self._items[index].name): + self._cursor.set(index) + return True + return False + + @property + def last_pattern(self) -> str | None: + return self._last_regex.pattern if self._last_regex else None + def _reset_cursor(self) -> None: if self._config.cursor_reset == "keep": self._cursor.clamp(len(self._items))