diff --git a/pygitweb/README.md b/pygitweb/README.md
index 8be1ee1..8e3dcf1 100644
--- a/pygitweb/README.md
+++ b/pygitweb/README.md
@@ -78,7 +78,8 @@ Load the `/docs` page for a detailed view of routes (below the readme) if in deb
 | blame_data | — | | TODO |
 | rss | — | | TODO |
 | atom | — | | TODO |
-| search | — | | TODO |
+| search | `patterns`, `paths`, `globs`, `heading`, `sort`, `max_count`, `multiline` | `GET /project/{project}?a=search&patterns=...` | Ripgrep search (via `python-ripgrep`); returns JSON. `paths` are relative to the project tree and cannot escape it. |
+| search (page) | — | `GET /project/{project}/search` | Per-project search UI page. |
 | search_help | — | | TODO |
 
 ## Query parameter short names (CGI mapping)
diff --git a/pygitweb/actions.py b/pygitweb/actions.py
index 4f0dfb2..964e830 100644
--- a/pygitweb/actions.py
+++ b/pygitweb/actions.py
@@ -9,12 +9,15 @@ import base64
 import mimetypes
 import os
 from datetime import UTC, datetime, timedelta, timezone
+from pathlib import Path
 from typing import Any, TypedDict
 from urllib.parse import quote, urlencode
 
 import pygit2
 from fastapi import HTTPException, Request
-from fastapi.responses import HTMLResponse, PlainTextResponse, Response
+from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response
+from python_ripgrep import PySortMode, PySortModeKind
+from python_ripgrep import search as rg_search
 
 from pygitweb.config import BLOB_LANG, settings
 from pygitweb.formatting import age_string, sanitize, to_utf8
@@ -41,6 +44,77 @@ from pygitweb.templates_env import POSTAMBLE, PREAMBLE, env, jinja_escape
 from pygitweb.validation import is_valid_pathname, is_valid_ref_format
 
 
+def _split_query_list(values: list[str]) -> list[str]:
+	parts: list[str] = []
+	for v in values:
+		for seg in (v or "").split(","):
+			s = seg.strip()
+			if s:
+				parts.append(s)
+	return parts
+
+
+def _parse_query_bool(value: str | None) -> bool | None:
+	if value is None:
+		return None
+	v = value.strip().lower()
+	if v in ("1", "true", "yes", "on"):
+		return True
+	if v in ("0", "false", "no", "off"):
+		return False
+	raise HTTPException(status_code=400, detail=f"Invalid boolean: {value!r}")
+
+
+def _project_worktree_root(project: str) -> Path:
+	root = (Path(settings.PROJECTROOT) / project).resolve()
+	if not root.exists() or not root.is_dir():
+		raise HTTPException(status_code=404, detail="Project directory not found")
+	return root
+
+
+def _validated_search_paths(project_root: Path, raw_paths: list[str]) -> list[str]:
+	root = project_root.resolve()
+	if not raw_paths:
+		return [str(root)]
+
+	paths: list[str] = []
+	for raw in raw_paths:
+		p = (raw or "").strip()
+		if not p:
+			continue
+		candidate = Path(p)
+		if candidate.is_absolute() or ":" in p:
+			raise HTTPException(status_code=400, detail="path not relative to the project root")
+		abs_path = (root / p).resolve()
+		if not abs_path.is_relative_to(root):
+			raise HTTPException(status_code=400, detail="path not within the project root")
+		if not abs_path.exists():
+			raise HTTPException(status_code=400, detail=f"path entry does not exist: {p}")
+		paths.append(str(abs_path))
+
+	if not paths:
+		return [str(root)]
+	return paths
+
+
+_SORT_KINDS: tuple[str, ...] = ("Path", "LastModified", "LastAccessed", "Created")
+
+
+def _parse_search_sort(value: str | None) -> PySortMode | None:
+	if value is None or not value.strip():
+		return None
+	raw = value.strip()
+	reverse = False
+	if raw.startswith("-"):
+		reverse = True
+		raw = raw[1:].strip()
+	if raw not in _SORT_KINDS:
+		allowed = ", ".join(_SORT_KINDS)
+		raise HTTPException(status_code=400, detail=f"Invalid sort kind; allowed: {allowed}")
+	kind = getattr(PySortModeKind, raw)
+	return PySortMode(kind=kind, reverse=reverse)
+
+
 def _pagination_url(request: Request, page: int, pagecount: int) -> str:
 	"""Build URL for a pagination page, preserving path and other query params."""
 	params = dict(request.query_params)
