"""Pipeline execution context and state management for cmdlet. This module provides functions for managing pipeline state, allowing cmdlet to emit results and control printing behavior within a piped execution context. Key Concepts: - Pipeline stages are chained command invocations - Each stage receives input items and emits output items - Printing behavior is controlled based on pipeline position - Stage context tracks whether this is the last stage (affects output verbosity) PowerShell-like piping model: - Each stage processes items individually - Stage calls emit() for each output item - Output items become input for next stage - Batch commands receive all items at once (special case) """ from __future__ import annotations import sys from typing import Any, Dict, List, Optional, Sequence from models import PipelineStageContext from SYS.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 STATE # ============================================================================ # Current pipeline context _CURRENT_CONTEXT: Optional[PipelineStageContext] = None # Remember last search query to support refreshing results after pipeline actions _LAST_SEARCH_QUERY: Optional[str] = None # Track whether the last pipeline execution already refreshed and displayed results _PIPELINE_REFRESHED: bool = False # Cache the last pipeline outputs so non-interactive callers can inspect results _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/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) _CURRENT_STAGE_TABLE: Optional[Any] = None # Items displayed by non-selectable commands (get-tag, delete-tag, etc.) _DISPLAY_ITEMS: List[Any] = [] # Table for display-only commands (overlay) _DISPLAY_TABLE: Optional[Any] = None # Subject for overlay/display-only tables (takes precedence over _LAST_RESULT_SUBJECT) _DISPLAY_SUBJECT: Optional[Any] = None # Track the indices the user selected via @ syntax for the current invocation _PIPELINE_LAST_SELECTION: List[int] = [] # Track the currently executing command/pipeline string for worker attribution _PIPELINE_COMMAND_TEXT: str = "" # Shared scratchpad for cmdlet/funacts to stash structured data between stages _PIPELINE_VALUES: Dict[str, Any] = {} _PIPELINE_MISSING = object() # Preserve downstream pipeline stages when a command pauses for @N selection _PENDING_PIPELINE_TAIL: List[List[str]] = [] _PENDING_PIPELINE_SOURCE: Optional[str] = None # Global callback to notify UI when library content changes _UI_LIBRARY_REFRESH_CALLBACK: Optional[Any] = None # ============================================================================ # PUBLIC API # ============================================================================ def set_stage_context(context: Optional[PipelineStageContext]) -> None: """Set the current pipeline stage context.""" global _CURRENT_CONTEXT _CURRENT_CONTEXT = context def get_stage_context() -> Optional[PipelineStageContext]: """Get the current pipeline stage context.""" return _CURRENT_CONTEXT def emit(obj: Any) -> None: """Emit an object to the current pipeline stage output. Call this from a cmdlet to pass data to the next pipeline stage. If not in a pipeline context, this is a no-op. Args: obj: Any object to emit downstream Example: ```python def _run(item, args, config): result = process(item) if result: emit(result) # Pass to next stage return 0 ``` """ if _CURRENT_CONTEXT is not None: _CURRENT_CONTEXT.emit(obj) def emit_list(objects: List[Any]) -> None: """Emit a list of objects to the next pipeline stage. This allows cmdlet to emit multiple results that are tracked as a list, enabling downstream cmdlet 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: """Print only if this is not a quiet mid-pipeline stage. - Always allow errors printed to stderr by callers (they pass file=sys.stderr). - For normal info messages, this suppresses printing for intermediate pipeline stages. - Use this instead of log() in cmdlet when you want stage-aware output. Args: *args: Arguments to print (same as built-in print) file: Output stream (default: stdout) **kwargs: Keyword arguments for print Example: ```python # Always shows errors print_if_visible("[error] Something failed", file=sys.stderr) # Only shows in non-piped context or as final stage print_if_visible(f"Processed {count} items") ``` """ try: # Print if: not in a pipeline OR this is the last stage 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: should_print = True if should_print: log(*args, **kwargs) if file is None else log(*args, file=file, **kwargs) except Exception: pass def store_value(key: str, value: Any) -> None: """Store a value to pass to later pipeline stages. Values are stored in a shared dictionary keyed by normalized lowercase strings. This allows one stage to prepare data for the next stage without intermediate output. Args: key: Variable name (normalized to lowercase, non-empty) value: Any Python object to store """ if not isinstance(key, str): return text = key.strip().lower() if not text: return try: _PIPELINE_VALUES[text] = value except Exception: pass def load_value(key: str, default: Any = None) -> Any: """Retrieve a value stored by an earlier pipeline stage. Supports dotted path notation for nested access (e.g., "metadata.tag" or "items.0"). Args: key: Variable name or dotted path (e.g., "my_var", "metadata.title", "list.0") default: Value to return if key not found or access fails Returns: The stored value, or default if not found """ 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() container = _PIPELINE_VALUES.get(root_key, _PIPELINE_MISSING) if container is _PIPELINE_MISSING: return default if len(parts) == 1: return container current: Any = container for fragment in parts[1:]: if isinstance(current, dict): fragment_lower = fragment.lower() if fragment in current: current = current[fragment] continue match = _PIPELINE_MISSING for key_name, value in current.items(): if isinstance(key_name, str) and key_name.lower() == fragment_lower: match = value break if match is _PIPELINE_MISSING: 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(stages: Optional[Sequence[Sequence[str]]], source_command: Optional[str] = None) -> None: """Store the remaining pipeline stages when execution pauses for @N selection. Args: stages: Iterable of pipeline stage token lists source_command: Command that produced the selection table (for validation) """ global _PENDING_PIPELINE_TAIL, _PENDING_PIPELINE_SOURCE try: pending: List[List[str]] = [] for stage in stages or []: if isinstance(stage, (list, tuple)): pending.append([str(token) for token in stage]) _PENDING_PIPELINE_TAIL = pending clean_source = (source_command or "").strip() _PENDING_PIPELINE_SOURCE = clean_source if clean_source else None except Exception: # Keep existing pending tail on failure pass def get_pending_pipeline_tail() -> List[List[str]]: """Get a copy of the pending pipeline tail (stages queued after selection).""" return [list(stage) for stage in _PENDING_PIPELINE_TAIL] def get_pending_pipeline_source() -> Optional[str]: """Get the source command associated with the pending pipeline tail.""" return _PENDING_PIPELINE_SOURCE def clear_pending_pipeline_tail() -> None: """Clear any stored pending pipeline tail.""" global _PENDING_PIPELINE_TAIL, _PENDING_PIPELINE_SOURCE _PENDING_PIPELINE_TAIL = [] _PENDING_PIPELINE_SOURCE = None def reset() -> None: """Reset all pipeline state. Called between pipeline executions.""" 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 _CURRENT_CONTEXT = None _LAST_SEARCH_QUERY = None _PIPELINE_REFRESHED = False _PIPELINE_LAST_ITEMS = [] _PIPELINE_VALUES = {} _PIPELINE_COMMAND_TEXT = "" _LAST_RESULT_SUBJECT = None _DISPLAY_SUBJECT = None _PENDING_PIPELINE_TAIL = [] _PENDING_PIPELINE_SOURCE = None def get_emitted_items() -> List[Any]: """Get a copy of all items emitted by the current pipeline stage.""" if _CURRENT_CONTEXT is not None: return list(_CURRENT_CONTEXT.emits) return [] def clear_emits() -> None: """Clear the emitted items list (called between stages).""" if _CURRENT_CONTEXT is not None: _CURRENT_CONTEXT.emits.clear() def set_last_selection(indices: Sequence[int]) -> None: """Record the indices selected via @ syntax for the next cmdlet. Args: indices: Iterable of 0-based indices captured from the REPL parser """ global _PIPELINE_LAST_SELECTION _PIPELINE_LAST_SELECTION = list(indices or []) def get_last_selection() -> List[int]: """Return the indices selected via @ syntax for the current invocation.""" return list(_PIPELINE_LAST_SELECTION) def clear_last_selection() -> None: """Clear the cached selection indices after a cmdlet finishes.""" global _PIPELINE_LAST_SELECTION _PIPELINE_LAST_SELECTION = [] def set_current_command_text(command_text: Optional[str]) -> None: """Record the raw pipeline/command text for downstream consumers.""" global _PIPELINE_COMMAND_TEXT _PIPELINE_COMMAND_TEXT = (command_text or "").strip() def get_current_command_text(default: str = "") -> str: """Return the last recorded command/pipeline text.""" text = _PIPELINE_COMMAND_TEXT.strip() return text if text else default def clear_current_command_text() -> None: """Clear the cached command text after execution completes.""" global _PIPELINE_COMMAND_TEXT _PIPELINE_COMMAND_TEXT = "" def set_search_query(query: Optional[str]) -> None: """Set the last search query for refresh purposes.""" global _LAST_SEARCH_QUERY _LAST_SEARCH_QUERY = query def get_search_query() -> Optional[str]: """Get the last search query.""" return _LAST_SEARCH_QUERY def set_pipeline_refreshed(refreshed: bool) -> None: """Track whether the pipeline already refreshed results.""" global _PIPELINE_REFRESHED _PIPELINE_REFRESHED = refreshed def was_pipeline_refreshed() -> bool: """Check if the pipeline already refreshed results.""" return _PIPELINE_REFRESHED def set_last_items(items: list) -> None: """Cache the last pipeline outputs.""" global _PIPELINE_LAST_ITEMS _PIPELINE_LAST_ITEMS = list(items) if items else [] def get_last_items() -> List[Any]: """Get the last pipeline outputs.""" return list(_PIPELINE_LAST_ITEMS) def set_ui_library_refresh_callback(callback: Any) -> None: """Set a callback to be called when library content is updated. The callback will be called with: callback(library_filter: str = 'local') Args: callback: A callable that accepts optional library_filter parameter Example: def my_refresh_callback(library_filter='local'): print(f"Refresh library: {library_filter}") set_ui_library_refresh_callback(my_refresh_callback) """ global _UI_LIBRARY_REFRESH_CALLBACK _UI_LIBRARY_REFRESH_CALLBACK = callback def get_ui_library_refresh_callback() -> Optional[Any]: """Get the current library refresh callback.""" return _UI_LIBRARY_REFRESH_CALLBACK 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(f"[trigger_ui_library_refresh] Error calling refresh callback: {e}", file=sys.stderr) def set_last_result_table(result_table: Optional[Any], items: Optional[List[Any]] = None, subject: Optional[Any] = None) -> None: """Store the last result table and items for @ selection syntax. This should be called after displaying a result table, so users can reference rows with @2, @2-5, @{1,3,5} syntax in subsequent commands. Also maintains a history stack for @.. navigation (restore previous result table). Only selectable commands (search-file, download-data) should call this to create history. For action commands (delete-tag, add-tags, etc), use set_last_result_table_preserve_history() instead. Args: result_table: The ResultTable object that was displayed (or None) items: List of items that populated the table (optional) """ global _LAST_RESULT_TABLE, _LAST_RESULT_ITEMS, _LAST_RESULT_SUBJECT global _RESULT_TABLE_HISTORY, _DISPLAY_ITEMS, _DISPLAY_TABLE, _DISPLAY_SUBJECT # Push current table to history before replacing if _LAST_RESULT_TABLE is not None: _RESULT_TABLE_HISTORY.append((_LAST_RESULT_TABLE, _LAST_RESULT_ITEMS.copy(), _LAST_RESULT_SUBJECT)) # Keep history size limited if len(_RESULT_TABLE_HISTORY) > _MAX_RESULT_TABLE_HISTORY: _RESULT_TABLE_HISTORY.pop(0) # Set new current table and clear any display items/table _DISPLAY_ITEMS = [] _DISPLAY_TABLE = None _DISPLAY_SUBJECT = None _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: """Set a result table as an overlay (display only, no history). Used for commands like get-tag that want to show a formatted table but should be treated as a transient view (closing it returns to previous table). Args: result_table: The ResultTable object to display items: List of items for @N selection """ global _DISPLAY_ITEMS, _DISPLAY_TABLE, _DISPLAY_SUBJECT _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: """Update the last result table WITHOUT adding to history. Used for action commands (delete-tag, add-tags, etc.) that modify data but shouldn't create history entries. This allows @.. to navigate search results, not undo stacks. Args: result_table: The ResultTable object that was displayed (or None) items: List of items that populated the table (optional) """ global _LAST_RESULT_TABLE, _LAST_RESULT_ITEMS, _LAST_RESULT_SUBJECT # Update current table WITHOUT pushing to history _LAST_RESULT_TABLE = result_table _LAST_RESULT_ITEMS = items or [] _LAST_RESULT_SUBJECT = subject def set_last_result_items_only(items: Optional[List[Any]]) -> None: """Store items for @N selection WITHOUT affecting history or saved search data. Used for display-only commands (get-tag, get-url, etc.) and action commands (delete-tag, add-tags, etc.) that emit results but shouldn't affect history. These items are available for @1, @2, etc. selection in the next command, but are NOT saved to history. This preserves search context for @.. navigation. Args: items: List of items to select from """ global _DISPLAY_ITEMS, _DISPLAY_TABLE, _DISPLAY_SUBJECT # Store items for immediate @N selection, but DON'T modify _LAST_RESULT_ITEMS # This ensures history contains original search data, not display transformations _DISPLAY_ITEMS = items or [] # Clear display table since we're setting items only (CLI will generate table if needed) _DISPLAY_TABLE = None _DISPLAY_SUBJECT = None def restore_previous_result_table() -> bool: """Restore the previous result table from history (for @.. navigation). Returns: 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, _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: _DISPLAY_ITEMS = [] _DISPLAY_TABLE = None _DISPLAY_SUBJECT = None # If an underlying table exists, we're done. # Otherwise, fall through to history restore so @.. actually returns to the last table. if _LAST_RESULT_TABLE is not None: return True if not _RESULT_TABLE_HISTORY: return True 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: _LAST_RESULT_TABLE, _LAST_RESULT_ITEMS, _LAST_RESULT_SUBJECT = prev[0], prev[1], prev[2] elif isinstance(prev, tuple) and len(prev) == 2: _LAST_RESULT_TABLE, _LAST_RESULT_ITEMS = prev _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 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 # If an underlying table exists, we're done. # Otherwise, fall through to forward restore when available. if _LAST_RESULT_TABLE is not None: return True if not _RESULT_TABLE_FORWARD: 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. Returns: The ResultTable object, or None if no overlay table is set """ return _DISPLAY_TABLE def get_last_result_subject() -> Optional[Any]: """Get the subject associated with the current result table or overlay. Overlay subject (from display-only tables) takes precedence; otherwise returns the subject stored with the last result table. """ if _DISPLAY_SUBJECT is not None: return _DISPLAY_SUBJECT return _LAST_RESULT_SUBJECT def get_last_result_table() -> Optional[Any]: """Get the current last result table. Returns: The ResultTable object, or None if no table is set """ return _LAST_RESULT_TABLE def get_last_result_items() -> List[Any]: """Get the items available for @N selection. Returns items from display/action commands (get-tag, delete-tag, etc.) if available, otherwise returns items from the last search command. This ensures @N selection works for both display operations and search results. Returns: List of items, or empty list if no prior results """ # 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 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]: """Get the source command from the last displayed result table. Returns: Command name (e.g., 'download-data') or None if not set """ if _is_selectable_table(_LAST_RESULT_TABLE) and hasattr(_LAST_RESULT_TABLE, 'source_command'): return _LAST_RESULT_TABLE.source_command return None def get_last_result_table_source_args() -> List[str]: """Get the base source arguments from the last displayed result table. Returns: List of arguments (e.g., ['https://example.com']) or empty list """ if _is_selectable_table(_LAST_RESULT_TABLE) and hasattr(_LAST_RESULT_TABLE, 'source_args'): return _LAST_RESULT_TABLE.source_args or [] return [] def get_last_result_table_row_selection_args(row_index: int) -> Optional[List[str]]: """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 """ 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'): return row.selection_args return None def set_current_stage_table(result_table: Optional[Any]) -> None: """Store the current pipeline stage table for @N expansion. Used by cmdlet that display tabular results (e.g., download-data with 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) """ global _CURRENT_STAGE_TABLE _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 _is_selectable_table(_CURRENT_STAGE_TABLE) and hasattr(_CURRENT_STAGE_TABLE, 'source_command'): return _CURRENT_STAGE_TABLE.source_command return None def get_current_stage_table_source_args() -> List[str]: """Get the source arguments from the current pipeline stage table. Returns: List of arguments or empty list """ if _is_selectable_table(_CURRENT_STAGE_TABLE) and hasattr(_CURRENT_STAGE_TABLE, 'source_args'): return _CURRENT_STAGE_TABLE.source_args or [] return [] def get_current_stage_table_row_selection_args(row_index: int) -> Optional[List[str]]: """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 """ 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'): return row.selection_args 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 = []