169 lines
4.7 KiB
Python
169 lines
4.7 KiB
Python
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
|
|
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 <value>`
|
|
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 <path>` 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
|