Surfx
A fast, clean, terminal-first web search CLI for developers and power users.
Search the web without leaving your terminal - powered by SearXNG, an open-source, privacy-respecting metasearch engine. Surfx queries a SearXNG instance over its documented JSON API; it does not scrape Google (or any other engine) directly, and it never requires you to create an API key or a cloud account.
╭──────────────────────────────────────────╮
│ Surfx │
│ Search results for "python asyncio" │
╰──────────────────────────────────────────╯
[1] Asyncio — Python documentation
https://docs.python.org/3/library/asyncio.html
Asyncio is a library to write concurrent code using async/await syntax.
via duckduckgo
[2] Async IO in Python: A Complete Walkthrough
https://realpython.com/async-io-python/
...
What is Surfx?
Surfx is a small, well-tested CLI that lets you run real web searches from the terminal and get clean, readable, Rich-formatted results (or raw JSON for scripting). It's built around a provider abstraction, so the underlying search backend can change without touching the CLI itself.
What is SearXNG, and why does Surfx need an instance URL?
SearXNG is open-source software that runs a "metasearch" service: it queries a number of upstream search engines on your behalf (Google, Bing, DuckDuckGo, and many others, depending on how the instance is configured) and returns aggregated results, without tracking you. Anyone can run a SearXNG instance, so Surfx needs to know which instance to talk to - that's the one thing you configure.
You have two options:
- Self-host an instance (recommended for regular use, and the most private option) - see the SearXNG installation docs. A basic Docker setup takes a few minutes.
- Use a public instance that has JSON output enabled - browse searx.space for currently active public instances and their capabilities. Public instances are run by volunteers, can disappear or change their settings at any time, and see the queries you send them - don't rely on one for anything sensitive.
Surfx does not ship with a hardcoded default instance: public instances are unreliable enough (many disable the JSON API entirely) that guessing one for you would just produce confusing failures. Point Surfx at an instance you trust instead.
Installation
pip install surfx
Requires Python 3.11+.
Configure a SearXNG instance
Set the instance URL as an environment variable:
export SURFX_SEARXNG_URL="https://your-searxng-instance.example"
Windows PowerShell:
$env:SURFX_SEARXNG_URL="https://your-searxng-instance.example"
Or store it in Surfx's config file instead (see Configuration):
surfx config set searxng_url https://your-searxng-instance.example
Then run:
surfx "python httpx tutorial"
No API keys, cloud accounts, or billing are involved - the instance URL is the only thing Surfx needs. (No secrets ever end up in the example commands above, and Surfx never writes secrets to its config file or cache in any case - see Security.)
Usage
# Basic search
surfx "python asyncio tutorial"
# Control how many results come back (1-50)
surfx "python asyncio tutorial" --limit 20
surfx "python asyncio tutorial" -n 20
# Choose a provider explicitly (currently: searxng)
surfx "python asyncio tutorial" --provider searxng
# Machine-readable output (stdout is valid JSON only; errors go to stderr)
surfx "python asyncio tutorial" --json
# Disable colors/styling, e.g. for logging pipelines
surfx "python asyncio tutorial" --no-color
# Open a specific result in your default browser
surfx "python asyncio tutorial" --open 1
# Skip the "open this?" confirmation prompt
surfx "python asyncio tutorial" --open 1 --yes
# Show full tracebacks for debugging
surfx "python asyncio tutorial" --debug
# Check your setup, including that your SearXNG instance is reachable
surfx doctor
# Inspect configuration
surfx config
surfx config set searxng_url https://your-searxng-instance.example
surfx config set limit 10
surfx --version
surfx --help
Interactive mode
Running surfx with no query drops you into an interactive search prompt:
Surfx
Search > python decorators
[1] ...
[2] ...
Search > machine learning tutorials
[1] ...
Search > /exit
Supported interactive commands: /help, /clear, /exit, /quit.
Configuration
Surfx resolves settings using this precedence, highest first:
CLI arguments
↓
environment variables
↓
config file (~/.config/surfx/config.toml)
↓
built-in defaults
The config file may contain:
provider = "searxng"
limit = 10
searxng_url = "https://your-searxng-instance.example"
Secret-shaped values (API keys, tokens, etc.) can never be stored in the config file - only environment variables are accepted for those. The SearXNG URL isn't a secret, so it may live in either place.
| Environment variable | Purpose |
|---|---|
SURFX_SEARXNG_URL |
SearXNG instance URL (e.g. https://...) |
SURFX_PROVIDER |
Default provider (searxng) |
SURFX_LIMIT |
Default result limit (1-50) |
SURFX_NO_COLOR |
Disable styled output when truthy |
SURFX_CACHE_ENABLED |
Enable the local result cache |
SURFX_CACHE_TTL_SECONDS |
Cache entry lifetime in seconds |
Architecture
Surfx separates concerns into a few small layers:
providers/- one module per search backend, each implementing theSearchProviderprotocol (validate_config(),search(query, limit)) and returning provider-independentSearchResultobjects. The CLI and service layer never see a provider's raw response format. The current implementation isSearXNGProvider, which talks to a SearXNG instance's/search?format=jsonendpoint.services/search.py- orchestrates provider selection, limit validation, and the optional cache.services/cache.py- a small, conservative, optional local cache (disabled by default, short TTL, never stores credentials).cli.py- the Typer-based command-line interface and interactive mode.terminal.py- Rich-based rendering, kept separate from business logic so it's easy to test the rest of the app without a real terminal.
Adding a new provider means adding one file under providers/ and
registering it in providers/__init__.py - no changes to the CLI
required. See CONTRIBUTING.md for the full walkthrough.
Development
git clone https://github.com/Sam3360/surfx.git
cd surfx
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest
ruff check .
ruff format --check .
The test suite runs entirely offline - all HTTP calls to SearXNG are mocked, so no real instance or network access is required.
Publishing
python -m build
python -m twine check dist/*
python -m twine upload dist/*
Releases to PyPI are automated via GitHub Actions
(.github/workflows/publish.yml) using
PyPI Trusted Publishing (OIDC),
triggered by pushing a v* tag - no long-lived PyPI token is stored in this
repository.
License
MIT - see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file surfx-0.2.0.tar.gz.
File metadata
- Download URL: surfx-0.2.0.tar.gz
- Upload date:
- Size: 25.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61dab9bb999b757ccdab813ec7883ae64d2adc06a81c637c1f83b8b4bc90682c
|
|
| MD5 |
989470520607c003bbfb1a5e3b32407b
|
|
| BLAKE2b-256 |
c470ddd09b26e10ab90d573da1103601cfd391ed60d8cb39a5ae7e2d1da71e7e
|
File details
Details for the file surfx-0.2.0-py3-none-any.whl.
File metadata
- Download URL: surfx-0.2.0-py3-none-any.whl
- Upload date:
- Size: 21.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f8bde8bd1a0d6adf081ecde0f4584d7848cb5237a352e385ba387e6d1bd6da3e
|
|
| MD5 |
de2c25562e1528f921b006ecefd35a2b
|
|
| BLAKE2b-256 |
464cd9894ce1523173dc1faee5986b1198af396885ebe743b399977e1230dcc5
|