This commit is contained in:
nose
2025-12-11 12:47:30 -08:00
parent 6b05dc5552
commit 65d12411a2
92 changed files with 17447 additions and 14308 deletions

View File

@@ -25,21 +25,18 @@ from models import PipelineStageContext
from helper.logger import log
def _is_selectable_table(table: Any) -> bool:
"""Return True when a table can be used for @ selection."""
return bool(table) and not getattr(table, "no_choice", False)
# ============================================================================
# PIPELINE GLOBALS (maintained for backward compatibility)
# PIPELINE STATE
# ============================================================================
# Current pipeline context (thread-local in real world, global here for simplicity)
# Current pipeline context
_CURRENT_CONTEXT: Optional[PipelineStageContext] = None
# Active execution state
_PIPE_EMITS: List[Any] = []
_PIPE_ACTIVE: bool = False
_PIPE_IS_LAST: bool = False
# Ephemeral handoff for direct pipelines (e.g., URL --screen-shot | ...)
_LAST_PIPELINE_CAPTURE: Optional[Any] = None
# Remember last search query to support refreshing results after pipeline actions
_LAST_SEARCH_QUERY: Optional[str] = None
@@ -52,25 +49,23 @@ _PIPELINE_LAST_ITEMS: List[Any] = []
# Store the last result table for @ selection syntax (e.g., @2, @2-5, @{1,3,5})
_LAST_RESULT_TABLE: Optional[Any] = None
_LAST_RESULT_ITEMS: List[Any] = []
# Subject for the current result table (e.g., the file whose tags/URLs are displayed)
# Subject for the current result table (e.g., the file whose tags/url are displayed)
_LAST_RESULT_SUBJECT: Optional[Any] = None
# History of result tables for @.. navigation (LIFO stack, max 20 tables)
_RESULT_TABLE_HISTORY: List[tuple[Optional[Any], List[Any], Optional[Any]]] = []
_MAX_RESULT_TABLE_HISTORY = 20
# Forward history for @,, navigation (LIFO stack for popped tables)
_RESULT_TABLE_FORWARD: List[tuple[Optional[Any], List[Any], Optional[Any]]] = []
# Current stage table for @N expansion (separate from history)
# Used to track the ResultTable with source_command + row_selection_args from current pipeline stage
# This is set by cmdlets that display tabular results (e.g., download-data showing formats)
# and used by CLI to expand @N into full commands like "download-data URL -item 2"
_CURRENT_STAGE_TABLE: Optional[Any] = None
# Items displayed by non-selectable commands (get-tag, delete-tag, etc.)
# These are available for @N selection but NOT saved to history
_DISPLAY_ITEMS: List[Any] = []
# Table for display-only commands (overlay)
# Used when a command wants to show a specific table formatting but not affect history
_DISPLAY_TABLE: Optional[Any] = None
# Subject for overlay/display-only tables (takes precedence over _LAST_RESULT_SUBJECT)
_DISPLAY_SUBJECT: Optional[Any] = None
@@ -98,7 +93,7 @@ _UI_LIBRARY_REFRESH_CALLBACK: Optional[Any] = None
# ============================================================================
def set_stage_context(context: Optional[PipelineStageContext]) -> None:
"""Internal: Set the current pipeline stage context."""
"""Set the current pipeline stage context."""
global _CURRENT_CONTEXT
_CURRENT_CONTEXT = context
@@ -126,26 +121,21 @@ def emit(obj: Any) -> None:
return 0
```
"""
# Try new context-based approach first
if _CURRENT_CONTEXT is not None:
import logging
logger = logging.getLogger(__name__)
logger.debug(f"[EMIT] Context-based: appending to _CURRENT_CONTEXT.emits. obj={obj}")
_CURRENT_CONTEXT.emit(obj)
return
def emit_list(objects: List[Any]) -> None:
"""Emit a list of objects to the next pipeline stage.
# Fallback to legacy global approach (for backward compatibility)
try:
import logging
logger = logging.getLogger(__name__)
logger.debug(f"[EMIT] Legacy: appending to _PIPE_EMITS. obj type={type(obj).__name__}, _PIPE_EMITS len before={len(_PIPE_EMITS)}")
_PIPE_EMITS.append(obj)
logger.debug(f"[EMIT] Legacy: _PIPE_EMITS len after={len(_PIPE_EMITS)}")
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"[EMIT] Error appending to _PIPE_EMITS: {e}", exc_info=True)
pass
This allows cmdlets to emit multiple results that are tracked as a list,
enabling downstream cmdlets to process all of them or filter by metadata.
Args:
objects: List of objects to emit
"""
if _CURRENT_CONTEXT is not None:
_CURRENT_CONTEXT.emit(objects)
def print_if_visible(*args: Any, file=None, **kwargs: Any) -> None:
@@ -171,7 +161,7 @@ def print_if_visible(*args: Any, file=None, **kwargs: Any) -> None:
"""
try:
# Print if: not in a pipeline OR this is the last stage
should_print = (not _PIPE_ACTIVE) or _PIPE_IS_LAST
should_print = (_CURRENT_CONTEXT is None) or (_CURRENT_CONTEXT and _CURRENT_CONTEXT.is_last_stage)
# Always print to stderr regardless
if file is not None:
@@ -304,17 +294,17 @@ def clear_pending_pipeline_tail() -> None:
_PENDING_PIPELINE_SOURCE = None
def reset() -> None:
"""Reset all pipeline state. Called between pipeline executions."""
global _PIPE_EMITS, _PIPE_ACTIVE, _PIPE_IS_LAST, _PIPELINE_VALUES
global _LAST_PIPELINE_CAPTURE, _PIPELINE_REFRESHED, _PIPELINE_LAST_ITEMS
global _PIPELINE_COMMAND_TEXT, _LAST_RESULT_SUBJECT, _DISPLAY_SUBJECT
global _PENDING_PIPELINE_TAIL, _PENDING_PIPELINE_SOURCE
global _PIPELINE_VALUES, _LAST_SEARCH_QUERY, _PIPELINE_REFRESHED
global _PIPELINE_LAST_ITEMS, _PIPELINE_COMMAND_TEXT, _LAST_RESULT_SUBJECT
global _DISPLAY_SUBJECT, _PENDING_PIPELINE_TAIL, _PENDING_PIPELINE_SOURCE
global _CURRENT_CONTEXT
_PIPE_EMITS = []
_PIPE_ACTIVE = False
_PIPE_IS_LAST = False
_LAST_PIPELINE_CAPTURE = None
_CURRENT_CONTEXT = None
_LAST_SEARCH_QUERY = None
_PIPELINE_REFRESHED = False
_PIPELINE_LAST_ITEMS = []
_PIPELINE_VALUES = {}
@@ -327,13 +317,15 @@ def reset() -> None:
def get_emitted_items() -> List[Any]:
"""Get a copy of all items emitted by the current pipeline stage."""
return list(_PIPE_EMITS)
if _CURRENT_CONTEXT is not None:
return list(_CURRENT_CONTEXT.emits)
return []
def clear_emits() -> None:
"""Clear the emitted items list (called between stages)."""
global _PIPE_EMITS
_PIPE_EMITS = []
if _CURRENT_CONTEXT is not None:
_CURRENT_CONTEXT.emits.clear()
def set_last_selection(indices: Sequence[int]) -> None:
@@ -375,20 +367,8 @@ def clear_current_command_text() -> None:
_PIPELINE_COMMAND_TEXT = ""
def set_active(active: bool) -> None:
"""Internal: Set whether we're in a pipeline context."""
global _PIPE_ACTIVE
_PIPE_ACTIVE = active
def set_last_stage(is_last: bool) -> None:
"""Internal: Set whether this is the last stage of the pipeline."""
global _PIPE_IS_LAST
_PIPE_IS_LAST = is_last
def set_search_query(query: Optional[str]) -> None:
"""Internal: Set the last search query for refresh purposes."""
"""Set the last search query for refresh purposes."""
global _LAST_SEARCH_QUERY
_LAST_SEARCH_QUERY = query
@@ -399,7 +379,7 @@ def get_search_query() -> Optional[str]:
def set_pipeline_refreshed(refreshed: bool) -> None:
"""Internal: Track whether the pipeline already refreshed results."""
"""Track whether the pipeline already refreshed results."""
global _PIPELINE_REFRESHED
_PIPELINE_REFRESHED = refreshed
@@ -410,7 +390,7 @@ def was_pipeline_refreshed() -> bool:
def set_last_items(items: list) -> None:
"""Internal: Cache the last pipeline outputs."""
"""Cache the last pipeline outputs."""
global _PIPELINE_LAST_ITEMS
_PIPELINE_LAST_ITEMS = list(items) if items else []
@@ -420,17 +400,6 @@ def get_last_items() -> List[Any]:
return list(_PIPELINE_LAST_ITEMS)
def set_last_capture(obj: Any) -> None:
"""Internal: Store ephemeral handoff for direct pipelines."""
global _LAST_PIPELINE_CAPTURE
_LAST_PIPELINE_CAPTURE = obj
def get_last_capture() -> Optional[Any]:
"""Get ephemeral pipeline handoff (e.g., URL --screen-shot | ...)."""
return _LAST_PIPELINE_CAPTURE
def set_ui_library_refresh_callback(callback: Any) -> None:
"""Set a callback to be called when library content is updated.
@@ -501,6 +470,22 @@ def set_last_result_table(result_table: Optional[Any], items: Optional[List[Any]
_LAST_RESULT_TABLE = result_table
_LAST_RESULT_ITEMS = items or []
_LAST_RESULT_SUBJECT = subject
# Sort table by Title/Name column alphabetically if available
if result_table is not None and hasattr(result_table, 'sort_by_title') and not getattr(result_table, 'preserve_order', False):
try:
result_table.sort_by_title()
# Re-order items list to match the sorted table
if _LAST_RESULT_ITEMS and hasattr(result_table, 'rows'):
sorted_items = []
for row in result_table.rows:
src_idx = getattr(row, 'source_index', None)
if isinstance(src_idx, int) and 0 <= src_idx < len(_LAST_RESULT_ITEMS):
sorted_items.append(_LAST_RESULT_ITEMS[src_idx])
if len(sorted_items) == len(result_table.rows):
_LAST_RESULT_ITEMS = sorted_items
except Exception:
pass
def set_last_result_table_overlay(result_table: Optional[Any], items: Optional[List[Any]] = None, subject: Optional[Any] = None) -> None:
@@ -518,6 +503,22 @@ def set_last_result_table_overlay(result_table: Optional[Any], items: Optional[L
_DISPLAY_TABLE = result_table
_DISPLAY_ITEMS = items or []
_DISPLAY_SUBJECT = subject
# Sort table by Title/Name column alphabetically if available
if result_table is not None and hasattr(result_table, 'sort_by_title') and not getattr(result_table, 'preserve_order', False):
try:
result_table.sort_by_title()
# Re-order items list to match the sorted table
if _DISPLAY_ITEMS and hasattr(result_table, 'rows'):
sorted_items = []
for row in result_table.rows:
src_idx = getattr(row, 'source_index', None)
if isinstance(src_idx, int) and 0 <= src_idx < len(_DISPLAY_ITEMS):
sorted_items.append(_DISPLAY_ITEMS[src_idx])
if len(sorted_items) == len(result_table.rows):
_DISPLAY_ITEMS = sorted_items
except Exception:
pass
def set_last_result_table_preserve_history(result_table: Optional[Any], items: Optional[List[Any]] = None, subject: Optional[Any] = None) -> None:
@@ -567,7 +568,7 @@ def restore_previous_result_table() -> bool:
True if a previous table was restored, False if history is empty
"""
global _LAST_RESULT_TABLE, _LAST_RESULT_ITEMS, _LAST_RESULT_SUBJECT
global _RESULT_TABLE_HISTORY, _DISPLAY_ITEMS, _DISPLAY_TABLE, _DISPLAY_SUBJECT
global _RESULT_TABLE_HISTORY, _RESULT_TABLE_FORWARD, _DISPLAY_ITEMS, _DISPLAY_TABLE, _DISPLAY_SUBJECT
# If we have an active overlay (display items/table), clear it to "go back" to the underlying table
if _DISPLAY_ITEMS or _DISPLAY_TABLE or _DISPLAY_SUBJECT is not None:
@@ -579,6 +580,9 @@ def restore_previous_result_table() -> bool:
if not _RESULT_TABLE_HISTORY:
return False
# Save current state to forward stack before popping
_RESULT_TABLE_FORWARD.append((_LAST_RESULT_TABLE, _LAST_RESULT_ITEMS, _LAST_RESULT_SUBJECT))
# Pop from history and restore
prev = _RESULT_TABLE_HISTORY.pop()
if isinstance(prev, tuple) and len(prev) >= 3:
@@ -595,6 +599,44 @@ def restore_previous_result_table() -> bool:
return True
def restore_next_result_table() -> bool:
"""Restore the next result table from forward history (for @,, navigation).
Returns:
True if a next table was restored, False if forward history is empty
"""
global _LAST_RESULT_TABLE, _LAST_RESULT_ITEMS, _LAST_RESULT_SUBJECT
global _RESULT_TABLE_HISTORY, _RESULT_TABLE_FORWARD, _DISPLAY_ITEMS, _DISPLAY_TABLE, _DISPLAY_SUBJECT
# If we have an active overlay (display items/table), clear it to "go forward" to the underlying table
if _DISPLAY_ITEMS or _DISPLAY_TABLE or _DISPLAY_SUBJECT is not None:
_DISPLAY_ITEMS = []
_DISPLAY_TABLE = None
_DISPLAY_SUBJECT = None
return True
if not _RESULT_TABLE_FORWARD:
return False
# Save current state to history stack before popping forward
_RESULT_TABLE_HISTORY.append((_LAST_RESULT_TABLE, _LAST_RESULT_ITEMS, _LAST_RESULT_SUBJECT))
# Pop from forward stack and restore
next_state = _RESULT_TABLE_FORWARD.pop()
if isinstance(next_state, tuple) and len(next_state) >= 3:
_LAST_RESULT_TABLE, _LAST_RESULT_ITEMS, _LAST_RESULT_SUBJECT = next_state[0], next_state[1], next_state[2]
elif isinstance(next_state, tuple) and len(next_state) == 2:
_LAST_RESULT_TABLE, _LAST_RESULT_ITEMS = next_state
_LAST_RESULT_SUBJECT = None
else:
_LAST_RESULT_TABLE, _LAST_RESULT_ITEMS, _LAST_RESULT_SUBJECT = None, [], None
# Clear display items so get_last_result_items() falls back to restored items
_DISPLAY_ITEMS = []
_DISPLAY_TABLE = None
_DISPLAY_SUBJECT = None
return True
def get_display_table() -> Optional[Any]:
"""Get the current display overlay table.
@@ -637,9 +679,15 @@ def get_last_result_items() -> List[Any]:
# Prioritize items from display commands (get-tag, delete-tag, etc.)
# These are available for immediate @N selection
if _DISPLAY_ITEMS:
if _DISPLAY_TABLE is not None and not _is_selectable_table(_DISPLAY_TABLE):
return []
return _DISPLAY_ITEMS
# Fall back to items from last search/selectable command
return _LAST_RESULT_ITEMS
if _LAST_RESULT_TABLE is None:
return _LAST_RESULT_ITEMS
if _is_selectable_table(_LAST_RESULT_TABLE):
return _LAST_RESULT_ITEMS
return []
def get_last_result_table_source_command() -> Optional[str]:
@@ -648,7 +696,7 @@ def get_last_result_table_source_command() -> Optional[str]:
Returns:
Command name (e.g., 'download-data') or None if not set
"""
if _LAST_RESULT_TABLE and hasattr(_LAST_RESULT_TABLE, 'source_command'):
if _is_selectable_table(_LAST_RESULT_TABLE) and hasattr(_LAST_RESULT_TABLE, 'source_command'):
return _LAST_RESULT_TABLE.source_command
return None
@@ -659,7 +707,7 @@ def get_last_result_table_source_args() -> List[str]:
Returns:
List of arguments (e.g., ['https://example.com']) or empty list
"""
if _LAST_RESULT_TABLE and hasattr(_LAST_RESULT_TABLE, 'source_args'):
if _is_selectable_table(_LAST_RESULT_TABLE) and hasattr(_LAST_RESULT_TABLE, 'source_args'):
return _LAST_RESULT_TABLE.source_args or []
return []
@@ -673,7 +721,7 @@ def get_last_result_table_row_selection_args(row_index: int) -> Optional[List[st
Returns:
Selection arguments (e.g., ['-item', '3']) or None
"""
if _LAST_RESULT_TABLE and hasattr(_LAST_RESULT_TABLE, 'rows'):
if _is_selectable_table(_LAST_RESULT_TABLE) and hasattr(_LAST_RESULT_TABLE, 'rows'):
if 0 <= row_index < len(_LAST_RESULT_TABLE.rows):
row = _LAST_RESULT_TABLE.rows[row_index]
if hasattr(row, 'selection_args'):
@@ -696,13 +744,18 @@ def set_current_stage_table(result_table: Optional[Any]) -> None:
_CURRENT_STAGE_TABLE = result_table
def get_current_stage_table() -> Optional[Any]:
"""Get the current pipeline stage table (if any)."""
return _CURRENT_STAGE_TABLE
def get_current_stage_table_source_command() -> Optional[str]:
"""Get the source command from the current pipeline stage table.
Returns:
Command name (e.g., 'download-data') or None
"""
if _CURRENT_STAGE_TABLE and hasattr(_CURRENT_STAGE_TABLE, 'source_command'):
if _is_selectable_table(_CURRENT_STAGE_TABLE) and hasattr(_CURRENT_STAGE_TABLE, 'source_command'):
return _CURRENT_STAGE_TABLE.source_command
return None
@@ -713,7 +766,7 @@ def get_current_stage_table_source_args() -> List[str]:
Returns:
List of arguments or empty list
"""
if _CURRENT_STAGE_TABLE and hasattr(_CURRENT_STAGE_TABLE, 'source_args'):
if _is_selectable_table(_CURRENT_STAGE_TABLE) and hasattr(_CURRENT_STAGE_TABLE, 'source_args'):
return _CURRENT_STAGE_TABLE.source_args or []
return []
@@ -727,7 +780,7 @@ def get_current_stage_table_row_selection_args(row_index: int) -> Optional[List[
Returns:
Selection arguments or None
"""
if _CURRENT_STAGE_TABLE and hasattr(_CURRENT_STAGE_TABLE, 'rows'):
if _is_selectable_table(_CURRENT_STAGE_TABLE) and hasattr(_CURRENT_STAGE_TABLE, 'rows'):
if 0 <= row_index < len(_CURRENT_STAGE_TABLE.rows):
row = _CURRENT_STAGE_TABLE.rows[row_index]
if hasattr(row, 'selection_args'):
@@ -735,23 +788,21 @@ def get_current_stage_table_row_selection_args(row_index: int) -> Optional[List[
return None
def get_current_stage_table_row_source_index(row_index: int) -> Optional[int]:
"""Get the original source index for a row in the current stage table.
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).
"""
if _is_selectable_table(_CURRENT_STAGE_TABLE) and hasattr(_CURRENT_STAGE_TABLE, 'rows'):
if 0 <= row_index < len(_CURRENT_STAGE_TABLE.rows):
row = _CURRENT_STAGE_TABLE.rows[row_index]
return getattr(row, 'source_index', None)
return None
def clear_last_result() -> None:
"""Clear the stored last result table and items."""
global _LAST_RESULT_TABLE, _LAST_RESULT_ITEMS
_LAST_RESULT_TABLE = None
_LAST_RESULT_ITEMS = []
def emit_list(objects: List[Any]) -> None:
"""Emit a list of PipeObjects to the next pipeline stage.
This allows cmdlets to emit multiple results that are tracked as a list,
enabling downstream cmdlets to process all of them or filter by metadata.
Args:
objects: List of PipeObject instances or dicts to emit
"""
if _CURRENT_CONTEXT is not None:
_CURRENT_CONTEXT.emit(objects)
else:
_PIPE_EMITS.append(objects)