From e453fa55592e806b59dfc0604fb1e060c50caa32 Mon Sep 17 00:00:00 2001 From: Tris Forster Date: Fri, 3 Oct 2025 14:39:30 +1000 Subject: [PATCH] Shifted to flask --- Dockerfile | 8 +- pyproject.toml | 12 +- squirrel-cmd | 3 + squirrel/api.py | 128 +++------- squirrel/app/static/css/search-source.css | 14 -- squirrel/app/templates/base.html | 13 - squirrel/app/templates/search_results.html | 54 ---- squirrel/auth.py | 96 ++++++++ squirrel/conf.py | 21 ++ squirrel/main.py | 127 ++++++++++ squirrel/repo.py | 113 +++++++++ squirrel/server.py | 9 - squirrel/static/css/search-source.css | 42 ++++ squirrel/{app => }/static/css/search.css | 233 ++++++++++++++++-- .../static/fonts/beyond_the_mountains.ttf | Bin .../{app => }/static/images/squirrel_32.png | Bin squirrel/templates/auth/generate.html | 22 ++ squirrel/templates/auth/login.html | 21 ++ squirrel/templates/base.html | 53 ++++ squirrel/{app => }/templates/home.html | 2 + squirrel/templates/results_general.html | 23 ++ squirrel/templates/results_image.html | 8 + squirrel/templates/search_results.html | 59 +++++ 23 files changed, 854 insertions(+), 207 deletions(-) create mode 100755 squirrel-cmd delete mode 100644 squirrel/app/static/css/search-source.css delete mode 100644 squirrel/app/templates/base.html delete mode 100644 squirrel/app/templates/search_results.html create mode 100644 squirrel/auth.py create mode 100644 squirrel/conf.py create mode 100644 squirrel/main.py create mode 100644 squirrel/repo.py delete mode 100644 squirrel/server.py create mode 100644 squirrel/static/css/search-source.css rename squirrel/{app => }/static/css/search.css (56%) rename squirrel/{app => }/static/fonts/beyond_the_mountains.ttf (100%) rename squirrel/{app => }/static/images/squirrel_32.png (100%) create mode 100644 squirrel/templates/auth/generate.html create mode 100644 squirrel/templates/auth/login.html create mode 100644 squirrel/templates/base.html rename squirrel/{app => }/templates/home.html (95%) create mode 100644 squirrel/templates/results_general.html create mode 100644 squirrel/templates/results_image.html create mode 100644 squirrel/templates/search_results.html 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 %} -
-
- Squirrel -
- -
-
- Foo -
-
- -
-
-

-

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 %} +
+ Squirrel +
+ {% if query %} +
+
+
Bubble
+ + +
+
+ {% endif %} +
+
User: {{ g.user.name }}
+
+ {% if g.user.name == "Guest" %} + lock_open + {% else %} + lock + {% endif %} +
+
+ {% 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 %}