@@ -336,6 +410,80 @@ def git_object(project: str, h: str | None) -> Response:
 	raise HTTPException(status_code=404, detail="Unknown object type")
 
 
+def git_search(project: str, request: Request) -> JSONResponse:
+	qp = request.query_params
+	patterns = _split_query_list(qp.getlist("patterns"))
+	if not patterns:
+		raise HTTPException(status_code=400, detail="patterns is required (repeat ?patterns= or use comma-separated)")
+
+	project_root = _project_worktree_root(project)
+	paths = _validated_search_paths(project_root, _split_query_list(qp.getlist("paths")))
+
+	globs = _split_query_list(qp.getlist("globs"))
+	heading = _parse_query_bool(qp.get("heading"))
+	multiline = _parse_query_bool(qp.get("multiline"))
+	sort = _parse_search_sort(qp.get("sort"))
+
+	max_count_raw = qp.get("max_count")
+	max_count: int = 25
+	if max_count_raw is not None and max_count_raw.strip() != "":
+		try:
+			max_count = int(max_count_raw)
+		except ValueError as e:
+			raise HTTPException(status_code=400, detail="max_count must be an integer") from e
+		if max_count < 1:
+			raise HTTPException(status_code=400, detail="max_count must be at least 1")
+
+	try:
+		results = rg_search(
+			patterns=patterns,
+			paths=paths,
+			globs=(globs or None),
+			heading=heading,
+			sort=sort,
+			max_count=max_count,
+			multiline=multiline,
+		)
+	except Exception as e:
+		raise HTTPException(status_code=500, detail=f"Search failed: {e}") from e
+
+	root_str = str(project_root.resolve())
+	rel_paths: list[str] = []
+	for p in paths:
+		abs_p = str(Path(p).resolve())
+		rel = os.path.relpath(abs_p, root_str)
+		rel_paths.append("" if rel == "." else rel.replace(os.sep, "/"))
+
+	return JSONResponse({
+		"project": project,
+		"project_root": root_str,
+		"patterns": patterns,
+		"paths": rel_paths,
+		"globs": globs,
+		"heading": heading,
+		"sort": (qp.get("sort") or None),
+		"max_count": max_count,
+		"multiline": multiline,
+		"results": results,
+	})
+
+
+def git_search_page(project: str) -> HTMLResponse:
+	"""Per-project search UI page. Calls /project/{project}?a=search via fetch."""
+	project_enc = quote(project, safe="/")
+	pre = PREAMBLE.render(
+		title=f"Search - {project}",
+		site_name=settings.SITE_NAME,
+	)
+	body = env.get_template("search.html").render(
+		project=project,
+		project_enc=project_enc,
+		project_url=f"/project/{project_enc}",
+		search_api_url=f"/project/{project_enc}",
+	)
+	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")
+
+
 def git_summary(project: str, extra_rows: list[list[str]] | None = None) -> HTMLResponse:
 	"""Project summary page. Port of git_summary."""
 	descr = git_get_project_description(project) or ""
@@ -376,6 +524,7 @@ def git_summary(project: str, extra_rows: list[list[str]] | None = None) -> HTML
 		["Tags", f"<a href='/project/{project}?a=tags'>view tags</a>"],
 		["Remotes", f"<a href='/project/{project}?a=remotes'>view remotes</a>"],
 		["Board", board_value],
+		["Search", f"<a href='/project/{project_enc}/search'>search files</a>"],
 	]
 	if extra_rows:
 		rows.extend(extra_rows)
