from __future__ import annotations import os import re import sys from pathlib import Path from typing import Any, Dict, Sequence, Optional from cmdlet._shared import Cmdlet, CmdletArg from SYS.logger import log from SYS import pipeline as ctx CMDLET = Cmdlet( name=".out-table", summary="Save the current result table to an SVG file.", usage='.out-table -path "C:\\Path\\To\\Dir"', arg=[ CmdletArg( "path", type="string", description="Directory (or file path) to write the SVG to", required=True, ), ], detail=[ "Exports the most recent table (overlay/stage/last) as an SVG using Rich.", "Default filename is derived from the table title (sanitized).", "Examples:", 'search-store "ext:mp3" | .out-table -path "C:\\Users\\Admin\\Desktop"', 'search-store "ext:mp3" | .out-table -path "C:\\Users\\Admin\\Desktop\\my-table.svg"', ], ) _WINDOWS_RESERVED_NAMES = { "con", "prn", "aux", "nul", *(f"com{i}" for i in range(1, 10)), *(f"lpt{i}" for i in range(1, 10)), } def _sanitize_filename_base(text: str) -> str: """Sanitize a string for use as a Windows-friendly filename (no extension).""" s = str(text or "").strip() if not s: return "table" # Replace characters illegal on Windows (and generally unsafe cross-platform). s = re.sub(r'[<>:"/\\|?*]', " ", s) # Drop control characters. s = "".join(ch for ch in s if ch.isprintable()) # Collapse whitespace. s = " ".join(s.split()).strip() # Windows disallows trailing space/dot. s = s.rstrip(" .") if not s: s = "table" # Avoid reserved device names. if s.lower() in _WINDOWS_RESERVED_NAMES: s = f"_{s}" # Keep it reasonably short. if len(s) > 200: s = s[:200].rstrip(" .") return s or "table" def _resolve_output_path(path_arg: str, *, table_title: str) -> Path: raw = str(path_arg or "").strip() if not raw: raise ValueError("-path is required") # Treat trailing slash as directory intent even if it doesn't exist yet. ends_with_sep = raw.endswith((os.sep, os.altsep or "")) target = Path(raw) if target.exists() and target.is_dir(): base = _sanitize_filename_base(table_title) return target / f"{base}.svg" if ends_with_sep and not target.suffix: target.mkdir(parents=True, exist_ok=True) base = _sanitize_filename_base(table_title) return target / f"{base}.svg" # File path intent. if not target.suffix: return target.with_suffix(".svg") if target.suffix.lower() != ".svg": return target.with_suffix(".svg") return target def _get_active_table(piped_result: Any) -> Optional[Any]: # Prefer an explicit ResultTable passed through the pipe, but normally `.out-table` # is used after `@` which pipes item selections (not the table itself). if piped_result is not None and hasattr(piped_result, "__rich__"): # Avoid mistakenly treating a dict/list as a renderable. if piped_result.__class__.__name__ == "ResultTable": return piped_result return ctx.get_display_table() or ctx.get_current_stage_table( ) or ctx.get_last_result_table() def _run(piped_result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: args_list = [str(a) for a in (args or [])] # Simple flag parsing: `.out-table -path ` path_arg: Optional[str] = None i = 0 while i < len(args_list): low = args_list[i].strip().lower() if low in {"-path", "--path"} and i + 1 < len(args_list): path_arg = args_list[i + 1] i += 2 continue if not args_list[i].startswith("-") and path_arg is None: # Allow `.out-table ` as a convenience. path_arg = args_list[i] i += 1 if not path_arg: log("Missing required -path", file=sys.stderr) return 1 table = _get_active_table(piped_result) if table is None: log("No table available to export", file=sys.stderr) return 1 title = getattr(table, "title", None) title_text = str(title or "table") try: out_path = _resolve_output_path(path_arg, table_title=title_text) out_path.parent.mkdir(parents=True, exist_ok=True) from rich.console import Console console = Console(record=True) console.print(table) console.save_svg(str(out_path)) log(f"Saved table SVG: {out_path}") return 0 except Exception as exc: log(f"Failed to save table SVG: {type(exc).__name__}: {exc}", file=sys.stderr) return 1 CMDLET.exec = _run