diff --git a/Dockerfile b/Dockerfile
index 871f296..b21b444 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,13 +1,17 @@
FROM recoll
-RUN apt-get update && apt-get -y install breeze-icon-theme python3-venv && apt-get clean
+RUN apt-get -y install breeze-icon-theme python3-venv && apt-get clean
-ARG SQUIRREL_VERSION=0.3.1
+ARG SQUIRREL_VERSION=0.4.0
COPY dist/squirrel-${SQUIRREL_VERSION}-py3-none-any.whl squirrel-${SQUIRREL_VERSION}-py3-none-any.whl
RUN python3 -m venv --system-site-packages /opt/squirrel
RUN /opt/squirrel/bin/pip install squirrel-${SQUIRREL_VERSION}-py3-none-any.whl
+RUN /opt/squirrel/bin/pip install gunicorn
+COPY squirrel-cmd /opt/squirrel/bin/squirrel
+
+ENV SQUIRREL_CONFIG /etc/squirrel/config.json
EXPOSE 8000
diff --git a/pyproject.toml b/pyproject.toml
index 412ebf7..f7a5089 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,18 +1,20 @@
[project]
name = "squirrel"
-version = "0.3.1"
+version = "0.4.0"
description = "Find your nuts"
authors = [
- {name = "Your Name",email = "you@example.com"}
+ {name = "trisf"}
]
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
- "fastapi[standard] (>=0.118.0,<0.119.0)"
+ "flask (>=3.1.2,<4.0.0)",
+ "pillow (>=11.3.0,<12.0.0)",
+ "dotenv (>=0.9.9,<0.10.0)"
]
-[project.scripts]
-"squirrel" = "squirrel.server:daemon"
+#[project.scripts]
+#"squirrel" = "squirrel.server:daemon"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
diff --git a/squirrel-cmd b/squirrel-cmd
new file mode 100755
index 0000000..f4d1956
--- /dev/null
+++ b/squirrel-cmd
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+/opt/squirrel/bin/gunicorn -w 4 -b 0.0.0.0 "squirrel.main:app"
diff --git a/squirrel/api.py b/squirrel/api.py
index 56b8d77..bee9cef 100644
--- a/squirrel/api.py
+++ b/squirrel/api.py
@@ -1,107 +1,57 @@
-from math import ceil
-from fastapi import FastAPI, Request, BackgroundTasks
-from fastapi.staticfiles import StaticFiles
-from fastapi.templating import Jinja2Templates
-from typing import Any
+from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
+
+from typing import Annotated, Any
import logging
-import subprocess
-import os.path
-from dotenv import dotenv_values
-
-from recoll import recoll
-
+from squirrel.auth import User, auth_user, auth_user_or_guest # type: ignore
+from squirrel.conf import settings # type: ignore
+from squirrel import repo # type:ignore
logger = logging.getLogger()
-logging.basicConfig(level=logging.DEBUG)
-config = {**dotenv_values(".env"), **os.environ}
+router = APIRouter(prefix="/api")
-SQUIRREL_APP = os.path.dirname(os.path.realpath(__file__)) + "/app"
-
-SQUIRREL_ICONS = config.get("SQUIRREL_ICONS", "/usr/share/icons/breeze/mimetypes/32")
-
-DOC_FOLDER = config.get("SQUIRREL_DOCS", "/mnt/docs")
-
-STRIP_CHARS = len(DOC_FOLDER) + len("file:///")
+archive = repo.get_archive()
-class Recoll(object):
- def __init__(self):
- self._indexing = False
-
- def reindex(self):
- if self._indexing:
- raise RuntimeError("Reindex already underway")
- self._indexing = True
- subprocess.call(["recollindex"])
- self._indexing = False
+@router.get("/reindex")
+async def get_reindex(
+ user: Annotated[User, Depends(auth_user)],
+ tasks: BackgroundTasks,
+ repo: str = "main",
+) -> dict[str, str]:
+ if repo not in user.repos:
+ raise HTTPException(status_code=403, detail="No privelleges on that repo")
+ tasks.add_task(archive.reindex, repo)
+ return {"message": f"Started indexing of {repo}"}
-repo = Recoll()
-
-app = FastAPI()
-app.mount("/static", StaticFiles(directory=f"{SQUIRREL_APP}/static"))
-app.mount("/docs", StaticFiles(directory=DOC_FOLDER))
-app.mount(
- "/icons",
- StaticFiles(directory=SQUIRREL_ICONS, follow_symlink=True),
-)
-
-templates = Jinja2Templates(directory=f"{SQUIRREL_APP}/templates")
+@router.get("/search")
+def get_search(
+ user: Annotated[User, Depends(auth_user_or_guest)],
+ q: str,
+ count: int = 20,
+ offset: int = 1,
+) -> dict[str, Any]:
+ return archive.query(q, count, offset, extra=user.repos)
-@app.get("/")
-def home(request: Request):
- ctx = {"request": request}
- return templates.TemplateResponse("home.html", context=ctx)
+@router.get("/repos")
+def get_repos():
+ return archive.user_repos("foo")
-@app.get("/search")
-def search_results(request: Request, q: str, count: int = 20, page: int = 1):
- ctx: dict[str, Any] = {"request": request}
- ctx["search"] = search(q, count, page)
- return templates.TemplateResponse("search_results.html", context=ctx)
+@router.get("/complete")
+def get_completions(q: str, field: str = "") -> list[str]:
+ if ":" in q:
+ field, _, q = q.partition(":")
-
-@app.get("/api/reindex")
-async def get_reindex(tasks: BackgroundTasks) -> dict[str, str]:
- tasks.add_task(repo.reindex)
- return {"message": "Recoll index started"}
-
-
-@app.get("/api/search")
-def get_search(q: str, count: int = 20, page: int = 1) -> dict[str, Any]:
- return search(q, count, page)
-
-
-def search(q: str, count: int, page: int):
- db = recoll.connect()
- query = db.query()
- nres = query.execute(q)
- result = {
- "query": q,
- "count": count,
- "page": page,
- "pages": ceil(nres / count),
- "total": nres,
- "xquery": query.getxquery(),
- "results": [],
- }
- if page > result["pages"] or page < 1:
- return result
-
- query.scroll((page - 1) * count)
- docs = query.fetchmany(count)
- for doc in docs:
- d = dict(doc)
- d["snippets"] = query.getsnippets(doc, ctxwords=20)
- d["fpath"] = d["url"][STRIP_CHARS:]
- d["title"] = d["title"] or d["filename"]
- result["results"].append(d)
+ db = archive.db([])
+ result = db.termMatch("wildcard", q + "*", field=field, maxlen=10)
+ if field:
+ result = [f"{field}:{x}" for x in result]
return result
-@app.get("/api/info")
+@router.get("/settings")
def get_info():
- db = recoll.connect()
- return dir(db)
+ return settings
diff --git a/squirrel/app/static/css/search-source.css b/squirrel/app/static/css/search-source.css
deleted file mode 100644
index 59dcbe2..0000000
--- a/squirrel/app/static/css/search-source.css
+++ /dev/null
@@ -1,14 +0,0 @@
-@import "tailwindcss";
-
-SPAN.rclmatch {
- font-weight: bold;
- color: var(--color-red-900);
-}
-@font-face {
- font-family: beyondTheMountains; /* set name */
- src: url(/static/fonts/beyond_the_mountains.ttf); /* url of the font */
-}
-
-.font-beyond-the-mountains {
- font-family: beyondTheMountains;
-}
diff --git a/squirrel/app/templates/base.html b/squirrel/app/templates/base.html
deleted file mode 100644
index 742b448..0000000
--- a/squirrel/app/templates/base.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
Squirrel
-
-
-
- {% block scripts %}{% endblock %}
-
-
-
- {% block content %}CONTENT{% endblock %}
-
-
diff --git a/squirrel/app/templates/search_results.html b/squirrel/app/templates/search_results.html
deleted file mode 100644
index d5776bd..0000000
--- a/squirrel/app/templates/search_results.html
+++ /dev/null
@@ -1,54 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-
-
-
-
-
-
Page of ,
- results.
-
-
-
-
-
-
-
![]()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
diff --git a/squirrel/auth.py b/squirrel/auth.py
new file mode 100644
index 0000000..18f9ac1
--- /dev/null
+++ b/squirrel/auth.py
@@ -0,0 +1,96 @@
+from flask import (
+ Blueprint,
+ request,
+ render_template,
+ session,
+ redirect,
+ url_for,
+ flash,
+ g,
+)
+from werkzeug.security import check_password_hash, generate_password_hash
+from dataclasses import dataclass
+import functools
+
+bp = Blueprint("auth", __name__, url_prefix="/auth")
+
+
+@dataclass
+class User:
+ name: str
+ password: str
+ repos: list[str] | None = None
+
+
+users: dict[str, dict] = {
+ "guest": {"password": "x", "name": "Guest"},
+ "tris": {
+ "password": "scrypt:32768:8:1$4srkVJ5rxSAvZp8s$6a9b1bd9199ceb07a8e650897d6ca0c229330773a77728f83c110525c0cde5c9a019bcafbfd0274a2e532e6db03996ff34521d8f36758f4892bcc13107ac24e5",
+ "name": "Tris",
+ "repos": ["test"],
+ },
+}
+
+
+@bp.route("/login", methods=("GET", "POST"))
+def login():
+ username = ""
+ referer = request.args.get("referer") or request.headers.get("Referer")
+
+ if request.method == "POST":
+ username = request.form["username"]
+ password = request.form["password"]
+ error = None
+
+ user = users.get(username)
+
+ if user is None or not check_password_hash(user["password"], password):
+ error = "Incorrect username or password."
+
+ if error is None:
+ session.clear()
+ session["user"] = username
+ return redirect(referer or url_for("render_home"))
+
+ flash(error)
+
+ return render_template("auth/login.html", username=username, referer=referer)
+
+
+@bp.route("/generate", methods=("GET", "POST"))
+def generate_password():
+ hashed = None
+
+ if request.method == "POST":
+ password = request.form["password"]
+ hashed = generate_password_hash(password)
+
+ return render_template("auth/generate.html", hashed=hashed)
+
+
+@bp.route("/logout")
+def logout():
+ referer = request.headers.get("Referer")
+ session.clear()
+ return redirect(referer or url_for("auth.login"))
+
+
+def login_required(view):
+ @functools.wraps(view)
+ def wrapped_view(**kwargs):
+ if g.user is None:
+ return redirect(url_for("auth.login"))
+
+ return view(**kwargs)
+
+ return wrapped_view
+
+
+@bp.before_app_request
+def load_logged_in_user():
+ username = session.get("user")
+
+ if username is None:
+ g.user = User(**users["guest"])
+ else:
+ g.user = User(**users[username])
diff --git a/squirrel/conf.py b/squirrel/conf.py
new file mode 100644
index 0000000..60d3174
--- /dev/null
+++ b/squirrel/conf.py
@@ -0,0 +1,21 @@
+import os.path
+import json
+from dotenv import dotenv_values
+
+env = {**dotenv_values(".env"), **os.environ}
+
+SQUIRREL_DIR = os.path.expanduser(env.get("SQUIRREL_DIR", "~/.config/squirrel"))
+SQUIRREL_CONFIG = env.get("SQUIRREL_CONFIG", os.path.join(SQUIRREL_DIR, "config.json"))
+
+DEFAULT_SETTINGS = {}
+
+settings = {
+ "base_dir": SQUIRREL_DIR,
+ "config_file": SQUIRREL_CONFIG,
+ "app_dir": os.path.dirname(os.path.realpath(__file__)) + "/app",
+ "icon_dir": env.get("SQUIREL_ICONS", "/usr/share/icons/breeze/mimetypes/32"),
+}
+settings.update(DEFAULT_SETTINGS)
+
+with open(SQUIRREL_CONFIG, "r") as f:
+ settings.update(json.load(f))
diff --git a/squirrel/main.py b/squirrel/main.py
new file mode 100644
index 0000000..7741edf
--- /dev/null
+++ b/squirrel/main.py
@@ -0,0 +1,127 @@
+from flask import (
+ Flask,
+ render_template,
+ redirect,
+ request,
+ g,
+ send_file,
+ jsonify,
+ Response,
+)
+from flask.logging import default_handler
+from werkzeug.exceptions import BadRequest, NotFound, Forbidden
+import io
+
+from squirrel.conf import settings
+from squirrel import auth, repo
+from PIL import Image
+
+import os.path
+
+# Enable logging on repo
+repo.logger.addHandler(default_handler)
+
+app = Flask(__name__)
+
+app.config["SECRET_KEY"] = "foobar23"
+
+# TODO: better singleton methods
+archive = repo.get_archive()
+
+app.register_blueprint(auth.bp)
+
+
+@app.get("/")
+def render_home():
+ return render_template("home.html")
+
+
+def search_params(d):
+ """
+ Reference:
+ """
+ try:
+ params = {
+ "q": d.get("q"),
+ "count": int(d.get("count", 20)),
+ "offset": int(d.get("offset", 0)),
+ }
+ except Exception as e:
+ raise BadRequest(str(e))
+ if not params["q"]:
+ raise BadRequest("Missing q parameter")
+ if params["offset"] < 0:
+ raise BadRequest("Invalid offset")
+ return params
+
+
+def user_repos(user):
+ if not user:
+ return []
+ return user.repos or []
+
+
+@app.get("/search/
")
+@app.get("/search")
+def render_search(doctype="general"):
+ try:
+ search = archive.query(**search_params(request.args), extra=user_repos(g.user))
+ except BadRequest:
+ return redirect("/")
+ return render_template(
+ "search_results.html",
+ **search,
+ doctype=doctype,
+ renderer=f"results_{doctype}.html",
+ )
+
+
+@app.get("/search-fragment/")
+def render_search_fragment(doctype):
+ search = archive.query(**search_params(request.args), extra=user_repos(g.user))
+ return render_template(f"results_{doctype}.html", **search)
+
+
+@app.get("/icon//.svg")
+def get_icon(type, subtype):
+ return send_file(os.path.join(settings["icon_dir"], f"{type}-{subtype}.svg"))
+
+
+@app.get("/doc//")
+def get_document(repo, udi):
+ if repo != "main" and repo not in user_repos(g.user):
+ raise Forbidden("No access to repo")
+
+ filepath = archive.path_for_udi(repo, "/" + udi)
+ if not os.path.exists(filepath):
+ raise NotFound("No such document")
+ return send_file(filepath)
+
+
+@app.get("/preview//")
+def get_preview(repo, docpath):
+ if repo not in user_repos(g.user):
+ raise Forbidden("No access to repo")
+
+ try:
+ im = Image.open("/" + docpath)
+ new_size = (200, 200)
+ resized_img = im.resize(new_size)
+ # Save the image to a buffer
+ img_io = io.BytesIO()
+ resized_img.save(img_io, "JPEG", quality=70)
+ img_io.seek(0)
+ return send_file(img_io, mimetype="image/jpeg")
+ except Exception as e:
+ return f"Error: {str(e)}"
+
+
+@app.get("/api/info")
+def get_info():
+ return jsonify(archive.config)
+
+
+@app.get("/api/reindex/")
+def get_reindex(repo):
+ p = archive.reindex(repo)
+ return Response(p.stderr, mimetype="text/plain")
diff --git a/squirrel/repo.py b/squirrel/repo.py
new file mode 100644
index 0000000..d2b760c
--- /dev/null
+++ b/squirrel/repo.py
@@ -0,0 +1,113 @@
+"""
+Original WebUI: https://framagit.org/medoc92/recollwebui
+"""
+
+import os.path
+import configparser
+import subprocess
+from recoll import recoll # type:ignore
+import logging
+from typing import Any
+from functools import cache
+from squirrel.conf import settings # type:ignore
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+STRIP_CHARS = len("file://")
+
+logger.info("TEST")
+
+
+class Squirrel(object):
+ def __init__(self, config):
+ self.config = config
+
+ self.repos = self.config["repos"]
+ self.extra_dbs = {}
+ self.paths = {}
+
+ for name, config_path in self.repos.items():
+ logger.info("Loading repo %s [%s]", name, config_path)
+ # Really basic RCLConfig parser
+ parser = configparser.ConfigParser(allow_unnamed_section=True)
+ if not parser.read(os.path.join(config_path, "recoll.conf")):
+ raise IOError(f"Failed to open config at {config_path}")
+ repo: dict[str, str] = dict(parser.items(configparser.UNNAMED_SECTION)) # type:ignore
+
+ for d in repo.get("topdirs", "").split():
+ self.paths[d] = name
+
+ self.extra_dbs[name] = os.path.join(
+ repo.get("cachedir", config_path), "xapiandb"
+ )
+
+ self.search_paths = list(self.paths.keys())
+ self.search_paths.sort(reverse=True)
+ logger.info("Search paths: %s", self.search_paths)
+
+ def db(self, repo: str = "main", extra: list[str] = []):
+ dbs = [db for name, db in self.extra_dbs.items() if name in extra]
+
+ return recoll.connect(self.repos[repo], extra_dbs=dbs)
+
+ def reindex(self, repo="main"):
+ config = self.config["repos"][repo]
+ p = subprocess.Popen(["recollindex", "-c", config], stderr=subprocess.PIPE)
+ return p
+
+ def get_repos(self, filename):
+ return {self.paths[x] for x in self.search_paths if filename.startswith(x)}
+
+ def query(
+ self,
+ q: str,
+ count: int = 20,
+ offset: int = 0,
+ repo: str = "main",
+ extra: list[str] = [],
+ ) -> dict[str, Any]:
+ db = self.db(repo, extra)
+ query = db.query()
+ nres = query.execute(q)
+ s_extra = set(extra + ["main"])
+ result = {
+ "query": q,
+ "count": count,
+ "start": offset,
+ "end": offset,
+ "total": nres,
+ "xquery": query.getxquery(),
+ "repos": [repo] + extra,
+ "results": [],
+ }
+ if offset >= nres or offset < 0:
+ return result
+ if nres == 0:
+ return result
+
+ if offset > 0:
+ query.scroll(offset)
+ docs = query.fetchmany(count)
+ for doc in docs:
+ d = dict(doc)
+ d["udi"] = doc.rcludi
+ d["snippets"] = query.getsnippets(doc, ctxwords=10)
+ d["fpath"] = d["url"][STRIP_CHARS:]
+ d["title"] = d["title"] or d["filename"]
+ d["repos"] = list(self.get_repos(d["fpath"]) & s_extra)
+ result["results"].append(d)
+ result["end"] += 1
+ return result
+
+ def path_for_udi(self, repo, udi: str):
+ db = recoll.connect(self.config["repos"][repo])
+ doc = db.getDoc(udi)
+ if not doc or not doc.url:
+ raise IOError("No such file")
+ return doc.url[STRIP_CHARS:]
+
+
+@cache
+def get_archive():
+ return Squirrel(settings)
diff --git a/squirrel/server.py b/squirrel/server.py
deleted file mode 100644
index 7d7fbc4..0000000
--- a/squirrel/server.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import uvicorn
-
-
-def daemon(reload=False):
- uvicorn.run("squirrel.api:app", host="0.0.0.0", port=8000, reload=reload)
-
-
-if __name__ == "__main__":
- daemon(True)
diff --git a/squirrel/static/css/search-source.css b/squirrel/static/css/search-source.css
new file mode 100644
index 0000000..215879f
--- /dev/null
+++ b/squirrel/static/css/search-source.css
@@ -0,0 +1,42 @@
+@import "tailwindcss";
+
+.flashes {
+ @apply bg-amber-300 my-2 mx-20 px-10 py-2 border border-amber-600 rounded-xl;
+}
+
+.header {
+ @apply mx-2;
+}
+
+.button {
+ @apply border rounded bg-gray-100 px-2 py-1 text-red-900 font-bold hover:cursor-pointer;
+}
+
+/* Form elements */
+
+.field {
+ @apply border rounded py-2 px-3 text-gray-900;
+}
+
+.field-label {
+ @apply mb-2 font-bold text-red-900;
+}
+
+.field-control {
+ @apply flex flex-col mb-2;
+}
+
+SPAN.rclmatch {
+ @apply font-bold text-red-900;
+}
+
+/* Custom fonts */
+
+@font-face {
+ font-family: beyondTheMountains; /* set name */
+ src: url(/static/fonts/beyond_the_mountains.ttf); /* url of the font */
+}
+
+.font-beyond-the-mountains {
+ font-family: beyondTheMountains;
+}
diff --git a/squirrel/app/static/css/search.css b/squirrel/static/css/search.css
similarity index 56%
rename from squirrel/app/static/css/search.css
rename to squirrel/static/css/search.css
index 37d68df..10e9be7 100644
--- a/squirrel/app/static/css/search.css
+++ b/squirrel/static/css/search.css
@@ -1,15 +1,24 @@
-/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
+/*! tailwindcss v4.1.14 | MIT License | https://tailwindcss.com */
+@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
- --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
- 'Noto Color Emoji';
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
- monospace;
- --color-red-900: oklch(0.396 0.141 25.723);
- --color-slate-600: oklch(0.446 0.043 257.281);
- --color-gray-700: oklch(0.373 0.034 259.733);
+ --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
+ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+ "Courier New", monospace;
+ --color-red-300: oklch(80.8% 0.114 19.571);
+ --color-red-600: oklch(57.7% 0.245 27.325);
+ --color-red-900: oklch(39.6% 0.141 25.723);
+ --color-amber-300: oklch(87.9% 0.169 91.605);
+ --color-amber-600: oklch(66.6% 0.179 58.318);
+ --color-slate-300: oklch(86.9% 0.022 252.894);
+ --color-slate-600: oklch(44.6% 0.043 257.281);
+ --color-gray-100: oklch(96.7% 0.003 264.542);
+ --color-gray-700: oklch(37.3% 0.034 259.733);
+ --color-gray-900: oklch(21% 0.034 264.665);
--spacing: 0.25rem;
+ --container-md: 28rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-xl: 1.25rem;
@@ -18,13 +27,11 @@
--text-2xl--line-height: calc(2 / 1.5);
--text-8xl: 6rem;
--text-8xl--line-height: 1;
+ --font-weight-bold: 700;
--leading-tight: 1.25;
+ --radius-xl: 0.75rem;
--default-font-family: var(--font-sans);
- --default-font-feature-settings: var(--font-sans--font-feature-settings);
- --default-font-variation-settings: var(--font-sans--font-variation-settings);
--default-mono-font-family: var(--font-mono);
- --default-mono-font-feature-settings: var(--font-mono--font-feature-settings);
- --default-mono-font-variation-settings: var(--font-mono--font-variation-settings);
}
}
@layer base {
@@ -38,14 +45,11 @@
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
- font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' );
+ font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
- body {
- line-height: inherit;
- }
hr {
height: 0;
color: inherit;
@@ -68,7 +72,7 @@
font-weight: bolder;
}
code, kbd, samp, pre {
- font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace );
+ font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
@@ -134,7 +138,14 @@
}
::placeholder {
opacity: 1;
- color: color-mix(in oklab, currentColor 50%, transparent);
+ }
+ @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
+ ::placeholder {
+ color: currentcolor;
+ @supports (color: color-mix(in lab, red, red)) {
+ color: color-mix(in oklab, currentcolor 50%, transparent);
+ }
+ }
}
textarea {
resize: vertical;
@@ -155,16 +166,19 @@
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
+ ::-webkit-calendar-picker-indicator {
+ line-height: 1;
+ }
:-moz-ui-invalid {
box-shadow: none;
}
- button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
+ button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
- [hidden]:where(:not([hidden='until-found'])) {
+ [hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
}
@@ -175,18 +189,36 @@
.m-4 {
margin: calc(var(--spacing) * 4);
}
+ .m-10 {
+ margin: calc(var(--spacing) * 10);
+ }
.mx-10 {
margin-inline: calc(var(--spacing) * 10);
}
.mx-auto {
margin-inline: auto;
}
+ .my-5 {
+ margin-block: calc(var(--spacing) * 5);
+ }
.my-10 {
margin-block: calc(var(--spacing) * 10);
}
+ .mt-20 {
+ margin-top: calc(var(--spacing) * 20);
+ }
.mt-50 {
margin-top: calc(var(--spacing) * 50);
}
+ .mb-2 {
+ margin-bottom: calc(var(--spacing) * 2);
+ }
+ .mb-4 {
+ margin-bottom: calc(var(--spacing) * 4);
+ }
+ .mb-6 {
+ margin-bottom: calc(var(--spacing) * 6);
+ }
.block {
display: block;
}
@@ -196,6 +228,18 @@
.h-16 {
height: calc(var(--spacing) * 16);
}
+ .h-18 {
+ height: calc(var(--spacing) * 18);
+ }
+ .h-20 {
+ height: calc(var(--spacing) * 20);
+ }
+ .h-22 {
+ height: calc(var(--spacing) * 22);
+ }
+ .h-64 {
+ height: calc(var(--spacing) * 64);
+ }
.w-1\/2 {
width: calc(1/2 * 100%);
}
@@ -205,9 +249,24 @@
.w-16 {
width: calc(var(--spacing) * 16);
}
+ .w-18 {
+ width: calc(var(--spacing) * 18);
+ }
+ .w-20 {
+ width: calc(var(--spacing) * 20);
+ }
+ .w-22 {
+ width: calc(var(--spacing) * 22);
+ }
+ .w-64 {
+ width: calc(var(--spacing) * 64);
+ }
.w-full {
width: 100%;
}
+ .max-w-md {
+ max-width: var(--container-md);
+ }
.grow {
flex-grow: 1;
}
@@ -217,9 +276,24 @@
.items-center {
align-items: center;
}
+ .rounded {
+ border-radius: 0.25rem;
+ }
.rounded-full {
border-radius: calc(infinity * 1px);
}
+ .rounded-l-full {
+ border-top-left-radius: calc(infinity * 1px);
+ border-bottom-left-radius: calc(infinity * 1px);
+ }
+ .rounded-r-full {
+ border-top-right-radius: calc(infinity * 1px);
+ border-bottom-right-radius: calc(infinity * 1px);
+ }
+ .border {
+ border-style: var(--tw-border-style);
+ border-width: 1px;
+ }
.border-b {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px;
@@ -230,6 +304,18 @@
.border-slate-600 {
border-color: var(--color-slate-600);
}
+ .bg-red-300 {
+ background-color: var(--color-red-300);
+ }
+ .bg-red-600 {
+ background-color: var(--color-red-600);
+ }
+ .bg-slate-300 {
+ background-color: var(--color-slate-300);
+ }
+ .mask-clip-border {
+ mask-clip: border-box;
+ }
.p-4 {
padding: calc(var(--spacing) * 4);
}
@@ -239,6 +325,12 @@
.px-5 {
padding-inline: calc(var(--spacing) * 5);
}
+ .px-10 {
+ padding-inline: calc(var(--spacing) * 10);
+ }
+ .px-20 {
+ padding-inline: calc(var(--spacing) * 20);
+ }
.py-2 {
padding-block: calc(var(--spacing) * 2);
}
@@ -251,9 +343,15 @@
.text-center {
text-align: center;
}
+ .text-justify {
+ text-align: justify;
+ }
.text-right {
text-align: right;
}
+ .font-mono {
+ font-family: var(--font-mono);
+ }
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
@@ -274,12 +372,18 @@
--tw-leading: var(--leading-tight);
line-height: var(--leading-tight);
}
+ .whitespace-pre {
+ white-space: pre;
+ }
.text-gray-700 {
color: var(--color-gray-700);
}
.text-red-900 {
color: var(--color-red-900);
}
+ .text-slate-300 {
+ color: var(--color-slate-300);
+ }
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -291,8 +395,58 @@
}
}
}
+.flashes {
+ margin-inline: calc(var(--spacing) * 20);
+ margin-block: calc(var(--spacing) * 2);
+ border-radius: var(--radius-xl);
+ border-style: var(--tw-border-style);
+ border-width: 1px;
+ border-color: var(--color-amber-600);
+ background-color: var(--color-amber-300);
+ padding-inline: calc(var(--spacing) * 10);
+ padding-block: calc(var(--spacing) * 2);
+}
+.header {
+ margin-inline: calc(var(--spacing) * 2);
+}
+.button {
+ border-radius: 0.25rem;
+ border-style: var(--tw-border-style);
+ border-width: 1px;
+ background-color: var(--color-gray-100);
+ padding-inline: calc(var(--spacing) * 2);
+ padding-block: calc(var(--spacing) * 1);
+ --tw-font-weight: var(--font-weight-bold);
+ font-weight: var(--font-weight-bold);
+ color: var(--color-red-900);
+ &:hover {
+ @media (hover: hover) {
+ cursor: pointer;
+ }
+ }
+}
+.field {
+ border-radius: 0.25rem;
+ border-style: var(--tw-border-style);
+ border-width: 1px;
+ padding-inline: calc(var(--spacing) * 3);
+ padding-block: calc(var(--spacing) * 2);
+ color: var(--color-gray-900);
+}
+.field-label {
+ margin-bottom: calc(var(--spacing) * 2);
+ --tw-font-weight: var(--font-weight-bold);
+ font-weight: var(--font-weight-bold);
+ color: var(--color-red-900);
+}
+.field-control {
+ margin-bottom: calc(var(--spacing) * 2);
+ display: flex;
+ flex-direction: column;
+}
SPAN.rclmatch {
- font-weight: bold;
+ --tw-font-weight: var(--font-weight-bold);
+ font-weight: var(--font-weight-bold);
color: var(--color-red-900);
}
@font-face {
@@ -320,6 +474,11 @@ SPAN.rclmatch {
syntax: "*";
inherits: false;
}
+@property --tw-shadow-alpha {
+ syntax: "";
+ inherits: false;
+ initial-value: 100%;
+}
@property --tw-inset-shadow {
syntax: "*";
inherits: false;
@@ -329,6 +488,11 @@ SPAN.rclmatch {
syntax: "*";
inherits: false;
}
+@property --tw-inset-shadow-alpha {
+ syntax: "";
+ inherits: false;
+ initial-value: 100%;
+}
@property --tw-ring-color {
syntax: "*";
inherits: false;
@@ -366,3 +530,30 @@ SPAN.rclmatch {
inherits: false;
initial-value: 0 0 #0000;
}
+@property --tw-font-weight {
+ syntax: "*";
+ inherits: false;
+}
+@layer properties {
+ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
+ *, ::before, ::after, ::backdrop {
+ --tw-border-style: solid;
+ --tw-leading: initial;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-color: initial;
+ --tw-shadow-alpha: 100%;
+ --tw-inset-shadow: 0 0 #0000;
+ --tw-inset-shadow-color: initial;
+ --tw-inset-shadow-alpha: 100%;
+ --tw-ring-color: initial;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-inset-ring-color: initial;
+ --tw-inset-ring-shadow: 0 0 #0000;
+ --tw-ring-inset: initial;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-font-weight: initial;
+ }
+ }
+}
diff --git a/squirrel/app/static/fonts/beyond_the_mountains.ttf b/squirrel/static/fonts/beyond_the_mountains.ttf
similarity index 100%
rename from squirrel/app/static/fonts/beyond_the_mountains.ttf
rename to squirrel/static/fonts/beyond_the_mountains.ttf
diff --git a/squirrel/app/static/images/squirrel_32.png b/squirrel/static/images/squirrel_32.png
similarity index 100%
rename from squirrel/app/static/images/squirrel_32.png
rename to squirrel/static/images/squirrel_32.png
diff --git a/squirrel/templates/auth/generate.html b/squirrel/templates/auth/generate.html
new file mode 100644
index 0000000..998fef7
--- /dev/null
+++ b/squirrel/templates/auth/generate.html
@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+ {% if hashed %}
+
+ Password: {{ hashed }}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/squirrel/templates/auth/login.html b/squirrel/templates/auth/login.html
new file mode 100644
index 0000000..1b054ae
--- /dev/null
+++ b/squirrel/templates/auth/login.html
@@ -0,0 +1,21 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/squirrel/templates/base.html b/squirrel/templates/base.html
new file mode 100644
index 0000000..bd7f6dc
--- /dev/null
+++ b/squirrel/templates/base.html
@@ -0,0 +1,53 @@
+
+
+
+ Squirrel
+
+
+
+
+ {% block scripts %}{% endblock %}
+
+
+
+ {% block header %}
+
+ {% endblock %}
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+
+
+ {% for message in messages %}
+ - {{ message }}
+ {% endfor %}
+
+
+ {% endif %}
+ {% endwith %}
+ {% block content %}CONTENT{% endblock %}
+
+
diff --git a/squirrel/app/templates/home.html b/squirrel/templates/home.html
similarity index 95%
rename from squirrel/app/templates/home.html
rename to squirrel/templates/home.html
index 7b377cf..506faa6 100644
--- a/squirrel/app/templates/home.html
+++ b/squirrel/templates/home.html
@@ -1,5 +1,7 @@
{% extends "base.html" %}
+{% block header %}{% endblock %}
+
{% block content %}
diff --git a/squirrel/templates/results_general.html b/squirrel/templates/results_general.html
new file mode 100644
index 0000000..025fd8e
--- /dev/null
+++ b/squirrel/templates/results_general.html
@@ -0,0 +1,23 @@
+
+
+{% for result in results %}
+
+
+
+

+
+
{{ result.title }}
+
{{ result.relevancyrating }} {{ result.repos|join(", ") }}
+
{{ result.fpath }}
+
+
+
+
+ {% for snippet in result.snippets %}
+
{{ snippet[2]|safe }}
+ {% endfor %}
+
+
+
+{% endfor %}
+
diff --git a/squirrel/templates/results_image.html b/squirrel/templates/results_image.html
new file mode 100644
index 0000000..029ff38
--- /dev/null
+++ b/squirrel/templates/results_image.html
@@ -0,0 +1,8 @@
+
+
+{% for result in results %}
+
+

+
+{% endfor %}
+
diff --git a/squirrel/templates/search_results.html b/squirrel/templates/search_results.html
new file mode 100644
index 0000000..172f72f
--- /dev/null
+++ b/squirrel/templates/search_results.html
@@ -0,0 +1,59 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
+
+
+
Results {{ start+1 }} to {{ end+1 }} of {{ total }}
+
+
+
+ {% include renderer %}
+
+
+
+
+
+
+
+
+
+
+
+
+{% if request.args.debug %}
+
+ {{ results|tojson(1) }}
+
+{% endif %}
+
+{% endblock %}