From 543ca6b471d8308df82a301506b350abd44e9ed5 Mon Sep 17 00:00:00 2001 From: scott Date: Wed, 4 Mar 2026 21:27:25 -0500 Subject: [PATCH] Revert "Add interactive checkbox share selector with arrow keys and spacebar" This reverts commit d0f3a7e77bb51f922d8be1e6b6711a0f33d84d3f. --- truenas_migrate.py | 132 ++++++--------------------------------------- 1 file changed, 15 insertions(+), 117 deletions(-) diff --git a/truenas_migrate.py b/truenas_migrate.py index 9af9680..b678f48 100644 --- a/truenas_migrate.py +++ b/truenas_migrate.py @@ -63,13 +63,6 @@ from dataclasses import dataclass, field from pathlib import Path 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) # ───────────────────────────────────────────────────────────────────────────── @@ -1117,130 +1110,33 @@ def _confirm(label: str) -> bool: 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]: """ - Interactive share selector. Uses arrow-key/spacebar UI when stdin is a TTY, - falls back to numbered text input otherwise. + Display a numbered list of *shares* and return only those the user selects. + Enter (or 'all') returns all shares unchanged. 'n' / 'none' returns []. """ if not shares: return shares - # Build display labels - labels: list[str] = [] - for share in shares: + print(f"\n {_bold(f'{share_type} shares in archive ({len(shares)}):')} \n") + for i, share in enumerate(shares, 1): if share_type == "SMB": name = share.get("name", "") path = share.get("path", "") - labels.append(f"{name:<22} {_dim(path)}") + print(f" {_cyan(str(i) + '.')} {name:<22} {_dim(path)}") else: # NFS - pl = share.get("paths") or [] + pl = share.get("paths") or [] path = share.get("path") or (pl[0] if pl 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() raw = _prompt( f" Select {share_type} shares to migrate " "(e.g. '1 3', Enter = all, 'n' = none)", default="all", ) + low = raw.strip().lower() if low in ("", "all"): 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"): print(f" {_yellow('–')} No {share_type} shares selected.") return [] + seen: set[int] = set() - result: list[dict] = [] + selected: list[dict] = [] for tok in raw.split(): if tok.isdigit(): idx = int(tok) - 1 if 0 <= idx < len(shares) and idx not in seen: seen.add(idx) - result.append(shares[idx]) - if result: - print(f" {_green('✓')} {len(result)} of {len(shares)} {share_type} share(s) selected.") + selected.append(shares[idx]) + + if selected: + print(f" {_green('✓')} {len(selected)} of {len(shares)} {share_type} share(s) selected.") else: print(f" {_yellow('–')} No valid selections; skipping {share_type} shares.") - return result + return selected def interactive_mode() -> None: