dfd
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
This commit is contained in:
168
cmdnat/out_table.py
Normal file
168
cmdnat/out_table.py
Normal file
@@ -0,0 +1,168 @@
|
||||
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
|
||||
Reference in New Issue
Block a user