from __future__ import annotations import sys from typing import Any, Dict, List, Sequence from . import _shared as sh from SYS.logger import log, debug from SYS import pipeline as ctx from SYS.result_table_api import ResultModel from SYS.result_table_adapters import get_provider from SYS.result_table_renderers import RichRenderer Cmdlet = sh.Cmdlet CmdletArg = sh.CmdletArg parse_cmdlet_args = sh.parse_cmdlet_args normalize_result_input = sh.normalize_result_input CMDLET = Cmdlet( name="select", summary="Select items from a piped result set (interactive or via -select) and emit the selected item(s).", usage="select -select [-multi] [-run-cmd ]", arg=[ CmdletArg("select", type="string", description="Selection string (e.g., 1, 2-4)", alias="s"), CmdletArg("multi", type="flag", description="Allow multiple selections."), CmdletArg("interactive", type="flag", description="Prompt interactively for selection."), CmdletArg("run-cmd", type="string", description="Cmdlet to invoke with selected items (each)"), ], detail=[ "Accepts piped input from provider-table or other sources and emits the selected item(s) as dicts.", "If -run-cmd is provided, invokes the named cmdlet for each selected item with selector args and the item as piped input.", ], ) def _parse_selection(selection: str, max_len: int) -> List[int]: """Parse a selection string like '1', '1-3', '1,3,5-7' into 0-based indices.""" if not selection: return [] parts = [p.strip() for p in str(selection).split(",") if p.strip()] indices = set() for part in parts: if "-" in part: try: a, b = part.split("-", 1) start = int(a.strip()) end = int(b.strip()) if start > end: start, end = end, start for i in range(start, end + 1): if 1 <= i <= max_len: indices.add(i - 1) except Exception: raise ValueError(f"Invalid range: {part}") else: try: v = int(part) if 1 <= v <= max_len: indices.add(v - 1) except Exception: raise ValueError(f"Invalid selection: {part}") return sorted(indices) def _dict_to_result_model(d: Dict[str, Any]) -> ResultModel: if isinstance(d, ResultModel): return d # Allow dicts or objects with attributes title = d.get("title") or d.get("name") or (d.get("path") and str(d.get("path")).split("/")[-1]) return ResultModel( title=str(title) if title is not None else "", path=d.get("path") if d.get("path") is not None else None, ext=d.get("ext") if d.get("ext") is not None else None, size_bytes=d.get("size_bytes") if d.get("size_bytes") is not None else None, metadata=d.get("metadata") or {}, source=d.get("source") or None, ) def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: parsed = parse_cmdlet_args(args, CMDLET) select_raw = parsed.get("select") allow_multi = bool(parsed.get("multi", False)) interactive = bool(parsed.get("interactive", False)) run_cmd = parsed.get("run-cmd") inputs = normalize_result_input(result) if not inputs: log("No input provided to select; pipe provider-table output or use a cmdlet that emits items.", file=sys.stderr) return 1 first_src = inputs[0].get("source") if isinstance(inputs[0], dict) else None if not first_src: log("Input items must include 'source' to resolve provider for selection.", file=sys.stderr) return 1 try: provider = get_provider(first_src) except Exception: log(f"Unknown provider: {first_src}", file=sys.stderr) return 1 # Model-ize items rows = [_dict_to_result_model(item if isinstance(item, dict) else item) for item in inputs] # Columns: provider must supply them (no legacy defaults) cols = provider.get_columns(rows) # Render table to console try: table = RichRenderer().render(rows, cols, None) try: from rich.console import Console Console().print(table) except Exception: for r in rows: print(" ".join(str((c.extractor(r) or "")) for c in cols)) except Exception as exc: log(f"Rendering failed: {exc}", file=sys.stderr) return 1 # Determine selection indices indices: List[int] = [] if select_raw: try: indices = _parse_selection(str(select_raw), len(rows)) except ValueError as exc: log(str(exc), file=sys.stderr) return 1 elif interactive: # Prompt user (single index only unless multi) try: from rich.prompt import Prompt prompt_text = "Select item(s) (e.g., 1 or 1,3-5)" if not allow_multi: prompt_text += " (single)" choice = Prompt.ask(prompt_text).strip() indices = _parse_selection(choice, len(rows)) except Exception as exc: log(f"Interactive selection failed: {exc}", file=sys.stderr) return 1 else: log("No selection requested. Use -select or -interactive.", file=sys.stderr) return 1 if not indices: log("No valid selection indices provided", file=sys.stderr) return 1 # Build selected items and emit selected_items: List[Dict[str, Any]] = [] for idx in indices: try: raw = inputs[idx] if idx < len(inputs) else None if isinstance(raw, dict): selected = dict(raw) elif isinstance(raw, ResultModel): selected = { "title": raw.title, "path": raw.path, "ext": raw.ext, "size_bytes": raw.size_bytes, "metadata": raw.metadata or {}, "source": raw.source, } else: try: selected = raw.to_dict() except Exception: selected = {"title": getattr(raw, "title", str(raw))} # Ensure selection args exist using provider's selector only if not selected.get("_selection_args"): try: sel_args = provider.selection_args(rows[idx]) selected["_selection_args"] = sel_args except Exception: log("Selection args missing and provider selector failed.", file=sys.stderr) return 1 selected_items.append(selected) except Exception: continue # Emit selected items so downstream cmdlets can consume them try: for itm in selected_items: ctx.emit(itm) except Exception: pass # Optionally run follow-up cmdlet for each selected item if run_cmd: try: from cmdlet import ensure_cmdlet_modules_loaded, get as get_cmdlet ensure_cmdlet_modules_loaded() cmd_fn = get_cmdlet(run_cmd) if not cmd_fn: log(f"Follow-up cmdlet not found: {run_cmd}", file=sys.stderr) return 1 exit_code = 0 for itm in selected_items: sel_args = itm.get("_selection_args") or [] # Invoke follow-up cmdlet with the selected item as piped input try: ret = cmd_fn(itm, sel_args, config or {}) except Exception as exc: log(f"Follow-up cmdlet raised: {exc}", file=sys.stderr) ret = 1 if ret != 0: exit_code = ret return exit_code except Exception as exc: log(f"Failed to invoke follow-up cmdlet: {exc}", file=sys.stderr) return 1 return 0 CMDLET.exec = _run CMDLET.register()