Revert "Add interactive checkbox share selector with arrow keys and spacebar"
This reverts commit d0f3a7e77b.
This commit is contained in:
@@ -63,13 +63,6 @@ from dataclasses import dataclass, field
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
try:
|
|
||||||
import tty
|
|
||||||
import termios
|
|
||||||
_TTY_SUPPORTED = True
|
|
||||||
except ImportError:
|
|
||||||
_TTY_SUPPORTED = False # Windows
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Color helpers (ANSI; auto-disabled when stderr is not a TTY)
|
# Color helpers (ANSI; auto-disabled when stderr is not a TTY)
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -1117,130 +1110,33 @@ def _confirm(label: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _checkbox_select(items: list[str], title: str) -> list[int]:
|
|
||||||
"""
|
|
||||||
Full-screen interactive checkbox selector. Returns the indices of selected
|
|
||||||
items. All items start selected.
|
|
||||||
|
|
||||||
Keys:
|
|
||||||
↑ / k move cursor up
|
|
||||||
↓ / j move cursor down
|
|
||||||
Space toggle item under cursor
|
|
||||||
a toggle all (select all if any unselected, else deselect all)
|
|
||||||
Enter confirm
|
|
||||||
q / Esc confirm with current selection
|
|
||||||
|
|
||||||
Falls back to simple numbered input if the terminal doesn't support raw mode.
|
|
||||||
"""
|
|
||||||
if not _TTY_SUPPORTED or not sys.stdin.isatty():
|
|
||||||
return list(range(len(items))) # caller will use fallback
|
|
||||||
|
|
||||||
selected = [True] * len(items)
|
|
||||||
cursor = 0
|
|
||||||
n = len(items)
|
|
||||||
|
|
||||||
def _render():
|
|
||||||
# Move cursor up to redraw in-place after first render
|
|
||||||
lines = [
|
|
||||||
"",
|
|
||||||
f" {_bold(title)}",
|
|
||||||
f" {_dim('↑/↓ move · Space toggle · a all/none · Enter confirm')}",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
for i, label in enumerate(items):
|
|
||||||
arrow = _cyan("▶") if i == cursor else " "
|
|
||||||
box = _bold_green("✓") if selected[i] else _dim("·")
|
|
||||||
if i == cursor:
|
|
||||||
lines.append(f" {arrow} [{box}] {_bold(label)}")
|
|
||||||
else:
|
|
||||||
lines.append(f" {arrow} [{box}] {label}")
|
|
||||||
lines.append("")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
def _read_key(fd: int) -> str:
|
|
||||||
ch = os.read(fd, 1)
|
|
||||||
if ch == b"\x1b":
|
|
||||||
rest = os.read(fd, 2)
|
|
||||||
return "\x1b" + rest.decode("latin-1", errors="replace")
|
|
||||||
return ch.decode("latin-1", errors="replace")
|
|
||||||
|
|
||||||
# Print initial render
|
|
||||||
output = _render()
|
|
||||||
print(output, end="", flush=True)
|
|
||||||
line_count = output.count("\n")
|
|
||||||
|
|
||||||
fd = sys.stdin.fileno()
|
|
||||||
old = termios.tcgetattr(fd)
|
|
||||||
try:
|
|
||||||
tty.setraw(fd)
|
|
||||||
while True:
|
|
||||||
key = _read_key(fd)
|
|
||||||
|
|
||||||
if key in ("\r", "\n", "q", "\x1b"):
|
|
||||||
break
|
|
||||||
elif key in ("\x1b[A", "k"): # up
|
|
||||||
cursor = (cursor - 1) % n
|
|
||||||
elif key in ("\x1b[B", "j"): # down
|
|
||||||
cursor = (cursor + 1) % n
|
|
||||||
elif key == " ":
|
|
||||||
selected[cursor] = not selected[cursor]
|
|
||||||
elif key in ("a", "A"):
|
|
||||||
# Select all if any are unselected, otherwise deselect all
|
|
||||||
new_state = not all(selected)
|
|
||||||
selected[:] = [new_state] * n
|
|
||||||
|
|
||||||
# Redraw in-place
|
|
||||||
sys.stdout.write(f"\033[{line_count}A\033[J")
|
|
||||||
output = _render()
|
|
||||||
print(output, end="", flush=True)
|
|
||||||
finally:
|
|
||||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
||||||
|
|
||||||
return [i for i, s in enumerate(selected) if s]
|
|
||||||
|
|
||||||
|
|
||||||
def _select_shares(shares: list[dict], share_type: str) -> list[dict]:
|
def _select_shares(shares: list[dict], share_type: str) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Interactive share selector. Uses arrow-key/spacebar UI when stdin is a TTY,
|
Display a numbered list of *shares* and return only those the user selects.
|
||||||
falls back to numbered text input otherwise.
|
Enter (or 'all') returns all shares unchanged. 'n' / 'none' returns [].
|
||||||
"""
|
"""
|
||||||
if not shares:
|
if not shares:
|
||||||
return shares
|
return shares
|
||||||
|
|
||||||
# Build display labels
|
print(f"\n {_bold(f'{share_type} shares in archive ({len(shares)}):')} \n")
|
||||||
labels: list[str] = []
|
for i, share in enumerate(shares, 1):
|
||||||
for share in shares:
|
|
||||||
if share_type == "SMB":
|
if share_type == "SMB":
|
||||||
name = share.get("name", "<unnamed>")
|
name = share.get("name", "<unnamed>")
|
||||||
path = share.get("path", "")
|
path = share.get("path", "")
|
||||||
labels.append(f"{name:<22} {_dim(path)}")
|
print(f" {_cyan(str(i) + '.')} {name:<22} {_dim(path)}")
|
||||||
else: # NFS
|
else: # NFS
|
||||||
pl = share.get("paths") or []
|
pl = share.get("paths") or []
|
||||||
path = share.get("path") or (pl[0] if pl else "")
|
path = share.get("path") or (pl[0] if pl else "")
|
||||||
extra = f" {_dim('+ ' + str(len(pl) - 1) + ' more')}" if len(pl) > 1 else ""
|
extra = f" {_dim('+ ' + str(len(pl) - 1) + ' more')}" if len(pl) > 1 else ""
|
||||||
labels.append(f"{path}{extra}")
|
print(f" {_cyan(str(i) + '.')} {path}{extra}")
|
||||||
|
|
||||||
title = f"{share_type} shares to migrate ({len(shares)} found)"
|
|
||||||
|
|
||||||
if _TTY_SUPPORTED and sys.stdin.isatty():
|
|
||||||
indices = _checkbox_select(labels, title)
|
|
||||||
selected = [shares[i] for i in indices]
|
|
||||||
if selected:
|
|
||||||
print(f" {_green('✓')} {len(selected)} of {len(shares)} {share_type} share(s) selected.\n")
|
|
||||||
else:
|
|
||||||
print(f" {_yellow('–')} No {share_type} shares selected.\n")
|
|
||||||
return selected
|
|
||||||
|
|
||||||
# ── Fallback: numbered text input ────────────────────────────────────────
|
|
||||||
print(f"\n {_bold(title)}\n")
|
|
||||||
for i, label in enumerate(labels, 1):
|
|
||||||
print(f" {_cyan(str(i) + '.')} {label}")
|
|
||||||
print()
|
print()
|
||||||
raw = _prompt(
|
raw = _prompt(
|
||||||
f" Select {share_type} shares to migrate "
|
f" Select {share_type} shares to migrate "
|
||||||
"(e.g. '1 3', Enter = all, 'n' = none)",
|
"(e.g. '1 3', Enter = all, 'n' = none)",
|
||||||
default="all",
|
default="all",
|
||||||
)
|
)
|
||||||
|
|
||||||
low = raw.strip().lower()
|
low = raw.strip().lower()
|
||||||
if low in ("", "all"):
|
if low in ("", "all"):
|
||||||
print(f" {_green('✓')} All {len(shares)} {share_type} share(s) selected.")
|
print(f" {_green('✓')} All {len(shares)} {share_type} share(s) selected.")
|
||||||
@@ -1248,19 +1144,21 @@ def _select_shares(shares: list[dict], share_type: str) -> list[dict]:
|
|||||||
if low in ("n", "none", "0"):
|
if low in ("n", "none", "0"):
|
||||||
print(f" {_yellow('–')} No {share_type} shares selected.")
|
print(f" {_yellow('–')} No {share_type} shares selected.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
seen: set[int] = set()
|
seen: set[int] = set()
|
||||||
result: list[dict] = []
|
selected: list[dict] = []
|
||||||
for tok in raw.split():
|
for tok in raw.split():
|
||||||
if tok.isdigit():
|
if tok.isdigit():
|
||||||
idx = int(tok) - 1
|
idx = int(tok) - 1
|
||||||
if 0 <= idx < len(shares) and idx not in seen:
|
if 0 <= idx < len(shares) and idx not in seen:
|
||||||
seen.add(idx)
|
seen.add(idx)
|
||||||
result.append(shares[idx])
|
selected.append(shares[idx])
|
||||||
if result:
|
|
||||||
print(f" {_green('✓')} {len(result)} of {len(shares)} {share_type} share(s) selected.")
|
if selected:
|
||||||
|
print(f" {_green('✓')} {len(selected)} of {len(shares)} {share_type} share(s) selected.")
|
||||||
else:
|
else:
|
||||||
print(f" {_yellow('–')} No valid selections; skipping {share_type} shares.")
|
print(f" {_yellow('–')} No valid selections; skipping {share_type} shares.")
|
||||||
return result
|
return selected
|
||||||
|
|
||||||
|
|
||||||
def interactive_mode() -> None:
|
def interactive_mode() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user