diff --git a/pygitweb/main.py b/pygitweb/main.py
index a90a9ff..107c750 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -31,6 +31,8 @@ from pygitweb.actions import (
 	git_patch,
 	git_patches,
 	git_remotes,
+	git_search,
+	git_search_page,
 	git_shortlog,
 	git_summary,
 	git_tag,
@@ -513,6 +515,13 @@ def project_summary_ref_state(
 	return summary_ref_state(project, ref)
 
 
+@app.get("/project/{project:path}/search", response_class=HTMLResponse)
+def project_search_page(project: str) -> HTMLResponse:
+	"""Per-project search page (UI only; results fetched from /project/{project}?a=search)."""
+	_validate_project(project)
+	return git_search_page(project)
+
+
 @app.get("/project/{project:path}/subpage/{subpage_name}", response_class=HTMLResponse)
 def project_subpage(
 	project: str,
@@ -635,6 +644,8 @@ def dispatch(
 		return git_remotes(project)
 	if action == "object":
 		return git_object(project, hash_param)
+	if action == "search":
+		return git_search(project, request)
 	# Stub others with minimal response
 	pre = PREAMBLE.render(title=f"{action} - {project}", site_name=settings.SITE_NAME)
 	return HTMLResponse(f"{pre}<p>Action: {jinja_escape(action)}</p><p>Project: {jinja_escape(project)}</p>{POSTAMBLE}")
diff --git a/pygitweb/pyproject.toml b/pygitweb/pyproject.toml
index cf30b5c..7b6ec90 100644
--- a/pygitweb/pyproject.toml
+++ b/pygitweb/pyproject.toml
@@ -17,6 +17,7 @@ dependencies = [
     "orjson>=0.19.0",
     "pygit2>=1.12.0",
     "pydantic-settings>=2.13.0",
+    "python-ripgrep==0.0.9",
 ]
 
 [tool.setuptools]
diff --git a/pygitweb/static/main.css b/pygitweb/static/main.css
index 5faf3f8..4725a0f 100644
--- a/pygitweb/static/main.css
+++ b/pygitweb/static/main.css
@@ -840,6 +840,42 @@ tbody tr {
 	}
 }
 
+/* Search page: input, status, and result lines */
+.search-status {
+	font-style: italic;
+}
+
+.search-results {
+	display: flex;
+	flex-direction: column;
+	gap: 0.25rem;
+
+	&.is-loading {
+		opacity: 0.6;
+	}
+}
+
+.search-result {
+	background-color: var(--pgw-bg-raised);
+	border: 1px solid var(--pgw-border);
+	border-radius: 0.25rem;
+	padding: 0.25rem 0.5rem;
+
+	&:hover {
+		background-color: var(--pgw-table-hover);
+	}
+}
+
+.search-result-line {
+	margin: 0;
+	font-family: ui-monospace, "Fira Code", "Fira Mono", monospace;
+	font-size: 0.85rem;
+	line-height: 1.4;
+	color: var(--pgw-text);
+	white-space: pre-wrap;
+	word-break: break-word;
+}
+
 .subpage-frame {
 	display: block;
 	width: 100%;
diff --git a/pygitweb/static/search-page.js b/pygitweb/static/search-page.js
new file mode 100644
index 0000000..ec952c5
--- /dev/null
+++ b/pygitweb/static/search-page.js
@@ -0,0 +1,139 @@
+(function () {
+	"use strict";
+
+	const apiUrl = window.SEARCH_API_URL;
+	const elPattern = document.getElementById("search-patterns");
+	const elPaths = document.getElementById("search-paths");
+	const elGlobs = document.getElementById("search-globs");
+	const elSort = document.getElementById("search-sort");
+	const elMax = document.getElementById("search-max-count");
+	const elHead = document.getElementById("search-heading");
+	const elMulti = document.getElementById("search-multiline");
+	const elStatus = document.getElementById("search-status");
+	const elResults = document.getElementById("search-results");
+	const elAdvanced = document.getElementById("search-advanced");
+
+	function escapeHtml(s) {
+		return String(s).replace(/[&<>"']/g, function (c) {
+			return ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c];
+		});
+	}
+
+	function readUrl() {
+		const p = new URLSearchParams(window.location.search);
+		if (p.has("patterns")) elPattern.value = p.get("patterns");
+		if (p.has("paths")) elPaths.value = p.get("paths");
+		if (p.has("globs")) elGlobs.value = p.get("globs");
+		if (p.has("sort")) elSort.value = p.get("sort");
+		elMax.value = p.has("max_count") ? p.get("max_count") : "25";
+		if (p.has("heading")) elHead.checked = p.get("heading") === "true";
+		if (p.has("multiline")) elMulti.checked = p.get("multiline") === "true";
+		const anyAdvanced = ["paths", "globs", "sort", "multiline"].some((k) => p.has(k));
+		if (anyAdvanced) elAdvanced.open = true;
+	}
+
+	function buildParams() {
+		const qs = new URLSearchParams();
+		qs.set("a", "search");
+		const pat = elPattern.value.trim();
+		if (pat) qs.set("patterns", pat);
+		const paths = elPaths.value.trim();
+		if (paths) qs.set("paths", paths);
+		const globs = elGlobs.value.trim();
+		if (globs) qs.set("globs", globs);
+		if (elSort.value) qs.set("sort", elSort.value);
+		const max = elMax.value.trim();
+		if (max) qs.set("max_count", max);
+		qs.set("heading", elHead.checked ? "true" : "false");
+		qs.set("multiline", elMulti.checked ? "true" : "false");
+		return qs;
+	}
+
+	function persistUrl(qs) {
+		const url = new URL(window.location.href);
+		const keep = new Set(Array.from(qs.keys()).filter((k) => k !== "a"));
+		Array.from(url.searchParams.keys()).forEach((k) => {
+			if (!keep.has(k)) url.searchParams.delete(k);
+		});
+		for (const [k, v] of qs.entries()) {
+			if (k === "a") continue;
+			url.searchParams.set(k, v);
+		}
+		window.history.replaceState(null, "", url);
+	}
+
+	function renderResults(data) {
+		const lines = Array.isArray(data.results) ? data.results : [];
+		if (lines.length === 0) {
+			elResults.innerHTML = "";
+			return;
+		}
+		const items = lines.map((line) => {
+			return '<div class="search-result"><pre class="search-result-line">' + escapeHtml(line) + "</pre></div>";
+		});
+		elResults.innerHTML = items.join("");
+	}
+
+	let pendingCtrl = null;
+
+	async function runSearch() {
+		const pat = elPattern.value.trim();
+		if (!pat) {
+			elStatus.textContent = "";
+			elResults.innerHTML = "";
+			persistUrl(new URLSearchParams());
+			return;
+		}
+		const qs = buildParams();
+		persistUrl(qs);
+		if (pendingCtrl) pendingCtrl.abort();
+		pendingCtrl = new AbortController();
+		elStatus.textContent = "Searching\u2026";
+		elResults.classList.add("is-loading");
+		try {
+			const resp = await fetch(apiUrl + "?" + qs.toString(), {
+				signal: pendingCtrl.signal,
+				headers: { Accept: "application/json" },
+			});
+			if (!resp.ok) {
+				let detail = resp.statusText;
+				try {
+					const j = await resp.json();
+					if (j && j.detail) detail = j.detail;
+				} catch (_) { /* ignore */ }
+				elStatus.textContent = "Error " + resp.status + ": " + detail;
+				elResults.innerHTML = "";
+				return;
+			}
+			const data = await resp.json();
+			const n = (data.results || []).length;
+			elStatus.textContent = n + " match" + (n === 1 ? "" : "es");
+			renderResults(data);
+		} catch (err) {
+			if (err && err.name === "AbortError") return;
+			elStatus.textContent = "Error: " + (err && err.message ? err.message : String(err));
+		} finally {
+			elResults.classList.remove("is-loading");
+		}
+	}
+
+	let debounceTimer = null;
+	function debounced() {
+		clearTimeout(debounceTimer);
+		debounceTimer = setTimeout(runSearch, 500);
+	}
+
+	for (const el of [elPattern, elPaths, elGlobs, elMax]) el.addEventListener("input", debounced);
+	for (const el of [elSort, elHead, elMulti]) el.addEventListener("change", debounced);
+
+	elPattern.addEventListener("keydown", function (event) {
+		if (event.key === "Enter") {
+			event.preventDefault();
+			clearTimeout(debounceTimer);
+			runSearch();
+		}
+	});
+
+	readUrl();
+	runSearch();
+})();
diff --git a/pygitweb/templates/search.html b/pygitweb/templates/search.html
new file mode 100644
index 0000000..687d18a
--- /dev/null
+++ b/pygitweb/templates/search.html
@@ -0,0 +1,77 @@
+{# Search UI page. Expects: project, project_enc, project_url, search_api_url. #}
+
+<h1 class="page-title d-flex align-items-center gap-2">
+  <a href="{{ project_url }}" class="text-reset">{{ project }}</a>
+  <span class="text-muted">/</span>
+  <span>search</span>
+</h1>
+
+<div class="card mb-3">
+  <div class="card-body">
+    <form id="search-form" class="search-container" autocomplete="off" onsubmit="return false;">
+      <input
+        id="search-patterns"
+        class="form-control search"
+        type="text"
+        spellcheck="false"
+        autocomplete="off"
+        placeholder="Search regex (e.g. TODO|FIXME)..."
+        aria-label="Search pattern"
+      >
+
+      <details id="search-advanced" class="mt-2">
+        <summary>Advanced options</summary>
+        <div class="row g-2 mt-1">
+          <div class="col-md-6">
+            <label class="form-label" for="search-paths">Paths (relative, comma separated)</label>
+            <input id="search-paths" class="form-control" type="text" placeholder="src,docs">
+            <div class="form-hint">Restricted to the project tree.</div>
+          </div>
+          <div class="col-md-6">
+            <label class="form-label" for="search-globs">Globs (comma separated)</label>
+            <input id="search-globs" class="form-control" type="text" placeholder="*.py,*.md,!**/vendor/**">
+          </div>
+          <div class="col-md-3">
+            <label class="form-label" for="search-sort">Sort</label>
+            <select id="search-sort" class="form-select">
+              <option value="">(none)</option>
+              <option value="Path">Path</option>
+              <option value="-Path">Path (reverse)</option>
+              <option value="LastModified">Last modified</option>
+              <option value="-LastModified">Last modified (reverse)</option>
+              <option value="LastAccessed">Last accessed</option>
+              <option value="-LastAccessed">Last accessed (reverse)</option>
+              <option value="Created">Created</option>
+              <option value="-Created">Created (reverse)</option>
+            </select>
+          </div>
+          <div class="col-md-3">
+            <label class="form-label" for="search-max-count">Max matches per file</label>
+            <input id="search-max-count" class="form-control" type="number" min="1" value="25">
+          </div>
+          <div class="col-md-3 align-self-end">
+            <label class="form-check">
+              <input id="search-heading" class="form-check-input" type="checkbox" checked>
+              <span class="form-check-label">Heading (group by file)</span>
+            </label>
+          </div>
+          <div class="col-md-3 align-self-end">
+            <label class="form-check">
+              <input id="search-multiline" class="form-check-input" type="checkbox">
+              <span class="form-check-label">Multiline</span>
+            </label>
+          </div>
+        </div>
+      </details>
+    </form>
+  </div>
+</div>
+
+<div id="search-status" class="search-status text-muted mb-2" aria-live="polite"></div>
+<div id="search-results" class="search-results" role="region" aria-label="Search results"></div>
+
+<script>
+  window.SEARCH_PROJECT = {{ project | tojson }};
+  window.SEARCH_API_URL = {{ search_api_url | tojson }};
+</script>
+<script src="/static/search-page.js"></script>
diff --git a/uv.lock b/uv.lock
index a654d5e..db25484 100644
--- a/uv.lock
+++ b/uv.lock
@@ -957,6 +957,7 @@ dependencies = [
     { name = "pygit2" },
     { name = "pygittools" },
     { name = "python-multipart" },
+    { name = "python-ripgrep" },
     { name = "uvicorn", extra = ["standard"] },
 ]
 
@@ -969,6 +970,7 @@ requires-dist = [
     { name = "pygit2", specifier = ">=1.12.0" },
     { name = "pygittools", editable = "pygittools" },
     { name = "python-multipart", specifier = ">=0.0.6" },
+    { name = "python-ripgrep", specifier = "==0.0.9" },
     { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" },
 ]
 
@@ -1074,6 +1076,28 @@ wheels = [
 ]
 
 [[package]]
+name = "python-ripgrep"
+version = "0.0.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/8f/98b524a7186e722dd5c33e033db28e30a0000438c5b2493b842f2766bcd0/python_ripgrep-0.0.9.tar.gz", hash = "sha256:5a65b8252c76df9c523b837c2a4f86478cc2af996bac4d10f9b321c3768e259b", size = 45207, upload-time = "2025-07-30T19:03:29.361Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/75/38/ef6ae18f59ee03e4e6666f86ccd1b7a31ee503768e6497eb71dfac1acb3d/python_ripgrep-0.0.9-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a1a38b5a39beeb1b07a7e0d27942442fd61e61065660aa986051c90bcdaa323f", size = 1607445, upload-time = "2025-07-30T19:56:25.056Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/1a/3d20679e64dacc467d95bd6630d76e1b57125efa09cb6a5f9e158cdb1f6e/python_ripgrep-0.0.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:83d9b7908ce61a2fc3bb4e2318705c87099cc43a4453e6768e860a329d0a1a7a", size = 1543925, upload-time = "2025-07-30T19:03:28.187Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/50/77f6eb5dfe56d55fd42da383017a3c2a9d42555bea232fc7ebb8d3291271/python_ripgrep-0.0.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e11004a12a31247c1d5a77bfb8a71793fa552a355c96498f68937348175ba2e", size = 1719093, upload-time = "2025-07-30T19:56:16.426Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/0f/4f2f624bc30783018649779c8b669e37d866c5b5caeb7e3a8e53bcc6256b/python_ripgrep-0.0.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:850ac515eb2ff370ccb0ddd934ca32f06f75e5fa5a3099111dad140079a26d99", size = 1669712, upload-time = "2025-07-30T19:56:18.154Z" },
+    { url = "https://files.pythonhosted.org/packages/25/b5/5fa748faeaee82e65983e450b5b52e626f0bdcb43420e113c3d0831c9e49/python_ripgrep-0.0.9-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:193db1978e52ec2959913bee0189a1f5366178bcf19473de702ac9235f122740", size = 1816691, upload-time = "2025-07-30T19:56:22.497Z" },
+    { url = "https://files.pythonhosted.org/packages/97/05/0c2ab823a9fcbbbf3f826c30bac762fc0a520f6ee111c9a83222cf810e27/python_ripgrep-0.0.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec6a25229cc5998860ed4c4a14e1b29816ed98f8d17d4847d6fbe625e0bfdfda", size = 2032756, upload-time = "2025-07-30T19:56:19.87Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/3f/19784133a96609e7682cf8020068d0f4810509d27eee6916b51ebbdc441b/python_ripgrep-0.0.9-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f9ff586d1f6667f60e29d5eadebcf28c01f9d68851662ad84665d83d940ed2d", size = 1878113, upload-time = "2025-07-30T19:56:21.315Z" },
+    { url = "https://files.pythonhosted.org/packages/74/b4/ac6b588d8437cdf22d9450cd87a2939ca91bd304ce7fa400484978ce6394/python_ripgrep-0.0.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acae870b9dba3d5a70aeeab687809dfad7c2afe3c422fedb9fca3d578b5d422", size = 1780017, upload-time = "2025-07-30T19:56:23.734Z" },
+    { url = "https://files.pythonhosted.org/packages/85/a5/baab3499501710421ba9ea0199f3ba2450bdf8eedaf9de6b5a043513d925/python_ripgrep-0.0.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2be02995828e36c8b37cdb6af50178021bc580f0ca6766f4d14806aa904641ae", size = 1893412, upload-time = "2025-07-30T19:56:26.123Z" },
+    { url = "https://files.pythonhosted.org/packages/73/88/d6d06fb08401a00ef58e6a3f6f486246e4cfd3b82cc7fed8c187fd273547/python_ripgrep-0.0.9-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fcf0007fd3e39b5fd2cc941af5eacb24de2d5268ad41ae6edaf7abbc5b3965d4", size = 1927522, upload-time = "2025-07-30T19:56:27.458Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/41/e8cbd79664bb091bfd3310758809c73ccf05d92a94b9768d913f667df03c/python_ripgrep-0.0.9-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:004b1223684f457ab96d180edb2ba64bc1c0c0b26053254bc345443e6a3d83c8", size = 1915262, upload-time = "2025-07-30T19:56:28.676Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/98/4830184a0fba24958f68f4c0e6749dd7ace120e08cbe1a70707fc05a98c1/python_ripgrep-0.0.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80cb59c90e98cf83325b4dbad36c1569fc9c658aa2d1244943bf0d98352227d4", size = 1951646, upload-time = "2025-07-30T19:56:29.741Z" },
+    { url = "https://files.pythonhosted.org/packages/30/2f/de02bc9476f56ae4d0a7838319d8b02b5f3ac45a9eee710f17c49738797e/python_ripgrep-0.0.9-cp38-abi3-win32.whl", hash = "sha256:69b4039f1c86313a4f39350db19a720c41a1b5396b61abb5c9b9077794b740ad", size = 1308752, upload-time = "2025-07-30T19:56:31.999Z" },
+    { url = "https://files.pythonhosted.org/packages/53/0f/bcbec2b9fb69be66600c63b97b18b50fa24decc9dd0cc826acc9d8c970dc/python_ripgrep-0.0.9-cp38-abi3-win_amd64.whl", hash = "sha256:fb92b64c9e854ab74e15ab4348f93fbc2c2a7384ab946be5e1fa59cc98cb516f", size = 1410074, upload-time = "2025-07-30T19:56:30.893Z" },
+]
+
+[[package]]
 name = "pyyaml"
 version = "6.0.3"
 source = { registry = "https://pypi.org/simple" }
