update local and mpv plugins, add file cmdlet, update docs

This commit is contained in:
2026-05-14 17:15:13 -07:00
parent 9f0eb29289
commit 036977832b
10 changed files with 653 additions and 297 deletions
+172 -14
View File
@@ -731,8 +731,6 @@ class CmdletCompleter(Completer):
return canonical_cmd
lowered = {str(tok or "").strip().lower() for tok in (stage_tokens or [])}
if "-search" in lowered or "--search" in lowered:
return "search-file"
if "-download" in lowered or "--download" in lowered or "-dl" in lowered or "--dl" in lowered:
return "download-file"
if "-add" in lowered or "--add" in lowered:
@@ -743,6 +741,10 @@ class CmdletCompleter(Completer):
return "delete-file"
if "-merge" in lowered or "--merge" in lowered:
return "merge-file"
if "-plugin" in lowered or "--plugin" in lowered or any(tok.startswith("-plugin=") or tok.startswith("--plugin=") for tok in lowered):
return "search-file"
if "-query" in lowered or "--query" in lowered or any(tok.startswith("-query=") or tok.startswith("--query=") for tok in lowered):
return "search-file"
return canonical_cmd
@staticmethod
@@ -750,7 +752,12 @@ class CmdletCompleter(Completer):
canonical_cmd = CmdletCompleter._effective_cmd_name(cmd_name, stage_tokens)
if canonical_cmd not in {"search-file", "add-file", "download-file"}:
return None
return CmdletCompleter._flag_value(stage_tokens, "-plugin", "--plugin")
raw_plugin = CmdletCompleter._flag_value(stage_tokens, "-plugin", "--plugin")
if raw_plugin:
# Strip quotes if present, then normalize to lowercase
stripped = CmdletCompleter._strip_quotes(str(raw_plugin or ""))
return stripped.strip().lower() if stripped else None
return None
@staticmethod
def _plugin_instance_choices(plugin_name: Optional[str], config: Dict[str, Any]) -> List[str]:
@@ -788,6 +795,84 @@ class CmdletCompleter(Completer):
out.append(text)
return out
@staticmethod
def _plugin_instance_accepts_direct_path(plugin_name: Optional[str]) -> bool:
return str(plugin_name or "").strip().lower() == "local"
@staticmethod
def _looks_like_path_fragment(value: str) -> bool:
text = str(value or "").strip()
if not text:
return False
if text[:1] in {"'", '"'}:
text = text[1:]
if not text:
return False
if text.startswith((".", "~", "\\", "/")):
return True
if "\\" in text or "/" in text:
return True
if len(text) >= 2 and text[1] == ":":
return True
return False
@staticmethod
def _path_instance_choices(current_token: str) -> List[str]:
raw = str(current_token or "")
if not CmdletCompleter._looks_like_path_fragment(raw):
return []
quote_prefix = raw[:1] if raw[:1] in {"'", '"'} else ""
fragment = raw[1:] if quote_prefix else raw
if not fragment:
return []
expanded = os.path.expanduser(fragment)
candidate = Path(expanded)
if fragment.endswith(("\\", "/")):
parent = candidate
prefix = ""
else:
parent = candidate.parent if str(candidate.parent) not in {"", "."} else Path.cwd()
prefix = candidate.name
try:
if not parent.exists() or not parent.is_dir():
return []
except Exception:
return []
out: List[str] = []
seen: Set[str] = set()
prefix_lower = prefix.lower()
try:
entries = sorted(parent.iterdir(), key=lambda item: item.name.lower())
except Exception:
return []
for entry in entries:
try:
if not entry.is_dir():
continue
if prefix_lower and not entry.name.lower().startswith(prefix_lower):
continue
suggestion = str(entry)
if quote_prefix:
suggestion = quote_prefix + suggestion
elif " " in suggestion:
suggestion = f'"{suggestion}"'
except Exception:
continue
lowered = suggestion.lower()
if lowered in seen:
continue
seen.add(lowered)
out.append(suggestion)
return out
def _filter_stage_arg_names(
self,
*,
@@ -803,12 +888,15 @@ class CmdletCompleter(Completer):
plugin_name = self._selected_plugin_name(canonical_cmd, stage_tokens)
instance_choices = self._plugin_instance_choices(plugin_name, config)
has_named_instances = bool(instance_choices)
accepts_direct_path = self._plugin_instance_accepts_direct_path(plugin_name)
filtered: List[str] = []
for arg in arg_names:
logical = str(arg or "").lstrip("-").strip().lower()
if logical == "instance":
if not plugin_name or not has_named_instances:
if not plugin_name:
continue
if not has_named_instances and not accepts_direct_path:
continue
if canonical_cmd == "search-file" and logical == "open":
if str(plugin_name or "").strip().lower() != "alldebrid":
@@ -816,6 +904,59 @@ class CmdletCompleter(Completer):
filtered.append(arg)
return filtered
@staticmethod
def _tokenize_quoted(text: str) -> List[str]:
"""Tokenize text preserving quoted strings as single tokens.
Handles pipes as pipeline separators and preserves quoted strings
(single or double quotes) as atomic tokens.
"""
tokens = []
current = ""
in_quote = None # None, "'", or '"'
i = 0
while i < len(text):
char = text[i]
if in_quote:
current += char
if char == in_quote and (i == 0 or text[i - 1] != "\\"):
in_quote = None
elif char in ("'", '"'):
in_quote = char
current += char
elif char == "|":
if current.strip():
tokens.append(current.strip())
tokens.append("|")
current = ""
elif char.isspace():
if current.strip():
tokens.append(current.strip())
current = ""
else:
current += char
i += 1
if current.strip():
tokens.append(current.strip())
return tokens
@staticmethod
def _strip_quotes(token: str) -> str:
"""Remove surrounding quotes from a token if present.
Preserves internal content exactly. Only removes matching outer quotes.
"""
token = str(token or "").strip()
if len(token) >= 2:
if (token[0] == '"' and token[-1] == '"') or (token[0] == "'" and token[-1] == "'"):
return token[1:-1]
return token
def get_completions(
self,
document: Document,
@@ -824,7 +965,7 @@ class CmdletCompleter(Completer):
self._refresh_cmdlet_names()
text = document.text_before_cursor
tokens = text.split()
tokens = self._tokenize_quoted(text)
ends_with_space = bool(text) and text[-1].isspace()
last_pipe = -1
@@ -885,17 +1026,22 @@ class CmdletCompleter(Completer):
cmd_name = stage_tokens[0].replace("_", "-").lower()
effective_cmd = self._effective_cmd_name(cmd_name, stage_tokens)
if ends_with_space:
raw_current_token = ""
current_token = ""
prev_token = stage_tokens[-1].lower()
else:
current_token = stage_tokens[-1].lower()
raw_current_token = stage_tokens[-1]
current_token = raw_current_token.lower()
prev_token = stage_tokens[-2].lower() if len(stage_tokens) > 1 else ""
config = self._config_loader.load_shared()
provider_name = None
if effective_cmd == "search-file":
provider_name = self._flag_value(stage_tokens, "-plugin", "--plugin")
raw_provider = self._flag_value(stage_tokens, "-plugin", "--plugin")
if raw_provider:
stripped = self._strip_quotes(str(raw_provider or ""))
provider_name = stripped.strip().lower() if stripped else None
selected_plugin = self._selected_plugin_name(effective_cmd, stage_tokens)
@@ -912,8 +1058,10 @@ class CmdletCompleter(Completer):
query_fragment: Optional[str] = None
if prev_token in {"-query", "--query"} and current_token[:1] in {"'", '"'}:
query_fragment = current_token
elif query_started_quoted and not ends_with_space:
query_fragment = current_token
elif query_started_quoted and not ends_with_space and not current_token.startswith("-"):
# Only continue in query mode if the previous token is not a flag (new argument)
if not prev_token.startswith("-"):
query_fragment = current_token
elif query_started_quoted and ends_with_space and ":" in prev_token:
query_fragment = ""
@@ -1010,6 +1158,16 @@ class CmdletCompleter(Completer):
choices: List[str] = []
if normalized_prev == "instance" and selected_plugin:
choices = self._plugin_instance_choices(selected_plugin, config)
if self._plugin_instance_accepts_direct_path(selected_plugin):
path_choices = self._path_instance_choices(raw_current_token)
if path_choices:
seen_choice_values = {str(choice).lower() for choice in choices}
for choice in path_choices:
lowered = str(choice).lower()
if lowered in seen_choice_values:
continue
choices.append(choice)
seen_choice_values.add(lowered)
if not choices:
choices = self._arg_choices(
cmd_name=effective_cmd,
@@ -1032,7 +1190,7 @@ class CmdletCompleter(Completer):
choice_list = filtered
for choice in choice_list:
yield Completion(choice, start_position=-len(current_token))
yield Completion(choice, start_position=-len(raw_current_token))
# Example: if the user has typed `download-file -url ...`, then `url`
# is considered used and should not be suggested again (even as `--url`).
return
@@ -1253,8 +1411,6 @@ class CmdletExecutor:
def _file_action(args: Optional[List[str]]) -> str | None:
tokens = [str(t or "").strip().lower() for t in (args or [])]
token_set = set(tokens)
if "-search" in token_set or "--search" in token_set:
return "search-file"
if "-download" in token_set or "--download" in token_set or "-dl" in token_set or "--dl" in token_set:
return "download-file"
if "-get" in token_set or "--get" in token_set:
@@ -1265,6 +1421,8 @@ class CmdletExecutor:
return "delete-file"
if "-merge" in token_set or "--merge" in token_set:
return "merge-file"
if "-query" in token_set or "--query" in token_set or any(tok.startswith("-query=") or tok.startswith("--query=") for tok in token_set):
return "search-file"
return None
normalized_cmd = str(cmd_name or "").replace("_", "-").lower().strip()
@@ -1761,8 +1919,6 @@ class CmdletExecutor:
if norm != "file":
return norm
lowered = {str(a or "").strip().lower() for a in (args or [])}
if "-search" in lowered or "--search" in lowered:
return "search-file"
if "-download" in lowered or "--download" in lowered or "-dl" in lowered or "--dl" in lowered:
return "download-file"
if "-get" in lowered or "--get" in lowered:
@@ -1771,6 +1927,8 @@ class CmdletExecutor:
return "add-file"
if "-delete" in lowered or "--delete" in lowered or "-del" in lowered or "--del" in lowered:
return "delete-file"
if "-query" in lowered or "--query" in lowered or any(a.startswith("-query=") or a.startswith("--query=") for a in lowered):
return "search-file"
return norm
effective_cmd = _effective_cmd_name(cmd_name, filtered_args)