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 __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ..items.Folder import Folder
from ..core.Prompt import Prompt
if TYPE_CHECKING: if TYPE_CHECKING:
from .App import App from .App import App
BACKSPACE_KEYS = ("KEY_BACKSPACE", "\x7f", "\x08")
ENTER_KEYS = ("\n", "KEY_ENTER")
ESCAPE_KEY = "\x1b"
class Input: class Input:
def __init__(self, app: App) -> None: def __init__(self, app: App) -> None:
@ -16,13 +22,40 @@ class Input:
"back": self._back, "back": self._back,
"parent": self._parent, "parent": self._parent,
"sort": self._sort, "sort": self._sort,
"new_folder": self._new_folder,
"delete_folder": self._delete_folder,
"new_file": self._new_file,
} }
def handle(self, key: str) -> bool: 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) action = self.app.config.action_for(key)
handler = self._actions.get(action) handler = self._actions.get(action)
return handler() if handler else True 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: def _quit(self) -> bool:
return False return False
@ -54,6 +87,57 @@ class Input:
self.app.navigator.cycle_sort() self.app.navigator.cycle_sort()
return True 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 @property
def app(self) -> App: def app(self) -> App:
return self._app return self._app

View file

@ -26,6 +26,7 @@ class UI:
stdscr.erase() stdscr.erase()
height, width = stdscr.getmaxyx() height, width = stdscr.getmaxyx()
navigator = self.app.navigator navigator = self.app.navigator
prompt = self.app.prompt
stdscr.addstr(0, 0, str(navigator.history[-1])[:width - 1]) stdscr.addstr(0, 0, str(navigator.history[-1])[:width - 1])
if navigator.error: if navigator.error:
@ -33,12 +34,23 @@ class UI:
stdscr.addstr(1, 0, navigator.error[:width - 1], stdscr.addstr(1, 0, navigator.error[:width - 1],
curses.color_pair(error_pair)) curses.color_pair(error_pair))
footer_rows = 1 if prompt else 0
items = navigator.items 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))): for index in range(min(available, len(items))):
item = items[index] item = items[index]
self._render_item(stdscr, item, index, index + self.HEADER_ROWS, width) 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") SIZE_UNITS = ("", "K", "M", "G", "T", "P")
@staticmethod @staticmethod

View file

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

View file

@ -14,6 +14,9 @@ DEFAULT_KEYBINDS: dict[str, list[str]] = {
"back": ["-", "KEY_LEFT", "KEY_BACKSPACE", "\x7f", "\x08"], "back": ["-", "KEY_LEFT", "KEY_BACKSPACE", "\x7f", "\x08"],
"parent": ["h"], "parent": ["h"],
"sort": ["i"], "sort": ["i"],
"new_folder": ["d"],
"delete_folder": ["D"],
"new_file": ["%"],
} }
DEFAULT_CURSOR_RESET = "top" 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._current: Folder | None = None
self._items: list[AItem] = [] self._items: list[AItem] = []
self._error: str | None = None self._error: str | None = None
self._action_error: str | None = None
self._load() self._load()
def _load(self) -> None: def _load(self) -> None:
@ -44,8 +45,12 @@ class Navigator:
else: else:
self._cursor.resete() self._cursor.resete()
def set_action_error(self, message: str) -> None:
self._action_error = message
def _navigate_to(self, path: Path) -> None: def _navigate_to(self, path: Path) -> None:
self._history.append(path) self._history.append(path)
self._action_error = None
self._load() self._load()
self._reset_cursor() self._reset_cursor()
@ -67,6 +72,7 @@ class Navigator:
if len(self._history) <= 1: if len(self._history) <= 1:
return False return False
self._history.pop() self._history.pop()
self._action_error = None
self._load() self._load()
self._reset_cursor() self._reset_cursor()
return True return True
@ -89,4 +95,4 @@ class Navigator:
@property @property
def error(self) -> str | None: 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 = ""