This commit is contained in:
2026-07-20 16:15:58 -07:00
parent a156700e6e
commit 7f12bc7f40
6 changed files with 217 additions and 7 deletions
+31 -3
View File
@@ -98,7 +98,7 @@ from SYS.result_table import Table
from SYS.worker import WorkerManagerRegistry, WorkerStages, WorkerOutputMirror, WorkerStageSession from SYS.worker import WorkerManagerRegistry, WorkerStages, WorkerOutputMirror, WorkerStageSession
from SYS.pipeline import PipelineExecutor from SYS.pipeline import PipelineExecutor
from PluginCore.registry import plugin_inline_query_choices from PluginCore.registry import plugin_inline_query_choices, plugin_query_field_map
@@ -1242,10 +1242,13 @@ class CmdletCompleter(Completer):
if query_fragment is not None: if query_fragment is not None:
field_choices: Dict[str, List[str]] = {} field_choices: Dict[str, List[str]] = {}
ordered_fields: List[str] = [] ordered_fields: List[str] = []
query_only_field_names: set = set()
for spec in query_specs: for spec in query_specs:
key = str(spec.get("query_key") or spec.get("name") or "").strip().lower() key = str(spec.get("query_key") or spec.get("name") or "").strip().lower()
if not key: if not key:
continue continue
if spec.get("query_only"):
query_only_field_names.add(key)
if key not in field_choices: if key not in field_choices:
ordered_fields.append(key) ordered_fields.append(key)
field_choices[key] = [str(choice) for choice in list(spec.get("choices", []) or [])] field_choices[key] = [str(choice) for choice in list(spec.get("choices", []) or [])]
@@ -1255,6 +1258,31 @@ class CmdletCompleter(Completer):
continue continue
field_choices.setdefault(alias_text, field_choices[key]) field_choices.setdefault(alias_text, field_choices[key])
plugin_field_map: Dict[str, List[str]] = {}
if selected_plugin:
try:
plugin_field_map = plugin_query_field_map(selected_plugin, config)
except Exception:
plugin_field_map = {}
if plugin_field_map:
ordered_fields = [f for f in ordered_fields if f not in query_only_field_names]
removed_keys = set()
for key in query_only_field_names:
field_choices.pop(key, None)
removed_keys.add(key)
aliases_to_remove = set()
for alias_key, alias_values in list(field_choices.items()):
if alias_key in removed_keys:
continue
if alias_key not in ordered_fields and alias_key not in plugin_field_map:
aliases_to_remove.add(alias_key)
for akey in aliases_to_remove:
field_choices.pop(akey, None)
for field_name, choices in plugin_field_map.items():
if field_name not in field_choices:
ordered_fields.append(field_name)
field_choices[field_name] = choices
raw_fragment = str(query_fragment or "") raw_fragment = str(query_fragment or "")
segment = raw_fragment[1:] if raw_fragment[:1] in {"'", '"'} else raw_fragment segment = raw_fragment[1:] if raw_fragment[:1] in {"'", '"'} else raw_fragment
if "," in segment: if "," in segment:
@@ -1267,8 +1295,8 @@ class CmdletCompleter(Completer):
partial_lower = partial.strip().lower() partial_lower = partial.strip().lower()
inline_choices = [] inline_choices = []
if effective_cmd == "search-file" and provider_name: if selected_plugin and effective_cmd in {"search-file", "download-file"}:
inline_choices = self._inline_query_choices(provider_name, field, config) inline_choices = self._inline_query_choices(selected_plugin, field, config)
choice_pool = inline_choices or field_choices.get(field, []) choice_pool = inline_choices or field_choices.get(field, [])
if choice_pool: if choice_pool:
+30
View File
@@ -863,6 +863,36 @@ def plugin_inline_query_choices(
return [] return []
def plugin_query_field_map(
plugin_name: str,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, List[str]]:
pname = str(plugin_name or "").strip().lower()
if not pname:
return {}
try:
info = REGISTRY.get(pname)
if info is not None:
mapping = collect_choice(info.plugin_class)
else:
plugin = get_plugin(pname, config)
if plugin is None:
return {}
mapping = collect_choice(plugin)
result: Dict[str, List[str]] = {}
for field, choices in mapping.items():
texts = []
for c in choices:
text = str(c.get("text") or c.get("value") or "")
if text:
texts.append(text)
if texts:
result[field] = texts
return result
except Exception:
return {}
def get_plugin_for_url(url: str, def get_plugin_for_url(url: str,
config: Optional[Dict[str, Any]] = None) -> Optional[Provider]: config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
name = match_plugin_name_for_url(url) name = match_plugin_name_for_url(url)
+2 -2
View File
@@ -2076,8 +2076,8 @@ class PipelineExecutor:
except Exception: except Exception:
row_action = None row_action = None
if row_action: if row_action:
prefer_row_action = True stages.insert(0, list(row_action))
preferred_row_action = list(row_action) return True, None
# Command expansion via @N: # Command expansion via @N:
# - Default behavior: expand ONLY for single-row selections. # - Default behavior: expand ONLY for single-row selections.
# - Special case: allow multi-row expansion for add-file directory tables by # - Special case: allow multi-row expansion for add-file directory tables by
+140 -1
View File
@@ -32,6 +32,7 @@ _PREFERENCES_BROWSE_PATH = "__preferences__"
_PLUGINS_BROWSE_PATH = "__plugins__" _PLUGINS_BROWSE_PATH = "__plugins__"
_PLUGIN_CATEGORY_KEYS = ("plugin",) _PLUGIN_CATEGORY_KEYS = ("plugin",)
_CREATE_INSTANCE_FLAG = "-create-instance" _CREATE_INSTANCE_FLAG = "-create-instance"
_CHOOSE_FLAG = "-choose"
_KNOWN_SECTION_LABELS = { _KNOWN_SECTION_LABELS = {
"plugin": "Plugins", "plugin": "Plugins",
} }
@@ -65,6 +66,7 @@ _CONFIG_ITEM_FIELDS = (
"type", "type",
"display_path", "display_path",
"instance_target", "instance_target",
"choices",
) )
CMDLET = Cmdlet( CMDLET = Cmdlet(
@@ -576,6 +578,7 @@ def _build_value_item(
key_path: str, key_path: str,
name: str, name: str,
value: Any, value: Any,
choices: Any = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
display_value = "***" if _is_sensitive_config_key(key_path) else _format_config_value(value) display_value = "***" if _is_sensitive_config_key(key_path) else _format_config_value(value)
path_parts = [part for part in str(key_path or "").split(".") if part] path_parts = [part for part in str(key_path or "").split(".") if part]
@@ -583,6 +586,7 @@ def _build_value_item(
[_format_config_path_label(".".join(path_parts[:-1]))] if len(path_parts) > 1 else [] [_format_config_path_label(".".join(path_parts[:-1]))] if len(path_parts) > 1 else []
+ [_format_config_label(path_parts[-1])] if path_parts else [_format_config_label(name)] + [_format_config_label(path_parts[-1])] if path_parts else [_format_config_label(name)]
) )
choice_list = list(choices) if isinstance(choices, (list, tuple)) else []
return { return {
"kind": "value", "kind": "value",
"key": key_path, "key": key_path,
@@ -592,6 +596,7 @@ def _build_value_item(
"value_display": display_value, "value_display": display_value,
"display_path": display_path, "display_path": display_path,
"type": type(value).__name__, "type": type(value).__name__,
"choices": choice_list,
} }
@@ -700,6 +705,25 @@ def _build_root_config_items(config_data: Dict[str, Any]) -> List[Dict[str, Any]
return items return items
def _lookup_plugin_choices(browse_path: str, key_name: str) -> Optional[List[str]]:
parts = _split_config_path(browse_path)
if len(parts) < 2 or parts[0] != "plugin":
return None
plugin_name = parts[1]
try:
from SYS.plugin_config import get_plugin_schema
schema = get_plugin_schema(plugin_name)
except Exception:
return None
for field in schema:
if field.get("key") == key_name:
choices = field.get("choices")
if isinstance(choices, list):
return [str(c) for c in choices]
break
return None
def _build_nested_config_items( def _build_nested_config_items(
config_data: Dict[str, Any], config_data: Dict[str, Any],
browse_path: str, browse_path: str,
@@ -737,6 +761,7 @@ def _build_nested_config_items(
key_path=full_key, key_path=full_key,
name=key, name=key,
value=value, value=value,
choices=_lookup_plugin_choices(browse_path, key),
) )
) )
@@ -799,6 +824,21 @@ def _extract_browse_arg(args: Sequence[str]) -> Optional[str]:
return _extract_arg_value(args, flags={"-browse", "--browse"}, allow_positional=False) return _extract_arg_value(args, flags={"-browse", "--browse"}, allow_positional=False)
def _extract_choose_arg(args: Sequence[str]) -> Optional[str]:
return _extract_arg_value(args, flags={_CHOOSE_FLAG}, allow_positional=False)
def _resolve_config_value_raw(config: Dict[str, Any], key_path: str) -> Any:
parts = [p for p in str(key_path or "").split(".") if p]
current: Any = config
for part in parts:
if isinstance(current, dict):
current = current.get(part)
else:
return None
return current
def _extract_selected_update_value(args: Sequence[str]) -> Optional[str]: def _extract_selected_update_value(args: Sequence[str]) -> Optional[str]:
explicit = _extract_arg_value(args, flags=VALUE_ARG_FLAGS, allow_positional=False) explicit = _extract_arg_value(args, flags=VALUE_ARG_FLAGS, allow_positional=False)
if explicit is not None: if explicit is not None:
@@ -871,6 +911,44 @@ def _normalize_config_item(candidate: Any) -> Optional[Dict[str, Any]]:
return normalized or None return normalized or None
def _show_choice_table(
target_key: str,
choices: List[str],
current_value: str,
display_name: str,
) -> int:
items: List[Dict[str, Any]] = []
for choice in choices:
choice_str = str(choice)
marker = " ← current" if choice_str == current_value else ""
items.append({
"kind": "choice",
"key": target_key,
"title": choice_str,
"value": choice_str,
"display": f"{choice_str}{marker}",
})
table = Table(f"Configuration: {display_name}", preserve_order=True)
table.set_table("config")
table.set_source_command(".config", [target_key])
table.set_header_lines([
f"Available values for {display_name}. Current: {current_value}",
"Use @N to select a value. Use @.. to go back.",
])
for idx, item in enumerate(items):
row = table.add_row()
row.add_column("Choice", item.get("display", item.get("title", "")))
table.set_row_selection_action(idx, [".config", str(item.get("key")), str(item.get("value"))])
ctx.set_last_result_table(table, items)
ctx.set_current_stage_table(table)
print()
print(table)
return 0
def _show_config_table( def _show_config_table(
config_data: Dict[str, Any], config_data: Dict[str, Any],
*, *,
@@ -893,13 +971,23 @@ def _show_config_table(
for idx, item in enumerate(items): for idx, item in enumerate(items):
row = table.add_row() row = table.add_row()
row.add_column("Name", item.get("title", "")) row.add_column("Name", item.get("title", ""))
row.add_column("Value", item.get("summary") or item.get("value_display", "")) value_text = item.get("summary") or item.get("value_display", "")
choices = item.get("choices")
if choices and isinstance(choices, list) and len(choices) > 0:
choices_repr = ", ".join(str(c) for c in choices)
value_text = f"{value_text} [{choices_repr}]"
row.add_column("Value", value_text)
row.add_column("Type", item.get("type", "")) row.add_column("Type", item.get("type", ""))
if item.get("kind") == "section" and item.get("browse_path"): if item.get("kind") == "section" and item.get("browse_path"):
table.set_row_selection_action( table.set_row_selection_action(
idx, idx,
[".config", "-browse", str(item.get("browse_path"))], [".config", "-browse", str(item.get("browse_path"))],
) )
elif item.get("kind") == "value" and choices and isinstance(choices, list) and len(choices) > 0 and item.get("key"):
table.set_row_selection_action(
idx,
[".config", "-choose", str(item.get("key"))],
)
elif item.get("kind") == "create_instance" and item.get("instance_target"): elif item.get("kind") == "create_instance" and item.get("instance_target"):
table.set_row_selection_action( table.set_row_selection_action(
idx, idx,
@@ -966,6 +1054,22 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
if browse_path: if browse_path:
return _show_config_table(current_config, browse_path=browse_path) return _show_config_table(current_config, browse_path=browse_path)
choose_key = _extract_choose_arg(args)
if choose_key:
choices = _lookup_plugin_choices(choose_key, choose_key.split(".")[-1]) or []
if not choices:
nested_branch = _resolve_config_branch(current_config, choose_key)
if isinstance(nested_branch, dict):
return _show_config_table(current_config, browse_path=choose_key)
print(f"No choices available for '{_format_config_path_label(choose_key)}'")
return 0
try:
current_value = str(_resolve_config_value_raw(current_config, choose_key) or "").strip()
except Exception:
current_value = ""
display_name = _format_config_path_label(choose_key)
return _show_choice_table(choose_key, choices, current_value, display_name)
selection_item = _get_selected_config_item() or _normalize_config_item(piped_result) selection_item = _get_selected_config_item() or _normalize_config_item(piped_result)
create_instance_target = _extract_create_instance_target(args) create_instance_target = _extract_create_instance_target(args)
@@ -1025,6 +1129,31 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
log(f"Error updating config '{target_key}': {exc}") log(f"Error updating config '{target_key}': {exc}")
print(f"Error updating config: {exc}") print(f"Error updating config: {exc}")
return 1 return 1
choices = (selection_item or {}).get("choices")
if choices and isinstance(choices, list) and len(choices) > 0:
current_value = str((selection_item or {}).get("value") or "").strip()
display_name = selection_display_path or selection_key
return _show_choice_table(selection_key, choices, current_value, display_name)
if selection_kind == "choice" and selection_key:
choice_value = str((selection_item or {}).get("value") or "").strip()
if choice_value:
try:
target_key = _resolve_update_key(current_config, selection_key)
set_nested_config(current_config, target_key, choice_value)
_save_updated_config(current_config, target_key)
print(f"Updated '{_format_config_path_label(selection_key)}' to '{choice_value}'")
current_config = load_config()
key_parts = [p for p in str(selection_key).split(".") if p]
if len(key_parts) >= 2:
parent_path = ".".join(key_parts[:-1])
if _resolve_config_branch(current_config, parent_path):
return _show_config_table(current_config, browse_path=parent_path)
return 0
except Exception as exc:
log(f"Error updating config '{selection_key}': {exc}")
print(f"Error updating config: {exc}")
return 1
if not args: if not args:
if sys.stdin.isatty() and not piped_result: if sys.stdin.isatty() and not piped_result:
@@ -1047,6 +1176,16 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
set_nested_config(current_config, key, value) set_nested_config(current_config, key, value)
_save_updated_config(current_config, key) _save_updated_config(current_config, key)
print(f"Updated '{key}' to '{value}'") print(f"Updated '{key}' to '{value}'")
current_config = load_config()
parent_path = _resolve_direct_browse_path(current_config, key)
if not parent_path:
key_parts = [p for p in str(key).split(".") if p]
if len(key_parts) >= 2:
candidate = ".".join(key_parts[:-1])
if _resolve_config_branch(current_config, candidate):
parent_path = candidate
if parent_path:
return _show_config_table(current_config, browse_path=parent_path)
return 0 return 0
except Exception as exc: except Exception as exc:
log(f"Error updating config '{key}': {exc}") log(f"Error updating config '{key}': {exc}")
+6
View File
@@ -251,6 +251,12 @@ class Local(Provider):
folder_name = str(kwargs.get("folder_name") or "").strip() folder_name = str(kwargs.get("folder_name") or "").strip()
if not folder_name and not direct_export_download: if not folder_name and not direct_export_download:
folder_name = self._folder_name_from_pipe(kwargs.get("pipe_obj")) folder_name = self._folder_name_from_pipe(kwargs.get("pipe_obj"))
if not folder_name and not direct_export_download:
title_hint = str(kwargs.get("title") or "").strip()
if not title_hint:
title_hint = source_path.stem.replace("_", " ").strip()
if title_hint:
folder_name = title_hint
export_root = destination_root export_root = destination_root
if folder_name and not direct_export_download: if folder_name and not direct_export_download:
+8 -1
View File
@@ -612,6 +612,13 @@ class OpenLibrary(Provider):
TABLE_AUTO_STAGES = { TABLE_AUTO_STAGES = {
"openlibrary.edition": ["download-file"], "openlibrary.edition": ["download-file"],
} }
QUERY_ARG_CHOICES = {
"quality": ["high", "medium", "low"],
"language": [
"english", "spanish", "french", "german", "italian",
"portuguese", "polish", "russian", "chinese", "japanese",
],
}
@classmethod @classmethod
def config_schema(cls) -> List[Dict[str, Any]]: def config_schema(cls) -> List[Dict[str, Any]]:
@@ -1175,7 +1182,7 @@ class OpenLibrary(Provider):
Returns a dict with the downloaded path and SearchResult when successful. Returns a dict with the downloaded path and SearchResult when successful.
""" """
self, sr = self.search_result_from_url(url)
if sr is None: if sr is None:
return None return None