This commit is contained in:
nose
2025-12-19 02:29:42 -08:00
parent d637532237
commit 52cf3f5c9f
24 changed files with 1284 additions and 176 deletions

166
CLI.py
View File

@@ -636,6 +636,7 @@ if (
and Completion is not None
and Completer is not None
and Document is not None
and Lexer is not None
):
CompletionType = cast(Any, Completion)
@@ -934,7 +935,11 @@ def _create_cmdlet_cli():
prompt_text = "🜂🜄🜁🜃|"
# Prepare startup table (always attempt; fall back gracefully if import fails)
startup_table = ResultTable("*********<IGNITIO>*********<NOUSEMPEH>*********<RUGRAPOG>*********<OMEGHAU>*********") if RESULT_TABLE_AVAILABLE else None
startup_table = None
if RESULT_TABLE_AVAILABLE and ResultTable is not None:
startup_table = ResultTable(
"*********<IGNITIO>*********<NOUSEMPEH>*********<RUGRAPOG>*********<OMEGHAU>*********"
)
if startup_table:
startup_table.set_no_choice(True).set_preserve_order(True)
@@ -1173,7 +1178,7 @@ def _create_cmdlet_cli():
api_key = _get_debrid_api_key(config)
if not api_key:
_add_startup_check("DISABLED", display, prov, "Not configured")
_add_startup_check("DISABLED", display, provider=prov, detail="Not configured")
else:
from API.alldebrid import AllDebridClient
@@ -1347,7 +1352,7 @@ def _create_cmdlet_cli():
except Exception:
pass # Silently ignore if config loading fails
if PROMPT_TOOLKIT_AVAILABLE and PromptSession is not None and CmdletCompleter is not None:
if PROMPT_TOOLKIT_AVAILABLE and PromptSession is not None and CmdletCompleter is not None and Style is not None:
completer = CmdletCompleter()
# Define style for syntax highlighting
@@ -1363,7 +1368,7 @@ def _create_cmdlet_cli():
# Toolbar state for background notifications
class ToolbarState:
text = ""
last_update_time = 0
last_update_time: float = 0.0
clear_timer: Optional[threading.Timer] = None
toolbar_state = ToolbarState()
@@ -1677,6 +1682,112 @@ def _execute_pipeline(tokens: list):
if isinstance(config, dict):
# Request terminal-only background updates for this pipeline session
config['_quiet_background_output'] = True
def _maybe_run_class_selector(selected_items: list, *, stage_is_last: bool) -> bool:
"""Allow providers/stores to override `@N` selection semantics."""
if not stage_is_last:
return False
# Gather potential keys from table + selected rows.
candidates: list[str] = []
seen: set[str] = set()
def _add(value) -> None:
try:
text = str(value or '').strip().lower()
except Exception:
return
if not text or text in seen:
return
seen.add(text)
candidates.append(text)
try:
current_table = ctx.get_current_stage_table() or ctx.get_last_result_table()
_add(current_table.table if current_table and hasattr(current_table, 'table') else None)
except Exception:
pass
for item in selected_items or []:
if isinstance(item, dict):
_add(item.get('provider'))
_add(item.get('store'))
_add(item.get('table'))
else:
_add(getattr(item, 'provider', None))
_add(getattr(item, 'store', None))
_add(getattr(item, 'table', None))
# Provider selector
try:
from ProviderCore.registry import get_provider as _get_provider
except Exception:
_get_provider = None
if _get_provider is not None:
for key in candidates:
try:
provider = _get_provider(key, config)
except Exception:
continue
try:
handled = bool(provider.selector(selected_items, ctx=ctx, stage_is_last=True))
except TypeError:
# Backwards-compat: selector(selected_items)
handled = bool(provider.selector(selected_items))
except Exception as exc:
print(f"{key} selector failed: {exc}\n")
return True
if handled:
return True
# Store selector
store_keys: list[str] = []
for item in selected_items or []:
if isinstance(item, dict):
v = item.get('store')
else:
v = getattr(item, 'store', None)
try:
name = str(v or '').strip()
except Exception:
name = ''
if name:
store_keys.append(name)
if store_keys:
try:
from Store.registry import Store as _StoreRegistry
store_registry = _StoreRegistry(config, suppress_debug=True)
try:
_backend_names = list(store_registry.list_backends())
except Exception:
_backend_names = []
_backend_by_lower = {str(n).lower(): str(n) for n in _backend_names if str(n).strip()}
for name in store_keys:
resolved_name = name
if not store_registry.is_available(resolved_name):
try:
resolved_name = _backend_by_lower.get(str(name).lower(), name)
except Exception:
resolved_name = name
if not store_registry.is_available(resolved_name):
continue
backend = store_registry[resolved_name]
selector = getattr(backend, 'selector', None)
if selector is None:
continue
try:
handled = bool(selector(selected_items, ctx=ctx, stage_is_last=True))
except TypeError:
handled = bool(selector(selected_items))
if handled:
return True
except Exception:
# Store init failure should not break normal selection.
pass
return False
# Check if the first stage has @ selection - if so, apply it before pipeline execution
first_stage_tokens = stages[0] if stages else []
@@ -1827,6 +1938,10 @@ def _execute_pipeline(tokens: list):
try:
filtered = [resolved_items[i] for i in first_stage_selection_indices if 0 <= i < len(resolved_items)]
if filtered:
# Allow providers/stores to override selection behavior (e.g., Matrix room picker).
if _maybe_run_class_selector(filtered, stage_is_last=(not stages)):
return
# Convert filtered items to PipeObjects for consistent pipeline handling
from cmdlet._shared import coerce_to_pipe_object
filtered_pipe_objs = [coerce_to_pipe_object(item) for item in filtered]
@@ -2011,17 +2126,21 @@ def _execute_pipeline(tokens: list):
# If not expanding, use as filter
if not should_expand_to_command:
# This is a selection stage - filter piped results
# Prefer selecting from the active result context even when nothing is piped.
# Some cmdlets present a selectable table and rely on @N afterwards.
if piped_result is None:
print(f"No piped results to select from with {cmd_name}\n")
pipeline_status = "failed"
pipeline_error = f"Selection {cmd_name} without upstream results"
return
# Normalize piped_result to always be a list for indexing
if isinstance(piped_result, dict) or not isinstance(piped_result, (list, tuple)):
piped_result_list = [piped_result]
piped_result_list = ctx.get_last_result_items()
if not piped_result_list:
print(f"No piped results to select from with {cmd_name}\n")
pipeline_status = "failed"
pipeline_error = f"Selection {cmd_name} without upstream results"
return
else:
piped_result_list = piped_result
# Normalize piped_result to always be a list for indexing
if isinstance(piped_result, dict) or not isinstance(piped_result, (list, tuple)):
piped_result_list = [piped_result]
else:
piped_result_list = piped_result
# Get indices to select
if is_select_all:
@@ -2038,12 +2157,29 @@ def _execute_pipeline(tokens: list):
stage_table = ctx.get_display_table()
if not stage_table:
stage_table = ctx.get_last_result_table()
resolved_list = _resolve_items_for_selection(stage_table, list(piped_result_list))
_debug_selection("pipeline-stage", selection_indices, stage_table, piped_result_list, resolved_list)
# Prefer selecting from the displayed table's items if available.
# This matters when a cmdlet shows a selectable overlay table but does not emit
# items downstream (e.g., add-file -provider matrix shows rooms, but the piped
# value is still the original file).
selection_base = list(piped_result_list)
try:
table_rows = len(stage_table.rows) if stage_table and hasattr(stage_table, 'rows') and stage_table.rows else None
last_items = ctx.get_last_result_items()
if last_items and table_rows is not None and len(last_items) == table_rows:
selection_base = list(last_items)
except Exception:
pass
resolved_list = _resolve_items_for_selection(stage_table, selection_base)
_debug_selection("pipeline-stage", selection_indices, stage_table, selection_base, resolved_list)
try:
filtered = [resolved_list[i] for i in selection_indices if 0 <= i < len(resolved_list)]
if filtered:
# Allow providers/stores to override selection behavior (e.g., Matrix room picker).
if _maybe_run_class_selector(filtered, stage_is_last=(stage_index + 1 >= len(stages))):
return
# Convert filtered items to PipeObjects for consistent pipeline handling
from cmdlet._shared import coerce_to_pipe_object
filtered_pipe_objs = [coerce_to_pipe_object(item) for item in filtered]