feat: add folder/file creation and folder deletion with prompts

This commit is contained in:
lohhiiccc 2026-07-02 19:45:01 +02:00
parent f94771c67a
commit 4b721cb993
7 changed files with 150 additions and 2 deletions

View file

@ -1,9 +1,15 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from ..items.Folder import Folder
from ..core.Prompt import Prompt
if TYPE_CHECKING:
from .App import App
BACKSPACE_KEYS = ("KEY_BACKSPACE", "\x7f", "\x08")
ENTER_KEYS = ("\n", "KEY_ENTER")
ESCAPE_KEY = "\x1b"
class Input:
def __init__(self, app: App) -> None:
@ -16,13 +22,40 @@ class Input:
"back": self._back,
"parent": self._parent,
"sort": self._sort,
"new_folder": self._new_folder,
"delete_folder": self._delete_folder,
"new_file": self._new_file,
}
def handle(self, key: str) -> bool:
if self.app.prompt is not None:
return self._handle_prompt(key)
action = self.app.config.action_for(key)
handler = self._actions.get(action)
return handler() if handler else True
def _handle_prompt(self, key: str) -> bool:
prompt = self.app.prompt
if prompt.kind == "confirm":
if key in ("y", "Y"):
self.app.close_prompt()
prompt.on_submit(True)
elif key in ("n", "N", ESCAPE_KEY):
self.app.close_prompt()
return True
if key in ENTER_KEYS:
self.app.close_prompt()
prompt.on_submit(prompt.buffer)
elif key == ESCAPE_KEY:
self.app.close_prompt()
elif key in BACKSPACE_KEYS:
prompt.buffer = prompt.buffer[:-1]
elif len(key) == 1 and key.isprintable():
prompt.buffer += key
return True
def _quit(self) -> bool:
return False
@ -54,6 +87,57 @@ class Input:
self.app.navigator.cycle_sort()
return True
def _new_folder(self) -> bool:
navigator = self.app.navigator
def on_submit(name: str) -> None:
if not name:
return
try:
self.app.file_actions.create_folder(navigator.current.path,
name)
except OSError as e:
navigator.set_action_error(str(e))
self.app.open_prompt(Prompt("New folder name: ", "text", on_submit))
return True
def _delete_folder(self) -> bool:
navigator = self.app.navigator
items = navigator.items
if not items:
return True
item = items[navigator.cursor.pos]
if not isinstance(item, Folder):
return True
def on_confirm(confirmed: bool) -> None:
if not confirmed:
return
try:
self.app.file_actions.delete_folder(item.path)
except OSError as e:
navigator.set_action_error(str(e))
self.app.open_prompt(
Prompt(f"Delete '{item.name}'? (y/n) ", "confirm", on_confirm))
return True
def _new_file(self) -> bool:
navigator = self.app.navigator
def on_submit(name: str) -> None:
if not name:
return
try:
self.app.file_actions.create_file(navigator.current.path,
name)
except OSError as e:
navigator.set_action_error(str(e))
self.app.open_prompt(Prompt("New file name: ", "text", on_submit))
return True
@property
def app(self) -> App:
return self._app

View file

@ -26,6 +26,7 @@ class UI:
stdscr.erase()
height, width = stdscr.getmaxyx()
navigator = self.app.navigator
prompt = self.app.prompt
stdscr.addstr(0, 0, str(navigator.history[-1])[:width - 1])
if navigator.error:
@ -33,12 +34,23 @@ class UI:
stdscr.addstr(1, 0, navigator.error[:width - 1],
curses.color_pair(error_pair))
footer_rows = 1 if prompt else 0
items = navigator.items
available = max(0, height - self.HEADER_ROWS)
available = max(0, height - self.HEADER_ROWS - footer_rows)
for index in range(min(available, len(items))):
item = items[index]
self._render_item(stdscr, item, index, index + self.HEADER_ROWS, width)
if prompt:
self._render_prompt(stdscr, prompt, height - 1, width)
def _render_prompt(self, stdscr: curses.window, prompt, row: int,
width: int) -> None:
text = prompt.message
if prompt.kind == "text":
text += prompt.buffer
stdscr.addstr(row, 0, text[:width - 1])
SIZE_UNITS = ("", "K", "M", "G", "T", "P")
@staticmethod

View file

@ -2,7 +2,9 @@ from pathlib import Path
from ..IO.UI import UI
from ..IO.Input import Input
from .Config import Config
from .FileActions import FileActions
from .Navigator import Navigator
from .Prompt import Prompt
import curses
@ -16,6 +18,8 @@ class App():
def __init__(self, path: Path) -> None:
self._config = Config.load()
self._navigator = Navigator(path, self._config)
self._file_actions = FileActions()
self._prompt: Prompt | None = None
self._UI = UI(self)
self._input = Input(self)
@ -36,6 +40,20 @@ class App():
def run(self) -> None:
curses.wrapper(self._run)
def open_prompt(self, prompt: Prompt) -> None:
self._prompt = prompt
def close_prompt(self) -> None:
self._prompt = None
@property
def prompt(self) -> Prompt | None:
return self._prompt
@property
def file_actions(self) -> FileActions:
return self._file_actions
@property
def config(self) -> Config:
return self._config

View file

@ -14,6 +14,9 @@ DEFAULT_KEYBINDS: dict[str, list[str]] = {
"back": ["-", "KEY_LEFT", "KEY_BACKSPACE", "\x7f", "\x08"],
"parent": ["h"],
"sort": ["i"],
"new_folder": ["d"],
"delete_folder": ["D"],
"new_file": ["%"],
}
DEFAULT_CURSOR_RESET = "top"

14
src/core/FileActions.py Normal file
View file

@ -0,0 +1,14 @@
from __future__ import annotations
import shutil
from pathlib import Path
class FileActions:
def create_folder(self, parent: Path, name: str) -> None:
(parent / name).mkdir()
def create_file(self, parent: Path, name: str) -> None:
(parent / name).touch(exist_ok=False)
def delete_folder(self, path: Path) -> None:
shutil.rmtree(path)

View file

@ -17,6 +17,7 @@ class Navigator:
self._current: Folder | None = None
self._items: list[AItem] = []
self._error: str | None = None
self._action_error: str | None = None
self._load()
def _load(self) -> None:
@ -44,8 +45,12 @@ class Navigator:
else:
self._cursor.resete()
def set_action_error(self, message: str) -> None:
self._action_error = message
def _navigate_to(self, path: Path) -> None:
self._history.append(path)
self._action_error = None
self._load()
self._reset_cursor()
@ -67,6 +72,7 @@ class Navigator:
if len(self._history) <= 1:
return False
self._history.pop()
self._action_error = None
self._load()
self._reset_cursor()
return True
@ -89,4 +95,4 @@ class Navigator:
@property
def error(self) -> str | None:
return self._error
return self._action_error or self._error

11
src/core/Prompt.py Normal file
View file

@ -0,0 +1,11 @@
from __future__ import annotations
from typing import Callable
class Prompt:
def __init__(self, message: str, kind: str,
on_submit: Callable[[object], None]) -> None:
self.message = message
self.kind = kind
self.on_submit = on_submit
self.buffer = ""