add new search engine commands

This commit is contained in:
2026-04-09 08:44:56 +02:00
parent ab4ba97886
commit 50dc2bb5e5
2 changed files with 107 additions and 14 deletions
+17 -14
View File
@@ -17,6 +17,7 @@ from browser_cli.commands.windows import windows_group
from browser_cli.commands.dom import dom_group from browser_cli.commands.dom import dom_group
from browser_cli.commands.extract import extract_group from browser_cli.commands.extract import extract_group
from browser_cli.commands.session import session_group from browser_cli.commands.session import session_group
from browser_cli.commands.search import search_group
from browser_cli.client import send_command, BrowserNotConnected from browser_cli.client import send_command, BrowserNotConnected
console = Console() console = Console()
@@ -52,6 +53,7 @@ main.add_command(windows_group)
main.add_command(dom_group) main.add_command(dom_group)
main.add_command(extract_group) main.add_command(extract_group)
main.add_command(session_group) main.add_command(session_group)
main.add_command(search_group)
# ── clients ──────────────────────────────────────────────────────────────────── # ── clients ────────────────────────────────────────────────────────────────────
@@ -118,20 +120,12 @@ def cmd_rename_profile(alias):
def cmd_install(browser): def cmd_install(browser):
"""Register the native messaging host and print extension load instructions.""" """Register the native messaging host and print extension load instructions."""
# Find the venv entry point for the native host (stable regardless of project location) # Install wrapper outside PATH — Chrome uses the absolute path from the manifest,
venv_script = Path(sys.executable).parent / "browser-cli-native-host" # so it doesn't need to be a shell command.
if not venv_script.exists(): share_dir = Path.home() / ".local" / "share" / "browser-cli"
console.print(f"[red]Cannot find browser-cli-native-host in venv ({venv_script})[/red]") share_dir.mkdir(parents=True, exist_ok=True)
console.print(" Run [cyan]uv sync[/cyan] first to install entry points.") wrapper_path = share_dir / "native-host"
sys.exit(1) wrapper_content = f'#!/bin/sh\nexec "{sys.executable}" -m browser_cli.native_host "$@"\n'
# Install wrapper to ~/.local/bin so the manifest path never changes
local_bin = Path.home() / ".local" / "bin"
local_bin.mkdir(parents=True, exist_ok=True)
wrapper_path = local_bin / "browser-cli-native-host"
wrapper_content = f"""#!/bin/sh
exec "{venv_script}" "$@"
"""
wrapper_path.write_text(wrapper_content) wrapper_path.write_text(wrapper_content)
wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
@@ -181,6 +175,15 @@ exec "{venv_script}" "$@"
console.print(" After restarting Chrome, try: [cyan]browser-cli tabs list[/cyan]") console.print(" After restarting Chrome, try: [cyan]browser-cli tabs list[/cyan]")
# ── native-host (hidden, called by Chrome via native messaging) ────────────────
@main.command("native-host", hidden=True)
def cmd_native_host():
"""Native messaging host — called by Chrome, not for direct use."""
from browser_cli.native_host import main as _main
_main()
# ── completion ───────────────────────────────────────────────────────────────── # ── completion ─────────────────────────────────────────────────────────────────
@main.command("completion") @main.command("completion")
+90
View File
@@ -0,0 +1,90 @@
import click
from urllib.parse import quote_plus
from browser_cli.client import send_command, BrowserNotConnected
from rich.console import Console
console = Console()
ENGINES = {
"google": "https://www.google.com/search?q={query}",
"brave": "https://search.brave.com/search?q={query}",
"duckduckgo": "https://duckduckgo.com/?q={query}",
"ddg": "https://duckduckgo.com/?q={query}",
"youtube": "https://www.youtube.com/results?search_query={query}",
"yt": "https://www.youtube.com/results?search_query={query}",
"spotify": "https://open.spotify.com/search/{query}",
"amazon": "https://www.amazon.com/s?k={query}",
"ecosia": "https://www.ecosia.org/search?q={query}",
"furaffinity": "https://www.furaffinity.net/search/?q={query}",
"fa": "https://www.furaffinity.net/search/?q={query}",
"bing": "https://www.bing.com/search?q={query}",
"github": "https://github.com/search?q={query}",
"wikipedia": "https://en.wikipedia.org/wiki/Special:Search?search={query}",
"wiki": "https://en.wikipedia.org/wiki/Special:Search?search={query}",
"reddit": "https://www.reddit.com/search/?q={query}",
"stackoverflow": "https://stackoverflow.com/search?q={query}",
"so": "https://stackoverflow.com/search?q={query}",
}
_DISPLAY_NAMES = {
"google": "Google", "brave": "Brave Search", "duckduckgo": "DuckDuckGo",
"ddg": "DuckDuckGo", "youtube": "YouTube", "yt": "YouTube",
"spotify": "Spotify", "amazon": "Amazon", "ecosia": "Ecosia",
"furaffinity": "FurAffinity", "fa": "FurAffinity", "bing": "Bing",
"github": "GitHub", "wikipedia": "Wikipedia", "wiki": "Wikipedia",
"reddit": "Reddit", "stackoverflow": "Stack Overflow", "so": "Stack Overflow",
}
_SUBCOMMANDS = [
("google", "Search with Google."),
("brave", "Search with Brave Search."),
("duckduckgo", "Search with DuckDuckGo."),
("ddg", "Search with DuckDuckGo (alias for duckduckgo)."),
("youtube", "Search YouTube videos."),
("yt", "Search YouTube (alias for youtube)."),
("spotify", "Search Spotify."),
("amazon", "Search Amazon."),
("ecosia", "Search with Ecosia."),
("furaffinity", "Search FurAffinity."),
("fa", "Search FurAffinity (alias for furaffinity)."),
("bing", "Search with Bing."),
("github", "Search GitHub."),
("wikipedia", "Search Wikipedia."),
("wiki", "Search Wikipedia (alias for wikipedia)."),
("reddit", "Search Reddit."),
("stackoverflow", "Search Stack Overflow."),
("so", "Search Stack Overflow (alias for stackoverflow)."),
]
@click.group("search")
def search_group():
"""Search the web — open a query in a search engine."""
def _build_command(engine_key: str, help_text: str) -> click.Command:
@click.command(engine_key, help=help_text)
@click.argument("query", nargs=-1, required=True)
@click.option("--bg", is_flag=True, help="Open in background (no focus)")
@click.option("--window", "window", default=None, help="Open in named window")
@click.option("--group", "group", default=None, help="Open in tab group (name or ID)")
def _cmd(query, bg, window, group):
terms = " ".join(query)
url = ENGINES[engine_key].format(query=quote_plus(terms))
try:
send_command("navigate.open", {"url": url, "background": bg, "window": window, "group": group})
except BrowserNotConnected as e:
console.print(f"[red]Error:[/red] {e}")
raise SystemExit(1)
except RuntimeError as e:
console.print(f"[red]Browser error:[/red] {e}")
raise SystemExit(1)
suffix = f" in group '{group}'" if group else (f" in window '{window}'" if window else "")
display = _DISPLAY_NAMES.get(engine_key, engine_key.capitalize())
console.print(f"[green]Searching[/green] [cyan]{display}[/cyan]: {terms}{suffix}")
return _cmd
for _name, _help in _SUBCOMMANDS:
search_group.add_command(_build_command(_name, _help))