mirror of
https://github.com/searxng/searxng.git
synced 2026-07-25 09:21:25 +00:00
Compare commits
1 Commits
master
...
0c4c8e346e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4c8e346e |
@@ -1,22 +1,4 @@
|
|||||||
ARG DEBIAN_CODENAME="bookworm"
|
FROM mcr.microsoft.com/devcontainers/base:debian
|
||||||
|
|
||||||
FROM mcr.microsoft.com/devcontainers/base:$DEBIAN_CODENAME
|
|
||||||
|
|
||||||
ARG DEBIAN_CODENAME="bookworm"
|
|
||||||
|
|
||||||
RUN cat <<EOF > /etc/apt/sources.list.d/debian.sources
|
|
||||||
Types: deb
|
|
||||||
URIs: http://deb.debian.org/debian
|
|
||||||
Suites: $DEBIAN_CODENAME $DEBIAN_CODENAME-updates $DEBIAN_CODENAME-backports
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
|
|
||||||
Types: deb
|
|
||||||
URIs: http://security.debian.org/debian-security
|
|
||||||
Suites: $DEBIAN_CODENAME-security
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get -y install python3 python3-venv valkey-server firefox-esr graphviz imagemagick librsvg2-bin fonts-dejavu shellcheck
|
apt-get -y install python3 python3-venv redis firefox-esr graphviz imagemagick librsvg2-bin fonts-dejavu shellcheck
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
{
|
{
|
||||||
"build": {
|
"build": {
|
||||||
"args": {
|
|
||||||
"DEBIAN_CODENAME": "bookworm",
|
|
||||||
},
|
|
||||||
"dockerfile": "Dockerfile"
|
"dockerfile": "Dockerfile"
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/github-cli": {},
|
"ghcr.io/devcontainers/features/github-cli": {}
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker": {}
|
|
||||||
},
|
},
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
|
|||||||
163
.dir-locals-template.el
Normal file
163
.dir-locals-template.el
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
;;; .dir-locals.el
|
||||||
|
;;
|
||||||
|
;; Per-Directory Local Variables:
|
||||||
|
;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html
|
||||||
|
;;
|
||||||
|
;; For full fledge developer tools install emacs packages:
|
||||||
|
;;
|
||||||
|
;; M-x package-install ...
|
||||||
|
;;
|
||||||
|
;; magit gitconfig
|
||||||
|
;; nvm lsp-mode lsp-pyright lsp-eslint
|
||||||
|
;; pyvenv pylint pip-requirements
|
||||||
|
;; jinja2-mode
|
||||||
|
;; json-mode
|
||||||
|
;; company company-jedi company-quickhelp company-shell
|
||||||
|
;; realgud
|
||||||
|
;; sphinx-doc markdown-mode graphviz-dot-mode
|
||||||
|
;; apache-mode nginx-mode
|
||||||
|
;;
|
||||||
|
;; To setup a developer environment, build target::
|
||||||
|
;;
|
||||||
|
;; $ make node.env.dev pyenv.install
|
||||||
|
;;
|
||||||
|
;; Some buffer locals are referencing the project environment:
|
||||||
|
;;
|
||||||
|
;; - prj-root --> <repo>/
|
||||||
|
;; - nvm-dir --> <repo>/.nvm
|
||||||
|
;; - python-environment-directory --> <repo>/local
|
||||||
|
;; - python-environment-default-root-name --> py3
|
||||||
|
;; - python-shell-virtualenv-root --> <repo>/local/py3
|
||||||
|
;; When this variable is set with the path of the virtualenv to use,
|
||||||
|
;; `process-environment' and `exec-path' get proper values in order to run
|
||||||
|
;; shells inside the specified virtualenv, example::
|
||||||
|
;; (setq python-shell-virtualenv-root "/path/to/env/")
|
||||||
|
;; - python-shell-interpreter --> <repo>/local/py3/bin/python
|
||||||
|
;;
|
||||||
|
;; Python development:
|
||||||
|
;;
|
||||||
|
;; Jedi, flycheck & other python stuff should use the 'python-shell-interpreter'
|
||||||
|
;; from the local py3 environment.
|
||||||
|
;;
|
||||||
|
|
||||||
|
((nil
|
||||||
|
. ((fill-column . 80)
|
||||||
|
(indent-tabs-mode . nil)
|
||||||
|
(eval . (progn
|
||||||
|
|
||||||
|
(add-to-list 'auto-mode-alist '("\\.html\\'" . jinja2-mode))
|
||||||
|
|
||||||
|
;; project root folder is where the `.dir-locals.el' is located
|
||||||
|
(setq-local prj-root
|
||||||
|
(locate-dominating-file default-directory ".dir-locals.el"))
|
||||||
|
|
||||||
|
(setq-local python-environment-directory
|
||||||
|
(expand-file-name "./local" prj-root))
|
||||||
|
|
||||||
|
;; to get in use of NVM environment, install https://github.com/rejeep/nvm.el
|
||||||
|
(setq-local nvm-dir (expand-file-name "./.nvm" prj-root))
|
||||||
|
|
||||||
|
;; use nodejs from the (local) NVM environment (see nvm-dir)
|
||||||
|
(nvm-use-for-buffer)
|
||||||
|
(ignore-errors (require 'lsp))
|
||||||
|
(setq-local lsp-server-install-dir (car (cdr nvm-current-version)))
|
||||||
|
(setq-local lsp-enable-file-watchers nil)
|
||||||
|
|
||||||
|
;; use 'py3' environment as default
|
||||||
|
(setq-local python-environment-default-root-name
|
||||||
|
"py3")
|
||||||
|
|
||||||
|
(setq-local python-shell-virtualenv-root
|
||||||
|
(expand-file-name
|
||||||
|
python-environment-default-root-name python-environment-directory))
|
||||||
|
|
||||||
|
(setq-local python-shell-interpreter
|
||||||
|
(expand-file-name
|
||||||
|
"bin/python" python-shell-virtualenv-root))))))
|
||||||
|
(makefile-gmake-mode
|
||||||
|
. ((indent-tabs-mode . t)))
|
||||||
|
|
||||||
|
(yaml-mode
|
||||||
|
. ((eval . (progn
|
||||||
|
|
||||||
|
;; flycheck should use the local py3 environment
|
||||||
|
(setq-local flycheck-yaml-yamllint-executable
|
||||||
|
(expand-file-name "bin/yamllint" python-shell-virtualenv-root))
|
||||||
|
|
||||||
|
(setq-local flycheck-yamllintrc
|
||||||
|
(expand-file-name ".yamllint.yml" prj-root))
|
||||||
|
|
||||||
|
(flycheck-checker . yaml-yamllint)))))
|
||||||
|
|
||||||
|
(json-mode
|
||||||
|
. ((eval . (progn
|
||||||
|
(setq-local js-indent-level 4)
|
||||||
|
(flycheck-checker . json-python-json)))))
|
||||||
|
|
||||||
|
(js-mode
|
||||||
|
. ((eval . (progn
|
||||||
|
(ignore-errors (require 'lsp-eslint))
|
||||||
|
(setq-local js-indent-level 2)
|
||||||
|
;; flycheck should use the eslint checker from developer tools
|
||||||
|
(setq-local flycheck-javascript-eslint-executable
|
||||||
|
(expand-file-name "node_modules/.bin/eslint" prj-root))
|
||||||
|
;; (flycheck-mode)
|
||||||
|
|
||||||
|
(if (featurep 'lsp-eslint)
|
||||||
|
(lsp))
|
||||||
|
))))
|
||||||
|
|
||||||
|
(python-mode
|
||||||
|
. ((eval . (progn
|
||||||
|
(ignore-errors (require 'jedi-core))
|
||||||
|
(ignore-errors (require 'lsp-pyright))
|
||||||
|
(ignore-errors (sphinx-doc-mode))
|
||||||
|
(setq-local python-environment-virtualenv
|
||||||
|
(list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
|
||||||
|
;;"--system-site-packages"
|
||||||
|
"--quiet"))
|
||||||
|
|
||||||
|
(setq-local pylint-command
|
||||||
|
(expand-file-name "bin/pylint" python-shell-virtualenv-root))
|
||||||
|
|
||||||
|
(if (featurep 'lsp-pyright)
|
||||||
|
(lsp))
|
||||||
|
|
||||||
|
;; pylint will find the '.pylintrc' file next to the CWD
|
||||||
|
;; https://pylint.readthedocs.io/en/latest/user_guide/run.html#command-line-options
|
||||||
|
(setq-local flycheck-pylintrc
|
||||||
|
".pylintrc")
|
||||||
|
|
||||||
|
;; flycheck & other python stuff should use the local py3 environment
|
||||||
|
(setq-local flycheck-python-pylint-executable
|
||||||
|
python-shell-interpreter)
|
||||||
|
|
||||||
|
;; use 'M-x jedi:show-setup-info' and 'M-x epc:controller' to inspect jedi server
|
||||||
|
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-root -- You
|
||||||
|
;; can specify a full path instead of a name (relative path). In that case,
|
||||||
|
;; python-environment-directory is ignored and Python virtual environment
|
||||||
|
;; is created at the specified path.
|
||||||
|
(setq-local jedi:environment-root
|
||||||
|
python-shell-virtualenv-root)
|
||||||
|
|
||||||
|
;; https://tkf.github.io/emacs-jedi/latest/#jedi:server-command
|
||||||
|
(setq-local jedi:server-command
|
||||||
|
(list python-shell-interpreter
|
||||||
|
jedi:server-script))
|
||||||
|
|
||||||
|
;; jedi:environment-virtualenv --> see above 'python-environment-virtualenv'
|
||||||
|
;; is set buffer local! No need to setup jedi:environment-virtualenv:
|
||||||
|
;;
|
||||||
|
;; Virtualenv command to use. A list of string. If it is nil,
|
||||||
|
;; python-environment-virtualenv is used instead. You must set non-nil
|
||||||
|
;; value to jedi:environment-root in order to make this setting work.
|
||||||
|
;;
|
||||||
|
;; https://tkf.github.io/emacs-jedi/latest/#jedi:environment-virtualenv
|
||||||
|
;;
|
||||||
|
;; (setq-local jedi:environment-virtualenv
|
||||||
|
;; (list (expand-file-name "bin/virtualenv" python-shell-virtualenv-root)
|
||||||
|
;; "--python"
|
||||||
|
;; "/usr/bin/python3.4"
|
||||||
|
;; ))
|
||||||
|
))))
|
||||||
|
)
|
||||||
@@ -1,6 +1,44 @@
|
|||||||
*
|
*~
|
||||||
|
*/*~
|
||||||
|
*/*/*~
|
||||||
|
*/*/*/*~
|
||||||
|
*/*/*/*/*~
|
||||||
|
|
||||||
!container/*.template.*
|
# Git
|
||||||
!container/entrypoint.sh
|
.git
|
||||||
!searx/**
|
.gitignore
|
||||||
!requirements*.txt
|
|
||||||
|
# CI
|
||||||
|
.codeclimate.yml
|
||||||
|
.travis.yml
|
||||||
|
.taskcluster.yml
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*/__pycache__/
|
||||||
|
*/*/__pycache__/
|
||||||
|
*/*/*/__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*/*.py[cod]
|
||||||
|
*/*/*.py[cod]
|
||||||
|
*/*/*/*.py[cod]
|
||||||
|
|
||||||
|
# node_modules
|
||||||
|
node_modules/
|
||||||
|
*/node_modules/
|
||||||
|
*/*/node_modules/
|
||||||
|
*/*/*/node_modules/
|
||||||
|
*/*/*/*/node_modules/
|
||||||
|
|
||||||
|
.tx/
|
||||||
|
|
||||||
|
# to sync with .gitignore
|
||||||
|
geckodriver.log
|
||||||
|
.coverage
|
||||||
|
coverage/
|
||||||
|
cache/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
local/
|
||||||
|
gh-pages/
|
||||||
|
*.egg-info/
|
||||||
|
|||||||
@@ -10,50 +10,21 @@ trim_trailing_whitespace = true
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
|
||||||
[{*.py,*.pyi}]
|
[*.py]
|
||||||
# code formatter accepts length of 120, but editor should prefer 80
|
max_line_length = 119
|
||||||
max_line_length = 80
|
|
||||||
|
|
||||||
[{*.sh,manage}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
|
|
||||||
# shfmt options
|
|
||||||
shell_variant = bash
|
|
||||||
switch_case_indent = true
|
|
||||||
|
|
||||||
[*.html]
|
[*.html]
|
||||||
# in the jinja templates we use indent size of 2 and we do not use tabs
|
indent_size = 4
|
||||||
indent_size = 2
|
|
||||||
indent_style = space
|
|
||||||
|
|
||||||
[*.css]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.less]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.js]
|
[*.js]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.ts]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.json]
|
[*.json]
|
||||||
indent_size = 2
|
indent_size = 4
|
||||||
insert_final_newline = ignore
|
|
||||||
|
|
||||||
[*.map]
|
|
||||||
indent_size = ignore
|
|
||||||
insert_final_newline = ignore
|
insert_final_newline = ignore
|
||||||
|
|
||||||
# Minified JavaScript files shouldn't be changed
|
# Minified JavaScript files shouldn't be changed
|
||||||
[*.min.js]
|
[**.min.js]
|
||||||
indent_style = ignore
|
|
||||||
insert_final_newline = ignore
|
|
||||||
|
|
||||||
# Minified CSS files shouldn't be changed
|
|
||||||
[*.min.css]
|
|
||||||
indent_style = ignore
|
indent_style = ignore
|
||||||
insert_final_newline = ignore
|
insert_final_newline = ignore
|
||||||
|
|
||||||
|
|||||||
65
.github/ISSUE_TEMPLATE/bug-report.md
vendored
65
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,50 +1,39 @@
|
|||||||
---
|
---
|
||||||
name: "Bug report"
|
name: Bug report
|
||||||
about: Report a bug in SearXNG"
|
about: Report a bug in SearXNG
|
||||||
labels: ["bug"]
|
title: ''
|
||||||
type: "bug"
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
|
||||||
|
|
||||||
_Replace this placeholder with a meaningful and precise description of the bug._
|
**Version of SearXNG, commit number if you are using on master branch and stipulate if you forked SearXNG**
|
||||||
|
<!-- If you are running on master branch using git execute this command
|
||||||
|
in order to fetch the latest commit ID:
|
||||||
|
```
|
||||||
|
git log -1
|
||||||
|
```
|
||||||
|
If you are using searxng-docker then look at the bottom of the SearXNG page
|
||||||
|
and check for the version after "Powered by SearXNG"
|
||||||
|
|
||||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
Please also stipulate if you are using a forked version of SearXNG and
|
||||||
|
include a link to the fork source code.
|
||||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
|
||||||
-->
|
-->
|
||||||
|
**How did you install SearXNG?**
|
||||||
|
<!-- Did you install SearXNG using the official wiki or using searxng-docker
|
||||||
|
or manually by executing the searx/webapp.py file? -->
|
||||||
|
**What happened?**
|
||||||
|
<!-- A clear and concise description of what the bug is. -->
|
||||||
|
|
||||||
### How To Reproduce?
|
**How To Reproduce**
|
||||||
|
<!-- How can we reproduce this issue? (as minimally and as precisely as possible) -->
|
||||||
<!-- How can we reproduce this issue? (as minimally and as precisely as
|
|
||||||
possible) -->
|
|
||||||
|
|
||||||
### Expected behavior
|
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
<!-- A clear and concise description of what you expected to happen. -->
|
<!-- A clear and concise description of what you expected to happen. -->
|
||||||
|
|
||||||
### Screenshots & Logs
|
**Screenshots & Logs**
|
||||||
|
|
||||||
<!-- If applicable, add screenshots, logs to help explain your problem. -->
|
<!-- If applicable, add screenshots, logs to help explain your problem. -->
|
||||||
|
|
||||||
### Version of SearXNG
|
**Additional context**
|
||||||
|
|
||||||
<!-- Commit number if you are using on master branch and stipulate if you forked
|
|
||||||
SearXNG -->
|
|
||||||
|
|
||||||
<!-- Look at the bottom of the SearXNG page and check for the version after
|
|
||||||
"Powered by SearXNG" If you are using a forked version of SearXNG include a
|
|
||||||
link to the fork source code. -->
|
|
||||||
|
|
||||||
### How did you install SearXNG?
|
|
||||||
|
|
||||||
<!-- Did you install SearXNG using the official documentation or using
|
|
||||||
searxng-docker? -->
|
|
||||||
|
|
||||||
### Additional context
|
|
||||||
|
|
||||||
<!-- Add any other context about the problem here. -->
|
<!-- Add any other context about the problem here. -->
|
||||||
|
|
||||||
### Code of Conduct
|
|
||||||
|
|
||||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
|
||||||
|
|
||||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Questions & Answers (Q&A)
|
- name: Questions & Answers (Q&A)
|
||||||
url: https://github.com/searxng/searxng/discussions/categories/q-a
|
url: https://github.com/searxng/searxng/discussions/categories/q-a
|
||||||
|
|||||||
61
.github/ISSUE_TEMPLATE/engine-request.md
vendored
61
.github/ISSUE_TEMPLATE/engine-request.md
vendored
@@ -1,46 +1,31 @@
|
|||||||
---
|
---
|
||||||
name: Engine request"
|
name: Engine request
|
||||||
about: Request a new engine in SearXNG"
|
about: Request a new engine in SearXNG
|
||||||
labels: ["engine request"]
|
title: ''
|
||||||
type: "feature"
|
labels: enhancement, engine request
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
|
||||||
|
|
||||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
**Working URL to the engine**
|
||||||
|
<!-- Please check if the engine is responding correctly before submitting it. -->
|
||||||
|
|
||||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
**Why do you want to add this engine?**
|
||||||
-->
|
<!-- What's special about this engine? Is it open source or libre? -->
|
||||||
|
|
||||||
### Working URL to the engine
|
**Features of this engine**
|
||||||
|
<!-- Features of this engine: Doesn't track its users, fast, easy to integrate, ... -->
|
||||||
|
|
||||||
<!-- Please check if the engine is responding correctly before submitting -->
|
**How can SearXNG fetch the information from this engine?**
|
||||||
|
<!-- List API URL, example code (using the correct markdown) and more
|
||||||
|
that could be useful for the developers in order to implement this engine.
|
||||||
|
If you don't know what to write, let this part blank. -->
|
||||||
|
|
||||||
### Why do you want to add this engine?
|
**Applicable category of this engine**
|
||||||
|
<!-- Where should this new engine fit in SearXNG? Current categories in SearXNG:
|
||||||
|
general, files, images, it, map, music, news, science, social media and videos.
|
||||||
|
You can add multiple categories at the same time. -->
|
||||||
|
|
||||||
<!-- What's special about this engine? -->
|
**Additional context**
|
||||||
|
<!-- Add any other context about this engine here. -->
|
||||||
### Features of this engine
|
|
||||||
|
|
||||||
<!-- Features of this engine: Serves special content, is fast, is easy to
|
|
||||||
integrate, ... ? -->
|
|
||||||
|
|
||||||
### How can SearXNG fetch results from this engine?
|
|
||||||
|
|
||||||
<!-- List API URL, example code and more that could be useful for the developers
|
|
||||||
in order to implement this engine. If you don't know what to write, let
|
|
||||||
this part blank. -->
|
|
||||||
|
|
||||||
### Applicable category of this engine
|
|
||||||
|
|
||||||
<!-- Where should this new engine fit in SearXNG? Current categories in
|
|
||||||
SearXNG: general, files, images, it, map, music, news, science, social
|
|
||||||
media and videos. -->
|
|
||||||
|
|
||||||
### Additional context
|
|
||||||
|
|
||||||
<!-- Add any other context about the problem here. -->
|
|
||||||
|
|
||||||
### Code of Conduct
|
|
||||||
|
|
||||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
|
||||||
|
|
||||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
|
||||||
|
|||||||
37
.github/ISSUE_TEMPLATE/feature-request.md
vendored
37
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -1,32 +1,21 @@
|
|||||||
---
|
---
|
||||||
name: "Feature request"
|
name: Feature request
|
||||||
about: "Request a new feature in SearXNG"
|
about: Request a new feature in SearXNG
|
||||||
labels: ["new feature"]
|
title: ''
|
||||||
type: "feature"
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF SearXNG -->
|
||||||
|
|
||||||
_Replace this placeholder with a concise description of the feature._
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
|
||||||
|
|
||||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Is your feature request related to a problem?
|
|
||||||
|
|
||||||
<!-- A clear and concise description of what the problem is. Ex. I'm always
|
|
||||||
frustrated when [...] -->
|
|
||||||
|
|
||||||
### Describe the solution you'd like
|
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
<!-- A clear and concise description of what you want to happen. -->
|
<!-- A clear and concise description of what you want to happen. -->
|
||||||
|
|
||||||
### Describe alternatives you've considered
|
**Describe alternatives you've considered**
|
||||||
|
|
||||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||||
|
|
||||||
### Code of Conduct
|
**Additional context**
|
||||||
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
|
||||||
|
|
||||||
- [ ] I read the [AI Policy] and hereby confirm that this issue conforms with the policy.
|
|
||||||
|
|||||||
57
.github/dependabot.yml
vendored
57
.github/dependabot.yml
vendored
@@ -10,66 +10,13 @@ updates:
|
|||||||
target-branch: "master"
|
target-branch: "master"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "[upd] pypi:"
|
prefix: "[upd] pypi:"
|
||||||
groups:
|
|
||||||
minor:
|
|
||||||
applies-to: version-updates
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
- "patch"
|
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/client/simple"
|
directory: "/searx/static/themes/simple"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
day: "friday"
|
day: "friday"
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 5
|
||||||
target-branch: "master"
|
target-branch: "master"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "[upd] web-client (simple):"
|
prefix: "[upd] npm:"
|
||||||
groups:
|
|
||||||
minor:
|
|
||||||
applies-to: version-updates
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
- "patch"
|
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
day: "friday"
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
target-branch: "master"
|
|
||||||
commit-message:
|
|
||||||
prefix: "[upd] searxng.org/devtools (Node.js):"
|
|
||||||
groups:
|
|
||||||
minor:
|
|
||||||
applies-to: version-updates
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
- "patch"
|
|
||||||
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
day: "friday"
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
target-branch: "master"
|
|
||||||
commit-message:
|
|
||||||
prefix: "[upd] searxng.org/devtools (Go):"
|
|
||||||
groups:
|
|
||||||
minor:
|
|
||||||
applies-to: version-updates
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
- "patch"
|
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
day: "friday"
|
|
||||||
target-branch: "master"
|
|
||||||
commit-message:
|
|
||||||
prefix: "[upd] github-actions:"
|
|
||||||
|
|||||||
31
.github/workflows/checker.yml
vendored
Normal file
31
.github/workflows/checker.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: "Checker"
|
||||||
|
on: # yamllint disable-line rule:truthy
|
||||||
|
schedule:
|
||||||
|
- cron: "0 4 * * 5"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checker:
|
||||||
|
name: Checker
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Ubuntu packages
|
||||||
|
run: |
|
||||||
|
sudo ./utils/searxng.sh install packages
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
architecture: 'x64'
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
make V=1 install
|
||||||
|
|
||||||
|
- name: Checker
|
||||||
|
run: |
|
||||||
|
make search.checker
|
||||||
170
.github/workflows/container.yml
vendored
170
.github/workflows/container.yml
vendored
@@ -1,170 +0,0 @@
|
|||||||
---
|
|
||||||
name: Container
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- Integration
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
PYTHON_VERSION: "3.14"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: |
|
|
||||||
github.event_name == 'workflow_dispatch'
|
|
||||||
|| (github.repository_owner == 'searxng' && github.event.workflow_run.conclusion == 'success')
|
|
||||||
name: Build (${{ matrix.arch }})
|
|
||||||
runs-on: ${{ matrix.runner }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- runner: ubuntu-26.04
|
|
||||||
arch: amd64
|
|
||||||
- runner: ubuntu-26.04-arm
|
|
||||||
arch: arm64
|
|
||||||
- runner: ubuntu-26.04-arm
|
|
||||||
arch: armv7
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
docker_tag: ${{ steps.build.outputs.docker_tag }}
|
|
||||||
git_url: ${{ steps.build.outputs.git_url }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
|
||||||
with:
|
|
||||||
registry: "ghcr.io"
|
|
||||||
username: "${{ github.repository_owner }}"
|
|
||||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
||||||
with:
|
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
|
||||||
|
|
||||||
- name: Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
|
||||||
persist-credentials: "false"
|
|
||||||
fetch-depth: "0"
|
|
||||||
|
|
||||||
- name: Setup cache Python
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
|
||||||
restore-keys: |
|
|
||||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
|
||||||
path: "./local/"
|
|
||||||
|
|
||||||
- name: Setup cache container
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
key: "container-${{ matrix.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
|
||||||
restore-keys: |
|
|
||||||
container-${{ matrix.arch }}-
|
|
||||||
path: "/var/tmp/buildah-cache-*/*"
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
id: build
|
|
||||||
env:
|
|
||||||
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
|
||||||
run: make container.build
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Test (${{ matrix.arch }})
|
|
||||||
runs-on: ${{ matrix.runner }}
|
|
||||||
needs: build
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- runner: ubuntu-26.04
|
|
||||||
arch: amd64
|
|
||||||
- runner: ubuntu-26.04-arm
|
|
||||||
arch: arm64
|
|
||||||
- runner: ubuntu-26.04-arm
|
|
||||||
arch: armv7
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
|
||||||
with:
|
|
||||||
registry: "ghcr.io"
|
|
||||||
username: "${{ github.repository_owner }}"
|
|
||||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Setup QEMU
|
|
||||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
|
||||||
persist-credentials: "false"
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
env:
|
|
||||||
OVERRIDE_ARCH: "${{ matrix.arch }}"
|
|
||||||
GIT_URL: "${{ needs.build.outputs.git_url }}"
|
|
||||||
run: make container.test
|
|
||||||
|
|
||||||
release:
|
|
||||||
if: github.repository_owner == 'searxng' && github.ref_name == 'master'
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-26.04-arm
|
|
||||||
needs:
|
|
||||||
- build
|
|
||||||
- test
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
|
||||||
with:
|
|
||||||
registry: "docker.io"
|
|
||||||
username: "${{ secrets.DOCKER_USER }}"
|
|
||||||
password: "${{ secrets.DOCKER_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
|
||||||
with:
|
|
||||||
registry: "ghcr.io"
|
|
||||||
username: "${{ github.repository_owner }}"
|
|
||||||
password: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
ref: "${{ github.event.workflow_run.head_sha || github.sha }}"
|
|
||||||
persist-credentials: "false"
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
env:
|
|
||||||
GIT_URL: "${{ needs.build.outputs.git_url }}"
|
|
||||||
DOCKER_TAG: "${{ needs.build.outputs.docker_tag }}"
|
|
||||||
run: make container.push
|
|
||||||
95
.github/workflows/data-update.yml
vendored
95
.github/workflows/data-update.yml
vendored
@@ -1,27 +1,14 @@
|
|||||||
---
|
name: "Update searx.data"
|
||||||
name: Update searx.data
|
on: # yamllint disable-line rule:truthy
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "59 23 28 * *"
|
- cron: "59 23 28 * *"
|
||||||
|
workflow_dispatch:
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
PYTHON_VERSION: "3.14"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
data:
|
updateData:
|
||||||
if: github.repository_owner == 'searxng'
|
name: Update data - ${{ matrix.fetch }}
|
||||||
name: ${{ matrix.fetch }}
|
runs-on: ubuntu-24.04
|
||||||
runs-on: ubuntu-26.04-arm
|
if: ${{ github.repository_owner == 'searxng'}}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -33,48 +20,48 @@ jobs:
|
|||||||
- update_engine_traits.py
|
- update_engine_traits.py
|
||||||
- update_wikidata_units.py
|
- update_wikidata_units.py
|
||||||
- update_engine_descriptions.py
|
- update_engine_descriptions.py
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
||||||
with:
|
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: "false"
|
|
||||||
|
|
||||||
- name: Setup cache Python
|
- name: Install Ubuntu packages
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
run: |
|
||||||
with:
|
sudo ./utils/searxng.sh install packages
|
||||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
|
||||||
restore-keys: |
|
|
||||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
|
||||||
path: "./local/"
|
|
||||||
|
|
||||||
- name: Setup venv
|
- name: Set up Python
|
||||||
run: make V=1 install
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
architecture: 'x64'
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
make V=1 install
|
||||||
|
|
||||||
- name: Fetch data
|
- name: Fetch data
|
||||||
run: V=1 ./manage pyenv.cmd python "./searxng_extra/update/${{ matrix.fetch }}"
|
env:
|
||||||
|
FETCH_SCRIPT: ./searxng_extra/update/${{ matrix.fetch }}
|
||||||
|
run: |
|
||||||
|
V=1 ./manage pyenv.cmd python "$FETCH_SCRIPT"
|
||||||
|
|
||||||
- name: Create PR
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
id: cpr
|
||||||
|
uses: peter-evans/create-pull-request@v6
|
||||||
with:
|
with:
|
||||||
author: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
commit-message: '[data] update searx.data - ${{ matrix.fetch }}'
|
||||||
committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
committer: searxng-bot <noreply@github.com>
|
||||||
title: "[mod] data: update searx.data - ${{ matrix.fetch }}"
|
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||||
commit-message: "[mod] data: update searx.data - ${{ matrix.fetch }}"
|
signoff: false
|
||||||
branch: "ci-data-${{ matrix.fetch }}"
|
branch: update_data_${{ matrix.fetch }}
|
||||||
delete-branch: "true"
|
delete-branch: true
|
||||||
draft: "false"
|
draft: false
|
||||||
signoff: "false"
|
title: '[data] update searx.data - ${{ matrix.fetch }}'
|
||||||
body: |
|
body: |
|
||||||
Update searx.data - ${{ matrix.fetch }}
|
update searx.data - ${{ matrix.fetch }}
|
||||||
labels: |
|
labels: |
|
||||||
data
|
data
|
||||||
|
|
||||||
|
- name: Check outputs
|
||||||
|
run: |
|
||||||
|
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
|
||||||
|
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||||
|
|||||||
71
.github/workflows/documentation.yml
vendored
71
.github/workflows/documentation.yml
vendored
@@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
name: Documentation
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
PYTHON_VERSION: "3.14"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
if: github.repository_owner == 'searxng' || github.event_name == 'workflow_dispatch'
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-26.04-arm
|
|
||||||
permissions:
|
|
||||||
# for JamesIves/github-pages-deploy-action to push
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
||||||
with:
|
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: "false"
|
|
||||||
fetch-depth: "0"
|
|
||||||
|
|
||||||
- name: Setup cache Python
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
|
||||||
restore-keys: |
|
|
||||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
|
||||||
path: "./local/"
|
|
||||||
|
|
||||||
- name: Setup dependencies
|
|
||||||
run: sudo ./utils/searxng.sh install buildhost
|
|
||||||
|
|
||||||
- name: Setup venv
|
|
||||||
run: make V=1 install
|
|
||||||
|
|
||||||
- name: Build documentation
|
|
||||||
run: make V=1 docs.html
|
|
||||||
|
|
||||||
- if: github.ref_name == 'master'
|
|
||||||
name: Release
|
|
||||||
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0
|
|
||||||
with:
|
|
||||||
folder: "dist/docs"
|
|
||||||
branch: "gh-pages"
|
|
||||||
commit-message: "[mod] docs: build from commit ${{ github.sha }}"
|
|
||||||
# Automatically remove deleted files from the deploy branch
|
|
||||||
clean: "true"
|
|
||||||
single-commit: "true"
|
|
||||||
254
.github/workflows/integration.yml
vendored
254
.github/workflows/integration.yml
vendored
@@ -1,103 +1,205 @@
|
|||||||
---
|
|
||||||
name: Integration
|
name: Integration
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
on: # yamllint disable-line rule:truthy
|
||||||
on:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: ["master"]
|
||||||
- master
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches: ["master"]
|
||||||
- master
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
|
||||||
PYTHON_VERSION: "3.14"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
python:
|
||||||
name: Python ${{ matrix.python-version }}
|
name: Python ${{ matrix.python-version }}
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
os: [ubuntu-20.04]
|
||||||
- "3.11"
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
- "3.12"
|
|
||||||
- "3.13"
|
|
||||||
- "3.14"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
||||||
with:
|
|
||||||
python-version: "${{ matrix.python-version }}"
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Ubuntu packages
|
||||||
|
run: |
|
||||||
|
sudo ./utils/searxng.sh install packages
|
||||||
|
sudo apt install firefox
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: "false"
|
python-version: ${{ matrix.python-version }}
|
||||||
|
architecture: 'x64'
|
||||||
- name: Setup cache Python
|
- name: Cache Python dependencies
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
id: cache-python
|
||||||
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
key: "python-${{ matrix.python-version }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
path: |
|
||||||
restore-keys: |
|
./local
|
||||||
python-${{ matrix.python-version }}-${{ runner.arch }}-
|
./.nvm
|
||||||
path: "./local/"
|
./node_modules
|
||||||
|
key: python-${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('requirements*.txt', 'setup.py') }}
|
||||||
- name: Setup venv
|
- name: Install Python dependencies
|
||||||
run: make V=1 install
|
if: steps.cache-python.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
make V=1 install
|
||||||
|
make V=1 gecko.driver
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make V=1 ci.test
|
run: make V=1 ci.test
|
||||||
|
|
||||||
theme:
|
themes:
|
||||||
name: Theme
|
name: Themes
|
||||||
runs-on: ubuntu-26.04-arm
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
||||||
with:
|
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
||||||
with:
|
|
||||||
node-version: "26"
|
|
||||||
check-latest: "true"
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Ubuntu packages
|
||||||
|
run: sudo ./utils/searxng.sh install buildhost
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: "false"
|
python-version: '3.12'
|
||||||
|
architecture: 'x64'
|
||||||
- name: Setup cache Python
|
- name: Cache Python dependencies
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
id: cache-python
|
||||||
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
path: |
|
||||||
restore-keys: |
|
./local
|
||||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
./.nvm
|
||||||
path: "./local/"
|
./node_modules
|
||||||
|
key: python-ubuntu-20.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||||
|
- name: Install node dependencies
|
||||||
|
run: make V=1 node.env
|
||||||
|
- name: Build themes
|
||||||
|
run: make V=1 themes.all
|
||||||
|
|
||||||
- name: Setup cache Node.js
|
documentation:
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
name: Documentation
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
permissions:
|
||||||
|
contents: write # for JamesIves/github-pages-deploy-action to push changes in repo
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
key: "nodejs-${{ runner.arch }}-${{ hashFiles('**/package-lock.json') }}"
|
fetch-depth: '0'
|
||||||
restore-keys: |
|
persist-credentials: false
|
||||||
nodejs-${{ runner.arch }}-
|
- name: Install Ubuntu packages
|
||||||
path: "./client/simple/node_modules/"
|
run: sudo ./utils/searxng.sh install buildhost
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
architecture: 'x64'
|
||||||
|
- name: Cache Python dependencies
|
||||||
|
id: cache-python
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./local
|
||||||
|
./.nvm
|
||||||
|
./node_modules
|
||||||
|
key: python-ubuntu-20.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||||
|
- name: Build documentation
|
||||||
|
run: |
|
||||||
|
make V=1 docs.clean docs.html
|
||||||
|
- name: Deploy
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
BRANCH: gh-pages
|
||||||
|
FOLDER: dist/docs
|
||||||
|
CLEAN: true # Automatically remove deleted files from the deploy branch
|
||||||
|
SINGLE_COMMIT: true
|
||||||
|
COMMIT_MESSAGE: '[doc] build from commit ${{ github.sha }}'
|
||||||
|
|
||||||
- name: Setup venv
|
babel:
|
||||||
run: make V=1 install
|
name: Update translations branch
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
if: ${{ github.repository_owner == 'searxng' && github.ref == 'refs/heads/master' }}
|
||||||
|
needs:
|
||||||
|
- python
|
||||||
|
- themes
|
||||||
|
- documentation
|
||||||
|
permissions:
|
||||||
|
contents: write # for make V=1 weblate.push.translations
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: '0'
|
||||||
|
token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
architecture: 'x64'
|
||||||
|
- name: Cache Python dependencies
|
||||||
|
id: cache-python
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./local
|
||||||
|
./.nvm
|
||||||
|
./node_modules
|
||||||
|
key: python-ubuntu-20.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||||
|
- name: weblate & git setup
|
||||||
|
env:
|
||||||
|
WEBLATE_CONFIG: ${{ secrets.WEBLATE_CONFIG }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.config
|
||||||
|
echo "${WEBLATE_CONFIG}" > ~/.config/weblate
|
||||||
|
git config --global user.email "searxng-bot@users.noreply.github.com"
|
||||||
|
git config --global user.name "searxng-bot"
|
||||||
|
- name: Update transations
|
||||||
|
id: update
|
||||||
|
run: |
|
||||||
|
make V=1 weblate.push.translations
|
||||||
|
|
||||||
- name: Lint
|
dockers:
|
||||||
run: make themes.lint
|
name: Docker
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
- name: Build
|
needs:
|
||||||
run: make themes.all
|
- python
|
||||||
|
- themes
|
||||||
|
- documentation
|
||||||
|
env:
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
if: env.DOCKERHUB_USERNAME != null
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# make sure "make docker.push" can get the git history
|
||||||
|
fetch-depth: '0'
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
architecture: 'x64'
|
||||||
|
- name: Cache Python dependencies
|
||||||
|
id: cache-python
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./local
|
||||||
|
./.nvm
|
||||||
|
./node_modules
|
||||||
|
key: python-ubuntu-20.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||||
|
- name: Set up QEMU
|
||||||
|
if: env.DOCKERHUB_USERNAME != null
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: env.DOCKERHUB_USERNAME != null
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: env.DOCKERHUB_USERNAME != null
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push
|
||||||
|
if: env.DOCKERHUB_USERNAME != null
|
||||||
|
run: make -e GIT_URL=$(git remote get-url origin) docker.buildx
|
||||||
|
|||||||
134
.github/workflows/l10n.yml
vendored
134
.github/workflows/l10n.yml
vendored
@@ -1,134 +0,0 @@
|
|||||||
---
|
|
||||||
name: Translation
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- Integration
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
schedule:
|
|
||||||
- cron: "05 07 * * 5"
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
PYTHON_VERSION: "3.14"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update:
|
|
||||||
if: github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'searxng'
|
|
||||||
name: Update
|
|
||||||
runs-on: ubuntu-26.04-arm
|
|
||||||
permissions:
|
|
||||||
# For "make V=1 weblate.push.translations"
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
||||||
with:
|
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
|
||||||
fetch-depth: "0"
|
|
||||||
|
|
||||||
- name: Setup cache Python
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
|
||||||
restore-keys: |
|
|
||||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
|
||||||
path: "./local/"
|
|
||||||
|
|
||||||
- name: Setup venv
|
|
||||||
run: make V=1 install
|
|
||||||
|
|
||||||
- name: Setup Weblate
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.config
|
|
||||||
echo "${{ secrets.WEBLATE_CONFIG }}" >~/.config/weblate
|
|
||||||
|
|
||||||
- name: Setup Git
|
|
||||||
run: |
|
|
||||||
git config --global user.email "searxng-bot@users.noreply.github.com"
|
|
||||||
git config --global user.name "searxng-bot"
|
|
||||||
|
|
||||||
- name: Update translations
|
|
||||||
run: make V=1 weblate.push.translations
|
|
||||||
|
|
||||||
pr:
|
|
||||||
if: |
|
|
||||||
github.repository_owner == 'searxng'
|
|
||||||
&& (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule')
|
|
||||||
name: Pull Request
|
|
||||||
runs-on: ubuntu-26.04-arm
|
|
||||||
permissions:
|
|
||||||
# For "make V=1 weblate.translations.commit"
|
|
||||||
contents: write
|
|
||||||
# For action "peter-evans/create-pull-request"
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
||||||
with:
|
|
||||||
python-version: "${{ env.PYTHON_VERSION }}"
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
token: "${{ secrets.WEBLATE_GITHUB_TOKEN }}"
|
|
||||||
fetch-depth: "0"
|
|
||||||
|
|
||||||
- name: Setup cache Python
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
|
|
||||||
restore-keys: |
|
|
||||||
python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-
|
|
||||||
path: "./local/"
|
|
||||||
|
|
||||||
- name: Setup venv
|
|
||||||
run: make V=1 install
|
|
||||||
|
|
||||||
- name: Setup Weblate
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.config
|
|
||||||
echo "${{ secrets.WEBLATE_CONFIG }}" >~/.config/weblate
|
|
||||||
|
|
||||||
- name: Setup Git
|
|
||||||
run: |
|
|
||||||
git config --global user.email "searxng-bot@users.noreply.github.com"
|
|
||||||
git config --global user.name "searxng-bot"
|
|
||||||
|
|
||||||
- name: Merge and push translation updates
|
|
||||||
run: make V=1 weblate.translations.commit
|
|
||||||
|
|
||||||
- name: Create PR
|
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
|
||||||
with:
|
|
||||||
author: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
|
||||||
committer: "searxng-bot <searxng-bot@users.noreply.github.com>"
|
|
||||||
title: "[mod] i18n: update translations from Weblate"
|
|
||||||
commit-message: "[mod] i18n: update translations from Weblate"
|
|
||||||
branch: "translations_update"
|
|
||||||
delete-branch: "true"
|
|
||||||
draft: "false"
|
|
||||||
signoff: "false"
|
|
||||||
body: |
|
|
||||||
Update translations from Weblate
|
|
||||||
labels: |
|
|
||||||
area:i18n
|
|
||||||
28
.github/workflows/security.yml
vendored
Normal file
28
.github/workflows/security.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: "Security checks"
|
||||||
|
on: # yamllint disable-line rule:truthy
|
||||||
|
schedule:
|
||||||
|
- cron: "42 05 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dockers:
|
||||||
|
name: Trivy ${{ matrix.image }}
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Trivy vulnerability scanner
|
||||||
|
uses: aquasecurity/trivy-action@master
|
||||||
|
with:
|
||||||
|
image-ref: 'searxng/searxng:latest'
|
||||||
|
ignore-unfixed: false
|
||||||
|
vuln-type: 'os,library'
|
||||||
|
severity: 'UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL'
|
||||||
|
format: 'sarif'
|
||||||
|
output: 'trivy-results.sarif'
|
||||||
|
|
||||||
|
- name: Upload Trivy scan results to GitHub Security tab
|
||||||
|
uses: github/codeql-action/upload-sarif@v2
|
||||||
|
with:
|
||||||
|
sarif_file: 'trivy-results.sarif'
|
||||||
59
.github/workflows/translations-update.yml
vendored
Normal file
59
.github/workflows/translations-update.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: "Update translations"
|
||||||
|
on: # yamllint disable-line rule:truthy
|
||||||
|
schedule:
|
||||||
|
- cron: "05 07 * * 5"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
babel:
|
||||||
|
name: "create PR for additions from weblate"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
if: ${{ github.repository_owner == 'searxng' && github.ref == 'refs/heads/master' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: '0'
|
||||||
|
token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
architecture: 'x64'
|
||||||
|
- name: Cache Python dependencies
|
||||||
|
id: cache-python
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./local
|
||||||
|
./.nvm
|
||||||
|
./node_modules
|
||||||
|
key: python-ubuntu-20.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
|
||||||
|
- name: weblate & git setup
|
||||||
|
env:
|
||||||
|
WEBLATE_CONFIG: ${{ secrets.WEBLATE_CONFIG }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.config
|
||||||
|
echo "${WEBLATE_CONFIG}" > ~/.config/weblate
|
||||||
|
git config --global user.email "searxng-bot@users.noreply.github.com"
|
||||||
|
git config --global user.name "searxng-bot"
|
||||||
|
- name: Merge and push transation updates
|
||||||
|
run: |
|
||||||
|
make V=1 weblate.translations.commit
|
||||||
|
- name: Create Pull Request
|
||||||
|
id: cpr
|
||||||
|
uses: peter-evans/create-pull-request@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
|
||||||
|
commit-message: '[l10n] update translations from Weblate'
|
||||||
|
committer: searxng-bot <searxng-bot@users.noreply.github.com>
|
||||||
|
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||||
|
signoff: false
|
||||||
|
branch: translations_update
|
||||||
|
delete-branch: true
|
||||||
|
draft: false
|
||||||
|
title: '[l10n] update translations from Weblate'
|
||||||
|
body: |
|
||||||
|
update translations from Weblate
|
||||||
|
labels: |
|
||||||
|
translation
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,7 +9,6 @@ geckodriver.log
|
|||||||
.coverage
|
.coverage
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
.govm/
|
|
||||||
.nvm/
|
.nvm/
|
||||||
cache/
|
cache/
|
||||||
build/
|
build/
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
[[language]]
|
|
||||||
name = "python"
|
|
||||||
language-servers = ["basedpyright", "pylsp"]
|
|
||||||
auto-format = true
|
|
||||||
|
|
||||||
[language-server.pylsp.config.pylsp]
|
|
||||||
plugins.pylint.enabled = true
|
|
||||||
plugins.isort.enabled = true
|
|
||||||
plugins.black.enabled = true
|
|
||||||
plugins.black.skip_string_normalization = true
|
|
||||||
plugins.black.line_length = 120
|
|
||||||
@@ -162,7 +162,7 @@ no-docstring-rgx=^_
|
|||||||
property-classes=abc.abstractproperty
|
property-classes=abc.abstractproperty
|
||||||
|
|
||||||
# Regular expression matching correct variable names
|
# Regular expression matching correct variable names
|
||||||
variable-rgx=([a-zA-Z0-9_]*)$
|
variable-rgx=(([a-z][a-zA-Z0-9_]{2,30})|(_[a-z0-9_]*)|([a-z]))$
|
||||||
|
|
||||||
|
|
||||||
[FORMAT]
|
[FORMAT]
|
||||||
@@ -311,7 +311,7 @@ dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
|||||||
ignored-argument-names=_.*|^ignored_|^unused_
|
ignored-argument-names=_.*|^ignored_|^unused_
|
||||||
|
|
||||||
# Tells whether we should check for unused import in __init__ files.
|
# Tells whether we should check for unused import in __init__ files.
|
||||||
init-import=yes
|
init-import=no
|
||||||
|
|
||||||
# List of qualified module names which can have objects that can redefine
|
# List of qualified module names which can have objects that can redefine
|
||||||
# builtins.
|
# builtins.
|
||||||
|
|||||||
4
.tool-versions
Normal file
4
.tool-versions
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
nodejs 23.5.0
|
||||||
|
python 3.13.1
|
||||||
|
shellcheck 0.10.0
|
||||||
|
sqlite 3.47.2
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
AI Policy
|
|
||||||
=========
|
|
||||||
|
|
||||||
Restrictions on Generative AI Usage
|
|
||||||
-----------------------------------
|
|
||||||
- **All AI usage in any form must be disclosed.** You must state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
|
|
||||||
- **The human-in-the-loop must fully understand all code.** If you use generative AI tools as an aid in developing code or documentation changes, ensure that you fully understand the proposed changes and can explain why they are the correct approach.
|
|
||||||
- **AI should never be the main author of the PR.** AI may be used as a tool to help with developing, but the human contribution to the code changes should always be reasonably larger than the part written by AI. For example, you should be the one that decides about the structure of the PR, not the LLM.
|
|
||||||
- **Issues and PR descriptions must be fully human-written.** Do not post output from Large Language Models or similar generative AI as comments on any of our discussion forums (e.g. GitHub Issues, Matrix, ...), as such comments tend to be formulaic and low content. If you're a not a native English speaker, using AI for translating self-written issue texts to English is okay, but please keep the wording as close as possible to the original wording.
|
|
||||||
- **Bad AI drivers will be denounced.** People who produce bad contributions that are clearly AI (slop) will be blocked for all future contributions.
|
|
||||||
|
|
||||||
There are Humans Here
|
|
||||||
---------------------
|
|
||||||
Every discussion, issue, and pull request is read and reviewed by humans. It is a boundary point at which people interact with each other and the work done. It is rude and disrespectful to approach this boundary with low-effort, unqualified work, since it puts the burden of validation on the maintainer.
|
|
||||||
|
|
||||||
It takes a lot of maintainer time and energy to review AI-generated contributions! Sending the output of an LLM to open source project maintainers extracts work from them in the form of design and code review, so we call this kind of contribution an "extractive contribution".
|
|
||||||
|
|
||||||
The *golden rule* is that a contribution should be worth more to the project than the time it takes to review it, which is usually not the case if large parts of your PR were written by LLMs.
|
|
||||||
@@ -2,7 +2,7 @@ searxng is a fork from `searx <https://github.com/searx/searx>`_ and is
|
|||||||
maintained by Markus Heiser (`@return42 <https://github.com/return42>`_)
|
maintained by Markus Heiser (`@return42 <https://github.com/return42>`_)
|
||||||
|
|
||||||
People who have submitted patches/translations, reported bugs, consulted
|
People who have submitted patches/translations, reported bugs, consulted
|
||||||
features or generally made SearXNG better:
|
features or generally made searx better:
|
||||||
|
|
||||||
- Adam Tauber `@asciimoo <https://github.com/asciimoo>`_
|
- Adam Tauber `@asciimoo <https://github.com/asciimoo>`_
|
||||||
- Matej Cotman `@matejc <https://github.com/matejc>`_
|
- Matej Cotman `@matejc <https://github.com/matejc>`_
|
||||||
@@ -174,8 +174,3 @@ features or generally made SearXNG better:
|
|||||||
- @micsthepick
|
- @micsthepick
|
||||||
- Daniel Kukula `<https://github.com/dkuku>`
|
- Daniel Kukula `<https://github.com/dkuku>`
|
||||||
- Patrick Evans `https://github.com/holysoles`
|
- Patrick Evans `https://github.com/holysoles`
|
||||||
- Daniel Mowitz `<https://daniel.mowitz.rocks>`
|
|
||||||
- `Bearz314 <https://github.com/bearz314>`_
|
|
||||||
- Tommaso Colella `<https://github.com/gioleppe>`
|
|
||||||
- @AgentScrubbles
|
|
||||||
- Filip Mikina `<https://github.com/fiffek>`
|
|
||||||
|
|||||||
49
CONTRIBUTING.md
Normal file
49
CONTRIBUTING.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# How to contribute
|
||||||
|
|
||||||
|
## Resources in the documentation
|
||||||
|
|
||||||
|
* [Development quickstart](https://docs.searxng.org/dev/quickstart.html)
|
||||||
|
* [Contribution guide](https://docs.searxng.org/dev/contribution_guide.html)
|
||||||
|
|
||||||
|
## Submitting PRs
|
||||||
|
|
||||||
|
Please follow the provided PR template when writing a description for your changes.
|
||||||
|
|
||||||
|
Do not take criticism personally. When you get feedback, it is about your work,
|
||||||
|
not your character, personality, etc. Keep in mind we all want to make the project better.
|
||||||
|
|
||||||
|
When something is not clear, please ask questions to clear things up.
|
||||||
|
|
||||||
|
If you would like to introduce a big architectural changes or do a refactoring
|
||||||
|
either in the codebase or the development tools, please open an issue with a proposal
|
||||||
|
first. This way we can think together about the problem and probably come up
|
||||||
|
with a better solution.
|
||||||
|
|
||||||
|
## Coding conventions and guidelines
|
||||||
|
|
||||||
|
### Commit messages
|
||||||
|
|
||||||
|
* Always write descriptive commit messages ("fix bug" is not acceptable).
|
||||||
|
* Use the present tense ("Add feature" not "Added feature").
|
||||||
|
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...").
|
||||||
|
* Limit the first line to 72 characters or less.
|
||||||
|
* Include the number of the issue you are fixing.
|
||||||
|
|
||||||
|
### Coding guidelines
|
||||||
|
|
||||||
|
As a Python project, we must follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 20](https://www.python.org/dev/peps/pep-0020/) guidelines.
|
||||||
|
|
||||||
|
Furthermore, follow the Clean code conventions. The most important
|
||||||
|
in this project are the following rules:
|
||||||
|
|
||||||
|
* Simpler is better. [KISS principle](https://en.wikipedia.org/wiki/KISS_principle)
|
||||||
|
* Be consistent.
|
||||||
|
* Every function must do one thing.
|
||||||
|
* Use descriptive names for functions and variables.
|
||||||
|
* Always look for the root cause.
|
||||||
|
* Keep configurable data high level.
|
||||||
|
* Avoid negative conditionals.
|
||||||
|
* Prefer fewer arguments.
|
||||||
|
* Do not add obvious comment to code.
|
||||||
|
* Do not comment out code, just delete lines.
|
||||||
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
.. _Quickstart guide: https://docs.searxng.org/dev/quickstart.html
|
|
||||||
.. _Commits guide: https://docs.searxng.org/dev/commits.html
|
|
||||||
.. _AI Policy: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
|
||||||
.. _Weblate: https://translate.codeberg.org/projects/searxng/searxng/
|
|
||||||
.. _GitHub Codespaces: https://docs.github.com/en/codespaces/overview
|
|
||||||
.. _120 hours per month: https://github.com/settings/billing
|
|
||||||
.. _list of existing Codespaces: https://github.com/codespaces
|
|
||||||
|
|
||||||
Thank you for your interest in SearXNG.
|
|
||||||
|
|
||||||
Have a look at our `Quickstart guide`_, it's very easy to contribute.
|
|
||||||
|
|
||||||
Further information on *how-to* can be found
|
|
||||||
`here <https://docs.searxng.org/dev/index.html>`_.
|
|
||||||
|
|
||||||
Translations
|
|
||||||
============
|
|
||||||
|
|
||||||
Help translate SearXNG at `Weblate`_.
|
|
||||||
|
|
||||||
.. image:: https://translate.codeberg.org/widget/searxng/searxng/horizontal-auto.svg
|
|
||||||
:target: https://translate.codeberg.org/engage/searxng/
|
|
||||||
:alt: Weblate
|
|
||||||
:width: 768px
|
|
||||||
|
|
||||||
Cloud development
|
|
||||||
=================
|
|
||||||
|
|
||||||
You can contribute from your browser using `GitHub Codespaces`_:
|
|
||||||
|
|
||||||
- Fork the repository.
|
|
||||||
- Click on the ``<> Code`` green button.
|
|
||||||
- Click on the ``Codespaces`` tab instead of ``Local``.
|
|
||||||
- Click on ``Create codespace on master``.
|
|
||||||
- VSCode is going to start in the browser.
|
|
||||||
- Wait for ``git pull && make install`` to appear and then disappear.
|
|
||||||
- You have `120 hours per month`_ (see also your `list of existing Codespaces`_).
|
|
||||||
- You can start SearXNG using ``make run`` in the terminal or by pressing ``Ctrl+Shift+B``.
|
|
||||||
|
|
||||||
How-to contribute
|
|
||||||
=================
|
|
||||||
|
|
||||||
Submitting pull requests
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
Please follow the provided PR template when writing a description for your
|
|
||||||
changes.
|
|
||||||
|
|
||||||
Do not take criticism personally. When you get feedback, it is about your work,
|
|
||||||
not your character or personality. Keep in mind we all want to make SearXNG
|
|
||||||
better.
|
|
||||||
|
|
||||||
When something is not clear, please ask questions to clear things up.
|
|
||||||
|
|
||||||
If you would like to introduce a big architectural change or do a refactor,
|
|
||||||
either in the codebase or the development tooling, please open an issue with a
|
|
||||||
proposal first. This way we can think together about the problem and probably
|
|
||||||
come up with a better solution.
|
|
||||||
|
|
||||||
Coding conventions and guidelines
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
Commit messages
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
- Always write descriptive commit messages *("fix bug" is not acceptable)*.
|
|
||||||
- Use the present tense *("Add feature", not "Added feature")*.
|
|
||||||
- Use the imperative mood *("Move cursor to...", not "Moves cursor to...")*.
|
|
||||||
- Limit the first line (commit title) to 72 characters or less.
|
|
||||||
|
|
||||||
See `Commits guide`_ for more details.
|
|
||||||
|
|
||||||
Coding guidelines
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
As a Python project, we must follow `PEP 8 <https://www.python.org/dev/peps/pep-0008/>`_
|
|
||||||
and `PEP 20 <https://www.python.org/dev/peps/pep-0020/>`_ guidelines.
|
|
||||||
|
|
||||||
Furthermore, follow Clean Code conventions. The most important
|
|
||||||
rules in this project are:
|
|
||||||
|
|
||||||
- Simpler is better. `KISS principle <https://en.wikipedia.org/wiki/KISS_principle>`_
|
|
||||||
- Be consistent.
|
|
||||||
- Every function must do one thing.
|
|
||||||
- Use descriptive names for functions and variables.
|
|
||||||
- Always look for the root cause.
|
|
||||||
- Keep configurable data high level.
|
|
||||||
- Avoid negative conditionals.
|
|
||||||
- Prefer fewer arguments.
|
|
||||||
- Do not add obvious comments to code.
|
|
||||||
- Do not comment out code, delete lines instead.
|
|
||||||
|
|
||||||
AI Policy
|
|
||||||
~~~~~~~~~
|
|
||||||
|
|
||||||
For our policy on the use of AI tools, please read `AI Policy`_.
|
|
||||||
89
Dockerfile
Normal file
89
Dockerfile
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
FROM alpine:3.20
|
||||||
|
ENTRYPOINT ["/sbin/tini","--","/usr/local/searxng/dockerfiles/docker-entrypoint.sh"]
|
||||||
|
EXPOSE 8080
|
||||||
|
VOLUME /etc/searxng
|
||||||
|
|
||||||
|
ARG SEARXNG_GID=977
|
||||||
|
ARG SEARXNG_UID=977
|
||||||
|
|
||||||
|
RUN addgroup -g ${SEARXNG_GID} searxng && \
|
||||||
|
adduser -u ${SEARXNG_UID} -D -h /usr/local/searxng -s /bin/sh -G searxng searxng
|
||||||
|
|
||||||
|
ENV INSTANCE_NAME=searxng \
|
||||||
|
AUTOCOMPLETE= \
|
||||||
|
BASE_URL= \
|
||||||
|
MORTY_KEY= \
|
||||||
|
MORTY_URL= \
|
||||||
|
SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml \
|
||||||
|
UWSGI_SETTINGS_PATH=/etc/searxng/uwsgi.ini \
|
||||||
|
UWSGI_WORKERS=%k \
|
||||||
|
UWSGI_THREADS=4
|
||||||
|
|
||||||
|
WORKDIR /usr/local/searxng
|
||||||
|
|
||||||
|
COPY requirements.txt ./requirements.txt
|
||||||
|
|
||||||
|
RUN apk add --no-cache -t build-dependencies \
|
||||||
|
build-base \
|
||||||
|
py3-setuptools \
|
||||||
|
python3-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libxslt-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
openssl-dev \
|
||||||
|
tar \
|
||||||
|
git \
|
||||||
|
&& apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
python3 \
|
||||||
|
py3-pip \
|
||||||
|
libxml2 \
|
||||||
|
libxslt \
|
||||||
|
openssl \
|
||||||
|
tini \
|
||||||
|
uwsgi \
|
||||||
|
uwsgi-python3 \
|
||||||
|
brotli \
|
||||||
|
&& pip3 install --break-system-packages --no-cache -r requirements.txt \
|
||||||
|
&& apk del build-dependencies \
|
||||||
|
&& rm -rf /root/.cache
|
||||||
|
|
||||||
|
COPY --chown=searxng:searxng dockerfiles ./dockerfiles
|
||||||
|
COPY --chown=searxng:searxng searx ./searx
|
||||||
|
|
||||||
|
ARG TIMESTAMP_SETTINGS=0
|
||||||
|
ARG TIMESTAMP_UWSGI=0
|
||||||
|
ARG VERSION_GITCOMMIT=unknown
|
||||||
|
|
||||||
|
RUN su searxng -c "/usr/bin/python3 -m compileall -q searx" \
|
||||||
|
&& touch -c --date=@${TIMESTAMP_SETTINGS} searx/settings.yml \
|
||||||
|
&& touch -c --date=@${TIMESTAMP_UWSGI} dockerfiles/uwsgi.ini \
|
||||||
|
&& find /usr/local/searxng/searx/static -a \( -name '*.html' -o -name '*.css' -o -name '*.js' \
|
||||||
|
-o -name '*.svg' -o -name '*.ttf' -o -name '*.eot' \) \
|
||||||
|
-type f -exec gzip -9 -k {} \+ -exec brotli --best {} \+
|
||||||
|
|
||||||
|
# Keep these arguments at the end to prevent redundant layer rebuilds
|
||||||
|
ARG LABEL_DATE=
|
||||||
|
ARG GIT_URL=unknown
|
||||||
|
ARG SEARXNG_GIT_VERSION=unknown
|
||||||
|
ARG SEARXNG_DOCKER_TAG=unknown
|
||||||
|
ARG LABEL_VCS_REF=
|
||||||
|
ARG LABEL_VCS_URL=
|
||||||
|
LABEL maintainer="searxng <${GIT_URL}>" \
|
||||||
|
description="A privacy-respecting, hackable metasearch engine." \
|
||||||
|
version="${SEARXNG_GIT_VERSION}" \
|
||||||
|
org.label-schema.schema-version="1.0" \
|
||||||
|
org.label-schema.name="searxng" \
|
||||||
|
org.label-schema.version="${SEARXNG_GIT_VERSION}" \
|
||||||
|
org.label-schema.url="${LABEL_VCS_URL}" \
|
||||||
|
org.label-schema.vcs-ref=${LABEL_VCS_REF} \
|
||||||
|
org.label-schema.vcs-url=${LABEL_VCS_URL} \
|
||||||
|
org.label-schema.build-date="${LABEL_DATE}" \
|
||||||
|
org.label-schema.usage="https://github.com/searxng/searxng-docker" \
|
||||||
|
org.opencontainers.image.title="searxng" \
|
||||||
|
org.opencontainers.image.version="${SEARXNG_DOCKER_TAG}" \
|
||||||
|
org.opencontainers.image.url="${LABEL_VCS_URL}" \
|
||||||
|
org.opencontainers.image.revision=${LABEL_VCS_REF} \
|
||||||
|
org.opencontainers.image.source=${LABEL_VCS_URL} \
|
||||||
|
org.opencontainers.image.created="${LABEL_DATE}" \
|
||||||
|
org.opencontainers.image.documentation="https://github.com/searxng/searxng-docker"
|
||||||
47
Makefile
47
Makefile
@@ -17,6 +17,7 @@ help:
|
|||||||
@echo 'install - developer install of SearxNG into virtualenv'
|
@echo 'install - developer install of SearxNG into virtualenv'
|
||||||
@echo 'uninstall - uninstall developer installation'
|
@echo 'uninstall - uninstall developer installation'
|
||||||
@echo 'clean - clean up working tree'
|
@echo 'clean - clean up working tree'
|
||||||
|
@echo 'search.checker - check search engines'
|
||||||
@echo 'test - run shell & CI tests'
|
@echo 'test - run shell & CI tests'
|
||||||
@echo 'test.shell - test shell scripts'
|
@echo 'test.shell - test shell scripts'
|
||||||
@echo 'ci.test - run CI tests'
|
@echo 'ci.test - run CI tests'
|
||||||
@@ -31,53 +32,61 @@ install uninstall:
|
|||||||
$(Q)./manage pyenv.$@
|
$(Q)./manage pyenv.$@
|
||||||
|
|
||||||
PHONY += clean
|
PHONY += clean
|
||||||
clean: py.clean docs.clean node.clean nvm.clean go.clean test.clean
|
clean: py.clean docs.clean node.clean nvm.clean test.clean
|
||||||
$(Q)./manage build_msg CLEAN "common files"
|
$(Q)./manage build_msg CLEAN "common files"
|
||||||
$(Q)find . -name '*.orig' -exec rm -f {} +
|
$(Q)find . -name '*.orig' -exec rm -f {} +
|
||||||
$(Q)find . -name '*.rej' -exec rm -f {} +
|
$(Q)find . -name '*.rej' -exec rm -f {} +
|
||||||
$(Q)find . -name '*~' -exec rm -f {} +
|
$(Q)find . -name '*~' -exec rm -f {} +
|
||||||
$(Q)find . -name '*.bak' -exec rm -f {} +
|
$(Q)find . -name '*.bak' -exec rm -f {} +
|
||||||
|
|
||||||
|
lxc.clean:
|
||||||
|
$(Q)rm -rf lxc-env
|
||||||
|
|
||||||
|
PHONY += search.checker search.checker.%
|
||||||
|
search.checker: install
|
||||||
|
$(Q)./manage pyenv.cmd searxng-checker -v
|
||||||
|
|
||||||
|
search.checker.%: install
|
||||||
|
$(Q)./manage pyenv.cmd searxng-checker -v "$(subst _, ,$(patsubst search.checker.%,%,$@))"
|
||||||
|
|
||||||
PHONY += test ci.test test.shell
|
PHONY += test ci.test test.shell
|
||||||
test: test.yamllint test.black test.pyright_modified test.pylint test.unit test.robot test.rst test.shell test.shfmt
|
ci.test: test.yamllint test.black test.pyright test.pylint test.unit test.robot test.rst test.pybabel
|
||||||
ci.test: test test.pybabel
|
test: test.yamllint test.black test.pyright test.pylint test.unit test.robot test.rst test.shell
|
||||||
test.shell:
|
test.shell:
|
||||||
$(Q)shellcheck -x -s dash \
|
$(Q)shellcheck -x -s dash \
|
||||||
container/entrypoint.sh
|
dockerfiles/docker-entrypoint.sh
|
||||||
$(Q)shellcheck -x -s bash \
|
$(Q)shellcheck -x -s bash \
|
||||||
utils/brand.sh \
|
utils/brand.sh \
|
||||||
$(MTOOLS) \
|
$(MTOOLS) \
|
||||||
utils/lib.sh \
|
utils/lib.sh \
|
||||||
utils/lib_sxng*.sh \
|
utils/lib_sxng*.sh \
|
||||||
utils/lib_govm.sh \
|
utils/lib_go.sh \
|
||||||
utils/lib_nvm.sh \
|
utils/lib_nvm.sh \
|
||||||
utils/lib_redis.sh \
|
utils/lib_redis.sh \
|
||||||
utils/lib_valkey.sh \
|
utils/searxng.sh \
|
||||||
utils/searxng.sh
|
utils/lxc.sh \
|
||||||
|
utils/lxc-searxng.env \
|
||||||
|
utils/searx.sh \
|
||||||
|
utils/filtron.sh \
|
||||||
|
utils/morty.sh
|
||||||
$(Q)$(MTOOLS) build_msg TEST "$@ OK"
|
$(Q)$(MTOOLS) build_msg TEST "$@ OK"
|
||||||
|
|
||||||
PHONY += format
|
|
||||||
format: format.python format.shell
|
|
||||||
|
|
||||||
# wrap ./manage script
|
# wrap ./manage script
|
||||||
|
|
||||||
MANAGE += weblate.translations.commit weblate.push.translations
|
MANAGE += weblate.translations.commit weblate.push.translations
|
||||||
MANAGE += data.all data.traits data.useragents data.locales data.currencies
|
MANAGE += data.all data.traits data.useragents data.locales
|
||||||
MANAGE += docs.html docs.live docs.gh-pages docs.prebuild docs.clean
|
MANAGE += docs.html docs.live docs.gh-pages docs.prebuild docs.clean
|
||||||
MANAGE += podman.build
|
MANAGE += docker.build docker.push docker.buildx
|
||||||
MANAGE += docker.build docker.buildx
|
|
||||||
MANAGE += container.build container.test container.push
|
|
||||||
MANAGE += gecko.driver
|
MANAGE += gecko.driver
|
||||||
MANAGE += node.env node.env.dev node.clean
|
MANAGE += node.env node.env.dev node.clean
|
||||||
MANAGE += py.build py.clean
|
MANAGE += py.build py.clean
|
||||||
MANAGE += pyenv pyenv.install pyenv.uninstall
|
MANAGE += pyenv pyenv.install pyenv.uninstall
|
||||||
MANAGE += format.python format.shell
|
MANAGE += format.python
|
||||||
MANAGE += test.yamllint test.pylint test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean test.themes test.pyright test.pyright_modified test.shfmt
|
MANAGE += test.yamllint test.pylint test.pyright test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean
|
||||||
MANAGE += themes.all themes.simple themes.simple.analyze themes.fix themes.lint themes.test
|
MANAGE += themes.all themes.simple themes.simple.test pygments.less
|
||||||
MANAGE += static.build.commit static.build.drop static.build.restore
|
MANAGE += static.build.commit static.build.drop static.build.restore
|
||||||
MANAGE += nvm.install nvm.clean nvm.status nvm.nodejs
|
MANAGE += nvm.install nvm.clean nvm.status nvm.nodejs
|
||||||
MANAGE += go.env.dev go.clean
|
|
||||||
|
|
||||||
PHONY += $(MANAGE)
|
PHONY += $(MANAGE)
|
||||||
|
|
||||||
@@ -86,8 +95,8 @@ $(MANAGE):
|
|||||||
|
|
||||||
# short hands of selected targets
|
# short hands of selected targets
|
||||||
|
|
||||||
PHONY += docs container themes
|
PHONY += docs docker themes
|
||||||
|
|
||||||
docs: docs.html
|
docs: docs.html
|
||||||
container: container.build
|
docker: docker.build
|
||||||
themes: themes.all
|
themes: themes.all
|
||||||
|
|||||||
@@ -1,34 +1,25 @@
|
|||||||
<!-- FILL IN THESE FIELDS .. and delete the comments after reading.
|
## What does this PR do?
|
||||||
|
|
||||||
Use Markdown for formatting -> https://www.markdowntools.io/cheat-sheet
|
<!-- MANDATORY -->
|
||||||
-->
|
|
||||||
|
|
||||||
### What does this PR do?
|
<!-- explain the changes in your PR, algorithms, design, architecture -->
|
||||||
|
|
||||||
<!-- Explain the motivation and changes in your pull request. -->
|
## Why is this change important?
|
||||||
|
|
||||||
### How to test this PR locally?
|
<!-- MANDATORY -->
|
||||||
|
|
||||||
<!-- Commands to run the tests or instructions to test the changes. Are there
|
<!-- explain the motivation behind your PR -->
|
||||||
any edge cases (environment, language, or other contexts) to take into
|
|
||||||
account? -->
|
|
||||||
|
|
||||||
### Related issues
|
## How to test this PR locally?
|
||||||
|
|
||||||
|
<!-- commands to run the tests or instructions to test the changes -->
|
||||||
|
|
||||||
|
## Author's checklist
|
||||||
|
|
||||||
|
<!-- additional notes for reviewers -->
|
||||||
|
|
||||||
|
## Related issues
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Closes: #234
|
Closes #234
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Code of Conduct
|
|
||||||
|
|
||||||
<!-- ⚠️ Bad AI drivers will be denounced: People who produce bad contributions
|
|
||||||
that are clearly AI (slop) will be blocked for all future contributions.
|
|
||||||
-->
|
|
||||||
|
|
||||||
[AI Policy]: https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
|
||||||
|
|
||||||
- [ ] **I hereby confirm that this PR conforms with the [AI Policy].**
|
|
||||||
|
|
||||||
If I have used AI tools for working on the changes in this PR, I will
|
|
||||||
attach a list of all AI tools I used and how I used them. I hereby confirm
|
|
||||||
that I haven't used any other tools than the ones I mention below.
|
|
||||||
|
|||||||
136
README.rst
136
README.rst
@@ -1,62 +1,130 @@
|
|||||||
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
.. SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
.. _metasearch engine: https://en.wikipedia.org/wiki/Metasearch_engine
|
----
|
||||||
.. _Installation guide: https://docs.searxng.org/admin/installation.html
|
|
||||||
.. _Configuration guide: https://docs.searxng.org/admin/settings/index.html
|
|
||||||
.. _CONTRIBUTING: https://github.com/searxng/searxng/blob/master/CONTRIBUTING.rst
|
|
||||||
.. _LICENSE: https://github.com/searxng/searxng/blob/master/LICENSE
|
|
||||||
|
|
||||||
.. figure:: https://raw.githubusercontent.com/searxng/searxng/master/client/simple/src/brand/searxng.svg
|
.. figure:: https://raw.githubusercontent.com/searxng/searxng/master/src/brand/searxng.svg
|
||||||
:target: https://searxng.org
|
:target: https://docs.searxng.org/
|
||||||
:alt: SearXNG
|
:alt: SearXNG
|
||||||
:width: 512px
|
:width: 100%
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
SearXNG is a `metasearch engine`_. Users are neither tracked nor profiled.
|
Privacy-respecting, hackable `metasearch engine`_
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/organization-3050ff?style=flat-square&logo=searxng&logoColor=fff&cacheSeconds=86400
|
Searx.space_ lists ready-to-use running instances.
|
||||||
:target: https://github.com/searxng
|
|
||||||
:alt: Organization
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/documentation-3050ff?style=flat-square&logo=readthedocs&logoColor=fff&cacheSeconds=86400
|
A user_, admin_ and developer_ handbook is available on the homepage_.
|
||||||
:target: https://docs.searxng.org
|
|
||||||
:alt: Documentation
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/github/license/searxng/searxng?style=flat-square&label=license&color=3050ff&cacheSeconds=86400
|
|SearXNG install|
|
||||||
|
|SearXNG homepage|
|
||||||
|
|SearXNG wiki|
|
||||||
|
|AGPL License|
|
||||||
|
|Issues|
|
||||||
|
|commits|
|
||||||
|
|weblate|
|
||||||
|
|SearXNG logo|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
.. _searx.space: https://searx.space
|
||||||
|
.. _user: https://docs.searxng.org/user
|
||||||
|
.. _admin: https://docs.searxng.org/admin
|
||||||
|
.. _developer: https://docs.searxng.org/dev
|
||||||
|
.. _homepage: https://docs.searxng.org/
|
||||||
|
.. _metasearch engine: https://en.wikipedia.org/wiki/Metasearch_engine
|
||||||
|
|
||||||
|
.. |SearXNG logo| image:: https://raw.githubusercontent.com/searxng/searxng/master/src/brand/searxng-wordmark.svg
|
||||||
|
:target: https://docs.searxng.org/
|
||||||
|
:width: 5%
|
||||||
|
|
||||||
|
.. |SearXNG install| image:: https://img.shields.io/badge/-install-blue
|
||||||
|
:target: https://docs.searxng.org/admin/installation.html
|
||||||
|
|
||||||
|
.. |SearXNG homepage| image:: https://img.shields.io/badge/-homepage-blue
|
||||||
|
:target: https://docs.searxng.org/
|
||||||
|
|
||||||
|
.. |SearXNG wiki| image:: https://img.shields.io/badge/-wiki-blue
|
||||||
|
:target: https://github.com/searxng/searxng/wiki
|
||||||
|
|
||||||
|
.. |AGPL License| image:: https://img.shields.io/badge/license-AGPL-blue.svg
|
||||||
:target: https://github.com/searxng/searxng/blob/master/LICENSE
|
:target: https://github.com/searxng/searxng/blob/master/LICENSE
|
||||||
:alt: License
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/github/commit-activity/y/searxng/searxng/master?style=flat-square&label=commits&color=3050ff&cacheSeconds=3600
|
.. |Issues| image:: https://img.shields.io/github/issues/searxng/searxng?color=yellow&label=issues
|
||||||
:target: https://github.com/searxng/searxng/commits/master/
|
:target: https://github.com/searxng/searxng/issues
|
||||||
:alt: Commits
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/weblate/progress/searxng?server=https%3A%2F%2Ftranslate.codeberg.org&style=flat-square&label=translated&color=3050ff&cacheSeconds=86400
|
.. |PR| image:: https://img.shields.io/github/issues-pr-raw/searxng/searxng?color=yellow&label=PR
|
||||||
|
:target: https://github.com/searxng/searxng/pulls
|
||||||
|
|
||||||
|
.. |commits| image:: https://img.shields.io/github/commit-activity/y/searxng/searxng?color=yellow&label=commits
|
||||||
|
:target: https://github.com/searxng/searxng/commits/master
|
||||||
|
|
||||||
|
.. |weblate| image:: https://translate.codeberg.org/widgets/searxng/-/searxng/svg-badge.svg
|
||||||
:target: https://translate.codeberg.org/projects/searxng/
|
:target: https://translate.codeberg.org/projects/searxng/
|
||||||
:alt: Translated
|
|
||||||
|
|
||||||
|
Contact
|
||||||
|
=======
|
||||||
|
|
||||||
|
Ask questions or chat with the SearXNG community (this not a chatbot) on
|
||||||
|
|
||||||
|
IRC
|
||||||
|
`#searxng on libera.chat <https://web.libera.chat/?channel=#searxng>`_
|
||||||
|
which is bridged to Matrix.
|
||||||
|
|
||||||
|
Matrix
|
||||||
|
`#searxng:matrix.org <https://matrix.to/#/#searxng:matrix.org>`_
|
||||||
|
|
||||||
|
|
||||||
Setup
|
Setup
|
||||||
=====
|
=====
|
||||||
|
|
||||||
To install SearXNG, see `Installation guide`_.
|
- A well maintained `Docker image`_, also built for ARM64 and ARM/v7
|
||||||
|
architectures.
|
||||||
|
- Alternatively there are *up to date* `installation scripts`_.
|
||||||
|
- For individual setup consult our detailed `Step by step`_ instructions.
|
||||||
|
- To fine-tune your instance, take a look at the `Administrator documentation`_.
|
||||||
|
|
||||||
To fine-tune SearXNG, see `Configuration guide`_.
|
.. _Administrator documentation: https://docs.searxng.org/admin/index.html
|
||||||
|
.. _Step by step: https://docs.searxng.org/admin/installation-searxng.html
|
||||||
|
.. _installation scripts: https://docs.searxng.org/admin/installation-scripts.html
|
||||||
|
.. _Docker image: https://github.com/searxng/searxng-docker
|
||||||
|
|
||||||
Further information on *how-to* can be found `here <https://docs.searxng.org/admin/index.html>`_.
|
Translations
|
||||||
|
============
|
||||||
|
|
||||||
Connect
|
.. _Weblate: https://translate.codeberg.org/projects/searxng/searxng/
|
||||||
=======
|
|
||||||
|
|
||||||
If you have questions or want to connect with others in the community:
|
Help translate SearXNG at `Weblate`_
|
||||||
|
|
||||||
|
.. figure:: https://translate.codeberg.org/widgets/searxng/-/multi-auto.svg
|
||||||
|
:target: https://translate.codeberg.org/projects/searxng/
|
||||||
|
|
||||||
- `#searxng:matrix.org <https://matrix.to/#/#searxng:matrix.org>`_
|
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
============
|
============
|
||||||
|
|
||||||
See CONTRIBUTING_ for more details.
|
.. _development quickstart: https://docs.searxng.org/dev/quickstart.html
|
||||||
|
.. _developer documentation: https://docs.searxng.org/dev/index.html
|
||||||
|
|
||||||
License
|
Are you a developer? Have a look at our `development quickstart`_ guide, it's
|
||||||
=======
|
very easy to contribute. Additionally we have a `developer documentation`_.
|
||||||
|
|
||||||
This project is licensed under the GNU Affero General Public License (AGPL-3.0).
|
|
||||||
See LICENSE_ for more details.
|
Codespaces
|
||||||
|
==========
|
||||||
|
|
||||||
|
You can contribute from your browser using `GitHub Codespaces`_:
|
||||||
|
|
||||||
|
- Fork the repository
|
||||||
|
- Click on the ``<> Code`` green button
|
||||||
|
- Click on the ``Codespaces`` tab instead of ``Local``
|
||||||
|
- Click on ``Create codespace on master``
|
||||||
|
- VSCode is going to start in the browser
|
||||||
|
- Wait for ``git pull && make install`` to appear and then disappear
|
||||||
|
- You have `120 hours per month`_ (see also your `list of existing Codespaces`_)
|
||||||
|
- You can start SearXNG using ``make run`` in the terminal or by pressing ``Ctrl+Shift+B``
|
||||||
|
|
||||||
|
.. _GitHub Codespaces: https://docs.github.com/en/codespaces/overview
|
||||||
|
.. _120 hours per month: https://github.com/settings/billing
|
||||||
|
.. _list of existing Codespaces: https://github.com/codespaces
|
||||||
|
|||||||
2
client/simple/.gitignore
vendored
2
client/simple/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
dist
|
|
||||||
node_modules
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/stylelintrc.json",
|
|
||||||
"plugins": ["stylelint-prettier"],
|
|
||||||
"extends": ["stylelint-config-standard-less"],
|
|
||||||
"rules": {
|
|
||||||
"at-rule-no-vendor-prefix": null,
|
|
||||||
"at-rule-prelude-no-invalid": null,
|
|
||||||
"declaration-empty-line-before": null,
|
|
||||||
"declaration-property-value-no-unknown": null,
|
|
||||||
"no-invalid-position-at-import-rule": null,
|
|
||||||
"prettier/prettier": true,
|
|
||||||
"property-no-vendor-prefix": null,
|
|
||||||
"selector-attribute-quotes": null,
|
|
||||||
"selector-class-pattern": null,
|
|
||||||
"selector-id-pattern": null,
|
|
||||||
"selector-no-vendor-prefix": null,
|
|
||||||
"shorthand-property-no-redundant-values": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
=====================
|
|
||||||
MEMO vite development
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Local install::
|
|
||||||
|
|
||||||
# in folder ./client/simple/
|
|
||||||
$ npm install
|
|
||||||
|
|
||||||
Start development server::
|
|
||||||
|
|
||||||
$ ./manage vite.simple.dev
|
|
||||||
|
|
||||||
# in folder ./client/simple/
|
|
||||||
$ npm exec -- vite
|
|
||||||
|
|
||||||
Fix source code::
|
|
||||||
|
|
||||||
# in folder ./client/simple/
|
|
||||||
$ npm run fix
|
|
||||||
|
|
||||||
Fix & Build::
|
|
||||||
|
|
||||||
$ ./manage vite.simple.build
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": true,
|
|
||||||
"includes": ["**", "!node_modules", "!src/brand", "!src/svg"]
|
|
||||||
},
|
|
||||||
"assist": {
|
|
||||||
"enabled": true,
|
|
||||||
"actions": {
|
|
||||||
"preset": "recommended",
|
|
||||||
"source": {
|
|
||||||
"useSortedAttributes": "on",
|
|
||||||
"useSortedProperties": "on"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"bracketSameLine": false,
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"formatWithErrors": false,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineEnding": "lf",
|
|
||||||
"lineWidth": 120
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"preset": "recommended",
|
|
||||||
"complexity": {
|
|
||||||
"noForEach": "error",
|
|
||||||
"noImplicitCoercions": "error",
|
|
||||||
"noRedundantDefaultExport": "error",
|
|
||||||
"noUselessCatchBinding": "error",
|
|
||||||
"noUselessUndefined": "error",
|
|
||||||
"useArrayFind": "error",
|
|
||||||
"useSimplifiedLogicExpression": "error"
|
|
||||||
},
|
|
||||||
"correctness": {
|
|
||||||
"noGlobalDirnameFilename": "error",
|
|
||||||
"useImportExtensions": "error",
|
|
||||||
"useJsonImportAttributes": "error",
|
|
||||||
"useSingleJsDocAsterisk": "error"
|
|
||||||
},
|
|
||||||
"nursery": {
|
|
||||||
"noFloatingPromises": "warn",
|
|
||||||
"noMisusedPromises": "warn",
|
|
||||||
"useAwaitThenable": "off",
|
|
||||||
"useExhaustiveSwitchCases": "warn",
|
|
||||||
"useExplicitType": "off",
|
|
||||||
"useRegexpExec": "warn"
|
|
||||||
},
|
|
||||||
"performance": {
|
|
||||||
"noAwaitInLoops": "error",
|
|
||||||
"noBarrelFile": "error",
|
|
||||||
"noDelete": "error",
|
|
||||||
"noNamespaceImport": "error",
|
|
||||||
"noReExportAll": "error",
|
|
||||||
"useTopLevelRegex": "error"
|
|
||||||
},
|
|
||||||
"style": {
|
|
||||||
"noCommonJs": "error",
|
|
||||||
"noEnum": "error",
|
|
||||||
"noImplicitBoolean": "error",
|
|
||||||
"noIncrementDecrement": "error",
|
|
||||||
"noInferrableTypes": "error",
|
|
||||||
"noMultiAssign": "error",
|
|
||||||
"noMultilineString": "error",
|
|
||||||
"noNamespace": "error",
|
|
||||||
"noNegationElse": "error",
|
|
||||||
"noNestedTernary": "error",
|
|
||||||
"noParameterAssign": "error",
|
|
||||||
"noParameterProperties": "error",
|
|
||||||
"noSubstr": "error",
|
|
||||||
"noUnusedTemplateLiteral": "error",
|
|
||||||
"noUselessElse": "error",
|
|
||||||
"noYodaExpression": "error",
|
|
||||||
"useAsConstAssertion": "error",
|
|
||||||
"useAtIndex": "error",
|
|
||||||
"useCollapsedElseIf": "error",
|
|
||||||
"useCollapsedIf": "error",
|
|
||||||
"useConsistentArrayType": {
|
|
||||||
"level": "error",
|
|
||||||
"options": {
|
|
||||||
"syntax": "shorthand"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"useConsistentBuiltinInstantiation": "error",
|
|
||||||
"useConsistentEnumValueType": "error",
|
|
||||||
"useConsistentMemberAccessibility": {
|
|
||||||
"level": "error",
|
|
||||||
"options": {
|
|
||||||
"accessibility": "explicit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"useConsistentObjectDefinitions": {
|
|
||||||
"level": "error",
|
|
||||||
"options": {
|
|
||||||
"syntax": "explicit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"useConsistentTypeDefinitions": {
|
|
||||||
"level": "error",
|
|
||||||
"options": {
|
|
||||||
"style": "type"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"useDefaultSwitchClause": "error",
|
|
||||||
"useDestructuring": "error",
|
|
||||||
"useExplicitLengthCheck": "error",
|
|
||||||
"useForOf": "error",
|
|
||||||
"useGroupedAccessorPairs": "error",
|
|
||||||
"useNumberNamespace": "error",
|
|
||||||
"useNumericSeparators": "error",
|
|
||||||
"useObjectSpread": "error",
|
|
||||||
"useReadonlyClassProperties": "error",
|
|
||||||
"useSelfClosingElements": "error",
|
|
||||||
"useShorthandAssign": "error",
|
|
||||||
"useSingleVarDeclarator": "error",
|
|
||||||
"useThrowNewError": "error",
|
|
||||||
"useThrowOnlyError": "error",
|
|
||||||
"useTrimStartEnd": "error",
|
|
||||||
"useUnifiedTypeSignatures": "error"
|
|
||||||
},
|
|
||||||
"suspicious": {
|
|
||||||
"noBitwiseOperators": "error",
|
|
||||||
"noConstantBinaryExpressions": "error",
|
|
||||||
"noDeprecatedImports": "error",
|
|
||||||
"noEmptyBlockStatements": "error",
|
|
||||||
"noEqualsToNull": "error",
|
|
||||||
"noEvolvingTypes": "error",
|
|
||||||
"noForIn": "error",
|
|
||||||
"noImportCycles": "error",
|
|
||||||
"noNestedPromises": "error",
|
|
||||||
"noParametersOnlyUsedInRecursion": "error",
|
|
||||||
"noReturnAssign": "error",
|
|
||||||
"noUnassignedVariables": "error",
|
|
||||||
"noVar": "error",
|
|
||||||
"useNumberToFixedDigitsArgument": "error",
|
|
||||||
"useStaticResponseMethods": "error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"arrowParentheses": "always",
|
|
||||||
"jsxQuoteStyle": "double",
|
|
||||||
"quoteProperties": "asNeeded",
|
|
||||||
"quoteStyle": "double",
|
|
||||||
"semicolons": "always",
|
|
||||||
"trailingCommas": "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
/*
|
|
||||||
this file is generated automatically by searxng_extra/update/update_pygments.py
|
|
||||||
using pygments version 2.20.0:
|
|
||||||
|
|
||||||
./manage templates.simple.pygments
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
.code-highlight {
|
|
||||||
|
|
||||||
pre { line-height: 125%; }
|
|
||||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
.hll { background-color: #ffffcc }
|
|
||||||
.c { color: #3D7B7B; font-style: italic } /* Comment */
|
|
||||||
.err { border: 1px solid #F00 } /* Error */
|
|
||||||
.k { color: #008000; font-weight: bold } /* Keyword */
|
|
||||||
.o { color: #666 } /* Operator */
|
|
||||||
.ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
|
|
||||||
.cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
|
|
||||||
.cp { color: #9C6500 } /* Comment.Preproc */
|
|
||||||
.cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
|
|
||||||
.c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
|
|
||||||
.cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
|
|
||||||
.gd { color: #A00000 } /* Generic.Deleted */
|
|
||||||
.ge { font-style: italic } /* Generic.Emph */
|
|
||||||
.ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
|
||||||
.gr { color: #E40000 } /* Generic.Error */
|
|
||||||
.gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
|
||||||
.gi { color: #008400 } /* Generic.Inserted */
|
|
||||||
.go { color: #717171 } /* Generic.Output */
|
|
||||||
.gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
|
||||||
.gs { font-weight: bold } /* Generic.Strong */
|
|
||||||
.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
|
||||||
.gt { color: #04D } /* Generic.Traceback */
|
|
||||||
.kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
|
||||||
.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
|
||||||
.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
|
|
||||||
.kp { color: #008000 } /* Keyword.Pseudo */
|
|
||||||
.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
|
||||||
.kt { color: #B00040 } /* Keyword.Type */
|
|
||||||
.m { color: #666 } /* Literal.Number */
|
|
||||||
.s { color: #BA2121 } /* Literal.String */
|
|
||||||
.na { color: #687822 } /* Name.Attribute */
|
|
||||||
.nb { color: #008000 } /* Name.Builtin */
|
|
||||||
.nc { color: #00F; font-weight: bold } /* Name.Class */
|
|
||||||
.no { color: #800 } /* Name.Constant */
|
|
||||||
.nd { color: #A2F } /* Name.Decorator */
|
|
||||||
.ni { color: #717171; font-weight: bold } /* Name.Entity */
|
|
||||||
.ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
|
|
||||||
.nf { color: #00F } /* Name.Function */
|
|
||||||
.nl { color: #767600 } /* Name.Label */
|
|
||||||
.nn { color: #00F; font-weight: bold } /* Name.Namespace */
|
|
||||||
.nt { color: #008000; font-weight: bold } /* Name.Tag */
|
|
||||||
.nv { color: #19177C } /* Name.Variable */
|
|
||||||
.ow { color: #A2F; font-weight: bold } /* Operator.Word */
|
|
||||||
.w { color: #BBB } /* Text.Whitespace */
|
|
||||||
.mb { color: #666 } /* Literal.Number.Bin */
|
|
||||||
.mf { color: #666 } /* Literal.Number.Float */
|
|
||||||
.mh { color: #666 } /* Literal.Number.Hex */
|
|
||||||
.mi { color: #666 } /* Literal.Number.Integer */
|
|
||||||
.mo { color: #666 } /* Literal.Number.Oct */
|
|
||||||
.sa { color: #BA2121 } /* Literal.String.Affix */
|
|
||||||
.sb { color: #BA2121 } /* Literal.String.Backtick */
|
|
||||||
.sc { color: #BA2121 } /* Literal.String.Char */
|
|
||||||
.dl { color: #BA2121 } /* Literal.String.Delimiter */
|
|
||||||
.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
|
||||||
.s2 { color: #BA2121 } /* Literal.String.Double */
|
|
||||||
.se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
|
|
||||||
.sh { color: #BA2121 } /* Literal.String.Heredoc */
|
|
||||||
.si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
|
|
||||||
.sx { color: #008000 } /* Literal.String.Other */
|
|
||||||
.sr { color: #A45A77 } /* Literal.String.Regex */
|
|
||||||
.s1 { color: #BA2121 } /* Literal.String.Single */
|
|
||||||
.ss { color: #19177C } /* Literal.String.Symbol */
|
|
||||||
.bp { color: #008000 } /* Name.Builtin.Pseudo */
|
|
||||||
.fm { color: #00F } /* Name.Function.Magic */
|
|
||||||
.vc { color: #19177C } /* Name.Variable.Class */
|
|
||||||
.vg { color: #19177C } /* Name.Variable.Global */
|
|
||||||
.vi { color: #19177C } /* Name.Variable.Instance */
|
|
||||||
.vm { color: #19177C } /* Name.Variable.Magic */
|
|
||||||
.il { color: #666 } /* Literal.Number.Integer.Long */
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-highlight-dark(){
|
|
||||||
.code-highlight {
|
|
||||||
|
|
||||||
pre { line-height: 125%; }
|
|
||||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
.hll { background-color: #49483e }
|
|
||||||
.c { color: #959077 } /* Comment */
|
|
||||||
.err { color: #ED007E; background-color: #1E0010 } /* Error */
|
|
||||||
.esc { color: #F8F8F2 } /* Escape */
|
|
||||||
.g { color: #F8F8F2 } /* Generic */
|
|
||||||
.k { color: #66D9EF } /* Keyword */
|
|
||||||
.l { color: #AE81FF } /* Literal */
|
|
||||||
.n { color: #F8F8F2 } /* Name */
|
|
||||||
.o { color: #FF4689 } /* Operator */
|
|
||||||
.x { color: #F8F8F2 } /* Other */
|
|
||||||
.p { color: #F8F8F2 } /* Punctuation */
|
|
||||||
.ch { color: #959077 } /* Comment.Hashbang */
|
|
||||||
.cm { color: #959077 } /* Comment.Multiline */
|
|
||||||
.cp { color: #959077 } /* Comment.Preproc */
|
|
||||||
.cpf { color: #959077 } /* Comment.PreprocFile */
|
|
||||||
.c1 { color: #959077 } /* Comment.Single */
|
|
||||||
.cs { color: #959077 } /* Comment.Special */
|
|
||||||
.gd { color: #FF4689 } /* Generic.Deleted */
|
|
||||||
.ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */
|
|
||||||
.ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
|
||||||
.gr { color: #F8F8F2 } /* Generic.Error */
|
|
||||||
.gh { color: #F8F8F2 } /* Generic.Heading */
|
|
||||||
.gi { color: #A6E22E } /* Generic.Inserted */
|
|
||||||
.go { color: #66D9EF } /* Generic.Output */
|
|
||||||
.gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */
|
|
||||||
.gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */
|
|
||||||
.gu { color: #959077 } /* Generic.Subheading */
|
|
||||||
.gt { color: #F8F8F2 } /* Generic.Traceback */
|
|
||||||
.kc { color: #66D9EF } /* Keyword.Constant */
|
|
||||||
.kd { color: #66D9EF } /* Keyword.Declaration */
|
|
||||||
.kn { color: #FF4689 } /* Keyword.Namespace */
|
|
||||||
.kp { color: #66D9EF } /* Keyword.Pseudo */
|
|
||||||
.kr { color: #66D9EF } /* Keyword.Reserved */
|
|
||||||
.kt { color: #66D9EF } /* Keyword.Type */
|
|
||||||
.ld { color: #E6DB74 } /* Literal.Date */
|
|
||||||
.m { color: #AE81FF } /* Literal.Number */
|
|
||||||
.s { color: #E6DB74 } /* Literal.String */
|
|
||||||
.na { color: #A6E22E } /* Name.Attribute */
|
|
||||||
.nb { color: #F8F8F2 } /* Name.Builtin */
|
|
||||||
.nc { color: #A6E22E } /* Name.Class */
|
|
||||||
.no { color: #66D9EF } /* Name.Constant */
|
|
||||||
.nd { color: #A6E22E } /* Name.Decorator */
|
|
||||||
.ni { color: #F8F8F2 } /* Name.Entity */
|
|
||||||
.ne { color: #A6E22E } /* Name.Exception */
|
|
||||||
.nf { color: #A6E22E } /* Name.Function */
|
|
||||||
.nl { color: #F8F8F2 } /* Name.Label */
|
|
||||||
.nn { color: #F8F8F2 } /* Name.Namespace */
|
|
||||||
.nx { color: #A6E22E } /* Name.Other */
|
|
||||||
.py { color: #F8F8F2 } /* Name.Property */
|
|
||||||
.nt { color: #FF4689 } /* Name.Tag */
|
|
||||||
.nv { color: #F8F8F2 } /* Name.Variable */
|
|
||||||
.ow { color: #FF4689 } /* Operator.Word */
|
|
||||||
.pm { color: #F8F8F2 } /* Punctuation.Marker */
|
|
||||||
.w { color: #F8F8F2 } /* Text.Whitespace */
|
|
||||||
.mb { color: #AE81FF } /* Literal.Number.Bin */
|
|
||||||
.mf { color: #AE81FF } /* Literal.Number.Float */
|
|
||||||
.mh { color: #AE81FF } /* Literal.Number.Hex */
|
|
||||||
.mi { color: #AE81FF } /* Literal.Number.Integer */
|
|
||||||
.mo { color: #AE81FF } /* Literal.Number.Oct */
|
|
||||||
.sa { color: #E6DB74 } /* Literal.String.Affix */
|
|
||||||
.sb { color: #E6DB74 } /* Literal.String.Backtick */
|
|
||||||
.sc { color: #E6DB74 } /* Literal.String.Char */
|
|
||||||
.dl { color: #E6DB74 } /* Literal.String.Delimiter */
|
|
||||||
.sd { color: #E6DB74 } /* Literal.String.Doc */
|
|
||||||
.s2 { color: #E6DB74 } /* Literal.String.Double */
|
|
||||||
.se { color: #AE81FF } /* Literal.String.Escape */
|
|
||||||
.sh { color: #E6DB74 } /* Literal.String.Heredoc */
|
|
||||||
.si { color: #E6DB74 } /* Literal.String.Interpol */
|
|
||||||
.sx { color: #E6DB74 } /* Literal.String.Other */
|
|
||||||
.sr { color: #E6DB74 } /* Literal.String.Regex */
|
|
||||||
.s1 { color: #E6DB74 } /* Literal.String.Single */
|
|
||||||
.ss { color: #E6DB74 } /* Literal.String.Symbol */
|
|
||||||
.bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */
|
|
||||||
.fm { color: #A6E22E } /* Name.Function.Magic */
|
|
||||||
.vc { color: #F8F8F2 } /* Name.Variable.Class */
|
|
||||||
.vg { color: #F8F8F2 } /* Name.Variable.Global */
|
|
||||||
.vi { color: #F8F8F2 } /* Name.Variable.Instance */
|
|
||||||
.vm { color: #F8F8F2 } /* Name.Variable.Magic */
|
|
||||||
.il { color: #AE81FF } /* Literal.Number.Integer.Long */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5161
client/simple/package-lock.json
generated
5161
client/simple/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
|
||||||
"name": "@searxng/theme-simple",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"private": true,
|
|
||||||
"license": "AGPL-3.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "npm run build:icons && npm run build:vite",
|
|
||||||
"build:icons": "node theme_icons.ts",
|
|
||||||
"build:vite": "vite build",
|
|
||||||
"clean": "rm -Rf node_modules",
|
|
||||||
"fix": "npm run fix:stylelint && npm run fix:biome && npm run fix:package",
|
|
||||||
"fix:biome": "biome check --write",
|
|
||||||
"fix:package": "sort-package-json --quiet",
|
|
||||||
"fix:stylelint": "stylelint --fix strict 'src/**/*.{scss,sass,less,styl}'",
|
|
||||||
"lint": "npm run lint:biome && npm run lint:tsc",
|
|
||||||
"lint:biome": "biome lint",
|
|
||||||
"lint:tsc": "tsc --noEmit"
|
|
||||||
},
|
|
||||||
"browserslist": [
|
|
||||||
"baseline 2022",
|
|
||||||
"not dead"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"ionicons": "^8.0.13",
|
|
||||||
"normalize.css": "8.0.1",
|
|
||||||
"ol": "^10.9.0",
|
|
||||||
"swiped-events": "1.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@biomejs/biome": "2.5.3",
|
|
||||||
"@types/node": "^26.1.1",
|
|
||||||
"browserslist": "^4.28.6",
|
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
|
||||||
"edge.js": "^6.5.1",
|
|
||||||
"less": "^4.6.7",
|
|
||||||
"mathjs": "^15.2.0",
|
|
||||||
"sharp": "~0.35.3",
|
|
||||||
"sort-package-json": "^4.0.0",
|
|
||||||
"stylelint": "^17.14.0",
|
|
||||||
"stylelint-config-standard-less": "^4.1.0",
|
|
||||||
"stylelint-prettier": "^5.0.3",
|
|
||||||
"svgo": "^4.0.2",
|
|
||||||
"typescript": "~7.0.2",
|
|
||||||
"vite": "^8.1.4",
|
|
||||||
"vite-bundle-analyzer": "^1.3.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for client-side plugins.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* Handle conditional loading of the plugin in:
|
|
||||||
*
|
|
||||||
* - client/simple/src/js/router.ts
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export abstract class Plugin {
|
|
||||||
/**
|
|
||||||
* Plugin name.
|
|
||||||
*/
|
|
||||||
protected readonly id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @remarks
|
|
||||||
* Don't hold references of this instance outside the class.
|
|
||||||
*/
|
|
||||||
protected constructor(id: string) {
|
|
||||||
this.id = id;
|
|
||||||
|
|
||||||
queueMicrotask(() => this.invoke());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async invoke(): Promise<void> {
|
|
||||||
try {
|
|
||||||
console.debug(`[PLUGIN] ${this.id}: Running...`);
|
|
||||||
const result = await this.run();
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
console.debug(`[PLUGIN] ${this.id}: Running post-exec...`);
|
|
||||||
// @ts-expect-error
|
|
||||||
void (await this.post(result as NonNullable<Awaited<ReturnType<this["run"]>>>));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[PLUGIN] ${this.id}:`, error);
|
|
||||||
} finally {
|
|
||||||
console.debug(`[PLUGIN] ${this.id}: Done.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin goes here.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* The plugin is already loaded at this point. If you wish to execute
|
|
||||||
* conditions to exit early, consider moving the logic to:
|
|
||||||
*
|
|
||||||
* - client/simple/src/js/router.ts
|
|
||||||
*
|
|
||||||
* ...to avoid unnecessarily loading this plugin on the client.
|
|
||||||
*/
|
|
||||||
protected abstract run(): Promise<unknown>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Post-execution hook.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* The hook is only executed if `#run()` returns a truthy value.
|
|
||||||
*/
|
|
||||||
// @ts-expect-error
|
|
||||||
protected abstract post(result: NonNullable<Awaited<ReturnType<this["run"]>>>): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
// core
|
|
||||||
void import.meta.glob(["./*.ts", "./util/**/.ts"], { eager: true });
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import type { Plugin } from "./Plugin.ts";
|
|
||||||
import { type EndpointsKeys, endpoint } from "./toolkit.ts";
|
|
||||||
|
|
||||||
type Options =
|
|
||||||
| {
|
|
||||||
on: "global";
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
on: "endpoint";
|
|
||||||
where: EndpointsKeys[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const load = <T extends Plugin>(instance: () => Promise<T>, options: Options): void => {
|
|
||||||
if (!check(options)) return;
|
|
||||||
|
|
||||||
void instance();
|
|
||||||
};
|
|
||||||
|
|
||||||
const check = (options: Options): boolean => {
|
|
||||||
// biome-ignore lint/style/useDefaultSwitchClause: options is typed
|
|
||||||
switch (options.on) {
|
|
||||||
case "global": {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case "endpoint": {
|
|
||||||
if (!options.where.includes(endpoint)) {
|
|
||||||
// not on the expected endpoint
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import { http, listen, settings } from "../toolkit.ts";
|
|
||||||
import { assertElement } from "../util/assertElement.ts";
|
|
||||||
|
|
||||||
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
let res: Response;
|
|
||||||
|
|
||||||
if (settings.method === "GET") {
|
|
||||||
res = await http("GET", `./autocompleter?q=${query}`);
|
|
||||||
} else {
|
|
||||||
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await res.json();
|
|
||||||
|
|
||||||
const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
|
|
||||||
assertElement(autocomplete);
|
|
||||||
|
|
||||||
const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
|
|
||||||
assertElement(autocompleteList);
|
|
||||||
|
|
||||||
autocomplete.classList.add("open");
|
|
||||||
autocompleteList.replaceChildren();
|
|
||||||
|
|
||||||
// show an error message that no result was found
|
|
||||||
if (results?.[1]?.length === 0) {
|
|
||||||
const noItemFoundMessage = Object.assign(document.createElement("li"), {
|
|
||||||
className: "no-item-found",
|
|
||||||
textContent: settings.translations?.no_item_found ?? "No results found"
|
|
||||||
});
|
|
||||||
autocompleteList.append(noItemFoundMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fragment = new DocumentFragment();
|
|
||||||
|
|
||||||
for (const result of results[1]) {
|
|
||||||
const li = Object.assign(document.createElement("li"), { textContent: result });
|
|
||||||
|
|
||||||
listen("mousedown", li, () => {
|
|
||||||
qInput.value = result;
|
|
||||||
|
|
||||||
const form = document.querySelector<HTMLFormElement>("#search");
|
|
||||||
form?.submit();
|
|
||||||
});
|
|
||||||
|
|
||||||
fragment.append(li);
|
|
||||||
}
|
|
||||||
|
|
||||||
autocompleteList.append(fragment);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching autocomplete results:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const qInput = document.getElementById("q") as HTMLInputElement | null;
|
|
||||||
assertElement(qInput);
|
|
||||||
|
|
||||||
let timeoutId: number;
|
|
||||||
|
|
||||||
listen("input", qInput, () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
const query = qInput.value;
|
|
||||||
const minLength = settings.autocomplete_min ?? 2;
|
|
||||||
|
|
||||||
if (query.length < minLength) return;
|
|
||||||
|
|
||||||
timeoutId = window.setTimeout(async () => {
|
|
||||||
if (query === qInput.value) {
|
|
||||||
await fetchResults(qInput, query);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
const autocomplete: HTMLElement | null = document.querySelector<HTMLElement>(".autocomplete");
|
|
||||||
const autocompleteList: HTMLUListElement | null = document.querySelector<HTMLUListElement>(".autocomplete ul");
|
|
||||||
if (autocompleteList) {
|
|
||||||
listen("keydown", qInput, (event: KeyboardEvent) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
autocomplete?.classList.remove("open");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
listen("keyup", qInput, (event: KeyboardEvent) => {
|
|
||||||
const listItems = [...autocompleteList.children] as HTMLElement[];
|
|
||||||
|
|
||||||
const currentIndex = listItems.findIndex((item) => item.classList.contains("active"));
|
|
||||||
let newCurrentIndex = -1;
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
case "ArrowUp": {
|
|
||||||
const currentItem = listItems[currentIndex];
|
|
||||||
if (currentItem && currentIndex >= 0) {
|
|
||||||
currentItem.classList.remove("active");
|
|
||||||
}
|
|
||||||
// we need to add listItems.length to the index calculation here because the JavaScript modulos
|
|
||||||
// operator doesn't work with negative numbers
|
|
||||||
newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowDown": {
|
|
||||||
const currentItem = listItems[currentIndex];
|
|
||||||
if (currentItem && currentIndex >= 0) {
|
|
||||||
currentItem.classList.remove("active");
|
|
||||||
}
|
|
||||||
newCurrentIndex = (currentIndex + 1) % listItems.length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "Enter":
|
|
||||||
if (autocomplete) {
|
|
||||||
autocomplete.classList.remove("open");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newCurrentIndex !== -1) {
|
|
||||||
const selectedItem = listItems[newCurrentIndex];
|
|
||||||
if (selectedItem) {
|
|
||||||
selectedItem.classList.add("active");
|
|
||||||
|
|
||||||
if (!selectedItem.classList.contains("no-item-found")) {
|
|
||||||
const qInput = document.getElementById("q") as HTMLInputElement | null;
|
|
||||||
if (qInput) {
|
|
||||||
qInput.value = selectedItem.textContent ?? "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("blur", qInput, () => {
|
|
||||||
autocomplete?.classList.remove("open");
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("focus", qInput, () => {
|
|
||||||
autocomplete?.classList.add("open");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,488 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import { listen, mutable, settings } from "../toolkit.ts";
|
|
||||||
import { assertElement } from "../util/assertElement.ts";
|
|
||||||
|
|
||||||
export type KeyBindingLayout = "default" | "vim";
|
|
||||||
|
|
||||||
type KeyBinding = {
|
|
||||||
key: string;
|
|
||||||
fun: (event: KeyboardEvent) => void;
|
|
||||||
des: string;
|
|
||||||
cat: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top";
|
|
||||||
|
|
||||||
/* common base for layouts */
|
|
||||||
const baseKeyBinding: Record<string, KeyBinding> = {
|
|
||||||
Escape: {
|
|
||||||
key: "ESC",
|
|
||||||
fun: (event: KeyboardEvent) => removeFocus(event),
|
|
||||||
des: "remove focus from the focused input",
|
|
||||||
cat: "Control"
|
|
||||||
},
|
|
||||||
c: {
|
|
||||||
key: "c",
|
|
||||||
fun: () => copyURLToClipboard(),
|
|
||||||
des: "copy url of the selected result to the clipboard",
|
|
||||||
cat: "Results"
|
|
||||||
},
|
|
||||||
h: {
|
|
||||||
key: "h",
|
|
||||||
fun: () => toggleHelp(keyBindings),
|
|
||||||
des: "toggle help window",
|
|
||||||
cat: "Other"
|
|
||||||
},
|
|
||||||
i: {
|
|
||||||
key: "i",
|
|
||||||
fun: () => searchInputFocus(),
|
|
||||||
des: "focus on the search input",
|
|
||||||
cat: "Control"
|
|
||||||
},
|
|
||||||
n: {
|
|
||||||
key: "n",
|
|
||||||
fun: () => GoToNextPage(),
|
|
||||||
des: "go to next page",
|
|
||||||
cat: "Results"
|
|
||||||
},
|
|
||||||
o: {
|
|
||||||
key: "o",
|
|
||||||
fun: () => openResult(false),
|
|
||||||
des: "open search result",
|
|
||||||
cat: "Results"
|
|
||||||
},
|
|
||||||
p: {
|
|
||||||
key: "p",
|
|
||||||
fun: () => GoToPreviousPage(),
|
|
||||||
des: "go to previous page",
|
|
||||||
cat: "Results"
|
|
||||||
},
|
|
||||||
r: {
|
|
||||||
key: "r",
|
|
||||||
fun: () => reloadPage(),
|
|
||||||
des: "reload page from the server",
|
|
||||||
cat: "Control"
|
|
||||||
},
|
|
||||||
t: {
|
|
||||||
key: "t",
|
|
||||||
fun: () => openResult(true),
|
|
||||||
des: "open the result in a new tab",
|
|
||||||
cat: "Results"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> = {
|
|
||||||
// SearXNG layout
|
|
||||||
default: {
|
|
||||||
ArrowLeft: {
|
|
||||||
key: "←",
|
|
||||||
fun: () => highlightResult("up")(),
|
|
||||||
des: "select previous search result",
|
|
||||||
cat: "Results"
|
|
||||||
},
|
|
||||||
ArrowRight: {
|
|
||||||
key: "→",
|
|
||||||
fun: () => highlightResult("down")(),
|
|
||||||
des: "select next search result",
|
|
||||||
cat: "Results"
|
|
||||||
},
|
|
||||||
...baseKeyBinding
|
|
||||||
},
|
|
||||||
|
|
||||||
// Vim-like keyboard layout
|
|
||||||
vim: {
|
|
||||||
b: {
|
|
||||||
key: "b",
|
|
||||||
fun: () => scrollPage(-window.innerHeight),
|
|
||||||
des: "scroll one page up",
|
|
||||||
cat: "Navigation"
|
|
||||||
},
|
|
||||||
d: {
|
|
||||||
key: "d",
|
|
||||||
fun: () => scrollPage(window.innerHeight / 2),
|
|
||||||
des: "scroll half a page down",
|
|
||||||
cat: "Navigation"
|
|
||||||
},
|
|
||||||
f: {
|
|
||||||
key: "f",
|
|
||||||
fun: () => scrollPage(window.innerHeight),
|
|
||||||
des: "scroll one page down",
|
|
||||||
cat: "Navigation"
|
|
||||||
},
|
|
||||||
g: {
|
|
||||||
key: "g",
|
|
||||||
fun: () => scrollPageTo(-document.body.scrollHeight, "top"),
|
|
||||||
des: "scroll to the top of the page",
|
|
||||||
cat: "Navigation"
|
|
||||||
},
|
|
||||||
j: {
|
|
||||||
key: "j",
|
|
||||||
fun: () => highlightResult("down")(),
|
|
||||||
des: "select next search result",
|
|
||||||
cat: "Results"
|
|
||||||
},
|
|
||||||
k: {
|
|
||||||
key: "k",
|
|
||||||
fun: () => highlightResult("up")(),
|
|
||||||
des: "select previous search result",
|
|
||||||
cat: "Results"
|
|
||||||
},
|
|
||||||
u: {
|
|
||||||
key: "u",
|
|
||||||
fun: () => scrollPage(-window.innerHeight / 2),
|
|
||||||
des: "scroll half a page up",
|
|
||||||
cat: "Navigation"
|
|
||||||
},
|
|
||||||
v: {
|
|
||||||
key: "v",
|
|
||||||
fun: () => scrollPageTo(document.body.scrollHeight, "bottom"),
|
|
||||||
des: "scroll to the bottom of the page",
|
|
||||||
cat: "Navigation"
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
key: "y",
|
|
||||||
fun: () => copyURLToClipboard(),
|
|
||||||
des: "copy url of the selected result to the clipboard",
|
|
||||||
cat: "Results"
|
|
||||||
},
|
|
||||||
...baseKeyBinding
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const keyBindings: Record<string, KeyBinding> =
|
|
||||||
settings.hotkeys && settings.hotkeys in keyBindingLayouts
|
|
||||||
? keyBindingLayouts[settings.hotkeys]
|
|
||||||
: keyBindingLayouts.default;
|
|
||||||
|
|
||||||
const isElementInDetail = (element?: HTMLElement): boolean => {
|
|
||||||
const ancestor = element?.closest(".detail, .result");
|
|
||||||
return ancestor?.classList.contains("detail") ?? false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getResultElement = (element?: HTMLElement): HTMLElement | undefined => {
|
|
||||||
return element?.closest(".result") ?? undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isImageResult = (resultElement?: HTMLElement): boolean => {
|
|
||||||
return resultElement?.classList.contains("result-images") ?? false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const highlightResult =
|
|
||||||
(which: HighlightResultElement | HTMLElement) =>
|
|
||||||
(noScroll?: boolean, keepFocus?: boolean): void => {
|
|
||||||
let effectiveWhich = which;
|
|
||||||
let current = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
|
||||||
if (!current) {
|
|
||||||
// no selection : choose the first one
|
|
||||||
current = document.querySelector<HTMLElement>(".result");
|
|
||||||
if (!current) {
|
|
||||||
// no first one : there are no results
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// replace up/down actions by selecting first one
|
|
||||||
if (which === "down" || which === "up") {
|
|
||||||
effectiveWhich = current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = Array.from(document.querySelectorAll<HTMLElement>(".result"));
|
|
||||||
|
|
||||||
let next: HTMLElement | undefined;
|
|
||||||
|
|
||||||
if (typeof effectiveWhich === "string") {
|
|
||||||
switch (effectiveWhich) {
|
|
||||||
case "visible": {
|
|
||||||
const top = document.documentElement.scrollTop || document.body.scrollTop;
|
|
||||||
const bot = top + document.documentElement.clientHeight;
|
|
||||||
|
|
||||||
for (const element of results) {
|
|
||||||
const etop = element.offsetTop;
|
|
||||||
const ebot = etop + element.clientHeight;
|
|
||||||
if (ebot <= bot && etop > top) {
|
|
||||||
next = element;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "down":
|
|
||||||
next = results[results.indexOf(current) + 1] || current;
|
|
||||||
break;
|
|
||||||
case "up":
|
|
||||||
next = results[results.indexOf(current) - 1] || current;
|
|
||||||
break;
|
|
||||||
case "bottom":
|
|
||||||
next = results.at(-1);
|
|
||||||
break;
|
|
||||||
// biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended
|
|
||||||
case "top":
|
|
||||||
default:
|
|
||||||
[next] = results;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next = effectiveWhich;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next) {
|
|
||||||
current.removeAttribute("data-vim-selected");
|
|
||||||
next.setAttribute("data-vim-selected", "true");
|
|
||||||
|
|
||||||
if (!keepFocus) {
|
|
||||||
const link = next.querySelector<HTMLAnchorElement>("h3 a") || next.querySelector<HTMLAnchorElement>("a");
|
|
||||||
link?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!noScroll) {
|
|
||||||
mutable.scrollPageToSelected?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadPage = (): void => {
|
|
||||||
document.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFocus = (event: KeyboardEvent): void => {
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
const tagName = target?.tagName?.toLowerCase();
|
|
||||||
|
|
||||||
if (document.activeElement && (tagName === "input" || tagName === "select" || tagName === "textarea")) {
|
|
||||||
(document.activeElement as HTMLElement).blur();
|
|
||||||
} else {
|
|
||||||
mutable.closeDetail?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageButtonClick = (css_selector: string): void => {
|
|
||||||
const button = document.querySelector<HTMLButtonElement>(css_selector);
|
|
||||||
if (button) {
|
|
||||||
button.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const GoToNextPage = (): void => {
|
|
||||||
pageButtonClick('nav#pagination .next_page button[type="submit"]');
|
|
||||||
};
|
|
||||||
|
|
||||||
const GoToPreviousPage = (): void => {
|
|
||||||
pageButtonClick('nav#pagination .previous_page button[type="submit"]');
|
|
||||||
};
|
|
||||||
|
|
||||||
mutable.scrollPageToSelected = (): void => {
|
|
||||||
const sel = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
|
||||||
if (!sel) return;
|
|
||||||
|
|
||||||
const wtop = document.documentElement.scrollTop || document.body.scrollTop;
|
|
||||||
const height = document.documentElement.clientHeight;
|
|
||||||
const etop = sel.offsetTop;
|
|
||||||
const ebot = etop + sel.clientHeight;
|
|
||||||
const offset = 120;
|
|
||||||
|
|
||||||
// first element ?
|
|
||||||
if (!sel.previousElementSibling && ebot < height) {
|
|
||||||
// set to the top of page if the first element
|
|
||||||
// is fully included in the viewport
|
|
||||||
window.scroll(window.scrollX, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wtop > etop - offset) {
|
|
||||||
window.scroll(window.scrollX, etop - offset);
|
|
||||||
} else {
|
|
||||||
const wbot = wtop + height;
|
|
||||||
if (wbot < ebot + offset) {
|
|
||||||
window.scroll(window.scrollX, ebot - height + offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollPage = (amount: number): void => {
|
|
||||||
window.scrollBy(0, amount);
|
|
||||||
highlightResult("visible")();
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollPageTo = (position: number, nav: HighlightResultElement): void => {
|
|
||||||
window.scrollTo(0, position);
|
|
||||||
highlightResult(nav)();
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchInputFocus = (): void => {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
|
|
||||||
const q = document.querySelector<HTMLInputElement>("#q");
|
|
||||||
if (q) {
|
|
||||||
q.focus();
|
|
||||||
|
|
||||||
if (q.setSelectionRange) {
|
|
||||||
const len = q.value.length;
|
|
||||||
|
|
||||||
q.setSelectionRange(len, len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openResult = (newTab: boolean): void => {
|
|
||||||
let link = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] h3 a");
|
|
||||||
if (!link) {
|
|
||||||
link = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] > a");
|
|
||||||
}
|
|
||||||
if (!link) return;
|
|
||||||
|
|
||||||
const url = link.getAttribute("href");
|
|
||||||
if (url) {
|
|
||||||
if (newTab) {
|
|
||||||
window.open(url);
|
|
||||||
} else {
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBinding): void => {
|
|
||||||
const categories: Record<string, KeyBinding[]> = {};
|
|
||||||
|
|
||||||
for (const binding of Object.values(keyBindings)) {
|
|
||||||
const { cat } = binding;
|
|
||||||
categories[cat] ??= [];
|
|
||||||
categories[cat].push(binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedCategoryKeys = Object.keys(categories).sort(
|
|
||||||
(a, b) => (categories[b]?.length ?? 0) - (categories[a]?.length ?? 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
let html = '<a href="#" class="close" aria-label="close" title="close">×</a>';
|
|
||||||
html += "<h3>How to navigate SearXNG with hotkeys</h3>";
|
|
||||||
html += "<table>";
|
|
||||||
|
|
||||||
for (const [i, categoryKey] of sortedCategoryKeys.entries()) {
|
|
||||||
const bindings = categories[categoryKey];
|
|
||||||
if (!bindings || bindings.length === 0) continue;
|
|
||||||
|
|
||||||
const isFirst = i % 2 === 0;
|
|
||||||
const isLast = i === sortedCategoryKeys.length - 1;
|
|
||||||
|
|
||||||
if (isFirst) {
|
|
||||||
html += "<tr>";
|
|
||||||
}
|
|
||||||
|
|
||||||
html += "<td>";
|
|
||||||
html += `<h4>${categoryKey}</h4>`;
|
|
||||||
html += '<ul class="list-unstyled">';
|
|
||||||
|
|
||||||
for (const binding of bindings) {
|
|
||||||
html += `<li><kbd>${binding.key}</kbd> ${binding.des}</li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
html += "</ul>";
|
|
||||||
html += "</td>";
|
|
||||||
|
|
||||||
if (!isFirst || isLast) {
|
|
||||||
html += "</tr>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html += "</table>";
|
|
||||||
|
|
||||||
divElement.innerHTML = html;
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
|
|
||||||
let helpPanel = document.querySelector<HTMLElement>("#vim-hotkeys-help");
|
|
||||||
if (helpPanel) {
|
|
||||||
// toggle hidden
|
|
||||||
helpPanel.classList.toggle("invisible");
|
|
||||||
} else {
|
|
||||||
// first call
|
|
||||||
helpPanel = Object.assign(document.createElement("div"), {
|
|
||||||
id: "vim-hotkeys-help",
|
|
||||||
className: "dialog-modal"
|
|
||||||
});
|
|
||||||
initHelpContent(helpPanel, keyBindings);
|
|
||||||
const [body] = document.getElementsByTagName("body");
|
|
||||||
if (body) {
|
|
||||||
body.appendChild(helpPanel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyURLToClipboard = async (): Promise<void> => {
|
|
||||||
const selectedResult = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
|
||||||
if (!selectedResult) return;
|
|
||||||
|
|
||||||
const resultAnchor = selectedResult.querySelector<HTMLAnchorElement>("a");
|
|
||||||
assertElement(resultAnchor);
|
|
||||||
|
|
||||||
const url = resultAnchor.getAttribute("href");
|
|
||||||
if (url) {
|
|
||||||
if (window.isSecureContext) {
|
|
||||||
await navigator.clipboard.writeText(url);
|
|
||||||
} else {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection) {
|
|
||||||
const node = document.createElement("span");
|
|
||||||
node.textContent = url;
|
|
||||||
resultAnchor.appendChild(node);
|
|
||||||
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(node);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
document.execCommand("copy");
|
|
||||||
node.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
listen("click", ".result", function (this: HTMLElement, event: PointerEvent) {
|
|
||||||
if (!isElementInDetail(event.target as HTMLElement)) {
|
|
||||||
highlightResult(this)(true, true);
|
|
||||||
|
|
||||||
const resultElement = getResultElement(event.target as HTMLElement);
|
|
||||||
|
|
||||||
if (resultElement && isImageResult(resultElement)) {
|
|
||||||
event.preventDefault();
|
|
||||||
mutable.selectImage?.(resultElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: Focus might also trigger Pointer event ^^^
|
|
||||||
listen(
|
|
||||||
"focus",
|
|
||||||
".result a",
|
|
||||||
(event: FocusEvent) => {
|
|
||||||
if (!isElementInDetail(event.target as HTMLElement)) {
|
|
||||||
const resultElement = getResultElement(event.target as HTMLElement);
|
|
||||||
|
|
||||||
if (resultElement && !resultElement.hasAttribute("data-vim-selected")) {
|
|
||||||
highlightResult(resultElement)(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resultElement && isImageResult(resultElement)) {
|
|
||||||
event.preventDefault();
|
|
||||||
mutable.selectImage?.(resultElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ capture: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
listen("keydown", document, (event: KeyboardEvent) => {
|
|
||||||
// check for modifiers so we don't break browser's hotkeys
|
|
||||||
if (Object.hasOwn(keyBindings, event.key) && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
|
|
||||||
const tagName = (event.target as HTMLElement)?.tagName?.toLowerCase();
|
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
keyBindings[event.key]?.fun(event);
|
|
||||||
} else if (event.target === document.body || tagName === "a" || tagName === "button") {
|
|
||||||
event.preventDefault();
|
|
||||||
keyBindings[event.key]?.fun(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mutable.selectNext = highlightResult("down");
|
|
||||||
mutable.selectPrevious = highlightResult("up");
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import { http, listen, settings } from "../toolkit.ts";
|
|
||||||
import { assertElement } from "../util/assertElement.ts";
|
|
||||||
|
|
||||||
let engineDescriptions: Record<string, [string, string]> | undefined;
|
|
||||||
|
|
||||||
const loadEngineDescriptions = async (): Promise<void> => {
|
|
||||||
if (engineDescriptions) return;
|
|
||||||
try {
|
|
||||||
const res = await http("GET", "engine_descriptions.json");
|
|
||||||
engineDescriptions = await res.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching engineDescriptions:", error);
|
|
||||||
}
|
|
||||||
if (!engineDescriptions) return;
|
|
||||||
|
|
||||||
for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {
|
|
||||||
const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name="${engine_name}"] .engine-description`);
|
|
||||||
const sourceText = ` (<i>${settings.translations?.Source}: ${source}</i>)`;
|
|
||||||
|
|
||||||
for (const element of elements) {
|
|
||||||
element.innerHTML = description + sourceText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {
|
|
||||||
for (const engineToggle of engineToggles) {
|
|
||||||
// check if element visible, so that only engines of the current category are modified
|
|
||||||
if (engineToggle.offsetParent) {
|
|
||||||
engineToggle.checked = !enable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>("[data-engine-name]");
|
|
||||||
for (const engineElement of engineElements) {
|
|
||||||
listen("mouseenter", engineElement, loadEngineDescriptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
const engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(
|
|
||||||
"tbody input[type=checkbox][class~=checkbox-onoff]"
|
|
||||||
);
|
|
||||||
|
|
||||||
const enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".enable-all-engines");
|
|
||||||
for (const engine of enableAllEngines) {
|
|
||||||
listen("click", engine, () => toggleEngines(true, engineToggles));
|
|
||||||
}
|
|
||||||
|
|
||||||
const disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".disable-all-engines");
|
|
||||||
for (const engine of disableAllEngines) {
|
|
||||||
listen("click", engine, () => toggleEngines(false, engineToggles));
|
|
||||||
}
|
|
||||||
|
|
||||||
listen("click", "#copy-hash", async function (this: HTMLElement) {
|
|
||||||
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
|
|
||||||
assertElement(target);
|
|
||||||
|
|
||||||
if (window.isSecureContext) {
|
|
||||||
await navigator.clipboard.writeText(target.innerText);
|
|
||||||
} else {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection) {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(target);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
document.execCommand("copy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dataset.copiedText) {
|
|
||||||
this.innerText = this.dataset.copiedText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import "../../../node_modules/swiped-events/src/swiped-events.js";
|
|
||||||
import { listen, mutable, settings } from "../toolkit.ts";
|
|
||||||
import { assertElement } from "../util/assertElement.ts";
|
|
||||||
|
|
||||||
let imgTimeoutID: number;
|
|
||||||
|
|
||||||
const imageLoader = (resultElement: HTMLElement): void => {
|
|
||||||
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
|
||||||
|
|
||||||
const imgElement = resultElement.querySelector<HTMLImageElement>(".result-images-source img");
|
|
||||||
if (!imgElement) return;
|
|
||||||
|
|
||||||
// use thumbnail until full image loads
|
|
||||||
const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
|
|
||||||
if (thumbnail) {
|
|
||||||
if (thumbnail.src === `${settings.theme_static_path}/img/img_load_error.svg`) return;
|
|
||||||
|
|
||||||
imgElement.onerror = (): void => {
|
|
||||||
imgElement.src = thumbnail.src;
|
|
||||||
};
|
|
||||||
|
|
||||||
imgElement.src = thumbnail.src;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imgSource = imgElement.getAttribute("data-src");
|
|
||||||
if (!imgSource) return;
|
|
||||||
|
|
||||||
// unsafe nodejs specific, cast to https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#return_value
|
|
||||||
// https://github.com/searxng/searxng/pull/5073#discussion_r2265767231
|
|
||||||
imgTimeoutID = setTimeout(() => {
|
|
||||||
imgElement.src = imgSource;
|
|
||||||
imgElement.removeAttribute("data-src");
|
|
||||||
}, 1000) as unknown as number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const imageThumbnails: NodeListOf<HTMLImageElement> = document.querySelectorAll<HTMLImageElement>(
|
|
||||||
"#urls img.image_thumbnail, img.thumbnail"
|
|
||||||
);
|
|
||||||
for (const thumbnail of imageThumbnails) {
|
|
||||||
if (thumbnail.complete && thumbnail.naturalWidth === 0) {
|
|
||||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbnail.onerror = (): void => {
|
|
||||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyUrlButton: HTMLButtonElement | null =
|
|
||||||
document.querySelector<HTMLButtonElement>("#search_url button#copy_url");
|
|
||||||
copyUrlButton?.style.setProperty("display", "block");
|
|
||||||
|
|
||||||
mutable.selectImage = (resultElement: HTMLElement): void => {
|
|
||||||
// add a class that can be evaluated in the CSS and indicates that the
|
|
||||||
// detail view is open
|
|
||||||
const resultsElement = document.getElementById("results");
|
|
||||||
resultsElement?.classList.add("image-detail-open");
|
|
||||||
|
|
||||||
// add a hash to the browser history so that pressing back doesn't return
|
|
||||||
// to the previous page this allows us to dismiss the image details on
|
|
||||||
// pressing the back button on mobile devices
|
|
||||||
window.location.hash = "#image-viewer";
|
|
||||||
|
|
||||||
mutable.scrollPageToSelected?.();
|
|
||||||
|
|
||||||
// if there is no element given by the caller, stop here
|
|
||||||
if (!resultElement) return;
|
|
||||||
|
|
||||||
imageLoader(resultElement);
|
|
||||||
};
|
|
||||||
|
|
||||||
mutable.closeDetail = (): void => {
|
|
||||||
const resultsElement = document.getElementById("results");
|
|
||||||
resultsElement?.classList.remove("image-detail-open");
|
|
||||||
|
|
||||||
// remove #image-viewer hash from url by navigating back
|
|
||||||
if (window.location.hash === "#image-viewer") {
|
|
||||||
window.history.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
mutable.scrollPageToSelected?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
listen("click", ".btn-collapse", function (this: HTMLElement) {
|
|
||||||
const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
|
|
||||||
const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
|
|
||||||
const target = this.getAttribute("data-target");
|
|
||||||
|
|
||||||
if (!(target && btnLabelCollapsed && btnLabelNotCollapsed)) return;
|
|
||||||
|
|
||||||
const targetElement = document.querySelector<HTMLElement>(target);
|
|
||||||
assertElement(targetElement);
|
|
||||||
|
|
||||||
const isCollapsed = this.classList.contains("collapsed");
|
|
||||||
const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed;
|
|
||||||
const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed;
|
|
||||||
|
|
||||||
this.innerHTML = this.innerHTML.replace(oldLabel, newLabel);
|
|
||||||
this.classList.toggle("collapsed");
|
|
||||||
|
|
||||||
targetElement.classList.toggle("invisible");
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("click", ".media-loader", function (this: HTMLElement) {
|
|
||||||
const target = this.getAttribute("data-target");
|
|
||||||
if (!target) return;
|
|
||||||
|
|
||||||
const iframeLoad = document.querySelector<HTMLIFrameElement>(`${target} > iframe`);
|
|
||||||
assertElement(iframeLoad);
|
|
||||||
|
|
||||||
const srctest = iframeLoad.getAttribute("src");
|
|
||||||
if (!srctest) {
|
|
||||||
const dataSrc = iframeLoad.getAttribute("data-src");
|
|
||||||
if (dataSrc) {
|
|
||||||
iframeLoad.setAttribute("src", dataSrc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("click", "#copy_url", async function (this: HTMLElement) {
|
|
||||||
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
|
|
||||||
assertElement(target);
|
|
||||||
|
|
||||||
if (window.isSecureContext) {
|
|
||||||
await navigator.clipboard.writeText(target.innerText);
|
|
||||||
} else {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection) {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(target);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
document.execCommand("copy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dataset.copiedText) {
|
|
||||||
this.innerText = this.dataset.copiedText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("click", ".result-detail-close", (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
mutable.closeDetail?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("click", ".result-detail-previous", (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
mutable.selectPrevious?.(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("click", ".result-detail-next", (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
mutable.selectNext?.(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// listen for the back button to be pressed and dismiss the image details when called
|
|
||||||
window.addEventListener("hashchange", () => {
|
|
||||||
if (window.location.hash !== "#image-viewer") {
|
|
||||||
mutable.closeDetail?.();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const swipeHorizontal: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".swipe-horizontal");
|
|
||||||
for (const element of swipeHorizontal) {
|
|
||||||
listen("swiped-left", element, () => {
|
|
||||||
mutable.selectNext?.(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("swiped-right", element, () => {
|
|
||||||
mutable.selectPrevious?.(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener(
|
|
||||||
"scroll",
|
|
||||||
() => {
|
|
||||||
const backToTopElement = document.getElementById("backToTop");
|
|
||||||
const resultsElement = document.getElementById("results");
|
|
||||||
|
|
||||||
if (backToTopElement && resultsElement) {
|
|
||||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
|
||||||
const isScrolling = scrollTop >= 100;
|
|
||||||
resultsElement.classList.toggle("scrolling", isScrolling);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import { listen } from "../toolkit.ts";
|
|
||||||
import { getElement } from "../util/getElement.ts";
|
|
||||||
|
|
||||||
const searchForm: HTMLFormElement = getElement<HTMLFormElement>("search");
|
|
||||||
const searchInput: HTMLInputElement = getElement<HTMLInputElement>("q");
|
|
||||||
const searchReset: HTMLButtonElement = getElement<HTMLButtonElement>("clear_search");
|
|
||||||
|
|
||||||
const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches;
|
|
||||||
const isResultsPage: boolean = document.querySelector("main")?.id === "main_results";
|
|
||||||
|
|
||||||
const categoryButtons: HTMLButtonElement[] = Array.from(
|
|
||||||
document.querySelectorAll<HTMLButtonElement>("#categories_container button.category")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (searchInput.value.length === 0) {
|
|
||||||
searchReset.classList.add("empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
// focus search input on large screens
|
|
||||||
if (!(isMobile || isResultsPage)) {
|
|
||||||
searchInput.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// On mobile, move cursor to the end of the input on focus
|
|
||||||
if (isMobile) {
|
|
||||||
listen("focus", searchInput, () => {
|
|
||||||
// Defer cursor move until the next frame to prevent a visual jump
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const end = searchInput.value.length;
|
|
||||||
searchInput.setSelectionRange(end, end);
|
|
||||||
searchInput.scrollLeft = searchInput.scrollWidth;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
listen("input", searchInput, () => {
|
|
||||||
searchReset.classList.toggle("empty", searchInput.value.length === 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("click", searchReset, (event: MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
searchInput.value = "";
|
|
||||||
searchInput.focus();
|
|
||||||
searchReset.classList.add("empty");
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const button of categoryButtons) {
|
|
||||||
listen("click", button, (event: MouseEvent) => {
|
|
||||||
if (event.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
button.classList.toggle("selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// deselect all other categories
|
|
||||||
for (const categoryButton of categoryButtons) {
|
|
||||||
categoryButton.classList.toggle("selected", categoryButton === button);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.querySelector("div.search_filters")) {
|
|
||||||
const safesearchElement = document.getElementById("safesearch");
|
|
||||||
if (safesearchElement) {
|
|
||||||
listen("change", safesearchElement, () => searchForm.submit());
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeRangeElement = document.getElementById("time_range");
|
|
||||||
if (timeRangeElement) {
|
|
||||||
listen("change", timeRangeElement, () => searchForm.submit());
|
|
||||||
}
|
|
||||||
|
|
||||||
const languageElement = document.getElementById("language");
|
|
||||||
if (languageElement) {
|
|
||||||
listen("change", languageElement, () => searchForm.submit());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// override searchForm submit event
|
|
||||||
listen("submit", searchForm, (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (categoryButtons.length > 0) {
|
|
||||||
const searchCategories = getElement<HTMLInputElement>("selected-categories");
|
|
||||||
searchCategories.value = categoryButtons
|
|
||||||
.filter((button) => button.classList.contains("selected"))
|
|
||||||
.map((button) => button.name.replace("category_", ""))
|
|
||||||
.join(",");
|
|
||||||
}
|
|
||||||
|
|
||||||
searchForm.submit();
|
|
||||||
});
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import {
|
|
||||||
absDependencies,
|
|
||||||
addDependencies,
|
|
||||||
create,
|
|
||||||
divideDependencies,
|
|
||||||
eDependencies,
|
|
||||||
evaluateDependencies,
|
|
||||||
expDependencies,
|
|
||||||
factorialDependencies,
|
|
||||||
gcdDependencies,
|
|
||||||
lcmDependencies,
|
|
||||||
log1pDependencies,
|
|
||||||
log2Dependencies,
|
|
||||||
log10Dependencies,
|
|
||||||
logDependencies,
|
|
||||||
modDependencies,
|
|
||||||
multiplyDependencies,
|
|
||||||
nthRootDependencies,
|
|
||||||
piDependencies,
|
|
||||||
powDependencies,
|
|
||||||
roundDependencies,
|
|
||||||
signDependencies,
|
|
||||||
sqrtDependencies,
|
|
||||||
subtractDependencies
|
|
||||||
} from "mathjs/number";
|
|
||||||
import { Plugin } from "../Plugin.ts";
|
|
||||||
import { appendAnswerElement } from "../util/appendAnswerElement.ts";
|
|
||||||
import { getElement } from "../util/getElement.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and solves mathematical expressions. Can do basic arithmetic and
|
|
||||||
* evaluate some functions.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* "(3 + 5) / 2" = "4"
|
|
||||||
* "e ^ 2 + pi" = "10.530648752520442"
|
|
||||||
* "gcd(48, 18) + lcm(4, 5)" = "26"
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* Depends on `mathjs` library.
|
|
||||||
*/
|
|
||||||
export default class Calculator extends Plugin {
|
|
||||||
public constructor() {
|
|
||||||
super("calculator");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @remarks
|
|
||||||
* Compare bundle size after adding or removing features.
|
|
||||||
*/
|
|
||||||
private static readonly math = create({
|
|
||||||
...absDependencies,
|
|
||||||
...addDependencies,
|
|
||||||
...divideDependencies,
|
|
||||||
...eDependencies,
|
|
||||||
...evaluateDependencies,
|
|
||||||
...expDependencies,
|
|
||||||
...factorialDependencies,
|
|
||||||
...gcdDependencies,
|
|
||||||
...lcmDependencies,
|
|
||||||
...log10Dependencies,
|
|
||||||
...log1pDependencies,
|
|
||||||
...log2Dependencies,
|
|
||||||
...logDependencies,
|
|
||||||
...modDependencies,
|
|
||||||
...multiplyDependencies,
|
|
||||||
...nthRootDependencies,
|
|
||||||
...piDependencies,
|
|
||||||
...powDependencies,
|
|
||||||
...roundDependencies,
|
|
||||||
...signDependencies,
|
|
||||||
...sqrtDependencies,
|
|
||||||
...subtractDependencies
|
|
||||||
});
|
|
||||||
|
|
||||||
protected async run(): Promise<string | undefined> {
|
|
||||||
const searchInput = getElement<HTMLInputElement>("q");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const node = Calculator.math.parse(searchInput.value);
|
|
||||||
return `${node.toString()} = ${node.evaluate()}`;
|
|
||||||
} catch {
|
|
||||||
// not a compatible math expression
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async post(result: string): Promise<void> {
|
|
||||||
appendAnswerElement(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import { Plugin } from "../Plugin.ts";
|
|
||||||
import { http, settings } from "../toolkit.ts";
|
|
||||||
import { assertElement } from "../util/assertElement.ts";
|
|
||||||
import { getElement } from "../util/getElement.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically loads the next page when scrolling to bottom of the current page.
|
|
||||||
*/
|
|
||||||
export default class InfiniteScroll extends Plugin {
|
|
||||||
public constructor() {
|
|
||||||
super("infiniteScroll");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async run(): Promise<void> {
|
|
||||||
const resultsElement = getElement<HTMLElement>("results");
|
|
||||||
|
|
||||||
const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
|
|
||||||
const observedSelector = "article.result:last-child";
|
|
||||||
|
|
||||||
const spinnerElement = document.createElement("div");
|
|
||||||
spinnerElement.className = "loader";
|
|
||||||
|
|
||||||
const loadNextPage = async (callback: () => void): Promise<void> => {
|
|
||||||
const searchForm = document.querySelector<HTMLFormElement>("#search");
|
|
||||||
assertElement(searchForm);
|
|
||||||
|
|
||||||
const form = document.querySelector<HTMLFormElement>("#pagination form.next_page");
|
|
||||||
assertElement(form);
|
|
||||||
|
|
||||||
const action = searchForm.getAttribute("action");
|
|
||||||
if (!action) {
|
|
||||||
throw new Error("Form action not defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
const paginationElement = document.querySelector<HTMLElement>("#pagination");
|
|
||||||
assertElement(paginationElement);
|
|
||||||
|
|
||||||
paginationElement.replaceChildren(spinnerElement);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await http("POST", action, { body: new FormData(form) });
|
|
||||||
const nextPage = await res.text();
|
|
||||||
if (!nextPage) return;
|
|
||||||
|
|
||||||
const nextPageDoc = new DOMParser().parseFromString(nextPage, "text/html");
|
|
||||||
const articleList = nextPageDoc.querySelectorAll<HTMLElement>("#urls article");
|
|
||||||
const nextPaginationElement = nextPageDoc.querySelector<HTMLElement>("#pagination");
|
|
||||||
|
|
||||||
document.querySelector("#pagination")?.remove();
|
|
||||||
|
|
||||||
const urlsElement = document.querySelector<HTMLElement>("#urls");
|
|
||||||
if (!urlsElement) {
|
|
||||||
throw new Error("URLs element not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (articleList.length > 0 && !onlyImages) {
|
|
||||||
// do not add <hr> element when there are only images
|
|
||||||
urlsElement.appendChild(document.createElement("hr"));
|
|
||||||
}
|
|
||||||
|
|
||||||
urlsElement.append(...articleList);
|
|
||||||
|
|
||||||
if (nextPaginationElement) {
|
|
||||||
const results = document.querySelector<HTMLElement>("#results");
|
|
||||||
results?.appendChild(nextPaginationElement);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading next page:", error);
|
|
||||||
|
|
||||||
const errorElement = Object.assign(document.createElement("div"), {
|
|
||||||
textContent: settings.translations?.error_loading_next_page ?? "Error loading next page",
|
|
||||||
className: "dialog-error"
|
|
||||||
});
|
|
||||||
errorElement.setAttribute("role", "alert");
|
|
||||||
document.querySelector("#pagination")?.replaceChildren(errorElement);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const intersectionObserveOptions: IntersectionObserverInit = {
|
|
||||||
rootMargin: "320px"
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer: IntersectionObserver = new IntersectionObserver(async (entries: IntersectionObserverEntry[]) => {
|
|
||||||
const [paginationEntry] = entries;
|
|
||||||
|
|
||||||
if (paginationEntry?.isIntersecting) {
|
|
||||||
observer.unobserve(paginationEntry.target);
|
|
||||||
|
|
||||||
await loadNextPage(() => {
|
|
||||||
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
|
|
||||||
if (nextObservedElement) {
|
|
||||||
observer.observe(nextObservedElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, intersectionObserveOptions);
|
|
||||||
|
|
||||||
const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
|
|
||||||
if (initialObservedElement) {
|
|
||||||
observer.observe(initialObservedElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async post(): Promise<void> {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import "ol/ol.css";
|
|
||||||
import { Feature, Map as OlMap, View } from "ol";
|
|
||||||
import { GeoJSON } from "ol/format";
|
|
||||||
import { Point } from "ol/geom";
|
|
||||||
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
|
|
||||||
import { fromLonLat } from "ol/proj";
|
|
||||||
import { OSM, Vector as VectorSource } from "ol/source";
|
|
||||||
import { Circle, Fill, Stroke, Style } from "ol/style";
|
|
||||||
import { Plugin } from "../Plugin.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MapView
|
|
||||||
*/
|
|
||||||
export default class MapView extends Plugin {
|
|
||||||
private readonly map: HTMLElement;
|
|
||||||
|
|
||||||
public constructor(map: HTMLElement) {
|
|
||||||
super("mapView");
|
|
||||||
|
|
||||||
this.map = map;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async run(): Promise<void> {
|
|
||||||
const { leafletTarget: target, mapLon = "0", mapLat = "0", mapGeojson } = this.map.dataset;
|
|
||||||
|
|
||||||
const lon = Number.parseFloat(mapLon);
|
|
||||||
const lat = Number.parseFloat(mapLat);
|
|
||||||
const view = new View({ maxZoom: 16, enableRotation: false });
|
|
||||||
const map = new OlMap({
|
|
||||||
target: target,
|
|
||||||
layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],
|
|
||||||
view: view
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const markerSource = new VectorSource({
|
|
||||||
features: [
|
|
||||||
new Feature({
|
|
||||||
geometry: new Point(fromLonLat([lon, lat]))
|
|
||||||
})
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const markerLayer = new VectorLayer({
|
|
||||||
source: markerSource,
|
|
||||||
style: new Style({
|
|
||||||
image: new Circle({
|
|
||||||
radius: 6,
|
|
||||||
fill: new Fill({ color: "#3050ff" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addLayer(markerLayer);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to create marker layer:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapGeojson) {
|
|
||||||
try {
|
|
||||||
const geoSource = new VectorSource({
|
|
||||||
features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {
|
|
||||||
dataProjection: "EPSG:4326",
|
|
||||||
featureProjection: "EPSG:3857"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const geoLayer = new VectorLayer({
|
|
||||||
source: geoSource,
|
|
||||||
style: new Style({
|
|
||||||
stroke: new Stroke({ color: "#3050ff", width: 2 }),
|
|
||||||
fill: new Fill({ color: "#3050ff33" })
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addLayer(geoLayer);
|
|
||||||
|
|
||||||
const geoSourceExtent = geoSource.getExtent();
|
|
||||||
if (geoSourceExtent) {
|
|
||||||
view.fit(geoSourceExtent, { padding: [20, 20, 20, 20] });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to create GeoJSON layer:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async post(): Promise<void> {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import { load } from "./loader.ts";
|
|
||||||
import { Endpoints, endpoint, listen, ready, settings } from "./toolkit.ts";
|
|
||||||
|
|
||||||
ready(() => {
|
|
||||||
document.documentElement.classList.remove("no-js");
|
|
||||||
document.documentElement.classList.add("js");
|
|
||||||
|
|
||||||
listen("click", ".close", function (this: HTMLElement) {
|
|
||||||
(this.parentNode as HTMLElement)?.classList.add("invisible");
|
|
||||||
});
|
|
||||||
|
|
||||||
listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.classList.remove("searxng_init_map");
|
|
||||||
|
|
||||||
load(() => import("./plugin/MapView.ts").then(({ default: Plugin }) => new Plugin(this)), {
|
|
||||||
on: "endpoint",
|
|
||||||
where: [Endpoints.results]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (settings.plugins?.includes("infiniteScroll")) {
|
|
||||||
load(() => import("./plugin/InfiniteScroll.ts").then(({ default: Plugin }) => new Plugin()), {
|
|
||||||
on: "endpoint",
|
|
||||||
where: [Endpoints.results]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.plugins?.includes("calculator")) {
|
|
||||||
load(() => import("./plugin/Calculator.ts").then(({ default: Plugin }) => new Plugin()), {
|
|
||||||
on: "endpoint",
|
|
||||||
where: [Endpoints.results]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ready(
|
|
||||||
() => {
|
|
||||||
void import("./main/keyboard.ts");
|
|
||||||
void import("./main/search.ts");
|
|
||||||
|
|
||||||
if (settings.autocomplete) {
|
|
||||||
void import("./main/autocomplete.ts");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ on: [endpoint === Endpoints.index] }
|
|
||||||
);
|
|
||||||
|
|
||||||
ready(
|
|
||||||
() => {
|
|
||||||
void import("./main/keyboard.ts");
|
|
||||||
void import("./main/results.ts");
|
|
||||||
void import("./main/search.ts");
|
|
||||||
|
|
||||||
if (settings.autocomplete) {
|
|
||||||
void import("./main/autocomplete.ts");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ on: [endpoint === Endpoints.results] }
|
|
||||||
);
|
|
||||||
|
|
||||||
ready(
|
|
||||||
() => {
|
|
||||||
void import("./main/preferences.ts");
|
|
||||||
},
|
|
||||||
{ on: [endpoint === Endpoints.preferences] }
|
|
||||||
);
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import type { KeyBindingLayout } from "./main/keyboard.ts";
|
|
||||||
|
|
||||||
// synced with searx/webapp.py get_client_settings
|
|
||||||
type Settings = {
|
|
||||||
plugins?: string[];
|
|
||||||
autocomplete?: string;
|
|
||||||
autocomplete_min?: number;
|
|
||||||
doi_resolver?: string;
|
|
||||||
favicon_resolver?: string;
|
|
||||||
hotkeys?: KeyBindingLayout;
|
|
||||||
method?: "GET" | "POST";
|
|
||||||
query_in_title?: boolean;
|
|
||||||
results_on_new_tab?: boolean;
|
|
||||||
safesearch?: 0 | 1 | 2;
|
|
||||||
search_on_category_select?: boolean;
|
|
||||||
theme?: string;
|
|
||||||
theme_static_path?: string;
|
|
||||||
translations?: Record<string, string>;
|
|
||||||
url_formatting?: "pretty" | "full" | "host";
|
|
||||||
};
|
|
||||||
|
|
||||||
type HTTPOptions = {
|
|
||||||
body?: BodyInit;
|
|
||||||
timeout?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ReadyOptions = {
|
|
||||||
// all values must be truthy for the callback to be executed
|
|
||||||
on?: (boolean | undefined)[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EndpointsKeys = keyof typeof Endpoints;
|
|
||||||
|
|
||||||
export const Endpoints = {
|
|
||||||
index: "index",
|
|
||||||
results: "results",
|
|
||||||
preferences: "preferences",
|
|
||||||
unknown: "unknown"
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const mutable = {
|
|
||||||
closeDetail: undefined as (() => void) | undefined,
|
|
||||||
scrollPageToSelected: undefined as (() => void) | undefined,
|
|
||||||
selectImage: undefined as ((resultElement: HTMLElement) => void) | undefined,
|
|
||||||
selectNext: undefined as ((openDetailView?: boolean) => void) | undefined,
|
|
||||||
selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEndpoint = (): EndpointsKeys => {
|
|
||||||
const metaEndpoint = document.querySelector('meta[name="endpoint"]')?.getAttribute("content");
|
|
||||||
|
|
||||||
if (metaEndpoint && metaEndpoint in Endpoints) {
|
|
||||||
return metaEndpoint as EndpointsKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Endpoints.unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSettings = (): Settings => {
|
|
||||||
const settings = document.querySelector("script[client_settings]")?.getAttribute("client_settings");
|
|
||||||
if (!settings) return {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(atob(settings));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load client_settings:", error);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000);
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
body: options?.body,
|
|
||||||
method: method,
|
|
||||||
signal: controller.signal
|
|
||||||
}).finally(() => clearTimeout(timeoutId));
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(res.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listen = <K extends keyof DocumentEventMap, E extends HTMLElement>(
|
|
||||||
type: string | K,
|
|
||||||
target: string | Document | E,
|
|
||||||
listener: (this: E, event: DocumentEventMap[K]) => void | Promise<void>,
|
|
||||||
options?: AddEventListenerOptions
|
|
||||||
): void => {
|
|
||||||
if (typeof target !== "string") {
|
|
||||||
target.addEventListener(type, listener as EventListener, options);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener(
|
|
||||||
type,
|
|
||||||
(event: Event) => {
|
|
||||||
for (const node of event.composedPath()) {
|
|
||||||
if (node instanceof HTMLElement && node.matches(target)) {
|
|
||||||
try {
|
|
||||||
listener.call(node as E, event as DocumentEventMap[K]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
options
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ready = (callback: () => void, options?: ReadyOptions): void => {
|
|
||||||
for (const condition of options?.on ?? []) {
|
|
||||||
if (!condition) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
listen("DOMContentLoaded", document, callback, { once: true });
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const endpoint: EndpointsKeys = getEndpoint();
|
|
||||||
export const settings: Settings = getSettings();
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import { getElement } from "./getElement.ts";
|
|
||||||
|
|
||||||
export const appendAnswerElement = (element: HTMLElement | string | number): void => {
|
|
||||||
const results = getElement<HTMLDivElement>("results");
|
|
||||||
|
|
||||||
// ./searx/templates/elements/answers.html
|
|
||||||
let answers = getElement<HTMLDivElement>("answers", { assert: false });
|
|
||||||
if (!answers) {
|
|
||||||
// what is this?
|
|
||||||
const answersTitle = document.createElement("h4");
|
|
||||||
answersTitle.setAttribute("class", "title");
|
|
||||||
answersTitle.setAttribute("id", "answers-title");
|
|
||||||
answersTitle.textContent = "Answers : ";
|
|
||||||
|
|
||||||
answers = document.createElement("div");
|
|
||||||
answers.setAttribute("id", "answers");
|
|
||||||
answers.setAttribute("role", "complementary");
|
|
||||||
answers.setAttribute("aria-labelledby", "answers-title");
|
|
||||||
answers.appendChild(answersTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(element instanceof HTMLElement)) {
|
|
||||||
const span = document.createElement("span");
|
|
||||||
span.innerHTML = element.toString();
|
|
||||||
// biome-ignore lint/style/noParameterAssign: TODO
|
|
||||||
element = span;
|
|
||||||
}
|
|
||||||
|
|
||||||
answers.appendChild(element);
|
|
||||||
|
|
||||||
results.insertAdjacentElement("afterbegin", answers);
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
type AssertElement = <T>(element?: T | null) => asserts element is T;
|
|
||||||
export const assertElement: AssertElement = <T>(element?: T | null): asserts element is T => {
|
|
||||||
if (!element) {
|
|
||||||
throw new Error("DOM element not found");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import { assertElement } from "./assertElement.ts";
|
|
||||||
|
|
||||||
type Options = {
|
|
||||||
assert?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getElement<T>(id: string, options?: { assert: true }): T;
|
|
||||||
export function getElement<T>(id: string, options?: { assert: false }): T | null;
|
|
||||||
export function getElement<T>(id: string, options: Options = {}): T | null {
|
|
||||||
options.assert ??= true;
|
|
||||||
|
|
||||||
const element = document.getElementById(id) as T | null;
|
|
||||||
|
|
||||||
if (options.assert) {
|
|
||||||
assertElement(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
/*
|
|
||||||
Layout of the Files result class
|
|
||||||
*/
|
|
||||||
|
|
||||||
#main_results .result-file {
|
|
||||||
border: 1px solid var(--color-result-border);
|
|
||||||
margin: 0 @results-tablet-offset 1rem @results-tablet-offset !important;
|
|
||||||
.rounded-corners;
|
|
||||||
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
padding: 10px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 0 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
/*
|
|
||||||
Layout of the KeyValue result class
|
|
||||||
*/
|
|
||||||
|
|
||||||
#main_results .result-keyvalue {
|
|
||||||
caption {
|
|
||||||
padding: 0.8rem 0.5rem;
|
|
||||||
font-style: italic;
|
|
||||||
caption-side: bottom;
|
|
||||||
background-color: var(--color-result-keyvalue-table);
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-key {
|
|
||||||
width: 25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
word-break: break-all;
|
|
||||||
table-layout: fixed;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--color-result-keyvalue-table);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.odd {
|
|
||||||
background-color: var(--color-result-keyvalue-odd);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.even {
|
|
||||||
background-color: var(--color-result-keyvalue-even);
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
padding: 0.3rem 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
/*
|
|
||||||
Layout of the Paper result class
|
|
||||||
*/
|
|
||||||
|
|
||||||
.result-paper {
|
|
||||||
.attributes {
|
|
||||||
display: table;
|
|
||||||
border-spacing: 0.125rem;
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: table-row;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
display: table-cell;
|
|
||||||
|
|
||||||
time {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span:first-child {
|
|
||||||
color: var(--color-base-font);
|
|
||||||
min-width: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
span:nth-child(2) {
|
|
||||||
color: var(--color-result-publishdate-font);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin: 0.25rem 0 0 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
line-height: 1.24;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: @phone) {
|
|
||||||
.result-paper {
|
|
||||||
.attributes {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
span:first-child {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
span:nth-child(2) {
|
|
||||||
.ltr-margin-left(0.5rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
#answers .weather {
|
|
||||||
summary {
|
|
||||||
display: block;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.summary {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background-color: var(--color-header-background);
|
|
||||||
.rounded-corners-tiny;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
table-layout: fixed;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0.1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.symbol {
|
|
||||||
width: 5rem;
|
|
||||||
margin: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
// background-color: var(--color-result-keyvalue-even);
|
|
||||||
}
|
|
||||||
|
|
||||||
.measured {
|
|
||||||
// background-color: var(--color-result-keyvalue-odd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<path fill="#58f" d="M11 20.85a.92.92 0 0 1-1.1.93A10 10 0 0 1 2.06 13c-.06-.55.4-1 .95-1h3a1 1 0 0 1 1 1 3 3 0 0 0 3 3 1 1 0 0 1 1 1v3.85Zm6-1.92c0 .77.83 1.23 1.42.74a10 10 0 0 0 2.03-2.32c.39-.61-.09-1.35-.81-1.35H18a1 1 0 0 0-1 1v1.93ZM12 2a10 10 0 0 1 6.65 2.53c.61.55.17 1.47-.65 1.47h-.15A2.85 2.85 0 0 0 15 8.85c0 .33-.18.62-.47.77l-.08.04a1 1 0 0 1-.9 0l-.08-.04a.85.85 0 0 1-.47-.77A2.85 2.85 0 0 0 10.15 6H10a1 1 0 0 1-1-1V3.2c0-.44.28-.84.7-.94C10.45 2.1 11.22 2 12 2Z"/>
|
|
||||||
<path fill="#58f" d="M3.42 10c-.63 0-1.1-.58-.9-1.18.6-1.8 1.7-3.36 3.12-4.53C6.2 3.82 7 4.26 7 5a3 3 0 0 0 3 3h.15c.47 0 .85.38.85.85 0 1.09.61 2.07 1.58 2.56l.08.04a3 3 0 0 0 2.68 0l.08-.04A2.85 2.85 0 0 0 17 8.85c0-.47.38-.85.85-.85h2.66c.4 0 .77.23.9.6a9.98 9.98 0 0 1 .52 4.6.94.94 0 0 1-.95.8H18a3 3 0 0 0-3 3v3.8c0 .44-.28.84-.7.94l-.2.04a.92.92 0 0 1-1.1-.93V17a3 3 0 0 0-3-3 1 1 0 0 1-1-1 3 3 0 0 0-3-3H3.42Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 989 B |
@@ -1,6 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<path d="M248 64C146.39 64 64 146.39 64 248s82.39 184 184 184 184-82.39 184-184S349.61 64 248 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/>
|
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M220 220h32v116"/>
|
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M208 340h88"/>
|
|
||||||
<path d="M248 130a26 26 0 1026 26 26 26 0 00-26-26z" fill="currentColor" stroke="currentColor" stroke-miterlimit="10" stroke-width="1"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 648 B |
@@ -1,6 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<path d="M368 415.86V72a24.07 24.07 0 00-24-24H72a24.07 24.07 0 00-24 24v352a40.12 40.12 0 0040 40h328" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
|
|
||||||
<path d="M416 464h0a48 48 0 01-48-48V128h72a24 24 0 0124 24v264a48 48 0 01-48 48z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
|
|
||||||
<path d="M240 128h64M240 192h64M112 256h192M112 320h192M112 384h192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/>
|
|
||||||
<path d="M176 208h-64a16 16 0 01-16-16v-64a16 16 0 0116-16h64a16 16 0 0116 16v64a16 16 0 01-16 16z" fill="currentColor" stroke="currentColor" stroke-linejoin="round" stroke-width="1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 787 B |
@@ -1,84 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate icons.html for the jinja templates of the simple theme.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
import { argv } from "node:process";
|
|
||||||
import type { Config as SvgoConfig } from "svgo";
|
|
||||||
import { type IconSet, type JinjaMacro, jinja_svg_sets } from "./tools/jinja_svg_catalog.ts";
|
|
||||||
|
|
||||||
const HERE = `${dirname(argv[1] || "")}/`;
|
|
||||||
const dest: string = resolve(HERE, "../../searx/templates/simple/icons.html");
|
|
||||||
|
|
||||||
const searxng_jinja_macros: JinjaMacro[] = [
|
|
||||||
{ name: "icon", class: "sxng-icon-set" },
|
|
||||||
{ name: "icon_small", class: "sxng-icon-set-small" },
|
|
||||||
{ name: "icon_big", class: "sxng-icon-set-big" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const sxng_icon_opts: SvgoConfig = {
|
|
||||||
multipass: true,
|
|
||||||
plugins: [
|
|
||||||
"removeTitle",
|
|
||||||
"removeXMLNS",
|
|
||||||
{
|
|
||||||
name: "addAttributesToSVGElement",
|
|
||||||
params: {
|
|
||||||
attributes: [{ "aria-hidden": "true" }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const simple_icons: IconSet[] = [
|
|
||||||
{
|
|
||||||
base: resolve(HERE, "node_modules/ionicons/dist/svg"),
|
|
||||||
set: {
|
|
||||||
alert: "alert-outline.svg",
|
|
||||||
appstore: "apps-outline.svg",
|
|
||||||
book: "book-outline.svg",
|
|
||||||
close: "close-outline.svg",
|
|
||||||
download: "download-outline.svg",
|
|
||||||
"ellipsis-vertical": "ellipsis-vertical-outline.svg",
|
|
||||||
"file-tray-full": "file-tray-full-outline.svg",
|
|
||||||
film: "film-outline.svg",
|
|
||||||
globe: "globe-outline.svg",
|
|
||||||
heart: "heart-outline.svg",
|
|
||||||
image: "image-outline.svg",
|
|
||||||
layers: "layers-outline.svg",
|
|
||||||
leecher: "arrow-down.svg",
|
|
||||||
location: "location-outline.svg",
|
|
||||||
magnet: "magnet-outline.svg",
|
|
||||||
"musical-notes": "musical-notes-outline.svg",
|
|
||||||
"navigate-down": "chevron-down-outline.svg",
|
|
||||||
"navigate-left": "chevron-back-outline.svg",
|
|
||||||
"navigate-right": "chevron-forward-outline.svg",
|
|
||||||
"navigate-up": "chevron-up-outline.svg",
|
|
||||||
people: "people-outline.svg",
|
|
||||||
play: "play-outline.svg",
|
|
||||||
radio: "radio-outline.svg",
|
|
||||||
save: "save-outline.svg",
|
|
||||||
school: "school-outline.svg",
|
|
||||||
search: "search-outline.svg",
|
|
||||||
seeder: "swap-vertical.svg",
|
|
||||||
settings: "settings-outline.svg",
|
|
||||||
tv: "tv-outline.svg"
|
|
||||||
},
|
|
||||||
svgo_opts: sxng_icon_opts
|
|
||||||
},
|
|
||||||
// some of the ionicons are not suitable for a dark theme, we fixed the svg
|
|
||||||
// manually in src/svg/ionicons
|
|
||||||
// - https://github.com/searxng/searxng/pull/4284#issuecomment-2680550342
|
|
||||||
{
|
|
||||||
base: resolve(HERE, "src/svg/ionicons"),
|
|
||||||
set: {
|
|
||||||
"information-circle": "information-circle-outline.svg",
|
|
||||||
newspaper: "newspaper-outline.svg"
|
|
||||||
},
|
|
||||||
svgo_opts: sxng_icon_opts
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
jinja_svg_sets(dest, searxng_jinja_macros, simple_icons);
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import sharp from "sharp";
|
|
||||||
import type { Config } from "svgo";
|
|
||||||
import { optimize as svgo } from "svgo";
|
|
||||||
|
|
||||||
// Mapping of src to dest
|
|
||||||
export type Src2Dest = {
|
|
||||||
// Name of the source file.
|
|
||||||
src: string;
|
|
||||||
// Name of the destination file.
|
|
||||||
dest: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a list of SVG files to PNG.
|
|
||||||
*
|
|
||||||
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
|
||||||
* @param width - (optional) width of the PNG pictures
|
|
||||||
* @param height - (optional) height of the PNG pictures.
|
|
||||||
*/
|
|
||||||
export const svg2png = (items: Src2Dest[], width?: number, height?: number): void => {
|
|
||||||
for (const item of items) {
|
|
||||||
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
|
|
||||||
|
|
||||||
sharp(item.src)
|
|
||||||
.png({
|
|
||||||
force: true,
|
|
||||||
compressionLevel: 9,
|
|
||||||
palette: true
|
|
||||||
})
|
|
||||||
.resize(width, height, {
|
|
||||||
fit: "contain"
|
|
||||||
})
|
|
||||||
.toFile(item.dest)
|
|
||||||
.then((info) => {
|
|
||||||
console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(`ERROR: ${item.dest} -- ${error}`);
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optimize SVG images for WEB.
|
|
||||||
*
|
|
||||||
* @param items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
|
||||||
* @param svgo_opts - Options passed to svgo.
|
|
||||||
*/
|
|
||||||
export const svg2svg = (items: Src2Dest[], svgo_opts: Config): void => {
|
|
||||||
for (const item of items) {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
|
|
||||||
|
|
||||||
const raw = fs.readFileSync(item.src, "utf8");
|
|
||||||
const opt = svgo(raw, svgo_opts);
|
|
||||||
|
|
||||||
fs.writeFileSync(item.dest, opt.data);
|
|
||||||
console.log(`[svg2svg] optimized: ${item.dest} -- src: ${item.src}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`ERROR: optimize src: ${item.src} -- ${error}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{{--
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
This is a EDGE https://edgejs.dev/ template to generate a HTML Jinja template
|
|
||||||
for the backend. Example output of this EDGE template:
|
|
||||||
- https://github.com/searxng/searxng/blob/master/searx/templates/simple/icons.html
|
|
||||||
--}}
|
|
||||||
{#
|
|
||||||
Catalog of SVG symbols that can be inserted into the HTML output of a Jinja
|
|
||||||
template. This file from:
|
|
||||||
|
|
||||||
client/simple/tools/icon_catalog.edge.html
|
|
||||||
#}
|
|
||||||
|
|
||||||
{%-
|
|
||||||
set catalog = {
|
|
||||||
@each((svg, name) in svg_catalog)
|
|
||||||
'{{{name}}}' : '{{{svg}}}',
|
|
||||||
@end
|
|
||||||
}
|
|
||||||
-%}
|
|
||||||
|
|
||||||
@each(macro in macros)
|
|
||||||
|
|
||||||
{% macro {{ macro.name }}(action, alt) -%}
|
|
||||||
{{ open_curly_brace }} catalog[action] | replace("{{__jinja_class_placeholder__}}", "{{ macro.class }}") | safe {{ close_curly_brace }}
|
|
||||||
{%- endmacro %}
|
|
||||||
@end
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import fs from "node:fs";
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
import { Edge } from "edge.js";
|
|
||||||
import { type Config as SvgoConfig, optimize as svgo } from "svgo";
|
|
||||||
|
|
||||||
const __jinja_class_placeholder__ = "__jinja_class_placeholder__";
|
|
||||||
|
|
||||||
// A set of icons
|
|
||||||
export type IconSet = {
|
|
||||||
// Object of SVG icons, where property name is the name of the icon and value is the src of the SVG (relative to base)
|
|
||||||
set: Record<string, string>;
|
|
||||||
// Folder in which the SVG src files are located
|
|
||||||
base: string;
|
|
||||||
// svgo options for this set
|
|
||||||
svgo_opts: SvgoConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mapping of icon name to SVG source file
|
|
||||||
type IconSVG = {
|
|
||||||
// Name of the icon isource file
|
|
||||||
name: string;
|
|
||||||
// Name of the destination file
|
|
||||||
src: string;
|
|
||||||
// Options passed to svgo
|
|
||||||
svgo_opts: SvgoConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Arguments to create a jinja macro
|
|
||||||
export type JinjaMacro = {
|
|
||||||
// Name of the jinja macro
|
|
||||||
name: string;
|
|
||||||
// SVG's class name (value of XML class attribute)
|
|
||||||
class: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a jinja template with a catalog of SVG icons that can be
|
|
||||||
* used in other HTML jinja templates.
|
|
||||||
*
|
|
||||||
* @param dest - filename of the generate jinja template.
|
|
||||||
* @param macros - Jinja macros to create.
|
|
||||||
* @param items - Array of SVG items.
|
|
||||||
*/
|
|
||||||
export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: IconSVG[]): void => {
|
|
||||||
const svg_catalog: Record<string, string> = {};
|
|
||||||
const edge_template = resolve(import.meta.dirname, "jinja_svg_catalog.html.edge");
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
// JSON.stringify & JSON.parse are used to create a deep copy of the item.svgo_opts object
|
|
||||||
const svgo_opts: SvgoConfig = JSON.parse(JSON.stringify(item.svgo_opts));
|
|
||||||
|
|
||||||
svgo_opts.plugins?.push({
|
|
||||||
name: "addClassesToSVGElement",
|
|
||||||
params: {
|
|
||||||
classNames: [__jinja_class_placeholder__]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = fs.readFileSync(item.src, "utf8");
|
|
||||||
const opt = svgo(raw, svgo_opts);
|
|
||||||
|
|
||||||
svg_catalog[item.name] = opt.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${error}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.mkdirSync(dirname(dest), { recursive: true });
|
|
||||||
|
|
||||||
const ctx = {
|
|
||||||
svg_catalog: svg_catalog,
|
|
||||||
macros: macros,
|
|
||||||
edge_template: edge_template,
|
|
||||||
__jinja_class_placeholder__: __jinja_class_placeholder__,
|
|
||||||
// see https://github.com/edge-js/edge/issues/162
|
|
||||||
open_curly_brace: "{{",
|
|
||||||
close_curly_brace: "}}"
|
|
||||||
};
|
|
||||||
|
|
||||||
const jinjatmpl = Edge.create().renderRawSync(fs.readFileSync(edge_template, "utf-8"), ctx);
|
|
||||||
|
|
||||||
fs.writeFileSync(dest, jinjatmpl);
|
|
||||||
console.log(`[jinja_svg_catalog] created: ${dest}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls jinja_svg_catalog for a collection of icon sets where each set has its
|
|
||||||
* own parameters.
|
|
||||||
*
|
|
||||||
* @param dest - filename of the generate jinja template.
|
|
||||||
* @param macros - Jinja macros to create.
|
|
||||||
* @param sets - Array of SVG sets.
|
|
||||||
*/
|
|
||||||
export const jinja_svg_sets = (dest: string, macros: JinjaMacro[], sets: IconSet[]): void => {
|
|
||||||
const items: IconSVG[] = [];
|
|
||||||
const all: string[] = [];
|
|
||||||
|
|
||||||
for (const obj of sets) {
|
|
||||||
for (const [name, file] of Object.entries(obj.set)) {
|
|
||||||
if (all.includes(name)) {
|
|
||||||
throw new Error(`ERROR: ${name} has already been defined`);
|
|
||||||
}
|
|
||||||
|
|
||||||
all.push(name);
|
|
||||||
items.push({
|
|
||||||
name: name,
|
|
||||||
src: resolve(obj.base, file),
|
|
||||||
svgo_opts: obj.svgo_opts
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jinja_svg_catalog(dest, macros, items);
|
|
||||||
};
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom vite plugins to build the web-client components of the simple theme.
|
|
||||||
*
|
|
||||||
* HINT:
|
|
||||||
* This is an initial implementation for the migration of the build process
|
|
||||||
* from grunt to vite. For fully support (vite: build & serve) more work is
|
|
||||||
* needed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Config } from "svgo";
|
|
||||||
import type { Plugin } from "vite";
|
|
||||||
import { type Src2Dest, svg2png, svg2svg } from "./img.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vite plugin to convert a list of SVG files to PNG.
|
|
||||||
*
|
|
||||||
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
|
||||||
* @param width - (optional) width of the PNG picture
|
|
||||||
* @param height - (optional) height of the PNG picture
|
|
||||||
*/
|
|
||||||
export const plg_svg2png = (items: Src2Dest[], width?: number, height?: number): Plugin => {
|
|
||||||
return {
|
|
||||||
name: "searxng-simple-svg2png",
|
|
||||||
apply: "build",
|
|
||||||
writeBundle: () => {
|
|
||||||
svg2png(items, width, height);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vite plugin to optimize SVG images for WEB.
|
|
||||||
*
|
|
||||||
* @param items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
|
||||||
* @param svgo_opts - Options passed to svgo.
|
|
||||||
*/
|
|
||||||
export const plg_svg2svg = (items: Src2Dest[], svgo_opts: Config): Plugin => {
|
|
||||||
return {
|
|
||||||
name: "searxng-simple-svg2svg",
|
|
||||||
apply: "build",
|
|
||||||
writeBundle: () => {
|
|
||||||
svg2svg(items, svgo_opts);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"target": "ES2022",
|
|
||||||
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"incremental": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
"strict": true,
|
|
||||||
"allowUnreachableCode": false,
|
|
||||||
"allowUnusedLabels": false,
|
|
||||||
"exactOptionalPropertyTypes": false,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noUncheckedSideEffectImports": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
|
||||||
"types": ["vite/client"]
|
|
||||||
},
|
|
||||||
"include": ["./"],
|
|
||||||
"exclude": ["./node_modules/"]
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CONFIG: https://vite.dev/config/
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { resolve } from "node:path";
|
|
||||||
import { constants as zlibConstants } from "node:zlib";
|
|
||||||
import browserslistToEsbuild from "browserslist-to-esbuild";
|
|
||||||
import { browserslistToTargets } from "lightningcss";
|
|
||||||
import type { PreRenderedAsset } from "rolldown";
|
|
||||||
import type { Config } from "svgo";
|
|
||||||
import type { UserConfig } from "vite";
|
|
||||||
import analyzer from "vite-bundle-analyzer";
|
|
||||||
import manifest from "./package.json" with { type: "json" };
|
|
||||||
import { plg_svg2png, plg_svg2svg } from "./tools/plg.ts";
|
|
||||||
|
|
||||||
const ROOT = "../../"; // root of the git repository
|
|
||||||
|
|
||||||
const PATH = {
|
|
||||||
brand: "src/brand/",
|
|
||||||
dist: resolve(ROOT, "searx/static/themes/simple/"),
|
|
||||||
modules: "node_modules/",
|
|
||||||
src: "src/",
|
|
||||||
templates: resolve(ROOT, "searx/templates/simple/")
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const svg2svg_opts: Config = {
|
|
||||||
plugins: [{ name: "preset-default" }, "sortAttrs", "convertStyleToAttrs"]
|
|
||||||
};
|
|
||||||
|
|
||||||
const svg2svg_favicon_opts: Config = {
|
|
||||||
plugins: [{ name: "preset-default" }, "sortAttrs"]
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
base: "./",
|
|
||||||
publicDir: "static/",
|
|
||||||
|
|
||||||
build: {
|
|
||||||
target: browserslistToEsbuild(manifest.browserslist),
|
|
||||||
assetsDir: "",
|
|
||||||
outDir: PATH.dist,
|
|
||||||
manifest: "manifest.json",
|
|
||||||
emptyOutDir: true,
|
|
||||||
sourcemap: true,
|
|
||||||
rolldownOptions: {
|
|
||||||
input: {
|
|
||||||
// entrypoint
|
|
||||||
core: `${PATH.src}/js/index.ts`,
|
|
||||||
|
|
||||||
// stylesheets
|
|
||||||
ltr: `${PATH.src}/less/style-ltr.less`,
|
|
||||||
rtl: `${PATH.src}/less/style-rtl.less`,
|
|
||||||
rss: `${PATH.src}/less/rss.less`
|
|
||||||
},
|
|
||||||
|
|
||||||
// file naming conventions / pathnames are relative to outDir (PATH.dist)
|
|
||||||
output: {
|
|
||||||
entryFileNames: "sxng-[name].min.js",
|
|
||||||
chunkFileNames: "chunk/[hash].min.js",
|
|
||||||
assetFileNames: ({ names }: PreRenderedAsset): string => {
|
|
||||||
const [name] = names;
|
|
||||||
|
|
||||||
switch (name?.split(".").pop()) {
|
|
||||||
case "css":
|
|
||||||
return "sxng-[name].min[extname]";
|
|
||||||
default:
|
|
||||||
return "sxng-[name][extname]";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sanitizeFileName: (name: string): string => {
|
|
||||||
return name
|
|
||||||
.normalize("NFD")
|
|
||||||
.replace(/[^a-zA-Z0-9.-]/g, "_")
|
|
||||||
.toLowerCase();
|
|
||||||
},
|
|
||||||
comments: {
|
|
||||||
legal: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, // end: build
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
// -- bundle analyzer
|
|
||||||
analyzer({
|
|
||||||
enabled: process.env.VITE_BUNDLE_ANALYZE === "true",
|
|
||||||
analyzerPort: "auto",
|
|
||||||
summary: true,
|
|
||||||
reportTitle: manifest.name,
|
|
||||||
|
|
||||||
// sidecars with max compression
|
|
||||||
gzipOptions: {
|
|
||||||
level: zlibConstants.Z_BEST_COMPRESSION
|
|
||||||
},
|
|
||||||
brotliOptions: {
|
|
||||||
params: {
|
|
||||||
[zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
// -- svg images
|
|
||||||
plg_svg2svg(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
src: `${PATH.src}/svg/empty_favicon.svg`,
|
|
||||||
dest: `${PATH.dist}/img/empty_favicon.svg`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: `${PATH.src}/svg/select-dark.svg`,
|
|
||||||
dest: `${PATH.dist}/img/select-dark.svg`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: `${PATH.src}/svg/select-light.svg`,
|
|
||||||
dest: `${PATH.dist}/img/select-light.svg`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
svg2svg_opts
|
|
||||||
),
|
|
||||||
|
|
||||||
// SearXNG brand (static)
|
|
||||||
plg_svg2png([
|
|
||||||
{
|
|
||||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
|
||||||
dest: `${PATH.dist}/img/favicon.png`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: `${PATH.brand}/searxng.svg`,
|
|
||||||
dest: `${PATH.dist}/img/searxng.png`
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
|
|
||||||
// SearXNG PWA Icons (static)
|
|
||||||
plg_svg2png(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
|
||||||
dest: `${PATH.dist}/img/512.png`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
512,
|
|
||||||
512
|
|
||||||
),
|
|
||||||
plg_svg2png(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
|
||||||
dest: `${PATH.dist}/img/192.png`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
192,
|
|
||||||
192
|
|
||||||
),
|
|
||||||
|
|
||||||
// -- svg
|
|
||||||
plg_svg2svg(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
src: `${PATH.brand}/searxng.svg`,
|
|
||||||
dest: `${PATH.dist}/img/searxng.svg`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: `${PATH.brand}/img_load_error.svg`,
|
|
||||||
dest: `${PATH.dist}/img/img_load_error.svg`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
svg2svg_opts
|
|
||||||
),
|
|
||||||
|
|
||||||
// -- favicon
|
|
||||||
plg_svg2svg(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
|
||||||
dest: `${PATH.dist}/img/favicon.svg`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
svg2svg_favicon_opts
|
|
||||||
),
|
|
||||||
|
|
||||||
// -- simple templates
|
|
||||||
plg_svg2svg(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
src: `${PATH.brand}/searxng-wordmark.svg`,
|
|
||||||
dest: `${PATH.templates}/searxng-wordmark.min.svg`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
svg2svg_opts
|
|
||||||
)
|
|
||||||
], // end: plugins
|
|
||||||
|
|
||||||
// FIXME: missing CCS sourcemaps!!
|
|
||||||
// see: https://github.com/vitejs/vite/discussions/13845#discussioncomment-11992084
|
|
||||||
//
|
|
||||||
// what I have tried so far (see config below):
|
|
||||||
//
|
|
||||||
// - build.sourcemap
|
|
||||||
// - esbuild.sourcemap
|
|
||||||
// - css.preprocessorOptions.less.sourceMap
|
|
||||||
css: {
|
|
||||||
transformer: "lightningcss",
|
|
||||||
lightningcss: {
|
|
||||||
targets: browserslistToTargets(manifest.browserslist)
|
|
||||||
},
|
|
||||||
devSourcemap: true
|
|
||||||
} // end: css
|
|
||||||
} satisfies UserConfig;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Read the documentation before using the `docker-compose.yml` file:
|
|
||||||
# https://docs.searxng.org/admin/installation-docker.html
|
|
||||||
#
|
|
||||||
# Additional ENVs:
|
|
||||||
# https://docs.searxng.org/admin/settings/settings_general.html#settings-general
|
|
||||||
# https://docs.searxng.org/admin/settings/settings_server.html#settings-server
|
|
||||||
|
|
||||||
# Use a specific version tag. E.g. "latest" or "2026.3.25-541c6c3cb".
|
|
||||||
#SEARXNG_VERSION=latest
|
|
||||||
|
|
||||||
# Listen to a specific address.
|
|
||||||
#SEARXNG_HOST=[::]
|
|
||||||
|
|
||||||
# Listen to a specific port.
|
|
||||||
#SEARXNG_PORT=8080
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
FROM docker.io/searxng/base:searxng-builder AS builder
|
|
||||||
|
|
||||||
COPY ./requirements.txt ./requirements-server.txt ./
|
|
||||||
|
|
||||||
ENV UV_NO_MANAGED_PYTHON="true"
|
|
||||||
ENV UV_NATIVE_TLS="true"
|
|
||||||
|
|
||||||
ARG TIMESTAMP_VENV="0"
|
|
||||||
|
|
||||||
RUN --mount=type=cache,id=uv,target=/root/.cache/uv set -eux -o pipefail; \
|
|
||||||
export SOURCE_DATE_EPOCH="$TIMESTAMP_VENV"; \
|
|
||||||
uv venv; \
|
|
||||||
uv pip install --requirements ./requirements.txt --requirements ./requirements-server.txt; \
|
|
||||||
uv cache prune --ci; \
|
|
||||||
find ./.venv/lib/ -type f -exec strip --strip-unneeded {} + || true; \
|
|
||||||
find ./.venv/lib/ -type d -name "__pycache__" -exec rm -rf {} +; \
|
|
||||||
find ./.venv/lib/ -type f -name "*.pyc" -delete; \
|
|
||||||
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./.venv/lib/; \
|
|
||||||
find ./.venv/lib/python*/site-packages/*.dist-info/ -type f -name "RECORD" -exec sort -t, -k1,1 -o {} {} \;; \
|
|
||||||
find ./.venv/ -exec touch -h --date="@$TIMESTAMP_VENV" {} +
|
|
||||||
|
|
||||||
COPY --exclude=./searx/version_frozen.py ./searx/ ./searx/
|
|
||||||
|
|
||||||
RUN set -eux -o pipefail; \
|
|
||||||
python -m compileall -q -f -j 0 --invalidation-mode=unchecked-hash ./searx/; \
|
|
||||||
find ./searx/static/ -type f \
|
|
||||||
\( -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
|
|
||||||
-exec gzip -9 -k {} + \
|
|
||||||
-exec brotli -9 -k {} + \
|
|
||||||
-exec gzip --test {}.gz + \
|
|
||||||
-exec brotli --test {}.br +
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
ARG CONTAINER_IMAGE_ORGANIZATION="searxng"
|
|
||||||
ARG CONTAINER_IMAGE_NAME="searxng"
|
|
||||||
|
|
||||||
FROM localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder AS builder
|
|
||||||
FROM docker.io/searxng/base:searxng AS dist
|
|
||||||
|
|
||||||
COPY --chown=977:977 --from=builder /usr/local/searxng/.venv/ ./.venv/
|
|
||||||
COPY --chown=977:977 --from=builder /usr/local/searxng/searx/ ./searx/
|
|
||||||
COPY --chown=977:977 ./container/ ./
|
|
||||||
COPY --chown=977:977 ./searx/version_frozen.py ./searx/
|
|
||||||
|
|
||||||
ARG CREATED="0001-01-01T00:00:00Z"
|
|
||||||
ARG VERSION="unknown"
|
|
||||||
ARG VCS_URL="unknown"
|
|
||||||
ARG VCS_REVISION="unknown"
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.created="$CREATED" \
|
|
||||||
org.opencontainers.image.description="SearXNG is a metasearch engine. Users are neither tracked nor profiled." \
|
|
||||||
org.opencontainers.image.documentation="https://docs.searxng.org/admin/installation-docker" \
|
|
||||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
|
||||||
org.opencontainers.image.revision="$VCS_REVISION" \
|
|
||||||
org.opencontainers.image.source="$VCS_URL" \
|
|
||||||
org.opencontainers.image.title="SearXNG" \
|
|
||||||
org.opencontainers.image.url="https://searxng.org" \
|
|
||||||
org.opencontainers.image.version="$VERSION"
|
|
||||||
|
|
||||||
ENV __SEARXNG_VERSION="$VERSION" \
|
|
||||||
__SEARXNG_SETTINGS_PATH="$__SEARXNG_CONFIG_PATH/settings.yml" \
|
|
||||||
GRANIAN_PROCESS_NAME="searxng" \
|
|
||||||
GRANIAN_INTERFACE="wsgi" \
|
|
||||||
GRANIAN_HOST="::" \
|
|
||||||
GRANIAN_PORT="8080" \
|
|
||||||
GRANIAN_WEBSOCKETS="false" \
|
|
||||||
GRANIAN_BLOCKING_THREADS="4" \
|
|
||||||
GRANIAN_WORKERS_KILL_TIMEOUT="30s" \
|
|
||||||
GRANIAN_BLOCKING_THREADS_IDLE_TIMEOUT="5m"
|
|
||||||
|
|
||||||
# "*_PATH" ENVs are defined in base images
|
|
||||||
VOLUME $__SEARXNG_CONFIG_PATH
|
|
||||||
VOLUME $__SEARXNG_DATA_PATH
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/searxng/entrypoint.sh"]
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Read the documentation before using the `docker-compose.yml` file:
|
|
||||||
# https://docs.searxng.org/admin/installation-docker.html
|
|
||||||
|
|
||||||
name: searxng
|
|
||||||
|
|
||||||
services:
|
|
||||||
core:
|
|
||||||
container_name: searxng-core
|
|
||||||
image: docker.io/searxng/searxng:${SEARXNG_VERSION:-latest}
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- ${SEARXNG_HOST:+${SEARXNG_HOST}:}${SEARXNG_PORT:-8080}:${SEARXNG_PORT:-8080}
|
|
||||||
env_file: ./.env
|
|
||||||
volumes:
|
|
||||||
- ./core-config/:/etc/searxng/:Z
|
|
||||||
- core-data:/var/cache/searxng/
|
|
||||||
|
|
||||||
valkey:
|
|
||||||
container_name: searxng-valkey
|
|
||||||
image: docker.io/valkey/valkey:9-alpine
|
|
||||||
command: valkey-server --save 30 1 --loglevel warning
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- valkey-data:/data/
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
core-data:
|
|
||||||
valkey-data:
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# shellcheck shell=dash
|
|
||||||
set -u
|
|
||||||
|
|
||||||
# Check if it's a valid file
|
|
||||||
check_file() {
|
|
||||||
local target="$1"
|
|
||||||
|
|
||||||
if [ ! -f "$target" ]; then
|
|
||||||
cat <<EOF
|
|
||||||
!!!
|
|
||||||
!!! ERROR
|
|
||||||
!!! "$target" is not a valid file, exiting...
|
|
||||||
!!!
|
|
||||||
EOF
|
|
||||||
exit 127
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if it's a valid directory
|
|
||||||
check_directory() {
|
|
||||||
local target="$1"
|
|
||||||
|
|
||||||
if [ ! -d "$target" ]; then
|
|
||||||
cat <<EOF
|
|
||||||
!!!
|
|
||||||
!!! ERROR
|
|
||||||
!!! "$target" is not a valid directory, exiting...
|
|
||||||
!!!
|
|
||||||
EOF
|
|
||||||
exit 127
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_ownership() {
|
|
||||||
local target="$1"
|
|
||||||
local type="$2"
|
|
||||||
|
|
||||||
case "$type" in
|
|
||||||
file | directory) ;;
|
|
||||||
*)
|
|
||||||
cat <<EOF
|
|
||||||
!!!
|
|
||||||
!!! ERROR
|
|
||||||
!!! "$type" is not a valid type, exiting...
|
|
||||||
!!!
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
target_ownership=$(stat -c %U:%G "$target")
|
|
||||||
|
|
||||||
if [ "$target_ownership" != "searxng:searxng" ]; then
|
|
||||||
if [ "${FORCE_OWNERSHIP:-true}" = true ] && [ "$(id -u)" -eq 0 ]; then
|
|
||||||
chown -R searxng:searxng "$target"
|
|
||||||
else
|
|
||||||
cat <<EOF
|
|
||||||
!!!
|
|
||||||
!!! WARNING
|
|
||||||
!!! "$target" $type is not owned by "searxng:searxng"
|
|
||||||
!!! This may cause issues when running SearXNG
|
|
||||||
!!!
|
|
||||||
!!! Expected "searxng:searxng"
|
|
||||||
!!! Got "$target_ownership"
|
|
||||||
!!!
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Handle volume mounts
|
|
||||||
volume_handler() {
|
|
||||||
local target="$1"
|
|
||||||
|
|
||||||
check_directory "$target"
|
|
||||||
setup_ownership "$target" "directory"
|
|
||||||
}
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
local template_settings="/usr/local/searxng/settings.template.yml"
|
|
||||||
local target_settings="$__SEARXNG_CONFIG_PATH/settings.yml"
|
|
||||||
|
|
||||||
if [ ! -f "$target_settings" ]; then
|
|
||||||
cat <<EOF
|
|
||||||
...
|
|
||||||
... INFORMATION
|
|
||||||
... "$target_settings" does not exist, creating from template...
|
|
||||||
...
|
|
||||||
EOF
|
|
||||||
cp -pfT "$template_settings" "$target_settings"
|
|
||||||
|
|
||||||
sed -i "s/ultrasecretkey/$(head -c 24 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9')/g" "$target_settings"
|
|
||||||
fi
|
|
||||||
|
|
||||||
check_file "$target_settings"
|
|
||||||
}
|
|
||||||
|
|
||||||
cat <<EOF
|
|
||||||
SearXNG $__SEARXNG_VERSION
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Check for volume mounts
|
|
||||||
volume_handler "$__SEARXNG_CONFIG_PATH"
|
|
||||||
volume_handler "$__SEARXNG_DATA_PATH"
|
|
||||||
|
|
||||||
setup
|
|
||||||
|
|
||||||
# root only features
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
|
||||||
update-ca-certificates
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ENVs aliases
|
|
||||||
# https://github.com/searxng/searxng/issues/5934
|
|
||||||
case "${SEARXNG_PORT:-}" in
|
|
||||||
'') ;;
|
|
||||||
*[!0-9]*)
|
|
||||||
unset SEARXNG_PORT
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
export GRANIAN_PORT="$SEARXNG_PORT"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
exec /usr/local/searxng/.venv/bin/granian searx.webapp:app
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Read the documentation before extending the defaults:
|
|
||||||
# https://docs.searxng.org/admin/settings/
|
|
||||||
|
|
||||||
use_default_settings: true
|
|
||||||
|
|
||||||
server:
|
|
||||||
secret_key: "ultrasecretkey"
|
|
||||||
image_proxy: true
|
|
||||||
178
dockerfiles/docker-entrypoint.sh
Executable file
178
dockerfiles/docker-entrypoint.sh
Executable file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
help() {
|
||||||
|
cat <<EOF
|
||||||
|
Command line:
|
||||||
|
-h Display this help
|
||||||
|
-d Dry run to update the configuration files.
|
||||||
|
-f Always update on the configuration files (existing files are renamed with
|
||||||
|
the .old suffix). Without this option, the new configuration files are
|
||||||
|
copied with the .new suffix
|
||||||
|
Environment variables:
|
||||||
|
INSTANCE_NAME settings.yml : general.instance_name
|
||||||
|
AUTOCOMPLETE settings.yml : search.autocomplete
|
||||||
|
BASE_URL settings.yml : server.base_url
|
||||||
|
MORTY_URL settings.yml : result_proxy.url
|
||||||
|
MORTY_KEY settings.yml : result_proxy.key
|
||||||
|
BIND_ADDRESS uwsgi bind to the specified TCP socket using HTTP protocol.
|
||||||
|
Default value: ${DEFAULT_BIND_ADDRESS}
|
||||||
|
Volume:
|
||||||
|
/etc/searxng the docker entry point copies settings.yml and uwsgi.ini in
|
||||||
|
this directory (see the -f command line option)"
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
export DEFAULT_BIND_ADDRESS="0.0.0.0:8080"
|
||||||
|
export BIND_ADDRESS="${BIND_ADDRESS:-${DEFAULT_BIND_ADDRESS}}"
|
||||||
|
|
||||||
|
# Parse command line
|
||||||
|
FORCE_CONF_UPDATE=0
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
while getopts "fdh" option
|
||||||
|
do
|
||||||
|
case $option in
|
||||||
|
|
||||||
|
f) FORCE_CONF_UPDATE=1 ;;
|
||||||
|
d) DRY_RUN=1 ;;
|
||||||
|
|
||||||
|
h)
|
||||||
|
help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknow option ${option}"
|
||||||
|
exit 42
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
get_searxng_version(){
|
||||||
|
su searxng -c \
|
||||||
|
'python3 -c "import six; import searx.version; six.print_(searx.version.VERSION_STRING)"' \
|
||||||
|
2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
SEARXNG_VERSION="$(get_searxng_version)"
|
||||||
|
export SEARXNG_VERSION
|
||||||
|
echo "SearXNG version ${SEARXNG_VERSION}"
|
||||||
|
|
||||||
|
# helpers to update the configuration files
|
||||||
|
patch_uwsgi_settings() {
|
||||||
|
CONF="$1"
|
||||||
|
|
||||||
|
# update uwsg.ini
|
||||||
|
sed -i \
|
||||||
|
-e "s|workers = .*|workers = ${UWSGI_WORKERS:-%k}|g" \
|
||||||
|
-e "s|threads = .*|threads = ${UWSGI_THREADS:-4}|g" \
|
||||||
|
"${CONF}"
|
||||||
|
}
|
||||||
|
|
||||||
|
patch_searxng_settings() {
|
||||||
|
CONF="$1"
|
||||||
|
|
||||||
|
# Make sure that there is trailing slash at the end of BASE_URL
|
||||||
|
# see https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Shell-Parameter-Expansion
|
||||||
|
export BASE_URL="${BASE_URL%/}/"
|
||||||
|
|
||||||
|
# update settings.yml
|
||||||
|
sed -i \
|
||||||
|
-e "s|base_url: false|base_url: ${BASE_URL}|g" \
|
||||||
|
-e "s/instance_name: \"SearXNG\"/instance_name: \"${INSTANCE_NAME}\"/g" \
|
||||||
|
-e "s/autocomplete: \"\"/autocomplete: \"${AUTOCOMPLETE}\"/g" \
|
||||||
|
-e "s/ultrasecretkey/$(openssl rand -hex 32)/g" \
|
||||||
|
"${CONF}"
|
||||||
|
|
||||||
|
# Morty configuration
|
||||||
|
|
||||||
|
if [ -n "${MORTY_KEY}" ] && [ -n "${MORTY_URL}" ]; then
|
||||||
|
sed -i -e "s/image_proxy: false/image_proxy: true/g" \
|
||||||
|
"${CONF}"
|
||||||
|
cat >> "${CONF}" <<-EOF
|
||||||
|
|
||||||
|
# Morty configuration
|
||||||
|
result_proxy:
|
||||||
|
url: ${MORTY_URL}
|
||||||
|
key: !!binary "${MORTY_KEY}"
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update_conf() {
|
||||||
|
FORCE_CONF_UPDATE=$1
|
||||||
|
CONF="$2"
|
||||||
|
NEW_CONF="${2}.new"
|
||||||
|
OLD_CONF="${2}.old"
|
||||||
|
REF_CONF="$3"
|
||||||
|
PATCH_REF_CONF="$4"
|
||||||
|
|
||||||
|
if [ -f "${CONF}" ]; then
|
||||||
|
if [ "${REF_CONF}" -nt "${CONF}" ]; then
|
||||||
|
# There is a new version
|
||||||
|
if [ "$FORCE_CONF_UPDATE" -ne 0 ]; then
|
||||||
|
# Replace the current configuration
|
||||||
|
printf '⚠️ Automatically update %s to the new version\n' "${CONF}"
|
||||||
|
if [ ! -f "${OLD_CONF}" ]; then
|
||||||
|
printf 'The previous configuration is saved to %s\n' "${OLD_CONF}"
|
||||||
|
mv "${CONF}" "${OLD_CONF}"
|
||||||
|
fi
|
||||||
|
cp "${REF_CONF}" "${CONF}"
|
||||||
|
$PATCH_REF_CONF "${CONF}"
|
||||||
|
else
|
||||||
|
# Keep the current configuration
|
||||||
|
printf '⚠️ Check new version %s to make sure SearXNG is working properly\n' "${NEW_CONF}"
|
||||||
|
cp "${REF_CONF}" "${NEW_CONF}"
|
||||||
|
$PATCH_REF_CONF "${NEW_CONF}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf 'Use existing %s\n' "${CONF}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf 'Create %s\n' "${CONF}"
|
||||||
|
cp "${REF_CONF}" "${CONF}"
|
||||||
|
$PATCH_REF_CONF "${CONF}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# searx compatibility: copy /etc/searx/* to /etc/searxng/*
|
||||||
|
SEARX_CONF=0
|
||||||
|
if [ -f "/etc/searx/settings.yml" ]; then
|
||||||
|
if [ ! -f "${SEARXNG_SETTINGS_PATH}" ]; then
|
||||||
|
printf '⚠️ /etc/searx/settings.yml is copied to /etc/searxng\n'
|
||||||
|
cp "/etc/searx/settings.yml" "${SEARXNG_SETTINGS_PATH}"
|
||||||
|
fi
|
||||||
|
SEARX_CONF=1
|
||||||
|
fi
|
||||||
|
if [ -f "/etc/searx/uwsgi.ini" ]; then
|
||||||
|
printf '⚠️ /etc/searx/uwsgi.ini is ignored. Use the volume /etc/searxng\n'
|
||||||
|
SEARX_CONF=1
|
||||||
|
fi
|
||||||
|
if [ "$SEARX_CONF" -eq "1" ]; then
|
||||||
|
printf '⚠️ The deprecated volume /etc/searx is mounted. Please update your configuration to use /etc/searxng ⚠️\n'
|
||||||
|
cat << EOF > /etc/searx/deprecated_volume_read_me.txt
|
||||||
|
This Docker image uses the volume /etc/searxng
|
||||||
|
Update your configuration:
|
||||||
|
* remove uwsgi.ini (or very carefully update your existing uwsgi.ini using https://github.com/searxng/searxng/blob/master/dockerfiles/uwsgi.ini )
|
||||||
|
* mount /etc/searxng instead of /etc/searx
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
# end of searx compatibility
|
||||||
|
|
||||||
|
# make sure there are uwsgi settings
|
||||||
|
update_conf "${FORCE_CONF_UPDATE}" "${UWSGI_SETTINGS_PATH}" "/usr/local/searxng/dockerfiles/uwsgi.ini" "patch_uwsgi_settings"
|
||||||
|
|
||||||
|
# make sure there are searxng settings
|
||||||
|
update_conf "${FORCE_CONF_UPDATE}" "${SEARXNG_SETTINGS_PATH}" "/usr/local/searxng/searx/settings.yml" "patch_searxng_settings"
|
||||||
|
|
||||||
|
# dry run (to update configuration files, then inspect them)
|
||||||
|
if [ $DRY_RUN -eq 1 ]; then
|
||||||
|
printf 'Dry run\n'
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset MORTY_KEY
|
||||||
|
|
||||||
|
# Start uwsgi
|
||||||
|
printf 'Listen on %s\n' "${BIND_ADDRESS}"
|
||||||
|
exec uwsgi --master --uid searxng --gid searxng --http-socket "${BIND_ADDRESS}" "${UWSGI_SETTINGS_PATH}"
|
||||||
54
dockerfiles/uwsgi.ini
Normal file
54
dockerfiles/uwsgi.ini
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
[uwsgi]
|
||||||
|
# Who will run the code
|
||||||
|
uid = searxng
|
||||||
|
gid = searxng
|
||||||
|
|
||||||
|
# Number of workers (usually CPU count)
|
||||||
|
# default value: %k (= number of CPU core, see Dockerfile)
|
||||||
|
workers = $(UWSGI_WORKERS)
|
||||||
|
|
||||||
|
# Number of threads per worker
|
||||||
|
# default value: 4 (see Dockerfile)
|
||||||
|
threads = $(UWSGI_THREADS)
|
||||||
|
|
||||||
|
# The right granted on the created socket
|
||||||
|
chmod-socket = 666
|
||||||
|
|
||||||
|
# Plugin to use and interpreter config
|
||||||
|
single-interpreter = true
|
||||||
|
master = true
|
||||||
|
plugin = python3
|
||||||
|
lazy-apps = true
|
||||||
|
enable-threads = true
|
||||||
|
|
||||||
|
# Module to import
|
||||||
|
module = searx.webapp
|
||||||
|
|
||||||
|
# Virtualenv and python path
|
||||||
|
pythonpath = /usr/local/searxng/
|
||||||
|
chdir = /usr/local/searxng/searx/
|
||||||
|
|
||||||
|
# automatically set processes name to something meaningful
|
||||||
|
auto-procname = true
|
||||||
|
|
||||||
|
# Disable request logging for privacy
|
||||||
|
disable-logging = true
|
||||||
|
log-5xx = true
|
||||||
|
|
||||||
|
# Set the max size of a request (request-body excluded)
|
||||||
|
buffer-size = 8192
|
||||||
|
|
||||||
|
# No keep alive
|
||||||
|
# See https://github.com/searx/searx-docker/issues/24
|
||||||
|
add-header = Connection: close
|
||||||
|
|
||||||
|
# Follow SIGTERM convention
|
||||||
|
# See https://github.com/searxng/searxng/issues/3427
|
||||||
|
die-on-term
|
||||||
|
|
||||||
|
# uwsgi serves the static files
|
||||||
|
static-map = /static=/usr/local/searxng/searx/static
|
||||||
|
# expires set to one day
|
||||||
|
static-expires = /* 86400
|
||||||
|
static-gzip-all = True
|
||||||
|
offload-threads = %k
|
||||||
57
docs/_static/searxng.css
vendored
57
docs/_static/searxng.css
vendored
@@ -1,57 +0,0 @@
|
|||||||
.content {
|
|
||||||
width: 52em; /* instead of 46em */
|
|
||||||
}
|
|
||||||
|
|
||||||
p code.literal {
|
|
||||||
text-wrap: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside.sidebar {
|
|
||||||
border-color: lightsteelblue;
|
|
||||||
border-radius: 3pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.sidebar-title, .sidebar p {
|
|
||||||
margin: 6pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar li,
|
|
||||||
.hlist li {
|
|
||||||
list-style-type: disclosure-closed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sphinxsidebar .current > a {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* admonitions with (rendered) reST markup examples (:class: rst-example)
|
|
||||||
*
|
|
||||||
* .. admonition:: title of the example
|
|
||||||
* :class: rst-example
|
|
||||||
* ....
|
|
||||||
*/
|
|
||||||
|
|
||||||
div.rst-example {
|
|
||||||
background-color: inherit;
|
|
||||||
margin: 0;
|
|
||||||
border-top: none;
|
|
||||||
border-right: 1px solid #ccc;
|
|
||||||
border-bottom: none;
|
|
||||||
border-left: none;
|
|
||||||
border-radius: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.rst-example > p.admonition-title {
|
|
||||||
font-family: Sans Serif;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 0.8em;
|
|
||||||
display: block;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinx-tabs {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
151
docs/_themes/searxng/static/searxng.css
vendored
Normal file
151
docs/_themes/searxng/static/searxng.css
vendored
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
@import url("pocoo.css");
|
||||||
|
|
||||||
|
a, a.reference, a.footnote-reference {
|
||||||
|
color: #004b6b;
|
||||||
|
border-color: #004b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #6d4100;
|
||||||
|
border-color: #6d4100;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.version-warning {
|
||||||
|
background-color: #004b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.sidebar {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
border-color: lightsteelblue;
|
||||||
|
border-radius: 3pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar p.caption {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.sidebar-title, .sidebar p {
|
||||||
|
margin: 6pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar li,
|
||||||
|
.hlist li {
|
||||||
|
list-style-type: disclosure-closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sphinxsidebar .current > a {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* admonitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.admonition, div.topic, nav.contents, div.toctree-wrapper {
|
||||||
|
background-color: #fafafa;
|
||||||
|
margin: 8px 0px;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 3pt 0 0 3pt;
|
||||||
|
border-top: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-left: 5pt solid #ccc;
|
||||||
|
list-style-type: disclosure-closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.toctree-wrapper p.caption {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.admonition-title:after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admonition.hint { border-color: #416dc0b0; }
|
||||||
|
.admonition.note { border-color: #6c856cb0; }
|
||||||
|
.admonition.tip { border-color: #85c5c2b0; }
|
||||||
|
.admonition.attention { border-color: #ecec97b0; }
|
||||||
|
.admonition.caution { border-color: #a6c677b0; }
|
||||||
|
.admonition.danger { border-color: #d46262b0; }
|
||||||
|
.admonition.important { border-color: #dfa3a3b0; }
|
||||||
|
.admonition.error { border-color: red; }
|
||||||
|
.admonition.warning { border-color: darkred; }
|
||||||
|
|
||||||
|
.admonition.admonition-generic-admonition-title {
|
||||||
|
border-color: #416dc0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* admonitions with (rendered) reST markup examples (:class: rst-example)
|
||||||
|
*
|
||||||
|
* .. admonition:: title of the example
|
||||||
|
* :class: rst-example
|
||||||
|
* ....
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.rst-example {
|
||||||
|
background-color: inherit;
|
||||||
|
margin: 0;
|
||||||
|
border-top: none;
|
||||||
|
border-right: 1px solid #ccc;
|
||||||
|
border-bottom: none;
|
||||||
|
border-left: none;
|
||||||
|
border-radius: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.rst-example > p.admonition-title {
|
||||||
|
font-family: Sans Serif;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: block;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* code block in figures
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.highlight pre {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table theme
|
||||||
|
*/
|
||||||
|
|
||||||
|
thead, tfoot {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:hover, td:hover {
|
||||||
|
background-color: #ffc;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th, tfoot th, tfoot td, tbody th {
|
||||||
|
background-color: #fffaef;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(odd) {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
font-family: Sans Serif;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.5em 0 0.5em 0;
|
||||||
|
caption-side: top;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinx-tabs {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
7
docs/_themes/searxng/theme.conf
vendored
Normal file
7
docs/_themes/searxng/theme.conf
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[theme]
|
||||||
|
inherit = pocoo
|
||||||
|
stylesheet = searxng.css
|
||||||
|
|
||||||
|
[options]
|
||||||
|
touch_icon =
|
||||||
|
globaltoc_maxdepth = 5
|
||||||
@@ -48,7 +48,7 @@ solve the CAPTCHA from `qwant.com <https://www.qwant.com/>`__.
|
|||||||
|
|
||||||
.. group-tab:: Firefox
|
.. group-tab:: Firefox
|
||||||
|
|
||||||
.. kernel-figure:: /assets/answer-captcha/ffox-setting-proxy-socks.png
|
.. kernel-figure:: answer-captcha/ffox-setting-proxy-socks.png
|
||||||
:alt: FFox proxy on SOCKS5, 127.0.0.1:8080
|
:alt: FFox proxy on SOCKS5, 127.0.0.1:8080
|
||||||
|
|
||||||
Firefox's network settings
|
Firefox's network settings
|
||||||
@@ -66,3 +66,4 @@ solve the CAPTCHA from `qwant.com <https://www.qwant.com/>`__.
|
|||||||
|
|
||||||
-N
|
-N
|
||||||
Do not execute a remote command. This is useful for just forwarding ports.
|
Do not execute a remote command. This is useful for just forwarding ports.
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
@@ -59,7 +59,7 @@ Sample response
|
|||||||
"shortcut": "bb"
|
"shortcut": "bb"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"instance_name": "SearXNG",
|
"instance_name": "searx",
|
||||||
"locales": {
|
"locales": {
|
||||||
"de": "Deutsch (German)",
|
"de": "Deutsch (German)",
|
||||||
"en": "English",
|
"en": "English",
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ digraph G {
|
|||||||
browser [label="browser", shape=tab, fillcolor=aliceblue];
|
browser [label="browser", shape=tab, fillcolor=aliceblue];
|
||||||
rp [label="reverse proxy"];
|
rp [label="reverse proxy"];
|
||||||
static [label="static files", shape=folder, href="url to configure static files", fillcolor=lightgray];
|
static [label="static files", shape=folder, href="url to configure static files", fillcolor=lightgray];
|
||||||
uwsgi [label="uwsgi", shape=parallelogram href="https://docs.searxng.org/utils/searxng.sh.html"]
|
uwsgi [label="uwsgi", shape=parallelogram href="https://docs.searxng.org/utils/searx.sh.html"]
|
||||||
valkey [label="valkey DB", shape=cylinder];
|
redis [label="redis DB", shape=cylinder];
|
||||||
|
|
||||||
searxng1 [label="SearXNG #1", fontcolor=blue3];
|
searxng1 [label="SearXNG #1", fontcolor=blue3];
|
||||||
searxng2 [label="SearXNG #2", fontcolor=blue3];
|
searxng2 [label="SearXNG #2", fontcolor=blue3];
|
||||||
searxng3 [label="SearXNG #3", fontcolor=blue3];
|
searxng3 [label="SearXNG #3", fontcolor=blue3];
|
||||||
@@ -22,10 +21,10 @@ digraph G {
|
|||||||
{ rank=same; static rp };
|
{ rank=same; static rp };
|
||||||
rp -> static [label="optional: reverse proxy serves static files", fillcolor=slategray, fontcolor=slategray];
|
rp -> static [label="optional: reverse proxy serves static files", fillcolor=slategray, fontcolor=slategray];
|
||||||
rp -> uwsgi [label="http:// (tcp) or unix:// (socket)"];
|
rp -> uwsgi [label="http:// (tcp) or unix:// (socket)"];
|
||||||
uwsgi -> searxng1 -> valkey;
|
uwsgi -> searxng1 -> redis;
|
||||||
uwsgi -> searxng2 -> valkey;
|
uwsgi -> searxng2 -> redis;
|
||||||
uwsgi -> searxng3 -> valkey;
|
uwsgi -> searxng3 -> redis;
|
||||||
uwsgi -> searxng4 -> valkey;
|
uwsgi -> searxng4 -> redis;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ up and maintained by the scripts from our :ref:`toolboxing`.
|
|||||||
|
|
||||||
Reference architecture of a public SearXNG setup.
|
Reference architecture of a public SearXNG setup.
|
||||||
|
|
||||||
The reference installation activates ``server.limiter`` and
|
The reference installation activates ``server.limiter``, ``server.image_proxy``
|
||||||
``server.image_proxy`` (:origin:`/etc/searxng/settings.yml
|
and ``ui.static_use_hash`` (:origin:`/etc/searxng/settings.yml
|
||||||
<utils/templates/etc/searxng/settings.yml>`)
|
<utils/templates/etc/searxng/settings.yml>`)
|
||||||
|
|
||||||
.. literalinclude:: ../../utils/templates/etc/searxng/settings.yml
|
.. literalinclude:: ../../utils/templates/etc/searxng/settings.yml
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
Buildhosts
|
Buildhosts
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:depth: 2
|
||||||
|
:local:
|
||||||
|
:backlinks: entry
|
||||||
|
|
||||||
To get best results from build, it's recommend to install additional packages on
|
To get best results from build, it's recommend to install additional packages on
|
||||||
build hosts (see :ref:`searxng.sh`).
|
build hosts (see :ref:`searxng.sh`).
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ Administrator documentation
|
|||||||
installation-docker
|
installation-docker
|
||||||
installation-scripts
|
installation-scripts
|
||||||
installation-searxng
|
installation-searxng
|
||||||
installation-granian
|
|
||||||
installation-uwsgi
|
installation-uwsgi
|
||||||
installation-nginx
|
installation-nginx
|
||||||
installation-apache
|
installation-apache
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ section might give you some guidance.
|
|||||||
- `Apache Fedora`_
|
- `Apache Fedora`_
|
||||||
- `Apache directives`_
|
- `Apache directives`_
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:depth: 2
|
||||||
|
:local:
|
||||||
|
:backlinks: entry
|
||||||
|
|
||||||
|
|
||||||
The Apache HTTP server
|
The Apache HTTP server
|
||||||
======================
|
======================
|
||||||
|
|||||||
@@ -1,349 +1,197 @@
|
|||||||
.. _installation container:
|
.. _installation docker:
|
||||||
|
|
||||||
======================
|
================
|
||||||
Installation container
|
Docker Container
|
||||||
======================
|
================
|
||||||
|
|
||||||
.. _Docker 101: https://docs.docker.com/get-started/docker-overview
|
.. _ENTRYPOINT: https://docs.docker.com/engine/reference/builder/#entrypoint
|
||||||
.. _Docker cheat sheet (PDF doc): https://docs.docker.com/get-started/docker_cheatsheet.pdf
|
.. _searxng/searxng @dockerhub: https://hub.docker.com/r/searxng/searxng
|
||||||
.. _Podman rootless containers: https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md
|
.. _searxng-docker: https://github.com/searxng/searxng-docker
|
||||||
.. _DockerHub mirror: https://hub.docker.com/r/searxng/searxng
|
.. _[caddy]: https://hub.docker.com/_/caddy
|
||||||
.. _GHCR mirror: https://ghcr.io/searxng/searxng
|
.. _Redis: https://redis.io/
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
.. sidebar:: info
|
.. sidebar:: info
|
||||||
|
|
||||||
- `Docker 101`_
|
- `searxng/searxng @dockerhub`_
|
||||||
- `Docker cheat sheet (PDF doc)`_
|
- :origin:`Dockerfile`
|
||||||
- `Podman rootless containers`_
|
- `Docker overview <https://docs.docker.com/get-started/overview>`_
|
||||||
|
- `Docker Cheat Sheet <https://docs.docker.com/get-started/docker_cheatsheet.pdf>`_
|
||||||
|
- `Alpine Linux <https://alpinelinux.org>`_
|
||||||
|
`(wiki) <https://en.wikipedia.org/wiki/Alpine_Linux>`__
|
||||||
|
`apt packages <https://pkgs.alpinelinux.org/packages>`_
|
||||||
|
- Alpine's ``/bin/sh`` is :man:`dash`
|
||||||
|
|
||||||
.. important::
|
**If you intend to create a public instance using Docker, use our well maintained
|
||||||
|
docker container**
|
||||||
|
|
||||||
Understanding container architecture basics is essential for properly
|
- `searxng/searxng @dockerhub`_.
|
||||||
maintaining your SearXNG instance. This guide assumes familiarity with
|
|
||||||
container concepts and provides deployment steps at a high level.
|
|
||||||
|
|
||||||
If you're new to containers, we recommend learning the fundamentals at
|
.. sidebar:: hint
|
||||||
`Docker 101`_ before proceeding.
|
|
||||||
|
|
||||||
Container images are the basis for deployments in containerized environments,
|
The rest of this article is of interest only to those who want to create and
|
||||||
Compose, Kubernetes and more.
|
maintain their own Docker images.
|
||||||
|
|
||||||
.. _Container installation:
|
The sources are hosted at searxng-docker_ and the container includes:
|
||||||
|
|
||||||
Installation
|
- a HTTPS reverse proxy `[caddy]`_ and
|
||||||
============
|
- a Redis_ DB
|
||||||
|
|
||||||
.. _Container prerequisites:
|
The `default SearXNG setup <https://github.com/searxng/searxng-docker/blob/master/searxng/settings.yml>`_
|
||||||
|
of this container:
|
||||||
|
|
||||||
Prerequisites
|
- enables :ref:`limiter <limiter>` to protect against bots
|
||||||
-------------
|
- enables :ref:`image proxy <image_proxy>` for better privacy
|
||||||
|
- enables :ref:`cache busting <static_use_hash>` to save bandwidth
|
||||||
|
|
||||||
You need a working Docker or Podman installation on your system. Choose the
|
----
|
||||||
option that works best for your environment:
|
|
||||||
|
|
||||||
- `Docker <https://docs.docker.com/get-docker/>`_ (recommended for most users)
|
|
||||||
- `Podman <https://podman.io/docs/installation>`_
|
|
||||||
|
|
||||||
In the case of Docker, you need to add the user running the container to the
|
Get Docker
|
||||||
``docker`` group and restart the session:
|
==========
|
||||||
|
|
||||||
|
If you plan to build and maintain a docker image by yourself, make sure you have
|
||||||
|
`Docker installed <https://docs.docker.com/get-docker/>`_. On Linux don't
|
||||||
|
forget to add your user to the docker group (log out and log back in so that
|
||||||
|
your group membership is re-evaluated):
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
$ sudo usermod -aG docker $USER
|
$ sudo usermod -a -G docker $USER
|
||||||
|
|
||||||
In the case of Podman, no additional steps are generally required, but there
|
|
||||||
are some considerations when running `Podman rootless containers`_.
|
|
||||||
|
|
||||||
.. _Container registries:
|
searxng/searxng
|
||||||
|
===============
|
||||||
|
|
||||||
Registries
|
.. sidebar:: ``docker run``
|
||||||
----------
|
|
||||||
|
|
||||||
.. note::
|
- `-\-rm <https://docs.docker.com/engine/reference/run/#clean-up---rm>`__
|
||||||
|
automatically clean up when container exits
|
||||||
|
- `-d <https://docs.docker.com/engine/reference/run/#detached--d>`__ start
|
||||||
|
detached container
|
||||||
|
- `-v <https://docs.docker.com/engine/reference/run/#volume-shared-filesystems>`__
|
||||||
|
mount volume ``HOST:CONTAINER``
|
||||||
|
|
||||||
DockerHub now applies rate limits to unauthenticated image pulls. If you
|
The docker image is based on :origin:`Dockerfile` and available from
|
||||||
are affected by this, you can use the `GHCR mirror`_ instead.
|
`searxng/searxng @dockerhub`_. Using the docker image is quite easy, for
|
||||||
|
instance you can pull the `searxng/searxng @dockerhub`_ image and deploy a local
|
||||||
The official images are mirrored at:
|
instance using `docker run <https://docs.docker.com/engine/reference/run/>`_:
|
||||||
|
|
||||||
- `DockerHub mirror`_
|
|
||||||
- `GHCR mirror`_ (GitHub Container Registry)
|
|
||||||
|
|
||||||
.. _Container compose instancing:
|
|
||||||
|
|
||||||
Compose instancing
|
|
||||||
==================
|
|
||||||
|
|
||||||
This is the recommended way to deploy SearXNG in a containerized environment.
|
|
||||||
Compose templates allow you to define container configurations in a
|
|
||||||
declarative manner.
|
|
||||||
|
|
||||||
.. _Container compose instancing setup:
|
|
||||||
|
|
||||||
Setup
|
|
||||||
-----
|
|
||||||
|
|
||||||
1. Create the environment:
|
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
# Create the environment and configuration directories
|
$ mkdir my-instance
|
||||||
$ mkdir -p ./searxng/core-config/
|
$ cd my-instance
|
||||||
$ cd ./searxng/
|
$ export PORT=8080
|
||||||
|
$ docker pull searxng/searxng
|
||||||
|
$ docker run --rm \
|
||||||
|
-d -p ${PORT}:8080 \
|
||||||
|
-v "${PWD}/searxng:/etc/searxng" \
|
||||||
|
-e "BASE_URL=http://localhost:$PORT/" \
|
||||||
|
-e "INSTANCE_NAME=my-instance" \
|
||||||
|
searxng/searxng
|
||||||
|
2f998.... # container's ID
|
||||||
|
|
||||||
# Fetch the latest compose template
|
The environment variables UWSGI_WORKERS and UWSGI_THREADS overwrite the default
|
||||||
$ curl -fsSL \
|
number of UWSGI processes and UWSGI threads specified in `/etc/searxng/uwsgi.ini`.
|
||||||
-O https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml \
|
|
||||||
-O https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
|
|
||||||
|
|
||||||
2. Copy the ``.env.example`` file and edit the values as needed:
|
Open your WEB browser and visit the URL:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
$ cp -i .env.example .env
|
$ xdg-open "http://localhost:$PORT"
|
||||||
|
|
||||||
# nano or your preferred text editor...
|
Inside ``${PWD}/searxng``, you will find ``settings.yml`` and ``uwsgi.ini``. You
|
||||||
$ nano .env
|
can modify these files according to your needs and restart the Docker image.
|
||||||
|
|
||||||
3. Start & stop the services:
|
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
$ docker compose up -d
|
$ docker container restart 2f998
|
||||||
$ docker compose down
|
|
||||||
|
|
||||||
4. Setup your settings in ``core-config/settings.yml`` according to your preferences.
|
Use command ``container ls`` to list running containers, add flag `-a
|
||||||
|
<https://docs.docker.com/engine/reference/commandline/container_ls>`__ to list
|
||||||
.. _Container compose instancing maintenance:
|
exited containers also. With ``container stop`` a running container can be
|
||||||
|
stopped. To get rid of a container use ``container rm``:
|
||||||
Management
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
Remember to review the new templates for any changes that may affect your
|
|
||||||
deployment, and update the ``.env`` file accordingly.
|
|
||||||
|
|
||||||
To update the templates to their latest versions:
|
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
$ docker compose down
|
$ docker container ls
|
||||||
$ curl -fsSLO \
|
CONTAINER ID IMAGE COMMAND CREATED ...
|
||||||
https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml \
|
2f998d725993 searxng/searxng "/sbin/tini -- /usr/…" 7 minutes ago ...
|
||||||
https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
|
|
||||||
$ docker compose up -d
|
|
||||||
|
|
||||||
To update the services to their latest versions:
|
$ docker container stop 2f998
|
||||||
|
$ docker container rm 2f998
|
||||||
|
|
||||||
|
.. sidebar:: Warning
|
||||||
|
|
||||||
|
This might remove all docker items, not only those from SearXNG.
|
||||||
|
|
||||||
|
If you won't use docker anymore and want to get rid of all containers & images
|
||||||
|
use the following *prune* command:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
$ docker compose down
|
$ docker stop $(docker ps -aq) # stop all containers
|
||||||
$ docker compose pull
|
$ docker system prune # make some housekeeping
|
||||||
$ docker compose up -d
|
$ docker rmi -f $(docker images -q) # drop all images
|
||||||
|
|
||||||
List running services:
|
|
||||||
|
shell inside container
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. sidebar:: Bashism
|
||||||
|
|
||||||
|
- `A tale of two shells: bash or dash <https://lwn.net/Articles/343924/>`_
|
||||||
|
- `How to make bash scripts work in dash <http://mywiki.wooledge.org/Bashism>`_
|
||||||
|
- `Checking for Bashisms <https://dev.to/bowmanjd/writing-bash-scripts-that-are-not-only-bash-checking-for-bashisms-and-testing-with-dash-1bli>`_
|
||||||
|
|
||||||
|
Like in many other distributions, Alpine's `/bin/sh
|
||||||
|
<https://wiki.ubuntu.com/DashAsBinSh>`__ is :man:`dash`. Dash is meant to be
|
||||||
|
`POSIX-compliant <https://pubs.opengroup.org/onlinepubs/9699919799>`__.
|
||||||
|
Compared to debian, in the Alpine image :man:`bash` is not installed. The
|
||||||
|
:origin:`dockerfiles/docker-entrypoint.sh` script is checked *against dash*
|
||||||
|
(``make tests.shell``).
|
||||||
|
|
||||||
|
To open a shell inside the container:
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
$ docker compose ps
|
$ docker exec -it 2f998 sh
|
||||||
NAME IMAGE ... CREATED STATUS PORTS
|
|
||||||
searxng-core ... ... 3 minutes ago Up 3 minutes 0.0.0.0:8080->8080/tcp
|
|
||||||
searxng-valkey ... ... 3 minutes ago Up 3 minutes 6379/tcp
|
|
||||||
|
|
||||||
Print a service container logs:
|
|
||||||
|
|
||||||
.. code:: sh
|
Build the image
|
||||||
|
===============
|
||||||
|
|
||||||
$ docker compose logs -f core
|
It's also possible to build SearXNG from the embedded :origin:`Dockerfile`::
|
||||||
|
|
||||||
Access a service container shell (troubleshooting):
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
$ docker compose exec -it --user root core /bin/sh -l
|
|
||||||
/usr/local/searxng #
|
|
||||||
|
|
||||||
Stop and remove the services:
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
$ docker compose down
|
|
||||||
|
|
||||||
.. _Container manual instancing:
|
|
||||||
|
|
||||||
Manual instancing
|
|
||||||
=================
|
|
||||||
|
|
||||||
This section is intended for advanced users who need custom deployments. We
|
|
||||||
recommend using `Container compose instancing`_, which provides a preconfigured
|
|
||||||
environment with sensible defaults.
|
|
||||||
|
|
||||||
Basic container instancing example:
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
# Create directories for configuration and persistent data
|
|
||||||
$ mkdir -p ./searxng/config/ ./searxng/data/
|
|
||||||
$ cd ./searxng/
|
|
||||||
|
|
||||||
# Run the container
|
|
||||||
$ docker run --name searxng -d \
|
|
||||||
-p 8888:8080 \
|
|
||||||
-v "./config/:/etc/searxng/" \
|
|
||||||
-v "./data/:/var/cache/searxng/" \
|
|
||||||
docker.io/searxng/searxng:latest
|
|
||||||
|
|
||||||
This will start SearXNG in the background, accessible at http://localhost:8888
|
|
||||||
|
|
||||||
.. _Container management:
|
|
||||||
|
|
||||||
Management
|
|
||||||
----------
|
|
||||||
|
|
||||||
List running containers:
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
$ docker container list
|
|
||||||
CONTAINER ID IMAGE ... CREATED PORTS NAMES
|
|
||||||
1af574997e63 ... ... 3 minutes ago 0.0.0.0:8888->8080/tcp searxng
|
|
||||||
|
|
||||||
Print the container logs:
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
$ docker container logs -f searxng
|
|
||||||
|
|
||||||
Access the container shell (troubleshooting):
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
$ docker container exec -it --user root searxng /bin/sh -l
|
|
||||||
/usr/local/searxng #
|
|
||||||
|
|
||||||
Stop and remove the container:
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
$ docker container stop searxng
|
|
||||||
$ docker container rm searxng
|
|
||||||
|
|
||||||
.. _Container volumes:
|
|
||||||
|
|
||||||
Volumes
|
|
||||||
=======
|
|
||||||
|
|
||||||
Two volumes are exposed that should be mounted to preserve its contents:
|
|
||||||
|
|
||||||
- ``/etc/searxng``: Configuration files (settings.yml, etc.)
|
|
||||||
- ``/var/cache/searxng``: Persistent data (faviconcache.db, etc.)
|
|
||||||
|
|
||||||
.. _Container environment variables:
|
|
||||||
|
|
||||||
Environment variables
|
|
||||||
=====================
|
|
||||||
|
|
||||||
The following environment variables can be configured:
|
|
||||||
|
|
||||||
- ``$SEARXNG_*``: Controls the SearXNG configuration options, look out for
|
|
||||||
environment ``$SEARXNG_*`` in :ref:`settings server`, :ref:`settings
|
|
||||||
general` and the :origin:`container/.env.example` template.
|
|
||||||
- ``$GRANIAN_*``: Controls the :ref:`Granian server options <Granian configuration>`.
|
|
||||||
- ``$FORCE_OWNERSHIP``: Ensures mounted volumes/files are owned by the
|
|
||||||
``searxng:searxng`` user (default: ``true``)
|
|
||||||
|
|
||||||
.. _Container custom certificates:
|
|
||||||
|
|
||||||
Custom certificates
|
|
||||||
===================
|
|
||||||
|
|
||||||
You can mount ``/usr/local/share/ca-certificates/`` folder to add/remove
|
|
||||||
additional certificates as needed.
|
|
||||||
|
|
||||||
They will be available on container (re)start or when running
|
|
||||||
``update-ca-certificates`` in the container shell.
|
|
||||||
|
|
||||||
This requires the container to be running with ``root`` privileges.
|
|
||||||
|
|
||||||
.. _Container custom images:
|
|
||||||
|
|
||||||
Custom images
|
|
||||||
=============
|
|
||||||
|
|
||||||
To build your own SearXNG container image from source (please note, custom
|
|
||||||
container images are not officially supported):
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
$ git clone https://github.com/searxng/searxng.git
|
$ git clone https://github.com/searxng/searxng.git
|
||||||
$ cd ./searxng/
|
$ cd searxng
|
||||||
|
$ make docker.build
|
||||||
# Run the container build script
|
...
|
||||||
$ make container
|
Successfully built 49586c016434
|
||||||
|
Successfully tagged searxng/searxng:latest
|
||||||
|
Successfully tagged searxng/searxng:1.0.0-209-9c823800-dirty
|
||||||
|
|
||||||
$ docker images
|
$ docker images
|
||||||
REPOSITORY TAG IMAGE ID SIZE
|
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||||
localhost/searxng/searxng 2026.6.19-93f66bfb4 ... 265 MB
|
searxng/searxng 1.0.0-209-9c823800-dirty 49586c016434 13 minutes ago 308MB
|
||||||
localhost/searxng/searxng latest ... 265 MB
|
searxng/searxng latest 49586c016434 13 minutes ago 308MB
|
||||||
localhost/searxng/searxng builder ... 687 MB
|
alpine 3.13 6dbb9cc54074 3 weeks ago 5.61MB
|
||||||
docker.io/searxng/base searxng-builder ... 565 MB
|
|
||||||
docker.io/searxng/base searxng ... 143 MB
|
|
||||||
|
|
||||||
Migrate from ``searxng-docker``
|
|
||||||
===============================
|
|
||||||
|
|
||||||
We expect the following source directory structure:
|
Command line
|
||||||
|
============
|
||||||
|
|
||||||
|
.. sidebar:: docker run
|
||||||
|
|
||||||
|
Use flags ``-it`` for `interactive processes
|
||||||
|
<https://docs.docker.com/engine/reference/run/#foreground>`__.
|
||||||
|
|
||||||
|
In the :origin:`Dockerfile` the ENTRYPOINT_ is defined as
|
||||||
|
:origin:`dockerfiles/docker-entrypoint.sh`
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
.
|
docker run --rm -it searxng/searxng -h
|
||||||
└── searxng-docker
|
|
||||||
├── searxng
|
|
||||||
│ ├── favicons.toml
|
|
||||||
│ ├── limiter.toml
|
|
||||||
│ ├── settings.yml
|
|
||||||
│ └── ...
|
|
||||||
├── .env
|
|
||||||
├── Caddyfile
|
|
||||||
├── docker-compose.yml
|
|
||||||
└── ...
|
|
||||||
|
|
||||||
Create a brand new environment outside ``searxng-docker`` directory, following
|
.. program-output:: ../dockerfiles/docker-entrypoint.sh -h
|
||||||
`Container compose instancing setup`_.
|
|
||||||
|
|
||||||
Once up and running, stop the services and move the configuration files from
|
|
||||||
the old mount to the new one:
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
$ mv ./searxng-docker/searxng/* ./searxng/core-config/
|
|
||||||
|
|
||||||
If using Valkey features like bot protection (limiter), you will need to update
|
|
||||||
the URL hostname in :origin:`searx/settings.yml` file to ``valkey`` or
|
|
||||||
``searxng-valkey``.
|
|
||||||
|
|
||||||
If you have any environment variables in the old ``.env`` file, make
|
|
||||||
sure to add them to the new one.
|
|
||||||
|
|
||||||
Consider setting up a reverse proxy if exposing the instance to the public.
|
|
||||||
|
|
||||||
You should end with the following directory structure:
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
.
|
|
||||||
├── searxng
|
|
||||||
│ ├── core-config
|
|
||||||
│ │ ├── favicons.toml
|
|
||||||
│ │ ├── limiter.toml
|
|
||||||
│ │ ├── settings.yml
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── .env.example
|
|
||||||
│ ├── .env
|
|
||||||
│ └── docker-compose.yml
|
|
||||||
└── searxng-docker
|
|
||||||
└── ...
|
|
||||||
|
|
||||||
If everything is working on the new environment, you can remove the old
|
|
||||||
``searxng-docker`` directory and its contents.
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
.. _searxng granian:
|
|
||||||
|
|
||||||
=======
|
|
||||||
Granian
|
|
||||||
=======
|
|
||||||
|
|
||||||
.. _Options: https://github.com/emmett-framework/granian/blob/master/README.md#options
|
|
||||||
.. _Workers and threads: https://github.com/emmett-framework/granian/blob/master/README.md#workers-and-threads
|
|
||||||
.. _Backpressure: https://github.com/emmett-framework/granian/blob/master/README.md#backpressure
|
|
||||||
.. _Runtime mode: https://github.com/emmett-framework/granian/blob/master/README.md#runtime-mode
|
|
||||||
|
|
||||||
.. sidebar:: further reading
|
|
||||||
|
|
||||||
- `Options`_
|
|
||||||
- `Workers and threads`_
|
|
||||||
- `Backpressure`_
|
|
||||||
- `Runtime mode`_
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Granian will be the future replacement for :ref:`searxng uwsgi` in SearXNG.
|
|
||||||
At the moment, it's only officially supported in the :ref:`installation
|
|
||||||
container`.
|
|
||||||
|
|
||||||
.. _Granian installation:
|
|
||||||
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
We only recommend installing Granian with pip, as officially documented. Run
|
|
||||||
the following command in the Python environment of the SearXNG installation:
|
|
||||||
|
|
||||||
.. code:: sh
|
|
||||||
|
|
||||||
$ pip install granian
|
|
||||||
|
|
||||||
.. _Granian configuration:
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
It's not advised to modify the amount of workers, expect increased resource
|
|
||||||
usage and potential issues with :ref:`botdetection`.
|
|
||||||
|
|
||||||
Granian can be configured via option parameters and environment variables
|
|
||||||
(``$GRANIAN_*``).
|
|
||||||
|
|
||||||
We provide sane defaults that should fit most use cases, however if you feel
|
|
||||||
you should change something, Granian documents all available parameters in the
|
|
||||||
`Options`_ section.
|
|
||||||
@@ -41,6 +41,12 @@ section might give you some guidance.
|
|||||||
- `uWSGI support from nginx`_
|
- `uWSGI support from nginx`_
|
||||||
|
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:depth: 2
|
||||||
|
:local:
|
||||||
|
:backlinks: entry
|
||||||
|
|
||||||
|
|
||||||
The nginx HTTP server
|
The nginx HTTP server
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ When all services are installed and running fine, you can add SearXNG to your
|
|||||||
HTTP server. We do not have any preferences regarding the HTTP server, you can use
|
HTTP server. We do not have any preferences regarding the HTTP server, you can use
|
||||||
whatever you prefer.
|
whatever you prefer.
|
||||||
|
|
||||||
We implemented installation procedures for:
|
We use caddy in our :ref:`docker image <installation docker>` and we have
|
||||||
|
implemented installation procedures for:
|
||||||
|
|
||||||
- :ref:`installation nginx`
|
- :ref:`installation nginx`
|
||||||
- :ref:`installation apache`
|
- :ref:`installation apache`
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
Step by step installation
|
Step by step installation
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:depth: 2
|
||||||
|
:local:
|
||||||
|
:backlinks: entry
|
||||||
|
|
||||||
|
|
||||||
In this section we show the setup of a SearXNG instance that will be installed
|
In this section we show the setup of a SearXNG instance that will be installed
|
||||||
by the :ref:`installation scripts`.
|
by the :ref:`installation scripts`.
|
||||||
|
|
||||||
@@ -80,6 +86,7 @@ below. This setup:
|
|||||||
|
|
||||||
- enables :ref:`limiter <limiter>` to protect against bots
|
- enables :ref:`limiter <limiter>` to protect against bots
|
||||||
- enables :ref:`image proxy <image_proxy>` for better privacy
|
- enables :ref:`image proxy <image_proxy>` for better privacy
|
||||||
|
- enables :ref:`cache busting <static_use_hash>` to save bandwidth
|
||||||
|
|
||||||
Modify the ``/etc/searxng/settings.yml`` to your needs:
|
Modify the ``/etc/searxng/settings.yml`` to your needs:
|
||||||
|
|
||||||
@@ -89,7 +96,7 @@ Modify the ``/etc/searxng/settings.yml`` to your needs:
|
|||||||
|
|
||||||
.. literalinclude:: ../../utils/templates/etc/searxng/settings.yml
|
.. literalinclude:: ../../utils/templates/etc/searxng/settings.yml
|
||||||
:language: yaml
|
:language: yaml
|
||||||
:end-before: # preferences:
|
:end-before: # hostnames:
|
||||||
|
|
||||||
To see the entire file jump to :origin:`utils/templates/etc/searxng/settings.yml`
|
To see the entire file jump to :origin:`utils/templates/etc/searxng/settings.yml`
|
||||||
|
|
||||||
@@ -122,3 +129,4 @@ configuration file.
|
|||||||
If everything works fine, hit ``[CTRL-C]`` to stop the *webapp* and disable the
|
If everything works fine, hit ``[CTRL-C]`` to stop the *webapp* and disable the
|
||||||
debug option in ``settings.yml``. You can now exit SearXNG user bash session (enter exit
|
debug option in ``settings.yml``. You can now exit SearXNG user bash session (enter exit
|
||||||
command twice). At this point SearXNG is not demonized; uwsgi allows this.
|
command twice). At this point SearXNG is not demonized; uwsgi allows this.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ uWSGI
|
|||||||
- `systemd.unit`_
|
- `systemd.unit`_
|
||||||
- `uWSGI Emperor`_
|
- `uWSGI Emperor`_
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:depth: 2
|
||||||
|
:local:
|
||||||
|
:backlinks: entry
|
||||||
|
|
||||||
|
|
||||||
.. _systemd.unit: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
|
.. _systemd.unit: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
|
||||||
.. _One service per app in systemd:
|
.. _One service per app in systemd:
|
||||||
https://uwsgi-docs.readthedocs.io/en/latest/Systemd.html#one-service-per-app-in-systemd
|
https://uwsgi-docs.readthedocs.io/en/latest/Systemd.html#one-service-per-app-in-systemd
|
||||||
@@ -175,7 +181,10 @@ uWSGI setup
|
|||||||
|
|
||||||
Create the configuration ini-file according to your distribution and restart the
|
Create the configuration ini-file according to your distribution and restart the
|
||||||
uwsgi application. As shown below, the :ref:`installation scripts` installs by
|
uwsgi application. As shown below, the :ref:`installation scripts` installs by
|
||||||
default a uWSGI setup that listens on a socket.
|
default:
|
||||||
|
|
||||||
|
- a uWSGI setup that listens on a socket and
|
||||||
|
- enables :ref:`cache busting <static_use_hash>`.
|
||||||
|
|
||||||
.. tabs::
|
.. tabs::
|
||||||
|
|
||||||
@@ -226,19 +235,19 @@ major release is from Dec. 2013, since the there had been only bugfix releases
|
|||||||
**In Tyrant mode, there is no way to get additional groups, and the uWSGI
|
**In Tyrant mode, there is no way to get additional groups, and the uWSGI
|
||||||
process misses additional permissions that may be needed.**
|
process misses additional permissions that may be needed.**
|
||||||
|
|
||||||
For example on Fedora (RHEL): If you try to install a valkey DB with socket
|
For example on Fedora (RHEL): If you try to install a redis DB with socket
|
||||||
communication and you want to connect to it from the SearXNG uWSGI, you will see a
|
communication and you want to connect to it from the SearXNG uWSGI, you will see a
|
||||||
*Permission denied* in the log of your instance::
|
*Permission denied* in the log of your instance::
|
||||||
|
|
||||||
ERROR:searx.valkeydb: [searxng (993)] can't connect valkey DB ...
|
ERROR:searx.redisdb: [searxng (993)] can't connect redis DB ...
|
||||||
ERROR:searx.valkeydb: Error 13 connecting to unix socket: /usr/local/searxng-valkey/run/valkey.sock. Permission denied.
|
ERROR:searx.redisdb: Error 13 connecting to unix socket: /usr/local/searxng-redis/run/redis.sock. Permission denied.
|
||||||
ERROR:searx.plugins.limiter: init limiter DB failed!!!
|
ERROR:searx.plugins.limiter: init limiter DB failed!!!
|
||||||
|
|
||||||
Even if your *searxng* user of the uWSGI process is added to additional groups
|
Even if your *searxng* user of the uWSGI process is added to additional groups
|
||||||
to give access to the socket from the valkey DB::
|
to give access to the socket from the redis DB::
|
||||||
|
|
||||||
$ groups searxng
|
$ groups searxng
|
||||||
searxng : searxng searxng-valkey
|
searxng : searxng searxng-redis
|
||||||
|
|
||||||
To see the effective groups of the uwsgi process, you have to look at the status
|
To see the effective groups of the uwsgi process, you have to look at the status
|
||||||
of the process, by example::
|
of the process, by example::
|
||||||
@@ -248,7 +257,7 @@ of the process, by example::
|
|||||||
searxng 186 93 0 12:44 ? 00:00:01 /usr/sbin/uwsgi --ini searxng.ini
|
searxng 186 93 0 12:44 ? 00:00:01 /usr/sbin/uwsgi --ini searxng.ini
|
||||||
|
|
||||||
Here you can see that the additional "Groups" of PID 186 are unset (missing gid
|
Here you can see that the additional "Groups" of PID 186 are unset (missing gid
|
||||||
of ``searxng-valkey``)::
|
of ``searxng-redis``)::
|
||||||
|
|
||||||
$ cat /proc/186/task/186/status
|
$ cat /proc/186/task/186/status
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ Installation
|
|||||||
|
|
||||||
*You're spoilt for choice*, choose your preferred method of installation.
|
*You're spoilt for choice*, choose your preferred method of installation.
|
||||||
|
|
||||||
- :ref:`installation container`
|
- :ref:`installation docker`
|
||||||
- :ref:`installation scripts`
|
- :ref:`installation scripts`
|
||||||
- :ref:`installation basic`
|
- :ref:`installation basic`
|
||||||
|
|
||||||
The :ref:`installation basic` is an excellent illustration of *how a SearXNG
|
The :ref:`installation basic` is an excellent illustration of *how a SearXNG
|
||||||
instance is build up* (see :ref:`architecture uWSGI`). If you do not have any
|
instance is build up* (see :ref:`architecture uWSGI`). If you do not have any
|
||||||
special preferences, it's recommended to use the :ref:`installation container` or the
|
special preferences, it's recommended to use the :ref:`installation docker` or the
|
||||||
:ref:`installation scripts`.
|
:ref:`installation scripts`.
|
||||||
|
|
||||||
.. attention::
|
.. attention::
|
||||||
|
|
||||||
SearXNG is growing rapidly, you should regularly read our :ref:`migrate and
|
SearXNG is growing rapidly, you should regularly read our :ref:`migrate and
|
||||||
stay tuned` section. If you want to upgrade an existing instance, you
|
stay tuned` section. If you want to upgrade an existing instance or migrate
|
||||||
should read this section first!
|
from searx to SearXNG, you should read this section first!
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
.. _plugins admin:
|
.. _plugins generic:
|
||||||
|
|
||||||
===============
|
===============
|
||||||
List of plugins
|
Plugins builtin
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Further reading ..
|
.. sidebar:: Further reading ..
|
||||||
|
|
||||||
- :ref:`SearXNG settings <settings plugins>`
|
- :ref:`dev plugin`
|
||||||
- :ref:`dev plugin`
|
|
||||||
|
Configuration defaults (at built time):
|
||||||
|
|
||||||
|
:DO: Default on
|
||||||
|
|
||||||
.. _configured plugins:
|
.. _configured plugins:
|
||||||
|
|
||||||
@@ -19,13 +22,18 @@ Further reading ..
|
|||||||
:widths: 3 1 9
|
:widths: 3 1 9
|
||||||
|
|
||||||
* - Name
|
* - Name
|
||||||
- Active
|
- DO
|
||||||
- Description
|
- Description
|
||||||
|
|
||||||
{% for plg in plugins %}
|
JS & CSS dependencies
|
||||||
|
|
||||||
* - {{plg.info.name}}
|
{% for plgin in plugins %}
|
||||||
- {{(plg.active and "yes") or "no"}}
|
|
||||||
- {{plg.info.description}}
|
* - {{plgin.name}}
|
||||||
|
- {{(plgin.default_on and "y") or ""}}
|
||||||
|
- {{plgin.description}}
|
||||||
|
|
||||||
|
{% for dep in (plgin.js_dependencies + plgin.css_dependencies) %}
|
||||||
|
| ``{{dep}}`` {% endfor %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ Favicons
|
|||||||
|
|
||||||
Don't activate the favicons before reading the documentation.
|
Don't activate the favicons before reading the documentation.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:depth: 2
|
||||||
|
:local:
|
||||||
|
:backlinks: entry
|
||||||
|
|
||||||
Activating the favicons in SearXNG is very easy, but this **generates a
|
Activating the favicons in SearXNG is very easy, but this **generates a
|
||||||
significantly higher load** in the client/server communication and increases
|
significantly higher load** in the client/server communication and increases
|
||||||
resources needed on the server.
|
resources needed on the server.
|
||||||
@@ -88,13 +93,15 @@ TOML_ configuration is created in the file ``/etc/searxng/favicons.toml``.
|
|||||||
:py:obj:`cache.db_url <.FaviconCacheConfig.db_url>`:
|
:py:obj:`cache.db_url <.FaviconCacheConfig.db_url>`:
|
||||||
The path to the (SQLite_) database file. The default path is in the `/tmp`_
|
The path to the (SQLite_) database file. The default path is in the `/tmp`_
|
||||||
folder, which is deleted on every reboot and is therefore unsuitable for a
|
folder, which is deleted on every reboot and is therefore unsuitable for a
|
||||||
production environment. The FHS_ provides the folder `/var/cache`_ for the
|
production environment. The FHS_ provides the folder for the
|
||||||
cache of applications, so a suitable storage location of SearXNG's caches is
|
application cache
|
||||||
folder ``/var/cache/searxng``.
|
|
||||||
|
|
||||||
In a standard installation (compare :ref:`create searxng user`), the folder
|
The FHS_ provides the folder `/var/cache`_ for the cache of applications, so a
|
||||||
must be created and the user under which the SearXNG process is running must
|
suitable storage location of SearXNG's caches is folder ``/var/cache/searxng``.
|
||||||
be given write permission to this folder.
|
In container systems, a volume should be mounted for this folder and in a
|
||||||
|
standard installation (compare :ref:`create searxng user`), the folder must be
|
||||||
|
created and the user under which the SearXNG process is running must be given
|
||||||
|
write permission to this folder.
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
@@ -102,10 +109,6 @@ TOML_ configuration is created in the file ``/etc/searxng/favicons.toml``.
|
|||||||
$ sudo chown root:searxng /var/cache/searxng/
|
$ sudo chown root:searxng /var/cache/searxng/
|
||||||
$ sudo chmod g+w /var/cache/searxng/
|
$ sudo chmod g+w /var/cache/searxng/
|
||||||
|
|
||||||
In container systems, a volume should be mounted for this folder. Check
|
|
||||||
whether the process in the container has read/write access to the mounted
|
|
||||||
folder.
|
|
||||||
|
|
||||||
:py:obj:`cache.LIMIT_TOTAL_BYTES <.FaviconCacheConfig.LIMIT_TOTAL_BYTES>`:
|
:py:obj:`cache.LIMIT_TOTAL_BYTES <.FaviconCacheConfig.LIMIT_TOTAL_BYTES>`:
|
||||||
Maximum of bytes stored in the cache of all blobs. The limit is only reached
|
Maximum of bytes stored in the cache of all blobs. The limit is only reached
|
||||||
at each maintenance interval after which the oldest BLOBs are deleted; the
|
at each maintenance interval after which the oldest BLOBs are deleted; the
|
||||||
@@ -245,3 +248,4 @@ into the *proxy*:
|
|||||||
.. _data URL:
|
.. _data URL:
|
||||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs
|
https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs
|
||||||
.. _FQN: https://en.wikipedia.org/wiki/Fully_qualified_name
|
.. _FQN: https://en.wikipedia.org/wiki/Fully_qualified_name
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ Limiter
|
|||||||
|
|
||||||
.. sidebar:: info
|
.. sidebar:: info
|
||||||
|
|
||||||
The limiter requires a :ref:`Valkey <settings valkey>` database.
|
The limiter requires a :ref:`Redis <settings redis>` database.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:depth: 2
|
||||||
|
:local:
|
||||||
|
:backlinks: entry
|
||||||
|
|
||||||
.. automodule:: searx.limiter
|
.. automodule:: searx.limiter
|
||||||
:members:
|
:members:
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ Settings
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
settings
|
settings
|
||||||
settings_engines
|
settings_engine
|
||||||
settings_brand
|
settings_brand
|
||||||
settings_general
|
settings_general
|
||||||
settings_search
|
settings_search
|
||||||
settings_server
|
settings_server
|
||||||
settings_ui
|
settings_ui
|
||||||
settings_preferences
|
|
||||||
settings_redis
|
settings_redis
|
||||||
settings_valkey
|
|
||||||
settings_outgoing
|
settings_outgoing
|
||||||
settings_categories_as_tabs
|
settings_categories_as_tabs
|
||||||
settings_plugins
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ file.
|
|||||||
- :ref:`use_default_settings.yml`
|
- :ref:`use_default_settings.yml`
|
||||||
- :ref:`search API`
|
- :ref:`search API`
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:depth: 2
|
||||||
|
:local:
|
||||||
|
:backlinks: entry
|
||||||
|
|
||||||
.. _settings location:
|
.. _settings location:
|
||||||
|
|
||||||
settings.yml location
|
settings.yml location
|
||||||
@@ -54,7 +59,7 @@ and can relied on the default configuration :origin:`searx/settings.yml` using:
|
|||||||
use_default_settings: true
|
use_default_settings: true
|
||||||
server:
|
server:
|
||||||
secret_key: "ultrasecretkey" # change this!
|
secret_key: "ultrasecretkey" # change this!
|
||||||
bind_address: "[::]"
|
bind_address: "0.0.0.0"
|
||||||
|
|
||||||
``engines:``
|
``engines:``
|
||||||
With ``use_default_settings: true``, each settings can be override in a
|
With ``use_default_settings: true``, each settings can be override in a
|
||||||
|
|||||||
@@ -4,5 +4,22 @@
|
|||||||
``brand:``
|
``brand:``
|
||||||
==========
|
==========
|
||||||
|
|
||||||
.. autoclass:: searx.brand.SettingsBrand
|
.. code:: yaml
|
||||||
:members:
|
|
||||||
|
brand:
|
||||||
|
issue_url: https://github.com/searxng/searxng/issues
|
||||||
|
docs_url: https://docs.searxng.org
|
||||||
|
public_instances: https://searx.space
|
||||||
|
wiki_url: https://github.com/searxng/searxng/wiki
|
||||||
|
|
||||||
|
``issue_url`` :
|
||||||
|
If you host your own issue tracker change this URL.
|
||||||
|
|
||||||
|
``docs_url`` :
|
||||||
|
If you host your own documentation change this URL.
|
||||||
|
|
||||||
|
``public_instances`` :
|
||||||
|
If you host your own https://searx.space change this URL.
|
||||||
|
|
||||||
|
``wiki_url`` :
|
||||||
|
Link to your wiki (or ``false``)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user