updated panel display

This commit is contained in:
2026-04-16 17:18:50 -07:00
parent 97e310be70
commit 343a7b37a0
14 changed files with 711 additions and 264 deletions
+6
View File
@@ -51,6 +51,12 @@ def global_config() -> List[Dict[str, Any]]:
"label": "Auto-Update",
"default": "true",
"choices": ["true", "false"]
},
{
"key": "table_appearance",
"label": "Table Appearance",
"default": "rainbow",
"choices": ["plain", "bw-striped", "rainbow"]
}
]
+116 -26
View File
@@ -5,7 +5,7 @@ import inspect
import logging
import threading
from pathlib import Path
from typing import Optional
from typing import Any, Optional, Sequence
from SYS.rich_display import console_for
@@ -43,6 +43,106 @@ def is_debug_enabled() -> bool:
return _DEBUG_ENABLED
def _debug_output_suppressed() -> bool:
try:
stderr_name = getattr(sys.stderr, "name", "")
return "nul" in str(stderr_name).lower() or "/dev/null" in str(stderr_name)
except Exception:
return False
def _debug_output_file(file=None):
stream = get_thread_stream()
if stream is not None:
return stream
if file is not None:
return file
return sys.stderr
def _is_rich_renderable(value: Any) -> bool:
if value is None:
return False
if isinstance(value, (str, bytes, bytearray)):
return False
return bool(
hasattr(value, "__rich_console__")
or hasattr(value, "__rich__")
or value.__class__.__module__.startswith("rich.")
)
def _caller_location(depth: int = 1) -> tuple[str, str]:
frame = inspect.currentframe()
current = frame
try:
for _ in range(max(0, int(depth))):
if current is None:
break
current = current.f_back
if current is None:
return "", ""
return Path(current.f_code.co_filename).stem, current.f_code.co_name
finally:
del frame
def _debug_db_log(*, caller_name: str, message: str, level: str = "DEBUG") -> None:
if not _DB_LOGGER:
return
try:
_DB_LOGGER(level, caller_name, message)
except Exception:
pass
def debug_panel(
title: str,
rows: Sequence[tuple[str, Any]],
*,
file=None,
border_style: str = "cyan",
) -> None:
"""Render a compact key/value debug panel when debug logging is enabled."""
if not _DEBUG_ENABLED or _debug_output_suppressed():
return
target_file = _debug_output_file(file)
try:
from rich.panel import Panel
from rich.table import Table as RichTable
grid = RichTable.grid(padding=(0, 1))
grid.add_column("Key", style="cyan", no_wrap=True)
grid.add_column("Value")
for key, val in rows:
try:
grid.add_row(str(key), str(val))
except Exception:
grid.add_row(str(key), "<unprintable>")
debug(
Panel(
grid,
title=str(title or "Debug"),
expand=False,
border_style=border_style,
),
file=target_file,
)
file_name, func_name = _caller_location(depth=2)
caller_name = f"{file_name}.{func_name}" if file_name and func_name else ""
_debug_db_log(
caller_name=caller_name,
message=f"[{title}] " + "; ".join(f"{k}={v}" for k, v in rows),
)
except Exception:
debug(title, list(rows), file=target_file)
def debug(*args, **kwargs) -> None:
"""Print debug message if debug logging is enabled.
@@ -53,26 +153,24 @@ def debug(*args, **kwargs) -> None:
# Check if stderr has been redirected to /dev/null (quiet mode)
# If so, skip output to avoid queuing in background worker's capture
try:
stderr_name = getattr(sys.stderr, "name", "")
if "nul" in str(stderr_name).lower() or "/dev/null" in str(stderr_name):
return
except Exception:
pass
if _debug_output_suppressed():
return
# Check for thread-local stream first
stream = get_thread_stream()
if stream:
kwargs["file"] = stream
# Set default to stderr for debug messages
elif "file" not in kwargs:
kwargs["file"] = sys.stderr
target_file = _debug_output_file(kwargs.pop("file", None))
if len(args) == 1 and _is_rich_renderable(args[0]):
renderable = args[0]
console_for(target_file).print(renderable)
file_name, func_name = _caller_location(depth=1)
caller_name = f"{file_name}.{func_name}" if file_name and func_name else ""
_debug_db_log(caller_name=caller_name, message=f"<rich:{type(renderable).__name__}>")
return
# Prepend DEBUG label
args = ("DEBUG:", *args)
# Use the same logic as log()
log(*args, **kwargs)
log(*args, file=target_file, **kwargs)
def debug_inspect(
@@ -97,19 +195,11 @@ def debug_inspect(
return
# Mirror debug() quiet-mode guard.
try:
stderr_name = getattr(sys.stderr, "name", "")
if "nul" in str(stderr_name).lower() or "/dev/null" in str(stderr_name):
return
except Exception:
pass
if _debug_output_suppressed():
return
# Resolve destination stream.
stream = get_thread_stream()
if stream is not None:
file = stream
elif file is None:
file = sys.stderr
file = _debug_output_file(file)
# Compute caller prefix (same as log()).
prefix = None
+76 -17
View File
@@ -10,7 +10,7 @@ from dataclasses import dataclass, field
from contextvars import ContextVar
from typing import Any, Dict, List, Optional, Sequence, Callable
from SYS.models import PipelineStageContext
from SYS.logger import log, debug, is_debug_enabled
from SYS.logger import log, debug, debug_panel, is_debug_enabled
import logging
logger = logging.getLogger(__name__)
from SYS.worker import WorkerManagerRegistry, WorkerStages
@@ -28,6 +28,47 @@ HELP_EXAMPLE_SOURCE_COMMANDS = {
}
def _emit_selection_debug_panel(
*,
selection_token: Any,
selection_indices: Sequence[int],
item_count: int,
filtered_count: int,
stage_table_present: bool,
display_table_present: bool,
stage_is_last: bool,
row_action: Optional[Sequence[Any]] = None,
downstream_stages: Optional[Sequence[Sequence[Any]]] = None,
mode: Optional[str] = None,
) -> None:
if not is_debug_enabled():
return
try:
rows: List[tuple[str, Any]] = [
("selection", str(selection_token or "")),
("indices", [int(idx) + 1 for idx in (selection_indices or [])]),
("items", int(item_count)),
("filtered", int(filtered_count)),
("stage_table", bool(stage_table_present)),
("display_table", bool(display_table_present)),
("stage_is_last", bool(stage_is_last)),
("downstream_stages", len(list(downstream_stages or []))),
]
if mode:
rows.insert(1, ("mode", str(mode)))
if row_action:
rows.append(("row_action", " ".join(str(part) for part in row_action if part is not None)))
debug_panel(
f"Selection replay {selection_token}",
rows,
border_style="magenta",
)
except Exception:
pass
def set_live_progress(progress_ui: Any) -> None:
"""Register the current Live progress UI so cmdlets can suspend it during prompts."""
state = _get_pipeline_state()
@@ -1883,6 +1924,7 @@ class PipelineExecutor:
selected_row_args: List[str] = []
skip_pipe_expansion = source_cmd in {".pipe", ".mpv"} and len(stages) > 0
prefer_row_action = False
preferred_row_action = None
if len(selection_indices) == 1 and not stages:
try:
row_action = _get_row_action(selection_indices[0])
@@ -1890,10 +1932,7 @@ class PipelineExecutor:
row_action = None
if row_action:
prefer_row_action = True
debug(
"@N: skipping source command expansion because row has explicit selection_action "
f"{row_action}"
)
preferred_row_action = list(row_action)
# Command expansion via @N:
# - Default behavior: expand ONLY for single-row selections.
# - Special case: allow multi-row expansion for add-file directory tables by
@@ -1978,7 +2017,7 @@ class PipelineExecutor:
except Exception:
logger.exception("Failed to record pipeline log step for @N expansion (pipeline_session=%r)", getattr(pipeline_session, 'worker_id', None))
elif selected_row_args and stages:
debug("@N: skipping source command expansion because downstream stages exist")
pass
stage_table = None
try:
@@ -2003,8 +2042,6 @@ class PipelineExecutor:
except Exception:
stage_table = None
debug(f"@N: stage_table={stage_table is not None}, display_table={display_table is not None}")
# ====================================================================
# PHASE 4: Retrieve and filter items from current result set
# ====================================================================
@@ -2015,14 +2052,31 @@ class PipelineExecutor:
except Exception as exc:
debug(f"@N: Exception getting items_list: {exc}")
items_list = []
debug(f"@N: selection_indices={selection_indices}, items_list length={len(items_list)}")
resolved_items = items_list if items_list else []
if items_list:
filtered = [
resolved_items[i] for i in selection_indices
if 0 <= i < len(resolved_items)
]
if selection_indices:
if len(selection_indices) == 1:
selection_label = f"@{selection_indices[0] + 1}"
else:
selection_label = "@{" + ",".join(str(idx + 1) for idx in selection_indices) + "}"
else:
selection_label = "@selection"
_emit_selection_debug_panel(
selection_token=selection_label,
selection_indices=selection_indices,
item_count=len(items_list),
filtered_count=len(filtered),
stage_table_present=(stage_table is not None),
display_table_present=(display_table is not None),
stage_is_last=(not stages),
row_action=preferred_row_action,
downstream_stages=stages,
mode=("row_action" if preferred_row_action else "selection"),
)
if not filtered:
print("No items matched selection in pipeline\n")
return False, None
@@ -2088,15 +2142,12 @@ class PipelineExecutor:
filtered = track_items
table_type_hint = "tidal.track"
debug(f"@N: calling _maybe_run_class_selector with filtered={len(filtered)} items, stage_is_last={not stages}")
if PipelineExecutor._maybe_run_class_selector(
ctx,
config,
filtered,
stage_is_last=(not stages)):
debug(f"@N: _maybe_run_class_selector returned True, returning False")
return False, None
debug(f"@N: _maybe_run_class_selector returned False, continuing")
from SYS.pipe_object import coerce_to_pipe_object
@@ -2105,7 +2156,6 @@ class PipelineExecutor:
filtered_pipe_objs
if len(filtered_pipe_objs) > 1 else filtered_pipe_objs[0]
)
debug(f"@N: coerced piped_result, stages={stages}")
if pipeline_session and worker_manager:
try:
@@ -2204,7 +2254,6 @@ class PipelineExecutor:
print("Auto-applying metadata selection via get-tag")
stages.append(["get-tag"])
elif auto_stage:
debug(f"@N: Found auto_stage={auto_stage}, appending")
try:
print(f"Auto-running selection via {auto_stage[0]}")
except Exception:
@@ -2238,7 +2287,6 @@ class PipelineExecutor:
if not stages and selection_indices and len(selection_indices) == 1:
row_action = _get_row_action(selection_indices[0], items_list)
if row_action:
debug(f"@N: applying row_action {row_action}")
stages.append(row_action)
if pipeline_session and worker_manager:
try:
@@ -2464,7 +2512,18 @@ class PipelineExecutor:
ctx = sys.modules[__name__]
try:
debug(f"execute_tokens: tokens={tokens}")
try:
from SYS.logger import debug_panel
debug_panel(
"Pipeline execution",
[
("command", " ".join(str(tok) for tok in tokens)),
("token_count", len(tokens)),
],
)
except Exception:
debug(f"execute_tokens: tokens={tokens}")
self._try_clear_pipeline_stop(ctx)
# REPL guard: stage-local tables should not persist across independent
+116 -19
View File
@@ -16,6 +16,7 @@ from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Callable, Set
from pathlib import Path
import json
import re
from rich.box import SIMPLE
from rich.console import Group
@@ -113,14 +114,97 @@ _RESULT_TABLE_ROW_STYLE_LOOP: List[tuple[str, str]] = [
("#ffffff", "#000000"),
]
_RESULT_TABLE_BW_ROW_STYLE_LOOP: List[tuple[str, str]] = [
("#000000", "#ffffff"),
("#ffffff", "#000000"),
]
RESULT_TABLE_HEADER_STYLE = "bold #000000 on #ffffff"
RESULT_TABLE_BORDER_STYLE = "#000000 on #ffffff"
RESULT_TABLE_PLAIN_HEADER_STYLE = "bold"
_RESULT_TABLE_APPEARANCE_ALIASES: Dict[str, str] = {
"": "rainbow",
"rainbow": "rainbow",
"default": "rainbow",
"plain": "plain",
"none": "plain",
"bw": "bw-striped",
"b-w": "bw-striped",
"b-w-striped": "bw-striped",
"bw-striped": "bw-striped",
"b-w-stripes": "bw-striped",
"bw-stripes": "bw-striped",
"black-white": "bw-striped",
"black-white-striped": "bw-striped",
"black-white-stripes": "bw-striped",
"black-and-white": "bw-striped",
"black-and-white-striped": "bw-striped",
"black-and-white-stripes": "bw-striped",
}
def get_result_table_row_style(row_index: int) -> str:
text_color, bg_color = _RESULT_TABLE_ROW_STYLE_LOOP[
row_index % len(_RESULT_TABLE_ROW_STYLE_LOOP)
]
def normalize_result_table_appearance_mode(value: Any) -> str:
text = str(value or "").strip().lower()
if not text:
return "rainbow"
collapsed = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
return _RESULT_TABLE_APPEARANCE_ALIASES.get(collapsed, "rainbow")
def get_result_table_appearance_mode(config: Optional[Dict[str, Any]] = None) -> str:
cfg = config
if cfg is None:
try:
from SYS.config import load_config
cfg = load_config() or {}
except Exception:
cfg = {}
raw_value = None
if isinstance(cfg, dict):
raw_value = cfg.get("table_appearance")
return normalize_result_table_appearance_mode(raw_value)
def get_result_table_header_style(config: Optional[Dict[str, Any]] = None) -> str:
mode = get_result_table_appearance_mode(config)
if mode == "plain":
return RESULT_TABLE_PLAIN_HEADER_STYLE
return RESULT_TABLE_HEADER_STYLE
def get_result_table_border_style(config: Optional[Dict[str, Any]] = None) -> str:
mode = get_result_table_appearance_mode(config)
if mode == "plain":
return ""
return RESULT_TABLE_BORDER_STYLE
def get_result_table_panel_style(config: Optional[Dict[str, Any]] = None) -> str:
mode = get_result_table_appearance_mode(config)
if mode == "plain":
return ""
return "on #ffffff"
def get_result_table_row_style(
row_index: int,
appearance_mode: Optional[str] = None,
config: Optional[Dict[str, Any]] = None,
) -> str:
mode = appearance_mode or get_result_table_appearance_mode(config)
if mode == "plain":
return ""
style_loop = (
_RESULT_TABLE_BW_ROW_STYLE_LOOP
if mode == "bw-striped" else _RESULT_TABLE_ROW_STYLE_LOOP
)
text_color, bg_color = style_loop[row_index % len(style_loop)]
return f"{text_color} on {bg_color}"
@@ -1435,16 +1519,21 @@ class Table:
def to_rich(self):
"""Return a Rich renderable representing this table."""
appearance_mode = get_result_table_appearance_mode()
header_style = get_result_table_header_style({"table_appearance": appearance_mode})
border_style = get_result_table_border_style({"table_appearance": appearance_mode})
panel_style = get_result_table_panel_style({"table_appearance": appearance_mode})
if not self.rows:
empty = Text("No results")
return (
Panel(
empty,
title=Text(str(self.title), style=RESULT_TABLE_HEADER_STYLE),
border_style=RESULT_TABLE_BORDER_STYLE,
title=Text(str(self.title), style=header_style),
border_style=border_style,
padding=(0, 0),
expand=False,
style="on #ffffff",
style=panel_style,
)
if self.title
else empty
@@ -1460,8 +1549,8 @@ class Table:
table = RichTable(
show_header=True,
header_style=RESULT_TABLE_HEADER_STYLE,
border_style=RESULT_TABLE_BORDER_STYLE,
header_style=header_style,
border_style=border_style,
box=None,
expand=False,
show_lines=False,
@@ -1497,7 +1586,13 @@ class Table:
for name in col_names:
val = row.get_column(name) or ""
cells.append(self._apply_value_case(_sanitize_cell_text(val)))
table.add_row(*cells, style=get_result_table_row_style(row_idx - 1))
table.add_row(
*cells,
style=get_result_table_row_style(
row_idx - 1,
appearance_mode=appearance_mode,
),
)
if self.title or self.header_lines:
header_bits = [Text(line) for line in (self.header_lines or [])]
@@ -1505,11 +1600,11 @@ class Table:
return (
Panel(
renderable,
title=Text(str(self.title), style=RESULT_TABLE_HEADER_STYLE),
border_style=RESULT_TABLE_BORDER_STYLE,
title=Text(str(self.title), style=header_style),
border_style=border_style,
padding=(0, 0),
expand=False,
style="on #ffffff",
style=panel_style,
)
if self.title
else renderable
@@ -2385,11 +2480,13 @@ class ItemDetailView(Table):
elements = []
if has_details:
header_style = get_result_table_header_style()
border_style = get_result_table_border_style()
detail_title = str(self.detail_title or "Item Details").strip() or "Item Details"
elements.append(Panel(
details_table,
title=Text(detail_title, style=RESULT_TABLE_HEADER_STYLE),
border_style=RESULT_TABLE_BORDER_STYLE,
title=Text(detail_title, style=header_style),
border_style=border_style,
padding=(1, 2)
))
@@ -2397,11 +2494,11 @@ class ItemDetailView(Table):
# If it's a Panel already (from super().to_rich() with title), use it directly
# but force the border style to the result-table standard for consistency
if isinstance(results_renderable, Panel):
results_renderable.border_style = RESULT_TABLE_BORDER_STYLE
results_renderable.border_style = get_result_table_border_style()
if results_renderable.title:
results_renderable.title = Text(
str(results_renderable.title),
style=RESULT_TABLE_HEADER_STYLE,
style=get_result_table_header_style(),
)
# Add a bit of padding inside if it contains a table
elements.append(results_renderable)
@@ -2416,8 +2513,8 @@ class ItemDetailView(Table):
elements.append(
Panel(
results_group,
title=Text(str(display_title), style=RESULT_TABLE_HEADER_STYLE),
border_style=RESULT_TABLE_BORDER_STYLE,
title=Text(str(display_title), style=get_result_table_header_style()),
border_style=get_result_table_border_style(),
)
)
+13 -5
View File
@@ -13,9 +13,10 @@ import logging
logger = logging.getLogger(__name__)
from SYS.result_table import (
RESULT_TABLE_BORDER_STYLE,
RESULT_TABLE_HEADER_STYLE,
apply_result_table_layout,
get_result_table_appearance_mode,
get_result_table_border_style,
get_result_table_header_style,
get_result_table_row_style,
)
from SYS.result_table_api import ColumnSpec, ResultModel, ResultTable, Renderer
@@ -38,8 +39,8 @@ class RichRenderer(Renderer):
table = RichTable(
show_header=True,
header_style=RESULT_TABLE_HEADER_STYLE,
border_style=RESULT_TABLE_BORDER_STYLE,
header_style=get_result_table_header_style(),
border_style=get_result_table_border_style(),
box=None,
padding=(0, 1),
pad_edge=False,
@@ -47,6 +48,7 @@ class RichRenderer(Renderer):
expand=False,
)
apply_result_table_layout(table)
appearance_mode = get_result_table_appearance_mode()
cols = list(columns)
for col in cols:
if str(col.header or "").strip().lower() == "tag":
@@ -71,7 +73,13 @@ class RichRenderer(Renderer):
logger.exception("Column extractor failed for '%s': %s", col.header, exc)
cell = ""
cells.append(cell)
table.add_row(*cells, style=get_result_table_row_style(row_idx))
table.add_row(
*cells,
style=get_result_table_row_style(
row_idx,
appearance_mode=appearance_mode,
),
)
return table