add somethin to myrummy and fix SJNfilemanager
This commit is contained in:
+4
-2
@@ -12,8 +12,10 @@ Beim Start werden Einstellungen abgefragt:
|
|||||||
- Erstmeldung 30: Für die erste Meldung werden 30 Punkte gebraucht
|
- Erstmeldung 30: Für die erste Meldung werden 30 Punkte gebraucht
|
||||||
|
|
||||||
Bots spielen automatisch: Sie legen, was sie können (Sätze und Folgen,
|
Bots spielen automatisch: Sie legen, was sie können (Sätze und Folgen,
|
||||||
beachten die 30-Punkte-Regel) und ziehen sonst eine Karte. Die Handkarten
|
beachten die 30-Punkte-Regel) und ziehen sonst eine Karte.
|
||||||
eines Bots werden nie angezeigt.
|
Was ein Bot tut, bleibt geheim: Weder seine Handkarten noch die gezogene
|
||||||
|
Karte oder seine Melde-Aktionen werden angezeigt - man sieht nur, welche
|
||||||
|
Gruppen danach auf dem Tisch liegen.
|
||||||
|
|
||||||
Nach jedem beendeten Zug erscheint ein Übergabe-Bildschirm, damit der
|
Nach jedem beendeten Zug erscheint ein Übergabe-Bildschirm, damit der
|
||||||
nächste Spieler die Karten des Vorgängers nicht sieht.
|
nächste Spieler die Karten des Vorgängers nicht sieht.
|
||||||
|
|||||||
+1
-8
@@ -202,7 +202,6 @@ class Game:
|
|||||||
|
|
||||||
def bot_turn(self):
|
def bot_turn(self):
|
||||||
p=self.p
|
p=self.p
|
||||||
print(f'\n {p.name} ist am Zug:')
|
|
||||||
melded=[]
|
melded=[]
|
||||||
while True:
|
while True:
|
||||||
m=self.find_meld(p.hand)
|
m=self.find_meld(p.hand)
|
||||||
@@ -213,8 +212,6 @@ class Game:
|
|||||||
if melded and (p.opened or not self.min30 or total>=30):
|
if melded and (p.opened or not self.min30 or total>=30):
|
||||||
for m in melded: self.table.append(m)
|
for m in melded: self.table.append(m)
|
||||||
if not p.opened: p.opened=True
|
if not p.opened: p.opened=True
|
||||||
for m in melded:
|
|
||||||
print(' legt: '+' '.join(c.plain() for c in m))
|
|
||||||
if not p.hand:
|
if not p.hand:
|
||||||
print(f'\n {p.name} gewinnt!')
|
print(f'\n {p.name} gewinnt!')
|
||||||
return True
|
return True
|
||||||
@@ -223,13 +220,9 @@ class Game:
|
|||||||
for c in m: p.hand.append(c)
|
for c in m: p.hand.append(c)
|
||||||
p.hand=sort_hand(p.hand)
|
p.hand=sort_hand(p.hand)
|
||||||
if self.deck:
|
if self.deck:
|
||||||
card=self.deck.pop(); p.hand.append(card); p.hand=sort_hand(p.hand)
|
p.hand.append(self.deck.pop()); p.hand=sort_hand(p.hand)
|
||||||
print(f' kann nicht legen und zieht {card.plain()}.')
|
|
||||||
else:
|
|
||||||
print(' Stapel leer - Zug wird beendet.')
|
|
||||||
self.turn=(self.turn+1)%len(self.players); self.drew=False; self.melded=False; self.history=[]
|
self.turn=(self.turn+1)%len(self.players); self.drew=False; self.melded=False; self.history=[]
|
||||||
self.handover=True
|
self.handover=True
|
||||||
input('\n ENTER...')
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|||||||
+269
-32
@@ -4,16 +4,27 @@
|
|||||||
Keys:
|
Keys:
|
||||||
Up/Down or k/j Navigate
|
Up/Down or k/j Navigate
|
||||||
Enter Open (folder -> cd, file -> menu)
|
Enter Open (folder -> cd, file -> menu)
|
||||||
/ Search (live-filter)
|
/ Search (live filter while typing)
|
||||||
Backspace or h Go to parent directory
|
Backspace or h Go to parent directory
|
||||||
. Toggle hidden files
|
. Toggle hidden files
|
||||||
|
r Rename selected entry
|
||||||
|
d Delete selected entry (asks for confirmation)
|
||||||
|
n Create a new folder
|
||||||
|
c / x Mark selected entry as copy / move source
|
||||||
|
p Paste the marked entry into the current folder
|
||||||
q Quit
|
q Quit
|
||||||
|
|
||||||
|
Symlinks to folders and files are followed: folder links can be entered
|
||||||
|
like real directories. Broken symlinks are not listed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
|
import locale
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +45,10 @@ FILE_CATEGORIES = {
|
|||||||
".mp3", ".wav", ".flac", ".ogg", ".m4a"
|
".mp3", ".wav", ".flac", ".ogg", ".m4a"
|
||||||
},
|
},
|
||||||
"Dokumente": {
|
"Dokumente": {
|
||||||
".pdf", ".doc", ".docx", ".odt", ".txt", ".rtf"
|
".doc", ".docx", ".odt", ".txt", ".rtf"
|
||||||
|
},
|
||||||
|
"PDF": {
|
||||||
|
".pdf"
|
||||||
},
|
},
|
||||||
"Tabellen": {
|
"Tabellen": {
|
||||||
".xls", ".xlsx", ".ods", ".csv"
|
".xls", ".xlsx", ".ods", ".csv"
|
||||||
@@ -53,7 +67,7 @@ FILE_CATEGORIES = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_category(filename):
|
def get_category(filename: str) -> str:
|
||||||
extension = Path(filename).suffix.lower()
|
extension = Path(filename).suffix.lower()
|
||||||
|
|
||||||
for category, extensions in FILE_CATEGORIES.items():
|
for category, extensions in FILE_CATEGORIES.items():
|
||||||
@@ -77,25 +91,31 @@ def human_size(n: int) -> str:
|
|||||||
return f"{int(size)}T"
|
return f"{int(size)}T"
|
||||||
|
|
||||||
|
|
||||||
def list_entries(cwd: Path, show_hidden: bool):
|
def list_entries(cwd: Path, show_hidden: bool) -> tuple[list[Path], str | None]:
|
||||||
"""Return [.., dir1, dir2, ..., file1, file2, ...]."""
|
"""Return (entries, error); error is None on success.
|
||||||
|
|
||||||
|
Entries are [.., dir1, dir2, ..., file1, file2, ...]. Symlinks are
|
||||||
|
followed: links to folders are listed and can be entered like real
|
||||||
|
directories, links to files are listed as files. Broken symlinks are
|
||||||
|
not listed. On error, entries is just [..] and error carries a message.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
all_entries = list(cwd.iterdir())
|
all_entries = list(cwd.iterdir())
|
||||||
except PermissionError:
|
except OSError as e:
|
||||||
return [PARENT]
|
return [PARENT], str(e) or type(e).__name__
|
||||||
|
|
||||||
if not show_hidden:
|
if not show_hidden:
|
||||||
all_entries = [e for e in all_entries if not e.name.startswith(".")]
|
all_entries = [e for e in all_entries if not e.name.startswith(".")]
|
||||||
|
|
||||||
dirs = sorted(
|
dirs = sorted(
|
||||||
(e for e in all_entries if e.is_dir(follow_symlinks=False)),
|
(e for e in all_entries if e.is_dir(follow_symlinks=True)),
|
||||||
key=lambda e: e.name.lower(),
|
key=lambda e: e.name.lower(),
|
||||||
)
|
)
|
||||||
files = sorted(
|
files = sorted(
|
||||||
(e for e in all_entries if e.is_file(follow_symlinks=False)),
|
(e for e in all_entries if e.is_file(follow_symlinks=True)),
|
||||||
key=lambda e: e.name.lower(),
|
key=lambda e: e.name.lower(),
|
||||||
)
|
)
|
||||||
return [PARENT] + dirs + files
|
return [PARENT] + dirs + files, None
|
||||||
|
|
||||||
|
|
||||||
def is_parent(entry: Path) -> bool:
|
def is_parent(entry: Path) -> bool:
|
||||||
@@ -116,9 +136,19 @@ def format_entry(name: str, is_dir: bool, full_path: Path, width: int) -> str:
|
|||||||
|
|
||||||
# ---------- input ------------------------------------------------------------
|
# ---------- input ------------------------------------------------------------
|
||||||
|
|
||||||
def prompt_line(stdscr, prompt: str, initial: str = "") -> str | None:
|
def prompt_line(
|
||||||
|
stdscr: curses.window,
|
||||||
|
prompt: str,
|
||||||
|
initial: str = "",
|
||||||
|
on_change: Callable[[str], None] | None = None,
|
||||||
|
) -> str | None:
|
||||||
"""Show a prompt at the bottom and read a line. Returns None if cancelled (ESC).
|
"""Show a prompt at the bottom and read a line. Returns None if cancelled (ESC).
|
||||||
|
|
||||||
|
Control characters (Tab, Ctrl+C, ...) never enter the buffer; printable
|
||||||
|
ASCII and Unicode characters (umlauts etc.) are accepted. If `on_change`
|
||||||
|
is given it is called with the current buffer after every edit, which
|
||||||
|
allows live-filtering while the user types.
|
||||||
|
|
||||||
Modes are restored in `finally` so a curses.error or any other exception
|
Modes are restored in `finally` so a curses.error or any other exception
|
||||||
cannot leave the terminal in a half-configured state.
|
cannot leave the terminal in a half-configured state.
|
||||||
"""
|
"""
|
||||||
@@ -136,19 +166,22 @@ def prompt_line(stdscr, prompt: str, initial: str = "") -> str | None:
|
|||||||
|
|
||||||
buf = initial
|
buf = initial
|
||||||
while True:
|
while True:
|
||||||
ch = stdscr.getch()
|
ch = stdscr.get_wch()
|
||||||
if ch == 27: # bare ESC (no following escape-sequence bytes)
|
if ch in (27, -1): # bare ESC (get_wch reports -1 for ESC)
|
||||||
return None
|
return None
|
||||||
if ch in (10, 13, curses.KEY_ENTER):
|
if ch in (10, 13, curses.KEY_ENTER):
|
||||||
break
|
break
|
||||||
if ch in (curses.KEY_BACKSPACE, 127, 8):
|
if ch in (curses.KEY_BACKSPACE, 127, 8) or ch in ("\x7f", "\x08"):
|
||||||
if buf:
|
if buf:
|
||||||
buf = buf[:-1]
|
buf = buf[:-1]
|
||||||
elif 0 <= ch < 256:
|
elif isinstance(ch, str) and 32 <= ord(ch) != 127:
|
||||||
# Plain ASCII printable; safe to append.
|
# Printable ASCII plus Unicode (umlauts, ...).
|
||||||
buf += chr(ch)
|
# Control characters (< 0x20) and DEL are ignored.
|
||||||
# Arrow keys, function keys, etc. are silently ignored.
|
buf += ch
|
||||||
|
# ints (arrow keys, function keys, ...) are silently ignored.
|
||||||
|
|
||||||
|
if on_change is not None:
|
||||||
|
on_change(buf)
|
||||||
stdscr.addstr(row, 0, prompt + (buf or ""))
|
stdscr.addstr(row, 0, prompt + (buf or ""))
|
||||||
stdscr.clrtoeol()
|
stdscr.clrtoeol()
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
@@ -164,6 +197,14 @@ def prompt_line(stdscr, prompt: str, initial: str = "") -> str | None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def confirm(stdscr: curses.window, prompt: str) -> bool:
|
||||||
|
"""Ask a yes/no question via prompt_line. True only on explicit y/yes."""
|
||||||
|
answer = prompt_line(stdscr, prompt, initial="n")
|
||||||
|
if answer is None:
|
||||||
|
return False
|
||||||
|
return answer.strip().lower().startswith("y")
|
||||||
|
|
||||||
|
|
||||||
# ---------- rendering -------------------------------------------------------
|
# ---------- rendering -------------------------------------------------------
|
||||||
|
|
||||||
def init_colors() -> dict[str, int]:
|
def init_colors() -> dict[str, int]:
|
||||||
@@ -194,7 +235,18 @@ def init_colors() -> dict[str, int]:
|
|||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
def draw(stdscr, entries, filtered, selected, cwd, status, status_kind, show_hidden, query, colors):
|
def draw(
|
||||||
|
stdscr: curses.window,
|
||||||
|
entries: list[Path],
|
||||||
|
filtered: list[Path] | None,
|
||||||
|
selected: int,
|
||||||
|
cwd: Path,
|
||||||
|
status: str,
|
||||||
|
status_kind: str,
|
||||||
|
show_hidden: bool,
|
||||||
|
query: str | None,
|
||||||
|
colors: dict[str, int],
|
||||||
|
) -> int:
|
||||||
stdscr.erase()
|
stdscr.erase()
|
||||||
max_y, max_x = stdscr.getmaxyx()
|
max_y, max_x = stdscr.getmaxyx()
|
||||||
|
|
||||||
@@ -242,7 +294,7 @@ def draw(stdscr, entries, filtered, selected, cwd, status, status_kind, show_hid
|
|||||||
full = cwd.parent
|
full = cwd.parent
|
||||||
else:
|
else:
|
||||||
name = entry.name
|
name = entry.name
|
||||||
is_dir = entry.is_dir(follow_symlinks=False)
|
is_dir = entry.is_dir(follow_symlinks=True)
|
||||||
full = entry
|
full = entry
|
||||||
|
|
||||||
label = format_entry(name, is_dir, full, max_x - 1)
|
label = format_entry(name, is_dir, full, max_x - 1)
|
||||||
@@ -287,7 +339,7 @@ def draw(stdscr, entries, filtered, selected, cwd, status, status_kind, show_hid
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Footer (key hints)
|
# Footer (key hints)
|
||||||
footer = " Up/Dn move | Enter open | / search | .. parent | . hidden | q quit "
|
footer = " Up/Dn move | Enter open | / live-search | r rename | d delete | n new dir | c copy | x move | p paste | . hidden | q quit "
|
||||||
try:
|
try:
|
||||||
stdscr.addnstr(max_y - 1, 0, footer, max_x - 1, curses.A_DIM)
|
stdscr.addnstr(max_y - 1, 0, footer, max_x - 1, curses.A_DIM)
|
||||||
except curses.error:
|
except curses.error:
|
||||||
@@ -342,7 +394,9 @@ OPEN_COMMANDS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def file_menu(stdscr, filepath: Path, colors) -> list[str] | None:
|
def file_menu(
|
||||||
|
stdscr: curses.window, filepath: Path, colors: dict[str, int]
|
||||||
|
) -> list[str] | None:
|
||||||
"""Show the open-with menu based on the file category.
|
"""Show the open-with menu based on the file category.
|
||||||
Returns argv list, or None if cancelled.
|
Returns argv list, or None if cancelled.
|
||||||
"""
|
"""
|
||||||
@@ -447,7 +501,7 @@ def file_menu(stdscr, filepath: Path, colors) -> list[str] | None:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_external(stdscr, argv: list[str]) -> str:
|
def run_external(stdscr: curses.window, argv: list[str]) -> str:
|
||||||
"""Suspend curses, run argv, then restore. Returns a status string.
|
"""Suspend curses, run argv, then restore. Returns a status string.
|
||||||
|
|
||||||
Defensive: we snapshot cwd before, run, then forcibly chdir back to the
|
Defensive: we snapshot cwd before, run, then forcibly chdir back to the
|
||||||
@@ -489,7 +543,7 @@ def run_external(stdscr, argv: list[str]) -> str:
|
|||||||
|
|
||||||
# ---------- main loop -------------------------------------------------------
|
# ---------- main loop -------------------------------------------------------
|
||||||
|
|
||||||
def main(stdscr):
|
def main(stdscr: curses.window) -> None:
|
||||||
colors = init_colors()
|
colors = init_colors()
|
||||||
curses.curs_set(0)
|
curses.curs_set(0)
|
||||||
stdscr.keypad(True)
|
stdscr.keypad(True)
|
||||||
@@ -502,10 +556,15 @@ def main(stdscr):
|
|||||||
status_kind = "status_ok"
|
status_kind = "status_ok"
|
||||||
query = None
|
query = None
|
||||||
filtered = None # when not None, used for display (search active)
|
filtered = None # when not None, used for display (search active)
|
||||||
|
copy_source: Path | None = None
|
||||||
|
move_source: Path | None = None
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
entries = list_entries(cwd, show_hidden)
|
entries, list_error = list_entries(cwd, show_hidden)
|
||||||
|
if list_error is not None:
|
||||||
|
status = f"Cannot read directory: {list_error}"
|
||||||
|
status_kind = "status_err"
|
||||||
|
|
||||||
if filtered is not None:
|
if filtered is not None:
|
||||||
# Re-apply filter against the fresh entries
|
# Re-apply filter against the fresh entries
|
||||||
@@ -578,7 +637,7 @@ def main(stdscr):
|
|||||||
selected = 0
|
selected = 0
|
||||||
filtered = None
|
filtered = None
|
||||||
query = None
|
query = None
|
||||||
elif entry.is_dir(follow_symlinks=False):
|
elif entry.is_dir(follow_symlinks=True):
|
||||||
try:
|
try:
|
||||||
os.chdir(entry)
|
os.chdir(entry)
|
||||||
cwd = Path.cwd()
|
cwd = Path.cwd()
|
||||||
@@ -592,16 +651,40 @@ def main(stdscr):
|
|||||||
status = f"Cannot enter {entry.name}: {e}"
|
status = f"Cannot enter {entry.name}: {e}"
|
||||||
status_kind = "status_err"
|
status_kind = "status_err"
|
||||||
else:
|
else:
|
||||||
if entry.is_file(follow_symlinks=False):
|
if entry.is_file(follow_symlinks=True):
|
||||||
argv = file_menu(stdscr, entry, colors)
|
argv = file_menu(stdscr, entry, colors)
|
||||||
if argv is not None:
|
if argv is not None:
|
||||||
status = run_external(stdscr, argv)
|
status = run_external(stdscr, argv)
|
||||||
status_kind = "status_ok" if status.startswith("Ran:") else "status_err"
|
status_kind = "status_ok" if status.startswith("Ran:") else "status_err"
|
||||||
cwd = Path.cwd()
|
new_cwd = Path.cwd()
|
||||||
selected = 0
|
if new_cwd != cwd:
|
||||||
|
cwd = new_cwd
|
||||||
|
selected = 0
|
||||||
|
|
||||||
elif ch == ord("/"):
|
elif ch == ord("/"):
|
||||||
q = prompt_line(stdscr, "/ ")
|
def apply_search(buf: str) -> None:
|
||||||
|
"""Live-render the list while the user types."""
|
||||||
|
nonlocal query, filtered, selected
|
||||||
|
query = buf
|
||||||
|
if buf == "":
|
||||||
|
filtered = None
|
||||||
|
else:
|
||||||
|
qlow = buf.lower()
|
||||||
|
candidates = [e for e in entries if qlow in e.name.lower()]
|
||||||
|
if not candidates:
|
||||||
|
filtered = []
|
||||||
|
else:
|
||||||
|
if PARENT in entries:
|
||||||
|
candidates = [PARENT] + [
|
||||||
|
e for e in candidates if e != PARENT
|
||||||
|
]
|
||||||
|
filtered = candidates
|
||||||
|
selected = draw(
|
||||||
|
stdscr, entries, filtered, selected, cwd,
|
||||||
|
"", "status_ok", show_hidden, query, colors,
|
||||||
|
)
|
||||||
|
|
||||||
|
q = prompt_line(stdscr, "/ ", on_change=apply_search)
|
||||||
if q is None:
|
if q is None:
|
||||||
# cancel search
|
# cancel search
|
||||||
filtered = None
|
filtered = None
|
||||||
@@ -625,17 +708,171 @@ def main(stdscr):
|
|||||||
selected = 0
|
selected = 0
|
||||||
# Re-apply search if active
|
# Re-apply search if active
|
||||||
if query:
|
if query:
|
||||||
|
fresh, _err = list_entries(cwd, show_hidden)
|
||||||
qlow = query.lower()
|
qlow = query.lower()
|
||||||
candidates = [
|
candidates = [
|
||||||
e for e in list_entries(cwd, show_hidden) if qlow in e.name.lower()
|
e for e in fresh if qlow in e.name.lower()
|
||||||
]
|
]
|
||||||
|
if PARENT in fresh:
|
||||||
|
candidates = [PARENT] + [
|
||||||
|
e for e in candidates if e != PARENT
|
||||||
|
]
|
||||||
filtered = candidates if candidates else []
|
filtered = candidates if candidates else []
|
||||||
|
|
||||||
|
elif ch in (ord("r"), ord("R")):
|
||||||
|
vis = filtered if filtered is not None else entries
|
||||||
|
if not vis:
|
||||||
|
continue
|
||||||
|
entry = vis[selected]
|
||||||
|
if is_parent(entry):
|
||||||
|
status = "Cannot rename '..'"
|
||||||
|
status_kind = "status_err"
|
||||||
|
continue
|
||||||
|
new_name = prompt_line(
|
||||||
|
stdscr, f"Rename '{entry.name}' to: ", initial=entry.name
|
||||||
|
)
|
||||||
|
if new_name is None:
|
||||||
|
continue
|
||||||
|
new_name = new_name.strip()
|
||||||
|
if not new_name or new_name == entry.name:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
target = entry.parent / new_name
|
||||||
|
if target.exists():
|
||||||
|
raise FileExistsError(f"'{new_name}' already exists")
|
||||||
|
entry.rename(target)
|
||||||
|
status = f"Renamed to '{new_name}'"
|
||||||
|
status_kind = "status_ok"
|
||||||
|
except OSError as e:
|
||||||
|
status = f"Rename failed: {e}"
|
||||||
|
status_kind = "status_err"
|
||||||
|
|
||||||
|
elif ch in (ord("d"), ord("D")):
|
||||||
|
vis = filtered if filtered is not None else entries
|
||||||
|
if not vis:
|
||||||
|
continue
|
||||||
|
entry = vis[selected]
|
||||||
|
if is_parent(entry):
|
||||||
|
status = "Cannot delete '..'"
|
||||||
|
status_kind = "status_err"
|
||||||
|
continue
|
||||||
|
if not confirm(stdscr, f"Delete '{entry.name}'? (y/N) "):
|
||||||
|
status = "Delete cancelled"
|
||||||
|
status_kind = "status_ok"
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if entry.is_symlink():
|
||||||
|
entry.unlink()
|
||||||
|
elif entry.is_dir():
|
||||||
|
shutil.rmtree(entry)
|
||||||
|
else:
|
||||||
|
entry.unlink()
|
||||||
|
status = f"Deleted '{entry.name}'"
|
||||||
|
status_kind = "status_ok"
|
||||||
|
except OSError as e:
|
||||||
|
status = f"Delete failed: {e}"
|
||||||
|
status_kind = "status_err"
|
||||||
|
|
||||||
|
elif ch in (ord("n"), ord("N")):
|
||||||
|
name = prompt_line(stdscr, "New folder name: ")
|
||||||
|
if name is None:
|
||||||
|
continue
|
||||||
|
name = name.strip()
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
(cwd / name).mkdir()
|
||||||
|
status = f"Created '{name}/'"
|
||||||
|
status_kind = "status_ok"
|
||||||
|
except OSError as e:
|
||||||
|
status = f"Create failed: {e}"
|
||||||
|
status_kind = "status_err"
|
||||||
|
|
||||||
|
elif ch in (ord("c"), ord("C")):
|
||||||
|
vis = filtered if filtered is not None else entries
|
||||||
|
if not vis:
|
||||||
|
continue
|
||||||
|
entry = vis[selected]
|
||||||
|
if is_parent(entry):
|
||||||
|
status = "Cannot copy '..'"
|
||||||
|
status_kind = "status_err"
|
||||||
|
continue
|
||||||
|
copy_source = entry
|
||||||
|
move_source = None
|
||||||
|
status = f"Copy: '{entry.name}' (press p to paste)"
|
||||||
|
status_kind = "status_ok"
|
||||||
|
|
||||||
|
elif ch in (ord("x"), ord("X")):
|
||||||
|
vis = filtered if filtered is not None else entries
|
||||||
|
if not vis:
|
||||||
|
continue
|
||||||
|
entry = vis[selected]
|
||||||
|
if is_parent(entry):
|
||||||
|
status = "Cannot move '..'"
|
||||||
|
status_kind = "status_err"
|
||||||
|
continue
|
||||||
|
move_source = entry
|
||||||
|
copy_source = None
|
||||||
|
status = f"Move: '{entry.name}' (press p to paste)"
|
||||||
|
status_kind = "status_ok"
|
||||||
|
|
||||||
|
elif ch in (ord("p"), ord("P")):
|
||||||
|
src = copy_source if copy_source is not None else move_source
|
||||||
|
if src is None:
|
||||||
|
status = "Nothing to paste (mark with c or x first)"
|
||||||
|
status_kind = "status_err"
|
||||||
|
continue
|
||||||
|
moving = move_source is not None
|
||||||
|
dest = cwd / src.name
|
||||||
|
if src == dest:
|
||||||
|
status = "Source and destination are the same"
|
||||||
|
status_kind = "status_err"
|
||||||
|
copy_source = None
|
||||||
|
move_source = None
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if dest.exists():
|
||||||
|
if not confirm(stdscr, f"Overwrite '{dest.name}'? (y/N) "):
|
||||||
|
status = "Paste cancelled"
|
||||||
|
status_kind = "status_ok"
|
||||||
|
continue
|
||||||
|
if dest.is_dir() and not src.is_dir():
|
||||||
|
raise IsADirectoryError(
|
||||||
|
f"'{dest.name}' is a directory"
|
||||||
|
)
|
||||||
|
if dest.is_file() and src.is_dir():
|
||||||
|
raise NotADirectoryError(
|
||||||
|
f"'{dest.name}' is a file"
|
||||||
|
)
|
||||||
|
if moving:
|
||||||
|
if dest.exists() and dest.is_dir() and src.is_dir():
|
||||||
|
# Replace the target folder (user confirmed).
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
shutil.move(src, dest)
|
||||||
|
else:
|
||||||
|
if src.is_dir():
|
||||||
|
shutil.copytree(src, dest, dirs_exist_ok=True)
|
||||||
|
else:
|
||||||
|
shutil.copy2(src, dest)
|
||||||
|
status = f"{'Moved' if moving else 'Copied'} '{src.name}'"
|
||||||
|
status_kind = "status_ok"
|
||||||
|
except OSError as e:
|
||||||
|
status = f"{'Move' if moving else 'Copy'} failed: {e}"
|
||||||
|
status_kind = "status_err"
|
||||||
|
finally:
|
||||||
|
copy_source = None
|
||||||
|
move_source = None
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def entrypoint():
|
def entrypoint() -> None:
|
||||||
|
# Make sure multi-byte input (umlauts, ...) is decoded correctly.
|
||||||
|
try:
|
||||||
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
|
except locale.Error:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
curses.wrapper(main)
|
curses.wrapper(main)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
Reference in New Issue
Block a user