2025-11-25 20:09:33 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Pipeline execution context and state management for cmdlet.
|
|
|
|
|
"""
|
2025-11-25 20:09:33 -08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import sys
|
2025-12-20 23:57:44 -08:00
|
|
|
import shlex
|
2025-12-21 05:10:09 -08:00
|
|
|
from contextlib import contextmanager
|
2025-12-30 01:33:45 -08:00
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from contextvars import ContextVar
|
2025-11-25 20:09:33 -08:00
|
|
|
from typing import Any, Dict, List, Optional, Sequence
|
2025-12-29 23:28:15 -08:00
|
|
|
from SYS.models import PipelineStageContext
|
2026-01-03 21:23:55 -08:00
|
|
|
from SYS.logger import log, debug, is_debug_enabled
|
2025-11-25 20:09:33 -08:00
|
|
|
|
2025-12-21 05:10:09 -08:00
|
|
|
|
|
|
|
|
def set_live_progress(progress_ui: Any) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Register the current Live progress UI so cmdlets can suspend it during prompts."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.live_progress = progress_ui
|
2025-12-21 05:10:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_live_progress() -> Any:
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.live_progress
|
2025-12-21 05:10:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
|
def suspend_live_progress():
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Temporarily pause Live progress rendering.
|
|
|
|
|
|
|
|
|
|
This avoids Rich Live cursor control interfering with interactive tables/prompts
|
|
|
|
|
emitted by cmdlets during preflight (e.g. URL-duplicate confirmation).
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
ui = get_live_progress()
|
2025-12-29 17:05:03 -08:00
|
|
|
paused = False
|
|
|
|
|
try:
|
|
|
|
|
if ui is not None and hasattr(ui, "pause"):
|
|
|
|
|
try:
|
|
|
|
|
ui.pause()
|
|
|
|
|
paused = True
|
|
|
|
|
except Exception:
|
|
|
|
|
paused = False
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
|
|
|
|
# If a stage requested the pipeline stop (e.g. user declined a preflight prompt),
|
|
|
|
|
# do not resume Live rendering.
|
|
|
|
|
if get_pipeline_stop() is not None:
|
|
|
|
|
return
|
|
|
|
|
if paused and ui is not None and hasattr(ui, "resume"):
|
|
|
|
|
try:
|
|
|
|
|
ui.resume()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2025-12-21 05:10:09 -08:00
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
|
2025-12-11 12:47:30 -08:00
|
|
|
def _is_selectable_table(table: Any) -> bool:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Return True when a table can be used for @ selection."""
|
2025-12-30 04:47:13 -08:00
|
|
|
# Avoid relying on truthiness for selectability.
|
|
|
|
|
# `ResultTable` can be falsey when it has 0 rows, but `@` selection/filtering
|
|
|
|
|
# should still be allowed when the backing `last_result_items` exist.
|
|
|
|
|
return table is not None and not getattr(table, "no_choice", False)
|
2025-12-11 12:47:30 -08:00
|
|
|
|
|
|
|
|
|
2025-12-30 01:33:45 -08:00
|
|
|
# Pipeline state container (prototype)
|
|
|
|
|
@dataclass
|
|
|
|
|
class PipelineState:
|
|
|
|
|
current_context: Optional[PipelineStageContext] = None
|
|
|
|
|
last_search_query: Optional[str] = None
|
|
|
|
|
pipeline_refreshed: bool = False
|
|
|
|
|
last_items: List[Any] = field(default_factory=list)
|
|
|
|
|
last_result_table: Optional[Any] = None
|
|
|
|
|
last_result_items: List[Any] = field(default_factory=list)
|
|
|
|
|
last_result_subject: Optional[Any] = None
|
|
|
|
|
result_table_history: List[tuple[Optional[Any], List[Any], Optional[Any]]] = field(default_factory=list)
|
|
|
|
|
result_table_forward: List[tuple[Optional[Any], List[Any], Optional[Any]]] = field(default_factory=list)
|
|
|
|
|
current_stage_table: Optional[Any] = None
|
|
|
|
|
display_items: List[Any] = field(default_factory=list)
|
|
|
|
|
display_table: Optional[Any] = None
|
|
|
|
|
display_subject: Optional[Any] = None
|
|
|
|
|
last_selection: List[int] = field(default_factory=list)
|
|
|
|
|
pipeline_command_text: str = ""
|
|
|
|
|
current_cmdlet_name: str = ""
|
|
|
|
|
current_stage_text: str = ""
|
|
|
|
|
pipeline_values: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
pending_pipeline_tail: List[List[str]] = field(default_factory=list)
|
|
|
|
|
pending_pipeline_source: Optional[str] = None
|
|
|
|
|
ui_library_refresh_callback: Optional[Any] = None
|
|
|
|
|
pipeline_stop: Optional[Dict[str, Any]] = None
|
|
|
|
|
live_progress: Any = None
|
|
|
|
|
|
|
|
|
|
def reset(self) -> None:
|
|
|
|
|
self.current_context = None
|
|
|
|
|
self.last_search_query = None
|
|
|
|
|
self.pipeline_refreshed = False
|
|
|
|
|
self.last_items = []
|
|
|
|
|
self.last_result_table = None
|
|
|
|
|
self.last_result_items = []
|
|
|
|
|
self.last_result_subject = None
|
|
|
|
|
self.result_table_history = []
|
|
|
|
|
self.result_table_forward = []
|
|
|
|
|
self.current_stage_table = None
|
|
|
|
|
self.display_items = []
|
|
|
|
|
self.display_table = None
|
|
|
|
|
self.display_subject = None
|
|
|
|
|
self.last_selection = []
|
|
|
|
|
self.pipeline_command_text = ""
|
|
|
|
|
self.current_cmdlet_name = ""
|
|
|
|
|
self.current_stage_text = ""
|
|
|
|
|
self.pipeline_values = {}
|
|
|
|
|
self.pending_pipeline_tail = []
|
|
|
|
|
self.pending_pipeline_source = None
|
|
|
|
|
self.ui_library_refresh_callback = None
|
|
|
|
|
self.pipeline_stop = None
|
|
|
|
|
self.live_progress = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ContextVar for per-run state (prototype)
|
|
|
|
|
_CTX_STATE: ContextVar[Optional[PipelineState]] = ContextVar("_pipeline_state", default=None)
|
|
|
|
|
_GLOBAL_STATE: PipelineState = PipelineState()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_pipeline_state() -> PipelineState:
|
|
|
|
|
"""Return the PipelineState for the current context or the global fallback."""
|
|
|
|
|
state = _CTX_STATE.get()
|
|
|
|
|
return state if state is not None else _GLOBAL_STATE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
|
def new_pipeline_state():
|
|
|
|
|
"""Context manager to use a fresh PipelineState for a run."""
|
|
|
|
|
token = _CTX_STATE.set(PipelineState())
|
|
|
|
|
try:
|
|
|
|
|
yield _CTX_STATE.get()
|
|
|
|
|
finally:
|
|
|
|
|
_CTX_STATE.reset(token)
|
|
|
|
|
|
|
|
|
|
|
2025-12-30 02:20:31 -08:00
|
|
|
# Legacy module-level synchronization removed — module-level pipeline globals are no longer maintained.
|
|
|
|
|
# Use `get_pipeline_state()` to access or mutate the per-run PipelineState.
|
2025-12-30 01:33:45 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Public accessors for pipeline state (for external callers that need to inspect
|
|
|
|
|
# or mutate the PipelineState directly). These provide stable, non-underscored
|
|
|
|
|
# entrypoints so other modules don't rely on implementation-internal names.
|
|
|
|
|
def get_pipeline_state() -> PipelineState:
|
|
|
|
|
"""Return the active PipelineState for the current context or the global fallback."""
|
|
|
|
|
return _get_pipeline_state()
|
|
|
|
|
|
|
|
|
|
|
2025-12-20 23:57:44 -08:00
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
|
2025-12-30 02:20:31 -08:00
|
|
|
# No module-level pipeline runtime variables; per-run pipeline state is stored in PipelineState (use `get_pipeline_state()`).
|
|
|
|
|
MAX_RESULT_TABLE_HISTORY = 20
|
|
|
|
|
PIPELINE_MISSING = object()
|
2025-12-21 05:10:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def request_pipeline_stop(*, reason: str = "", exit_code: int = 0) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Request that the pipeline runner stop gracefully after the current stage."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.pipeline_stop = {
|
2025-12-29 18:42:02 -08:00
|
|
|
"reason": str(reason or "").strip(),
|
|
|
|
|
"exit_code": int(exit_code)
|
|
|
|
|
}
|
2025-12-21 05:10:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_pipeline_stop() -> Optional[Dict[str, Any]]:
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.pipeline_stop
|
2025-12-21 05:10:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_pipeline_stop() -> None:
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.pipeline_stop = None
|
2025-12-21 05:10:09 -08:00
|
|
|
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
# ============================================================================
|
|
|
|
|
# PUBLIC API
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
def set_stage_context(context: Optional[PipelineStageContext]) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Set the current pipeline stage context."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.current_context = context
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_stage_context() -> Optional[PipelineStageContext]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the current pipeline stage context."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.current_context
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def emit(obj: Any) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Emit an object to the current pipeline stage output.
|
|
|
|
|
"""
|
|
|
|
|
ctx = _get_pipeline_state().current_context
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
ctx.emit(obj)
|
2025-12-11 12:47:30 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def emit_list(objects: List[Any]) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Emit a list of objects to the next pipeline stage.
|
|
|
|
|
"""
|
|
|
|
|
ctx = _get_pipeline_state().current_context
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
ctx.emit(objects)
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_if_visible(*args: Any, file=None, **kwargs: Any) -> None:
|
2025-12-30 01:33:45 -08:00
|
|
|
"""
|
|
|
|
|
Print only if this is not a quiet mid-pipeline stage.
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Print if: not in a pipeline OR this is the last stage
|
2025-12-30 01:33:45 -08:00
|
|
|
ctx = _get_pipeline_state().current_context
|
|
|
|
|
should_print = (ctx is None) or (ctx and ctx.is_last_stage)
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# Always print to stderr regardless
|
|
|
|
|
if file is not None:
|
|
|
|
|
should_print = True
|
|
|
|
|
|
|
|
|
|
if should_print:
|
|
|
|
|
log(*args, **kwargs) if file is None else log(*args, file=file, **kwargs)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def store_value(key: str, value: Any) -> None:
|
2025-12-30 01:33:45 -08:00
|
|
|
"""
|
|
|
|
|
Store a value to pass to later pipeline stages.
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
|
|
|
|
if not isinstance(key, str):
|
|
|
|
|
return
|
|
|
|
|
text = key.strip().lower()
|
|
|
|
|
if not text:
|
|
|
|
|
return
|
|
|
|
|
try:
|
2025-12-30 02:20:31 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.pipeline_values[text] = value
|
2025-12-29 17:05:03 -08:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_value(key: str, default: Any = None) -> Any:
|
2025-12-30 01:33:45 -08:00
|
|
|
"""
|
|
|
|
|
Retrieve a value stored by an earlier pipeline stage.
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
|
|
|
|
if not isinstance(key, str):
|
|
|
|
|
return default
|
|
|
|
|
text = key.strip()
|
|
|
|
|
if not text:
|
|
|
|
|
return default
|
|
|
|
|
parts = [segment.strip() for segment in text.split(".") if segment.strip()]
|
|
|
|
|
if not parts:
|
|
|
|
|
return default
|
|
|
|
|
root_key = parts[0].lower()
|
2025-12-30 02:20:31 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
container = state.pipeline_values.get(root_key, PIPELINE_MISSING)
|
|
|
|
|
if container is PIPELINE_MISSING:
|
2025-12-29 17:05:03 -08:00
|
|
|
return default
|
|
|
|
|
if len(parts) == 1:
|
|
|
|
|
return container
|
2025-12-30 02:20:31 -08:00
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
current: Any = container
|
|
|
|
|
for fragment in parts[1:]:
|
|
|
|
|
if isinstance(current, dict):
|
|
|
|
|
fragment_lower = fragment.lower()
|
|
|
|
|
if fragment in current:
|
|
|
|
|
current = current[fragment]
|
|
|
|
|
continue
|
2025-12-30 02:20:31 -08:00
|
|
|
match = PIPELINE_MISSING
|
2025-12-29 17:05:03 -08:00
|
|
|
for key_name, value in current.items():
|
|
|
|
|
if isinstance(key_name, str) and key_name.lower() == fragment_lower:
|
|
|
|
|
match = value
|
|
|
|
|
break
|
2025-12-30 02:20:31 -08:00
|
|
|
if match is PIPELINE_MISSING:
|
2025-12-29 17:05:03 -08:00
|
|
|
return default
|
|
|
|
|
current = match
|
|
|
|
|
continue
|
|
|
|
|
if isinstance(current, (list, tuple)):
|
|
|
|
|
if fragment.isdigit():
|
|
|
|
|
try:
|
|
|
|
|
idx = int(fragment)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return default
|
|
|
|
|
if 0 <= idx < len(current):
|
|
|
|
|
current = current[idx]
|
|
|
|
|
continue
|
|
|
|
|
return default
|
|
|
|
|
if hasattr(current, fragment):
|
|
|
|
|
try:
|
|
|
|
|
current = getattr(current, fragment)
|
|
|
|
|
continue
|
|
|
|
|
except Exception:
|
|
|
|
|
return default
|
|
|
|
|
return default
|
|
|
|
|
return current
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_pending_pipeline_tail(
|
2025-12-29 18:42:02 -08:00
|
|
|
stages: Optional[Sequence[Sequence[str]]],
|
|
|
|
|
source_command: Optional[str] = None
|
2025-12-29 17:05:03 -08:00
|
|
|
) -> None:
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Store the remaining pipeline stages when execution pauses for @N selection.
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
2025-12-29 17:05:03 -08:00
|
|
|
try:
|
|
|
|
|
pending: List[List[str]] = []
|
|
|
|
|
for stage in stages or []:
|
|
|
|
|
if isinstance(stage, (list, tuple)):
|
|
|
|
|
pending.append([str(token) for token in stage])
|
2025-12-30 01:33:45 -08:00
|
|
|
state.pending_pipeline_tail = pending
|
2025-12-29 17:05:03 -08:00
|
|
|
clean_source = (source_command or "").strip()
|
2025-12-30 01:33:45 -08:00
|
|
|
state.pending_pipeline_source = clean_source if clean_source else None
|
2025-12-29 17:05:03 -08:00
|
|
|
except Exception:
|
|
|
|
|
# Keep existing pending tail on failure
|
|
|
|
|
pass
|
2025-12-07 00:21:30 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_pending_pipeline_tail() -> List[List[str]]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get a copy of the pending pipeline tail (stages queued after selection)."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return [list(stage) for stage in state.pending_pipeline_tail]
|
2025-12-07 00:21:30 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_pending_pipeline_source() -> Optional[str]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the source command associated with the pending pipeline tail."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.pending_pipeline_source
|
2025-12-07 00:21:30 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_pending_pipeline_tail() -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Clear any stored pending pipeline tail."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.pending_pipeline_tail = []
|
|
|
|
|
state.pending_pipeline_source = None
|
2025-12-11 12:47:30 -08:00
|
|
|
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
def reset() -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Reset all pipeline state. Called between pipeline executions."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.reset()
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
def get_emitted_items() -> List[Any]:
|
2025-12-30 01:33:45 -08:00
|
|
|
"""
|
|
|
|
|
Get a copy of all items emitted by the current pipeline stage.
|
|
|
|
|
"""
|
2025-12-30 02:20:31 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
ctx = state.current_context
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
return list(ctx.emits)
|
2025-12-29 17:05:03 -08:00
|
|
|
return []
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_emits() -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Clear the emitted items list (called between stages)."""
|
2025-12-30 02:20:31 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
ctx = state.current_context
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
ctx.emits.clear()
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_last_selection(indices: Sequence[int]) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Record the indices selected via @ syntax for the next cmdlet.
|
2025-11-25 20:09:33 -08:00
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
Args:
|
|
|
|
|
indices: Iterable of 0-based indices captured from the REPL parser
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.last_selection = list(indices or [])
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_last_selection() -> List[int]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Return the indices selected via @ syntax for the current invocation."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return list(state.last_selection)
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_last_selection() -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Clear the cached selection indices after a cmdlet finishes."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.last_selection = []
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_current_command_text(command_text: Optional[str]) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Record the raw pipeline/command text for downstream consumers."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.pipeline_command_text = (command_text or "").strip()
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_command_text(default: str = "") -> str:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Return the last recorded command/pipeline text."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
text = state.pipeline_command_text.strip()
|
2025-12-29 17:05:03 -08:00
|
|
|
return text if text else default
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_current_command_text() -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Clear the cached command text after execution completes."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.pipeline_command_text = ""
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
2025-12-20 23:57:44 -08:00
|
|
|
def split_pipeline_text(pipeline_text: str) -> List[str]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Split a pipeline string on unquoted '|' characters.
|
|
|
|
|
|
|
|
|
|
Preserves original quoting/spacing within each returned stage segment.
|
|
|
|
|
"""
|
|
|
|
|
text = str(pipeline_text or "")
|
|
|
|
|
if not text:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
stages: List[str] = []
|
|
|
|
|
buf: List[str] = []
|
|
|
|
|
quote: Optional[str] = None
|
|
|
|
|
escape = False
|
|
|
|
|
|
|
|
|
|
for ch in text:
|
|
|
|
|
if escape:
|
|
|
|
|
buf.append(ch)
|
|
|
|
|
escape = False
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if ch == "\\" and quote is not None:
|
|
|
|
|
buf.append(ch)
|
|
|
|
|
escape = True
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if ch in ('"', "'"):
|
|
|
|
|
if quote is None:
|
|
|
|
|
quote = ch
|
|
|
|
|
elif quote == ch:
|
|
|
|
|
quote = None
|
|
|
|
|
buf.append(ch)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if ch == "|" and quote is None:
|
|
|
|
|
stages.append("".join(buf).strip())
|
|
|
|
|
buf = []
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
buf.append(ch)
|
|
|
|
|
|
|
|
|
|
tail = "".join(buf).strip()
|
|
|
|
|
if tail:
|
|
|
|
|
stages.append(tail)
|
|
|
|
|
return [s for s in stages if s]
|
2025-12-20 23:57:44 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_command_stages() -> List[str]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Return the raw stage segments for the current command text."""
|
|
|
|
|
return split_pipeline_text(get_current_command_text(""))
|
2025-12-20 23:57:44 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_current_stage_text(stage_text: Optional[str]) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Record the raw stage text currently being executed."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.current_stage_text = str(stage_text or "").strip()
|
2025-12-20 23:57:44 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_stage_text(default: str = "") -> str:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Return the raw stage text currently being executed."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
text = state.current_stage_text.strip()
|
2025-12-29 17:05:03 -08:00
|
|
|
return text if text else default
|
2025-12-20 23:57:44 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_current_stage_text() -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Clear the cached stage text after a stage completes."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.current_stage_text = ""
|
2025-12-20 23:57:44 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_current_cmdlet_name(cmdlet_name: Optional[str]) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Record the currently executing cmdlet name (stage-local)."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.current_cmdlet_name = str(cmdlet_name or "").strip()
|
2025-12-20 23:57:44 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_cmdlet_name(default: str = "") -> str:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Return the currently executing cmdlet name (stage-local)."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
text = state.current_cmdlet_name.strip()
|
2025-12-29 17:05:03 -08:00
|
|
|
return text if text else default
|
2025-12-20 23:57:44 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_current_cmdlet_name() -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Clear the cached cmdlet name after a stage completes."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.current_cmdlet_name = ""
|
2025-12-20 23:57:44 -08:00
|
|
|
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
def set_search_query(query: Optional[str]) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Set the last search query for refresh purposes."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.last_search_query = query
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_search_query() -> Optional[str]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the last search query."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.last_search_query
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_pipeline_refreshed(refreshed: bool) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Track whether the pipeline already refreshed results."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.pipeline_refreshed = refreshed
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def was_pipeline_refreshed() -> bool:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Check if the pipeline already refreshed results."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.pipeline_refreshed
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_last_items(items: list) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Cache the last pipeline outputs."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.last_items = list(items) if items else []
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
def get_last_items() -> List[Any]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the last pipeline outputs."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return list(state.last_items)
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_ui_library_refresh_callback(callback: Any) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Set a callback to be called when library content is updated.
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.ui_library_refresh_callback = callback
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_ui_library_refresh_callback() -> Optional[Any]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the current library refresh callback."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.ui_library_refresh_callback
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def trigger_ui_library_refresh(library_filter: str = "local") -> None:
|
|
|
|
|
"""Trigger a library refresh in the UI if callback is registered.
|
|
|
|
|
|
|
|
|
|
This should be called from cmdlet/funacts after content is added to library.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
library_filter: Which library to refresh ('local', 'hydrus', etc)
|
|
|
|
|
"""
|
|
|
|
|
callback = get_ui_library_refresh_callback()
|
|
|
|
|
if callback:
|
|
|
|
|
try:
|
|
|
|
|
callback(library_filter)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(
|
2025-12-29 18:42:02 -08:00
|
|
|
f"[trigger_ui_library_refresh] Error calling refresh callback: {e}",
|
|
|
|
|
file=sys.stderr
|
2025-12-29 17:05:03 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_last_result_table(
|
2025-12-29 18:42:02 -08:00
|
|
|
result_table: Optional[Any],
|
|
|
|
|
items: Optional[List[Any]] = None,
|
|
|
|
|
subject: Optional[Any] = None
|
2025-12-29 17:05:03 -08:00
|
|
|
) -> None:
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Store the last result table and items for @ selection syntax.
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# Push current table to history before replacing
|
2025-12-30 01:33:45 -08:00
|
|
|
if state.last_result_table is not None:
|
|
|
|
|
state.result_table_history.append(
|
|
|
|
|
(
|
|
|
|
|
state.last_result_table,
|
|
|
|
|
state.last_result_items.copy(),
|
|
|
|
|
state.last_result_subject,
|
|
|
|
|
)
|
2025-12-29 17:05:03 -08:00
|
|
|
)
|
|
|
|
|
# Keep history size limited
|
2025-12-30 02:20:31 -08:00
|
|
|
if len(state.result_table_history) > MAX_RESULT_TABLE_HISTORY:
|
2025-12-30 01:33:45 -08:00
|
|
|
state.result_table_history.pop(0)
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# Set new current table and clear any display items/table
|
2025-12-30 01:33:45 -08:00
|
|
|
state.display_items = []
|
|
|
|
|
state.display_table = None
|
|
|
|
|
state.display_subject = None
|
|
|
|
|
state.last_result_table = result_table
|
|
|
|
|
state.last_result_items = items or []
|
|
|
|
|
state.last_result_subject = subject
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# Sort table by Title/Name column alphabetically if available
|
2025-12-30 01:33:45 -08:00
|
|
|
if (
|
|
|
|
|
result_table is not None
|
|
|
|
|
and hasattr(result_table, "sort_by_title")
|
|
|
|
|
and not getattr(result_table, "preserve_order", False)
|
|
|
|
|
):
|
2025-12-29 17:05:03 -08:00
|
|
|
try:
|
|
|
|
|
result_table.sort_by_title()
|
|
|
|
|
# Re-order items list to match the sorted table
|
2025-12-30 01:33:45 -08:00
|
|
|
if state.last_result_items and hasattr(result_table, "rows"):
|
|
|
|
|
sorted_items: List[Any] = []
|
2025-12-29 17:05:03 -08:00
|
|
|
for row in result_table.rows:
|
|
|
|
|
src_idx = getattr(row, "source_index", None)
|
2025-12-30 01:33:45 -08:00
|
|
|
if isinstance(src_idx, int) and 0 <= src_idx < len(state.last_result_items):
|
|
|
|
|
sorted_items.append(state.last_result_items[src_idx])
|
2025-12-29 17:05:03 -08:00
|
|
|
if len(sorted_items) == len(result_table.rows):
|
2025-12-30 01:33:45 -08:00
|
|
|
state.last_result_items = sorted_items
|
2025-12-29 17:05:03 -08:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_last_result_table_overlay(
|
2025-12-29 18:42:02 -08:00
|
|
|
result_table: Optional[Any],
|
|
|
|
|
items: Optional[List[Any]] = None,
|
|
|
|
|
subject: Optional[Any] = None
|
2025-12-29 17:05:03 -08:00
|
|
|
) -> None:
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Set a result table as an overlay (display only, no history).
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
2025-12-29 17:05:03 -08:00
|
|
|
|
2025-12-30 01:33:45 -08:00
|
|
|
state.display_table = result_table
|
|
|
|
|
state.display_items = items or []
|
|
|
|
|
state.display_subject = subject
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# Sort table by Title/Name column alphabetically if available
|
2025-12-30 01:33:45 -08:00
|
|
|
if (
|
|
|
|
|
result_table is not None
|
|
|
|
|
and hasattr(result_table, "sort_by_title")
|
|
|
|
|
and not getattr(result_table, "preserve_order", False)
|
|
|
|
|
):
|
2025-12-29 17:05:03 -08:00
|
|
|
try:
|
|
|
|
|
result_table.sort_by_title()
|
|
|
|
|
# Re-order items list to match the sorted table
|
2025-12-30 01:33:45 -08:00
|
|
|
if state.display_items and hasattr(result_table, "rows"):
|
|
|
|
|
sorted_items: List[Any] = []
|
2025-12-29 17:05:03 -08:00
|
|
|
for row in result_table.rows:
|
|
|
|
|
src_idx = getattr(row, "source_index", None)
|
2025-12-30 01:33:45 -08:00
|
|
|
if isinstance(src_idx, int) and 0 <= src_idx < len(state.display_items):
|
|
|
|
|
sorted_items.append(state.display_items[src_idx])
|
2025-12-29 17:05:03 -08:00
|
|
|
if len(sorted_items) == len(result_table.rows):
|
2025-12-30 01:33:45 -08:00
|
|
|
state.display_items = sorted_items
|
2025-12-29 17:05:03 -08:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_last_result_table_preserve_history(
|
2025-12-29 18:42:02 -08:00
|
|
|
result_table: Optional[Any],
|
|
|
|
|
items: Optional[List[Any]] = None,
|
|
|
|
|
subject: Optional[Any] = None
|
2025-12-29 17:05:03 -08:00
|
|
|
) -> None:
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Update the last result table WITHOUT adding to history.
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# Update current table WITHOUT pushing to history
|
2025-12-30 01:33:45 -08:00
|
|
|
state.last_result_table = result_table
|
|
|
|
|
state.last_result_items = items or []
|
|
|
|
|
state.last_result_subject = subject
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-30 01:33:45 -08:00
|
|
|
def set_last_result_items_only(items: Optional[List[Any]]) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Store items for @N selection WITHOUT affecting history or saved search data.
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
2025-12-29 17:05:03 -08:00
|
|
|
|
2025-12-30 01:33:45 -08:00
|
|
|
# Store items for immediate @N selection, but DON'T modify last_result_items
|
2025-12-29 17:05:03 -08:00
|
|
|
# This ensures history contains original search data, not display transformations
|
2025-12-30 01:33:45 -08:00
|
|
|
state.display_items = items or []
|
2025-12-29 17:05:03 -08:00
|
|
|
# Clear display table since we're setting items only (CLI will generate table if needed)
|
2025-12-30 01:33:45 -08:00
|
|
|
state.display_table = None
|
|
|
|
|
state.display_subject = None
|
2025-11-25 20:09:33 -08:00
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
|
2025-12-30 01:33:45 -08:00
|
|
|
def restore_previous_result_table() -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Restore the previous result table from history (for @.. navigation).
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# If we have an active overlay (display items/table), clear it to "go back" to the underlying table
|
2025-12-30 01:33:45 -08:00
|
|
|
if state.display_items or state.display_table or state.display_subject is not None:
|
|
|
|
|
state.display_items = []
|
|
|
|
|
state.display_table = None
|
|
|
|
|
state.display_subject = None
|
2025-12-29 17:05:03 -08:00
|
|
|
# If an underlying table exists, we're done.
|
|
|
|
|
# Otherwise, fall through to history restore so @.. actually returns to the last table.
|
2025-12-30 01:33:45 -08:00
|
|
|
if state.last_result_table is not None:
|
2026-01-03 21:23:55 -08:00
|
|
|
# Ensure subsequent @N selection uses the table the user sees.
|
|
|
|
|
state.current_stage_table = state.last_result_table
|
2025-12-29 17:05:03 -08:00
|
|
|
return True
|
2025-12-30 01:33:45 -08:00
|
|
|
if not state.result_table_history:
|
2026-01-03 21:23:55 -08:00
|
|
|
state.current_stage_table = state.last_result_table
|
2025-12-29 17:05:03 -08:00
|
|
|
return True
|
|
|
|
|
|
2025-12-30 01:33:45 -08:00
|
|
|
if not state.result_table_history:
|
2025-12-29 17:05:03 -08:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Save current state to forward stack before popping
|
2025-12-30 01:33:45 -08:00
|
|
|
state.result_table_forward.append(
|
|
|
|
|
(state.last_result_table, state.last_result_items, state.last_result_subject)
|
2025-12-29 18:42:02 -08:00
|
|
|
)
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# Pop from history and restore
|
2025-12-30 01:33:45 -08:00
|
|
|
prev = state.result_table_history.pop()
|
2025-12-29 17:05:03 -08:00
|
|
|
if isinstance(prev, tuple) and len(prev) >= 3:
|
2025-12-30 01:33:45 -08:00
|
|
|
state.last_result_table, state.last_result_items, state.last_result_subject = prev[0], prev[1], prev[2]
|
2025-12-29 17:05:03 -08:00
|
|
|
elif isinstance(prev, tuple) and len(prev) == 2:
|
2025-12-30 01:33:45 -08:00
|
|
|
state.last_result_table, state.last_result_items = prev
|
|
|
|
|
state.last_result_subject = None
|
2025-12-29 17:05:03 -08:00
|
|
|
else:
|
2025-12-30 01:33:45 -08:00
|
|
|
state.last_result_table, state.last_result_items, state.last_result_subject = None, [], None
|
|
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
# Clear display items so get_last_result_items() falls back to restored items
|
2025-12-30 01:33:45 -08:00
|
|
|
state.display_items = []
|
|
|
|
|
state.display_table = None
|
|
|
|
|
state.display_subject = None
|
|
|
|
|
|
2026-01-03 21:23:55 -08:00
|
|
|
# Sync current stage table to the restored view so provider selectors run
|
|
|
|
|
# against the correct table type.
|
|
|
|
|
state.current_stage_table = state.last_result_table
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
debug_table_state("restore_previous_result_table")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
return True
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
2025-12-11 12:47:30 -08:00
|
|
|
def restore_next_result_table() -> bool:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Restore the next result table from forward history (for @,, navigation).
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# If we have an active overlay (display items/table), clear it to "go forward" to the underlying table
|
2025-12-30 01:33:45 -08:00
|
|
|
if state.display_items or state.display_table or state.display_subject is not None:
|
|
|
|
|
state.display_items = []
|
|
|
|
|
state.display_table = None
|
|
|
|
|
state.display_subject = None
|
2025-12-29 17:05:03 -08:00
|
|
|
# If an underlying table exists, we're done.
|
|
|
|
|
# Otherwise, fall through to forward restore when available.
|
2025-12-30 01:33:45 -08:00
|
|
|
if state.last_result_table is not None:
|
2026-01-03 21:23:55 -08:00
|
|
|
# Ensure subsequent @N selection uses the table the user sees.
|
|
|
|
|
state.current_stage_table = state.last_result_table
|
2025-12-29 17:05:03 -08:00
|
|
|
return True
|
2025-12-30 01:33:45 -08:00
|
|
|
if not state.result_table_forward:
|
2026-01-03 21:23:55 -08:00
|
|
|
state.current_stage_table = state.last_result_table
|
2025-12-29 17:05:03 -08:00
|
|
|
return True
|
|
|
|
|
|
2025-12-30 01:33:45 -08:00
|
|
|
if not state.result_table_forward:
|
2025-12-29 17:05:03 -08:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Save current state to history stack before popping forward
|
2025-12-30 01:33:45 -08:00
|
|
|
state.result_table_history.append(
|
|
|
|
|
(state.last_result_table, state.last_result_items, state.last_result_subject)
|
2025-12-29 18:42:02 -08:00
|
|
|
)
|
2025-12-29 17:05:03 -08:00
|
|
|
|
|
|
|
|
# Pop from forward stack and restore
|
2025-12-30 01:33:45 -08:00
|
|
|
next_state = state.result_table_forward.pop()
|
2025-12-29 17:05:03 -08:00
|
|
|
if isinstance(next_state, tuple) and len(next_state) >= 3:
|
2025-12-30 01:33:45 -08:00
|
|
|
state.last_result_table, state.last_result_items, state.last_result_subject = (
|
|
|
|
|
next_state[0], next_state[1], next_state[2]
|
2025-12-29 17:05:03 -08:00
|
|
|
)
|
|
|
|
|
elif isinstance(next_state, tuple) and len(next_state) == 2:
|
2025-12-30 01:33:45 -08:00
|
|
|
state.last_result_table, state.last_result_items = next_state
|
|
|
|
|
state.last_result_subject = None
|
2025-12-29 17:05:03 -08:00
|
|
|
else:
|
2025-12-30 01:33:45 -08:00
|
|
|
state.last_result_table, state.last_result_items, state.last_result_subject = None, [], None
|
|
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
# Clear display items so get_last_result_items() falls back to restored items
|
2025-12-30 01:33:45 -08:00
|
|
|
state.display_items = []
|
|
|
|
|
state.display_table = None
|
|
|
|
|
state.display_subject = None
|
|
|
|
|
|
2026-01-03 21:23:55 -08:00
|
|
|
# Sync current stage table to the restored view so provider selectors run
|
|
|
|
|
# against the correct table type.
|
|
|
|
|
state.current_stage_table = state.last_result_table
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
debug_table_state("restore_next_result_table")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
return True
|
2025-12-11 12:47:30 -08:00
|
|
|
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
def get_display_table() -> Optional[Any]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Get the current display overlay table.
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.display_table
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
2025-12-06 00:10:19 -08:00
|
|
|
def get_last_result_subject() -> Optional[Any]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Get the subject associated with the current result table or overlay.
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
if state.display_subject is not None:
|
|
|
|
|
return state.display_subject
|
|
|
|
|
return state.last_result_subject
|
2025-12-06 00:10:19 -08:00
|
|
|
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
def get_last_result_table() -> Optional[Any]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the current last result table.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The ResultTable object, or None if no table is set
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.last_result_table
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_last_result_items() -> List[Any]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
Get the items available for @N selection.
|
|
|
|
|
"""
|
|
|
|
|
state = _get_pipeline_state()
|
2025-12-29 17:05:03 -08:00
|
|
|
# Prioritize items from display commands (get-tag, delete-tag, etc.)
|
|
|
|
|
# These are available for immediate @N selection
|
2025-12-30 01:33:45 -08:00
|
|
|
if state.display_items:
|
|
|
|
|
if state.display_table is not None and not _is_selectable_table(state.display_table):
|
2025-12-29 17:05:03 -08:00
|
|
|
return []
|
2025-12-30 01:33:45 -08:00
|
|
|
return state.display_items
|
2025-12-29 17:05:03 -08:00
|
|
|
# Fall back to items from last search/selectable command
|
2025-12-30 01:33:45 -08:00
|
|
|
if state.last_result_table is None:
|
|
|
|
|
return state.last_result_items
|
|
|
|
|
if _is_selectable_table(state.last_result_table):
|
|
|
|
|
return state.last_result_items
|
2025-12-29 17:05:03 -08:00
|
|
|
return []
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
2026-01-03 21:23:55 -08:00
|
|
|
def debug_table_state(label: str = "") -> None:
|
|
|
|
|
"""Dump pipeline table and item-buffer state (debug-only).
|
|
|
|
|
|
|
|
|
|
Useful for diagnosing cases where `@N` selection appears to act on a different
|
|
|
|
|
table than the one currently displayed.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if not is_debug_enabled():
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
|
|
|
|
|
def _tbl(name: str, t: Any) -> None:
|
|
|
|
|
if t is None:
|
|
|
|
|
debug(f"[table] {name}: None")
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
table_type = getattr(t, "table", None)
|
|
|
|
|
except Exception:
|
|
|
|
|
table_type = None
|
|
|
|
|
try:
|
|
|
|
|
title = getattr(t, "title", None)
|
|
|
|
|
except Exception:
|
|
|
|
|
title = None
|
|
|
|
|
try:
|
|
|
|
|
src_cmd = getattr(t, "source_command", None)
|
|
|
|
|
except Exception:
|
|
|
|
|
src_cmd = None
|
|
|
|
|
try:
|
|
|
|
|
src_args = getattr(t, "source_args", None)
|
|
|
|
|
except Exception:
|
|
|
|
|
src_args = None
|
|
|
|
|
try:
|
|
|
|
|
no_choice = bool(getattr(t, "no_choice", False))
|
|
|
|
|
except Exception:
|
|
|
|
|
no_choice = False
|
|
|
|
|
try:
|
|
|
|
|
preserve_order = bool(getattr(t, "preserve_order", False))
|
|
|
|
|
except Exception:
|
|
|
|
|
preserve_order = False
|
|
|
|
|
try:
|
|
|
|
|
row_count = len(getattr(t, "rows", []) or [])
|
|
|
|
|
except Exception:
|
|
|
|
|
row_count = 0
|
|
|
|
|
try:
|
|
|
|
|
meta = (
|
|
|
|
|
t.get_table_metadata() if hasattr(t, "get_table_metadata") else getattr(t, "table_metadata", None)
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
meta = None
|
|
|
|
|
meta_keys = list(meta.keys()) if isinstance(meta, dict) else []
|
|
|
|
|
|
|
|
|
|
debug(
|
|
|
|
|
f"[table] {name}: id={id(t)} class={type(t).__name__} title={repr(title)} table={repr(table_type)} rows={row_count} "
|
|
|
|
|
f"source={repr(src_cmd)} source_args={repr(src_args)} no_choice={no_choice} preserve_order={preserve_order} meta_keys={meta_keys}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if label:
|
|
|
|
|
debug(f"[table] state: {label}")
|
|
|
|
|
_tbl("display_table", getattr(state, "display_table", None))
|
|
|
|
|
_tbl("current_stage_table", getattr(state, "current_stage_table", None))
|
|
|
|
|
_tbl("last_result_table", getattr(state, "last_result_table", None))
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
debug(
|
|
|
|
|
f"[table] buffers: display_items={len(state.display_items or [])} last_result_items={len(state.last_result_items or [])} "
|
|
|
|
|
f"history={len(state.result_table_history or [])} forward={len(state.result_table_forward or [])} last_selection={list(state.last_selection or [])}"
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2025-12-25 04:49:22 -08:00
|
|
|
def get_last_selectable_result_items() -> List[Any]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get items from the last *selectable* result table, ignoring display-only items.
|
2025-12-25 04:49:22 -08:00
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
This is useful when a selection stage should target the last visible selectable table
|
|
|
|
|
(e.g., a playlist/search table), even if a prior action command emitted items and
|
2025-12-30 01:33:45 -08:00
|
|
|
populated display_items.
|
2025-12-29 17:05:03 -08:00
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
if state.last_result_table is None:
|
|
|
|
|
return list(state.last_result_items)
|
|
|
|
|
if _is_selectable_table(state.last_result_table):
|
|
|
|
|
return list(state.last_result_items)
|
2025-12-29 17:05:03 -08:00
|
|
|
return []
|
2025-12-25 04:49:22 -08:00
|
|
|
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
def get_last_result_table_source_command() -> Optional[str]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the source command from the last displayed result table.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Command name (e.g., 'download-file') or None if not set
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
if _is_selectable_table(state.last_result_table) and hasattr(state.last_result_table, "source_command"):
|
|
|
|
|
return state.last_result_table.source_command
|
2025-12-29 17:05:03 -08:00
|
|
|
return None
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_last_result_table_source_args() -> List[str]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the base source arguments from the last displayed result table.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
List of arguments (e.g., ['https://example.com']) or empty list
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
if _is_selectable_table(state.last_result_table) and hasattr(state.last_result_table, "source_args"):
|
|
|
|
|
return state.last_result_table.source_args or []
|
2025-12-29 17:05:03 -08:00
|
|
|
return []
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_last_result_table_row_selection_args(row_index: int) -> Optional[List[str]]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the selection arguments for a specific row in the last result table.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
row_index: Index of the row (0-based)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Selection arguments (e.g., ['-item', '3']) or None
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
if _is_selectable_table(state.last_result_table) and hasattr(state.last_result_table, "rows"):
|
|
|
|
|
if 0 <= row_index < len(state.last_result_table.rows):
|
|
|
|
|
row = state.last_result_table.rows[row_index]
|
2025-12-29 17:05:03 -08:00
|
|
|
if hasattr(row, "selection_args"):
|
|
|
|
|
return row.selection_args
|
|
|
|
|
return None
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_current_stage_table(result_table: Optional[Any]) -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Store the current pipeline stage table for @N expansion.
|
|
|
|
|
|
|
|
|
|
Used by cmdlet that display tabular results (e.g., download-file listing formats)
|
|
|
|
|
to make their result table available for @N expansion logic.
|
|
|
|
|
|
|
|
|
|
Does NOT push to history - purely for command expansion in the current pipeline.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
result_table: The ResultTable object (or None to clear)
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.current_stage_table = result_table
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
|
2025-12-11 12:47:30 -08:00
|
|
|
def get_current_stage_table() -> Optional[Any]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the current pipeline stage table (if any)."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
return state.current_stage_table
|
2025-12-11 12:47:30 -08:00
|
|
|
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
def get_current_stage_table_source_command() -> Optional[str]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the source command from the current pipeline stage table.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Command name (e.g., 'download-file') or None
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
if _is_selectable_table(state.current_stage_table) and hasattr(state.current_stage_table, "source_command"):
|
|
|
|
|
return state.current_stage_table.source_command
|
2025-12-29 17:05:03 -08:00
|
|
|
return None
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_stage_table_source_args() -> List[str]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the source arguments from the current pipeline stage table.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
List of arguments or empty list
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
if _is_selectable_table(state.current_stage_table) and hasattr(state.current_stage_table, "source_args"):
|
|
|
|
|
return state.current_stage_table.source_args or []
|
2025-12-29 17:05:03 -08:00
|
|
|
return []
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_stage_table_row_selection_args(row_index: int) -> Optional[List[str]]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the selection arguments for a row in the current pipeline stage table.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
row_index: Index of the row (0-based)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Selection arguments or None
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
if _is_selectable_table(state.current_stage_table) and hasattr(state.current_stage_table, "rows"):
|
|
|
|
|
if 0 <= row_index < len(state.current_stage_table.rows):
|
|
|
|
|
row = state.current_stage_table.rows[row_index]
|
2025-12-29 17:05:03 -08:00
|
|
|
if hasattr(row, "selection_args"):
|
|
|
|
|
return row.selection_args
|
|
|
|
|
return None
|
2025-11-25 20:09:33 -08:00
|
|
|
|
|
|
|
|
|
2025-12-11 12:47:30 -08:00
|
|
|
def get_current_stage_table_row_source_index(row_index: int) -> Optional[int]:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Get the original source index for a row in the current stage table.
|
2025-12-11 12:47:30 -08:00
|
|
|
|
2025-12-29 17:05:03 -08:00
|
|
|
Useful when the table has been sorted for display but selections should map
|
|
|
|
|
back to the original item order (e.g., playlist or provider order).
|
|
|
|
|
"""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
if _is_selectable_table(state.current_stage_table) and hasattr(state.current_stage_table, "rows"):
|
|
|
|
|
if 0 <= row_index < len(state.current_stage_table.rows):
|
|
|
|
|
row = state.current_stage_table.rows[row_index]
|
2025-12-29 17:05:03 -08:00
|
|
|
return getattr(row, "source_index", None)
|
|
|
|
|
return None
|
2025-12-11 12:47:30 -08:00
|
|
|
|
|
|
|
|
|
2025-11-25 20:09:33 -08:00
|
|
|
def clear_last_result() -> None:
|
2025-12-29 17:05:03 -08:00
|
|
|
"""Clear the stored last result table and items."""
|
2025-12-30 01:33:45 -08:00
|
|
|
state = _get_pipeline_state()
|
|
|
|
|
state.last_result_table = None
|
|
|
|
|
state.last_result_items = []
|
|
|
|
|
state.last_result_subject = None
|