feat: add folder/file creation and folder deletion with prompts
This commit is contained in:
parent
f94771c67a
commit
4b721cb993
7 changed files with 150 additions and 2 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
14
src/IO/UI.py
14
src/IO/UI.py
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
14
src/core/FileActions.py
Normal 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)
|
||||||
|
|
@ -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
11
src/core/Prompt.py
Normal 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 = ""
|
||||||
Loading…
Add table
Reference in a new issue